From 4753e285eb6957a4f159654c7e1cc32983a92a84 Mon Sep 17 00:00:00 2001 From: fred Date: Sat, 29 Aug 2026 19:37:16 -0500 Subject: [PATCH 1/3] docs: agent enrollment command family v1 design (M4-4-0) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Design note for the rank-4 enrollment command family: contract composition (contract 5 rank-4 row + envelope, contract 3 §3.5, custody-schema rev 4 FK binding), assignment-scope pin per D11, measured current state, command surface with closed error enum, additive schema delta plan (0021), and the witness list the implementation slice must ship. Amends no contract. --- ...6-08-29-agent-enrollment-command-design.md | 190 ++++++++++++++++++ docs/plans/README.md | 4 + 2 files changed, 194 insertions(+) create mode 100644 docs/plans/2026-08-29-agent-enrollment-command-design.md diff --git a/docs/plans/2026-08-29-agent-enrollment-command-design.md b/docs/plans/2026-08-29-agent-enrollment-command-design.md new file mode 100644 index 00000000..f2359dee --- /dev/null +++ b/docs/plans/2026-08-29-agent-enrollment-command-design.md @@ -0,0 +1,190 @@ +--- +kind: spec +status: active +audience: developer +--- + +# Agent Enrollment Command Family — v1 Design (M4-4-0) + +Status: design note (implementation-facing; amends no contract). +Authority chain: tool-gateway-mapping.md §3.1 rank-4 row + §4 envelope +(ruled 2026-08-27), onboarding-wizard.md §3.5 (D11 minimal enrollment), +custody-schema.md rev 4 (agent-grantee FK bound to the live `agents` +table), PRD §9 D11. Where this note and a ratified contract disagree, +the contract wins. + +## 1. What the contracts bind (and what they leave open) + +There is no standalone enrollment contract. The rank-4 family is defined +by composition: + +1. **Contract 5 §3.1 rank 4:** "Enroll one agent: harness, credential + reference/API-key intake (values never echoed), name/persona, + assignment scope (contract 3 §3.5)." +2. **Contract 5 §4:** typed request/result DTOs validated at the Gateway + boundary; closed per-family error enum (validation, authentication, + authorization, not-found, conflict, precondition, internal) with HTTP + mappings; §4.3 audit with trace correlation and idempotency keys. +3. **Contract 3 §3.5:** the wizard's enrollment step is minimal (one + harness, API-key login, agent name and persona — D11), uses ONLY this + family, and is skippable. Wizard witness §6.10: a run that skips the + step produces zero enrollment-family mutations. +4. **Custody-schema rev 4:** contract 7's agent-grantee FK references + the live `agents` table (`agents.id`, uuid); an enrollment surface + with its own table would force a contract-7 amendment. + +**Assignment scope (open point, pinned here):** the rank-4 row cites +contract 3 §3.5, which defines no assignment semantics; the PRD's full +enrollment vision (Part I, Standalone flow) includes "account +assignment", but the D11 v1 slice is exactly "one harness, API key, +name/persona". v1 therefore scopes assignment to the two bindings the +minimal slice already implies — the enrolling user becomes the agent's +owner (`agents.owner_id`), and the credential reference names which of +that user's stored provider credentials the agent uses. Richer +assignment (multi-account, comms auto-enroll, workspace placement) is +deferred with the rest of the PRD's full flow (D11); when a contract +defines it, this family extends by ordinary amendment of the design. + +## 2. Current state (measured 2026-08-29 at `origin/next` = `94d626df`) + +- `agents` table (packages/db `schema.ts`): id uuid PK, name, provider, + model, status enum, project_id (legacy `projects`, ON DELETE SET + NULL), owner_id → users, system_prompt, allowed_tools, skills, + is_system, config jsonb, timestamps. No harness column (provider and + model describe the LLM backend, not the harness), no audit coupling. +- Sole write path: `packages/brain/src/agents.ts` repository, consumed + by the legacy `/api/agents` CRUD controller + (`apps/gateway/src/agent/agent-configs.controller.ts`). +- Sealed credential store exists: `ProviderCredentialsService` + (apps/gateway/src/agent/) — one row per (userId, provider), values + sealed at rest, decrypt server-side only, summaries never carry + values. +- Harness registry exists (`apps/gateway/src/harness/`), the validation + source for the harness field. +- Implementation pattern: the merged hierarchy module (M4-1) — + transaction-scoped command context, in-tx authorization, discriminated + result unions, same-transaction semantic audit event + transactional + outbox, no-oracle not_found folding. + +**F1 — contract-5 mapping note (disposition, not an amendment):** +`/api/agents` appears nowhere in contract 5 — neither as a P0 row nor in +the §3.2 legacy non-substitutes list. Under Ruling 5's freeze, this +design treats it as a frozen legacy surface: no new consumer is added to +it, and the rank-4 command family is the P1 path for enrollment. Adding +the missing P0 row is a contract amendment for a future S2 pass; nothing +in M4-4 depends on it. + +## 3. Command family surface (v1) + +One command, one query. Module: `apps/gateway/src/enrollment/` +(`enrollment.module.ts`), mirroring the hierarchy module's shape. + +### 3.1 `agent.enroll` (mutation) + +Request DTO (shared types package, class-validator at the boundary): + +| Field | Type | Rule | +| ---------------- | ------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `harness` | string | MUST name a registered harness (harness registry); else `validation_failed` | +| `name` | string | non-empty, trimmed, ≤ 200 chars | +| `persona` | string \| null | optional; stored as the agent's system prompt | +| `model` | string | non-empty (provider-qualified model id) | +| `provider` | string | non-empty; names the credential's provider | +| `credential` | discriminated union | `{ mode: 'reference' }` — a credential for (actor, provider) MUST already exist; `{ mode: 'intake', type: 'api_key', value: string }` — value is sealed into the credential store in the same flow | +| `idempotencyKey` | string (uuid) | required (§4.3) | + +Rules: + +1. **Never echoed.** The credential value appears in no result DTO, no + audit event, no outbox payload, and no log line. The result carries + only `{ provider, credentialMode }`. +2. **Intake = the existing sealed store.** `intake` writes through + `ProviderCredentialsService.store` (seal-at-rest, upsert per + (userId, provider)); enrollment persists no second copy and no + plaintext. +3. **Reference must resolve.** `reference` with no stored credential for + (actor, provider) refuses with `precondition_failed` (nothing is + created). +4. **Ownership.** `owner_id` = the authenticated actor. v1 authorization + is AuthGuard-authenticated user; no hierarchy grant is required + because v1 enrollment binds no hierarchy node (§1 assignment-scope + pin). `is_system` is never settable through this command. +5. **Idempotency (§4.3).** Replay with the same key and same actor + returns the original result; same key with a different payload + refuses with `conflict`. +6. **Audit + outbox, same transaction.** Insert into `agents` + + semantic audit event (`agent.enrolled`: actor, agent id, harness, + provider, name, credentialMode — no credential material) + outbox row + commit atomically, hierarchy-pattern style. + +Result union: `enrolled { agent }` | refusal from the §3.3 enum. +`agent` in the result is the persisted row minus nothing sensitive (the +table stores no credential material). + +### 3.2 `agent.enrollment.get` (query) + +By agent id; actor must be the owner (or admin). Unauthorized and +missing fold to the same `not_found` wire shape (contract 2 +no-existence-oracle rule, applied family-wide for uniformity). + +### 3.3 Error enum (closed, §4.2) + +`validation_failed` 400 · `authentication_failed` 401 · +`authorization_refused` 403 (owner-only paths; folded to `not_found` +where §3.2 applies) · `not_found` 404 · `conflict` 409 (idempotency-key +payload mismatch) · `precondition_failed` 422 (unresolvable credential +reference; unknown harness beyond syntactic validation) · +`internal_fault` 500. + +## 4. Schema delta (migration 0021, additive-only) + +Extend `agents` — no new agent table, preserving custody-schema rev 4's +FK binding without amendment: + +- `harness` text NULL — registered harness name; NULL for pre-existing + rows (legacy rows predate the concept). +- `enrolled_at` timestamptz NULL — set by `agent.enroll`; NULL marks a + legacy (non-enrolled) row. No backfill: enrollment is a fact this + command creates, not one to invent for existing rows. + +New tables, mirroring the hierarchy audit/outbox pair (pattern reuse, +separate store): `agent_audit_events` (append-only: id, event_type, +actor id, agent id, correlation id, causation id, idempotency key, +payload jsonb, created_at; per-agent ordering index) and `agent_outbox` +(hierarchy-outbox shape). Persona reuses the existing `system_prompt` +column; no version column (no ratified expected-version rule names +`agents` — §4.1 binds only where the owning contract defines one). + +Witnesses (real PostgreSQL, lane standard): append-only enforcement, +same-tx atomicity (agent row + audit + outbox all-or-nothing under +injected failure), idempotency uniqueness. + +Sequencing: additive DDL via the same migration path as 0018–0020 +(hierarchy). The KBN-101 §5.3 gate binds KBN-100 kanban DDL, not this +lane; if the pending ASKS-5 ruling changes migration mechanics +lane-wide, re-check before generating 0021. + +## 5. Witnesses the implementation slice must ship + +1. Never-echo: enroll via `intake`, assert the value string is absent + from the HTTP result, the audit row, the outbox payload, and captured + logs. +2. Sealed-store single-copy: after intake, the credential exists only in + `provider_credentials` (sealed), and `agents` has no credential + column at all. +3. Reference-resolution refusal (`precondition_failed`, no row created). +4. Unknown-harness refusal against the live registry. +5. Idempotent replay + `conflict` on payload mismatch. +6. Same-tx atomicity fault injection (agent/audit/outbox). +7. Wizard-facing zero-mutation witness (contract 3 §6.10 shape): no + call → zero rows in `agents`/`agent_audit_events`/`agent_outbox` + attributable to the family. +8. `is_system` injection attempt is rejected by DTO validation. + +## 6. Out of scope + +Wizard orchestration (M4-6); any UI (D8/D12); un-enroll/update lifecycle +(no contract requires it in v1 — the frozen legacy CRUD keeps serving +existing consumers); OAuth login, multi-account, comms auto-enroll, +model recommendation (PRD full flow, deferred by D11); contract +amendments (F1 recorded above for a future S2 pass). diff --git a/docs/plans/README.md b/docs/plans/README.md index 28d9056d..73818187 100644 --- a/docs/plans/README.md +++ b/docs/plans/README.md @@ -13,6 +13,10 @@ status: active - [Documentation structure README implementation](2026-08-10-docs-structure-readme.md) — completed implementation plan for the documentation contract and atlas. - [Documentation catalog and truth audit](2026-08-10-docs-catalog-audit.md) — audit method, evidence statuses, deliverables, and acceptance criteria. +## Feature design plans + +- [Agent enrollment command design](2026-08-29-agent-enrollment-command-design.md) — v1 rank-4 enrollment command family: contract composition, command surface, schema delta, witnesses (M4-4-0). + After a plan is delivered, update the canonical guide, contract, decision, or index. Do not cite a plan as proof that intended behavior shipped. ## Related -- 2.54.0 From 7bc69286d111919506607bac5a637b460b83d543 Mon Sep 17 00:00:00 2001 From: fred Date: Sat, 29 Aug 2026 19:49:49 -0500 Subject: [PATCH 2/3] design rev 2: GLM review findings 1-8 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Contract 5 §4 composed in full (fail-closed §4.4, CLI parity §4.5, correlation id returned in result); idempotency re-attributed to contract 3 §4.3 (via §7 item 4) and designed to the full fence/replay envelope; harness error-code split fixed; full write-consumer set; intake credential write joins the transaction; audit linkage pinned FK-free; custody-schema cited at rev 13; witnesses 9-11 added (correlation, CLI parity, fail-closed) plus the full §4.3 replay set. --- ...6-08-29-agent-enrollment-command-design.md | 214 +++++++++++++----- 1 file changed, 154 insertions(+), 60 deletions(-) diff --git a/docs/plans/2026-08-29-agent-enrollment-command-design.md b/docs/plans/2026-08-29-agent-enrollment-command-design.md index f2359dee..9dafc443 100644 --- a/docs/plans/2026-08-29-agent-enrollment-command-design.md +++ b/docs/plans/2026-08-29-agent-enrollment-command-design.md @@ -9,8 +9,9 @@ audience: developer Status: design note (implementation-facing; amends no contract). Authority chain: tool-gateway-mapping.md §3.1 rank-4 row + §4 envelope (ruled 2026-08-27), onboarding-wizard.md §3.5 (D11 minimal enrollment), -custody-schema.md rev 4 (agent-grantee FK bound to the live `agents` -table), PRD §9 D11. Where this note and a ratified contract disagree, +custody-schema.md §5.2 at revision 13 (agent-grantee FK bound to the +live `agents` table — a binding introduced at rev 4 and standing +verbatim), PRD §9 D11. Where this note and a ratified contract disagree, the contract wins. ## 1. What the contracts bind (and what they leave open) @@ -21,17 +22,33 @@ by composition: 1. **Contract 5 §3.1 rank 4:** "Enroll one agent: harness, credential reference/API-key intake (values never echoed), name/persona, assignment scope (contract 3 §3.5)." -2. **Contract 5 §4:** typed request/result DTOs validated at the Gateway - boundary; closed per-family error enum (validation, authentication, - authorization, not-found, conflict, precondition, internal) with HTTP - mappings; §4.3 audit with trace correlation and idempotency keys. +2. **Contract 5 §4 — all five sub-clauses:** §4.1 typed request/result + DTOs validated at the Gateway boundary (expected-version only where + an owning contract defines one); §4.2 closed per-family error enum + (validation, authentication, authorization, not-found, conflict, + precondition, internal) with HTTP mappings; §4.3 audit linkage — the + envelope contributes correlation: every request accepts/generates a + correlation id, carried into the audit events **and returned in the + result**, with no second audit stream; §4.4 fail-closed — an + operation that cannot evaluate its authorization or reach its owning + tool refuses, never degrading to a fallback read or direct data + access; §4.5 CLI parity — the family MUST be invocable through the + official CLI against the same Gateway commands with the same + request/result/error contracts (a Gateway command without CLI + exposure is a tracked conformance gap). + **Idempotency keys are NOT contract 5 §4.3:** the idempotency-key + envelope is contract 3 §4.3, ratified as a drafting addition to + contract 5 §4's command envelope via contract 3 §7 item 4. Its fence + and replay rules bind as written there; §3.1 rule 5 below designs to + them. 3. **Contract 3 §3.5:** the wizard's enrollment step is minimal (one harness, API-key login, agent name and persona — D11), uses ONLY this family, and is skippable. Wizard witness §6.10: a run that skips the step produces zero enrollment-family mutations. -4. **Custody-schema rev 4:** contract 7's agent-grantee FK references - the live `agents` table (`agents.id`, uuid); an enrollment surface - with its own table would force a contract-7 amendment. +4. **Custody-schema §5.2 (rev 13; binding introduced at rev 4):** + contract 7's agent-grantee FK references the live `agents` table + (`agents.id`, uuid); an enrollment surface with its own table would + force a contract-7 amendment. **Assignment scope (open point, pinned here):** the rank-4 row cites contract 3 §3.5, which defines no assignment semantics; the PRD's full @@ -44,6 +61,10 @@ that user's stored provider credentials the agent uses. Richer assignment (multi-account, comms auto-enroll, workspace placement) is deferred with the rest of the PRD's full flow (D11); when a contract defines it, this family extends by ordinary amendment of the design. +The deferral rests on contract 3 §3.5's explicit delegation of +enrollment specifics to this family — not on reading the D11 list as +exhaustive (it is not: the §3.1 `model`/`provider` fields are required +by the live table's NOT NULL columns, though D11 does not name them). ## 2. Current state (measured 2026-08-29 at `origin/next` = `94d626df`) @@ -52,9 +73,14 @@ defines it, this family extends by ordinary amendment of the design. NULL), owner_id → users, system_prompt, allowed_tools, skills, is_system, config jsonb, timestamps. No harness column (provider and model describe the LLM backend, not the harness), no audit coupling. -- Sole write path: `packages/brain/src/agents.ts` repository, consumed - by the legacy `/api/agents` CRUD controller - (`apps/gateway/src/agent/agent-configs.controller.ts`). +- Sole write path: `packages/brain/src/agents.ts` repository (the only + module issuing `insert(agents)`), with three write consumers: the + legacy `/api/agents` CRUD controller + (`apps/gateway/src/agent/agent-configs.controller.ts`), the `/agent +new` chat command (`apps/gateway/src/commands/command-executor.service.ts` + → `brain.agents.create`), and workspace bootstrap + (`apps/gateway/src/workspace/project-bootstrap.service.ts`). All + three keep serving existing consumers; none is touched by M4-4. - Sealed credential store exists: `ProviderCredentialsService` (apps/gateway/src/agent/) — one row per (userId, provider), values sealed at rest, decrypt server-side only, summaries never carry @@ -68,11 +94,13 @@ defines it, this family extends by ordinary amendment of the design. **F1 — contract-5 mapping note (disposition, not an amendment):** `/api/agents` appears nowhere in contract 5 — neither as a P0 row nor in -the §3.2 legacy non-substitutes list. Under Ruling 5's freeze, this -design treats it as a frozen legacy surface: no new consumer is added to -it, and the rank-4 command family is the P1 path for enrollment. Adding -the missing P0 row is a contract amendment for a future S2 pass; nothing -in M4-4 depends on it. +the §3.2 legacy non-substitutes list (the ruled §3.2 freeze names +specific endpoints, and `/api/agents` is not among them). The operative +constraints are §3.3's amendment-only rule for new mapping rows and §5's +closure rule: this design adds no new consumer to `/api/agents` and +builds the rank-4 family as the P1 path for enrollment. Adding the +missing P0 row is a contract amendment for a future S2 pass; nothing in +M4-4 depends on it. ## 3. Command family surface (v1) @@ -83,25 +111,30 @@ One command, one query. Module: `apps/gateway/src/enrollment/` Request DTO (shared types package, class-validator at the boundary): -| Field | Type | Rule | -| ---------------- | ------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `harness` | string | MUST name a registered harness (harness registry); else `validation_failed` | -| `name` | string | non-empty, trimmed, ≤ 200 chars | -| `persona` | string \| null | optional; stored as the agent's system prompt | -| `model` | string | non-empty (provider-qualified model id) | -| `provider` | string | non-empty; names the credential's provider | -| `credential` | discriminated union | `{ mode: 'reference' }` — a credential for (actor, provider) MUST already exist; `{ mode: 'intake', type: 'api_key', value: string }` — value is sealed into the credential store in the same flow | -| `idempotencyKey` | string (uuid) | required (§4.3) | +| Field | Type | Rule | +| ---------------- | ------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `harness` | string | syntactically invalid (empty/malformed) → `validation_failed`; well-formed but not in the harness registry → `precondition_failed` | +| `correlationId` | string (uuid) | optional; generated when absent (contract 5 §4.3); carried into audit events and returned in the result | +| `replayMode` | 'actor-bound' \| 'shared' | optional, default `actor-bound` (contract 3 §4.3) | +| `name` | string | non-empty, trimmed, ≤ 200 chars | +| `persona` | string \| null | optional; stored as the agent's system prompt | +| `model` | string | non-empty (provider-qualified model id) | +| `provider` | string | non-empty; names the credential's provider | +| `credential` | discriminated union | `{ mode: 'reference' }` — a credential for (actor, provider) MUST already exist; `{ mode: 'intake', type: 'api_key', value: string }` — value is sealed into the credential store in the same flow | +| `idempotencyKey` | string (uuid) | required (contract 3 §4.3, ratified into contract 5 §4 via contract 3 §7 item 4) | Rules: 1. **Never echoed.** The credential value appears in no result DTO, no audit event, no outbox payload, and no log line. The result carries only `{ provider, credentialMode }`. -2. **Intake = the existing sealed store.** `intake` writes through - `ProviderCredentialsService.store` (seal-at-rest, upsert per - (userId, provider)); enrollment persists no second copy and no - plaintext. +2. **Intake = the existing sealed store, inside the transaction.** + `intake` writes through the sealed-store path + (`ProviderCredentialsService.store` semantics: seal-at-rest, upsert + per (userId, provider)) **in the same transaction** as the agent + insert — a failure after the credential write rolls everything back, + leaving no orphan credential. Enrollment persists no second copy and + no plaintext. 3. **Reference must resolve.** `reference` with no stored credential for (actor, provider) refuses with `precondition_failed` (nothing is created). @@ -109,17 +142,41 @@ Rules: is AuthGuard-authenticated user; no hierarchy grant is required because v1 enrollment binds no hierarchy node (§1 assignment-scope pin). `is_system` is never settable through this command. -5. **Idempotency (§4.3).** Replay with the same key and same actor - returns the original result; same key with a different payload - refuses with `conflict`. +5. **Idempotency fence (contract 3 §4.3, in full).** The command layer + records, in a uniqueness-constrained fence table in the same + transaction as the mutation and its audit event: the key, the + operation identifier (`agent.enroll`), the acting principal, the + authorization scope, a digest of the canonicalized request payload + (the digest input EXCLUDES the credential value — it covers + provider + credentialMode, never plaintext), the declared replay + mode (`actor-bound` default / `shared`), and a reference to the + committed outcome (the agent id). Fence uniqueness is the pair + (operation identifier, key). **Replay:** a submission whose + (operation, key) is recorded is first authorized exactly as a fresh + submission; then replay-mode, scope, and digest equality are + checked; then **target-result authorization** — the submitter must + hold, at replay time, read authority on the referenced agent row + under §3.2's rule (owner or admin) — in every mode, with + recorded-actor equality as the additional `actor-bound` condition. + A passing replay executes nothing, returns the recorded outcome, + and appends a replay access event (non-mutation audit class: + accessing principal, current correlation id, fence-row reference). + Any equality or authorization failure refuses with the single + bounded `conflict` shape — constant, identifying no record — + preserving the no-existence-oracle rule. 6. **Audit + outbox, same transaction.** Insert into `agents` + - semantic audit event (`agent.enrolled`: actor, agent id, harness, - provider, name, credentialMode — no credential material) + outbox row - commit atomically, hierarchy-pattern style. + sealed credential write (intake mode) + fence row + semantic audit + event (`agent.enrolled`: actor, agent id, harness, provider, name, + credentialMode — no credential material) + outbox row commit + atomically, hierarchy-pattern style. Audit rows reference the agent + by **snapshot id, not FK** — mirroring the hierarchy audit tables' + deliberate FK-free linkage so audit history survives agent deletion + through the legacy CRUD DELETE path. -Result union: `enrolled { agent }` | refusal from the §3.3 enum. -`agent` in the result is the persisted row minus nothing sensitive (the -table stores no credential material). +Result union: `enrolled { agent, correlationId }` | refusal from the +§3.3 enum (refusals also carry the correlation id, per contract 5 +§4.3's end-to-end traceability). `agent` in the result is the persisted +row minus nothing sensitive (the table stores no credential material). ### 3.2 `agent.enrollment.get` (query) @@ -131,14 +188,17 @@ no-existence-oracle rule, applied family-wide for uniformity). `validation_failed` 400 · `authentication_failed` 401 · `authorization_refused` 403 (owner-only paths; folded to `not_found` -where §3.2 applies) · `not_found` 404 · `conflict` 409 (idempotency-key -payload mismatch) · `precondition_failed` 422 (unresolvable credential -reference; unknown harness beyond syntactic validation) · -`internal_fault` 500. +where §3.2 applies) · `not_found` 404 · `conflict` 409 (the single +bounded idempotency refusal shape of §3.1 rule 5) · `precondition_failed` +422 (unresolvable credential reference; well-formed harness not in the +registry — syntactic invalidity is `validation_failed` per the §3.1 +table) · `internal_fault` 500 (also the §4.4 fail-closed class when the +owning tool is unreachable; unauthorized-fallback behavior is +prohibited). ## 4. Schema delta (migration 0021, additive-only) -Extend `agents` — no new agent table, preserving custody-schema rev 4's +Extend `agents` — no new agent table, preserving custody-schema §5.2's FK binding without amendment: - `harness` text NULL — registered harness name; NULL for pre-existing @@ -149,15 +209,22 @@ FK binding without amendment: New tables, mirroring the hierarchy audit/outbox pair (pattern reuse, separate store): `agent_audit_events` (append-only: id, event_type, -actor id, agent id, correlation id, causation id, idempotency key, -payload jsonb, created_at; per-agent ordering index) and `agent_outbox` -(hierarchy-outbox shape). Persona reuses the existing `system_prompt` -column; no version column (no ratified expected-version rule names -`agents` — §4.1 binds only where the owning contract defines one). +actor id, agent id — snapshot value, no FK, per §3.1 rule 6 — +correlation id, causation id, payload jsonb, created_at; per-agent +ordering index), `agent_outbox` (hierarchy-outbox shape), and +`agent_idempotency_fence` (contract 3 §4.3 shape: operation identifier, +key, acting principal, authorization scope, canonicalized-payload +digest, replay mode, committed-outcome reference (agent id), created_at; +UNIQUE (operation identifier, key)). Persona reuses the existing +`system_prompt` column; no version column (no ratified expected-version +rule names `agents` — §4.1 binds only where the owning contract defines +one). Witnesses (real PostgreSQL, lane standard): append-only enforcement, -same-tx atomicity (agent row + audit + outbox all-or-nothing under -injected failure), idempotency uniqueness. +same-tx atomicity (agent row + credential write + fence row + audit + +outbox all-or-nothing under injected failure at multiple points, +including after the credential write), fence uniqueness on +(operation, key). Sequencing: additive DDL via the same migration path as 0018–0020 (hierarchy). The KBN-101 §5.3 gate binds KBN-100 kanban DDL, not this @@ -173,18 +240,45 @@ lane-wide, re-check before generating 0021. `provider_credentials` (sealed), and `agents` has no credential column at all. 3. Reference-resolution refusal (`precondition_failed`, no row created). -4. Unknown-harness refusal against the live registry. -5. Idempotent replay + `conflict` on payload mismatch. -6. Same-tx atomicity fault injection (agent/audit/outbox). +4. Harness refusals, both codes: syntactically invalid → + `validation_failed`; well-formed registry miss → + `precondition_failed` (against the live registry). +5. Idempotency (contract 3 §4.3 set): actor-bound replay returns the + recorded outcome and executes nothing (no new agent/audit/outbox + mutation rows; a replay access event is appended); payload-digest + mismatch, replay-mode mismatch, and different-actor actor-bound + replay each refuse with the single bounded `conflict` shape; a + replay is re-authorized fresh (a submitter whose authorization was + revoked since the original is refused, not replayed). +6. Same-tx atomicity fault injection (agent / credential write / fence + / audit / outbox), including a failure injected after the intake + credential write commits its statement — everything rolls back, no + orphan credential. 7. Wizard-facing zero-mutation witness (contract 3 §6.10 shape): no - call → zero rows in `agents`/`agent_audit_events`/`agent_outbox` - attributable to the family. + call → zero rows in `agents`/`agent_audit_events`/`agent_outbox`/ + `agent_idempotency_fence` attributable to the family. 8. `is_system` injection attempt is rejected by DTO validation. +9. Correlation-id witness (contract 5 §6.3): a correlation id submitted + on `agent.enroll` appears in its audit event(s) and in the result; + the §6.3 static companions (no `any`-typed boundary pass-through; + single audit emitter) apply. +10. CLI-parity witness (contract 5 §6.4): a CLI smoke invocation of + `agent.enroll` and `agent.enrollment.get` against the Gateway + succeeds with the same typed results the web client receives. The + implementation slice therefore SHIPS CLI exposure for both + operations (contract 5 §4.5 — a Gateway command without CLI + exposure is a tracked conformance gap; this design refuses to open + one). +11. Fail-closed witness (contract 5 §6.5): with the owning tool or + grant state unreachable (fault injection), the operation returns + the internal-fault or authorization-refusal class and performs no + fallback read/write. ## 6. Out of scope Wizard orchestration (M4-6); any UI (D8/D12); un-enroll/update lifecycle -(no contract requires it in v1 — the frozen legacy CRUD keeps serving -existing consumers); OAuth login, multi-account, comms auto-enroll, -model recommendation (PRD full flow, deferred by D11); contract -amendments (F1 recorded above for a future S2 pass). +(no contract requires it in v1 — the legacy write surfaces named in §2 +keep serving existing consumers); OAuth login, multi-account, comms +auto-enroll, model recommendation (PRD full flow, deferred by D11); +contract amendments (F1 recorded above for a future S2 pass). CLI +exposure is explicitly IN scope (witness 10 — contract 5 §4.5 binds it). -- 2.54.0 From d18ce049593444f6e46be782cce01a7e7f7cf096 Mon Sep 17 00:00:00 2001 From: fred Date: Sat, 29 Aug 2026 20:00:54 -0500 Subject: [PATCH 3/3] design rev 3: GLM round-2 findings shared replay mode refused (seed-only rule, contract 3 SS4.3/SS7 item 4); concurrency loser semantics and scope-mismatch collision added to rule 5 and witness 5; query-side correlation envelope specified; no-oracle indistinguishability probe added to witness 9; fence authorization scope pinned; SHARED-CONTRACT SS5.3 citation fixed, fleet-side ASKS reference removed. --- ...6-08-29-agent-enrollment-command-design.md | 95 ++++++++++++------- 1 file changed, 63 insertions(+), 32 deletions(-) diff --git a/docs/plans/2026-08-29-agent-enrollment-command-design.md b/docs/plans/2026-08-29-agent-enrollment-command-design.md index 9dafc443..f1ff1175 100644 --- a/docs/plans/2026-08-29-agent-enrollment-command-design.md +++ b/docs/plans/2026-08-29-agent-enrollment-command-design.md @@ -111,17 +111,17 @@ One command, one query. Module: `apps/gateway/src/enrollment/` Request DTO (shared types package, class-validator at the boundary): -| Field | Type | Rule | -| ---------------- | ------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `harness` | string | syntactically invalid (empty/malformed) → `validation_failed`; well-formed but not in the harness registry → `precondition_failed` | -| `correlationId` | string (uuid) | optional; generated when absent (contract 5 §4.3); carried into audit events and returned in the result | -| `replayMode` | 'actor-bound' \| 'shared' | optional, default `actor-bound` (contract 3 §4.3) | -| `name` | string | non-empty, trimmed, ≤ 200 chars | -| `persona` | string \| null | optional; stored as the agent's system prompt | -| `model` | string | non-empty (provider-qualified model id) | -| `provider` | string | non-empty; names the credential's provider | -| `credential` | discriminated union | `{ mode: 'reference' }` — a credential for (actor, provider) MUST already exist; `{ mode: 'intake', type: 'api_key', value: string }` — value is sealed into the credential store in the same flow | -| `idempotencyKey` | string (uuid) | required (contract 3 §4.3, ratified into contract 5 §4 via contract 3 §7 item 4) | +| Field | Type | Rule | +| ---------------- | ------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `harness` | string | syntactically invalid (empty/malformed) → `validation_failed`; well-formed but not in the harness registry → `precondition_failed` | +| `correlationId` | string (uuid) | optional; generated when absent (contract 5 §4.3); carried into audit events and returned in the result | +| `replayMode` | 'actor-bound' | optional, default `actor-bound`. `shared` is seed-only (contract 3 §4.3 binds it to the §3.4 canonical seed key set and "no other operation can carry a shared declaration"; §7 item 4 closes it); a `shared` declaration here is refused `validation_failed`, executes nothing, and records no fence row | +| `name` | string | non-empty, trimmed, ≤ 200 chars | +| `persona` | string \| null | optional; stored as the agent's system prompt | +| `model` | string | non-empty (provider-qualified model id) | +| `provider` | string | non-empty; names the credential's provider | +| `credential` | discriminated union | `{ mode: 'reference' }` — a credential for (actor, provider) MUST already exist; `{ mode: 'intake', type: 'api_key', value: string }` — value is sealed into the credential store in the same flow | +| `idempotencyKey` | string (uuid) | required (contract 3 §4.3, ratified into contract 5 §4 via contract 3 §7 item 4) | Rules: @@ -149,21 +149,35 @@ Rules: authorization scope, a digest of the canonicalized request payload (the digest input EXCLUDES the credential value — it covers provider + credentialMode, never plaintext), the declared replay - mode (`actor-bound` default / `shared`), and a reference to the - committed outcome (the agent id). Fence uniqueness is the pair - (operation identifier, key). **Replay:** a submission whose + mode (always `actor-bound` for this family — the `shared` refusal + in the table above means no shared fence row can exist here; the + column is kept for envelope-shape fidelity and mode-mismatch + collision checks), and a reference to the committed outcome (the + agent id). The recorded **authorization scope** for this family is + pinned to the acting principal's platform-user scope (v1 + authorization is grant-free per rule 4, so the scope is the + authenticated-user identity domain — recorded so the §4.3 + scope-equality check has a defined value). Fence uniqueness is the + pair (operation identifier, key). **Replay:** a submission whose (operation, key) is recorded is first authorized exactly as a fresh submission; then replay-mode, scope, and digest equality are - checked; then **target-result authorization** — the submitter must - hold, at replay time, read authority on the referenced agent row - under §3.2's rule (owner or admin) — in every mode, with - recorded-actor equality as the additional `actor-bound` condition. - A passing replay executes nothing, returns the recorded outcome, - and appends a replay access event (non-mutation audit class: - accessing principal, current correlation id, fence-row reference). - Any equality or authorization failure refuses with the single - bounded `conflict` shape — constant, identifying no record — - preserving the no-existence-oracle rule. + checked (a mismatch on any — including scope — is a collision); + then **target-result authorization** — the submitter must hold, at + replay time, read authority on the referenced agent row under + §3.2's rule (owner or admin) — plus recorded-actor equality + (`actor-bound`). A passing replay executes nothing, returns the + recorded outcome, and appends a replay access event (non-mutation + audit class: accessing principal, current correlation id, + fence-row reference). Any equality or authorization failure refuses + with the single bounded `conflict` shape — constant, identifying no + record — preserving the no-existence-oracle rule. **Concurrency + (contract 3 §4.3's rule, ratified via §7 item 4):** two submissions + with the same (operation, key) serialize on the fence's unique + constraint — exactly one executes; the loser waits for the winner's + transaction, and is then handled as a replay if it committed + (through the full replay path above) or executes afresh if it + aborted. A unique-violation race never surfaces as an unhandled + internal fault. 6. **Audit + outbox, same transaction.** Insert into `agents` + sealed credential write (intake mode) + fence row + semantic audit event (`agent.enrolled`: actor, agent id, harness, provider, name, @@ -184,6 +198,14 @@ By agent id; actor must be the owner (or admin). Unauthorized and missing fold to the same `not_found` wire shape (contract 2 no-existence-oracle rule, applied family-wide for uniformity). +The query carries the same non-state envelope as the mutation +(contract 5 §4.3; contract 3's envelope reconciliation confirms closed +query responses carry it): typed request DTO with an optional +`correlationId` (generated when absent) and a typed result — +`found { agent, correlationId }` | `not_found` (the folded shape, +also carrying the correlation id). Queries take no idempotency key +(the fence binds mutations). + ### 3.3 Error enum (closed, §4.2) `validation_failed` 400 · `authentication_failed` 401 · @@ -227,8 +249,9 @@ including after the credential write), fence uniqueness on (operation, key). Sequencing: additive DDL via the same migration path as 0018–0020 -(hierarchy). The KBN-101 §5.3 gate binds KBN-100 kanban DDL, not this -lane; if the pending ASKS-5 ruling changes migration mechanics +(hierarchy). The docs/native-kanban-sot/SHARED-CONTRACT.md §5.3 DDL +gate binds the kanban lane's audit/proposal DDL, not this lane; if a +pending operator ruling on migration sequencing changes mechanics lane-wide, re-check before generating 0021. ## 5. Witnesses the implementation slice must ship @@ -246,10 +269,15 @@ lane-wide, re-check before generating 0021. 5. Idempotency (contract 3 §4.3 set): actor-bound replay returns the recorded outcome and executes nothing (no new agent/audit/outbox mutation rows; a replay access event is appended); payload-digest - mismatch, replay-mode mismatch, and different-actor actor-bound - replay each refuse with the single bounded `conflict` shape; a - replay is re-authorized fresh (a submitter whose authorization was - revoked since the original is refused, not replayed). + mismatch, replay-mode mismatch, scope mismatch, and different-actor + actor-bound replay each refuse with the single bounded `conflict` + shape; a replay is re-authorized fresh (a submitter whose + authorization was revoked since the original is refused, not + replayed); a `shared` declaration on `agent.enroll` is refused + `validation_failed` with nothing executed and no fence row + recorded (seed-only rule); two concurrent same-(operation, key) + submissions produce exactly one mutation, the loser resolving + through the replay path (no unhandled unique-violation fault). 6. Same-tx atomicity fault injection (agent / credential write / fence / audit / outbox), including a failure injected after the intake credential write commits its statement — everything rolls back, no @@ -260,8 +288,11 @@ lane-wide, re-check before generating 0021. 8. `is_system` injection attempt is rejected by DTO validation. 9. Correlation-id witness (contract 5 §6.3): a correlation id submitted on `agent.enroll` appears in its audit event(s) and in the result; - the §6.3 static companions (no `any`-typed boundary pass-through; - single audit emitter) apply. + the same holds for `agent.enrollment.get`'s result; the §6.3 static + companions (no `any`-typed boundary pass-through; single audit + emitter) apply. §6.3's no-existence-oracle probe: an unauthorized + `agent.enrollment.get` of an existing agent and a get of a + nonexistent id return indistinguishable results. 10. CLI-parity witness (contract 5 §6.4): a CLI smoke invocation of `agent.enroll` and `agent.enrollment.get` against the Gateway succeeds with the same typed results the web client receives. The -- 2.54.0