mirror of
https://github.com/boshu2/agentops.git
synced 2026-09-14 15:08:13 +08:00
81f864da52
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.
70 lines
2.7 KiB
Bash
Executable File
70 lines
2.7 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# scripts/check-mutation-route-coverage.sh
|
|
#
|
|
# PER PRE-MORTEM AMENDMENT A2 (security, soc-8inr.5): proves that
|
|
# registerMutationRoute is the ONLY path through which mutation HTTP routes
|
|
# are registered in cli/internal/daemon/. A developer could otherwise call
|
|
# mux.HandleFunc("/v1/foo", handler) directly and bypass the auth wrapper
|
|
# entirely.
|
|
#
|
|
# CI gate: this script must exit 0. Wired into scripts/pre-push-gate.sh.
|
|
#
|
|
# Heuristic: every mux.HandleFunc callsite in cli/internal/daemon/ that lives
|
|
# outside auth.go fails the gate. auth.go is the single allowed home for
|
|
# raw mux registration via registerMutationRoute / registerReadOnlyRoute
|
|
# helpers; all other files MUST go through those helpers.
|
|
#
|
|
# Read-only routes registered via registerReadOnlyRoute are intentionally
|
|
# exempt from the auth wrapper but still go through auth.go (single
|
|
# registration choke-point), so this gate covers both.
|
|
|
|
set -euo pipefail
|
|
|
|
if REPO_ROOT="$(git rev-parse --show-toplevel 2>/dev/null)" && [[ -n "$REPO_ROOT" ]]; then
|
|
:
|
|
else
|
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
|
|
fi
|
|
SERVER_DIR="$REPO_ROOT/cli/internal/daemon"
|
|
|
|
if [[ ! -d "$SERVER_DIR" ]]; then
|
|
echo "ERROR: daemon source dir not found: $SERVER_DIR" >&2
|
|
exit 2
|
|
fi
|
|
|
|
# Find all mux.HandleFunc callsites in non-test daemon files. Exclude auth.go
|
|
# (the registration choke-point) and *_test.go (tests may register fixtures).
|
|
violations=$(grep -rEn 'mux\.HandleFunc\b' "$SERVER_DIR" \
|
|
--include='*.go' \
|
|
--exclude='*_test.go' \
|
|
| grep -v '/auth\.go:' \
|
|
| grep -v '^[^:]*:[0-9]\+://' \
|
|
|| true)
|
|
|
|
# Strip pure comment lines (// at start, after optional whitespace).
|
|
violations=$(echo "$violations" | awk -F: '{
|
|
line=""
|
|
for (i=3; i<=NF; i++) line = line (i==3?"":":") $i
|
|
gsub(/^[ \t]+/, "", line)
|
|
if (line ~ /^\/\//) next
|
|
print
|
|
}')
|
|
|
|
if [[ -n "$violations" ]]; then
|
|
echo "ERROR: mux.HandleFunc called outside auth.go in cli/internal/daemon/" >&2
|
|
echo " (registerMutationRoute / registerReadOnlyRoute is the ONLY allowed path)" >&2
|
|
echo "" >&2
|
|
echo "$violations" >&2
|
|
echo "" >&2
|
|
echo "If a route is read-only, register via registerReadOnlyRoute(mux, path, handler)." >&2
|
|
echo "If a route is a mutation, register via registerMutationRoute(mux, path, policy, handler)." >&2
|
|
echo "" >&2
|
|
echo "PER PRE-MORTEM AMENDMENT A2 (soc-8inr.5): direct mux.HandleFunc bypasses" >&2
|
|
echo "the auth wrapper. The gate refuses to merge any route that escapes the helpers." >&2
|
|
exit 1
|
|
fi
|
|
|
|
echo "OK: all mux.HandleFunc callsites in cli/internal/daemon/ live in auth.go (registerMutationRoute / registerReadOnlyRoute scope)"
|
|
exit 0
|