Goal contracts
Goal contracts
Section titled “Goal contracts”Goal contracts answer what is this run actually allowed to do? They are operator-authored, append-only versioned envelopes around bounded delegation. A contract names a goal, fixes the rails (allowed tools, paths, budgets), and sets an expiry. Agents read contracts but never author or mutate them; the runtime enforces them through a single preflight choke point.
Quick inspect
Section titled “Quick inspect”| Surface | Command |
|---|---|
| CLI (full JSON) | bmo config show-goal-contracts |
| CLI (single contract) | bmo config show-goal-contracts --id <id> |
| TUI | /contracts (dialog) or sidebar Contracts: ok / attention |
| MCP resource | bmo://goal-contracts (list), bmo://goal-contracts/{id} (one) |
| Agent tool | list_goal_contracts, get_goal_contract (read-only) |
bmo config show-goal-contracts mirrors bmo contract list --format=json
contract bodies and adds an attention envelope (paused, expiring within 7d) the TUI sidebar uses; piping to jq .attention matches what the sidebar
renders.
Author and operate
Section titled “Author and operate”| Surface | Command |
|---|---|
| Create | bmo contract create |
| List | bmo contract list |
| Get | bmo contract get <id> |
| Pause / resume | bmo contract pause <id> / bmo contract resume <id> |
| Extend expiry | bmo contract extend <id> |
| Soft-delete | bmo contract delete <id> |
All authoring lives in bmo contract *. Goal contracts are distinct from
change contracts (the per-edit obligation surface — see
/contract-obligations); the two systems do not share state or commands.
Activation paths
Section titled “Activation paths”A run is constrained by a goal contract when any of these supply a
goal_contract_id:
- Scheduler —
bmo schedule run-recipe ... --goal-contract <id>(preflight blocks unknown / paused / expired contracts and rejects budget caps). - Plan envelopes — a plan template that names a contract activates rail narrowing on every tool call within the envelope.
- Direct preflight —
goals.Preflight.Checkis the single entry point; every other surface composes it.
Enforcement
Section titled “Enforcement”The runtime, not the agent, honors:
- Kill switch (paused) — paused contracts deny immediately and take precedence over any unit-level rail narrowing in the plan envelope.
- Expiry — contracts past
authorship.expires_atdeny withdenied_expired. - Budgets — counter store debits are checked against rails per-window;
exhaustion denies with
denied_budget. - Allowed tools / paths — declarative rails on the contract; tool gates call back through the preflight to evaluate them.
Observability
Section titled “Observability”Every contract decision emits a goal_contract.fired event followed by a
goal_contract.action event with a bounded outcome enum:
legacy_no_contract · allowed · denied_unknown · denied_paused ·
denied_expired · denied_budget · denied_pinned_version · error_store
Filter examples (full recipe set lives in Agent tracing):
bmo logs --tail 1000 | jq -c 'select(.msg|startswith("goal_contract."))'bmo logs --tail 1000 | jq -c 'select(.msg=="goal_contract.action" and (.outcome|startswith("denied_")))'The single choke-point design means scheduler / plan / CLI all share the same audit trail; there is no per-surface duplicate emit.
Containment contract (Phase 5)
Section titled “Containment contract (Phase 5)”Goal contracts are an opt-in containment tier. The fail-closed invariants — honored uniformly by scheduler, plan envelope, and CLI preflight — are:
- Unknown / paused / expired contracts deny at preflight (
denied_unknown,denied_paused,denied_expired). There is no caller-side override. - Budget caps in
BlastRadius.BudgetCapsveto runs once exceeded (denied_budget). Rails-levelBudgets.MaxRunsPerDayandBlastRadius.BudgetCaps.MaxWallTimeare hard caps debited by the scheduler counter store. - Phase 5 budget gap (advisory only): subprocess
MaxInputTokensis not observable across the process boundary today, so token caps log a Warn and permit the run rather than fail closed. Treat per-token budgets as advisory guidance until in-process telemetry lands. - Goal vs change contracts: goal contracts gate runs at the rail level
(tools, paths, budgets, expiry); change contracts (
/contract-obligations) gate edits at the file level. The two systems share no state and use separate slash commands.
What goal contracts do not do
Section titled “What goal contracts do not do”- They do not author themselves — agents have no create/update/delete surface, and prompts cannot extend or pause a contract.
- They do not replace change contracts. Change contracts gate edits at the file level; goal contracts gate runs at the rail level.
- They do not ship an HTTP route this iteration. Inspect goes through
CLI and MCP resources; mutation goes through
bmo contract *. The parity matrix carries a documented exception. - They do not fail closed on subprocess token budgets — see the Phase 5 gap above.