Skip to content

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.

SurfaceCommand
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 resourcebmo://goal-contracts (list), bmo://goal-contracts/{id} (one)
Agent toollist_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.

SurfaceCommand
Createbmo contract create
Listbmo contract list
Getbmo contract get <id>
Pause / resumebmo contract pause <id> / bmo contract resume <id>
Extend expirybmo contract extend <id>
Soft-deletebmo 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.

A run is constrained by a goal contract when any of these supply a goal_contract_id:

  • Schedulerbmo 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 preflightgoals.Preflight.Check is the single entry point; every other surface composes it.

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_at deny with denied_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.

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):

Terminal window
bmo logs --tail 1000 | jq -c 'select(.msg|startswith("goal_contract."))'
Terminal window
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.

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.BudgetCaps veto runs once exceeded (denied_budget). Rails-level Budgets.MaxRunsPerDay and BlastRadius.BudgetCaps.MaxWallTime are hard caps debited by the scheduler counter store.
  • Phase 5 budget gap (advisory only): subprocess MaxInputTokens is 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.
  • 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.