feat(agent-actions): add an operator route for the global kill-switch - #2412
Conversation
setGlobalAgentFrozen (the write side of the DB-backed global agent kill-switch, documented as "an operator flips with one row, no redeploy") had zero callers anywhere in src/ — no API route, no MCP tool, no admin surface. The only way to actually flip global_agent_controls.frozen was a direct SQL statement against D1. Found while fixing #2125: closing that issue's fail-open observability gap doesn't help much if there's no application-level way to set the switch in the first place. Add GET/POST /v1/app/kill-switch, gated by the same requireAppRole(..., ["operator"]) check used by the other operator-only routes: - GET returns the current { frozen, updatedAt, updatedBy } via a new getGlobalAgentFrozenState — a strict, non-fail-open read distinct from isGlobalAgentFrozen (which stays fail-open on the enforcement hot path so a D1 hiccup never silently freezes the fleet). A read failure here surfaces as a clear 503 instead of a falsely reassuring "unfrozen". - POST validates a { frozen: boolean } body, calls setGlobalAgentFrozen, then re-reads via getGlobalAgentFrozenState to confirm the write actually landed before reporting success — a verify failure returns 503, an observed value that doesn't match the request returns 502 — and records an operator.kill_switch_set audit event on success.
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #2412 +/- ##
=======================================
Coverage 95.78% 95.79%
=======================================
Files 224 224
Lines 24917 24944 +27
Branches 9056 9061 +5
=======================================
+ Hits 23867 23894 +27
Misses 428 428
Partials 622 622
🚀 New features to boost your workflow:
|
|
Tip 🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩 ✅ Gittensory review result - approve/merge recommendedReview updated: 2026-07-01 21:00:29 UTC
✅ Suggested Action - Approve/Merge
Review summary Nits — 6 non-blocking
Review context
Contributor next steps
Signal definitions
🟩 Safe / merged · 🟦 Advisory · 🟨 Held for review · 🟥 Blocked / closed 💰 Earn for open-source contributions like this. Gittensor lets GitHub contributors earn for the work they already do — register to start earning →. Checked by Gittensory, a quiet PR intelligence layer for OSS maintainers.
|
What
setGlobalAgentFrozen— the write side of the DB-backed global agent kill-switch, documented as "an operator flips with one row (no redeploy)" — had zero callers anywhere insrc/. There was no API route, no MCP tool, no admin surface that invoked it. The only way to actually flipglobal_agent_controls.frozenwas a direct SQL statement against D1 (e.g. viawrangler d1 execute).Found while fixing #2125 (the kill-switch's fail-open observability gap): closing the "silent failure" half of that issue doesn't help much if there's no application-level way to set the switch in the first place. The read side (
isGlobalAgentFrozen) and the/statushealth surface were both already fully wired — only the write path was missing.Fix
Added
GET/POST /v1/app/kill-switch, gated by the samerequireAppRole(c, ["operator"])check used by the existing/v1/app/operator-dashboardroute (a trusted static token bypasses the role check entirely, matching every other operator route's model).{ frozen, updatedAt, updatedBy }via a newgetGlobalAgentFrozenState. This is a strict, non-fail-open read — deliberately different fromisGlobalAgentFrozen, which stays fail-open on the enforcement hot path so a D1 hiccup never silently freezes the fleet. Here, a read failure must surface as a clear503, never a falsely reassuring "unfrozen".{ frozen: boolean }body (zod,.strict()), callssetGlobalAgentFrozen, then re-reads viagetGlobalAgentFrozenStateto confirm the write actually landed before reporting success — a verify-read failure returns503, an observed value that doesn't match the request returns502— and records anoperator.kill_switch_setaudit event on success.No DB schema, OpenAPI, or wrangler-binding changes: the whole
/v1/app/...route family (including the existingoperator-dashboard) is intentionally excluded from the public OpenAPI spec, and this only reads/writes the existingglobal_agent_controlstable.Tests
New
test/unit/routes-kill-switch.test.ts(10 tests):GET returns the seeded-default state for a trusted static token.
GET is
403for a non-operator session,401with no identity.GET surfaces
503(not a false "unfrozen") when the singleton row is missing.POST freezes and unfreezes for an operator session, verifies the write, and audits both transitions.
POST rejects a schema-invalid body and a body that isn't valid JSON at all —
400, no write attempted.POST is
403/401for a non-operator/unauthenticated caller.POST surfaces
503when the post-write verification read fails, and502when the read-after-write observes a value that doesn't match the write (both via a scopeddb/repositoriesmock — the underlyingINSERT ... ON CONFLICTis effectively atomic, so these failure modes aren't reachable through the real D1 path in an integration test).npx tsc --noEmitclean.Diff-range coverage check: both changed source files fully covered, zero gaps.
Regression sweep across routes/repositories consumers (kill-switch, api, agent-action-executor, ops, access-boundary) — 123/123 pass.
Full unsharded
npm run test:coverage— 310 files / 5693 tests pass.npm audit --audit-level=moderate— 0 vulnerabilities.Advances #1936. Closes #2359.