refactor(structure): promote app to repo root + remove redundant workspace cruft
Standard production layout: the OKR app (was nested under AINative_OKR_CASAN5/) is now
the repository root. No more wrapper directory.
- Promote AINative_OKR_CASAN5/* -> repo root (backend/ frontend/ packages/ apps/
.specify/ docs/ infra/ nginx/ scripts/ + configs). Merge tool dirs: .gitea (kept the
active deploy ci.yml, added harness-ci.yml + runbooks), .claude (agents/commands +
launch.json), .github moved up.
- Remove redundant: 00_SUBMISSION_PACKAGE, scattered root notes (FPT_CASAN_Full.md,
tu-tuong-casan.md, casan-tu-sinh..., casan_harness_assessment.md, source-review...,
README_CASAN5_REFINED.md), casan-next-plans/ and optimize-docs/ (competition/planning
artifacts — roadmap + design history preserved in git log / commit messages).
- Update all references to the old layout:
- .gitea/workflows/{ci,harness-ci}.yml, .github/workflows/{ci,deploy}.yml:
working-directory .; drop AINative_OKR_CASAN5/ prefix; .specify/{tests,scripts}
-> packages/casan-harness/... (.specify/logs state kept)
- .claude/launch.json, .gitea/*-runbook.md: path prefixes
- CLAUDE.md, README.md: docs/input -> apps/okr/domain/input
- policy-bundle.yaml: 8 policy paths -> packages/casan-harness/...; manifest re-signed
- secrets-scan.sh: fixture excludes -> new package/domain paths.
Full gate from the new root: PASS=64 FAIL=0 SKIP=3.
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
7101af9fd4
commit
36a4812ef3
@@ -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())
|
||||
Reference in New Issue
Block a user