Repository navigation
feat(api): gate auto-purchase arming behind execute:purchases - #841
Conversation
Arming a plan (enabled + auto_purchase + non-NULL max_step_commitment), raising an armed plan's cap, or flipping the global kill switch off->on now requires execute:purchases (carved out of admin:*), on POST/PUT/PATCH plans and PUT /api/config (inside the atomic closure, after the RI-exchange grant). The stateless admin API key is refused for these escalations; disarming, lowering a cap and switching off stay on the ordinary verbs. Switch changes are stamped (who, when, credential kind); plan changes are logged. Reject max_step_commitment below 0.01 or above 1e15 with 400 (the column is NUMERIC(18,2); a sub-cent cap or 1e308 was a 500). Raise detection compares cent-rounded values. Refs #631 Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Gate review (Opus 5.5,
|
Caps with more than two decimals are now rejected (400) instead of rounded: Postgres rounds the decimal pgx sends, so 1.005 was stored as 1.01 while the gate compared 1.00 and let an admin raise a cap without execute:purchases. StepCapCents parses the decimal text, so the compared and stored values are identical (proved on a real PG17 round trip). Also: stamp credential kind is "session" in production vocabulary, the kill-switch change is logged after the commit, and the cap bounds const no longer splits the validator's doc comment. Refs #631 Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Gate re-review (Opus 5.5,
|
Refs #631 (R1b: kill-switch grant; follows R1a #840; R2 stacks on this).
What
Arming now needs
execute:purchases(already carved out of admin:*): a plan going not-armed -> armed (planArmed= enabled && auto_purchase && max_step_commitment != nil) or an armed plan's cap rising, on POST, PUT and PATCH /api/plans, regardless of switch state (plan handlers do not read global_config); andauto_purchase_enabledoff -> on in PUT /api/config, inside theUpdateGlobalConfigAtomicclosure withbeforefromexisting, after the RI-exchange grant.The stateless admin API key is refused (403) for those escalations; it skips every permission check, so reusing the verb alone would not stop it. Disarm, lower cap and switching off stay allowed with the ordinary verbs, including the admin key.
Switch changes write the R1a stamps (who, when, credential kind); plan arming/cap changes emit a structured log line (actor, credential kind, plan id, armed and cap old -> new). No plan stamp columns.
Cap validation:
max_step_commitmentmust be within [0.01, 1e15] and have at most 2 decimal places (parsed from the decimal text viaStepCapCents; a half-cent cap was stored rounded by Postgres while the gate compared the unrounded value). Stored == compared proved on real PG17. Before, 0.004 rounded to 0 and violated the column CHECK, and 1e308 overflowed NUMERIC(18,2): both were 500s. Raise detection compares cent-rounded values.Not changed: the RI-exchange auto-mode path has the same admin-key hole; follow-up issue, out of scope here.
Owner decision (N4, left as is):
PUT /api/plans/{id}/accountsand term/payment/coverage edits on an armed plan need onlyupdate:plans(they change what a plan buys, not the per-step ceiling).Tests
auto_purchase_grant_test.godrives patchPlan/updatePlan/createPlan/updateConfig with mock auth that answers from the realauth.AuthContext(admin:* alone is denied execute:purchases; admin + execute:purchases allowed; admin API key). Cases: arm, enable, disarm, rename, NULL cap not armed, cap raise/lower/same/sub-cent/one-cent, create, switch on/off, unchanged switch not stamped. Fixture/mock-based evidence, labelled as such.json:"-"; gate follow-up from feat(config): add auto-purchase switch stamp columns (migration 000115) #840).>->>=in the raise check; drop the admin-key refusal (two variants).Verification
go build ./..., go test ./internal/api ./internal/config ./internal/server, golangci-lint (0 issues), gofmt, gocyclo hook, git-secrets. No database cluster was needed (no SQL change). Size is about 450 lines of which ~300 are tests.
🤖 Generated with Claude Code
Summary by CodeRabbit
Access Controls
Validation
Auditability