Story 1566: Who governs the agent when nobody is watching
Why this exists (the catastrophe it prevents). The rails (Story-1562) gave every agent a leash. But a leash has to be set by someone — and in an Auto run there is no human in the room to set it. Auto is the extraction worker: a document lands, an event fires, the agent processes a batch overnight, and counsel reads the summary in the morning. Three doors swing open if a firm can't configure that headless run. First, no default: the agent has no Thinking level or review posture to use, so it either fails or falls back to something arbitrary. Second, no spend cap: counsel says "process everything" and wakes to a 500-file batch that silently ran every step at Extra-High and blew the firm's budget — or, worse, a tight budget quietly drops a privilege-detection step below its safety floor to fit, and a privileged document leaks. Third, no firm posture: an enterprise client wants "delegate routine work in trusted matters" expressed once, firm-wide, not re-picked per run. This Story builds the admin/firm configuration that governs all three — and wires it so that the budget governs cost, the floor governs safety, and safety always wins.
firm_policy authority profiles, D8 posture provenance (policy_version), and D11 Auto budget governance with a floor-protected minimum. Target repo = pramaan-functions (+ the admin UI, which is a sibling-repo follow-up — see the scope note at the end; ADR-031 forbids executable frontend changes in this repo).The one analogy: a thermostat with a hard-wired frost guard
Think of the firm config as a building thermostat the office manager sets. The Auto defaults are the schedule it runs on when nobody's touching it — "overnight, run at this setting." The budget ceiling is a cap on the energy bill: spend no more than this. But the thermostat has a frost guard wired behind the panel — a minimum the heat will hold no matter how low you set the budget, because frozen pipes cost more than electricity. If your budget is so tight it can't even afford the frost guard, the thermostat doesn't quietly let the pipes freeze — it flashes a warning and waits for you. The authority profiles are named presets ("Eco", "Comfort") the manager defines once and selects. And the floor list is a label on the panel telling you the frost guard exists and at what temperature — readable, never a dial you can turn off.
🛰️ Satellite: what shipped
Two firm-scoped tables, one alembic migration (0122), a firm-config domain with a router under /app/agent-runtime, and two new pure rails the broker consumes. No model name, price, or token figure crosses any surface — the budget is in abstract cost units.
- Auto-mode defaults (D3) — per-firm default
thinking_level+authority_modefor headless runs. The newauto_posturerail resolvesrequested_levelby precedence (user > matter > firm > agent default) and then floors it:effective_level = max(requested, step_floor). A cost-cutting firm default oflowstill runs privilege detection athigh. - Budget ceiling, floor-protected (D11) — the new
budgetrail. Floors drive a non-negotiablemin_batch_costthe budget can never cap below. A batch over the ceiling chunks + checkpoints + alerts; a batch whose floor alone exceeds the ceiling is held and surfaced (never run below a floor). Never a silent overrun, never a floor violation. - Firm-policy authority profiles (D4) —
business.firm_agent_authority_profiles: named profiles each declaring a concrete base posture. When a run's mode isfirm_policy, the broker resolves through the selected profile. The non-delegable list is not stored here — it is doctrine enforced per-effect, regardless of profile. - Floor list config (D3) —
GET /app/agent-runtime/floorsrenders the declared global floors. Read-only by construction: there is no write endpoint, because floors are additive and can never be removed or lowered by config. - Posture provenance (D8) — a monotonic
policy_versionon the firm policy row, bumped on every material edit, stamped into each applied effect so an effect is auditable months later to whose config authorized it.
✈️ Airplane: how the budget governor decides (D11)
estimate = { item_count, total_cost_units, min_batch_cost_units } <- min is floor-driven
ceiling = firm.budget_ceiling_units (None = unbounded)
resolve_budget(ceiling, estimate):
ceiling is None -> WITHIN_BUDGET (nothing to cap)
min_batch_cost > ceiling -> FLOOR_EXCEEDS_BUDGET (HELD + alert; floor NOT lowered)
total_cost <= ceiling -> WITHIN_BUDGET (run whole, silent)
else (total > ceiling >= min) -> CHUNK_AND_CHECKPOINT (ceil(total/ceiling) chunks + alert)
cost units are ABSTRACT: low=1, medium=2, high=4, extra_high=8 (no $/token/model)
a higher level costs strictly more, so a floor that RAISES a level RAISES its cost too
Four turns make this correct:
- The floor is checked before the budget can chunk. If the floored minimum alone is over the ceiling, the budget is unsatisfiable without lowering a floor — which is forbidden — so the governor returns
FLOOR_EXCEEDS_BUDGETand the batch is held for counsel, never trimmed below the floor to fit. Safety beats cost, structurally. - Over-budget never means silent overrun. A batch above the ceiling is split into ceiling-sized, checkpointed chunks (reusing
document_status_events, D6) with a counsel-facing alert at the review point. The cap throttles throughput, never safety — each chunk still runs its floored steps at floor. - Cost is abstract, so no model or price leaks. The weights are relative units with one required property: strict monotonicity. The admin sees "units", never tokens or rupees (D2), and the broker never has to expose a model name to compute a budget.
- The floor default beats the Auto default. The
auto_posturerail floors the resolved level after precedence, so the firm's headless default can be cheap for ordinary steps yet never for a floored one.
✈️ Airplane: how a firm config row becomes a governed run
admin PUTs /app/agent-runtime/policy -> firm_agent_runtime_policies (1 row/firm, RLS)
{ auto_thinking_level, auto_authority_mode,
active_authority_profile_id, budget_ceiling_units,
budget_default_level, policy_version++ }
# later, a headless Auto run (broker-side, Story-1562 rails):
posture = resolve_auto_posture( <- this Story's rail
firm_level = policy.auto_thinking_level, <- config, never content
firm_authority = policy.auto_authority_mode,
firm_policy_profile_mode = profile.base_authority_mode,
step = "privilege_detection")
requested=low? -> effective = max(low, HIGH) = HIGH (floor wins)
firm_policy -> authority resolves through the selected profile
stamps { config_source: firm, policy_version, floor_applied } (D8)
🔬 Microscopic: the boundary lines that matter
| Detail (titled by its reason) | What & where |
|---|---|
The floor cannot be configured away. The floor surface is read-only — there is a GET /floors and no write route — because floors are additive doctrine, not a firm setting. |
floors.py :: declared_floors(); router.py :: list_agent_floors (no POST/PUT/DELETE). |
A profile can never name firm_policy itself. A profile declares a concrete posture (review_every_step / review_exceptions / authorized); a self-referential firm_policy profile is rejected by a DB CHECK and the DTO enum. |
entity.py CHECK _PROFILE_MODES_SQL; DTO ProfileAuthorityMode. |
The non-delegable list holds under any profile. Even a profile resolving to authorized cannot auto-apply "submit to court" — the per-effect resolver short-circuits on the non-delegable list first, before the mode is consulted. |
authority.py :: resolve_effective_authority; proved in test_auto_posture.py. |
| The active profile is in-use; it can't be deleted. Deleting the firm's selected profile would dangle the policy's reference. Delete is rejected with a typed error (archive instead); a composite FK also blocks it at the DB. | service.py :: AuthorityProfileService.delete_profile; FK fk_firm_agent_runtime_policies_active_profile to (firm_id, id). |
The active profile must belong to the same firm. A firm-leading composite FK (firm_id, active_authority_profile_id) → profiles(firm_id, id) makes a cross-firm reference impossible — tenant isolation enforced by the schema, not just RLS. |
entity.py FirmAgentRuntimePolicy.__table_args__; migration 0122. |
A new firm gets usable defaults, not an empty door. The zero-row first run synthesises medium / review_exceptions / unbounded / version 1 rather than 404-ing, so an Auto run for a never-configured firm has a posture (system-boundary doctrine). |
service.py :: FirmAgentRuntimePolicyService._view (row=None branch); integration test first-run case. |
A no-op PUT does not bump the version. policy_version is stamped into provenance, so it must move only on a material change — re-saving identical settings leaves it untouched (D8). |
service.py :: _apply_updates returns changed; version bumps only when true. |
Budget telemetry carries no firm or content. The governor logs {outcome, units, chunk_count, floor_protected, run_mode} — abstract units only, no firm_id, no document text (ADR-124). |
budget.py :: _emit_budget_telemetry. |
ruff clean; new rail unit tests (test_budget.py, test_auto_posture.py) green; integration tests against a freshly provisioned EMPTY firm cover the zero-row first run, the version bump, the active-profile delete guard, and all three budget outcomes (within / chunk / floor-exceeds); RLS isolation test classifies both new tables as firm-config (support-readable, firm-isolated); alembic base→head + check shows zero drift; OpenAPI app-api.json regenerated and committed.⚡ functions. This Story ships the complete backend contract the admin UI binds to — the /app/agent-runtime routes and the synced openapi/app-api.json mirror — and the UI is a sibling-repo (pramaan-app) follow-up. Per repo doctrine, the cross-repo executable split is surfaced here rather than silently bundled.One-sentence recall
When no human is watching, the firm config sets the agent's Auto defaults and a spend ceiling — but the safety floor is wired behind the panel: the budget can throttle the work into chunks, never below the floor, and if the floor alone costs more than the budget, the batch waits for counsel rather than running cheap.