Add Remote session foundations and feasibility checks
Add opt-in Tentacle protocol 1.1, explicit remote grants, lifecycle IPC and expiring core leases in both SDKs. Revoke access before update preparation, including failed updates. Add transient secret entries, shared Braille cell export and isolated Xorg recovery experiments with focused tests and documented limitations. Production consent, desktop services and live interoperability remain pending.
This commit is contained in:
1 parent
a47253d697
commit
573d15d7b4
45 files changed
+3634
-80
No files matched your search
@@ -0,0 +1,181 @@
|
||||
# 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. There is no production consent issuer, connection dialog,
|
||||
output observer, keyboard service, clipboard service, or Braille session service.
|
||||
Only private fixtures call the core `Session.AuthorizeRemote` hook. 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.
|
||||
|
||||
## 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.
|
||||
|
||||
These are lifecycle queues only. Separate key/speech queues, latest-frame Braille
|
||||
coalescing, typed effect validation, and their delivery-time owner checks remain
|
||||
unimplemented. Do not send those payloads through lifecycle events.
|
||||
|
||||
## 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, lease, epoch and reason (`closed`, `expired`,
|
||||
`local`, `replaced`). Failure to enqueue revocation retires the session; it
|
||||
cannot block immediate cancellation. An unconsumed approval expires silently.
|
||||
|
||||
Child calls:
|
||||
|
||||
| Method | Parameters | Result |
|
||||
| --- | --- | --- |
|
||||
| `remote.open` | `action`, `epoch`, `role`, `grants` | `lease`, `epoch` |
|
||||
| `remote.heartbeat` | `lease`, `epoch`, `sequence` | `{}` |
|
||||
| `remote.close` | `lease`, `epoch`, `sequence` | `{}` |
|
||||
|
||||
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.
|
||||
|
||||
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
|
||||
lock, handoff, origin, focus and ordinary output policy at delivery; a context
|
||||
alone does not implement those 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/presentation/key/Braille/clipboard methods deliberately return
|
||||
unsupported-operation errors. 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.
|
||||
|
||||
There is no Remote connection UI to test live yet. 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.
|
||||
Reference in new issue
Block a user