docs: agent enrollment command family v1 design (M4-4-0)
ci/woodpecker/pr/ci Pipeline was successful
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:
@@ -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).
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user