Skip to content

feat(agent-actions): add an operator route for the global kill-switch - #2412

Merged
JSONbored merged 1 commit into
mainfrom
claude/kill-switch-operable-route
Jul 1, 2026
Merged

feat(agent-actions): add an operator route for the global kill-switch#2412
JSONbored merged 1 commit into
mainfrom
claude/kill-switch-operable-route

Conversation

@JSONbored

Copy link
Copy Markdown
Owner

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 in src/. There was no API route, no MCP tool, no admin surface that invoked it. The only way to actually flip global_agent_controls.frozen was a direct SQL statement against D1 (e.g. via wrangler 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 /status health surface were both already fully wired — only the write path was missing.

Fix

Added GET/POST /v1/app/kill-switch, gated by the same requireAppRole(c, ["operator"]) check used by the existing /v1/app/operator-dashboard route (a trusted static token bypasses the role check entirely, matching every other operator route's model).

  • GET returns the current { frozen, updatedAt, updatedBy } via a new getGlobalAgentFrozenState. This is a strict, non-fail-open read — deliberately different from isGlobalAgentFrozen, 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 clear 503, never a falsely reassuring "unfrozen".
  • POST validates a { frozen: boolean } body (zod, .strict()), calls setGlobalAgentFrozen, then re-reads via getGlobalAgentFrozenState to confirm the write actually landed before reporting success — a verify-read 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.

No DB schema, OpenAPI, or wrangler-binding changes: the whole /v1/app/... route family (including the existing operator-dashboard) is intentionally excluded from the public OpenAPI spec, and this only reads/writes the existing global_agent_controls table.

Tests

New test/unit/routes-kill-switch.test.ts (10 tests):

  • GET returns the seeded-default state for a trusted static token.

  • GET is 403 for a non-operator session, 401 with 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/401 for a non-operator/unauthenticated caller.

  • POST surfaces 503 when the post-write verification read fails, and 502 when the read-after-write observes a value that doesn't match the write (both via a scoped db/repositories mock — the underlying INSERT ... ON CONFLICT is effectively atomic, so these failure modes aren't reachable through the real D1 path in an integration test).

  • npx tsc --noEmit clean.

  • 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.

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.
@dosubot dosubot Bot added the size:M label Jul 1, 2026
@codecov

codecov Bot commented Jul 1, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 95.79%. Comparing base (a8030ca) to head (e47969b).
✅ All tests successful. No failed tests found.

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           
Files with missing lines Coverage Δ
src/api/routes.ts 94.72% <100.00%> (+0.07%) ⬆️
src/db/repositories.ts 96.31% <100.00%> (+<0.01%) ⬆️
🚀 New features to boost your workflow:
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@loopover-orb

loopover-orb Bot commented Jul 1, 2026

Copy link
Copy Markdown
Contributor

Tip

🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩

✅ Gittensory review result - approve/merge recommended

Review updated: 2026-07-01 21:00:29 UTC

3 files · 1 AI reviewer · no blockers · readiness 75/100 · CI green · clean

✅ Suggested Action - Approve/Merge

  • safe to merge

Review summary
The change adds an operator-gated GET/POST surface for the global agent kill-switch and backs it with a strict repository read used for status and read-after-write verification. The visible implementation follows the existing role-gated route pattern, validates POST bodies strictly, confirms the persisted value before reporting success, and covers the main authorization, validation, missing-row, verification-failure, and audit-success paths. I do not see a reachable correctness break in the provided diff.

Nits — 6 non-blocking
  • nit: src/api/routes.ts:1359 lets setGlobalAgentFrozen throw outside the route-specific error handling, so a write-side D1 failure returns the generic worker failure shape instead of the clear JSON 503 used for read and verify failures.
  • nit: src/api/routes.ts:1377 performs the audit write after the kill-switch state has already changed and been verified, so an audit insert failure can make the client see a failed request even though the switch was flipped.
  • nit: test/unit/routes-kill-switch.test.ts:26 orders audit rows only by created_at desc, which can be nondeterministic if the two audit rows share a timestamp; add a stable secondary key or assert without depending on order.
  • src/api/routes.ts:1359 should wrap setGlobalAgentFrozen in the same explicit error response style as the verification read, so operators can distinguish write failure from verify failure.
  • src/api/routes.ts:1377 should make the audit failure semantics intentional: either let audit failure block the response and document that contract, or catch it and return success with the verified state while surfacing the audit issue through existing observability.
  • PR author also opened the linked issue — Link an issue that was opened by a different contributor, or provide a rationale for why this self-authored issue represents genuine discovery work.
Signal Result Evidence
Code review ✅ No blockers 1 reviewer
Linked issue ✅ Linked #2359
Related work ⚠️ 2 scoped overlaps Top overlaps are listed below; lower-confidence bulk is hidden.
Change scope ❌ 8/20 High review scope from cached public metadata (size label size:M; 1 linked issue).
Validation posture ✅ 25/25 PR body includes validation/test evidence.
Contributor workload ✅ 10/10 Author activity: 67 registered-repo PR(s), 57 merged, 589 issue(s).
Contributor context ✅ Confirmed Gittensor contributor JSONbored; Gittensor profile; 67 PR(s), 589 issue(s).
Gate result ✅ Passing No configured blocker found.
Review context
Contributor next steps
  • Treat this as maintainer-lane context rather than normal contributor-lane activity.
  • Review top overlaps.
  • Add a concise scope and risk note.
  • Triage stale or unlinked PRs.
  • No action.
  • Check active issues and PRs before submitting.
Signal definitions
  • Related work = same linked issue, overlapping active PRs, or title/path similarity.
  • Change scope = cached public metadata such as size labels, draft state, and review-burden hints.
  • Validation posture = whether the PR provides enough public validation/test evidence for maintainer review.
  • Contributor workload = public contributor activity and cleanup pressure, not a repo-wide quality failure.
  • Contributor context = public GitHub/Gittensor identity context; non-Gittensor status is not a blocker.

🟩 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.

  • Re-run Gittensory review

@loopover-orb loopover-orb Bot added gittensor gittensor:feature Gittensor-scored feature linked to a feature issue — scores a 0.25x multiplier. labels Jul 1, 2026
@JSONbored JSONbored self-assigned this Jul 1, 2026
@JSONbored
JSONbored merged commit 88af2a0 into main Jul 1, 2026
12 checks passed
@JSONbored
JSONbored deleted the claude/kill-switch-operable-route branch July 1, 2026 22:14
@github-project-automation github-project-automation Bot moved this from Todo to Done in gittensory - v1 roadmap Jul 1, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

gittensor:feature Gittensor-scored feature linked to a feature issue — scores a 0.25x multiplier.

Projects

No open projects
Status: Done

Development

Successfully merging this pull request may close these issues.

feat(agent-actions): the global kill-switch write path has no operable route (setGlobalAgentFrozen is uncalled)

1 participant