Parent: #1936
Problem
setGlobalAgentFrozen(env, frozen, updatedBy) (src/db/repositories.ts) — the write side of the DB-backed global agent kill-switch documented as "an operator flips with one row (no redeploy)" — has zero callers anywhere in src/. There is no API route, no MCP tool, and no admin surface that invokes it. The only way to actually flip global_agent_controls.frozen today is a direct SQL statement against the D1 database (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 (defaultOpsHealthDeps.isFrozen) are both fully wired — only the write path is missing.
Requirements
- An authenticated, operator-only way to read and flip the global kill-switch without shelling into the database.
- Must reuse the existing
isAuthorizedGitHubSessionLogin/operator-scope model (the same authority tier that already bypasses per-repo scope elsewhere), not a new auth mechanism.
- Should be reachable from at least one of: a REST route (
/v1/app/..., matching the existing operator-only path prefix) or an MCP tool (mirroring gittensory_get_fleet_analytics's requireOperatorAccess pattern).
Deliverables
- Add a route (or MCP tool) that calls
setGlobalAgentFrozen, gated by the same operator-only check used elsewhere (requireOperatorAccess-equivalent).
- A companion read endpoint/tool surfacing the current
frozen state (today only visible indirectly via /status's broader health snapshot).
- Read-after-write verification: after writing, re-read the row and confirm it reflects the intended value before reporting success, surfacing a clear error if it doesn't (closes the incident-window race where a write could appear to succeed but not actually be confirmed under a degraded D1).
- Add tests: operator can set/clear the flag and read it back; a non-operator session is denied.
Acceptance criteria
- An authorized operator can freeze/unfreeze the fleet through the application (API or MCP) without direct database access.
- A non-operator identity is denied.
- The write is confirmed via a read-after-write check before the caller is told it succeeded.
Expected outcome
The kill-switch documented as "an operator flips with one row (no redeploy)" is actually operable that way, closing the gap between the read/observability side (already correct) and the write side (currently unreachable except via raw SQL).
Parent: #1936
Problem
setGlobalAgentFrozen(env, frozen, updatedBy)(src/db/repositories.ts) — the write side of the DB-backed global agent kill-switch documented as "an operator flips with one row (no redeploy)" — has zero callers anywhere insrc/. There is no API route, no MCP tool, and no admin surface that invokes it. The only way to actually flipglobal_agent_controls.frozentoday is a direct SQL statement against the D1 database (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 (defaultOpsHealthDeps.isFrozen) are both fully wired — only the write path is missing.Requirements
isAuthorizedGitHubSessionLogin/operator-scope model (the same authority tier that already bypasses per-repo scope elsewhere), not a new auth mechanism./v1/app/..., matching the existing operator-only path prefix) or an MCP tool (mirroringgittensory_get_fleet_analytics'srequireOperatorAccesspattern).Deliverables
setGlobalAgentFrozen, gated by the same operator-only check used elsewhere (requireOperatorAccess-equivalent).frozenstate (today only visible indirectly via/status's broader health snapshot).Acceptance criteria
Expected outcome
The kill-switch documented as "an operator flips with one row (no redeploy)" is actually operable that way, closing the gap between the read/observability side (already correct) and the write side (currently unreachable except via raw SQL).