9. Cross-cutting concerns
Each policy below is read from YAML. The file is named in brackets.
- Human approval and checkpoints [
workflows/*.yaml]: thehuman.approvalkind pauses the graph. Sessions live inworkflow_sessionsthrough the SeaORM storage (§7.6), and theapprovalstable holds interrupt state and expiry. Resume works across restarts because the session id equals the AG-UI thread id. - Idempotency: documents are deduplicated by tenant and SHA-256, invoices by tenant, vendor and invoice number. Connector calls carry an idempotency key from the workflow YAML. Resume replays are no-ops through the unique interrupt id.
- Retries [
workflows/*.yaml, per node]: backoff applies only to retryable errors (network, 429, 5xx). Loco workers have their ownmax_retries. Each workflow sets a task timeout and a step limit. - Audit [
policies.yaml]: every task transition, LLM call (model id, token counts, prompt hash), approval decision and connector call is written to a hash-chained, append-onlyaudit_logwith the trace id. - Authentication and roles [
config/{env}.yamlauth,rbac.yaml]: Loco verifies the JWT. Roles and permissions come fromrbac.yaml. Approvals need the roles named on the approval node, and four-eyes means the approver differs from the person who started the run. Keycloak (Apache-2.0) can act as the identity provider. - Multi-tenancy [
tenants/*.yaml]: every table hastenant_id. Queries use Loco's.in_tenant()helper, and Postgres row-level security is a second layer. Per-tenant files override thresholds, enabled workflows and connector choices. - Secrets: never in YAML. Files reference them with
get_env. A CI check scansconfig/for high-entropy strings. OpenBao (MPL-2.0) is an optional later addition. - Prompt injection [
prompts/*.yamlguards]: document text is fenced as data, output is constrained by the JSON schema, and deterministic rules run after the model. The model has no tool that moves money; posting happens only after approval. A bank-detail change always forces approval throughalways_require_when. - PII and retention [
policies.yaml]: prompt bodies are not exported to traces by default. Listed fields are hashed or redacted. Only whitelisted state reaches the browser. A scheduled task purges parsed text after the configured number of days. - Rate limits [
policies.yaml]: per-tenant and per-user limits are applied as a router layer, and each LLM provider has amax_concurrencyin settings. - Evaluation [
evals/]: 200+ labelled invoices, field-level accuracy per model and prompt version, prompt-injection canary documents, and golden AG-UI event sequences. CI blocks a merge when accuracy drops by more than two points.