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_unmetered_runner ·
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 / active plan session / 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, active plan sessions, 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).BlastRadius.BudgetCaps.MaxRunsPerDayandBlastRadius.BudgetCaps.MaxWallTimeare hard caps debited by the shared counter store. - Hard token caps fail closed: when a contract declares
BlastRadius.BudgetCaps.MaxInputTokens, the runtime must report trustworthy input-token usage. Scheduler-backed in-process runs do this through the shared token sink, and active plan sessions debit provider usage frames into the same counter store; runners without observable usage are denied before launch withdenied_unmetered_runnerinstead of silently degrading the cap. - 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 add subprocess IPC metering in this iteration. Unmetered
runners still return zero-signal
RunUsage; the containment change is the fail-closed deny on hard token-cap contracts, not cross-process usage transport.
Related
Section titled “Related”- Plan contracts — typed envelopes on plan Markdown that materialize as goal contracts
- Goal & plan contract internals
- Change contracts (
/contract-obligations) - Agent tracing recipes