diff --git a/docs/canvases/agent-bundle-walkthrough.canvas.tsx b/docs/canvases/agent-bundle-walkthrough.canvas.tsx new file mode 100644 index 000000000..b2b1e0b32 --- /dev/null +++ b/docs/canvases/agent-bundle-walkthrough.canvas.tsx @@ -0,0 +1,971 @@ +import { + BarChart, + Callout, + Card, + CardBody, + CardHeader, + CollapsibleSection, + Divider, + Grid, + H1, + H2, + H3, + Pill, + Row, + Stack, + Stat, + Table, + Text, + useHostTheme, + type CSSProperties, +} from "cursor/canvas"; + +/* ---------------------------------------------------------------- helpers */ + +const MONO = + 'ui-monospace, SFMono-Regular, Menlo, Consolas, "Liberation Mono", monospace'; + +function Kicker({ children }: { children: string }) { + const t = useHostTheme(); + return ( + + {children} + + ); +} + +function MonoBlock({ text, dim }: { text: string; dim?: boolean }) { + const t = useHostTheme(); + const style: CSSProperties = { + background: t.fill.quaternary, + border: `1px solid ${t.stroke.tertiary}`, + borderRadius: 6, + color: dim ? t.text.secondary : t.text.primary, + fontFamily: MONO, + fontSize: 12, + lineHeight: "18px", + margin: 0, + overflowX: "auto", + padding: "10px 12px", + whiteSpace: "pre", + }; + return
{text}
; +} + +function StageBox({ + title, + detail, + emphasized, +}: { + title: string; + detail: string; + emphasized?: boolean; +}) { + const t = useHostTheme(); + return ( +
+ + {title} + + + {detail} + +
+ ); +} + +function FlowArrow() { + const t = useHostTheme(); + return ( +
+ {"\u2192"} +
+ ); +} + +function StepMarker({ n }: { n: number }) { + const t = useHostTheme(); + return ( +
+ {n} +
+ ); +} + +function WireStep({ + n, + title, + channel, + payload, + note, + last, +}: { + n: number; + title: string; + channel: string; + payload?: string; + note?: string; + last?: boolean; +}) { + const t = useHostTheme(); + return ( +
+
+ + {!last && ( +
+ )} +
+
+ + {title} + + {channel} + + + {note !== undefined && ( + + {note} + + )} + {payload !== undefined && ( +
+ +
+ )} +
+
+ ); +} + +/* ------------------------------------------------------------ wire bodies */ + +const AUTHORED_TREE = `my-plugin/ + agent-bundle.config.ts defineConfig({ plugin, targets, ... }) + src/ + mcp/curator/tools/search.tsx MCP tool (id: tool:curator/search) + mcp/curator/resources/catalog.tsx + mcp/curator/prompts/curate.tsx + mcp/curator/apps/panel.tsx MCP App (config.resourceUri) + events/tool/before.tsx event route (id: event:tool/before) + events/stop.tsx event route (id: event:stop) + providers/build-info.ts context provider factory + cli/library/audit.ts CLI command "library audit" + scripts/verify-release.ts plain script -> scripts/*.mjs + state.ts defineState({ id, lifetime, budgets }) + skills/curate/SKILL.md agent skill (or rendered SKILL.tsx) + rules/style.mdc Cursor rules component + commands/triage.md chat command (Claude + Cursor)`; + +const TOOL_ROUTE_SNIPPET = `// src/mcp/curator/tools/search_audible.tsx (examples/audiobook-curator) +export const config = { + annotations: { openWorldHint: true, readOnlyHint: false }, + description: "Search Audible regions and return ranked identity evidence...", +}; // statically extracted, never executed +export const inputSchema = operation.inputSchema; // zod, runtime boundary +export const resultSchema = operation.resultSchema; + +export default async function Route({ input, signal }: ToolRouteProps) { + const receipt = await operation.handler(input, { signal }); + return ; // renders Agent.* elements +}`; + +const EVENT_ROUTE_SNIPPET = `// src/events/tool/after.tsx (examples/rsc-agent-runtime) +export const config = { + runtime: 'standalone', // 'shared' (warm IPC runtime) | 'standalone' + targets: ['claude', 'codex'], + timeoutMs: 30_000, // budget inside the host's native deadline + tools: ['file.write'], // canonical selector -> per-host matcher regex +}; + +export default async function AfterFileEdit({ canonical, native, signal }: AgentEventRouteProps) { + // canonical.provenance = { host, hostContractRevision, nativeEvent, source: 'native' } + const snapshot = await kernel.recordEdit({ idempotencyKey: canonical.idempotencyKey, ... }); + return ( + + {\`Recorded \${path} from \${canonical.provenance.host}.\`} + + ); +}`; + +const CLAUDE_HOOKS_JSON = `// claude/hooks/hooks.json - real emitted bytes (examples/hooks-and-scripts) +{ + "hooks": { + "SessionStart": [ + { + "hooks": [ + { + "command": "node \\"\${CLAUDE_PLUGIN_ROOT}/hooks/session-start-session-start-7ab7e8a5.mjs\\"", + "type": "command" + } + ] + } + ] + } +} +// Cursor keeps a flat document instead: +// { "version": 1, "hooks": { "preToolUse": [ { "command": "node \\"\${CURSOR_PLUGIN_ROOT}/...\\"", "matcher": "^Write$" } ] } }`; + +const WIRE_STDIN = `{ + "hook_event_name": "PreToolUse", + "session_id": "6f9a2c1e-4d15-4a44-9be2-6b8f0f6f2a10", + "transcript_path": "/home/dev/.claude/projects/acme/transcript.jsonl", + "cwd": "/home/dev/acme", + "permission_mode": "default", + "tool_name": "Write", + "tool_input": { "file_path": "src/payments.ts", "content": "..." }, + "tool_use_id": "toolu_01H8KQ2ZC4" +}`; + +const WIRE_IPC_REQUEST = `{ + "protocolVersion": 1, + "artifactEpoch": "17903a885df8d142e2fc4457e61bb34479e81a8f4cc64e24cb92db49eaabe3f1", + "event": "tool/before", + "hostContractRevision": "2.1.250", + "target": "claude", + "native": { ...the stdin envelope, byte-for-byte... } +} +// socket: /tmp/agent-bundle-/event-.sock (mode 0600) +// endpointId = "::" - two installs never share a runtime`; + +const WIRE_RENDER_PROPS = `props = { + canonical: { + event: "tool/before", + idempotencyKey: sha256({ event, native, target }), + observedAt: "2026-09-01T23:41:07.512Z", + provenance: { host: "claude", hostContractRevision: "2.1.250", + nativeEvent: "PreToolUse", source: "native" }, + sequence: 1, + }, + native: { ...frozen structuredClone of the envelope... }, + signal, // aborted when the socket drops or the client times out +} +// runAgentRequest installs host/session/workspace axes read from the envelope, +// then the react-server Flight worker renders the route's default component.`; + +const WIRE_IPC_RESPONSE = `{ + "protocolVersion": 1, + "artifactEpoch": "17903a88...eaabe3f1", + "status": "ok", + "output": { + "hookSpecificOutput": { + "hookEventName": "PreToolUse", + "permissionDecision": "deny", + "permissionDecisionReason": "payments.ts is frozen during release week" + } + } +} +// on failure: { "status": "error", "code": "epoch-mismatch" | "invalid-message" | "runtime-failed", ... }`; + +const WIRE_STDOUT = `{"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"deny", + "permissionDecisionReason":"payments.ts is frozen during release week"}}`; + +const STATE_SNIPPET = `// src/state.ts - one direct defineState call (AB4818 otherwise) +export default defineState({ + id: 'library-index', + lifetime: 'workspace-durable', // request | process | workspace-durable | external + budgets: { maxStateBytes: 262144, maxRevisions: 64 }, // fail closed: budget-exceeded +});`; + +/* ------------------------------------------------------------------ page */ + +export default function AgentBundleWalkthrough() { + const t = useHostTheme(); + + return ( + + {/* ============================================================ hero */} + + Framework walkthrough +

agent-bundle, end to end

+ + Author agent behavior as React Server Components; compile it into a + route-graph IR; emit native, independently distributable artifacts + for each host; and execute hooks and MCP tools as Flight renders + against a warm runtime. Flight is an internal transport only — the + hosts see their own native JSON contracts, pinned by revision. + + + + + + + + +
+ + {/* ======================================================== pipeline */} + +

The pipeline: author → compile → emit → install → execute

+ + + + + + + + + + + + + + + Source: packages/agent-bundle/src — routes/graph.ts, config/validate.ts, + build/entries.ts + entry-shell.ts, adapters/*, install/install.ts. + +
+ + + + {/* ======================================================= authoring */} + +

1 · Authoring model: files are the app

+ + One flat config owns identity and policy; everything executable is a + conventional file whose path is its identity. A route module is one + async default Server Component plus statically extractable + config, + inputSchema, and + resultSchema exports — there is no + execute/render split (exporting either is the AB4811 error). + + + + Conventional source tree + + + + + + real route}> + src/mcp/curator/tools/search_audible.tsx + + + + + + +

Route kinds compiled into the graph

+ src/mcp/<server>/{tools,resources,prompts,apps}/*.tsx, tool:curator/search, "Flight render in the generated MCP server"], + ["event-route", src/events/<family>/*.tsx, src/events/stop.tsx, event:tool/before, "Hook wrapper → IPC → warm runtime (or standalone)"], + ["cli", src/cli/**/*.ts(x) — nesting is the command path, cli:library/audit, "Generated package bin; .tsx renders via dispatcher"], + ["script", src/scripts/*.ts(x) — direct children only, script:verify-release, "scripts/.mjs (+ -flight.mjs worker when rendered)"], + ["provider", src/providers/*.ts — factory, not addressable, provider:build-info, "Wraps every render with request-scoped context values"], + ]} + /> + + Source: routes/graph.ts routeGlobs + classifyModule; routes/types.ts. Config always + wins: modules claimed by explicit scripts/hooks/bin/lib/mcp config never become routes. + + + + + + {/* ===================================================== compile+val */} + +

2 · Compile and validate: one immutable IR, structured diagnostics

+ + Discovery globs the conventional roots, rejects unsafe identity + segments, and refuses to choose sides on any collision — an MCP + server with both discovered routes and an explicit entry is a hard + AB4800 error until routes.servers.<id> picks + a mode. The graph digest covers only project-relative identity, so equal + trees hash equally on every machine. Every failure is one diagnostic: + stable AB code, severity, message, and usually a recovery hint. + + +
AB30xx, "Skill documents: Markdown parsing and rendered-skill compilation"], + [AB40xx, "Plugin metadata, skill fields, package identity (AB4008 version mismatch)"], + [AB41xx / AB42xx, "Normalized-model invariants; hook configuration and native hook sources"], + [AB43xx / AB44xx / AB46xx, "MCP servers and Apps; scripts; assets and the generated-runtime floor"], + [AB47xx, "Package build (bin/lib, AB4716 declaration replay), migration nudges, prebuilt payloads"], + [AB48xx, "Route graph: collisions AB4800–AB4803, static config grammar AB4805/AB4806, route contract AB4810/AB4811, event vocabulary + CLI command-graph collisions AB4813, CLI argv grammar AB4814, shared-runtime placement AB4817, state AB4818–AB4821"], + [AB494x, "Providers: default-factory contract, key uniqueness, reserved processLifetime"], + [AB5000 / AB60xx, "CLI and adapter failures; built-artifact validation against pinned schemas"], + [AB70xx, "Host installation; read-only Doctor probes AB7300–AB7316; dev rebuilds (AB7103)"], + [AB8xxx / AB9xxx, "Dev server configuration; eval selection, harnesses, persisted runs"], + ]} + /> + + Source: docs/diagnostics.md. Only error severity gates a build; warnings and infos never block. + + + + A route's config export is parsed with the TypeScript compiler from a bounded literal + grammar (object/array/string/number literals, as/satisfies casts). Anything dynamic + compiles with an empty config beside a named AB4806 error. The same static approach + projects each CLI route's zod inputSchema onto kebab-case argv options (AB4814 when a + construct leaves the grammar) — the module's real zod schema still validates at run time. + + + + + + {/* ======================================================== emission */} + +

3 · Per-host emission: adapters, pinned contracts, provenance

+ + Each adapter projects the normalized model against a pinned host + capability table (a JSON file with observed versions, evidence + strings, and per-event support states) and validates its output + against pinned host schemas whose sha256 hashes ship in the artifact + manifest. Every capability is supported or{" "} + unavailable with a written reason — never a + silent guess. + +
.claude-plugin/plugin.json, + .codex-plugin/plugin.json, + .cursor-plugin/plugin.json, + plugin.json], + ["Marketplace", + .claude-plugin/marketplace.json, + .agents/plugins/marketplace.json, + .cursor-plugin/marketplace.json, + "—"], + ["Hooks wiring", + "hooks/hooks.json, grouped { matcher, hooks: [{ type: 'command', command }] }", + "same grouped shape; hook processes get PLUGIN_ROOT / PLUGIN_DATA", + "hooks/hooks.json, flat { command, matcher?, timeout? } entries", + "none — Agent Plugins 1.0.0 defines no hooks"], + ["MCP registration", + .mcp.json (stdio + streamable HTTP), + .mcp.json, + mcp.json at plugin root, + "plugin.json mcp block, ${PLUGIN_ROOT} tokens"], + ["Skills / rules / commands", + "skills + commands", + "skills", + "skills + rules + commands", + "skills"], + ["Path token", + {"${CLAUDE_PLUGIN_ROOT}"}, + "relative paths + PLUGIN_ROOT cwd", + {"${CURSOR_PLUGIN_ROOT}"}, + {"${PLUGIN_ROOT}"}], + ["Install", + "claude plugin marketplace add ./ + install --scope user|project|local", + "codex plugin add @ (user scope)", + "copy to ~/.cursor/plugins/local/ (no non-interactive verb)", + "distribution profile — no single host install location"], + ]} + /> + + Source: src/adapters/capabilities/*.json (observed 2026-08-28…09-01) and src/adapters/{claude,codex,cursor,portable}.ts. + A composite "plugin" target emits one directory loadable by both Claude and Codex, with a + universal hook wrapper that discriminates the host at run time via PLUGIN_ROOT. + + + + Emitted artifact size by target and file kind (bytes) + + + X: target directory · Y: total bytes, summed per emitted-file kind. Portable has no + hook wrapper — its spec defines no hooks. Source: examples/hooks-and-scripts + dist/agent-bundle.manifest.json. + + + + Provenance in every artifact + + agent-bundle.manifest.json records, per emitted file, its sha256 and the exact + source inputs that produced it, plus the project revision (the artifact epoch), + each target's adapter revision, capability-table hash, and the sha256 of every + pinned host schema it was validated against. agent-bundle.hooks.json is the + canonical hook index across targets. + + + Source: dist/agent-bundle.manifest.json fields files[].sha256, files[].sourceInputs, + project.revision, targets[].schemas[]. + + + + + + + + {/* ================================================== HOOKS DEEP DIVE */} + + Deep dive +

4 · The hooks pipeline: what is on the wire at every step

+ + Two authoring shapes share the emitted hooks.json wiring. A{" "} + plain handler hook{" "} + (config hooks: { sessionStart: { handler } }) + compiles into a self-contained wrapper that decodes the native envelope, calls the + default-export function, and encodes the result — no IPC. An{" "} + event route{" "} + (src/events/**) compiles into a thin + client that forwards the validated envelope over a per-user Unix socket (a named pipe on + Windows) to the warm runtime living inside the generated MCP server process, where the + route component renders as a Flight request. The walkthrough below is the event-route + path for a Claude PreToolUse on a{" "} + tool/before route. + + + + generated wiring}> + What the host actually invokes + + + + + + + + + + + + + + + + +

Failure semantics: fail closed, one narrow fallback

+
runtime-unavailable, "No live socket (connect refused / missing)", "Standalone fallback if the route declares runtime or fallback 'standalone'; otherwise nonzero exit"], + [runtime-timeout, "No reply within timeoutMs (default 5 s)", "Nonzero exit — never a fabricated response"], + [epoch-mismatch, "Wrapper and runtime built from different artifact epochs", "Nonzero exit; no fallback — stale code never answers"], + [invalid-message, "Payload over 1 MiB, non-JSON, or off-schema", "Nonzero exit"], + [runtime-failed, "Render threw, socket error, endpoint contention", "Nonzero exit"], + ]} + /> + + Source: src/events/ipc.ts (EventRuntimeTransportError, requestEventRuntime) and the + generated wrapper in src/adapters/hook-contract.ts — fallback fires only on + runtime-unavailable with fallback: 'standalone'. + + + +

The standalone path

+ + A route with runtime: 'standalone' (or + as fallback) bundles the route module into the wrapper itself: the same + createCanonicalEventProps builds identity, the component resolves in-process + (Server Components and Agent protocol elements only), and the same + projectEventDocument lowers the output. No shared state, no warm process — + identical wire contract to the host. + + + + AB4817 guards placement: an event route requiring the shared runtime on a target + with no generated MCP entry hosting it — and no standalone fallback — fails the build. + +
+ + + + + + The event runtime server claims its endpoint with an exclusive{" "} + .lock file recording pid and (on + Linux) the /proc start time. A stale claim is reclaimed only when its owner is + provably dead — signal-0 probe plus start-time comparison — and on Linux the + reclamation itself is serialized through a kernel-released abstract-socket gate, so + a namespace squatter can force bounded retries (100 × 10 ms) but never steal + ownership. Live endpoints are never stolen; unverifiable claims stay fail-closed. + Reads use StringDecoder for chunk-safe UTF-8; each connection gets an + AbortController wired to close/end/error so orphaned renders cancel; teardown + removes the socket only when its device and inode still match the one it created. + + + Source: src/events/ipc.ts — claimEndpoint, reclaimOrphanedEndpointClaim, + tryAcquireEndpointRecoveryGate, readOneMessage, closeServer. + + + + + + + + {/* ==================================================== event matrix */} + +

5 · Event families × hosts

+ + The v1 vocabulary is seven canonical families. Six are supported on + all three interactive hosts under their native names;{" "} + workspace/open is supported on + Cursor as a fire-and-forget observation (the optional native pluginPaths + return is deliberately not modeled) and stays unavailable on Claude and + Codex — with a written reason per host, not a silent gap. + +
session/start, "SessionStart", "SessionStart", "sessionStart", "—"], + [tool/before, "PreToolUse", "PreToolUse", "preToolUse", "—"], + [tool/after, "PostToolUse", "PostToolUse", "postToolUse", "—"], + [stop, "Stop", "Stop", "stop", "—"], + [agent/start, "SubagentStart", "SubagentStart (adds turn_id, model, permission_mode)", "subagentStart", "—"], + [agent/stop, "SubagentStop", "SubagentStop", "subagentStop", "—"], + [workspace/open, "unavailable: no such event", "unavailable: no such event", "workspaceOpen (observe-only; optional pluginPaths return not modeled)", "unavailable: spec defines no hooks"], + ]} + /> + + +

What each event may answer

+
+ + Source: events/projection.ts projectEventDocument and the wrapper validateResult in + adapters/hook-contract.ts — illegal combinations throw before anything reaches the host. + + + +

Canonical tool selectors → native matchers

+
file.read, ^Read$, "—", ^Read$], + [file.write, ^(?:Write|Edit)$, ^(?:apply_patch|Edit|Write)$, ^Write$], + [shell, ^Bash$, ^Bash$, ^Shell$], + [mcp, ^mcp__, ^mcp__, ^MCP:], + [agent, "—", "—", ^Task$], + ]} + /> + + A hook can also pin host-scoped native tool names; a selector no target can map is a + per-target diagnostic, never a silently empty matcher. + + + + + standalone example}> + src/events/tool/after.tsx — a real event route + + + + + + + + + + {/* ======================================================== MCP flow */} + +

6 · MCP tools: the same architecture, request-shaped

+ + + + + + + + + + + The generated entry carries the compiled route table as data and hands a warm worker to + the shared server runtime (createGeneratedRouteMcpServer). Each tools/call validates + input with the route's real zod schema, renders the async default component inside + runAgentRequest — with providers, the state kernel, and the notice ledger bound — + streams Flight bytes back, and projects them onto MCP: progress reports become + notifications, Agent.Result becomes structured content validated by resultSchema. MCP + Apps embed their built HTML into the bundle as resources keyed by resourceUri. When the + bundle has event routes, exactly one generated server also hosts the event runtime IPC + socket — that is the warm runtime hooks talk to, so hooks share process state with tools. + + + Source: build/entry-shell.ts (generatedRouteMcpEntrySource, generatedRouteFlightWorkerSource), + src/mcp-server-runtime.ts (startEventRuntime, projectMcpRenderStream), build/entries.ts. + +
+ + + + {/* ==================================================== supporting */} + +

7 · Supporting systems

+ + + State + + + + + Drivers supply storage for exactly one lifetime — memory for request/process, + SQLite under the plugin root's state/ for workspace-durable; external needs + embedder wiring and is rejected for generated mounting (AB4820). Budgets + (bytes, revisions, commit latency) fail closed with budget-exceeded. Doctor + inventories durable stores by filesystem metadata only — it never opens a database. + + + + + + Notices + + + + An append-only ledger co-mounted with state (reserved id, AB4821). A notice + targets a recipient — the conjunction of observed identity axes — and moves + through evidenced states only: pending, attempted, expired, unavailable, + withdrawn. Delivery is attempted on the next event render; a recipient-scoped + MCP inbox resource (agent-bundle://notices/inbox) exposes pending notices with + exposure receipts. No host claims are fabricated. + + + pending + attempted + expired + unavailable + withdrawn + + + + + + Context axes + + + + Inside any route, await agent() returns the request context: host, session, + actor, and workspace each as an Observed value — either + { state: 'available', value, source } or + { state: 'unavailable', reason }. Honest absence is the contract: a CLI + render reports host unavailable ('unsupported-surface') rather than inventing + one. Capabilities (command, filesystem, network, projectRoot) follow the same + shape; src/providers/* factories add request-scoped values beside the + framework-owned processLifetime. + + + + + + +
+ + + + {/* ======================================================== dev loop */} + +

8 · The dev loop: epochs, Workbench, doctor, install

+ + agent-bundle dev watches the project, serializes rebuilds, and commits each successful + build as an immutable epoch under .agent-bundle/epochs/<uuid>/ with an + active-epoch.json pointer — the same epoch identity that fences hook IPC, so a stale + wrapper can never talk to a newer runtime. A package-build failure never invalidates a + committed artifact epoch (it surfaces as the AB7103 warning and retries). Generated + route declarations publish atomically to .agent-bundle/routes.d.ts. + + + +

Workbench (desktop web UI over the dev server)

+
+ + +

Doctor and install

+ + agent-bundle doctor is strictly read-only (AB7300–AB7316): host CLI probes, + installed-bundle inventory, bundle-to-source comparison, registration proof, event + runtime endpoint health, and durable-state inventory. It never repairs anything. + + + + Every target directory is independently distributable with a generated INSTALL.md; + cursor/portable/plugin targets also ship a standalone install.mjs whose staged copy + is idempotent for identical content, refuses version or content collisions, and + never touches sudo or PATH. The packed proof level (agent-bundle/test) runs the + same mcp-server-runtime in memory, and deleted-source proofs verify artifacts stay + self-contained after the source tree is gone. + +
+ + + + + + {/* ========================================================== footer */} + + + Verified against /fast/projects/agent-bundle source: routes/{graph,types,contract,public}.ts · + config/validate.ts · adapters/hook-contract.ts + capabilities/*.json · + events/{ipc,project,projection}.ts · build/{entries,entry-shell}.ts · + mcp-server-runtime.ts · rsc-runtime state/notices/agent-request · docs/diagnostics.md + + docs/framework-mode.md · examples/{hooks-and-scripts,audiobook-curator,rsc-agent-runtime}. + Wire payloads in section 4 are illustrative values over verified shapes. Reflects the + post-PR-#280 split of React rendering (events/project.ts) from envelope projection + (events/projection.ts). Host capability facts pinned at Claude Code 2.1.250, Codex + 0.147.0, Cursor 2026-08-28, Agent Plugins 1.0.0 (observed 2026-08-28 … 2026-09-01). + + + Generated 2026-09-01 · colors follow the host theme ({t.kind}) + + + + ); +}