407 lines
18 KiB
Python
407 lines
18 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)
|
|
result = subprocess.run(
|
|
["bash", SECURITY, source, output, mode],
|
|
cwd=ROOT,
|
|
capture_output=True,
|
|
text=True,
|
|
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"
|
|
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("local_worker_failed")
|
|
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())
|