The Collider Pilot is the browser harness surface for mo:os — a thin, provider-neutral MV3 extension (side panel as the persistent seat, Document PiP as an optional always-on-top mirror) that projects a purpose-selected, timestamped HG frame from an engine over MCP Streamable HTTP, and lifts only user-approved observations and tool intents back through the four rewrites; it is not an engine, owns no graph identity, and the append-only rewrite log remains the sole source of truth. Revival per ffs0#158 (Steinberger readback T=260, decisions D1-D6 approved).
Status: Phase 0-3 merged to main; Phase 4 built on feat/phase4-tools — provenance
preserved, sources pinned, secret-scanned, legacy builds attempted (see
PROVENANCE.md); on top of that, a clean read-only MV3 side-panel shell
(narrow manifest, Cytoscape inspector); the live MCP read path — a
StreamableHttpMcpAdapter that reads a live, purpose-selected HG frame from the Z440
primary engine over MCP Streamable HTTP (read-only); the Document Picture-in-Picture
mirror — an always-on-top compact mirror opened from an explicit button, sharing ONE
state store, feature-detected, degrading gracefully; and now controlled tools + model
targets — the FIRST apply capability, where every mutating act is gated behind a
confirmation UI, tool calls are structured (never text-parsed), and HG rewrites produce a
REVIEW-ONLY program preview that is never posted (no live HG write exists). Legacy
lineage lives on legacy/pilot-2026-01 and legacy/sidepanel-2026-01; main stays clean.
- Phase 0 — preserve and prove provenance (this repo, done)
- Phase 1 — side-panel shell: narrow MV3 manifest, persisted state across worker restarts, mock MCP adapter, Cytoscape inspector on a fixed typed frame; no model, no writes
- Phase 2 — live read path (this branch): MCP Streamable HTTP to the Z440 primary engine, read-only frame/node/relation tools, retry/reconnect; the extension defaults to the live adapter
- Phase 3 — Document PiP mirror (this branch): opened from an explicit side-panel button, same shared state store, feature-detected, graceful degradation
- Phase 4 — controlled tools and model targets: MCP affordance discovery, structured tool calls only, confirmation UI for every mutating act, provider-neutral model adapter
- Phase 5 — optional high-rate data plane (WebTransport) only behind measured need
A clean MV3 extension that opens a side-panel "seat", asks a mock MCP adapter for a fixed typed HG frame, and renders it: a provenance header, a Cytoscape graph, and a textual node inspector. No model, no network, no page access, no writes.
npm ci
npm run build # -> dist/ (load-unpacked target)
npm run typecheck # optional: tsc --noEmitThen, in Chrome/Edge: chrome://extensions → Developer mode → Load unpacked →
select dist/. Click the toolbar action to open the side panel.
Four gates — typecheck plus the three smoke:* scripts — and an in-extension self-test.
build is in the sequence below as a prerequisite (smoke:worker reads dist/), not as a
gate. See TESTING.md for what each one covers, the two dev-harness traps
that have cost real time, and the honest limits that are deliberately not hidden.
npm run typecheck && npm run build
npm run smoke:worker && npm run smoke:live && npm run smoke:llmThe Z440 launcher opens each generated surface window with this extension's own
sidepanel.html?surface=<surface-key> as one of its tabs. That page hands the key to the
MV3 worker, which titles the window's tab group mo:os - <surface-key> in a colour derived
from the key (stable across restarts).
Why a handshake rather than a tab-title marker: reading tab.title would require the
browser-wide tabs permission, which grants url/title/favIconUrl for every tab in every
window and appears to the user as "Read your browsing history" — a very large grant for a
window label, on a seat whose design is minimal privilege. A page can also set its own
title, so a title marker is attacker-controlled: any site could make the extension regroup
and rename whatever window it is in. Here the key travels launcher → the extension's own URL
→ worker, and the window comes from sender.tab.windowId. Neither is forgeable by a web
page and neither needs a permission; only tabGroups is required, for the title and
colour. tabs.query / tabs.group themselves need no permission.
What it will not do: it never sweeps a tab that is pinned or already in one of your own tab
groups (tabs.group MOVES tabs, so absorbing everything would empty and destroy your
group), it only ever touches the sender's window, it is idempotent, and it refuses a key that
does not match [a-z0-9][a-z0-9-]{0,63}. With no ?surface= in the URL — the docked panel,
the pop-out, the full-tab mirror — it does nothing at all.
The same ?surface=<key> also decides which engine the window reads. The key rides on
every GET_FRAME; the worker resolves it at runtime — surface key → the t266
urn:moos:channel:virtual-desktop.<key> channel node (a read-only directory lookup on the
default engine) → the engine that channel names — and builds that window's adapter against
the resolved endpoints (src/mcp/surface-resolver.js). A ?surface=z440-menno (or bare
?surface=menno) window therefore reads urn:moos:kernel:hp-z440.menno on :8001/:9001
instead of inheriting the build-time primary default. The panel's log feed and fold stream
follow the same resolution via provenance.engine_url.
Verification is not the hint: every frame also stamps provenance.engine_reported — the
connected engine's own /healthz kernel_urn. When it differs from the engine the surface
expects (an unreachable engine fell back to the default, or an endpoint serves a different
kernel than the channel names), the PostureStrip renders an ENGINE MISMATCH warning
rather than letting the window silently render the wrong fold. Unknown keys, engine-less
rooms (the manifold/collective desktops), and a down directory all degrade to the default
engine with no warning suppressed and no window dead.
sidepanel.html side-panel entry (React root)
src/sidepanel.tsx the seat: fetch frame, restore scratch, compose UI
src/sidepanel.css dark theme
src/worker.ts MV3 service worker (opens panel; serves frame via factory)
src/mcp/types.ts HgFrame / HgNode / HgRelation / provenance + McpAdapter (the seam)
src/mcp/fixture.ts the fixed typed frame (real seat model, t259/t260 nodes)
src/mcp/mock-adapter.ts MockMcpAdapter — returns the fixture, no I/O
src/mcp/transform.js SHARED pure fold -> HgFrame transform + view_filter (Phase 2)
src/mcp/streamable-http-client.js SHARED read-only MCP/REST transport (Phase 2)
src/mcp/streamable-http-adapter.ts StreamableHttpMcpAdapter — live read (Phase 2)
src/mcp/adapter-factory.ts mode switch: 'mock' | 'live' (extension defaults live)
src/state/scratch.ts chrome.storage.session helpers (selection + frame cache; onChanged subscription — per-surface store since A17)
src/components/ PostureStrip (strip + audit drawer) · SettingsPanel · GraphControls · FrameGraph (Cytoscape) · NodeInspector · ActionsPanel + ConfirmActionModal
src/tools/types.ts controlled-tools contract: ToolKind/ToolChannel, ToolSpec, AffordancePack, ToolCall, PendingAction (Phase 4)
src/tools/tool-call.ts structured ToolCall validator (args_schema JSON-shape check; NO text parsing) (Phase 4)
src/tools/affordance.ts affordance-pack projection: tools/list classify + curated acts + mock fallback (Phase 4)
src/tools/browser-acts.ts the clipboard browser act executor (local; runs on Confirm) (Phase 4)
src/tools/hg-program-preview.ts REVIEW-ONLY WF19 pins-urn apply_program preview builder (stringifies; NEVER posts) (Phase 4)
src/tools/model-providers.ts provider-neutral model registry + WebGPU capability stub (seam; no calls, no keys) (Phase 4)
src/pip/document-pip.d.ts ambient Document PiP types (not yet in lib.dom.d.ts) (Phase 3)
src/pip/pip-content.tsx PipContent — pure compact frame view (reused by the PiP window + the pip-preview harness) (Phase 3)
src/pip/pip-window.tsx Document PiP controller: gesture open, shared-scratch sync, style adoption, teardown (Phase 3)
preview.html / src/preview.tsx dev harness on the MOCK adapter
preview-live.html / preview-live.tsx dev harness on the LIVE adapter (CORS-blocked from a served page)
pip-preview.html / src/pip-preview.tsx dev harness rendering the PiP content view standalone (served-page testable) (Phase 3)
scripts/live-smoke.mjs headless live-read proof (Node; no CORS)
public/manifest.json narrow MV3 manifest
public/icons/ toolbar icons (from legacy/sidepanel-2026-01)
- Everything past the
McpAdapterboundary.MockMcpAdapter.getFrame()returns a fixed fixture (src/mcp/fixture.ts). There is no MCP client, no transport, no session, no engine connection, noOriginhandshake, no auth — none of it exists yet. - The frame is data, not a live read.
provenance.mock === trueandfolded_atis a fixed timestamp. The urns/log_seq/t_day are realistic (enginehp-z440.primary, sessionsam.z440-cowork-workspace, purposesam.cowork-workspace-curation, the t259 access-law / symmetries derivations held at gate 2) but nothing is fetched. - No model, no writes, no page access. No provider/LLM, no rewrite/apply path, no
content script, no
<all_urls>. The four rewrites (ADD/LINK/MUTATE/UNLINK) are not reachable from this build. view_filteris advertised, not applied. Phase 1 returns one fixed frame; the request's filter is ignored (the provenance still states the filter the fixture stands for).
- Narrow MV3 manifest (
sidePanel,storage,activeTab; host_permissions limited tohttp://localhost:8080/*+http://localhost:8000/*; no popup soaction.onClickedopens the panel). - Service worker holds no correctness-critical globals; selection + frame cache live
in
chrome.storage.session, so a forced worker termination is survivable (reopen restores instantly; the⟳button re-asks the worker, which re-answers identically). - Cytoscape inspector with URNs as stable node ids, relations (never "edges") on arrows, layout/selection kept in browser scratch, not node data.
- Provenance header showing engine, log_seq · t_day, workspace, purpose, and view_filter.
- Textual inspector: selected node's urn / type_id / properties + incident relations.
The live read plugs into the McpAdapter seam in src/mcp/types.ts
with no signature change — the worker still calls adapter.getFrame(request) and
forwards the typed frame; the side panel, the GET_FRAME/FRAME envelope, and the
component tree are untouched. The one type change is FrameProvenance.mock widened from
the literal true to boolean, so a live frame can state mock: false (the header now
renders a green LIVE badge instead of the amber MOCK one).
The extension now defaults to the live adapter (src/mcp/adapter-factory.ts). It calls
only these engine reads:
| Surface | Endpoint | Tool / route | Used for |
|---|---|---|---|
| MCP Streamable HTTP | POST http://localhost:8080/sse |
initialize |
handshake (JSON-RPC 2.0, protocol 2024-11-05; server is stateless per request, no mcp-session-id) |
| MCP Streamable HTTP | POST http://localhost:8080/sse |
tools/call → graph_state |
the whole current fold {nodes, relations} |
| MCP Streamable HTTP | POST http://localhost:8080/sse |
tools/call → node_lookup |
single node by urn (helper) |
| REST | GET http://localhost:8000/healthz |
— | log_len, t_day, ontology_version for provenance |
| REST | GET http://localhost:8000/state/relations/src/{urn} |
— | outgoing-relation helper |
There is NO apply path. graph_state / node_lookup are the only tools invoked, gated
by a positive read-only allowlist in streamable-http-client.js; the engine's
apply_rewrite / apply_program tools are never named or called. The four rewrites
(ADD/LINK/MUTATE/UNLINK) are unreachable from this build. No model, no credential, no
Origin-bypassing config is wired.
graph_state returns the fold as stringified JSON inside result.content[0].text, with
each property wrapped as {value, mutability, authority_scope, stratum_origin}. The pure
transform (src/mcp/transform.js) unwraps .value, maps nodes →
HgNode and relations → HgRelation (type_id = rewrite_category/WFxx, label = the
relation-kind port, e.g. provides-kb), then applies the client-side view_filter (L_p):
by default it keeps types [knowledge_item, derivation, purpose, session] scoped to the
live Cowork seat session:sam.z440-cowork-workspace plus its 1-hop neighbourhood — the
same shape as the Phase 1 mock, so the UI stays continuous. Provenance is stamped from
/healthz. A FrameRequest.view_filter ({purpose?, scope_urns?, types?, t?}) overrides
the defaults.
transform.js and streamable-http-client.js are authored in JS + JSDoc precisely so the
same real code runs in the service worker (via the .ts adapter) and in the Node smoke
test — the smoke test is not a re-implementation.
A served page (the vite dev server, preview-live.html opened over http://…)
cannot reach the engine: the browser applies CORS to the cross-origin POST to
http://localhost:8080/sse and forbids overriding Origin, so the request is blocked and
preview-live shows a transport error. That is expected. The live path renders in the
browser only from a CORS-exempt context — i.e. the actual unpacked extension, whose
host_permissions for :8080/:8000 grant the cross-origin reads (or a browser started
with web security disabled). preview.html deliberately stays on the mock adapter for
served-page use.
The headless, CORS-free proof is the Node smoke test:
node scripts/live-smoke.mjsIt imports the same shared streamable-http-client + transform modules, runs
initialize → graph_state → /healthz → selectFrame → node_lookup, prints the resulting
HgFrame summary (node count by type, relation count, provenance header), and exits
non-zero unless a live frame with non-zero nodes was read.
- Clean MV3 manifest with narrow permissions (
sidePanel,storage,activeTab; localhost:8080/:8000hosts only; no<all_urls>, history, bookmarks, downloads, or content script). - Side panel opens from the toolbar action (
chrome.action.onClicked→chrome.sidePanel.open; nodefault_popup, so no onClicked conflict). - Persisted state survives forced service-worker termination (
chrome.storage.session; no correctness-critical worker globals). - Mock MCP adapter returns a fixed typed frame.
- Frame metadata visibly includes source engine, log sequence/time, workspace, purpose, and view filter (the provenance header).
- Cytoscape inspector renders the mock frame plus a textual inspector.
-
StreamableHttpMcpAdapter implements McpAdapter(no seam signature change). - MCP Streamable HTTP
initializehandshake +tools/call graph_state; REST/healthzfor provenance. - Wrapped-property unwrap + fold →
HgFrametransform, shared by adapter and smoke test. - Client-side
view_filterselection (default = live Cowork-seat slice; honors request). - Retry/backoff on transport failure; errors surfaced (not thrown uncaught).
- Read-only helpers (health,
node_lookup, relation neighborhood); no apply. - Runtime adapter switch; extension defaults to live, mock still available.
-
node scripts/live-smoke.mjsreads a real non-zero frame from the running kernel.
A Pop out ⧉ button in the side-panel header opens a Document Picture-in-Picture window that mirrors the panel: the SAME frame, the SAME selection, kept in lock-step through ONE shared state store. It is a pure read/UI surface — no new engine capability, no adapter call, no model, no writes. The four rewrites (ADD/LINK/MUTATE/UNLINK) stay unreachable; the write path (controlled tool calls + confirmation UI) remains gated to Phase 4.
- Explicit, gesture-driven open. The panel header shows a visible Pop out ⧉ button.
Document PiP's
requestWindow()REQUIRES a user activation, so the click handler callsopenPipMirror()which invokesrequestWindow({width, height})synchronously — noawaitruns before it, so the gesture is still valid. Clicking again while a mirror is open just focuses the existing window (never a second one). - One shared store per surface (
chrome.storage.session, keypilot.scratch.v1— orpilot.scratch.v1.<scope>when the page carries a scope key, A17). The scope is the validated?scope=param when present (both pip.html routes thread it from their opener, with no?surface=needed), else the launcher's?surface=room key on sidepanel.html, else the default. A single browser-wide key meant 14 generated windows overwrote one selection and one cached frame; each surface now scratches on its own channel, and a mirror always lands on its opener's channel. The panel and the PiP mirror read and write the same scratch store. Sync is event-driven viachrome.storage.onChanged(plus a re-read when the PiP window regains focus) — no polling loop. The frame flows one-way (panel authors it → scratch → mirror); the selection flows both ways (saveSelectedUrnpreserves the panel-authored frame while flipping the shared selection). Selecting a node in either surface reflects in the other. The mirror renders the identicalHgFramethe panel holds (passed through scratch, not re-fetched — guaranteeing frame identity, criterion 3). - The PiP React tree runs in the opener's JS realm.
createRoot(pipWindow.document.body)targets the PiP document's DOM, but the component code, hooks, andchrome.storageaccess all execute in the side panel's realm — so the extension APIs are always available regardless of the PiP window's own realm. - Styles are adopted, not inherited. A PiP document inherits none of the opener's CSS, so
copyStyles()reconstructs every same-origin sheet as an inline<style>in the PiP head (falling back to a linkedhreffor any unreadable sheet). The Pop-out button and the compact.pip-*styles live in the samesidepanel.cssthat gets adopted, so the mirror looks like a compact side panel. - Graceful degradation.
pagehideon the PiP window tears down the mirror (unmount React, drop handles) and notifies the panel so the button re-enables; the side panel keeps working throughout. Apagehideon the opener proactively closes the PiP first. The PiP window never outlives its opener (browser-enforced) — we handle that cleanly, no dangling root. - No placement control. Only
width/heightare passed; window position is UA-owned. Software remembers content/state, never placement (criterion 5). - Feature-detected.
isDocumentPipSupported()checks forwindow.documentPictureInPicture. When absent (mobile / unsupported browsers) the button is disabled with the tooltip "Document PiP not supported in this browser" and the side panel remains fully functional. (The Document PiP API isn't yet in TypeScript'slib.dom.d.ts, sosrc/pip/document-pip.d.tsdeclares the minimal typed surface.)
A real PiP window needs a user gesture and a loaded extension, so it can't be driven from
a served page. pip-preview.html renders the PiP CONTENT component
(PipContent) standalone — fed a frame from the MOCK adapter, with
selection in local React state (exactly what the shared-scratch sync supplies inside a real
extension). Same pattern as preview.html. Because PipContent is a pure presentational
component, this harness pulls in no chrome.* / Document-PiP code — verified in the build,
where documentPictureInPicture lands only in the side-panel entry chunk, not the shared or
pip-preview chunks. It is a fifth vite entry (pip-preview), built for local/CI UI checks
and never loaded by the extension.
npm ci
npm run typecheck # tsc --noEmit
npm run build # -> dist/ (worker.js stays unhashed; pip-preview.html emitted)
npm run dev:preview # then open /pip-preview.html on an allowed dev portnpm run build, then loaddist/unpacked (chrome://extensions→ Load unpacked).- Click the toolbar action → the side panel opens.
- Click Pop out ⧉ → a Document PiP window opens mirroring the panel (same provenance header, same graph, same selection).
- Select a node in the panel → it highlights in the PiP mirror; select a node in the PiP → it highlights in the panel. (Selection is the shared, bidirectional state.)
- Close the PiP window → the button re-enables and the side panel keeps working. Close/reload the panel → the browser closes the PiP with it.
Phase 4 is controlled tools and model targets and is implemented on feat/phase4-tools.
It is the first phase that mounts an apply capability — nothing before it (Phases 0-3) could
reach ADD/LINK/MUTATE/UNLINK. Crucially, Phase 4 still performs no live HG write: the
apply capability is a REVIEW-ONLY preview, and every mutating act is gated behind a
confirmation UI. The full write-up and exit-criteria checklist are in the Phase 4 section
at the end of this document.
- PiP opens from an EXPLICIT side-panel button (Pop out ⧉ in the header);
requestWindow()is called synchronously in the click handler (noawaitbefore it). - The PiP window shows the SAME frame + selection as the side panel via ONE shared store
(
chrome.storage.sessionscratchpilot.scratch.v1); selection mirrors both ways, event-driven onstorage.onChanged(+ focus re-read), no polling loop. - Same conversation/frame id — the PiP renders the identical
HgFramethe panel holds, passed through the shared scratch (not re-fetched), guaranteeing identity. - Closing/navigating the opener degrades GRACEFULLY back to the side panel:
pagehidetears down PiP state; the panel keeps working; the PiP never outlives its opener. - No programmatic screen placement assumed (only width/height passed; UA owns position).
- Feature-detected: when
documentPictureInPictureis unavailable the button is disabled with a tooltip and the side panel stays fully functional (mobile/unsupported fallback). - Still READ-ONLY: no adapter/engine call in the PiP path, no apply tool anywhere
(
apply_rewrite/apply_program/POST /programsappear only in read-only-guarantee comments); typecheck + build pass,pip-previewis a self-contained served page.
Phase 4 mounts the FIRST apply capability — and does so without ever writing to the HG. The controlling idea: an act is a typed object that must survive a confirmation gate, and an HG rewrite only ever becomes a review-only preview, never a live post.
- No live HG write exists. There is zero executable call to
apply_rewrite/apply_program, and zeroPOST /programsorPOST /rewrites, anywhere in the build. The HG act builds a preview envelope and stringifies it — that is the only thing that happens. - Every mutating act is confirmation-gated. A
mutatetool cannot act until the user clicks Confirm inConfirmActionModal, which shows tool / target / args / actor / workspace / purpose / expected effect. Cancel (and Escape) is a strict no-op. - Structured calls only. An act is a
ToolCall {name, args}validated against the tool'sargs_schema(a flat JSON-shape check insrc/tools/tool-call.ts) before the modal opens and again before Confirm resolves. There is no text/regex/fenced-block parser — the legacy scaffold's ad-hoc```tool`` reader was deleted in Phase 0 and never returns. - Confirm's meaning is fixed by the tool's
channel:browser→ run a local browser act (no engine);hg→ build/reveal the review-only preview (never post).
src/tools/affordance.ts projects the tools reachable for the current frame's
actor / workspace / purpose, modelling the SHAPE of the mo:os session-affordance map
({actor, workspace, purpose, tools:[{name, kind, description, args_schema}]}). It prefers
the LIVE MCP tools/list catalog (fetched read-only through the worker — listing is not
calling), classifies each tool read-vs-mutate by name (unknown ⇒ mutate, i.e. gated), and
down-projects each inputSchema to a flat args_schema. Offline / against the mock adapter
it falls back to a labelled MOCK pack. Discovered MCP tools are catalogued for
transparency but are not actionable — no executable path exists for them, so the write
invariant stays airtight regardless of what the server advertises.
copy_urn_to_clipboard— a harmless BROWSER act: copies the selected node's urn to the clipboard on Confirm. No engine, no HG. Still gated (it touches state outside the panel).pin_ki_to_workspace— a REVIEW-ONLY HG rewrite: Confirm builds a WF19pins-urnLINKapply_programenvelope (kernel actor,pins-urn/pinned-by-sessionports — the canonical WF19 pin form) and displays it with a download button. It is never posted; the panel shows "Not posted. Applying is a separate, out-of-band, human step."
src/tools/model-providers.ts is a provider-neutral registry. Since Phase 7 the seam is
LIVE: it defaults to ollama-local (on-box, keyless, :11434), offers gemini via
the kernel-proxy (:8000/llm/gemini, key server-side, gated on the scope-split LLM bearer
pilot.llmToken + the A3 access posture — never called anon), and keeps none-manual
(no model). Provider/model/bearer are configured in the Settings block (t263); the model
only ever PROPOSES a structured tool call — validateToolCall (incl. the semantic urn
checks) and the confirmation modal gate everything, exactly like a hand-composed act.
(The Phase-4-era "Local WebLLM behind a WebGPU stub" option was never shipped and is gone.)
The Actions section is wired into the side panel only (ActionsPanel, each surface
ErrorBoundary-wrapped and defensively rendered). The PiP mirror deliberately does not
mount it — it stays a read/observe surface; a note in the panel states so.
npm ci && npm run build, loaddist/unpacked, click the toolbar action.- In the Actions section, confirm the affordance pack shows the frame's actor/workspace/
purpose and a
LIVEbadge (orMOCKif the engine is unreachable — it degrades cleanly). - Select a node → Copy urn to clipboard → the confirmation modal shows the structured call → Confirm & run → the urn is on your clipboard. Cancel/Escape ⇒ nothing happens.
- Select a
knowledge_item→ Preview: pin KI to workspace → the modal shows the WF19 intent → Reveal review-only preview → inspect (and optionally download) theapply_programenvelope. Nothing is posted;/healthzlog_lenis unchanged. - Open Settings (top of the panel): the provider selector defaults to Ollama (local · keyless); Gemini (via kernel-proxy) requires the scope-split LLM bearer (set/clear it there). With Ollama running, "ask the model" proposes a structured call that lands in the same confirmation modal as the buttons — nothing auto-applies.
preview.html renders the real ActionsPanel on the MOCK adapter (liveTools=null ⇒ the
labelled mock pack), so the whole confirmation flow, the review-only preview, and the provider
seam are browser-testable without the extension.
- MCP affordance discovery filtered by actor/workspace/purpose —
deriveAffordancePackprojects the frame's actor (session occupant) / workspace / purpose; prefers the livetools/listcatalog (read-only), classifies read-vs-mutate, falls back to a labelled MOCK pack. - Structured tool calls only — a typed
ToolCall {name, args}validated againstargs_schema(validateToolCall) before the modal and before Confirm. No text parsing anywhere. - Confirmation UI for every mutating act —
ConfirmActionModalshows tool / target / args / actor / workspace / purpose / expected effect with Confirm / Cancel; Cancel and Escape are strict no-ops. - One harmless browser act + one review-only HG preview —
copy_urn_to_clipboard(executes on Confirm) andpin_ki_to_workspace(builds a WF19pins-urnpreview on Confirm; never posts). - Provider-neutral model adapter —
ModelProviderregistry defaulting tonone-manual; a seam with no real invocation and no credential. - Local WebLLM only after a capability check — the WebLLM provider is gated behind the
hasWebGpu()stub (navigator.gpu); not bundled. - No live HG write — zero executable
apply_rewrite/apply_programcalls; zeroPOST /programsorPOST /rewrites. Those strings appear only in comments, UI labels, the mock catalog, and the preview builder that stringifies (never posts) an envelope. typecheck + build pass;preview.htmlis a self-contained served page.
WebTransport / a second transport is deferred by decision: the reliable MCP path already
sustains ~61 Hz at p95 for a full graph read (measured — see docs/phase5-data-plane.md), which
exceeds any realistic UI update rate. No high-rate surface exists in the Pilot to justify a lossy
data plane. The gate + instrument:
- Decision + criteria:
docs/phase5-data-plane.md - Baseline benchmark (run before ever proposing WebTransport):
node scripts/bench-frame-read.mjs
Durable tool/rewrite semantics never leave the reliable MCP path regardless.