feat(plan-01): Phase 1 — relocate harness code to packages/casan-harness (symlink facade)
Physically move the pure-code subtrees out of .specify into the package, leaving compat symlinks at the old .specify/<dir> paths so every existing reference (internal CASAN_HARNESS_ROOT + external CI/docker/mjs) keeps resolving. Runtime state stays put. Moved (git mv): scripts/ tests/ security/ templates/ config/ governance/ memory/ .specify/<dir> -> packages/casan-harness/<dir> (+ .specify/<dir> symlink) Stays in .specify (state/governance/domain, handled later): logs/ agentops/ level5/ init-options.json traceability-map.json Python `.resolve()` self-location followed the compat symlink into packages and lost the app root; generate-casan-demo-context.py, generate-agentops-dashboard.py and dashboard-server.py now walk UP for the `.specify` state marker instead of a fixed parent depth (fixes "missing trace files" in run-casan4). Full gate: PASS=64 FAIL=0 SKIP=3 (CASAN_CI_STEP_TIMEOUT_SEC=1200 — track-a ~450s runs close to the 600s default and can tip over under load; this is timing variance, not a regression — it passed cleanly with headroom). Runtime log/audit artifacts kept unstaged. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
2c765c9a45
commit
664bd1f00c
@@ -0,0 +1,148 @@
|
||||
#!/usr/bin/env python3
|
||||
"""CASAN Loop Budget Governor (Plan-17 Track 1, harness-owned).
|
||||
|
||||
The loop-breaker. Given a run's *cumulative* usage (steps / tokens / wall-clock /
|
||||
cost / corrections), decide whether the agent loop may CONTINUE or must HALT.
|
||||
|
||||
Deny-by-default & fail-closed:
|
||||
* no policy file -> strictest built-in ceiling (STRICT_CEILING),
|
||||
* present-but-corrupt policy -> HALT (exit 3), never "run on",
|
||||
* any unexpected error -> HALT (exit 3).
|
||||
|
||||
Loosening the ceiling is a security-sensitive setting: it must be changed through
|
||||
the governed settings store (control-plane-settings.py: approval JWT + SoD +
|
||||
versioned + rollback), not by editing this script. This governor only *reads* the
|
||||
(governed) policy — see Plan-17 17.3.
|
||||
|
||||
Usage:
|
||||
loop-governor.py check --run-id R --step N \
|
||||
[--tokens T] [--elapsed S] [--cost C] [--corrections K] \
|
||||
[--profile prod|dev] [--delegation-level L2] [--project okr]
|
||||
|
||||
Exit codes:
|
||||
0 CONTINUE (within ceiling)
|
||||
3 HALT(budget) (ceiling exceeded with on_exceed=halt, OR fail-closed error)
|
||||
4 ESCALATE (ceiling exceeded with on_exceed=escalate)
|
||||
2 usage error
|
||||
"""
|
||||
import argparse
|
||||
import json
|
||||
import sys
|
||||
|
||||
import loop_common as lc
|
||||
|
||||
|
||||
def _emit(payload, stream=sys.stdout):
|
||||
json.dump(payload, stream, ensure_ascii=False, indent=2, sort_keys=True)
|
||||
stream.write("\n")
|
||||
|
||||
|
||||
def cmd_check(args):
|
||||
usage = {
|
||||
"steps": args.step,
|
||||
"tokens": args.tokens,
|
||||
"elapsed_s": args.elapsed,
|
||||
"cost_usd": args.cost,
|
||||
"corrections": args.corrections,
|
||||
}
|
||||
prof = args.profile or lc.profile()
|
||||
|
||||
try:
|
||||
policy = lc.load_policy()
|
||||
ceiling, source = lc.resolve_budget(
|
||||
policy, prof, args.delegation_level, args.project
|
||||
)
|
||||
except lc.PolicyError as exc:
|
||||
# Fail-closed: a policy we cannot trust must stop the loop, not free it.
|
||||
payload = {
|
||||
"decision": "HALT",
|
||||
"reason": "fail_closed",
|
||||
"detail": str(exc),
|
||||
"run_id": args.run_id,
|
||||
"step": args.step,
|
||||
"profile": prof,
|
||||
"provenance": lc.provenance("loop-governor", lc.policy_path(), verified=False),
|
||||
}
|
||||
lc.append_audit({"kind": "governor_halt", **payload})
|
||||
_emit(payload)
|
||||
return 3
|
||||
|
||||
exceeded = []
|
||||
checks = (
|
||||
("steps", "max_steps", usage["steps"]),
|
||||
("tokens", "max_tokens", usage["tokens"]),
|
||||
("elapsed_s", "max_wall_clock_sec", usage["elapsed_s"]),
|
||||
("cost_usd", "max_cost_usd", usage["cost_usd"]),
|
||||
("corrections", "max_corrections_per_step", usage["corrections"]),
|
||||
)
|
||||
for usage_key, limit_key, value in checks:
|
||||
limit = ceiling[limit_key]
|
||||
if value is not None and value > limit:
|
||||
exceeded.append({"metric": usage_key, "value": value, "limit": limit})
|
||||
|
||||
on_exceed = ceiling.get("on_exceed", "halt")
|
||||
|
||||
if not exceeded:
|
||||
payload = {
|
||||
"decision": "CONTINUE",
|
||||
"run_id": args.run_id,
|
||||
"step": args.step,
|
||||
"profile": prof,
|
||||
"policy_source": source,
|
||||
"limits": ceiling,
|
||||
"usage": usage,
|
||||
"provenance": lc.provenance("loop-governor", lc.policy_path(), verified=policy is not None),
|
||||
}
|
||||
_emit(payload)
|
||||
return 0
|
||||
|
||||
decision = "ESCALATE" if on_exceed == "escalate" else "HALT"
|
||||
payload = {
|
||||
"decision": decision,
|
||||
"reason": "budget_exceeded",
|
||||
"run_id": args.run_id,
|
||||
"step": args.step,
|
||||
"profile": prof,
|
||||
"policy_source": source,
|
||||
"limits": ceiling,
|
||||
"usage": usage,
|
||||
"exceeded": exceeded,
|
||||
"on_exceed": on_exceed,
|
||||
"provenance": lc.provenance("loop-governor", lc.policy_path(), verified=policy is not None),
|
||||
}
|
||||
lc.append_audit({"kind": "governor_" + decision.lower(), **payload})
|
||||
_emit(payload)
|
||||
return 4 if decision == "ESCALATE" else 3
|
||||
|
||||
|
||||
def build_parser():
|
||||
p = argparse.ArgumentParser(description="CASAN Loop Budget Governor (Plan-17 T1)")
|
||||
sub = p.add_subparsers(dest="cmd", required=True)
|
||||
c = sub.add_parser("check", help="Evaluate cumulative usage against the ceiling")
|
||||
c.add_argument("--run-id", required=True)
|
||||
c.add_argument("--step", type=int, required=True)
|
||||
c.add_argument("--tokens", type=int, default=None)
|
||||
c.add_argument("--elapsed", type=float, default=None)
|
||||
c.add_argument("--cost", type=float, default=None)
|
||||
c.add_argument("--corrections", type=int, default=None)
|
||||
c.add_argument("--profile", default=None)
|
||||
c.add_argument("--delegation-level", default=None)
|
||||
c.add_argument("--project", default=None)
|
||||
c.set_defaults(func=cmd_check)
|
||||
return p
|
||||
|
||||
|
||||
def main(argv=None):
|
||||
args = build_parser().parse_args(argv)
|
||||
try:
|
||||
return args.func(args)
|
||||
except lc.PolicyError as exc:
|
||||
_emit({"decision": "HALT", "reason": "fail_closed", "detail": str(exc)})
|
||||
return 3
|
||||
except Exception as exc: # never fail open
|
||||
_emit({"decision": "HALT", "reason": "internal_error", "detail": str(exc)}, sys.stderr)
|
||||
return 3
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
Reference in New Issue
Block a user