docs: agent enrollment command family v1 design (M4-4-0)
ci/woodpecker/pr/ci Pipeline was successful

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.
This commit is contained in:
fred
2026-08-29 19:37:16 -05:00
parent 94d626dff9
commit 4753e285eb
2 changed files with 194 additions and 0 deletions
@@ -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 00180020
(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).
+4
View File
@@ -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