feat: harden chat state by tenant
This commit is contained in:
@@ -13,6 +13,7 @@ import argparse
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
from datetime import datetime, timezone
|
||||
|
||||
@@ -31,6 +32,7 @@ def project_root() -> str:
|
||||
|
||||
ROOT = project_root()
|
||||
HARNESS_ROOT = os.path.join(ROOT, "packages", "casan-harness")
|
||||
TENANT_STORE = os.path.join(HARNESS_ROOT, "scripts", "bash", "tenant-store.sh")
|
||||
|
||||
|
||||
def now_iso() -> str:
|
||||
@@ -38,9 +40,26 @@ def now_iso() -> str:
|
||||
|
||||
|
||||
def inbox_path() -> str:
|
||||
return os.environ.get("CASAN_APPROVAL_INBOX_FILE") or os.path.join(
|
||||
ROOT, ".specify", "level5", "approval-inbox.json"
|
||||
)
|
||||
if os.environ.get("CASAN_APPROVAL_INBOX_FILE"):
|
||||
return guarded_override(os.environ["CASAN_APPROVAL_INBOX_FILE"])
|
||||
if os.environ.get("CASAN_TENANT_ID"):
|
||||
return tenant_path("approvals/approval-inbox.json")
|
||||
return os.path.join(ROOT, ".specify", "level5", "approval-inbox.json")
|
||||
|
||||
|
||||
def tenant_path(logical: str) -> str:
|
||||
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()
|
||||
|
||||
|
||||
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 policy_path() -> str:
|
||||
|
||||
@@ -29,6 +29,7 @@ ROOT = project_root()
|
||||
HARNESS_ROOT = os.path.join(ROOT, "packages", "casan-harness")
|
||||
HARNESS_BIN = os.path.join(HARNESS_ROOT, "scripts", "bash")
|
||||
RBAC = os.path.join(HARNESS_BIN, "rbac-check.py")
|
||||
TENANT_STORE = os.path.join(HARNESS_BIN, "tenant-store.sh")
|
||||
GENESIS_HASH = "0" * 64
|
||||
|
||||
|
||||
@@ -44,16 +45,33 @@ 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 registry_path() -> str:
|
||||
return os.environ.get("CASAN_AGENT_REGISTRY_FILE") or os.path.join(HARNESS_ROOT, "config", "agent-registry.yaml")
|
||||
|
||||
|
||||
def audit_path() -> str:
|
||||
return os.environ.get("CASAN_AGENT_BIND_AUDIT_LOG") or os.path.join(state_root(), "logs", "chat", "agent-bindings.jsonl")
|
||||
return guarded_override(os.environ["CASAN_AGENT_BIND_AUDIT_LOG"]) if os.environ.get("CASAN_AGENT_BIND_AUDIT_LOG") else tenant_path("chat/agent-bindings.jsonl", "logs/chat/agent-bindings.jsonl")
|
||||
|
||||
|
||||
def head_path() -> str:
|
||||
return os.environ.get("CASAN_AGENT_BIND_AUDIT_HEAD") or os.path.join(state_root(), "logs", "chat", "agent-bindings-head.txt")
|
||||
return guarded_override(os.environ["CASAN_AGENT_BIND_AUDIT_HEAD"]) if os.environ.get("CASAN_AGENT_BIND_AUDIT_HEAD") else tenant_path("chat/agent-bindings-head.txt", "logs/chat/agent-bindings-head.txt")
|
||||
|
||||
|
||||
def load_registry():
|
||||
@@ -149,6 +167,8 @@ def run_rbac(args):
|
||||
|
||||
|
||||
def bind(args) -> int:
|
||||
if args.tenant and args.tenant != "default":
|
||||
os.environ["CASAN_TENANT_ID"] = args.tenant
|
||||
registry = load_registry()
|
||||
if registry.get("_error"):
|
||||
out = decision_payload(args, "DENIED", registry["_error"])
|
||||
|
||||
@@ -35,30 +35,49 @@ HARNESS_BIN = os.path.join(HARNESS_ROOT, "scripts", "bash")
|
||||
ROUTER = os.path.join(HARNESS_BIN, "prompt-mode-router.py")
|
||||
SECURITY = os.path.join(HARNESS_BIN, "security-check.sh")
|
||||
ACTION_GATE = os.path.join(HARNESS_BIN, "action-gate.sh")
|
||||
TENANT_STORE = os.path.join(HARNESS_BIN, "tenant-store.sh")
|
||||
TENANT_CRYPT = os.path.join(HARNESS_BIN, "tenant-crypt.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 config_path() -> str:
|
||||
return os.environ.get("CASAN_OPERATOR_ACTIONS_FILE") or os.path.join(HARNESS_ROOT, "config", "operator-actions.yaml")
|
||||
|
||||
|
||||
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 metrics_path() -> str:
|
||||
return os.environ.get("CASAN_CHAT_METRICS_LOG") or os.path.join(state_root(), "logs", "cost", "metrics.jsonl")
|
||||
return guarded_override(os.environ["CASAN_CHAT_METRICS_LOG"]) if os.environ.get("CASAN_CHAT_METRICS_LOG") else tenant_path("telemetry/cost/metrics.jsonl", "logs/cost/metrics.jsonl")
|
||||
|
||||
|
||||
def artifact_dir() -> str:
|
||||
return os.path.join(state_root(), "logs", "chat", "operator-artifacts")
|
||||
return tenant_path("chat/operator-artifacts", "logs/chat/operator-artifacts")
|
||||
|
||||
|
||||
def now_iso() -> str:
|
||||
@@ -181,9 +200,18 @@ def record_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
|
||||
subprocess.run(["bash", TENANT_CRYPT, "encrypt", path, path + ".enc"], cwd=ROOT, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
||||
|
||||
|
||||
def record_metrics(trace_id: str, message: str, answer: str, status: str, latency_ms: int, action_id: str):
|
||||
input_tokens = len(message.split())
|
||||
output_tokens = len(answer.split())
|
||||
@@ -287,6 +315,8 @@ def list_actions(args) -> int:
|
||||
|
||||
|
||||
def run(args) -> int:
|
||||
if args.tenant and args.tenant != "default":
|
||||
os.environ["CASAN_TENANT_ID"] = args.tenant
|
||||
started = datetime.now(timezone.utc)
|
||||
trace_id = str(uuid.uuid4())
|
||||
chat_id = args.chat_id or "chat-default"
|
||||
|
||||
@@ -39,22 +39,41 @@ ROOT = project_root()
|
||||
HARNESS_BIN = os.path.join(ROOT, "packages", "casan-harness", "scripts", "bash")
|
||||
ROUTER = os.path.join(HARNESS_BIN, "prompt-mode-router.py")
|
||||
SECURITY = os.path.join(HARNESS_BIN, "security-check.sh")
|
||||
TENANT_STORE = os.path.join(HARNESS_BIN, "tenant-store.sh")
|
||||
TENANT_CRYPT = os.path.join(HARNESS_BIN, "tenant-crypt.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 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 metrics_path() -> str:
|
||||
return os.environ.get("CASAN_CHAT_METRICS_LOG") or os.path.join(state_root(), "logs", "cost", "metrics.jsonl")
|
||||
return guarded_override(os.environ["CASAN_CHAT_METRICS_LOG"]) if os.environ.get("CASAN_CHAT_METRICS_LOG") else tenant_path("telemetry/cost/metrics.jsonl", "logs/cost/metrics.jsonl")
|
||||
|
||||
|
||||
def now_iso() -> str:
|
||||
@@ -223,9 +242,18 @@ def record_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
|
||||
subprocess.run(["bash", TENANT_CRYPT, "encrypt", path, path + ".enc"], cwd=ROOT, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
||||
|
||||
|
||||
def record_metrics(trace_id: str, message: str, answer: str, status: str, latency_ms: int):
|
||||
input_tokens = len(message.split())
|
||||
output_tokens = len(answer.split())
|
||||
@@ -253,6 +281,8 @@ def record_metrics(trace_id: str, message: str, answer: str, status: str, latenc
|
||||
|
||||
|
||||
def ask(args):
|
||||
if args.tenant and args.tenant != "default":
|
||||
os.environ["CASAN_TENANT_ID"] = args.tenant
|
||||
started = datetime.now(timezone.utc)
|
||||
trace_id = str(uuid.uuid4())
|
||||
message = args.message
|
||||
|
||||
@@ -33,14 +33,32 @@ def project_root() -> str:
|
||||
ROOT = project_root()
|
||||
BIN = os.path.join(ROOT, "packages", "casan-harness", "scripts", "bash")
|
||||
LOOP_TRACE = os.path.join(BIN, "loop-trace.py")
|
||||
TENANT_STORE = os.path.join(BIN, "tenant-store.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 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 sha_text(text: str) -> str:
|
||||
@@ -87,12 +105,12 @@ def resolve_path(path: str) -> str:
|
||||
def loop_state_root(loop_run):
|
||||
explicit = os.environ.get("CASAN_LOOP_STATE_ROOT")
|
||||
if explicit:
|
||||
return explicit
|
||||
return guarded_override(explicit)
|
||||
artifact = loop_run.get("artifact") or ""
|
||||
marker = os.sep + "logs" + os.sep + "chat" + os.sep + "loop-runs" + os.sep
|
||||
if marker in artifact:
|
||||
return artifact.split(marker, 1)[0] + os.sep + "logs" + os.sep + "chat" + os.sep + "loop-state"
|
||||
return os.path.join(state_root(), "logs", "chat", "loop-state")
|
||||
return tenant_path("chat/loop-state", "logs/chat/loop-state")
|
||||
|
||||
|
||||
def run_loop_trace(loop_run, cmd: str):
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -155,6 +155,7 @@ run "phase-chat-stream-hold" bash "$TESTS/phase-chat-stream-hold-tests.sh"
|
||||
run "phase-chat-replay" bash "$TESTS/phase-chat-replay-tests.sh"
|
||||
run "phase-chat-approval" bash "$TESTS/phase-chat-approval-tests.sh"
|
||||
run "phase-chat-codegen" bash "$TESTS/phase-chat-codegen-tests.sh"
|
||||
run "phase-chat-tenant" bash "$TESTS/phase-chat-tenant-tests.sh"
|
||||
|
||||
# ARCH-02: coverage cannot silently drop; ARCH-01: harness/policy cannot silently
|
||||
# drift. Both SKIP cleanly when no manifest is provisioned (non-strict dev/CI).
|
||||
|
||||
Reference in New Issue
Block a user