"""Authenticated LAN suite planner; the private Switchyard adapter cannot infer.""" from __future__ import annotations import hashlib import hmac from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer import ipaddress import json import os from pathlib import Path import re import subprocess import threading from suite_contract import (CLAUDE_MODELS, CLAUDE_VERSION, COST_LIMIT, EXECUTION_REVISION, MAX_BODY, MAX_CASES, MAX_RESULT, MODELS, PROMPT_REVISION, PROMPT_SHA256, REVISION, TIMEOUT, Problem, encoded, preflight, validate_request) from suite_jobs import Jobs from suite_policy import POLICY_REVISION from suite_synthetic import allowed_synthetic, fixture def strict_json(raw): """Reject duplicate object keys and non-JSON numeric constants.""" def pairs(items): value = {} for key, item in items: if key in value: raise ValueError("duplicate key") value[key] = item return value def constant(_): raise ValueError("invalid constant") try: return json.loads(raw, object_pairs_hook=pairs, parse_constant=constant) except (ValueError, UnicodeError): raise Problem("invalid_json") from None def credential(header, directory="/vault/secrets"): """Map bearer credentials to server-owned permissions, never request claims.""" candidate = header[7:] if header.startswith("Bearer ") else "" if not candidate or len(candidate) > 256: raise Problem("authentication", 401) for name, providers in (("token", []), ("synthetic-token", ["claude", "codex"]), ("operational-token", ["claude"])): expected = Path(directory, name).read_text().strip() if expected and hmac.compare_digest(candidate, expected): return name, providers raise Problem("authentication", 401) def generalized_claude_approved(): """Read the explicit operator permission for this configured Claude account.""" return os.environ.get("PLANNING_GENERALIZED_CLAUDE_APPROVED") == "true" def authorize(raw, providers, *, operational=False): """Intersect client permissions, request policy, and approved data scope.""" request = validate_request(raw, providers) if request["routing"]["allow_external"] and not allowed_synthetic(request): if not operational or not generalized_claude_approved(): raise Problem("external_data_not_approved", 403) if request["routing"]["allowed_external_providers"] != ["claude"]: raise Problem("provider_forbidden", 403) return request class Handler(BaseHTTPRequestHandler): """Only bounded inference job operations, never tools or cluster management.""" server_version = "SuitePlanner/1" def log_message(self, *args): """Disable URL/header/body logging; workers emit fixed metadata events.""" def setup(self): super().setup() self.connection.settimeout(30) def send(self, status, value): body = encoded(value) self.send_response(status) self.send_header("Content-Type", "application/json") self.send_header("Content-Length", str(len(body))) self.send_header("Cache-Control", "no-store") self.send_header("Connection", "close") if status == 429: self.send_header("Retry-After", "30") self.end_headers() self.wfile.write(body) self.close_connection = True def body(self): if self.headers.get("Transfer-Encoding") or self.headers.get("Content-Encoding"): raise Problem("unsupported_encoding") lengths = self.headers.get_all("Content-Length", []) if len(lengths) != 1 or not lengths[0].isdigit(): raise Problem("content_length_required", 411) length = int(lengths[0]) if not 0 < length <= MAX_BODY: raise Problem("request_too_large", 413) if self.headers.get("Content-Type", "").split(";")[0] != "application/json": raise Problem("content_type", 415) raw = self.rfile.read(length) if len(raw) != length: raise Problem("incomplete_request") return strict_json(raw) def dispatch(self, method): try: client_ip = self.headers.get("X-Forwarded-For", "").split(",")[-1].strip() try: if ipaddress.ip_address(client_ip) not in ipaddress.ip_network("192.168.22.0/24"): raise ValueError() except ValueError: raise Problem("internal_network_required", 403) from None owner, providers = credential(self.headers.get("Authorization", "")) jobs = self.server.jobs if method == "GET" and self.path == "/healthz": return self.send(200, {"status": "ready", "configuration_revision": REVISION, "execution_revision": EXECUTION_REVISION, "policy_revision": POLICY_REVISION, "prompt_revision": PROMPT_REVISION, "prompt_sha256": PROMPT_SHA256}) if method == "GET" and self.path == "/v1/capabilities": return self.send(200, {"configuration_revision": REVISION, "models": MODELS, "claude_model_options": list(CLAUDE_MODELS), "model_selection": "server_configuration", "execution_revision": EXECUTION_REVISION, "policy_revision": POLICY_REVISION, "max_final_group_cases": 5, "max_final_name_characters": 64, "model_passes": {"minimum": 3, "maximum": 5}, "review_summary_location": "GET /v1/jobs//result: top-level review_summary", "prompt_revision": PROMPT_REVISION, "prompt_sha256": PROMPT_SHA256, "allowed_external_providers": providers, "external_scope": "generalized_claude_and_exact_synthetic_fixtures" if generalized_claude_approved() and owner == "operational-token" else "exact_synthetic_fixtures", "generalized_external_providers": ["claude"] if generalized_claude_approved() and owner == "operational-token" else [], "strategy": ["whole_suite"], "max_request_bytes": MAX_BODY, "max_result_bytes": MAX_RESULT, "max_cases": MAX_CASES, "max_seconds": TIMEOUT, "concurrency": 1, "queue": False, "max_cost_usd": COST_LIMIT, "cost_basis": "CLI estimate guard; not subscription billing", "progress_interval_seconds": 5, "result_retention_seconds": 3600, "idempotency_retention_seconds": 604800, "tokenizer": None, "provider_retention_verified": False}) match = re.fullmatch(r"/v1/synthetic/(14|75|363)", self.path) if method == "GET" and match: return self.send(200, fixture(int(match[1]))[0]) if method == "POST" and self.path in {"/v1/preflight", "/v1/jobs"}: request = authorize(self.body(), providers, operational=owner == "operational-token") selected = preflight(request) if self.path == "/v1/preflight": return self.send(200, {"status": "eligible", "routing": request["routing"], "selection": selected, "availability": "checked_at_dispatch"}) key = self.headers.get("Idempotency-Key", "") if not re.fullmatch(r"[A-Za-z0-9_-]{8,128}", key): raise Problem("idempotency_key_required") document, created = jobs.submit(owner, key, request, selected, client_ip) return self.send(202 if created else 200, document) match = re.fullmatch(r"/v1/jobs/([0-9a-f]{32})(/result)?", self.path) if match and method == "GET": return self.send(200, jobs.get(match[1], owner, bool(match[2]))) if match and method == "DELETE" and not match[2]: return self.send(200, jobs.cancel(match[1], owner)) raise Problem("not_found", 404) except Problem as exc: self.send(exc.status, exc.document()) except (OSError, TimeoutError): self.send(503, {"error": {"code": "service_unavailable"}}) def do_GET(self): self.dispatch("GET") def do_POST(self): self.dispatch("POST") def do_DELETE(self): self.dispatch("DELETE") class Decision(Handler): """Private fixed route mapping: no source input, credentials, or inference.""" def do_GET(self): if self.path == "/healthz": self.send(200, {"status": "ready"}) else: self.send(404, {"error": {"code": "not_found"}}) def do_POST(self): try: if self.path != "/v1/chat/completions": raise Problem("not_found", 404) value = self.body() provider = value.get("model", "").removeprefix("planning/") if provider not in MODELS: raise Problem("unknown_provider") content = encoded({"provider": provider, "model": MODELS[provider]["model"]}).decode() self.send(200, {"id": "fixed-routing-decision", "object": "chat.completion", "model": value["model"], "created": 0, "choices": [{"index": 0, "finish_reason": "stop", "message": { "role": "assistant", "content": content}}], "usage": {"prompt_tokens": 0, "completion_tokens": 0, "total_tokens": 0}}) except Problem as exc: self.send(exc.status, exc.document()) def do_DELETE(self): self.send(404, {"error": {"code": "not_found"}}) def main(): """Start independent public job and private decision listeners.""" os.umask(0o077) binary_hash = hashlib.sha256(Path("/opt/cli/claude").read_bytes()).hexdigest() if binary_hash != os.environ["PLANNING_CLAUDE_SHA256"]: raise SystemExit("Pinned CLI binary mismatch") version = subprocess.run(["/opt/cli/claude", "--version"], capture_output=True, text=True, timeout=10, check=True) if version.stdout.strip() != CLAUDE_VERSION + " (Claude Code)": raise SystemExit("Pinned CLI version mismatch") private = ThreadingHTTPServer(("0.0.0.0", 9001), Decision) threading.Thread(target=private.serve_forever, daemon=True).start() server = ThreadingHTTPServer(("0.0.0.0", 9000), Handler) server.jobs = Jobs("/state/jobs.sqlite") def expire(): """Remove expired results even when no client requests arrive.""" while True: threading.Event().wait(30) with server.jobs.lock: server.jobs._prune() threading.Thread(target=expire, daemon=True).start() server.serve_forever() if __name__ == "__main__": main()