update doc and optimize

This commit is contained in:
thanhnv
2026-07-06 17:47:12 +09:00
parent ace442da0e
commit 4419cd9eae
95 changed files with 2951 additions and 1353 deletions
@@ -78,6 +78,23 @@ run "phase-h4-multilingual" bash "$TESTS/phase-h4-multilingual-tests.sh"
run "phase-c6-sandbox" bash "$TESTS/phase-c6-sandbox-tests.sh"
run "phase-h4-split-inject" bash "$TESTS/phase-h4-split-inject-tests.sh"
run "phase10-traceability" bash "$TESTS/phase10-traceability-tests.sh"
run "phase08-compression" bash "$TESTS/phase08-compression-tests.sh"
run "phase-control-plane" bash "$TESTS/phase-control-plane-tests.sh"
run "phase-rbac" bash "$TESTS/phase-rbac-tests.sh"
run "phase-rai" bash "$TESTS/phase-rai-tests.sh"
run "phase-selfimprove" bash "$TESTS/phase-selfimprove-tests.sh"
run "phase-governance-report" bash "$TESTS/phase-governance-report-tests.sh"
run "phase-preflight" bash "$TESTS/phase-preflight-tests.sh"
if [[ "${CASAN_CI_RUN_BACKEND:-1}" == "1" ]]; then
if command -v npm >/dev/null 2>&1; then
run "backend-tests" npm test -w backend
else
skip "backend-tests (npm unavailable)"
fi
else
skip "backend-tests (CASAN_CI_RUN_BACKEND=0)"
fi
if [[ "${CASAN_CI_RUN_FRONTEND:-1}" == "1" ]]; then
if command -v npm >/dev/null 2>&1; then
@@ -0,0 +1,156 @@
#!/usr/bin/env python3
"""CASAN-native token-killer (Plan-08 Track 3).
A deterministic tool-output compressor written for CASAN — NOT a wrapper around
RTK. It reduces token count of long command/tool output before it enters model
context, while (a) always preserving must-keep lines, (b) never compressing on
failure (raw passthrough for debugging, RTK-style tee), and (c) reporting the
token savings for H6 telemetry.
Modes:
dedup collapse consecutive duplicate lines with an (xN) counter
extractive keep only important lines (errors/failures/warnings) + must-keep
structural dedup + keep summary/important/must-keep lines (for test/log output)
Governance note: this runs AFTER `H4 scan raw` + `H5 hash raw` and BEFORE
`H4 scan compressed` in the Plan-08 pipeline; it is deterministic and needs no
model, so it cannot be used as a path to evade H4.
"""
import argparse
import json
import os
import re
import sys
def compression_enabled() -> bool:
"""Read the effective `compression.enabled` from the control-plane settings
store (the harness-owned governed settings). Absent/invalid ⇒ enabled (default).
This is how a Control Plane setting change actually governs the harness."""
store_file = os.environ.get(
"CASAN_CP_STORE_FILE",
os.path.join(
os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..", "..")),
".specify/level5/control-plane-settings.json",
),
)
if not os.path.isfile(store_file):
return True
try:
data = json.load(open(store_file, encoding="utf-8"))
setting = data.get("settings", {}).get("compression.enabled")
return True if setting is None else bool(setting["value"])
except (OSError, ValueError, KeyError, TypeError):
return True
IMPORTANT_RE = re.compile(
r"\b(error|errors|fail|failed|failure|failing|exception|panic|denied|blocked|warn|warning)\b",
re.IGNORECASE,
)
SUMMARY_RE = re.compile(r"\b(\d+)\s+(pass|passed|fail|failed|tests?|errors?|warnings?)\b", re.IGNORECASE)
def estimate_tokens(text: str) -> int:
return len(text.split())
def load_patterns(path: str):
if not path:
return []
with open(path, encoding="utf-8") as fh:
return [line.strip() for line in fh if line.strip()]
def is_must_keep(line: str, patterns) -> bool:
return any(re.search(p, line) for p in patterns)
def dedup(lines):
out = []
i = 0
n = len(lines)
while i < n:
j = i
while j + 1 < n and lines[j + 1] == lines[i]:
j += 1
count = j - i + 1
out.append(lines[i] if count == 1 else f"{lines[i]} (x{count})")
i = j + 1
return out
def compress(text: str, mode: str, must):
lines = text.split("\n")
if mode == "dedup":
return dedup(lines)
if mode == "extractive":
return [ln for ln in lines if IMPORTANT_RE.search(ln) or is_must_keep(ln, must)]
if mode == "structural":
kept = [
ln
for ln in lines
if IMPORTANT_RE.search(ln) or SUMMARY_RE.search(ln) or is_must_keep(ln, must)
]
return dedup(kept)
raise ValueError(f"unknown mode: {mode}")
def main() -> int:
ap = argparse.ArgumentParser()
ap.add_argument("--mode", choices=["dedup", "extractive", "structural"], default="structural")
ap.add_argument("--input", default="-", help="input file or - for stdin")
ap.add_argument("--must-keep-file", default="", help="file with one must-keep regex per line")
ap.add_argument("--failed", action="store_true", help="raw passthrough (tee) when the command failed")
ap.add_argument(
"--respect-policy",
action="store_true",
help="honor control-plane `compression.enabled`; if disabled, pass raw through",
)
ap.add_argument(
"--require-must-keep-file",
default="",
help="verify every pattern in this file still appears; exit 1 (gate) if any is missing",
)
args = ap.parse_args()
raw = sys.stdin.read() if args.input == "-" else open(args.input, encoding="utf-8").read()
must = load_patterns(args.must_keep_file)
if args.failed:
# RTK-style tee: never compress failing output; keep raw for debugging.
out_text = raw
mode_used = "passthrough"
elif args.respect_policy and not compression_enabled():
# Control-plane setting governs the harness: compression disabled ⇒ raw.
out_text = raw
mode_used = "policy-disabled"
else:
out_lines = compress(raw, args.mode, must)
out_text = "\n".join(out_lines)
mode_used = args.mode
in_tokens = estimate_tokens(raw)
out_tokens = estimate_tokens(out_text)
saved = in_tokens - out_tokens
ratio = round(out_tokens / in_tokens, 4) if in_tokens else 1.0
verify_patterns = load_patterns(args.require_must_keep_file)
missing = [p for p in verify_patterns if not re.search(p, out_text)]
sys.stdout.write(out_text)
if not out_text.endswith("\n"):
sys.stdout.write("\n")
print(
f"COMPRESS mode={mode_used} in_tokens={in_tokens} out_tokens={out_tokens} "
f"saved={saved} ratio={ratio} must_keep_missing={len(missing)}",
file=sys.stderr,
)
if missing:
print(f"COMPRESS_MUST_KEEP_DROPPED {','.join(missing)}", file=sys.stderr)
return 1
return 0
if __name__ == "__main__":
raise SystemExit(main())
@@ -0,0 +1,203 @@
#!/usr/bin/env python3
"""CASAN Control Plane — governed settings store (Plan-13 core, harness-owned).
This is the REUSABLE governance asset: it lives in the core harness, not in any
generated app (e.g. OKR). Every write is (1) deny-by-default (only whitelisted
keys), (2) approval-gated for security-sensitive keys, (3) versioned, and (4)
recorded in a hash-linked audit chain so tampering is detectable.
The standalone Control Plane web app (control-plane/) calls this CLI for all
writes so the governance logic exists exactly once — in the harness.
Store file: $CASAN_CP_STORE_FILE (default .specify/level5/control-plane-settings.json)
"""
import argparse
import hashlib
import json
import os
import sys
from datetime import datetime, timezone
SETTINGS_POLICY = {
"compression.enabled": {"securitySensitive": False, "description": "Toggle context/token compression"},
"compression.mode": {"securitySensitive": False, "description": "extractive | structural | semantic-dedup | abstractive"},
"cost.absolute_cap_usd": {"securitySensitive": False, "description": "Absolute per-call cost cap"},
"model.primary": {"securitySensitive": False, "description": "Primary model spec (e.g. ollama:ornith:9b)"},
"security.strict": {"securitySensitive": True, "description": "H4 fail-closed strict mode"},
"kill_switch.global": {"securitySensitive": True, "description": "Global kill-switch engage/disengage"},
}
GENESIS_HASH = "0" * 64
def project_root() -> str:
return os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..", ".."))
def store_path() -> str:
return os.environ.get(
"CASAN_CP_STORE_FILE",
os.path.join(project_root(), ".specify/level5/control-plane-settings.json"),
)
def now_iso() -> str:
return datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
def empty_store():
return {"settings": {}, "history": {}, "audit": []}
def load_store():
path = store_path()
if not os.path.isfile(path):
return empty_store()
with open(path, encoding="utf-8") as fh:
data = json.load(fh)
return {"settings": data.get("settings", {}), "history": data.get("history", {}), "audit": data.get("audit", [])}
def save_store(store) -> None:
path = store_path()
os.makedirs(os.path.dirname(path), exist_ok=True)
with open(path, "w", encoding="utf-8") as fh:
json.dump(store, fh, indent=2, ensure_ascii=False)
fh.write("\n")
def hash_entry(entry) -> str:
return hashlib.sha256(json.dumps(entry, sort_keys=True, ensure_ascii=False).encode("utf-8")).hexdigest()
def append_audit(store, base) -> None:
prev = store["audit"][-1] if store["audit"] else None
prev_hash = prev["hash"] if prev else GENESIS_HASH
seq = len(store["audit"]) + 1
without_hash = {"seq": seq, **base, "prevHash": prev_hash}
store["audit"].append({**without_hash, "hash": hash_entry(without_hash)})
def do_set(key, value, actor, reason, approval):
policy = SETTINGS_POLICY.get(key)
if policy is None:
print(f"SETTING_NOT_ALLOWED {key}", file=sys.stderr)
return 2
if policy["securitySensitive"] and not (approval or "").strip():
print(f"APPROVAL_REQUIRED {key}", file=sys.stderr)
return 3
store = load_store()
prev = store["settings"].get(key)
if prev is not None:
store["history"].setdefault(key, []).append(prev)
nxt = {
"value": value,
"version": (prev["version"] if prev else 0) + 1,
"updatedAt": now_iso(),
"actor": actor,
"reason": reason,
}
store["settings"][key] = nxt
append_audit(store, {
"key": key, "action": "set", "value": value,
"prevValue": prev["value"] if prev else None,
"actor": actor, "reason": reason, "at": nxt["updatedAt"],
})
save_store(store)
print(json.dumps(nxt, ensure_ascii=False))
return 0
def do_rollback(key, actor, reason):
store = load_store()
history = store["history"].get(key, [])
if not history:
print(f"NO_PRIOR_VERSION {key}", file=sys.stderr)
return 4
previous = history.pop()
current = store["settings"].get(key)
restored = {
"value": previous["value"],
"version": ((current["version"] if current else previous["version"]) + 1),
"updatedAt": now_iso(),
"actor": actor,
"reason": reason,
}
store["settings"][key] = restored
append_audit(store, {
"key": key, "action": "rollback", "value": previous["value"],
"prevValue": current["value"] if current else None,
"actor": actor, "reason": reason, "at": restored["updatedAt"],
})
save_store(store)
print(json.dumps(restored, ensure_ascii=False))
return 0
def verify_audit():
store = load_store()
prev_hash = GENESIS_HASH
for entry in store["audit"]:
rest = {k: v for k, v in entry.items() if k != "hash"}
if rest.get("prevHash") != prev_hash or hash_entry(rest) != entry["hash"]:
return {"ok": False, "brokenAt": entry["seq"]}
prev_hash = entry["hash"]
return {"ok": True, "brokenAt": None}
def parse_value(raw):
try:
return json.loads(raw)
except (ValueError, TypeError):
return raw
def main() -> int:
ap = argparse.ArgumentParser()
sub = ap.add_subparsers(dest="cmd", required=True)
sub.add_parser("list-policy")
sub.add_parser("get-all")
g = sub.add_parser("get"); g.add_argument("key")
s = sub.add_parser("set")
s.add_argument("key"); s.add_argument("value")
s.add_argument("--actor", required=True); s.add_argument("--reason", required=True); s.add_argument("--approval", default="")
r = sub.add_parser("rollback")
r.add_argument("key"); r.add_argument("--actor", required=True); r.add_argument("--reason", required=True)
sub.add_parser("get-audit")
sub.add_parser("verify-audit")
e = sub.add_parser("effective")
e.add_argument("key")
e.add_argument("--default", default="")
args = ap.parse_args()
if args.cmd == "list-policy":
print(json.dumps(SETTINGS_POLICY, ensure_ascii=False)); return 0
if args.cmd == "get-all":
print(json.dumps(load_store()["settings"], ensure_ascii=False)); return 0
if args.cmd == "get":
print(json.dumps(load_store()["settings"].get(args.key), ensure_ascii=False)); return 0
if args.cmd == "set":
return do_set(args.key, parse_value(args.value), args.actor, args.reason, args.approval)
if args.cmd == "rollback":
return do_rollback(args.key, args.actor, args.reason)
if args.cmd == "get-audit":
print(json.dumps(load_store()["audit"], ensure_ascii=False)); return 0
if args.cmd == "verify-audit":
verdict = verify_audit()
print(f"CP_AUDIT ok={verdict['ok']} brokenAt={verdict['brokenAt']}")
return 0 if verdict["ok"] else 1
if args.cmd == "effective":
# Single read path for "effective setting": store value if set, else default.
current = load_store()["settings"].get(args.key)
if current is None:
print(args.default)
else:
value = current["value"]
print(value if isinstance(value, str) else json.dumps(value, ensure_ascii=False))
return 0
return 2
if __name__ == "__main__":
raise SystemExit(main())
@@ -0,0 +1,119 @@
#!/usr/bin/env python3
"""CASAN unified governance evidence report (Plan-09 tie-in, harness-owned).
Aggregates the harness governance controls into ONE certified-run artifact so a
reviewer can verify the whole posture from a single file instead of scattered
outputs. Deterministic and offline — it composes existing harness cores:
- Traceability : traceability-matrix.py (REQ→code→test, incl. symbol/line)
- Control Plane : control-plane-settings.py verify-audit (audit hash-chain)
- RBAC : rbac-check.py list-roles (policy present)
- RAI/Data Gov : rai-guard.py presence + optional model-cards
- Self-improve : self-improve.py presence
A run is CERTIFIED only when: traceability has 0 failing FRs AND the control-plane
audit chain is intact. `--gate` exits non-zero when not certified (no false
certification).
"""
import argparse
import json
import os
import subprocess
import sys
def here() -> str:
return os.path.dirname(os.path.abspath(__file__))
def run(cmd):
return subprocess.run(cmd, capture_output=True, text=True)
def traceability_summary(out_dir):
script = os.path.join(here(), "traceability-matrix.py")
out = os.path.join(out_dir, "traceability-matrix.json")
res = run([sys.executable, script, "--out", out, "--gate"])
summary = {"available": os.path.isfile(out), "gate_pass": res.returncode == 0}
if summary["available"]:
try:
data = json.load(open(out, encoding="utf-8"))
summary.update(data.get("summary", {}))
except ValueError:
pass
return summary
def audit_integrity():
script = os.path.join(here(), "control-plane-settings.py")
res = run([sys.executable, script, "verify-audit"])
return {"ok": res.returncode == 0, "detail": (res.stdout or res.stderr).strip()}
def rbac_present():
script = os.path.join(here(), "rbac-check.py")
res = run([sys.executable, script, "list-roles"])
return {"available": res.returncode == 0, "roles": len([l for l in res.stdout.splitlines() if l.strip()])}
def control_present(name):
return os.path.isfile(os.path.join(here(), name))
def build_report(out_dir):
trace = traceability_summary(out_dir)
audit = audit_integrity()
rbac = rbac_present()
controls = {
"traceability": control_present("traceability-matrix.py"),
"control_plane_settings": control_present("control-plane-settings.py"),
"rbac": control_present("rbac-check.py"),
"rai_data_governance": control_present("rai-guard.py"),
"self_improve": control_present("self-improve.py"),
"compression": control_present("context-compress.py"),
}
certified = (
trace.get("gate_pass", False)
and int(trace.get("failed", 1)) == 0
and audit["ok"]
and all(controls.values())
)
return {
"certified": certified,
"controls_present": controls,
"traceability": trace,
"control_plane_audit": audit,
"rbac": rbac,
}
def main() -> int:
ap = argparse.ArgumentParser()
ap.add_argument("--out", default=os.path.join(
os.path.abspath(os.path.join(here(), "..", "..", "..")),
"docs/output/casan/governance-report.json",
))
ap.add_argument("--gate", action="store_true")
args = ap.parse_args()
out_dir = os.path.dirname(args.out)
os.makedirs(out_dir, exist_ok=True)
report = build_report(out_dir)
with open(args.out, "w", encoding="utf-8") as fh:
json.dump(report, fh, indent=2, ensure_ascii=False)
fh.write("\n")
badge = "CERTIFIED" if report["certified"] else "NOT_CERTIFIED"
print(
f"GOVERNANCE_REPORT badge={badge} traceability_fail={report['traceability'].get('failed')} "
f"audit_ok={report['control_plane_audit']['ok']} controls={sum(report['controls_present'].values())}/"
f"{len(report['controls_present'])} out={args.out}"
)
if args.gate and not report["certified"]:
print("GOVERNANCE_NOT_CERTIFIED", file=sys.stderr)
return 1
return 0
if __name__ == "__main__":
raise SystemExit(main())
@@ -0,0 +1,36 @@
#!/usr/bin/env bash
set -uo pipefail
# CASAN harness preflight (Plan-15/13 enforcement wiring).
# Runs governance cores BEFORE a model call. Currently enforces RAI data
# governance: a prompt classified PII/confidential must not go to a CLOUD model
# without approval. Opt-in via CASAN_PREFLIGHT=1 so existing flows are unchanged.
#
# Args mirror model-router: <prompt-file> <out-json> [--role R] [--model M]
# Env: CASAN_MODEL_APPROVAL (approval token for sending sensitive data to cloud)
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROMPT="${1:-}"
MODEL=""
while [[ $# -gt 0 ]]; do
case "$1" in
--model) MODEL="${2:-}"; shift 2 ;;
*) shift ;;
esac
done
case "$MODEL" in
openai:*|anthropic:*)
if [[ -n "$PROMPT" && -f "$PROMPT" ]]; then
if ! python3 "$SCRIPT_DIR/rai-guard.py" check-cloud --input "$PROMPT" --target cloud \
--approval "${CASAN_MODEL_APPROVAL:-}" >/dev/null 2>&1; then
echo "PREFLIGHT_BLOCK rai-check-cloud model=$MODEL" >&2
exit 1
fi
fi
;;
esac
echo "PREFLIGHT_OK model=${MODEL:-local}"
exit 0
@@ -9,4 +9,12 @@ set -euo pipefail
# and usage logging live in model-call.py.
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# Opt-in governance preflight (default off ⇒ no change to existing flows).
# When enabled, enforces RAI data governance (e.g. PII must not reach a cloud
# model without approval) BEFORE the model call.
if [[ "${CASAN_PREFLIGHT:-0}" == "1" ]]; then
bash "$SCRIPT_DIR/harness-preflight.sh" "$@" >/dev/null || exit $?
fi
exec python "$SCRIPT_DIR/model-call.py" "$@"
@@ -0,0 +1,166 @@
#!/usr/bin/env python3
"""CASAN Responsible AI & Data Governance guard (Plan-15 core, harness-owned).
Reusable governance asset in the core harness. Three deterministic controls:
classify label text by data sensitivity (PII / confidential / internal / public)
check-cloud DENY sending PII/confidential to a cloud model without approval (extends C3)
model-card require an approved model card (source/version/role/risks) — block uncarded
Exit codes: 0 = OK/ALLOW, 1 = DENY/BLOCK. Reasons on stderr.
"""
import argparse
import json
import os
import re
import sys
import time
PII_PATTERNS = [
(r"[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}", "email"),
(r"\b(?:\d[ -]?){13,16}\b", "card-number"),
(r"\b\d{3}-\d{2}-\d{4}\b", "ssn"),
(r"\b(?:sk-[A-Za-z0-9]{16,}|AKIA[0-9A-Z]{12,}|ghp_[A-Za-z0-9]{20,})\b", "secret-token"),
(r"(?i)\bpassword\s*[:=]\s*\S+", "password"),
(r"(?i)\b(?:\+?\d[\d -]{8,}\d)\b", "phone"),
]
CONFIDENTIAL_PATTERNS = [
(r"(?i)\b(confidential|internal only|top secret|restricted)\b", "marker"),
]
SENSITIVE_LABELS = {"PII", "confidential"}
def classify(text: str):
pii = [name for pat, name in PII_PATTERNS if re.search(pat, text)]
if pii:
return "PII", pii
conf = [name for pat, name in CONFIDENTIAL_PATTERNS if re.search(pat, text)]
if conf:
return "confidential", conf
return "internal", []
def read_input(path: str) -> str:
return sys.stdin.read() if path == "-" else open(path, encoding="utf-8").read()
def cmd_classify(args) -> int:
label, matches = classify(read_input(args.input))
print(f"RAI_CLASSIFY label={label} matches={','.join(matches) if matches else '-'}")
return 0
def cmd_check_cloud(args) -> int:
label, matches = classify(read_input(args.input))
if args.target == "cloud" and label in SENSITIVE_LABELS and not (args.approval or "").strip():
print(f"RAI_DENY DATA_TO_CLOUD label={label} matches={','.join(matches)}", file=sys.stderr)
return 1
print(f"RAI_ALLOW target={args.target} label={label}")
return 0
REQUIRED_CARD_FIELDS = ["source", "role", "risks"]
def cmd_model_card(args) -> int:
try:
cards = json.load(open(args.cards, encoding="utf-8"))
except (OSError, ValueError):
print(f"RAI_DENY MODEL_CARDS_UNREADABLE {args.cards}", file=sys.stderr)
return 1
card = cards.get(args.model)
if card is None:
print(f"RAI_DENY MODEL_UNCARDED {args.model}", file=sys.stderr)
return 1
missing = [f for f in REQUIRED_CARD_FIELDS if not card.get(f)]
if "version" not in card and "digest" not in card:
missing.append("version|digest")
if missing:
print(f"RAI_DENY MODEL_CARD_INCOMPLETE {args.model} missing={','.join(missing)}", file=sys.stderr)
return 1
print(f"RAI_ALLOW MODEL_CARDED {args.model}")
return 0
def read_items(path):
items = []
with open(path, encoding="utf-8") as fh:
for line in fh:
line = line.strip()
if line:
try:
items.append(json.loads(line))
except ValueError:
continue
return items
def cmd_report(args) -> int:
"""RAI aggregate: distribution of data sensitivity across a set of items."""
dist = {"PII": 0, "confidential": 0, "internal": 0}
for item in read_items(args.items):
label, _ = classify(str(item.get("text", "")))
dist[label] = dist.get(label, 0) + 1
print(json.dumps({"distribution": dist, "total": sum(dist.values())}, ensure_ascii=False))
return 0
def cmd_retention(args) -> int:
"""Data retention: flag items older than the policy; purge writes an audit
record. --gate fails when expired items remain un-purged (retention breach)."""
now = args.now if args.now is not None else int(time.time())
items = read_items(args.items)
expired = [i for i in items if (now - int(i.get("created_epoch", now))) / 86400.0 > args.days]
purged = 0
if args.purge and expired:
audit_file = args.audit or os.environ.get("CASAN_RAI_AUDIT", "")
if audit_file:
os.makedirs(os.path.dirname(os.path.abspath(audit_file)), exist_ok=True)
with open(audit_file, "a", encoding="utf-8") as fh:
for i in expired:
fh.write(json.dumps({"id": i.get("id"), "purged_at": now, "reason": "retention"}, ensure_ascii=False) + "\n")
purged = len(expired)
remaining = 0 if args.purge else len(expired)
print(f"RAI_RETENTION total={len(items)} expired={len(expired)} purged={purged} remaining_expired={remaining}")
if args.gate and remaining > 0:
print(f"RAI_DENY RETENTION_BREACH expired_unpurged={remaining}", file=sys.stderr)
return 1
return 0
def main() -> int:
ap = argparse.ArgumentParser()
sub = ap.add_subparsers(dest="cmd", required=True)
c = sub.add_parser("classify"); c.add_argument("--input", default="-")
cc = sub.add_parser("check-cloud")
cc.add_argument("--input", default="-"); cc.add_argument("--target", choices=["cloud", "local"], required=True)
cc.add_argument("--approval", default="")
mc = sub.add_parser("model-card")
mc.add_argument("--model", required=True); mc.add_argument("--cards", required=True)
rp = sub.add_parser("report"); rp.add_argument("--items", required=True)
rt = sub.add_parser("retention")
rt.add_argument("--items", required=True)
rt.add_argument("--days", type=int, required=True)
rt.add_argument("--now", type=int, default=None)
rt.add_argument("--purge", action="store_true")
rt.add_argument("--audit", default="")
rt.add_argument("--gate", action="store_true")
args = ap.parse_args()
if args.cmd == "classify":
return cmd_classify(args)
if args.cmd == "check-cloud":
return cmd_check_cloud(args)
if args.cmd == "model-card":
return cmd_model_card(args)
if args.cmd == "report":
return cmd_report(args)
if args.cmd == "retention":
return cmd_retention(args)
return 2
if __name__ == "__main__":
raise SystemExit(main())
@@ -0,0 +1,123 @@
#!/usr/bin/env python3
"""CASAN RBAC — role-based access control decision engine (Plan-14 core, harness-owned).
Reusable governance asset in the core harness (not in any generated app). The
Control Plane web app and any harness action call this to authorize a request.
Model: role × (resource:action) with scope. deny-by-default + fail-closed:
unknown role / action / cross-tenant / sensitive-without-org-admin ⇒ DENY.
Separation of Duties (SoD): a proposer cannot approve their own request.
Exit codes: 0 = ALLOW, 1 = DENY. Reason printed to stderr.
"""
import argparse
import sys
# scope: "org" (all projects) or "project" (must match the acted-on project)
PERMISSIONS = {
"org-admin": {"scope": "org", "allow": {"*"}},
"project-admin": {"scope": "project", "allow": {"settings:read", "settings:write", "monitoring:read", "audit:read"}},
"approver": {"scope": "project", "allow": {"settings:read", "monitoring:read", "approval:grant"}},
"operator": {"scope": "project", "allow": {"monitoring:read", "kill_switch:engage"}},
"viewer": {"scope": "project", "allow": {"settings:read", "monitoring:read"}},
"auditor": {"scope": "org", "allow": {"settings:read", "monitoring:read", "audit:read"}},
}
# Maps an IdP-issued claim (role name / group) to an RBAC role. The IdP (Plan-07
# C4) is the identity authority; RBAC only maps a *verified* claim to a role.
# deny-by-default: an unmapped claim yields no role.
CLAIM_ROLE_MAP = {
"casan-org-admin": "org-admin",
"casan-project-admin": "project-admin",
"casan-approver": "approver",
"casan-operator": "operator",
"casan-viewer": "viewer",
"casan-auditor": "auditor",
}
def decide(role, resource, action, role_project, target_project, sensitive):
perm = PERMISSIONS.get(role)
if perm is None:
return False, f"UNKNOWN_ROLE {role}"
action_key = f"{resource}:{action}"
# Sensitive settings writes are org-admin only, regardless of other grants.
if sensitive and not (resource == "settings" and action == "write"):
# sensitivity only meaningful for settings:write
pass
if sensitive and resource == "settings" and action == "write" and role != "org-admin":
return False, f"SENSITIVE_REQUIRES_ORG_ADMIN {action_key}"
if "*" in perm["allow"]:
return True, "ALLOW org-admin"
if action_key not in perm["allow"]:
return False, f"ACTION_NOT_ALLOWED {role} {action_key}"
if perm["scope"] == "project":
if not role_project or not target_project:
return False, "PROJECT_SCOPE_REQUIRED"
if role_project != target_project:
return False, f"CROSS_TENANT {role_project}!={target_project}"
return True, f"ALLOW {role} {action_key}"
def main() -> int:
ap = argparse.ArgumentParser()
sub = ap.add_subparsers(dest="cmd", required=True)
c = sub.add_parser("check")
c.add_argument("--role", required=True)
c.add_argument("--resource", required=True)
c.add_argument("--action", required=True)
c.add_argument("--role-project", default="")
c.add_argument("--target-project", default="")
c.add_argument("--sensitive", action="store_true")
s = sub.add_parser("check-sod")
s.add_argument("--proposer", required=True)
s.add_argument("--approver", required=True)
p = sub.add_parser("list-roles")
m = sub.add_parser("map-claim")
m.add_argument("--claim", required=True, help="IdP-issued role/group claim")
args = ap.parse_args()
if args.cmd == "list-roles":
for role, perm in PERMISSIONS.items():
print(f"{role} scope={perm['scope']} allow={sorted(perm['allow'])}")
return 0
if args.cmd == "map-claim":
role = CLAIM_ROLE_MAP.get(args.claim)
if role is None:
print(f"RBAC_DENY UNKNOWN_CLAIM {args.claim}", file=sys.stderr)
return 1
print(role)
return 0
if args.cmd == "check-sod":
if args.proposer == args.approver:
print(f"RBAC_DENY SOD_SELF_APPROVAL actor={args.approver}", file=sys.stderr)
return 1
print(f"RBAC_ALLOW SOD_OK proposer={args.proposer} approver={args.approver}")
return 0
allowed, reason = decide(
args.role, args.resource, args.action, args.role_project, args.target_project, args.sensitive
)
if allowed:
print(f"RBAC_ALLOW {reason}")
return 0
print(f"RBAC_DENY {reason}", file=sys.stderr)
return 1
if __name__ == "__main__":
raise SystemExit(main())
@@ -0,0 +1,133 @@
#!/usr/bin/env python3
"""CASAN self-improve core (Plan-04, harness-owned).
Reads real telemetry (provider-usage / metrics JSONL + optional drift report) and
emits improvement PROPOSALS (dry-run, never writes). Applying a proposal requires
human approval (`--approval`) and goes through the governed settings store
(control-plane-settings.py) so every change is audited. Loosen / security-sensitive
proposals always require approval.
Subcommands:
propose --metrics <jsonl> [--drift <json>] -> proposals JSON on stdout (no writes)
apply --proposals <json> --id <ID> [--approval <tok>] -> governed set (needs approval)
"""
import argparse
import json
import os
import statistics
import subprocess
import sys
def read_jsonl(path):
rows = []
if not path or not os.path.isfile(path):
return rows
with open(path, encoding="utf-8") as fh:
for line in fh:
line = line.strip()
if not line:
continue
try:
rows.append(json.loads(line))
except ValueError:
continue
return rows
def build_proposals(metrics_rows, drift):
proposals = []
costs = [r["cost_usd"] for r in metrics_rows if isinstance(r.get("cost_usd"), (int, float))]
if costs:
cap = round(max(costs) * 1.5, 4)
proposals.append({
"id": "P-COST-CAP",
"type": "calibrate_cost_cap",
"key": "cost.absolute_cap_usd",
"value": cap,
"direction": "tighten",
"security_sensitive": False,
"reason": f"observed max cost {max(costs)}; set cap to 1.5x = {cap}",
})
if drift and (drift.get("drift") is True or drift.get("entries")):
proposals.append({
"id": "P-GOLDEN",
"type": "update_golden",
"key": None,
"value": None,
"direction": "loosen",
"security_sensitive": True,
"reason": "drift detected; updating golden may hide real regressions — needs review",
})
return proposals
def cmd_propose(args):
metrics = read_jsonl(args.metrics)
drift = None
if args.drift and os.path.isfile(args.drift):
try:
drift = json.load(open(args.drift, encoding="utf-8"))
except ValueError:
drift = None
proposals = build_proposals(metrics, drift)
print(json.dumps({"proposals": proposals, "count": len(proposals)}, ensure_ascii=False, indent=2))
return 0
def cmd_apply(args):
try:
data = json.load(open(args.proposals, encoding="utf-8"))
except (OSError, ValueError):
print(f"IMPROVE_DENY PROPOSALS_UNREADABLE {args.proposals}", file=sys.stderr)
return 1
proposal = next((p for p in data.get("proposals", []) if p.get("id") == args.id), None)
if proposal is None:
print(f"IMPROVE_DENY UNKNOWN_PROPOSAL {args.id}", file=sys.stderr)
return 1
# Proposal != application: applying ALWAYS requires human approval (Plan-04).
if not (args.approval or "").strip():
print(f"IMPROVE_DENY APPROVAL_REQUIRED {args.id}", file=sys.stderr)
return 1
if proposal.get("key") is None:
# Non-settings proposal (e.g. update_golden) — record intent, no auto-apply.
print(f"IMPROVE_MANUAL {args.id} type={proposal.get('type')} (no auto-apply; do it under review)")
return 0
cps = os.path.join(os.path.dirname(__file__), "control-plane-settings.py")
cmd = [
sys.executable, cps, "set", proposal["key"], json.dumps(proposal["value"]),
"--actor", "casan-improve", "--reason", f"auto-improve {args.id}",
]
if proposal.get("security_sensitive"):
cmd += ["--approval", args.approval]
result = subprocess.run(cmd, capture_output=True, text=True)
if result.returncode != 0:
print(f"IMPROVE_DENY GOVERNED_SET_FAILED {result.stderr.strip()}", file=sys.stderr)
return 1
print(f"IMPROVE_APPLIED {args.id} key={proposal['key']} value={proposal['value']}")
return 0
def main() -> int:
ap = argparse.ArgumentParser()
sub = ap.add_subparsers(dest="cmd", required=True)
pr = sub.add_parser("propose")
pr.add_argument("--metrics", default="")
pr.add_argument("--drift", default="")
ap_ = sub.add_parser("apply")
ap_.add_argument("--proposals", required=True)
ap_.add_argument("--id", required=True)
ap_.add_argument("--approval", default="")
args = ap.parse_args()
if args.cmd == "propose":
return cmd_propose(args)
if args.cmd == "apply":
return cmd_apply(args)
return 2
if __name__ == "__main__":
raise SystemExit(main())
@@ -32,14 +32,71 @@ def parse_requirements(path: str):
return [seen[k] for k in sorted(seen)]
def existing_files(root: str, values):
def normalize_entry(value):
"""Accept either a plain path string or an object with symbol/line refs.
Backward compatible: a bare string behaves exactly as file-level tracing.
Object form: {"file": "path", "symbols": ["name", ...], "lines": [n, ...]}.
"""
if isinstance(value, str):
return value, [], []
if isinstance(value, dict):
return value.get("file", ""), list(value.get("symbols", [])), list(value.get("lines", []))
return "", [], []
def symbol_present(text: str, symbol: str) -> bool:
"""Symbol-level check: the symbol appears as a definition or reference.
Covers common TS/JS/Python forms: `class X`, `function x`, `x(`, `const x`,
`x =`, `x:` (method/property). Deliberately permissive but anchored on word
boundaries so a substring alone does not count.
"""
esc = re.escape(symbol)
patterns = [
rf"\b(?:function|class|interface|type|enum|const|let|var|def)\s+{esc}\b",
rf"\b{esc}\s*[=:(]",
]
return any(re.search(p, text) for p in patterns)
def resolve_files(root: str, values):
"""Resolve file existence plus optional symbol/line coverage.
Returns present files, missing files, per-file symbol results, and the list
of unsatisfied symbol/line references (which make the requirement FAIL).
"""
present, missing = [], []
for rel in values or []:
if os.path.isfile(os.path.join(root, rel)):
present.append(rel)
else:
symbol_results = []
missing_symbols = []
missing_lines = []
for value in values or []:
rel, symbols, lines = normalize_entry(value)
if not rel:
continue
abs_path = os.path.join(root, rel)
if not os.path.isfile(abs_path):
missing.append(rel)
return present, missing
for sym in symbols:
missing_symbols.append(f"{rel}#{sym}")
for ln in lines:
missing_lines.append(f"{rel}:L{ln}")
continue
present.append(rel)
if not symbols and not lines:
continue
with open(abs_path, encoding="utf-8", errors="replace") as fh:
text = fh.read()
total_lines = text.count("\n") + 1
for sym in symbols:
found = symbol_present(text, sym)
symbol_results.append({"file": rel, "symbol": sym, "found": found})
if not found:
missing_symbols.append(f"{rel}#{sym}")
for ln in lines:
if not isinstance(ln, int) or ln < 1 or ln > total_lines:
missing_lines.append(f"{rel}:L{ln}")
return present, missing, symbol_results, missing_symbols, missing_lines
def main() -> int:
@@ -57,11 +114,30 @@ def main() -> int:
rows = []
failures = []
total_symbols = 0
total_symbols_found = 0
for req in reqs:
entry = mapping.get(req["id"], {})
code, missing_code = existing_files(root, entry.get("code", []))
tests, missing_tests = existing_files(root, entry.get("tests", []))
status = "PASS" if code and tests and not missing_code and not missing_tests else "FAIL"
code, missing_code, code_syms, code_missing_syms, code_missing_lines = resolve_files(
root, entry.get("code", [])
)
tests, missing_tests, test_syms, test_missing_syms, test_missing_lines = resolve_files(
root, entry.get("tests", [])
)
symbol_results = code_syms + test_syms
missing_symbols = code_missing_syms + test_missing_syms
missing_lines = code_missing_lines + test_missing_lines
total_symbols += len(symbol_results)
total_symbols_found += sum(1 for s in symbol_results if s["found"])
ok = (
code
and tests
and not missing_code
and not missing_tests
and not missing_symbols
and not missing_lines
)
status = "PASS" if ok else "FAIL"
row = {
"id": req["id"],
"name": req["name"],
@@ -70,6 +146,9 @@ def main() -> int:
"tests": tests,
"missing_code": missing_code,
"missing_tests": missing_tests,
"symbol_refs": symbol_results,
"missing_symbols": missing_symbols,
"missing_lines": missing_lines,
}
rows.append(row)
if status != "PASS":
@@ -84,6 +163,9 @@ def main() -> int:
"requirements": len(reqs),
"passed": sum(1 for r in rows if r["status"] == "PASS"),
"failed": len(failures),
"symbol_refs": total_symbols,
"symbols_found": total_symbols_found,
"symbols_missing": total_symbols - total_symbols_found,
"orphan_mappings": orphan_mappings,
},
"matrix": rows,
@@ -97,14 +179,15 @@ def main() -> int:
for row in failures:
print(
f"TRACEABILITY_FAIL {row['id']} code={len(row['code'])} tests={len(row['tests'])} "
f"missing_code={len(row['missing_code'])} missing_tests={len(row['missing_tests'])}",
f"missing_code={len(row['missing_code'])} missing_tests={len(row['missing_tests'])} "
f"missing_symbols={len(row['missing_symbols'])} missing_lines={len(row['missing_lines'])}",
file=sys.stderr,
)
if orphan_mappings:
print(f"TRACEABILITY_WARN orphan_mappings={','.join(orphan_mappings)}", file=sys.stderr)
print(
f"TRACEABILITY_MATRIX requirements={len(reqs)} pass={out['summary']['passed']} "
f"fail={len(failures)} out={args.out}"
f"fail={len(failures)} symbols={total_symbols_found}/{total_symbols} out={args.out}"
)
if args.gate and failures:
return 1