docs: agent enrollment command family v1 design (M4-4-0) #1481
@@ -0,0 +1,315 @@
|
|||||||
|
---
|
||||||
|
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 §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)
|
||||||
|
|
||||||
|
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 — 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 §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
|
||||||
|
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.
|
||||||
|
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`)
|
||||||
|
|
||||||
|
- `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 (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
|
||||||
|
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 (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)
|
||||||
|
|
||||||
|
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 | 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:
|
||||||
|
|
||||||
|
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, 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).
|
||||||
|
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 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 (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 (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,
|
||||||
|
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, 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)
|
||||||
|
|
||||||
|
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 ·
|
||||||
|
`authorization_refused` 403 (owner-only paths; folded to `not_found`
|
||||||
|
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 §5.2'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 — 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 + 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 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
|
||||||
|
|
||||||
|
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. 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, 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
|
||||||
|
orphan credential.
|
||||||
|
7. Wizard-facing zero-mutation witness (contract 3 §6.10 shape): no
|
||||||
|
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 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
|
||||||
|
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 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).
|
||||||
@@ -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 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.
|
- [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.
|
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
|
## Related
|
||||||
|
|||||||
Reference in New Issue
Block a user