228 lines
10 KiB
Python

"""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 (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_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,
"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,
"execution_revision": EXECUTION_REVISION,
"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,
"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() != "2.1.226 (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()