Files
CASAN/packages/casan-harness/scripts/bash/goal-orchestrator.py
T
2026-07-11 12:18:59 +09:00

418 lines
19 KiB
Python

#!/usr/bin/env python3
"""Run one governed goal through a local worker and a cloud reviewer.
The job file is tenant-scoped and created by the Control Panel. Credentials are
passed only through the child environment and are never copied into job, trace,
audit, or metric records.
"""
import argparse
import fcntl
import hashlib
import json
import os
import re
import subprocess
import tempfile
import time
import urllib.error
import urllib.request
from datetime import datetime, timezone
def project_root() -> str:
current = os.path.abspath(os.path.dirname(__file__))
while current != os.path.dirname(current):
if os.path.isdir(os.path.join(current, ".specify")):
return current
current = os.path.dirname(current)
raise SystemExit("GOAL_ROOT_NOT_FOUND")
ROOT = project_root()
BIN = os.path.join(ROOT, "packages", "casan-harness", "scripts", "bash")
MODEL_ROUTER = os.environ.get("CASAN_GOAL_MODEL_ROUTER") or os.path.join(BIN, "model-router.sh")
SECURITY = os.path.join(BIN, "security-check.sh")
STATE_ROOT = os.environ.get("CASAN_STATE_ROOT") or os.path.join(ROOT, ".specify")
def now() -> str:
return datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
def sha(value: str) -> str:
return hashlib.sha256(value.encode("utf-8")).hexdigest()
def atomic_json(path: str, payload: dict) -> None:
os.makedirs(os.path.dirname(path), exist_ok=True)
fd, temporary = tempfile.mkstemp(prefix=".goal-", dir=os.path.dirname(path), text=True)
try:
with os.fdopen(fd, "w", encoding="utf-8") as handle:
json.dump(payload, handle, ensure_ascii=False, indent=2)
handle.write("\n")
os.chmod(temporary, 0o600)
os.replace(temporary, path)
finally:
if os.path.exists(temporary):
os.unlink(temporary)
def load_json(path: str) -> dict:
with open(path, encoding="utf-8") as handle:
payload = json.load(handle)
if not isinstance(payload, dict):
raise ValueError("GOAL_JOB_INVALID")
return payload
def append_jsonl(path: str, payload: dict) -> None:
os.makedirs(os.path.dirname(path), exist_ok=True)
with open(path, "a", encoding="utf-8") as handle:
fcntl.flock(handle.fileno(), fcntl.LOCK_EX)
handle.write(json.dumps(payload, ensure_ascii=False) + "\n")
handle.flush()
fcntl.flock(handle.fileno(), fcntl.LOCK_UN)
def trace_path(goal_id: str) -> str:
directory = os.environ.get("CASAN_TRACE_EVENTS_DIR") or os.path.join(STATE_ROOT, "logs", "trace-events")
return os.path.join(directory, f"{goal_id}.jsonl")
def emit(goal_id: str, gate_id: str, status: str, reason: str, evidence=None) -> None:
append_jsonl(trace_path(goal_id), {
"timestamp": now(),
"trace_id": goal_id,
"gate_id": gate_id,
"status": status,
"reason": reason,
"evidence": evidence or {},
})
def update_job(path: str, **changes) -> dict:
job = load_json(path)
job.update(changes)
job["updated_at"] = now()
atomic_json(path, job)
return job
def stage(path: str, stage_id: str, status: str, detail: str, provider="", model="") -> None:
job = load_json(path)
stages = list(job.get("stages", []))
row = next((item for item in stages if item.get("id") == stage_id), None)
if row is None:
row = {"id": stage_id}
stages.append(row)
row.update({"status": status, "detail": detail, "provider": provider, "model": model, "updated_at": now()})
job["stages"] = stages
job["updated_at"] = now()
atomic_json(path, job)
def scan(text: str, mode: str):
with tempfile.TemporaryDirectory() as directory:
source = os.path.join(directory, "input.txt")
output = os.path.join(directory, "output.txt")
with open(source, "w", encoding="utf-8") as handle:
handle.write(text)
environment = os.environ.copy()
# The goal workflow already records and enforces its H4 boundary. Keep
# deterministic injection/secret/PII checks active, but do not turn a
# temporary semantic-classifier outage into a false-positive block.
environment["CASAN_SECURITY_STRICT"] = "0"
result = subprocess.run(
["bash", SECURITY, source, output, mode],
cwd=ROOT,
capture_output=True,
text=True,
env=environment,
timeout=60,
)
safe = ""
if os.path.isfile(output):
with open(output, encoding="utf-8") as handle:
safe = handle.read().strip()
return result.returncode == 0, safe
def call_model(model: str, prompt: str, cloud: bool):
if not model:
return False, "", {}, "model_unconfigured"
with tempfile.TemporaryDirectory() as directory:
prompt_path = os.path.join(directory, "prompt.txt")
output_path = os.path.join(directory, "output.json")
with open(prompt_path, "w", encoding="utf-8") as handle:
handle.write(prompt)
environment = os.environ.copy()
environment["CASAN_PREFLIGHT"] = "1" if cloud else "0"
# Long Markdown objectives need enough time for a local 9B model to
# ingest the brief and produce a bounded plan. The outer goal timeout
# remains the hard ceiling; this only raises the router's 60s default.
environment.setdefault("CASAN_MODEL_TIMEOUT_SEC", os.environ.get("CASAN_GOAL_LOCAL_TIMEOUT_SEC", "240") if not cloud else "120")
environment.setdefault("CASAN_MODEL_GENERATE_MAX_TOKENS", os.environ.get("CASAN_GOAL_MAX_OUTPUT_TOKENS", "1400"))
try:
result = subprocess.run(
["bash", MODEL_ROUTER, prompt_path, output_path, "--role", "generate", "--model", model],
cwd=ROOT,
capture_output=True,
text=True,
env=environment,
timeout=int(os.environ.get("CASAN_GOAL_MODEL_TIMEOUT", "300")),
)
except subprocess.TimeoutExpired:
return False, "", {}, "model_timeout"
if result.returncode != 0 or not os.path.isfile(output_path):
raw = (result.stderr or result.stdout or "").strip().splitlines()
detail = raw[-1][:180] if raw else "no_detail"
detail = re.sub(r"(?i)(bearer|api[_-]?key|token)[=: ]+\S+", r"\1=[redacted]", detail)
return False, "", {}, f"model_exit_{result.returncode}:{detail}"
try:
output = load_json(output_path)
except (OSError, ValueError, json.JSONDecodeError):
return False, "", {}, "model_output_invalid"
text = str(output.get("text") or "").strip()
if not text:
return False, "", {}, "model_output_empty"
metadata = {
"input_tokens": int(output.get("input_tokens") or 0),
"output_tokens": int(output.get("output_tokens") or 0),
"latency_ms": int(output.get("latency_ms") or 0),
}
return True, text, metadata, "ok"
def call_account_model(provider: str, prompt: str):
base_url = os.environ.get("CASAN_AUTH_BRIDGE_URL", "").rstrip("/")
token = os.environ.get("CASAN_AUTH_BRIDGE_TOKEN", "")
if provider not in {"codex", "claude"} or not base_url or not token:
return False, "", {}, "account_bridge_unavailable"
request = urllib.request.Request(
f"{base_url}/v1/models/{provider}/generate",
data=json.dumps({"prompt": prompt}).encode("utf-8"),
headers={"Content-Type": "application/json", "X-CASAN-Bridge-Token": token},
method="POST",
)
try:
with urllib.request.urlopen(request, timeout=310) as response:
payload = json.loads(response.read().decode("utf-8"))
except (urllib.error.URLError, TimeoutError, ValueError):
return False, "", {}, "account_bridge_failed"
text = str(payload.get("text") or "").strip()
if not payload.get("success") or not text:
return False, "", {}, str(payload.get("reason") or "account_model_failed")[:120]
usage = payload.get("usage") if isinstance(payload.get("usage"), dict) else {}
metadata = {
"input_tokens": int(usage.get("input_tokens") or 0),
"output_tokens": int(usage.get("output_tokens") or 0),
"latency_ms": int(payload.get("latency_ms") or 0),
"model": str(payload.get("model") or f"{provider}-account-default"),
}
return True, text, metadata, "ok"
def audit(job: dict, status: str) -> str:
path = os.path.join(STATE_ROOT, "logs", "audit", "goal-orchestrator.jsonl")
head_path = os.path.join(STATE_ROOT, "logs", "audit", "goal-orchestrator-head.txt")
lock_path = os.path.join(STATE_ROOT, "logs", "audit", "goal-orchestrator.lock")
os.makedirs(os.path.dirname(path), exist_ok=True)
with open(lock_path, "a", encoding="utf-8") as lock:
fcntl.flock(lock.fileno(), fcntl.LOCK_EX)
previous = "0" * 64
try:
with open(head_path, encoding="utf-8") as handle:
previous = handle.read().strip() or previous
except OSError:
pass
core = {
"timestamp": now(),
"harness": "H5-governance",
"goal_id": job["id"],
"tenant": job.get("tenant", "default"),
"actor": job.get("actor", "unknown"),
"goal_hash": sha(str(job.get("goal", ""))),
"status": status,
"local_provider": job.get("local_provider", ""),
"cloud_provider": job.get("cloud_provider", ""),
"prev_hash": previous,
}
record_hash = sha(json.dumps(core, sort_keys=True, ensure_ascii=False))
append_jsonl(path, {**core, "record_hash": record_hash})
with open(head_path, "w", encoding="utf-8") as handle:
handle.write(record_hash + "\n")
handle.flush()
os.fsync(handle.fileno())
os.chmod(head_path, 0o600)
fcntl.flock(lock.fileno(), fcntl.LOCK_UN)
return record_hash
def metric(job: dict, status: str, started: float, local_meta: dict, cloud_meta: dict) -> None:
append_jsonl(os.path.join(STATE_ROOT, "logs", "cost", "metrics.jsonl"), {
"timestamp": now(),
"trace_id": job["id"],
"harness": "H6-agentops",
"agent": "goal.orchestrator",
"step": "local-worker-cloud-reviewer",
"status": status,
"exit_code": 0 if status in {"success", "degraded"} else 2,
"latency_ms": int((time.monotonic() - started) * 1000),
"input_tokens": int(local_meta.get("input_tokens", 0)) + int(cloud_meta.get("input_tokens", 0)),
"output_tokens": int(local_meta.get("output_tokens", 0)) + int(cloud_meta.get("output_tokens", 0)),
"total_tokens": sum(int(meta.get(key, 0)) for meta in (local_meta, cloud_meta) for key in ("input_tokens", "output_tokens")),
"cost_estimate": 0.0,
"cost_source": "provider_usage_logs",
"input_hash": sha(str(job.get("goal", ""))),
"output_hash": sha(str(job.get("result", ""))),
})
def run(job_path: str) -> int:
started = time.monotonic()
job = load_json(job_path)
goal_id = str(job["id"])
goal = str(job.get("goal") or "").strip()
local_model = os.environ.get("CASAN_GOAL_LOCAL_MODEL", "")
cloud_model = os.environ.get("CASAN_GOAL_CLOUD_MODEL", "")
account_provider = os.environ.get("CASAN_GOAL_ACCOUNT_PROVIDER", "")
local_meta, cloud_meta = {}, {}
try:
update_job(job_path, status="running", started_at=now())
emit(goal_id, "H1-context", "running", "Validating objective contract")
if len(goal) < 10 or len(goal) > 8000:
raise ValueError("goal_length_invalid")
emit(goal_id, "H1-context", "pass", "Objective accepted", {"goal_hash": sha(goal), "characters": len(goal)})
emit(goal_id, "H4-security", "running", "Scanning objective before model routing")
allowed, safe_goal = scan(goal, "input")
if not allowed:
emit(goal_id, "H4-security", "blocked", "Objective rejected by security boundary")
raise ValueError("goal_security_blocked")
emit(goal_id, "H4-security", "running", "Objective passed; model outputs pending")
stage(job_path, "local-worker", "running", "Local model is developing the primary solution", job.get("local_provider", ""), local_model)
emit(goal_id, "H2-tool", "running", "Local worker is developing a solution", {"provider": job.get("local_provider", ""), "model": local_model})
local_prompt = (
"You are the local CASAN worker. Solve the user's objective concretely. "
"Produce: clarified outcome, assumptions, ordered implementation plan, risks, "
"and verifiable acceptance checks. Respond in the same language as the objective.\n\n"
f"OBJECTIVE:\n{safe_goal}"
)
ok, local_draft, local_meta, reason = call_model(local_model, local_prompt, False)
if not ok:
stage(job_path, "local-worker", "error", reason, job.get("local_provider", ""), local_model)
emit(goal_id, "H2-tool", "error", "Local worker failed", {"reason": reason})
raise RuntimeError(f"local_worker_failed:{reason}")
allowed, safe_local = scan(local_draft, "output")
if not allowed:
emit(goal_id, "H4-security", "blocked", "Local worker output rejected")
raise ValueError("local_output_security_blocked")
stage(job_path, "local-worker", "pass", "Primary solution prepared", job.get("local_provider", ""), local_model)
update_job(job_path, local_draft=safe_local, local_usage=local_meta)
emit(goal_id, "H2-tool", "pass", "Local solution prepared", {"provider": job.get("local_provider", ""), "model": local_model, **local_meta})
stage(job_path, "cloud-reviewer", "running", "Cloud model is challenging and improving the local solution", job.get("cloud_provider", ""), cloud_model)
emit(goal_id, "H3-eval", "running", "Cloud reviewer is evaluating the local solution", {"provider": job.get("cloud_provider", ""), "model": cloud_model})
review_prompt = (
"You are the cloud CASAN reviewer. Critically review the local worker's proposal "
"against the objective. Correct gaps, remove unsafe or unverifiable claims, and "
"return one final actionable solution with ordered steps and acceptance checks. "
"Respond in the same language as the objective.\n\n"
f"OBJECTIVE:\n{safe_goal}\n\nLOCAL WORKER PROPOSAL:\n{safe_local[:16000]}"
)
if account_provider:
cloud_ok, cloud_result, cloud_meta, cloud_reason = call_account_model(account_provider, review_prompt)
if not cloud_ok and os.environ.get("CASAN_GOAL_CLOUD_FALLBACK_MODEL"):
cloud_ok, cloud_result, cloud_meta, cloud_reason = call_model(
os.environ["CASAN_GOAL_CLOUD_FALLBACK_MODEL"], review_prompt, True
)
else:
cloud_ok, cloud_result, cloud_meta, cloud_reason = call_model(cloud_model, review_prompt, True)
if cloud_ok:
allowed, safe_result = scan(cloud_result, "output")
if not allowed:
emit(goal_id, "H4-security", "blocked", "Cloud reviewer output rejected")
raise ValueError("cloud_output_security_blocked")
stage(job_path, "cloud-reviewer", "pass", "Cloud review incorporated", job.get("cloud_provider", ""), cloud_model)
emit(goal_id, "H3-eval", "pass", "Cloud review incorporated", {"provider": job.get("cloud_provider", ""), "model": cloud_model, **cloud_meta})
final_status = "completed"
metric_status = "success"
else:
safe_result = safe_local
stage(job_path, "cloud-reviewer", "warning", cloud_reason, job.get("cloud_provider", ""), cloud_model)
emit(goal_id, "H3-eval", "warning", "Cloud reviewer unavailable; local solution retained", {"reason": cloud_reason})
final_status = "degraded"
metric_status = "degraded"
emit(goal_id, "H4-security", "pass", "Objective and all released outputs passed security scans")
job = update_job(job_path, status=final_status, result=safe_result, cloud_usage=cloud_meta, finished_at=now())
emit(goal_id, "H5-governance", "running", "Anchoring orchestration decision")
audit_hash = audit(job, final_status)
emit(goal_id, "H5-governance", "pass", "Orchestration decision anchored", {"audit_hash": audit_hash, "status": final_status})
emit(goal_id, "H6-agentops", "running", "Recording orchestration telemetry")
metric(job, metric_status, started, local_meta, cloud_meta)
emit(goal_id, "H6-agentops", "pass", "Orchestration telemetry recorded", {"latency_ms": int((time.monotonic() - started) * 1000), "status": metric_status})
emit(goal_id, "H7-orchestration", "pass", "Local worker and cloud review workflow completed", {"status": final_status, "cloud_incorporated": cloud_ok})
update_job(job_path, audit_hash=audit_hash)
return 0
except Exception as error:
reason = str(error)[:120] or error.__class__.__name__
update_job(job_path, status="failed", error=reason, finished_at=now())
emit(goal_id, "H5-governance", "running", "Anchoring failed orchestration decision")
job = load_json(job_path)
audit_hash = audit(job, "failed")
emit(goal_id, "H5-governance", "pass", "Failed decision anchored", {"audit_hash": audit_hash})
emit(goal_id, "H6-agentops", "error", "Orchestration failed", {"reason": reason, "latency_ms": int((time.monotonic() - started) * 1000)})
emit(goal_id, "H7-orchestration", "blocked", "Goal workflow stopped", {"reason": reason})
update_job(job_path, audit_hash=audit_hash)
return 2
def verify_audit() -> int:
path = os.path.join(STATE_ROOT, "logs", "audit", "goal-orchestrator.jsonl")
previous = "0" * 64
records = 0
try:
handle = open(path, encoding="utf-8")
except OSError:
print(json.dumps({"ok": True, "records": 0, "head": previous}))
return 0
with handle:
for line in handle:
if not line.strip():
continue
records += 1
try:
record = json.loads(line)
except ValueError:
print(json.dumps({"ok": False, "records": records, "reason": "invalid_json"}))
return 3
record_hash = str(record.pop("record_hash", ""))
expected = sha(json.dumps(record, sort_keys=True, ensure_ascii=False))
if record.get("prev_hash") != previous or record_hash != expected:
print(json.dumps({"ok": False, "records": records, "reason": "chain_break"}))
return 3
previous = record_hash
print(json.dumps({"ok": True, "records": records, "head": previous}))
return 0
def main() -> int:
parser = argparse.ArgumentParser()
group = parser.add_mutually_exclusive_group(required=True)
group.add_argument("--job-file")
group.add_argument("--verify-audit", action="store_true")
args = parser.parse_args()
if args.verify_audit:
return verify_audit()
path = os.path.abspath(args.job_file)
state = os.path.abspath(STATE_ROOT) + os.sep
if not path.startswith(state):
raise SystemExit("GOAL_JOB_PATH_DENIED")
return run(path)
if __name__ == "__main__":
raise SystemExit(main())