feat: casan chat optz

This commit is contained in:
thanhnv
2026-07-19 12:14:12 +07:00
parent 709b6cccd6
commit f462079435
16 changed files with 486 additions and 90 deletions
@@ -43,6 +43,7 @@ TENANT_STORE = os.path.join(HARNESS_BIN, "tenant-store.sh")
TENANT_CRYPT = os.path.join(HARNESS_BIN, "tenant-crypt.sh")
MODEL_ROUTER = os.path.join(HARNESS_BIN, "model-router.sh")
CONTEXT_COMPRESS = os.path.join(HARNESS_BIN, "context-compress.py")
PYTHON_BIN = sys.executable or "/usr/bin/python3"
def model_providers_path() -> str:
@@ -126,6 +127,27 @@ def provenance(source: str, path: str, verified=True):
}
def parse_security_verdict(output: str, return_code: int):
trace_match = re.search(r"trace_id=([^\s]+)", output)
risk_match = re.search(r"risk=([^\s]+)", output)
categories_match = re.search(r"categories=([^\s]*)", output)
categories = [item for item in (categories_match.group(1).split(",") if categories_match else []) if item]
if not categories and "semantic-strict-unavailable" in output:
categories = ["semantic-availability"]
elif not categories and "prompt-injection:" in output:
categories = ["prompt-injection"]
elif not categories and "secret-in-input" in output:
categories = ["secret-in-input"]
return {
"gate": "H4",
"outcome": "deny" if return_code != 0 else ("degraded" if "semantic-availability" in categories else "allow"),
"reason_code": "H4_INPUT_BLOCKED" if return_code != 0 else ("H4_SEMANTIC_DEGRADED" if "semantic-availability" in categories else "H4_INPUT_ALLOWED"),
"risk": risk_match.group(1) if risk_match else ("high" if return_code != 0 else "low"),
"trace_id": trace_match.group(1) if trace_match else "",
"categories": categories,
}
def run_security(text: str, mode: str):
with tempfile.TemporaryDirectory() as td:
inp = os.path.join(td, "in.txt")
@@ -136,11 +158,12 @@ def run_security(text: str, mode: str):
safe = ""
if os.path.exists(out):
safe = open(out, encoding="utf-8").read()
return r.returncode, safe.strip(), (r.stdout + r.stderr).strip()
output = (r.stdout + r.stderr).strip()
return r.returncode, safe.strip(), output, parse_security_verdict(output, r.returncode)
def classify(message: str):
r = subprocess.run(["python3", ROUTER, "classify", "--message", message], cwd=ROOT, capture_output=True, text=True)
r = subprocess.run([PYTHON_BIN, ROUTER, "classify", "--message", message], cwd=ROOT, capture_output=True, text=True)
if r.returncode != 0:
return {"mode": "BLOCK", "risk": "high", "reason": "router_failed", "matched_rules": [r.stderr.strip()]}
try:
@@ -174,7 +197,91 @@ def terms(text: str):
return [t for t in raw if t not in STOPWORDS][:20]
def is_latest_run_query(query: str) -> bool:
normalized = re.sub(r"\s+", " ", query.lower()).strip()
return any(phrase in normalized for phrase in (
"lần chạy cuối", "lần chạy gần nhất", "run cuối", "latest run",
"last run", "most recent run", "bằng chứng lần chạy", "evidence run",
))
def field_line(path: str, field: str) -> int:
try:
with open(path, encoding="utf-8", errors="ignore") as handle:
for line_no, line in enumerate(handle, start=1):
if f'"{field}"' in line:
return line_no
except OSError:
pass
return 1
def collect_latest_goal_sources():
tenant = os.environ.get("CASAN_TENANT_ID", "default")
goals_root = os.path.join(state_root(), "state", "goals", tenant)
candidates = []
if not os.path.isdir(goals_root):
return []
for name in os.listdir(goals_root):
if not name.endswith(".json") or name.endswith(".context.json"):
continue
path = os.path.join(goals_root, name)
try:
with open(path, encoding="utf-8") as handle:
goal = json.load(handle)
if not isinstance(goal, dict) or not goal.get("id"):
continue
candidates.append((str(goal.get("updated_at") or goal.get("created_at") or ""), path, goal))
except (OSError, ValueError):
continue
if not candidates:
return []
_, path, goal = max(candidates, key=lambda item: (item[0], item[1]))
stages = goal.get("stages") if isinstance(goal.get("stages"), list) else []
stage_summary = ", ".join(
f"{stage.get('id', 'stage')}={stage.get('status', 'unknown')}"
for stage in stages if isinstance(stage, dict)
) or "no stage evidence"
verification = goal.get("verification") if isinstance(goal.get("verification"), list) else []
verification_summary = ", ".join(
f"{item.get('command', 'check')} (exit {item.get('exit_code', '?')})"
for item in verification if isinstance(item, dict)
) or "no verification evidence"
patch = goal.get("patch_artifact") if isinstance(goal.get("patch_artifact"), dict) else {}
relative = os.path.relpath(path, ROOT)
envelope = provenance("goal-runtime-state", path, True)
common = {
"source_kind": "latest_goal_run",
"run_id": str(goal.get("id")),
"status": str(goal.get("status") or "unknown"),
"updated_at": str(goal.get("updated_at") or ""),
"project": str(goal.get("project") or "default"),
"result": str(goal.get("result") or ""),
"error": goal.get("error"),
}
return [
{
**common, "path": relative, "line": field_line(path, "status"), "score": 100,
"excerpt": f"Run {goal.get('id')} · status={goal.get('status', 'unknown')} · project={goal.get('project', 'default')} · updated={goal.get('updated_at', '')}",
"envelope": envelope,
},
{
**common, "path": relative, "line": field_line(path, "stages"), "score": 99,
"excerpt": f"Stages: {stage_summary}", "envelope": envelope,
},
{
**common, "path": relative, "line": field_line(path, "verification"), "score": 98,
"excerpt": f"Verification: {verification_summary}; patch={patch.get('status', 'none')} {patch.get('path', '')}".strip(),
"envelope": envelope,
},
]
def collect_sources(query: str):
if is_latest_run_query(query):
runtime_sources = collect_latest_goal_sources()
if runtime_sources:
return runtime_sources
roots = whitelist_roots()
qterms = terms(query)
scored = []
@@ -230,6 +337,23 @@ def collect_sources(query: str):
def answer_from_sources(message: str, sources):
if not sources:
return "Khong tim thay nguon trong whitelist evidence/docs nen khong tra loi suy doan."
if sources[0].get("source_kind") == "latest_goal_run":
source = sources[0]
status = source.get("status", "unknown")
result = source.get("result") or "Không có kết luận được ghi nhận."
error = source.get("error")
details = "\n".join(f"- {item['excerpt']} [{item['path']}:{item['line']}]" for item in sources[:3])
error_line = f"\n- Lỗi cuối: {error}" if error else "\n- Lỗi cuối: không có."
return (
f"Bằng chứng của Goal run gần nhất:\n\n"
f"- Run ID: {source.get('run_id')}\n"
f"- Trạng thái: {status}\n"
f"- Project: {source.get('project')}\n"
f"- Cập nhật cuối: {source.get('updated_at')}"
f"{error_line}\n"
f"- Kết luận: {result}\n\n"
f"Evidence:\n{details}"
)
bullets = []
for s in sources[:3]:
bullets.append(f"- {s['path']}:{s['line']} — {s['excerpt']}")
@@ -454,7 +578,7 @@ def load_history(chat_id: str, tenant_id: str, limit: int = 3) -> str:
raw = "\n".join(f"- user: {u}\n casan: {a}" for u, a in recent)
try:
r = subprocess.run(
["python3", CONTEXT_COMPRESS, "--mode", "structural"],
[PYTHON_BIN, CONTEXT_COMPRESS, "--mode", "structural"],
input=raw, cwd=ROOT, capture_output=True, text=True,
)
if r.returncode == 0 and r.stdout.strip():
@@ -508,6 +632,7 @@ def ask(args):
chat_id = args.chat_id or "chat-default"
turn_id = args.turn_id or f"turn-{trace_id[:12]}"
tenant_id = args.tenant or "default"
security_verdict = None
record_trace_event(trace_id, "H1-context", "running", "Classifying prompt contract")
record_trace_event(trace_id, "H1-context", "pass" if router.get("mode") in ("READ_ONLY", "ANALYSIS") else "blocked", router.get("reason", "Prompt classified"), {
"mode": router.get("mode", "BLOCK"),
@@ -552,6 +677,32 @@ def ask(args):
"decision": decision,
"certified": decision == "ANSWERED",
})
verdict = security_verdict or {
"gate": "H4", "outcome": "not_run", "reason_code": "H4_NOT_RUN",
"risk": router.get("risk", "high"), "trace_id": "", "categories": [],
}
if verdict.get("outcome") == "deny":
remediation = [
"Remove credentials, personal data, or instructions that try to bypass system policy.",
"For security research, describe the goal without pasting an executable jailbreak payload.",
]
summary = "H4 stopped this request at the input boundary. No model or tool received the blocked content."
elif verdict.get("outcome") == "degraded":
remediation = ["Semantic classification is temporarily unavailable; deterministic controls remain active."]
summary = "The request continued in read-only mode with deterministic H4 controls."
else:
remediation = []
summary = "The request passed the H4 input boundary."
governance = {
"outcome": verdict.get("outcome", "not_run"),
"gate": verdict.get("gate", "H4"),
"reason_code": verdict.get("reason_code", "H4_NOT_RUN"),
"summary": summary,
"remediation": remediation,
"categories": verdict.get("categories", []),
"security_trace_id": verdict.get("trace_id", ""),
"side_effect_allowed": bool(router.get("side_effect_allowed", False)),
}
return {
"success": decision == "ANSWERED",
"chat_id": chat_id,
@@ -566,6 +717,7 @@ def ask(args):
"synthesis": synthesis or {"mode": "deterministic"},
"audit": {"seq": rec["seq"], "record_hash": rec["record_hash"], "head": rec["record_hash"]},
"router": router,
"governance": governance,
}
if router.get("mode") not in ("READ_ONLY", "ANALYSIS"):
@@ -576,17 +728,17 @@ def ask(args):
role = "analysis" if router.get("mode") == "ANALYSIS" else "read_only"
record_trace_event(trace_id, "H4-security", "running", "Scanning input boundary")
rc, safe_input, scan_msg = run_security(message, "input")
rc, safe_input, scan_msg, security_verdict = run_security(message, "input")
if rc != 0:
record_trace_event(trace_id, "H4-security", "blocked", "Input rejected by security boundary", {"scan": scan_msg})
record_trace_event(trace_id, "H4-security", "blocked", "Input rejected by security boundary", {"reason_code": security_verdict["reason_code"], "categories": security_verdict["categories"], "security_trace_id": security_verdict["trace_id"]})
router["mode"] = "BLOCK"
router["reason"] = "h4_input_denied"
router["matched_rules"] = router.get("matched_rules", []) + [scan_msg]
answer = "Denied by H4 input scan."
router["matched_rules"] = router.get("matched_rules", []) + security_verdict["categories"]
answer = "H4 đã dừng yêu cầu này trước khi gọi model hoặc công cụ. Hãy bỏ dữ liệu nhạy cảm hoặc diễn đạt lại mục tiêu mà không kèm chỉ dẫn vượt qua chính sách."
print(json.dumps(finish("DENIED", answer), ensure_ascii=False))
return 2
record_trace_event(trace_id, "H4-security", "running", "Input passed; output scan pending", {"input_scan": "pass"})
record_trace_event(trace_id, "H4-security", "running", "Input passed; output scan pending", {"input_scan": security_verdict["outcome"], "categories": security_verdict["categories"], "security_trace_id": security_verdict["trace_id"]})
record_trace_event(trace_id, "H2-tool", "running", "Retrieving allowlisted evidence")
sources = collect_sources(safe_input)
history = load_history(chat_id, tenant_id)
@@ -619,13 +771,15 @@ def ask(args):
"model": synthesis.get("model", "none"),
"source_count": len(sources),
})
rc, safe_answer, scan_msg = run_security(answer, "output")
rc, safe_answer, scan_msg, output_verdict = run_security(answer, "output")
if rc != 0:
record_trace_event(trace_id, "H4-security", "blocked", "Output rejected by security boundary", {"scan": scan_msg})
security_verdict = output_verdict
security_verdict["reason_code"] = "H4_OUTPUT_BLOCKED"
record_trace_event(trace_id, "H4-security", "blocked", "Output rejected by security boundary", {"reason_code": security_verdict["reason_code"], "categories": security_verdict["categories"], "security_trace_id": security_verdict["trace_id"]})
router["mode"] = "BLOCK"
router["reason"] = "h4_output_denied"
router["matched_rules"] = router.get("matched_rules", []) + [scan_msg]
result = finish("DENIED", "Denied by H4 output scan.", sources, safe_input, synthesis)
router["matched_rules"] = router.get("matched_rules", []) + security_verdict["categories"]
result = finish("DENIED", "H4 đã giữ lại phản hồi vì phát hiện nội dung nhạy cảm trong đầu ra. Không có nội dung bị chặn nào được trả về giao diện.", sources, safe_input, synthesis)
if getattr(args, "stream", False):
result["phase"] = "final"
print(json.dumps(result, ensure_ascii=False))