238 lines
11 KiB
Python
238 lines
11 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 (CLAUDE_MODELS, CLAUDE_VERSION, COST_LIMIT, EXECUTION_REVISION, MAX_BODY, MAX_CASES, MAX_JOB_SECONDS, 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/<id>/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": MAX_JOB_SECONDS, "default_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, "execution": request["execution"],
|
|
"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()
|