feat: harden chat state by tenant

This commit is contained in:
thanhnv
2026-07-08 23:38:22 +09:00
parent b3b0544ba8
commit c800f7edf7
20 changed files with 380 additions and 54 deletions
@@ -38,22 +38,43 @@ PREFLIGHT = os.path.join(BIN, "harness-preflight.sh")
CONTEXT_SCAN = os.path.join(BIN, "context-assemble-scan.sh")
TOOL_OUTPUT_SCAN = os.path.join(BIN, "tool-output-scan.sh")
ARTIFACT_SCAN = os.path.join(BIN, "artifact-scan.sh")
TENANT_STORE = os.path.join(BIN, "tenant-store.sh")
TENANT_CRYPT = os.path.join(BIN, "tenant-crypt.sh")
KILL_SWITCH = os.path.join(BIN, "kill-switch.sh")
COST_SPIKE = os.path.join(BIN, "cost-spike-detect.sh")
def state_root() -> str:
return os.environ.get("CASAN_STATE_ROOT") or os.path.join(ROOT, ".specify")
def tenant_path(logical: str, fallback: str) -> str:
if os.environ.get("CASAN_TENANT_ID"):
r = subprocess.run(["bash", TENANT_STORE, "resolve", logical], cwd=ROOT, capture_output=True, text=True)
if r.returncode != 0:
raise SystemExit((r.stderr or r.stdout or "TENANT_DENIED").strip())
return r.stdout.strip()
return os.path.join(state_root(), fallback)
def guarded_override(path: str) -> str:
if path and os.environ.get("CASAN_TENANT_ID"):
r = subprocess.run(["bash", TENANT_STORE, "guard", path], cwd=ROOT, capture_output=True, text=True)
if r.returncode != 0:
raise SystemExit((r.stderr or r.stdout or "TENANT_DENIED").strip())
return path
def now_iso() -> str:
return datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
def audit_path() -> str:
return os.environ.get("CASAN_CHAT_AUDIT_LOG") or os.path.join(state_root(), "logs", "chat", "chat-turns.jsonl")
return guarded_override(os.environ["CASAN_CHAT_AUDIT_LOG"]) if os.environ.get("CASAN_CHAT_AUDIT_LOG") else tenant_path("chat/chat-turns.jsonl", "logs/chat/chat-turns.jsonl")
def head_path() -> str:
return os.environ.get("CASAN_CHAT_AUDIT_HEAD") or os.path.join(state_root(), "logs", "chat", "chat-head.txt")
return guarded_override(os.environ["CASAN_CHAT_AUDIT_HEAD"]) if os.environ.get("CASAN_CHAT_AUDIT_HEAD") else tenant_path("chat/chat-head.txt", "logs/chat/chat-head.txt")
def sha(text: str) -> str:
@@ -147,11 +168,11 @@ def write_text(path: str, text: str):
def loop_dir(run_id: str) -> str:
return os.path.join(state_root(), "logs", "chat", "loop-runs", run_id)
return tenant_path(f"chat/loop-runs/{run_id}", f"logs/chat/loop-runs/{run_id}")
def codegen_dir(run_id: str) -> str:
return os.path.join(state_root(), "logs", "chat", "codegen-artifacts", run_id)
return tenant_path(f"chat/codegen-artifacts/{run_id}", f"logs/chat/codegen-artifacts/{run_id}")
def load_chat_head() -> str:
@@ -177,9 +198,65 @@ def append_chat_turn(base):
os.makedirs(os.path.dirname(head_path()), exist_ok=True)
with open(head_path(), "w", encoding="utf-8") as fh:
fh.write(record_hash + "\n")
encrypt_chat_audit_snapshot(path)
return rec
def encrypt_chat_audit_snapshot(path: str):
if not os.environ.get("CASAN_TENANT_ID"):
return
if not os.path.isfile(path):
return
enc = path + ".enc"
subprocess.run(["bash", TENANT_CRYPT, "encrypt", path, enc], cwd=ROOT, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
def tenant_runtime_guard(args):
if args.tenant and args.tenant != "default":
os.environ["CASAN_TENANT_ID"] = args.tenant
if os.environ.get("CASAN_TENANT_ID"):
ks = subprocess.run(["bash", KILL_SWITCH, "check", "tenant", os.environ["CASAN_TENANT_ID"]], cwd=ROOT, capture_output=True, text=True)
if ks.returncode != 0:
print(json.dumps({
"success": False,
"mode": "BLOCK",
"risk": "high",
"decision": "HALTED",
"answer": f"Tenant kill-switch active for {os.environ['CASAN_TENANT_ID']}.",
"sources": [],
"certified": False,
"audit": {},
"router": {"reason": "tenant_kill_switch_active", "matched_rules": ["tenant_kill_switch"]},
}, ensure_ascii=False))
return 3
if os.environ.get("CASAN_COST_CUMULATIVE_BUDGET_TOKENS"):
quota = subprocess.run(["bash", COST_SPIKE], cwd=ROOT, capture_output=True, text=True)
if quota.returncode == 2:
print(json.dumps({
"success": False,
"mode": "BLOCK",
"risk": "high",
"decision": "HALTED",
"answer": f"Tenant quota exceeded for {os.environ['CASAN_TENANT_ID']}.",
"sources": [],
"certified": False,
"audit": {},
"router": {"reason": "tenant_quota_exceeded", "matched_rules": ["tenant_quota"]},
}, ensure_ascii=False))
return 3
return 0
def loop_env_for(args):
env = {
**os.environ,
"CASAN_LOOP_STATE_ROOT": os.environ.get("CASAN_LOOP_STATE_ROOT") or tenant_path("chat/loop-state", "logs/chat/loop-state"),
}
if os.environ.get("CASAN_TENANT_ID"):
env["CASAN_TENANT_ID"] = os.environ["CASAN_TENANT_ID"]
return env
def run_preflight_and_context(run_id: str, draft_path: str):
preflight_out = os.path.join(loop_dir(run_id), "preflight.json")
r = subprocess.run(["bash", PREFLIGHT, draft_path, preflight_out, "--model", "local:chat-turn"], cwd=ROOT, capture_output=True, text=True)
@@ -235,11 +312,7 @@ def certify_operator_draft(args, router, binding):
"success_criteria": criteria_path,
}, 2
loop_env = {
**os.environ,
"CASAN_LOOP_STATE_ROOT": os.environ.get("CASAN_LOOP_STATE_ROOT") or os.path.join(state_root(), "logs", "chat", "loop-state"),
"CASAN_TENANT_ID": args.tenant,
}
loop_env = loop_env_for(args)
dlevel = f"L{binding.get('delegation_level', 0)}"
r = subprocess.run([
"bash", LOOP_RUN,
@@ -318,11 +391,7 @@ def certify_codegen_draft(args, router, binding):
"artifact_scan": {"ok": False, "output": scan_out},
}, 2
loop_env = {
**os.environ,
"CASAN_LOOP_STATE_ROOT": os.environ.get("CASAN_LOOP_STATE_ROOT") or os.path.join(state_root(), "logs", "chat", "loop-state"),
"CASAN_TENANT_ID": args.tenant,
}
loop_env = loop_env_for(args)
dlevel = f"L{binding.get('delegation_level', 0)}"
r = subprocess.run([
"bash", LOOP_RUN,
@@ -530,6 +599,9 @@ def submit_escalation(args, router, binding):
def ask(args) -> int:
guard_rc = tenant_runtime_guard(args)
if guard_rc != 0:
return guard_rc
router = classify(args.message)
bind_rc, binding = bind_agent(args, router)
if bind_rc != 0: