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
settings.auto_approve_enabled(envMG_AUTO_APPROVE_ENABLED) →auto_approve_disabled_settings- Template lookup by
template_key→template_not_found - Template
auto_approve_enabled=True→template_auto_approve_disabled - Template
daily_cap_asset == intent.asset→asset_mismatch - Per-request:
intent.amount <= daily_cap_amount→amount_exceeded_per_request - 24h rolling SUM (other approved requests for same template, last 24h)
+ intent.amount <= daily_cap_amount→daily_cap_exceeded - 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 aroute_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.
-
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. -
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
staleand writes nothing.
auto_approve_enabled=true with NULL caps
(cap_consistency_violation).
TOCTOU note (resolved)
The Free hook acquiresSELECT … 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.pyasserts the post-commit serialization invariant. - Escape hatch: enable the Pro hook
(
register_auto_approve_hook(AutoApproveHookImpl())) for cross-template / multi-strategy budget serialization viaBudgetLedgerEntry.
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 requiresstrategy_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).