Skip to main content

9. Cross-cutting concerns

Each policy below is read from YAML. The file is named in brackets.

  • Human approval and checkpoints [workflows/*.yaml]: the human.approval kind pauses the graph. Sessions live in workflow_sessions through the SeaORM storage (§7.6), and the approvals table 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 own max_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-only audit_log with the trace id.
  • Authentication and roles [config/{env}.yaml auth, rbac.yaml]: Loco verifies the JWT. Roles and permissions come from rbac.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 has tenant_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 scans config/ for high-entropy strings. OpenBao (MPL-2.0) is an optional later addition.
  • Prompt injection [prompts/*.yaml guards]: 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 through always_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 a max_concurrency in 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.