Files
factory/vibe/PRD/ai-back-office/agent-architecture.md
T
arcodangeandClaude Fable 5 169c8debb4 docs(prd): AI back-office — agent fleet for daily admin & accounting
New PRD tree vibe/PRD/ai-back-office/ (hub + 6 leaves + STATUS): task
inventory T01-T16 with mode operatoire, atom/contract architecture on
the ADR-0003 write gate, four-tier model fleet (Claude/Mistral/M4/Pi),
12 challenges with mitigations, POC plan with exit criteria, QA
strategy with autonomy promotion gates. Index row + bidirectional
backlinks (erp guidebook, safe-prod PRD).

Co-Authored-By: Claude Fable 5 <[email protected]>
2026-07-11 14:25:04 +02:00

146 lines
12 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
[vibe](../../README.md) > [PRD](../README.md) > [AI back-office](README.md) > **Agent architecture**
# Agent architecture — atoms, contracts, gates
> **Status:** In design
> **Last Updated:** 2026-07-11
> **Up:** [AI back-office hub](README.md)
> **Related:** [Task inventory](task-inventory.md) · [Model fleet](model-fleet.md) · [Challenges](challenges.md) · [ADR 0003 — sandbox state lifecycle](../../ADR/0003-sandbox-state-lifecycle.md)
## Design principles
1. **Atoms, not monoliths.** Each capability (classify, extract, validate, record, reconcile, report, remind) is one narrow agent with a strict I/O contract. Workflows are compositions of atoms with explicit gates — never one prompt that "does the accounting".
2. **The LLM proposes, code disposes.** Formats, arithmetic, checksums, dedup, and referential integrity are enforced by deterministic validators. A model output that fails validation is quarantined, never auto-corrected.
3. **Data is never instructions.** Inbound content (mails, PDFs, bank labels) flows through typed fields; extraction atoms hold zero credentials and zero action tools.
4. **Writes are rehearsed, gated, and replayable.** The only path to prod mutation is manifest → sandbox rehearsal → human approval → gated promote ([ADR 0003](../../ADR/0003-sandbox-state-lifecycle.md)).
5. **Silence is an alert.** Every standing loop heartbeats; a quiet fleet must be provably quiet, not possibly dead.
6. **Earn autonomy.** Levels ([A0A3](README.md#the-autonomy-ladder)) are granted per-atom from measured evals and revoked on incident ([QA strategy](qa-strategy.md)).
## Atom contract
Every atom is registered in a versioned YAML registry (git) with:
| Field | Meaning |
| --- | --- |
| `name`, `version` | Identity; version bumps on any behavioral change (re-triggers evals). |
| `input_schema` / `output_schema` | JSON Schema; enforced at runtime (constrained decoding where the tier supports it). |
| `invariants` | Deterministic post-conditions (e.g. `HT + TVA == TTC ± 0.01`). |
| `side_effect_class` | `read` · `draft` · `write-sandbox` · `write-prod` · `outbound` — drives which gates apply. |
| `idempotency_key` | How a replay is recognized (e.g. supplier + `ref_supplier` + TTC). |
| `autonomy` | Current earned level (A0A3) + link to the eval evidence. |
| `model_policy` | Preferred tier, fallbacks, escalation rule ([model fleet](model-fleet.md)). |
| `eval_ref` | Golden set + scoring script for this atom. |
The registry is the source of truth for what the fleet may do; an atom absent from the registry does not run.
## The pipeline shape
Every workflow instantiates the same stage skeleton (skipping stages it doesn't need):
**watch → classify → extract → validate → stage → approve → apply → verify → journal**
The flagship instance — supplier invoice end-to-end ([T01](task-inventory.md#t01--mailbox-triage--routing)→[T03](task-inventory.md#t03--supplier-invoice-recording), POC-1):
```mermaid
%%{init: {'theme':'base'}}%%
flowchart TB
mail["Zoho books@<br>new message"]:::src
triage["T01 classify<br>(Pi tier, constrained)"]:::proc
extract1["T02 extract A<br>(M4 local)"]:::proc
extract2["T02 extract B<br>(Mistral EU)"]:::proc
agree{"critical fields<br>agree?"}:::gate
escal["escalate<br>(Claude tier)"]:::proc
valid["deterministic validators<br>arithmetic · rates · SIREN · IBAN · dedupe"]:::gate
quarantine["quarantine queue<br>(review in digest)"]:::store
manifest["T03 manifest + sandbox rehearsal<br>predicted-delta check"]:::proc
card["Telegram approval card"]:::gate
promote["gated promote to prod<br>(human key + confirm)"]:::gate
ged["attach PDF (GED)<br>re-read + snapshot delta"]:::proc
journal["run journal<br>+ golden-set feedback"]:::store
mail --> triage --> extract1
triage --> extract2
extract1 --> agree
extract2 --> agree
agree -- "no" --> escal --> valid
agree -- "yes" --> valid
valid -- "fail" --> quarantine
valid -- "pass" --> manifest --> card --> promote --> ged --> journal
quarantine --> journal
classDef src fill:#2563eb,stroke:#1e40af,color:#fff
classDef proc fill:#059669,stroke:#047857,color:#fff
classDef store fill:#7c3aed,stroke:#6d28d9,color:#fff
classDef gate fill:#b45309,stroke:#92400e,color:#fff
```
1. A new message on `books@` is classified by the **T01 sentinel** (Pi tier, schema-constrained output).
2. The PDF is extracted **twice independently** — locally on the M4 and on the Mistral EU cloud.
3. Critical fields (amounts, IBAN, ref, dates) must **agree exactly**; disagreement escalates to the Claude tier; still-ambiguous items stop here.
4. **Deterministic validators** check arithmetic, VAT rates, SIREN/IBAN checksums, and duplicates; any failure lands in the **quarantine queue**, surfaced in the digest.
5. A **write manifest** is rehearsed on the sandbox and its result re-read and compared to the draft (predicted-delta check).
6. The human gets a **Telegram approval card**; approval triggers the **gated promote** to prod (human-held key + explicit confirm).
7. The source PDF is **attached in the GED** (Dolibarr's document store), the write is verified by re-read + snapshot delta, and the full run is **journaled** — rejections and corrections feed the golden set.
## Write safety (inherited, not reinvented)
[ADR 0003](../../ADR/0003-sandbox-state-lifecycle.md) already delivers the hard part, proven live on the erp repo:
- **Sandbox host-guard**: the write skill structurally refuses any host that is not `erp-sandbox` — a sandbox atom *cannot* mutate prod.
- **Manifests with portable refs**: `@ref` (created earlier in the run) and `#entity:field=value` business-key lookups (aborts on 0 or >1 match — never guesses ids).
- **Gated promote**: `promote-plan` (human-readable review) → `promote-apply --target prod` requiring the prod write key from ENV only (never stored) + an explicit confirm variable.
- **Iso-prod checkpoints**: the sandbox is re-seedable from prod at will, so rehearsals run against *today's* real state.
This PRD adds around it: idempotency keys on every write atom, predicted-delta assertions (rehearse → re-read → compare *before* asking for approval), pre/post snapshots ([T13](task-inventory.md#t13--erp-snapshot--drift-detection)), and approval cards as the human interface to the gate.
## Security model
- **Least privilege per atom.** Extraction and classification atoms hold no credentials at all. Read atoms use the read-only `ai_agent` key. Sandbox writes use the sandbox-only agent. The prod write key exists only in the human's hands at promote time.
- **Ephemeral scoped ERP workers.** For orchestrated batches, the orchestrator mints short-lived Dolibarr users scoped to the subtask (`supplier-ingest`, `bank-reconciler`, `readonly` — the `PERMISSION_SCOPES` pattern prototyped in erp `test/orchestratorExample.ts` + `test/scripts/admin/permissions.ts`), and deletes them when the batch ends. A leaked worker key is narrow and already dead.
- **Secrets discipline.** All standing credentials live in Vault (house pattern, VSO-injected); skill `.env` files are mode-600 and gitignored; agents never echo credentials into journals or prompts.
- **Blast-radius honesty.** Bank access is read-only by construction (no payment-initiation scopes are ever requested). The mailbox OAuth is read-only. The single irreversible surface is prod ERP writes — hence the gate.
## Prompt-injection defenses
Inbound documents are adversarial by default — an invoice PDF or a mail body can contain text addressed to an LLM. Defense in depth:
1. **No-tool extraction**: atoms that read untrusted content can only emit schema-constrained JSON — there is nothing to hijack.
2. **Typed handoffs**: downstream atoms receive extracted *fields*, never raw document text; the raw source travels as an opaque attachment (hash-addressed) for human eyes.
3. **Instruction-shaped content is a finding**: validators flag imperative/LLM-addressed text in extracted fields; such items are quarantined and surfaced verbatim to the human.
4. **Action allowlists**: outbound mail only to allowlisted recipients; calendar mutations sourced from mail content require human confirmation ([T11](task-inventory.md#t11--compliance-calendar--reminders)).
5. **Injection fixtures in evals**: every extraction atom's golden set includes adversarial documents; a regression here blocks autonomy promotion ([QA strategy](qa-strategy.md)).
## Runtimes & scheduling
| Runtime | Runs | Scheduling | Notes |
| --- | --- | --- | --- |
| **k3s cluster (Pis)** | T01 sentinel inference, T11 reminders, T13/T14 verifications, queue + gateway | CronJobs + long-running Deployments (ArgoCD apps per the lab's `<app>` join-key convention) | Proven pattern: the erp backup CronJob. No LLM heavier than the Pi tier. |
| **M4 MacBook** | T02/T16 local extraction, T09 report, interactive Claude Code sessions (the atom factory) | opportunistic — on-wake/launchd + queue pull | **Not a server**: availability model in [model fleet](model-fleet.md); time-critical work must not depend on it. |
| **Cloud APIs** | Mistral extraction/OCR; Claude reasoning steps (headless `claude -p` / Agent SDK) | invoked by pipeline stages | Budget-capped; degraded modes defined. |
| **telegram-gateway** | digests, approval cards, human commands | webhook-driven | Roadmapped phases (durable Postgres queue, async handlers) are exactly what the fleet needs — see open decisions. |
**Work queue.** Pipeline stages communicate through a durable queue with dead-letter semantics (an item that fails N times parks in the DLQ and appears in the digest). Start minimal; the queue technology is an open decision below.
**Graduation path.** New atoms are prototyped as Claude Code skills (fast iteration, human in the loop), then frozen into deterministic scripts + tests once stable — the house already does this (`.claude/skills/` scripts wrapped by `bin/arcodange`). Claude-tier involvement in a mature atom shrinks to escalation handling.
## Human channel
- **One daily digest** (Telegram, morning): items awaiting approval, quarantined items, aging unresolved work, heartbeat summary, upcoming deadlines (D-30/D-7/D-1). An empty day still sends "all green" — silence must be distinguishable from failure.
- **Approval cards**: one decision per card (approve / edit / reject-with-reason); rejection reasons are first-class data feeding golden sets.
- **Escape hatch**: every automated lane has a documented manual runbook fallback (the fleet augments the operator; it never becomes the only way to run the company).
## Open decisions
To be settled by POC evidence, each closing with a short ADR:
| # | Decision | Options (leaning) |
| --- | --- | --- |
| D1 | Work queue | telegram-gateway's planned Postgres durable queue (**leaning** — already roadmapped, transactional, one less system) vs. flat files in git vs. Redis |
| D2 | Orchestration runtime | Claude Agent SDK headless on cluster-triggered jobs (**leaning**) vs. bespoke TS orchestrator (erp `test/` Deno codebase) vs. pure CronJobs + scripts |
| D3 | KM monthly invoice firing | enable Dolibarr template auto-fire (`frequency>0`) vs. agent-fired via sandbox+promote (**leaning** — keeps the gate + mention audit in-line) |
| D4 | PDP (e-invoicing platform) | shortlist + Dolibarr 22 module compatibility test on sandbox — **must close before 2026-09-01** ([C12](challenges.md#c12--e-invoicing-reform-unknowns)) |
| D5 | OCR provider for scanned docs | Mistral OCR (EU cloud) vs. local vision model on M4 vs. Tesseract baseline |
| D6 | Pi inference serving | llama.cpp server vs. Ollama on arm64, resource limits, node pinning ([C5](challenges.md#c5--slm-capability-ceiling-on-pi-hardware)) |
D4D6 close with their mapped POCs ([POC-6](poc-plan.md#poc-6--e-invoicing-readiness-spike), [POC-5](poc-plan.md#poc-5--model-routing-bench), [POC-2](poc-plan.md#poc-2--pi-sentinel)); D1D2 are settled while building phase 3's standing fleet (the queue and scheduler *are* its skeleton); D3 lands with phase 4's money loops.