Skip to content
Spice Framework on GitHub

RFC 0004: Local Engine Protocol

agentMaturity: experimentalSource: spice-agent@4726fb8Exact reviewed source
  • Status: provisional common/v1 and engine/v1 schema baseline; host semantics and final freeze remain in progress
  • Initial packages: common/v1, engine/v1
  • Transport: authenticated user-local gRPC

Scope

The protocol separates a generated headless daemon from local clients. It is not an alternate kernel API and does not expose internal Go structs. Protobuf exists because there is a real process/language boundary; embedded applications call Go interfaces directly.

Initialization

Transport middleware authenticates the endpoint token from gRPC metadata before the first application payload is decoded. Initialize then presents protocol major/minor, client build identity, supported capabilities, and maximum message limits. A new owner receives a stable client ID at epoch one. Reconnect supplies that client ID and its expected epoch; the daemon performs a compare-and-swap and returns the same ID at exactly the next epoch. A stale claim fails without changing ownership.

The daemon returns its build identity, supported capabilities, configured limits, health, and one immutable generated DefinitionSet. Definitions own model and turn-limit policy on the server. A run request selects only an exact definition ID/revision; it cannot supply provider configuration or any static or dynamic plan fingerprint. Incompatible major versions or required capabilities fail before a run is created, with both observed and required values.

The implementation advertises protocol range 1.0 through 1.1. Snapshot transfer is atomic with authenticated authority: minor-0 negotiation removes both snapshots and snapshot-authority-v1. A client requiring either receives a typed missing-capability result. A minor-1 client must require snapshot-authority-v1 before relying on export or import.

The transport-independent host represents this catalog as exact immutable agent.Definition values rather than reconstructing model or maximum-turn policy from wire data. Its stable client store derives every ownership epoch context from the daemon root and performs reconnect as an exact one-winner CAS. Mutating RPC adapters will use the bounded idempotency ledger keyed by stable client identity plus operation identity; an epoch change fences execution but does not erase committed operation outcomes.

Operations

  • Health reports readiness, version, replay limits, active-run count, and bounded degraded reasons without configuration secrets.
  • StartRun accepts an exact server-advertised definition reference, initial message, and client operation ID. The server selects and leases the current dynamic generation, then returns the stable run ID, initial sequence, and immutable plan_id; clients cannot choose a model or plugin generation.
  • StreamEvents(after_sequence) atomically captures retained bounds and a count/byte-bounded page. Its control reports optional page_last_sequence, has_more, and tailing. Live tail registration occurs under the same lock only at the captured head, preventing a replay/tail gap. Current peers always send the page cursor; absence is accepted only as the provisional non-paging, non-tailing compatibility shape. For current controls, has_more is exactly equivalent to page_last_sequence < latest_sequence.
  • StreamInteractions always sends a complete atomic snapshot of every pending prompt as its first frame. Revision-contiguous opened/closed deltas and a final control follow. Reconnect never relies on retained delta history to discover an unresolved prompt. Prompt/schema/response content remains outside authoritative run events.
  • CancelRun is idempotent and reports whether the run was already terminal.
  • RespondInteraction uses the client operation ID plus run and interaction IDs to reject stale correlation and deduplicate the mutation. It has no redundant response identity or synthetic event-sequence acknowledgement.
  • SuspendRun pauses at a safe completed-turn boundary and ResumeRun continues the same locally owned run without changing its identity.
  • ExportSnapshot returns a versioned provider-neutral safe snapshot at a supported boundary. ImportSnapshot is a separate explicit mutation with idempotency and uncertain-outcome rules, accepts only suspended v1alpha2 snapshots, and preserves the run ID embedded in the snapshot. Clients cannot rename an imported run or assert a replacement plan. Export requires a trusted signer and import requires keyed HMAC verification before state mutation; an unkeyed structural check is never import authority.

The daemon adapter must use the kernel’s transactional preparation boundary for both StartRun and ImportSnapshot. It first prepares the execution, uses the prepared immutable run ID to acquire daemon ownership, and commits only after that ownership is durable. The commit receives a daemon-owned run-root context; the setup or RPC context never becomes the execution lifetime. If ownership cannot be acquired, the adapter closes the prepared execution and releases its event log and dynamic-plan lease. These kernel contracts enable the adapter but do not themselves implement daemon authority or protocol behavior.

Bounds and backpressure

Every unary/stream request has encoded-byte, collection-count, and deadline limits. Server queues are bounded. A slow client is disconnected with last delivered sequence; it never backpressures kernel execution unless it explicitly configured a required durability observer outside this protocol.

Compatibility

Buf lint and breaking checks govern schemas. Additive optional fields are tolerated. Unknown enum values are retained only where a safe textual fallback exists; unknown lifecycle operations fail closed. Removed/renumbered fields and semantic reuse are breaking. Supported client/server ranges are machine-readable in compatibility manifests. Security-significant additions to snapshot authority require a new MAC domain and versioned capability because older peers ignore unknown Protobuf fields by design.

Security boundary

The initial protocol has no TCP listener. Unix sockets or Windows named pipes and endpoint metadata are current-user only. A random metadata token protects against accidental/ambient local connections but does not defend against code already running as the same user. Remote access requires a new threat model and protocol extension.

Snapshot authority uses a distinct server-owned key. The public 32-byte scope ID and positive generation select that key; neither is a secret. The HMAC proves integrity and authority but does not encrypt conversation content. Scope keys must never be derived from or reused as endpoint authentication tokens.

Failure semantics

Transport failure never implies a mutating request failed before commit. Operation IDs provide deduplication where defined. Mutating tool outcomes that lose acknowledgement are marked uncertain and never replayed automatically. Cancellation is cooperative and terminal events remain authoritative. Executor panics and unexpected errors are contained as one bounded canonical uncertain outcome and a secret-safe sentinel; expected business failures are explicit canonical outcomes. A canceled duplicate waiter never cancels or replaces the operation owner.

Acceptance before freeze

The schema-foundation acceptance covers old/new versions, unknown fields, capability mismatch, transport-metadata authentication separation, deadline/cancellation fields, overload, cursor gap/recovery, stale interaction response, snapshot version skew, malformed input, fuzz smoke, Buf lint/breaking, and deterministic generation. Duplicate-operation behavior, actual transport authentication, reconnect, half-close, and Windows/Unix endpoint permissions remain acceptance requirements for the daemon host slice.

The pre-host contract repair resolves interaction prompt discovery, reconnect ownership CAS, remote suspend/resume, imported run identity, and atomic replay bounds in the provisional schema. The host slice must still prove those contracts over real RPCs and enforce the invariant that a unary RPC context never owns the lifetime of the run it creates. The committed Buf baseline makes the amendments explicit; this provisional RFC does not claim a daemon exists.