feat(grid-agent): add portable control plane (#126)
Some checks failed
CI / rust-skia (Rust only) (push) Successful in 2m44s
CI / required (push) Failing after 2m43s

This commit is contained in:
2026-08-18 04:29:12 +00:00
parent 058ed10005
commit 962d17257d
16 changed files with 4023 additions and 31 deletions

View File

@@ -0,0 +1,91 @@
# Grid-agent control plane v1
The grid agent exposes one transport-neutral request/response and event API.
Integrated clients receive an `InProcessControlClient`; split clients use the
same `ControlRequestEnvelope`, `ControlResponseEnvelope`, and `ControlEvent`
types over TCP. No terminal, Unix socket, named-pipe, permission-bit, or
platform-specific type appears in the core API.
## Transport and negotiation
TCP frames are a four-byte unsigned big-endian length followed by one UTF-8
JSON value. Empty, partial, malformed, idle, and oversized frames close the
connection within configured deadlines. The first frame must be:
```json
{"frame":"hello","body":{"version":1,"token":"separate-operator-capability"}}
```
The server answers with a `hello` containing version `1`, the authenticated
`operator` or `observer` role, or a typed error. Every later client frame is a
`request`; server frames are `response` or `event`. Request IDs contain at most
96 ASCII identifier bytes and cannot be replayed on a connection. A caller can
cancel an active request by its ID. Responses and events can arrive in either
order, so clients correlate responses by request ID.
Plain TCP binds only to IPv4 or IPv6 loopback. Remote control is off by default
and a non-loopback address is rejected unless both DER certificate and private
key paths are explicitly configured. Remote mode uses Rustls before the hello
exchange and emits an operator warning at startup. The TLS client API requires
the embedding client to supply its certificate roots and server-name policy.
Every split connection must still authenticate with a dedicated control token;
observer and operator tokens must differ and configuration rejects reuse of an
LLM API key or grid password. Tokens are absent from protocol errors, audit,
serialization traits, and diagnostics.
Remote service configuration uses the JSON fields
`control.remote_tls_certificate_der` and
`control.remote_tls_private_key_der` together with a non-loopback
`control.listen` value. Both files are bounded, non-symlink DER files. Omitting
either field, or selecting a non-loopback address without both, fails
configuration before a listener is created.
## Requests
The JSON request envelope is stable and versioned. For example:
```json
{"version":1,"request_id":"health-1","request":{"method":"health"}}
```
Observers can call `health`, `runtime`, `list_sessions`,
`list_scheduled_jobs`, `list_pending_approvals`, `list_audit_events`, and
`subscribe_events`. Operators can additionally call `cancel_request`,
`pause_autonomy`, `resume_autonomy`, `cancel_action`, `decide_approval`,
`force_reconnect`, `expire_conversation`, `set_roaming_job`,
`inject_operator_message`, and `graceful_shutdown`. Cancellation is a mutation
and is operator-only. List requests use an opaque numeric cursor and a page
size of 1 through 100.
Runtime projections contain lifecycle/readiness, session generation, safe
region and pose fields when known, behavior mode, control-queue utilization,
and aggregate budget use. Conversation responses contain metadata only. Audit
and approval responses exclude arguments, prompt contents, credentials,
authorization headers, capability URLs, model reasoning, and filesystem data.
`cancel_action` binds to the exact bounded action ID carried by behavior audit
observations; it can cancel a queued or executing embodied action without
preempting unrelated work. The built-in roaming job ID is `default-roaming`.
Errors are typed as `authentication_failed`, `version_mismatch`,
`permission_denied`, `invalid_request`, `replay`, `not_found`, `conflict`,
`cancelled`, `timed_out`, `busy`, `backpressure`, `frame_too_large`,
`idle_timeout`, `transport_closed`, or `internal`, with a bounded safe message
and a retryable flag.
## Events, bounds, and reconnect
Events carry monotonically increasing sequence numbers. A subscription can
resume after its last observed sequence. If retained history no longer covers
that point, its first item is an explicit `gap` with `first_available` and
`last_missed`, followed by retained events. Mutation events record the
authenticated role/connection principal, operation name, and completed or
rejected outcome, never request contents.
Connections, unauthenticated handshakes, in-flight request tasks,
subscriptions, command/event queues, replay history, event history, event
bytes, event rate, frames, pages, idle time, request time, and writes all have
validated hard bounds. A full event or writer queue disconnects the slow
consumer instead of blocking the headless service. With zero clients, event
publication retains only the configured history and creates no background
work. Server shutdown cancels and joins all listener, connection, request, and
writer tasks within its deadline.