From d18ce049593444f6e46be782cce01a7e7f7cf096 Mon Sep 17 00:00:00 2001 From: fred Date: Sat, 29 Aug 2026 20:00:54 -0500 Subject: [PATCH] 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