The PARA Obsidian vault (arcodange/SecondBrain — git-synced, sb.py digest/inbox/gitea-ingest jobs on the hermes cron ticker, local Ornith model, mcp-obsidian access) enters the PRD as a first-class component: new T17 knowledge capture & retrieval fiche, knowledge-layer section in the architecture (ERP = book of record, vault = context + institutional memory, append-only idempotent deposits, trusted-but-stale retrieval), hermes/Ornith recognized as the resident M4 runtime (D2 leaning, new D7 cluster<->vault access decision), foundation ledger row, diagram + goals updated (mermaid revalidated, 231 links/anchors re-checked green). Co-Authored-By: Claude Fable 5 <[email protected]>
14 KiB
vibe > PRD > AI back-office > Agent architecture
Agent architecture — atoms, contracts, gates
Status: In design Last Updated: 2026-07-11 Up: AI back-office hub Related: Task inventory · Model fleet · Challenges · ADR 0003 — sandbox state lifecycle
Design principles
- 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".
- 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.
- Data is never instructions. Inbound content (mails, PDFs, bank labels) flows through typed fields; extraction atoms hold zero credentials and zero action tools.
- Writes are rehearsed, gated, and replayable. The only path to prod mutation is manifest → sandbox rehearsal → human approval → gated promote (ADR 0003).
- Silence is an alert. Every standing loop heartbeats; a quiet fleet must be provably quiet, not possibly dead.
- Earn autonomy. Levels (A0–A3) are granted per-atom from measured evals and revoked on incident (QA strategy).
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 (A0–A3) + link to the eval evidence. |
model_policy |
Preferred tier, fallbacks, escalation rule (model fleet). |
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→T03, POC-1):
%%{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
- A new message on
books@is classified by the T01 sentinel (Pi tier, schema-constrained output). - The PDF is extracted twice independently — locally on the M4 and on the Mistral EU cloud.
- Critical fields (amounts, IBAN, ref, dates) must agree exactly; disagreement escalates to the Claude tier; still-ambiguous items stop here.
- Deterministic validators check arithmetic, VAT rates, SIREN/IBAN checksums, and duplicates; any failure lands in the quarantine queue, surfaced in the digest.
- A write manifest is rehearsed on the sandbox and its result re-read and compared to the draft (predicted-delta check).
- The human gets a Telegram approval card; approval triggers the gated promote to prod (human-held key + explicit confirm).
- 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 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=valuebusiness-key lookups (aborts on 0 or >1 match — never guesses ids). - Gated promote:
promote-plan(human-readable review) →promote-apply --target prodrequiring 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), 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_agentkey. 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— thePERMISSION_SCOPESpattern prototyped in erptest/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
.envfiles 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:
- No-tool extraction: atoms that read untrusted content can only emit schema-constrained JSON — there is nothing to hijack.
- Typed handoffs: downstream atoms receive extracted fields, never raw document text; the raw source travels as an opaque attachment (hash-addressed) for human eyes.
- 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.
- Action allowlists: outbound mail only to allowlisted recipients; calendar mutations sourced from mail content require human confirmation (T11).
- Injection fixtures in evals: every extraction atom's golden set includes adversarial documents; a regression here blocks autonomy promotion (QA strategy).
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, T17 vault capture/retrieval, interactive Claude Code sessions (the atom factory) | hermes cron ticker (already driving the vault jobs) + on-wake queue drain | Not a server: availability model in model fleet; time-critical work must not depend on it. hermes = the local agent runtime (skills, cron, the Ornith model). |
| 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).
Knowledge layer — the second brain
The operator's second brain is already in place and already agent-integrated: a PARA Obsidian vault (00-Inbox … 06-Zettel), git-synced to the forge (arcodange/SecondBrain) via obsidian-git, exposed to agents through mcp-obsidian (local REST API), and automated by .automation/sb.py (weekly digest, inbox triage, daily prefill, idempotent Gitea→Inbox ingest) scheduled on the hermes cron ticker — with Ornith, hermes's local reasoning model (127.0.0.1:18080), as the confidential/offline lane. The vault even declares its own AI routing doctrine — Claude by default, Mistral for well-defined tasks, Ornith/hermes for the confidential — which is precisely the policy the model fleet generalizes.
The integration contract (T17):
- Division of truth: the ERP is the book of record; the vault is context and institutional memory (contract nuances, client history, decisions, REX). No accounting fact is authoritative in the vault.
- Capture: fleet outputs worth remembering land as append-only inbox/area notes with idempotent frontmatter — the pattern the Gitea ingest already proves; human-authored notes are never edited in place.
- Retrieval: context-hungry atoms query the vault and carry facts with their note dates — notes are trusted-but-stale: anything contradicting the ERP, or older than its subject's last change, triggers re-verification rather than belief.
- Rails reused, not rebuilt: M4-side access is direct filesystem +
mcp-obsidian; the weekly digest and the human's PARA filing ritual remain the curation loop; cluster-side access is open decision D7.
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 for cluster-triggered jobs + hermes for M4-side lanes (leaning — hermes already runs skills + cron there) 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) |
| 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) |
| D7 | Cluster↔vault access | git clone/pull of the SecondBrain remote (leaning — the Gitea remote exists, offline-friendly, reviewable) vs. tunneled Obsidian REST API (M4-only today) vs. keeping vault access M4-exclusive |
D4–D6 close with their mapped POCs (POC-6, POC-5, POC-2); D1–D2 are settled while building phase 3's standing fleet (the queue and scheduler are its skeleton); D3 lands with phase 4's money loops; D7 closes when the first cluster-side atom needs vault context (phase 3 at the earliest).