This document is the working guide for the boundary between octoscode-web and Octos Core. The authoritative Rust contract lives in:
crates/octos-core/src/app_ui.rscrates/octos-core/src/ui_protocol.rs
The browser client must not become a second protocol authority.
The React-free client connects to /api/ui-protocol/ws, validates JSON-RPC
frames, correlates responses, and requests Web features during the handshake.
The application enables a method only after the server accepts or advertises it.
Missing optional capabilities reduce the interface; they do not trigger a
best-effort wire guess.
The generic request primitive is private. Every public method with a structured result applies a fail-closed decoder before returning. Cold receipt decoders load asynchronously without deferring the authority-sensitive RPC dispatch; live event decoders remain synchronous. Request timeouts and disconnects move their ids into a bounded quarantine so a valid late server response is ignored instead of reported as an unrelated protocol error. Method and feature names exported by Core come from the generated contract index; only the isolated CLI AppUI extensions (including onboarding, Profile configuration, peer prepare/gather, context, inventory, skills, and research lanes) remain isolated handwritten contracts until Core exports them.
Browser WebSockets cannot attach an Authorization header. The current Core
endpoint accepts ?token= and repeated ui_feature parameters. Tokens remain
in tab-scoped sessionStorage so a refresh can reconnect without placing the
credential in durable cross-tab storage. They must travel over HTTPS/WSS outside
loopback and must be excluded from diagnostics and access logs.
A reconnecting socket is not a recovered session. Recovery is complete only when the client treats these behaviors as one invariant:
- Open or hydrate the intended session scope.
- Resume from the last durable cursor.
- Deduplicate replayed envelopes.
- Reject events from another session scope.
- Repair detectable gaps from server truth.
- Fail closed when the server reports lossy replay.
Notifications become feature-specific actions before reaching React. The app does not route raw frames through a global event bus or duplicate durable state across compatibility and projection stores.
Settled assistant output is rendered as safe GFM. Streaming text stays plain until persistence so repeated deltas do not continually reparse an unbounded Markdown tree.
- Permission controls are populated by
permission/profile/list; the set response is authoritative. - Diff preview ids come only from typed approval or mutation fields. The app
reads proposal-time snapshots through
diff/preview/getand never substitutes a browser-computed git diff. - Plans, policy, tasks, output, cancellation, and artifacts use the advertised status and task methods. Output cursors are byte offsets; reads are bounded.
- A successfully committed
session/openmay add its exact(workspace_root, active_profile_id, session_id)tuple to bounded, identity-bound tab navigation. rc.9session/listrows are not projected because the response proves neither Workspace nor Profile scope. Confirmed refs are not a server catalog and contain no transcript or projection data. - Context, cost, and effective runtime-model usage merge typed progress metadata with the latest server status snapshot.
The current client uses one physical pooled WebSocket for all confirmed
Sessions. One SessionRecord per full endpoint/Workspace/Profile/Session/auth
epoch owns its controller, FIFO, interactions, cursor, and bounded timeline.
Selection does not close a socket, reset a queue, or wait for another turn's
ACK. Candidate open/hydrate commits only while its captured scope, transport,
and generation remain current; rejected candidates cannot replace a live record.
Every record receives its own admitted events, including background approvals, questions, and canonical terminals. Terminal handling advances only that record's FIFO. Reconnect suspends all record leases, rehydrates them through the single new transport, and reconciles sent versus unsent work before drain. Changing authentication retires the old semantic authority. Page reload does not restore browser-local pending prompts or attachment drafts.
Core rc11 still couples interactive execution to the accepting connection.
Closing it interrupts connection-owned work; ordinary teardown persists a
connection_closed terminal. A daemon restart likewise requires fresh hydrate
and cannot be presented as continuous detached execution. A terminal event is
not permission to close the shared transport while other work may remain. This
ownership is recorded in
ADR 0020, which
supersedes ADR 0019's ACK navigation guard and eight retained-owner-socket
budget. An unresolved turn-recovery state still blocks navigation until checked.
The downloadable runtime baseline is
core-runtime.json, currently v2.0.3;
historical rc.11 testing is separate evidence.
Native peer peer/staged and peer/closed events route to the retained master
record even while backgrounded. The coordinator binds one peer manager to that
record, buffers bounded events during its typed factory load with a transport
fence, and opens the server-supplied full peer identity without selecting it.
Kickoff uses the peer record's ordinary FIFO/native turn UUID; enqueue alone is
not a server receipt. Existing peer history or uncertain dispatch is not
automatically kicked off again after recovery. Close invalidates pending host
work and retires only the matching peer, preserving an honest selected view.
peer/gather itself is a Profile-scoped read. Refresh blackboard remains
read-only. The candidate /gather [all|slug…] composes the pinned TUI's bounded
64 KiB UTF-8 synthesis prompt and admits it to the originating master record's
ordinary FIFO. Empty results create no turn; a busy master queues synthesis.
Selection changes do not redirect it, while authority/recovery changes prevent
late dispatch. This acceptance correction still needs its browser verification;
a successful blackboard read alone does not prove synthesis.
The model served by a Session runtime and the active Profile default are different projections. The composer reports only the former. Settings uses independently capability-gated Profile operations:
profile/llm/listwithoutsession_idreads the configured primary and fallbacks. Its rows contain provider/model/route metadata andhas_api_key, never the secret value.profile/llm/catalogsupplies canonical families, models, and routes.profile/llm/testprobes the exact draft route/model and supplied or Core-owned credential.profile/llm/fetch_modelsasks Core to discover model ids for the draft provider and route. An empty result orprovider_unavailableis not treated as proof that the credential is invalid; manual model-id entry remains available.profile/llm/upsertsaves a tested configuration and may make it the Profile primary.profile/llm/deleteremoves an exact family/model/route tuple.profile/llm/selectchanges the Profile default among configured entries.
Capabilities are evaluated per operation: a server that advertises only list
still gets a useful read-only page, while unavailable mutation or discovery
controls remain absent. The API key remains transient form state in the open
editor and is passed as an operation argument; it never enters published
controller state or browser storage. Omitting it reuses a key already saved by
Core. Core owns persistence and returns only the redacted has_api_key
projection; the client does not assert that the server-side store is encrypted.
A dedicated server-side credential contract is tracked in
octos#2163.
Save performs Test and Upsert from one immutable draft so the two requests
cannot diverge across an await. Responses are accepted only for the current
transport/Profile authority, and reflected credential-shaped errors are redacted
before publication. The inspected rc11 AppUI model selection accepts typed
temperature, top_p, context_window, reasoning_effort, and model_hints.
Upsert replaces the complete inference configuration: null and omission both
mean inherit, not retain the prior value. Web preserves known configured values
on existing edits and blocks edits containing unsupported configuration; it does
not provide an inference editor absent from the pinned TUI. max_output_tokens
is gateway-owned and rejected by this RPC, not an additional model-selection
field. Per-turn reasoning is a separate turn/start.reasoning_effort contract,
captured in each queued prompt. This rc11 runtime observation does not change
the repository's generated Core contract pin.
Profile changes affect every Session on that Profile and may require an Octos restart. They are not treated as a Session override; that missing Core contract is tracked in octos#2148, while upsert/delete runtime invalidation is tracked separately in octos#2164. Provider-aware model discovery is tracked in octos#2165.
For a path-based launch, the client requests capabilities before opening a
session. It calls launch/resolve only when both the method and
session.workspace_cwd.v1 are available. The result chooses a Profile; it does
not identify an existing Session. New Session and Add workspace keep a neutral
web-<uuid> launch intent until that Profile is resolved, then open a fresh
<profile>:api:web-<uuid> Session. An unambiguous fresh activate follows that
resolved Profile automatically; cross_profile and no_profile remain
explicit. Existing confirmed sidebar rows bypass launch resolution and open
their exact server identity and Profile. The Web client never aliases or
overwrites Octoscode TUI's <profile>:local:tui#coding conversation.
A launch whose socket closes keeps its error after the automatic reconnect,
because the reconnect does not retry it. Core closes with 1008 auth_expired
when a launch asks for a Profile outside the authenticated identity. The token
is still valid in that case, so the client reports a Profile permission failure
and keeps the token; a token that is really bad fails the reconnect handshake.
This profile-bearing Web id is a compatibility requirement, not proof that Core
has one unified Session resolver. Current Core can accept a raw id with an
explicit Profile during session/open and then route hydrate/status elsewhere;
the server-side identity split is tracked in
octos#2162.
On no_profile, the browser offers solo onboarding only when Core advertises
the complete method set:
profile/local/createprofile/llm/catalogprofile/llm/testprofile/llm/upsert
Families, models, and routes come from Core. A credential must pass the provider
test before the selection is saved, and the API key exists only in the live
form/request closure. requested_id is only a request: Core's returned
profile_id is authoritative and may be normalized or collision-suffixed.
Partial failure retains both that returned id and its original request so a
retry does not duplicate it. Older servers show octoscode onboard instead.
These onboarding names currently come from the CLI AppUI transport rather than
exported Core constants. They are isolated in
packages/client/src/onboarding.ts behind strict bounded decoders until Core
exports them in the generated contract.
One capability-gated control crosses the session boundary downward:
server/shutdown stops a local octos serve --solo from the Settings General
section. The method is only offered when the connected server advertises it, the
dialog confirms before dispatch, a failure keeps the tab connected, and success
disconnects to the connect screen. The browser never infers the capability from
the endpoint shape.
Two machine-readable pins answer different questions:
| File | What it proves |
|---|---|
packages/client/contract-source.json |
Generated method, feature, notification, and protocol vocabulary matches an immutable Core source revision and Git blob. |
packages/client/core-runtime.json |
A checksummed released octos binary completes the browser-critical runtime flow. |
Payload decoders remain deliberately narrow and fixture-checked until Core emits a complete machine-readable schema. Permission enums and other safety-bearing values fail closed. Display-only labels may remain forward compatible after their surrounding structure is validated.
Verify the source contract:
pnpm contract:verifyAfter intentionally changing the immutable source pin:
pnpm contract:updateReview the generated diff. Do not treat a source pin as proof that an unreleased binary was executed.
Exercise the released runtime baseline:
OCTOS_BINARY=/absolute/path/to/pinned/octos pnpm integration:coreThe integration rejects a binary whose release and commit do not match
core-runtime.json. It starts an isolated octos serve and covers health,
negotiation, no-profile launch, provider catalog/test/save through a local
OpenAI-compatible fixture, exact TUI session open, hydrate, permissions,
supervision, and status without making an external model turn.
Unit fixtures provide deterministic parser and reducer coverage. Playwright provides browser product coverage. Neither replaces the pinned real-Core gate. The expanded rc11 parity implementation is still an acceptance candidate; passing any one of these layers does not establish a full native-peer live soak or complete TUI parity. See Feature parity.
- ADR 0003: Octoscode semantic parity
- ADR 0005: Durable session recovery
- ADR 0007: Coding safety surfaces
- ADR 0010: Server-resolved workspace launch
- ADR 0013: Generated Core contract index
- ADR 0014: Pinned Core runtime smoke
- ADR 0015: Solo Web onboarding
- ADR 0019: Tab Session navigation and background turn ownership
- ADR 0020: Retained Sessions on a shared transport
- ADR 0021: User-scoped durable composer drafts
- ADR 0022: Activity for confirmed Sessions