NK-WP-0056-T07: founder kill switch and execution findings in posture feedback
All checks were successful
All checks were successful
- cli: SIGTERM/SIGINT (kubectl delete job) sets the stop flag; the runner stops at the next step boundary and reverts. A stop before consume refuses and spends nothing. - posture feedback v0.1 section 4a: --receipts reads bounded-executor events through the receipt validator: execution-receipt-missing, -not-completed, -reverted, -budget-pressure, and the aggregate execution-recurring-revert. Owners come from the authoritative tools/posture-feedback/execution-owners.yaml, never inferred. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Assistant: claude-code Assistant-Model: opus Assistant-Process: 523928@bnt-lap001 Assistant-Session: 6c737afb-9c14-4152-9f02-477214b9c350
This commit is contained in:
parent
d7133e3a04
commit
9bd7bf0bdc
9 changed files with 309 additions and 3 deletions
|
|
@ -17,6 +17,11 @@ The config (JSON, non-secret) names endpoints, token files and the pilot's targe
|
|||
Exit codes: 0 completed, 3 reverted, 4 refused before any effect, 5 stopped / unknown / revert failed / budget,
|
||||
6 consumed but not in custody. The result printed is value-free.
|
||||
|
||||
Kill switch (NK-WP-0056-T07): SIGTERM or SIGINT, e.g. the founder's `kubectl -n sso delete job <job>`, sets the stop
|
||||
flag. Before consume the run refuses and spends nothing; after it, the runner finishes the step in flight, then
|
||||
stops and reverts what it changed (BEA stop_and_revert). The Job's terminationGracePeriodSeconds must cover a
|
||||
revert.
|
||||
|
||||
python3 bounded-executor/cli.py --journey-only --config FILE [--result FILE]
|
||||
|
||||
python3 bounded-executor/cli.py --request --admission FILE --config FILE [--result FILE]
|
||||
|
|
@ -46,7 +51,9 @@ import argparse
|
|||
import importlib.util
|
||||
import json
|
||||
import pathlib
|
||||
import signal
|
||||
import sys
|
||||
import threading
|
||||
|
||||
HERE = pathlib.Path(__file__).resolve().parent
|
||||
|
||||
|
|
@ -63,6 +70,14 @@ X = _load("executor")
|
|||
OPS = _load("operations")
|
||||
C = _load("clients")
|
||||
EXIT = {"completed": 0, "reverted": 3, "refused": 4}
|
||||
STOP = threading.Event() # the kill switch; set by SIGTERM/SIGINT, read between steps
|
||||
|
||||
|
||||
def install_kill_switch() -> None:
|
||||
def stop(_signum, _frame):
|
||||
STOP.set() # never raises into the step in flight: the runner stops at the next boundary and reverts
|
||||
signal.signal(signal.SIGTERM, stop)
|
||||
signal.signal(signal.SIGINT, stop)
|
||||
|
||||
|
||||
def approval_carrier(cfg: dict, logins: list):
|
||||
|
|
@ -125,7 +140,7 @@ def build(cfg: dict, logins: list):
|
|||
operations=operations, checks=checks, trust=lambda state: state in trusted,
|
||||
audit=C.AuditCore(cfg["audit_core"]["url"], cfg["audit_core"]["token_file"],
|
||||
cfg["audit_core"].get("ca_file")),
|
||||
alert=alert)
|
||||
alert=alert, should_stop=STOP.is_set)
|
||||
config = X.Config(executor_identity=cfg["executor_identity"], runtime_principal=cfg["runtime_principal"],
|
||||
tenant=cfg["tenant"], environment=cfg["environment"],
|
||||
approver_subjects=dict(cfg["approver_subjects"]),
|
||||
|
|
@ -316,6 +331,7 @@ def main(argv=None) -> int:
|
|||
result, code = run_journey_only(cfg)
|
||||
_emit(result, args.result)
|
||||
return code
|
||||
install_kill_switch()
|
||||
admission = X.bea.load(pathlib.Path(args.admission))
|
||||
logins: list = []
|
||||
try:
|
||||
|
|
|
|||
|
|
@ -354,6 +354,8 @@ class Run:
|
|||
self.preflight()
|
||||
except Refused as refusal:
|
||||
return {"outcome": "refused", "reason": str(refusal), "run_id": None, "effects": False}
|
||||
if self.d.should_stop(): # the founder's stop before the one irreversible act: nothing is spent
|
||||
return {"outcome": "refused", "reason": "stopped_before_consume", "run_id": None, "effects": False}
|
||||
self.start_wall, self.start_mono = self.d.wall(), self.d.monotonic()
|
||||
try:
|
||||
self.consume()
|
||||
|
|
|
|||
|
|
@ -336,5 +336,20 @@ class RequestPhase(Base):
|
|||
self.assertNotIn(SECRET, text)
|
||||
|
||||
|
||||
|
||||
|
||||
class KillSwitch(unittest.TestCase):
|
||||
def test_sigterm_sets_the_stop_flag_the_runner_reads(self):
|
||||
import os
|
||||
import signal
|
||||
before = {sig: signal.getsignal(sig) for sig in (signal.SIGTERM, signal.SIGINT)}
|
||||
self.addCleanup(lambda: [signal.signal(sig, h) for sig, h in before.items()])
|
||||
self.addCleanup(CLI.STOP.clear)
|
||||
CLI.STOP.clear()
|
||||
CLI.install_kill_switch()
|
||||
os.kill(os.getpid(), signal.SIGTERM)
|
||||
self.assertTrue(CLI.STOP.wait(2))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
|
|
|||
|
|
@ -402,6 +402,12 @@ class FailureTests(Base):
|
|||
self.assertIn("restore-kv", self.ops())
|
||||
self.assertNotIn("deliver", self.ops())
|
||||
|
||||
def test_stop_before_consume_spends_nothing(self):
|
||||
self.w.stop_after = 0
|
||||
result = self.run_it()
|
||||
self.assertEqual((result["outcome"], result["reason"]), ("refused", "stopped_before_consume"))
|
||||
self.assertEqual((self.w.carrier.consumes, self.w.events, self.ops()), (0, [], []))
|
||||
|
||||
def test_settle_time_is_waited_and_counted(self):
|
||||
self.doc = synthetic_admission(self.root, lambda s: s["checks"][3].update(settle_seconds=125))
|
||||
self.w = World(self.doc, self.clock)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue