Squash merge PR #546. Local evidence: doc-release, smoke, snapshot gate, pre-push Bats, snapshot Bats, structural gates, and pre-push --fast passed. PR GitHub Actions were blocked in runner setup by GitHub codeload/action download failures before repository code executed. Snapshot records current corpus as DECAYING rather than preserving stale compounding evidence.
PER PRE-MORTEM AMENDMENT A2 (soc-8inr.5): introduce a single registration
choke-point for mutation HTTP routes plus a build-time guard that asserts
no other code path bypasses it.
- registerMutationRoute(mux, path, policyFn, handler) wraps handler in
AuthorizeMutationDecision and atomically appends path to the tracked
mutationRoutes slice. Decision is stashed in request context so handlers
can read TokenName for ledger actor attribution without re-running auth.
- registerReadOnlyRoute keeps read-only registrations in auth.go too, so
ALL mux.HandleFunc callsites in cli/internal/daemon/ live in one file
(the single audit surface).
- Existing /jobs, /v1/jobs, /jobs/cancel, /v1/jobs/cancel, and
/openclaw/v1/triggers/jobs migrate to registerMutationRoute. Read-only
/health, /ready, /status, /events, /openclaw/v1/* migrate to
registerReadOnlyRoute. Behavior is preserved including 405 Method Not
Allowed semantics (auth wrapper passes non-mutation methods through to
the handler so requireMethod can still return 405).
- New /v1/schedules routes:
- POST /v1/schedules creates a RecurringJobTemplate (admin capability).
- GET /v1/schedules lists schedules — read-only, no auth.
- DELETE /v1/schedules/{name} removes a schedule (admin capability).
Validation: malformed JSON / missing fields / invalid cron return 400.
Duplicate names return 409.
- mutationPathInSet + mutationCapabilityForPath add "/foo/*" wildcard
support so DELETE /v1/schedules/{name} can be authorized without
enumerating every name in AllowedPaths.
- Default legacy mutation tokens now carry MutationCapabilityAdmin (in
addition to submit/cancel/openclaw_trigger). Single-token mode is
local-only by construction; scoped tokens that need a narrower scope
must use the JSON token-file format.
- scripts/check-mutation-route-coverage.sh greps cli/internal/daemon/
for raw mux.HandleFunc callsites outside auth.go and fails the gate
if any are found. Wired into scripts/pre-push-gate.sh as an always-on
check (security gate; not skipped on local fast mode).
Tests:
- TestRegisterMutationRoute_AppendsAndWraps: helper appends to tracked
list, rejects unauthenticated POST, runs handler with decision in
context on valid token.
- TestMutation_AllRegisteredRoutesEnforcePolicy: parity test — every path
in MutationRoutesForTest() returns 403 without a token.
- TestMutation_GetSchedulesIsReadOnly: GET /v1/schedules is 200 without
auth and is NOT in the tracked mutation slice.
- TestRoute_PostSchedules_Creates / DuplicateNameReturns409 /
MalformedJSONReturns400 / MissingFieldsReturns400.
- TestRoute_GetSchedules_ReturnsList.
- TestRoute_DeleteSchedule_Removes / AuthRequired.
Closes soc-8inr.5.