design rev 2: GLM review findings 1-8
ci/woodpecker/pr/ci Pipeline was canceled

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.
This commit is contained in:
fred
2026-08-29 19:49:49 -05:00
parent 4753e285eb
commit 7bc69286d1
@@ -9,8 +9,9 @@ audience: developer
Status: design note (implementation-facing; amends no contract). Status: design note (implementation-facing; amends no contract).
Authority chain: tool-gateway-mapping.md §3.1 rank-4 row + §4 envelope 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), (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` custody-schema.md §5.2 at revision 13 (agent-grantee FK bound to the
table), PRD §9 D11. Where this note and a ratified contract disagree, 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. the contract wins.
## 1. What the contracts bind (and what they leave open) ## 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 1. **Contract 5 §3.1 rank 4:** "Enroll one agent: harness, credential
reference/API-key intake (values never echoed), name/persona, reference/API-key intake (values never echoed), name/persona,
assignment scope (contract 3 §3.5)." assignment scope (contract 3 §3.5)."
2. **Contract 5 §4:** typed request/result DTOs validated at the Gateway 2. **Contract 5 §4 — all five sub-clauses:** §4.1 typed request/result
boundary; closed per-family error enum (validation, authentication, DTOs validated at the Gateway boundary (expected-version only where
authorization, not-found, conflict, precondition, internal) with HTTP an owning contract defines one); §4.2 closed per-family error enum
mappings; §4.3 audit with trace correlation and idempotency keys. (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 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 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 family, and is skippable. Wizard witness §6.10: a run that skips the
step produces zero enrollment-family mutations. step produces zero enrollment-family mutations.
4. **Custody-schema rev 4:** contract 7's agent-grantee FK references 4. **Custody-schema §5.2 (rev 13; binding introduced at rev 4):**
the live `agents` table (`agents.id`, uuid); an enrollment surface contract 7's agent-grantee FK references the live `agents` table
with its own table would force a contract-7 amendment. (`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 **Assignment scope (open point, pinned here):** the rank-4 row cites
contract 3 §3.5, which defines no assignment semantics; the PRD's full 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 assignment (multi-account, comms auto-enroll, workspace placement) is
deferred with the rest of the PRD's full flow (D11); when a contract 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. 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`) ## 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, NULL), owner_id → users, system_prompt, allowed_tools, skills,
is_system, config jsonb, timestamps. No harness column (provider and is_system, config jsonb, timestamps. No harness column (provider and
model describe the LLM backend, not the harness), no audit coupling. model describe the LLM backend, not the harness), no audit coupling.
- Sole write path: `packages/brain/src/agents.ts` repository, consumed - Sole write path: `packages/brain/src/agents.ts` repository (the only
by the legacy `/api/agents` CRUD controller module issuing `insert(agents)`), with three write consumers: the
(`apps/gateway/src/agent/agent-configs.controller.ts`). 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` - Sealed credential store exists: `ProviderCredentialsService`
(apps/gateway/src/agent/) — one row per (userId, provider), values (apps/gateway/src/agent/) — one row per (userId, provider), values
sealed at rest, decrypt server-side only, summaries never carry 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):** **F1 — contract-5 mapping note (disposition, not an amendment):**
`/api/agents` appears nowhere in contract 5 — neither as a P0 row nor in `/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 the §3.2 legacy non-substitutes list (the ruled §3.2 freeze names
design treats it as a frozen legacy surface: no new consumer is added to specific endpoints, and `/api/agents` is not among them). The operative
it, and the rank-4 command family is the P1 path for enrollment. Adding constraints are §3.3's amendment-only rule for new mapping rows and §5's
the missing P0 row is a contract amendment for a future S2 pass; nothing closure rule: this design adds no new consumer to `/api/agents` and
in M4-4 depends on it. 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) ## 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): Request DTO (shared types package, class-validator at the boundary):
| Field | Type | Rule | | Field | Type | Rule |
| ---------------- | ------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | ---------------- | ------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `harness` | string | MUST name a registered harness (harness registry); else `validation_failed` | | `harness` | string | syntactically invalid (empty/malformed) → `validation_failed`; well-formed but not in the harness registry → `precondition_failed` |
| `name` | string | non-empty, trimmed, ≤ 200 chars | | `correlationId` | string (uuid) | optional; generated when absent (contract 5 §4.3); carried into audit events and returned in the result |
| `persona` | string \| null | optional; stored as the agent's system prompt | | `replayMode` | 'actor-bound' \| 'shared' | optional, default `actor-bound` (contract 3 §4.3) |
| `model` | string | non-empty (provider-qualified model id) | | `name` | string | non-empty, trimmed, ≤ 200 chars |
| `provider` | string | non-empty; names the credential's provider | | `persona` | string \| null | optional; stored as the agent's system prompt |
| `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 | | `model` | string | non-empty (provider-qualified model id) |
| `idempotencyKey` | string (uuid) | required (§4.3) | | `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: Rules:
1. **Never echoed.** The credential value appears in no result DTO, no 1. **Never echoed.** The credential value appears in no result DTO, no
audit event, no outbox payload, and no log line. The result carries audit event, no outbox payload, and no log line. The result carries
only `{ provider, credentialMode }`. only `{ provider, credentialMode }`.
2. **Intake = the existing sealed store.** `intake` writes through 2. **Intake = the existing sealed store, inside the transaction.**
`ProviderCredentialsService.store` (seal-at-rest, upsert per `intake` writes through the sealed-store path
(userId, provider)); enrollment persists no second copy and no (`ProviderCredentialsService.store` semantics: seal-at-rest, upsert
plaintext. 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 3. **Reference must resolve.** `reference` with no stored credential for
(actor, provider) refuses with `precondition_failed` (nothing is (actor, provider) refuses with `precondition_failed` (nothing is
created). created).
@@ -109,17 +142,41 @@ Rules:
is AuthGuard-authenticated user; no hierarchy grant is required is AuthGuard-authenticated user; no hierarchy grant is required
because v1 enrollment binds no hierarchy node (§1 assignment-scope because v1 enrollment binds no hierarchy node (§1 assignment-scope
pin). `is_system` is never settable through this command. pin). `is_system` is never settable through this command.
5. **Idempotency (§4.3).** Replay with the same key and same actor 5. **Idempotency fence (contract 3 §4.3, in full).** The command layer
returns the original result; same key with a different payload records, in a uniqueness-constrained fence table in the same
refuses with `conflict`. 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` + 6. **Audit + outbox, same transaction.** Insert into `agents` +
semantic audit event (`agent.enrolled`: actor, agent id, harness, sealed credential write (intake mode) + fence row + semantic audit
provider, name, credentialMode — no credential material) + outbox row event (`agent.enrolled`: actor, agent id, harness, provider, name,
commit atomically, hierarchy-pattern style. 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. Result union: `enrolled { agent, correlationId }` | refusal from the
`agent` in the result is the persisted row minus nothing sensitive (the §3.3 enum (refusals also carry the correlation id, per contract 5
table stores no credential material). §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) ### 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 · `validation_failed` 400 · `authentication_failed` 401 ·
`authorization_refused` 403 (owner-only paths; folded to `not_found` `authorization_refused` 403 (owner-only paths; folded to `not_found`
where §3.2 applies) · `not_found` 404 · `conflict` 409 (idempotency-key where §3.2 applies) · `not_found` 404 · `conflict` 409 (the single
payload mismatch) · `precondition_failed` 422 (unresolvable credential bounded idempotency refusal shape of §3.1 rule 5) · `precondition_failed`
reference; unknown harness beyond syntactic validation) · 422 (unresolvable credential reference; well-formed harness not in the
`internal_fault` 500. 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) ## 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: FK binding without amendment:
- `harness` text NULL — registered harness name; NULL for pre-existing - `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, New tables, mirroring the hierarchy audit/outbox pair (pattern reuse,
separate store): `agent_audit_events` (append-only: id, event_type, separate store): `agent_audit_events` (append-only: id, event_type,
actor id, agent id, correlation id, causation id, idempotency key, actor id, agent id — snapshot value, no FK, per §3.1 rule 6 —
payload jsonb, created_at; per-agent ordering index) and `agent_outbox` correlation id, causation id, payload jsonb, created_at; per-agent
(hierarchy-outbox shape). Persona reuses the existing `system_prompt` ordering index), `agent_outbox` (hierarchy-outbox shape), and
column; no version column (no ratified expected-version rule names `agent_idempotency_fence` (contract 3 §4.3 shape: operation identifier,
`agents` — §4.1 binds only where the owning contract defines one). 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, Witnesses (real PostgreSQL, lane standard): append-only enforcement,
same-tx atomicity (agent row + audit + outbox all-or-nothing under same-tx atomicity (agent row + credential write + fence row + audit +
injected failure), idempotency uniqueness. 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 00180020 Sequencing: additive DDL via the same migration path as 00180020
(hierarchy). The KBN-101 §5.3 gate binds KBN-100 kanban DDL, not this (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 `provider_credentials` (sealed), and `agents` has no credential
column at all. column at all.
3. Reference-resolution refusal (`precondition_failed`, no row created). 3. Reference-resolution refusal (`precondition_failed`, no row created).
4. Unknown-harness refusal against the live registry. 4. Harness refusals, both codes: syntactically invalid →
5. Idempotent replay + `conflict` on payload mismatch. `validation_failed`; well-formed registry miss →
6. Same-tx atomicity fault injection (agent/audit/outbox). `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 7. Wizard-facing zero-mutation witness (contract 3 §6.10 shape): no
call → zero rows in `agents`/`agent_audit_events`/`agent_outbox` call → zero rows in `agents`/`agent_audit_events`/`agent_outbox`/
attributable to the family. `agent_idempotency_fence` attributable to the family.
8. `is_system` injection attempt is rejected by DTO validation. 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 ## 6. Out of scope
Wizard orchestration (M4-6); any UI (D8/D12); un-enroll/update lifecycle Wizard orchestration (M4-6); any UI (D8/D12); un-enroll/update lifecycle
(no contract requires it in v1 — the frozen legacy CRUD keeps serving (no contract requires it in v1 — the legacy write surfaces named in §2
existing consumers); OAuth login, multi-account, comms auto-enroll, keep serving existing consumers); OAuth login, multi-account, comms
model recommendation (PRD full flow, deferred by D11); contract auto-enroll, model recommendation (PRD full flow, deferred by D11);
amendments (F1 recorded above for a future S2 pass). contract amendments (F1 recorded above for a future S2 pass). CLI
exposure is explicitly IN scope (witness 10 — contract 5 §4.5 binds it).