276 lines
16 KiB
Markdown
276 lines
16 KiB
Markdown
# Experimental Tentacle session protocol 1.1
|
|
|
|
Implemented on 2026-11-03 against reader `a47253d697c1aa44dea80646cad99a4fe163c7f1`
|
|
with local Remote prerequisite changes, and Tentacles
|
|
`bc305993b8ad5c2374c8fc2c8b886c9cd6697b52`. Both branches were synchronized before
|
|
this increment. These are base revisions, not commits containing the new work.
|
|
|
|
This increment supplies negotiation, bounded bidirectional IPC, lifecycle events,
|
|
install grants, and an inert remote lease service. It does **not** supply desktop
|
|
remote control. A later [reader authorization increment](tentacle-remote-authorization.md)
|
|
adds the core consent procedure, sensitive XI2 forms and lock/handoff policy.
|
|
The external source-built Remote command now requests that consent through a
|
|
typed interaction; physical-keyboard approval remains unverified. There is no
|
|
production keyboard, clipboard or Braille session service. Subsequent speech
|
|
integration supplies limited observation and isolated incoming software speech;
|
|
the [keyboard IPC prerequisite](tentacle-remote-keyboard.md) adds a typed mailbox
|
|
without a native input owner. The unrestricted `Session.AuthorizeRemote` hook is
|
|
limited to development managers; reader managers require a core approval ticket.
|
|
Packaging Remote itself remains premature.
|
|
|
|
## Negotiation and installation
|
|
|
|
Existing protocol 1.0 packages, launch descriptions and SDK `Serve`/`serve` calls
|
|
keep the exact ordered API prefix and mirrored initialize capabilities. They do
|
|
not receive events and may not send requests to the host.
|
|
|
|
Protocol 1.1 is explicit. A native package declares `major=1`, `minor=1` and APIs
|
|
containing `commands` and `session-events`. Optional APIs are `presentation`,
|
|
`interaction`, `reader-settings` and `remote-access`. API order does not matter;
|
|
duplicates, unknown entries and missing dependencies are rejected. Interaction
|
|
requires presentation; reader settings require interaction. Remote access requires
|
|
at least one remote grant, and every remote grant requires remote access.
|
|
|
|
In 1.1 `capabilities` has the same meaning in the manifest, catalog and initialize
|
|
message: install permissions. It contains the requested presentation, interaction
|
|
and reader-settings grants, plus independent remote grants:
|
|
|
|
| Grant | Direction and intended use |
|
|
| --- | --- |
|
|
| `output-observe` | Controlled reader shares local policy-approved output. |
|
|
| `remote-present` | Controller presents output received from the controlled reader. |
|
|
| `keyboard-capture` | Controller consumes and forwards its local keyboard. |
|
|
| `keyboard-inject` | Controlled reader accepts remote key transitions. |
|
|
| `braille-stream` | Exchange display cells and supported Braille gestures. |
|
|
| `clipboard-exchange` | Explicit send and separately consented receive. |
|
|
|
|
`commands`, `session-events` and `remote-access` are API names, not capability
|
|
strings in 1.1. The installer checks manifest/catalog agreement and persists
|
|
`session-events` and `remote-grants` in the registry. Local launch `Spec` uses
|
|
`SessionEvents` and `RemoteGrants`. The existing presentation/interaction/reader
|
|
settings fields retain their meanings. Snapshot and launch copies own their grant
|
|
slices. Install and update confirmations disclose directions and the requirement
|
|
for fresh local session approval. Older readers reject the package protocol;
|
|
there is no fallback to 1.0. Grant changes still use the normal reviewed update
|
|
transaction and existing process disable/restart behavior. Remote access is revoked
|
|
before update download/preparation, so failed preparation cannot preserve a lease.
|
|
Disable/remove/retry/local-update requests also revoke access before persistence
|
|
or queueing; rollback never recreates consent.
|
|
|
|
An install grant never creates session consent. Reader desktop services are not
|
|
implemented by this increment, even if all grants are declared.
|
|
|
|
## Public SDK entry points
|
|
|
|
Go provides `NewSessionClient`, `ServeSession`, `SessionClient.Events`, and
|
|
`SessionClient.Call(ctx, method, params, result)`. Public aliases expose
|
|
`SessionEvent`, `RemoteOpen`, `RemoteLease` and `RemoteOperation`; callers need no
|
|
reader-internal imports. `SessionError.Code` is available for handling failures;
|
|
its error string omits the host's message. Clients are single-use.
|
|
|
|
Python exports `SessionClient` and `SessionError`. Pass a client as
|
|
`serve(commands, session_client=client)`, call
|
|
`client.call(method, params, timeout=2)`, and consume lifecycle events through
|
|
`client.next_event()`. The Python 1.1 transport requires owned binary streams with
|
|
file descriptors. The implementation uses nonblocking pipes and a wake pipe so
|
|
shutdown can interrupt a stalled read or write. Go adopts pollable duplicates of
|
|
owned file streams for the same reason. Both still support existing command
|
|
handlers alongside session calls.
|
|
|
|
Use one event consumer, dispatch short bounded work, and keep network work off
|
|
the receive loop. Go event consumers must handle channel closure; Python returns
|
|
`None` on shutdown. Workers must stop when the serving context/session is
|
|
cancelled. Events already taken from a queue do not prove a lease remains valid.
|
|
|
|
## Reader-owned consent interactions
|
|
|
|
An interactive protocol 1.1 command may return this request, using Go's public
|
|
`RemoteRequest` alias or an ordinary Python interaction dictionary:
|
|
|
|
```json
|
|
{"remote":{"role":"controller","grants":["remote-present"]},"next":"remote-approved"}
|
|
```
|
|
|
|
It asks for a core-owned local consent dialog; it does not authorize anything.
|
|
There is no target-session parameter. The host binds the exact requesting
|
|
process session, checks its installed grants and reserves the existing
|
|
interaction lifetime. The request requires a registered interactive continuation
|
|
and cannot be combined with a form, reader request, source, clipboard effect,
|
|
polling or Done. Unknown roles, duplicate/unknown grants and role-direction
|
|
mismatches are rejected. A child cannot supply the core approval ticket.
|
|
|
|
`optional_grants` may list additional role-valid permissions. The reader offers
|
|
only those also present in installation grants; it neither installs nor approves
|
|
them. Required `grants` still fail if unavailable. Duplicate grants across both
|
|
lists and role-direction mismatches are rejected.
|
|
|
|
The reader's local-only consent form starts on Cancel with all requested grants
|
|
unchecked. A nonempty explicit approval produces the ordinary SDK
|
|
`remote.action` event and invokes Next with a `Submission.remote` containing the
|
|
same event. Denial invokes Next with `cancelled: true` and a generic error, then
|
|
ends the interaction. The continuation must match its reply against the actual
|
|
SDK event stream before consuming approval; command arguments alone are not
|
|
authority. The 30-second pending-action expiry still applies.
|
|
|
|
The host reuses its already-held interaction slot, avoiding nested admission.
|
|
Lock/handoff cancels the original policy lifetime. Finishing the interaction
|
|
revokes its original approved epoch, including an unused action; stale cleanup
|
|
cannot revoke replacement consent. On an unfinished exit the host also invokes
|
|
the outstanding child continuation with cancellation and a separate bounded
|
|
cleanup context, so pre-lease inspection does not depend on a revocation event.
|
|
No form values accompany that cleanup. Session failure/disable still uses
|
|
normal process supervision. Existing non-remote interactions keep their path.
|
|
|
|
A return-to-desktop action or successful connection poll may instead finish with:
|
|
|
|
```json
|
|
{"remote_background":{"lease":"0123456789abcdef0123456789abcdef","epoch":1},"done":true}
|
|
```
|
|
|
|
This terminal result requires an already-open, unexpired lease belonging to the
|
|
requesting process session. If this interaction approved an epoch, it must match
|
|
the requested lease. Forms, continuations, polling, clipboard, reader/consent
|
|
requests and uninterruptible speech cannot accompany it; ordinary speech/Braille
|
|
presentation is allowed. A polling response may return this terminal result; core closes the progress form
|
|
and validates its lease before releasing capture. Go exposes
|
|
`Interaction.RemoteBackground`; Python uses the dictionary above.
|
|
|
|
On success, core releases the interaction/result slots without invoking child
|
|
cancellation or revoking that epoch. The original policy/session lease remains
|
|
its owner: heartbeats, expiry, lock, handoff, disconnect, replacement, disable,
|
|
update and process loss retain their existing behavior. This result neither
|
|
renews authority nor starts networking. The Tentacle must offer an explicit
|
|
status/disconnect command; reopening status needs no new consent, while a new
|
|
connection still does. Ordinary cancellation and form closure retain their
|
|
existing cleanup behavior. `RemoteLeaseContext` checks identity/lifetime only;
|
|
desktop effects still require `RemoteContext` with their specific grant.
|
|
|
|
This addition requires the matching updated reader; protocol 1.1 by itself does
|
|
not imply that an older reader accepts the new interaction field. Remote still
|
|
has no release manifest advertising a minimum supported reader version.
|
|
|
|
## Framing, ownership and limits
|
|
|
|
Frames remain UTF-8 newline-delimited JSON-RPC 2.0, at most 64 KiB excluding LF.
|
|
Duplicate fields and excessive JSON depth remain invalid. Host request IDs retain
|
|
canonical increasing decimal strings. Child request IDs are canonical increasing
|
|
`c:<decimal>` strings. Both directions correlate replies independently. Duplicate,
|
|
unknown or wrong-namespace replies and unnegotiated notifications retire the
|
|
process session. Child notifications are not supported.
|
|
|
|
The host answers lifecycle requests directly with bounded work; it never waits
|
|
on a command handler or invokes an external service under the session mutex.
|
|
Each endpoint has one writer, a 32-frame queue and a 256 KiB aggregate byte limit
|
|
including the in-flight write. The SDK permits eight outstanding child calls;
|
|
the host retains its existing command-call limits. Each SDK has a 32-event queue.
|
|
Event/control overflow closes the process session, immediately cancelling leases.
|
|
It does not discard individual control messages to keep a session running.
|
|
|
|
Session request parameters are limited to 8 KiB. Calls have a maximum two-second
|
|
deadline; timeout/cancellation retires the SDK session rather than accumulating
|
|
abandoned work. Go also respects a shorter caller context; Python accepts a
|
|
shorter explicit timeout. Shutdown/EOF wakes pending calls and event consumers.
|
|
The process supervisor still enforces cleanup for a child ignoring cancellation.
|
|
|
|
Typed speech IPC now extends this transport; see
|
|
[the speech contract](tentacle-remote-speech.md). It uses the same ordered event
|
|
queue for exported speech and a separate 64-item incoming mailbox. Limited
|
|
production speech observation and isolated incoming software speech are connected.
|
|
The [keyboard IPC prerequisite](tentacle-remote-keyboard.md) adds `remote.key`
|
|
and a separate bounded mailbox, unavailable without a reader input owner.
|
|
Production keyboard/Braille/clipboard effects remain unsupported.
|
|
|
|
## Implemented lifecycle operations
|
|
|
|
`session.event` is a host notification whose parameters are a `SessionEvent`.
|
|
Its sequence starts at one and increases exactly once per event. Supported kinds:
|
|
|
|
- `remote.action`: sequence, kind, action token, host-issued epoch, role and approved
|
|
grant subset. Created only by the internal core authorizer. Approval expires
|
|
after 30 seconds if not consumed. No network secret is included.
|
|
- `remote.revoked`: sequence, kind, epoch, exactly one of action or lease, and reason (`closed`, `expired`,
|
|
`local`, `replaced`). Failure to enqueue revocation retires the session; it
|
|
cannot block immediate cancellation. Pending approval expiry/revocation carries
|
|
its action token; an opened lease carries its lease token. Both retire external
|
|
setup workers through the same epoch-bound notification.
|
|
|
|
Child calls:
|
|
|
|
| Method | Parameters | Result |
|
|
| --- | --- | --- |
|
|
| `remote.open` | `action`, `epoch`, `role`, `grants` | `lease`, `epoch` |
|
|
| `remote.heartbeat` | `lease`, `epoch`, `sequence` | `{}` |
|
|
| `remote.close` | `lease`, `epoch`, `sequence` | `{}` |
|
|
| `remote.present` | `lease`, `epoch`, `sequence`, typed `speech` | `{}` after mailbox admission; unavailable without reader owner |
|
|
| `remote.key` | `lease`, `epoch`, `sequence`, typed `key` | `{}` after mailbox admission; controlled `keyboard-inject` only; unavailable without reader input owner |
|
|
|
|
The action token must match the unconsumed core approval. Role must match exactly,
|
|
and grants must be a nonempty subset of the approved install grants. Controller
|
|
roles cannot obtain output-observe/keyboard-inject; controlled roles cannot
|
|
obtain remote-present/keyboard-capture. A role cannot be changed with heartbeat.
|
|
|
|
The manager admits one remote owner, including pending approval. Opening consumes
|
|
the token and creates a random 128-bit lease ID. Operations use strictly increasing
|
|
positive sequence numbers. Heartbeat should be sent each second and expires after
|
|
three seconds without refresh. Expiry revokes the lease independently of command
|
|
workers and pipe writes. A late heartbeat cannot revive it. A new local action
|
|
revokes old access and advances the core epoch. Closing an already retired epoch
|
|
is idempotent and cannot revoke a newer one. An authenticated close also succeeds
|
|
when expiry or policy cancellation has invalidated the current lease but its
|
|
retirement callback has not yet acquired the session lock. It still checks the
|
|
lease token, epoch and operation sequence; it never renews authority.
|
|
|
|
Core `RemoteContext(lease, grant)` returns a cancellation context only for a live
|
|
matching lease with that grant. Close, expiry, process failure, disable and parent
|
|
cancellation invalidate buffered work. Future effect owners must also enforce
|
|
origin, focus and ordinary output policy at delivery; the reader's policy now
|
|
cancels these contexts on its existing lock/handoff transitions. A context alone
|
|
does not implement all effect-owner policies. No production effects currently use it.
|
|
|
|
Errors are generic and contain no parameters: `-32001` permission denied,
|
|
`-32602` invalid parameters, `-32601` unsupported operation, `-32603` unavailable.
|
|
State/focus/key/Braille/clipboard methods deliberately return
|
|
unsupported-operation errors. Speech mailbox overflow returns `-32002` and
|
|
revokes its lease. Malformed envelopes, replayed IDs or frame/queue
|
|
violations terminate the process session instead of attempting recovery.
|
|
|
|
## Verification
|
|
|
|
Real child-process conformance fixtures cover both SDKs: bidirectional requests
|
|
while another command is blocked, action/event delivery, denied consent, wrong
|
|
role/grants, replay, stale epochs, close idempotence, heartbeat expiry, exclusive
|
|
ownership and disable. Host/SDK malformed-message tests cover unnegotiated events,
|
|
bad IDs, unknown methods, bad sequences and event overflow. Stalled-reader and
|
|
stalled-writer tests verify that call timeout exits the child without host stdin
|
|
closure. Queue tests cover the count and aggregate byte ceilings and synchronous
|
|
revocation on overflow. Package tests cover valid persistence and rejected API,
|
|
grant and version combinations.
|
|
|
|
Focused checks:
|
|
|
|
```sh
|
|
GOCACHE=/tmp/cthulhu-go-cache GOPROXY=off go test -race ./internal/tentaclewire ./internal/tentacle ./internal/preferences ./sdk/... -count=1 -timeout=240s
|
|
GOCACHE=/tmp/cthulhu-go-cache GOPROXY=off go test -race ./internal/app -run 'Tentacle|Upgrade|Update|RemoteLease' -count=1 -timeout=180s
|
|
GOCACHE=/tmp/cthulhu-go-cache GOPROXY=off go vet ./internal/tentaclewire ./internal/tentacle ./internal/preferences ./sdk/... ./cmd/tentacle-host
|
|
```
|
|
|
|
Private GTK checks use `tests/native_preferences.py` with respectively
|
|
`CTHULHU_PREFS_TENTACLES_ONLY=1`, `CTHULHU_PREFS_TENTACLE_UPDATES_ONLY=1`,
|
|
`CTHULHU_PREFS_TENTACLE_SETTINGS_ONLY=1`, and `CTHULHU_PREFS_EXTENSIONS_ONLY=1`.
|
|
The individual `native_tentacle_*.py` files are imported helpers, not standalone
|
|
harnesses. Tests use a separately built binary selected by `CTHULHU_BINARY`,
|
|
private Xvfb and private D-Bus; they do not replace the running reader.
|
|
|
|
The external source command now exposes connection UI for disposable-session
|
|
acceptance; see [its procedure](../../cthulhu-tentacles/remote/command.md).
|
|
Physical-source approval, physical keyboard recovery,
|
|
audible speech, physical Braille, real NVDA/Orca sessions and installed Remote
|
|
package behavior remain unverified. Human diff review and relevant live acceptance
|
|
are still required before submission.
|
|
|
|
Progress polling may also return a replacement form or a reader-owned consent
|
|
request. The host closes the previous form, revalidates continuations and uses
|
|
the ordinary interaction path. It does not authorize polling replies directly.
|
|
Polling still cannot read a source or apply reader settings. Empty unchanged
|
|
replies produce no repeated notice. See [Remote UI checks](testing/remote-ui.md).
|