Skip to main content

Auto-Approve — Free vs Pro

QTG’s auto-approve subsystem lets pre-defined movement templates auto-approve small, well-bounded transfers without operator intervention. There are two implementations: Both implement the same AutoApproveHook protocol (src/qtg/domain/protocols.py); the registry (pro_registry.py) selects which one is live.

Free hook: _SimpleCapAutoApproveHook

Source: src/qtg/application/services/free_auto_approve.py

Evaluation order

  1. settings.auto_approve_enabled (env MG_AUTO_APPROVE_ENABLED) → auto_approve_disabled_settings
  2. Template lookup by template_key → template_not_found
  3. Template auto_approve_enabled=True → template_auto_approve_disabled
  4. Template daily_cap_asset == intent.asset → asset_mismatch
  5. Per-request: intent.amount <= daily_cap_amount → amount_exceeded_per_request
  6. 24h rolling SUM (other approved requests for same template, last 24h) + intent.amount <= daily_cap_amount → daily_cap_exceeded
  7. Approved → free_simple_cap_passed

Enabling a template for auto-approve

The toggle and both caps are authority-expanding writes (raising a live cap widens automatic approval), so they change only through a route_template.auto_approve configuration proposal. PATCH /v3/plan-templates/{key} with auto_approve_enabled, daily_cap_amount, or daily_cap_asset in the body returns 409 CONFIG_PROPOSAL_REQUIRED and changes nothing; qtg template patch no longer offers those flags at all. namespace stays directly patchable.
  1. An HMAC admin proposes the complete state from the dashboard (Governance -> Routes -> select the template -> Automatic approval (Free cap)) or via POST /dashboard/configuration-proposals:
    Enabling requires both caps (a positive decimal and the exact movement asset string). Disabling is {"template_key": ..., "auto_approve_enabled": false} and clears the caps.
  2. A different admin applies it under Governance -> Configuration proposals. Apply compares the template’s current toggle/caps with the values captured at proposal time under the same row lock the hook takes; any drift marks the proposal stale and writes nothing.
Unlike Pro policy revisions, a template cap change has no cooling delay: it is usable as soon as it is applied. DB-level CHECK constraint blocks auto_approve_enabled=true with NULL caps (cap_consistency_violation).

TOCTOU note (resolved)

The Free hook acquires SELECT … FOR UPDATE on the MovementPlanTemplate row before evaluating the cap, so concurrent submits on the same template are serialized — the historical overshoot race no longer applies.
  • Test invariant: tests/qtg/test_free_simple_cap_toctou_documented.py asserts the post-commit serialization invariant.
  • Escape hatch: enable the Pro hook (register_auto_approve_hook(AutoApproveHookImpl())) for cross-template / multi-strategy budget serialization via BudgetLedgerEntry.

Pro overrides Free

When Pro is installed its hook is registered at bootstrap and takes precedence; the Free hook is never invoked. The Pro path additionally requires strategy_id and client_id on the movement, because policy lookup keys on both. Pro policies are configuration proposals (auto_approve_policy.create / replace / revoke / activate) made by an HMAC admin via POST /dashboard/configuration-proposals and applied by a different admin under Governance -> Configuration proposals. The dashboard Auto-approval page still calls the retired direct route and receives 409 CONFIG_PROPOSAL_REQUIRED (JEP-1027). A policy names client_id, strategy_id, asset, allowed_templates (exact template_key + template_version pairs), max_amount_per_request, and optional window caps (max_amount_per_window, max_count_per_window, window_hours, default 24). Neither hook reads trusted targets or the policy trace, and neither runs for an agent-originated movement (agent_auto_approve_skipped).

Settings reference