From 76462d2b43e17c32fae0a663ca2c8260d503f9cb Mon Sep 17 00:00:00 2001 From: hachej Date: Fri, 31 Jul 2026 17:41:30 +0000 Subject: [PATCH] docs: one owner per question, and a contract that lives with its code MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit An audit of the planning layers found the code healthy and the coordination layer not: a frozen contract that had drifted from its own types, a decision log that did not contain the shipped architecture, and two documents each claiming top authority. - Promote the AgentGateway v0 contract out of docs/issues/909/ into packages/agent/docs/AGENT_GATEWAY_V0.md, beside the types it describes, and correct it to the shipped shape (input DTOs nest scope rather than inheriting it; listAgents takes ListAgentsInput; JsonSafe and the snapshot state type differ from the frozen text). Code written from the old §6 does not typecheck. - Add Decision 29 (PROPOSED) recording AgentGateway v0, and settle the AgentHost naming collision: createAgentHost is a deployment-static composition helper, not the publication machinery D25-D28 rejected. - Mark Decision 26 SUPERSEDED where it lives, rather than only in DIRECTION. - Banner 909/plan.md as a historical record and 391/plan.md as subordinate to DIRECTION on sequencing; mark the three-tier host topology as future work owned by #905, not shipped architecture. - Document the five planning layers and one-owner-per-question rule in docs/README.md so the next reader routes correctly. Docs only. --- docs/DECISIONS.md | 13 +- docs/README.md | 26 ++++ docs/issues/391/plan.md | 12 +- docs/issues/909/plan.md | 23 +++- packages/agent/docs/AGENT_GATEWAY_V0.md | 152 ++++++++++++++++++++++++ 5 files changed, 223 insertions(+), 3 deletions(-) create mode 100644 packages/agent/docs/AGENT_GATEWAY_V0.md diff --git a/docs/DECISIONS.md b/docs/DECISIONS.md index 16df80151..41779f8fa 100644 --- a/docs/DECISIONS.md +++ b/docs/DECISIONS.md @@ -426,7 +426,7 @@ Each decision has four fields: | Field | | |---|---| -| **Status** | **Accepted (2026-07-17; ownership/multi-agent foundation clarified 2026-07-20).** Owner-directed in the #391 planning sessions after the Decision 25 reset. | +| **Status** | **SUPERSEDED by Decision 28 (2026-07-21).** Originally accepted 2026-07-17 (ownership/multi-agent foundation clarified 2026-07-20). The domain-routed typed-Workspace topology below is history, not a build target: domain no longer routes, selects, or authorizes anything, and `workspaceTypeId` survives only as compatibility metadata pinned to `default`. Read for context; do not implement. | | **What** | #391 ships in product-led steps. **Step 1A:** exact trusted domain → persisted `workspaceTypeId` → authenticated membership-authorized Workspace → Workspace-selected default agent. Core owns authentication, membership, Workspace persistence, and typed list/select/create; it does not load or compose agents. Workspace owns deployment-static `defaultAgentTypeId` + `allowedAgentTypeIds`, one shared WorkspaceRuntime/Workspace/Sandbox, effective provisioning-plugin union, and a lazy actor-neutral singleton per `(workspaceId, agentTypeId)`. Agent loads/executes one requested type against that supplied runtime. The backend proves two allowed types share exact Workspace/Sandbox identity now, while public human ingress starts new sessions with only the default and accepts no arbitrary agent selector. Existing hosts normalize through the same orchestrator as `default → primary`. Authored agent data is declarative identity/safe metadata/instructions only; trusted host plugins own executable behavior. **Step 1B:** authenticated external MCP reaches the same authorized Workspace and server-selected default. **Step 2:** activate Workspace-local native collaboration after a `pi-subagents` executor/backend can share WorkspaceRuntime; human selector/switch/fork remains a separate product decision. **Step 3:** durable task/events, replay/approvals/recovery, external A2A, hardened transports, runtime extraction, custom sandbox tools, and channels follow named consumers. Contracted agents later use explicit separate Workspaces, governed readonly projections, and returned artifacts. The canonical plan is [`issues/391/plan.md`](issues/391/plan.md); A1/runtime details are in [`issues/805/runtime-refactor/work/A1-agent-authoring/PLAN.md`](issues/805/runtime-refactor/work/A1-agent-authoring/PLAN.md). | | **Why** | The immediate product is several focused Workspace products reached by domain, not a human agent catalog. Persisted Workspace type makes product identity independent of hostname and reusable by UI/MCP/A2A. Building the typed singleton/shared-runtime substrate now prevents the default-agent shipment from cementing a second singular runtime design, while keeping selectors and collaboration out of the first UX. Separating Core authorization, Workspace orchestration, and one-type Agent execution prevents Core from becoming an executable behavior registry. | | **Rationale** | A static Workspace-ID map would require deployment on every creation; a mutable classifier could silently change product identity. Deployment-static type/default/allowed-agent declarations are auditable and need no registry/controller or AgentHost deployment/publication content-addressed store. One WorkspaceRuntime plus typed AgentBindings preserves the Workspace as security/provisioning boundary: plugin/tool differences are behavior, never isolation. New sessions persist trusted agent type; legacy sessions without it use the current default and reviewed history is not rewritten. All host-selected static source/plugin/policy references validate at host startup, but WorkspaceRuntime and AgentBindings remain lazy. Workspace/Sandbox creation failure affects the Workspace; background provisioning failure is one shared degraded-readiness state that preserves current non-runtime chat behavior; one agent load failure is isolated and retryable. Authored JSON never selects executable packages, tools, credentials, MCP commands, models, or runtime policy. Tool collisions retain current deterministic non-fatal Boring/Pi behavior. `agent dev` launches the regular server. A future Boring Pi package may adapt arbitrary Pi agents, but it cannot own auth, Workspace policy, server routes, persistence, provisioning, or the shared runtime lifecycle. Same-process collaboration stays native; A2A remains external. | @@ -464,6 +464,17 @@ Each decision has four fields: | **Supersedes / retains** | Supersedes Decision 26's domain-routed typed-Workspace topology, Core typed list/select/create track, Workspace-type default/allowed policy, exact shared `WorkspaceRuntime + Sandbox` object-identity requirement across Agent types, and combined-host R1–R6 dispatch graph. Retains Decisions 19/21/22 and compatible parts of Decisions 25/26: package layering and EU/self-hostability; ordinary Workspace membership before effects; static/no-controller composition; declarative source plus trusted executable plugins; actor-neutral Agent identity; session/history compatibility; protocol bindings at edges; governed contracted projections; and rejection of AgentHost, deployment/publication content-addressed storage, mutable registries, authored executable catalogs, and second behavior composers. | | **Re-evaluate when** | A named product needs public default changes, per-Workspace fleet allowlists, remote Agent deployment, remote Environment transport, per-Agent copied workspaces, stronger isolation than execution views, cross-Workspace delegation, or restart-safe external tasks. Re-evaluation must specify authorization, persistence, filesystem authority, governance enforcement, session migration, rollout, rollback, and proof. It cannot let signup domain/Agent identity/Environment capability grant membership, silently rewrite a persisted Workspace default, couple Core and CLI through each other, let an Agent mint its own Environment authority, create competing authoritative file trees, or restore AgentHost/controller/publication machinery. | +## 29. AgentGateway v0 — one session contract, one construction funnel + +| Field | | +|---|---| +| **Status** | **PROPOSED — awaiting owner ratification.** Records architecture that shipped in v0.1.91 and that every subsequent wave depends on. Drafted 2026-07-31 after an audit found the decision log ended at 28 while the shipped architecture was authorized only by an issue plan folder. | +| **What** | Workspace, Core, CLI and the playground consume agents exclusively through an Agent-owned `AgentGateway`: seven session methods plus `close()`, addressed per agent at `/api/v1/agents/{agentTypeId}/sessions/...`. `createAgentHost()` is the single construction funnel producing that gateway; a CI invariant forbids any other construction path. Fleets are compiled and validated at startup (`AgentFleetCompiler`), failing loudly rather than degrading. Authorization enters as a branded `AuthorizedAgentScope` — an issuer-owned runtime capability re-checked on every use, never a transport DTO. Execution environments are leased per `(workspaceScopeId, placementIdentity)` with a provisioning fingerprint. Conformance Level B (bounded replay + snapshot rehydrate) is the shipped bar; Level D (durable) is specified and deferred. The binding contract description is [`packages/agent/docs/AGENT_GATEWAY_V0.md`](../packages/agent/docs/AGENT_GATEWAY_V0.md), colocated with its types. | +| **Why** | Decision 28 requires a deployment-static fleet with no registry or controller, and requires that agents receive capabilities rather than policy sources. A single construction funnel is how that becomes enforceable instead of aspirational: one place validates the fleet, one place mints scope, one CI rule proves no consumer bypasses it. Addressing sessions per agent is what makes more than one agent possible in a workspace at all — the legacy single-agent wire cannot express which agent a session belongs to. | +| **Rationale** | `createAgentHost()` is a composition helper that returns a gateway and a `close()`. It is **not** the AgentHost deployment/publication machinery rejected by Decisions 25–28: there is no controller, no content-addressed store, no mutable registry, no authored executable catalog. The name collides; the semantics do not. Future host tiers — internal/embedded versus external/remote, and a later third-party-authored tier — are owned by #905 and are not part of v0. Session refs carry no `hostId` precisely so that remote hosts stay an additive v2 concern. Level B was chosen because durable replay is only load-bearing once concurrent multi-agent streams exist; shipping it earlier would have been unfalsifiable. | +| **Supersedes / retains** | Supersedes nothing. Retains Decision 28 in full — this is its first construction segment, not an amendment to it. Retains D28's rejection of AgentHost/controller/publication machinery, mutable registries, authored executable catalogs, and second behavior composers. | +| **Re-evaluate when** | A remote or third-party host requires session identity to carry host information; durable replay (Level D) is activated; per-workspace credentials require the harness to accept invocation-scoped capability rather than process env; or a second construction path becomes genuinely necessary. Re-evaluation must not reintroduce a registry, controller, or publication authority, and must not let an agent mint its own scope. | + ## Process 1. Any PR that changes a locked decision **must** update this document. diff --git a/docs/README.md b/docs/README.md index 54a66b9a3..508a68a16 100644 --- a/docs/README.md +++ b/docs/README.md @@ -73,8 +73,34 @@ standalone (`createAgentApp`) with zero core dependency. autonomy. - [`web/`](web/README.md) — human-oriented guide: architecture overview, full package map, getting started, composition guide, design FAQ, troubleshooting map, per-package explainers, glossary. Orientation, not normative spec — canonical specs live in `packages/*/docs/`. +## Which document answers which question + +Five kinds of planning document, **one owner per kind**. A doc that answers two +of these questions is a doc that will silently go stale in one of them — that +is how a "frozen" contract drifted from its own types for a full release. + +| Question | Owner | Notes | +|---|---|---| +| **Why** are we building this | issue [#391](https://github.com/hachej/boring-ui/issues/391) | The long-form vision | +| **What** did we rule, and what did it kill | [`DECISIONS.md`](DECISIONS.md) | Ratified, append-only, supersession stated explicitly | +| **When** — order and triggers | [`DIRECTION.md`](DIRECTION.md) | Owner-ratified waves. Wins over any issue plan on sequencing | +| **How** it works — contracts | package `docs/` beside the code | e.g. [`AGENT_GATEWAY_V0.md`](../packages/agent/docs/AGENT_GATEWAY_V0.md), `PLUGIN_SYSTEM.md` | +| **What now** — dispatchable work | beads + GitHub issues | Must be reachable from a DIRECTION wave | + +Rules that follow from this: + +1. **Contracts live next to their types, never in `docs/issues/`.** Issue + folders are historical the moment their issue closes; contracts are living. +2. **`docs/issues//` is a historical record.** It may describe what was + planned and what shipped. It does not govern what happens next. +3. **State what a document does *not* govern**, in its header. Most confusion + here came from documents claiming broad authority and being read literally. +4. **Supersession is written down where the old text lives** — a banner on the + superseded section, not only a line in the newer file. + ## Normative specs (code cites these) +- [`packages/agent/docs/AGENT_GATEWAY_V0.md`](../packages/agent/docs/AGENT_GATEWAY_V0.md) — the AgentGateway v0 session contract (7 methods, branded scope, 13 error codes, conformance levels). Supersedes `docs/issues/909/plan.md` §6. - [`packages/workspace/docs/PLUGIN_SYSTEM.md`](../packages/workspace/docs/PLUGIN_SYSTEM.md) — the plugin/agent-layer spec; source cites it as `Per PLUGIN_SYSTEM.md §X`. Keep section numbering stable. - [`packages/workspace/docs/PLUGIN_STRUCTURE.md`](../packages/workspace/docs/PLUGIN_STRUCTURE.md) — canonical layout + code patterns for new plugins. diff --git a/docs/issues/391/plan.md b/docs/issues/391/plan.md index 1f03d51df..6340c19bf 100644 --- a/docs/issues/391/plan.md +++ b/docs/issues/391/plan.md @@ -11,7 +11,17 @@ track: owner ## Authority -This is the product roadmap, product-gate, correction, and release authority +> **Amended 2026-07-31.** This file no longer holds sequencing authority. +> [`docs/DIRECTION.md`](../../DIRECTION.md) (owner-ratified 2026-07-27) decides +> what happens next and in what order; where this file and DIRECTION disagree, +> DIRECTION wins. In particular the F-graph ordering below places a visible +> second agent downstream of F3b-ii/F7, and names a public agent selector as a +> failure condition — the owner reversed both when ratifying Wave 1, which +> shipped the multi-agent console first. Treat those passages as superseded. +> This file remains authoritative for **F-graph node definitions and +> dependency edges** once Wave 2 opens. + +This is the technical roadmap, product-gate, correction, and release detail for #391. Decision 28 owns durable invariants. The #805 fleet plan is the sole source for implementation node definitions, dependency edges, and replacement Bead acceptance; diagrams here are non-normative mirrors. diff --git a/docs/issues/909/plan.md b/docs/issues/909/plan.md index 25125781c..cfb2cd88b 100644 --- a/docs/issues/909/plan.md +++ b/docs/issues/909/plan.md @@ -9,6 +9,17 @@ parent: 905 # gh-909 AgentGateway v0 — the definitive gateway plan +> **Kind: historical record.** This plan executed and closed. It governs +> nothing today. +> - **Sequencing** is owned by [`docs/DIRECTION.md`](../../DIRECTION.md). +> - **The gateway contract** is owned by +> [`packages/agent/docs/AGENT_GATEWAY_V0.md`](../../../packages/agent/docs/AGENT_GATEWAY_V0.md), +> colocated with its types. **§6 below drifted from the shipped code** (input +> DTOs nest `scope` rather than inheriting it, among other differences) and is +> retained only as the pre-implementation record. Do not build from §6. +> - **Architecture rulings** are owned by [`docs/DECISIONS.md`](../../DECISIONS.md) +> (Decision 29 records what this plan shipped). + One frozen contract (`AgentGateway`), one canonical construction path (`createAgentHost()`), all consumers aligned onto both. This is the grunt-work spine extracted from #905: after it lands, every remaining concern — durable @@ -49,7 +60,12 @@ the sandbox network firewall, never inside). Our tiers: 3. **Shared host + sandbox-executed author tools** — our extension beyond eve; a density/economics play, reserved until marketplace demand (§9). -**Third-party consumption topology (owner-ratified).** "Own host" means the +**Third-party consumption topology (owner-ratified) — FUTURE WAVE, NOT SHIPPED.** +The tiers below are target architecture owned by **#905** (DIRECTION Wave 4), +not part of v0. `AgentHostProtocol` appears in no source file today; hosts +currently split only into embedded (in-process) with remote/external deferred. +Recorded here for provenance; #905 is where it is planned. + "Own host" means the agent's own *compute plane*, never the user's console: ```txt @@ -207,6 +223,11 @@ storage are internals. ## 6. Exact specs (v0) +> **Superseded as a build target.** Written before implementation; the shipped +> types diverge. Build against +> [`packages/agent/docs/AGENT_GATEWAY_V0.md`](../../../packages/agent/docs/AGENT_GATEWAY_V0.md) +> and `packages/agent/src/shared/gateway/`. Retained for provenance. + Public Gateway types live in `packages/agent/src/shared/gateway/` (no `node:*`, `Buffer`, Fastify, Pi SDK, or React imports). Server-only Host construction and HTTP projection types live in `packages/agent/src/server/agent-host/`. diff --git a/packages/agent/docs/AGENT_GATEWAY_V0.md b/packages/agent/docs/AGENT_GATEWAY_V0.md new file mode 100644 index 000000000..edf9c719d --- /dev/null +++ b/packages/agent/docs/AGENT_GATEWAY_V0.md @@ -0,0 +1,152 @@ +# AgentGateway v0 — the session contract + +**Kind:** technical contract. **Governs:** the gateway surface only. +**Does not govern:** sequencing (see `docs/DIRECTION.md`), architecture rulings +(see `docs/DECISIONS.md`), or history (see `docs/issues/909/plan.md`). + +This file supersedes `docs/issues/909/plan.md` §6 as the binding description of +the gateway contract. §6 was written before implementation and drifted; it is +retained there as a historical record, not as a build target. + +Source of truth is the code: `packages/agent/src/shared/gateway/types.ts` and +`errors.ts`. If this file and those files disagree, the code wins and this file +is a bug. Keep them together — that colocation is the point. The v0 contract +lived in an issue folder nobody updated, and it silently stopped matching the +types within one release. + +## Scope discipline + +`AuthorizedAgentScope` is an **issuer-owned runtime capability, not a transport +DTO**. It carries a `unique symbol` brand, so it cannot be forged by spreading +an object across a boundary. It must be re-checked against issuer identity and +current membership on every use — possession is not authorization. + +```ts +interface AuthorizedAgentScope { + readonly workspaceScopeId: WorkspaceScopeId + readonly authSubjectId: AuthSubjectId + readonly [authorizedAgentScope]: true // brand +} +``` + +Only the app-owned verifier returns identity facts, as +`VerifiedAgentScopeClaim`. `AgentSessionRef` deliberately carries **no +`hostId`** — session refs are `{ agentTypeId, sessionId }` and nothing else. + +## The 7 methods (+ close) + +```ts +interface AgentGateway { + listAgents(input: ListAgentsInput): Promise + listSessions(input: AuthorizedAgentSessionQuery): Promise + createSession(input: CreateAgentSessionInput): Promise + connectSession(input: ConnectAgentSessionInput): Promise + readSessionState(input: ReadAgentSessionStateInput): Promise + renameSession(input: RenameAgentSessionInput): Promise + deleteSession(input: DeleteAgentSessionInput): Promise + close(): Promise +} +``` + +### Input DTOs — scope is NESTED + +**This is the correction.** §6 froze the scope fields as inherited +(`interface CreateAgentSessionInput extends AuthorizedAgentScope`). The shipped +contract nests scope under its own field on **every** input: + +```ts +interface ListAgentsInput { scope } +interface AuthorizedAgentSessionQuery{ scope; agentTypeId?; cursor?; limit? } +interface CreateAgentSessionInput { scope; agentTypeId; requestId; title? } +interface ConnectAgentSessionInput { scope; ref; cursor? } +interface ReadAgentSessionStateInput { scope; ref } +interface RenameAgentSessionInput { scope; ref; requestId; title } +interface DeleteAgentSessionInput { scope; ref; requestId } +``` + +Nesting keeps the branded capability a single indivisible value instead of +scattering its fields across every DTO, which is why callers adopted it. Code +written against §6's inherited form does not typecheck. + +Note `listAgents` takes `ListAgentsInput`, not a bare `AuthorizedAgentScope`. + +### Output DTOs + +`AgentSummary` (`agentTypeId`, `label`, `description?`, `definition?{version,digest}`), +`AgentSessionSummary` (`ref`, `title`, `status`, `createdAt`, `updatedAt`), +`AgentSessionPage` (`sessions`, `nextCursor?`), and the receipts +(`CommandReceipt`, `AgentSendReceipt`, `QueueClearReceipt`, `StopReceipt`) are +unchanged from §6, field for field. + +Session activity is the extracted alias `AgentSessionActivity`: +`'idle' | 'running' | 'aborting' | 'error'`. + +`AgentSessionStateSnapshot.state` is **`JsonSafe`**, not the raw +snapshot §6 named — the transport boundary is expressed in the type. + +`JsonSafe` itself differs from §6's version: it hoists `unknown extends T` to +the top, maps functions to `never`, and terminates non-object/non-array at +`never`. Read it from `types.ts`; do not reimplement it from memory. + +### Commands + +`AgentPromptCommand` and `AgentFollowUpCommand` both require +**`{ requestId, clientNonce, content }`** — there is no `text` or `kind:'user'` +shorthand. Prompts add `displayContent?`, `model?`, `thinkingLevel?`, +`attachments?`; follow-ups add `clientSeq`. + +## Error codes + +Thirteen, exhaustive, order-stable, exported as both a const map and +`AGENT_GATEWAY_ERROR_CODES`: + +`AGENT_TYPE_UNKNOWN` · `AGENT_SESSION_NOT_FOUND` · `AGENT_SCOPE_DENIED` · +`AGENT_SESSION_REPLAY_GAP` · `AGENT_SESSION_CURSOR_AHEAD` · +`AGENT_SESSION_CURSOR_EXPIRED` · `AGENT_SESSION_CURSOR_INVALID` · +`AGENT_REQUEST_CONFLICT` · `AGENT_REQUEST_OUTCOME_UNKNOWN` · +`AGENT_COMMAND_INVALID_STATE` · `AGENT_SESSION_RUNTIME_SCOPE_MISMATCH` · +`AGENT_SHARED_ENVIRONMENT_UNAVAILABLE` · `AGENT_GATEWAY_CLOSED` + +HTTP mapping lives in `server/agent-host/httpProjection.ts` +(`statusForGatewayError`). Note `AGENT_SESSION_RUNTIME_SCOPE_MISMATCH`, +`AGENT_REQUEST_CONFLICT` and `AGENT_COMMAND_INVALID_STATE` project to **409**. + +## Conformance levels + +- **Level B** — bounded replay within a process lifetime, plus snapshot + rehydrate. This is what ships. A cursor older than the replay window, or one + from before a restart, yields `REPLAY_GAP` / `CURSOR_AHEAD` and the client + refetches state. Never a silent gap. +- **Level D** — durable replay across restarts and processes. Specified, + deliberately skipped, and owner-annotated in + `server/agent-host/testing/gatewayConformance.ts`. Owned by the streaming + lane (#1009). + +The suite is parameterized by `replayLevel`/`paginationLevel`; a Level D host +must pass the same file with the flag raised. + +## Keyset pagination + +Cursors are HMAC-signed and bound to scope and filter; tampering or reuse under +a different filter yields `AGENT_SESSION_CURSOR_INVALID`. Ordering is total, so +a session mutated between pages shifts predictably rather than vanishing. + +## Known descope + +§10 of the 909 plan carries an unchecked acceptance box demanding recursive +JSON/size/depth validation of attachment and path leaves on the transport +boundary. §6.4 descoped exactly that to v2, and the conformance suite skips it +(`v2 remote wire validates JSON event leaves, paths, depth, and size`). The +plan is internally inconsistent on this point; the descope is what shipped. +Anything relying on recursive leaf validation must not assume it exists. + +## Construction + +`createAgentHost()` is the single construction funnel — see +`server/agent-host/createAgentHost.ts` and Decision 29. It is a +deployment-static composition helper that returns a gateway and a `close()`; it +is **not** the AgentHost deployment/publication machinery that Decisions 25–28 +rejected. Future internal/external host tiers belong to #905, not here. + +`scripts/check-alignment-invariants.mjs` enforces that nothing outside an +allowlist calls it.