817 lines
27 KiB
Python
817 lines
27 KiB
Python
#!/usr/bin/env python3
|
|
"""Credential-isolated host-side ingress for externally scheduled read-only Online Tasks.
|
|
|
|
This service is intentionally not a public AIOS control plane. It accepts a tiny,
|
|
authenticated external intent and translates it to the canonical loopback Bridge.
|
|
Callers cannot select repository coordinates, capabilities, execution action,
|
|
worker, branch policy, credentials, verification decisions, or write permissions.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from dataclasses import dataclass
|
|
from datetime import datetime, timezone
|
|
import hashlib
|
|
import hmac
|
|
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
|
import json
|
|
import os
|
|
from pathlib import Path
|
|
import re
|
|
import sqlite3
|
|
import threading
|
|
import urllib.error
|
|
import urllib.request
|
|
from typing import Any, Mapping
|
|
|
|
SCHEMA = "aios.gpt-online-task-ingress.request.v1"
|
|
RESPONSE_SCHEMA = "aios.gpt-online-task-ingress.response.v1"
|
|
OPENAPI_VERSION = "3.1.0"
|
|
|
|
REQUEST_ID_RE = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._:-]{7,127}$")
|
|
TASK_ID_RE = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._:-]{2,179}$")
|
|
JOB_ID_RE = re.compile(r"^job-[0-9a-f]{32}$")
|
|
OID_RE = re.compile(r"^[0-9a-f]{40}$")
|
|
MAX_BODY = 64 * 1024
|
|
MAX_GOAL = 12_000
|
|
MAX_INSTRUCTIONS = 24_000
|
|
MAX_CRITERIA = 16
|
|
MAX_CRITERION_TEXT = 1_000
|
|
|
|
READONLY_CAPABILITIES = ("git.read", "git.test")
|
|
REPOSITORY = "aios/aios"
|
|
|
|
|
|
class IngressError(RuntimeError):
|
|
def __init__(self, code: str, status: int = 400) -> None:
|
|
super().__init__(code)
|
|
self.code = code
|
|
self.status = status
|
|
|
|
|
|
def canonical(value: object) -> bytes:
|
|
return json.dumps(
|
|
value,
|
|
ensure_ascii=False,
|
|
sort_keys=True,
|
|
separators=(",", ":"),
|
|
).encode("utf-8")
|
|
|
|
|
|
def digest(value: object) -> str:
|
|
return hashlib.sha256(canonical(value)).hexdigest()
|
|
|
|
|
|
def now() -> str:
|
|
return datetime.now(timezone.utc).isoformat()
|
|
|
|
|
|
def load_secret(path: Path, *, code: str) -> str:
|
|
if path.is_symlink() or not path.is_file():
|
|
raise RuntimeError(code + "_UNAVAILABLE")
|
|
value = path.read_text(encoding="utf-8").strip()
|
|
if len(value) < 32 or "\n" in value or "\r" in value:
|
|
raise RuntimeError(code + "_INVALID")
|
|
return value
|
|
|
|
|
|
def normalized_request(value: Mapping[str, Any]) -> dict[str, Any]:
|
|
allowed = {
|
|
"schema",
|
|
"request_id",
|
|
"goal",
|
|
"instructions",
|
|
"success_criteria",
|
|
"constraints",
|
|
}
|
|
if set(value) - allowed:
|
|
raise IngressError("UNKNOWN_FIELDS")
|
|
if value.get("schema") != SCHEMA:
|
|
raise IngressError("SCHEMA_INVALID")
|
|
|
|
request_id = value.get("request_id")
|
|
if not isinstance(request_id, str) or REQUEST_ID_RE.fullmatch(request_id) is None:
|
|
raise IngressError("REQUEST_ID_INVALID")
|
|
|
|
goal = value.get("goal")
|
|
if not isinstance(goal, str) or not goal.strip() or len(goal) > MAX_GOAL:
|
|
raise IngressError("GOAL_INVALID")
|
|
instructions = value.get("instructions", "")
|
|
if not isinstance(instructions, str) or len(instructions) > MAX_INSTRUCTIONS:
|
|
raise IngressError("INSTRUCTIONS_INVALID")
|
|
|
|
criteria = value.get("success_criteria") or []
|
|
if not isinstance(criteria, list) or len(criteria) > MAX_CRITERIA:
|
|
raise IngressError("SUCCESS_CRITERIA_INVALID")
|
|
normalized_criteria = []
|
|
for item in criteria:
|
|
if not isinstance(item, Mapping) or set(item) != {"id", "description"}:
|
|
raise IngressError("SUCCESS_CRITERIA_INVALID")
|
|
criterion_id = item.get("id")
|
|
description = item.get("description")
|
|
if (
|
|
not isinstance(criterion_id, str)
|
|
or not criterion_id.strip()
|
|
or len(criterion_id) > 120
|
|
or not isinstance(description, str)
|
|
or not description.strip()
|
|
or len(description) > MAX_CRITERION_TEXT
|
|
):
|
|
raise IngressError("SUCCESS_CRITERIA_INVALID")
|
|
normalized_criteria.append(
|
|
{"id": criterion_id.strip(), "description": description.strip()}
|
|
)
|
|
|
|
constraints = value.get("constraints") or {}
|
|
if not isinstance(constraints, Mapping):
|
|
raise IngressError("CONSTRAINTS_INVALID")
|
|
allowed_constraints = {
|
|
"max_files",
|
|
"focus_paths",
|
|
"notes",
|
|
}
|
|
if set(constraints) - allowed_constraints:
|
|
raise IngressError("CONSTRAINTS_FIELDS_INVALID")
|
|
normalized_constraints: dict[str, Any] = {}
|
|
if "max_files" in constraints:
|
|
max_files = constraints["max_files"]
|
|
if (
|
|
not isinstance(max_files, int)
|
|
or isinstance(max_files, bool)
|
|
or not 1 <= max_files <= 500
|
|
):
|
|
raise IngressError("MAX_FILES_INVALID")
|
|
normalized_constraints["max_files"] = max_files
|
|
if "focus_paths" in constraints:
|
|
paths = constraints["focus_paths"]
|
|
if (
|
|
not isinstance(paths, list)
|
|
or len(paths) > 32
|
|
or not all(
|
|
isinstance(path, str)
|
|
and path
|
|
and len(path) <= 512
|
|
and not path.startswith("/")
|
|
and ".." not in Path(path).parts
|
|
for path in paths
|
|
)
|
|
):
|
|
raise IngressError("FOCUS_PATHS_INVALID")
|
|
normalized_constraints["focus_paths"] = paths
|
|
if "notes" in constraints:
|
|
notes = constraints["notes"]
|
|
if not isinstance(notes, str) or len(notes) > 4_000:
|
|
raise IngressError("CONSTRAINT_NOTES_INVALID")
|
|
normalized_constraints["notes"] = notes
|
|
|
|
return {
|
|
"schema": SCHEMA,
|
|
"request_id": request_id,
|
|
"goal": goal.strip(),
|
|
"instructions": instructions,
|
|
"success_criteria": normalized_criteria,
|
|
"constraints": normalized_constraints,
|
|
}
|
|
|
|
|
|
def validated_bind_host(value: str) -> str:
|
|
host = value.strip()
|
|
if host not in {"127.0.0.1", "::1", "localhost"}:
|
|
raise RuntimeError("GPT_INGRESS_NON_LOOPBACK_BIND_DENY")
|
|
return host
|
|
|
|
|
|
def task_id_for(request_id: str) -> str:
|
|
suffix = hashlib.sha256(request_id.encode("utf-8")).hexdigest()[:24]
|
|
return "gpt-readonly-" + suffix
|
|
|
|
|
|
def branch_for(task_id: str) -> str:
|
|
return "agent/external-gpt-readonly/" + task_id[-24:]
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class Upstream:
|
|
bridge_base: str
|
|
task_base: str
|
|
control_token: str
|
|
timeout: int = 20
|
|
|
|
def _request(
|
|
self,
|
|
base: str,
|
|
method: str,
|
|
path: str,
|
|
body: Mapping[str, Any] | None = None,
|
|
) -> tuple[int, dict[str, Any]]:
|
|
raw_body = None if body is None else canonical(body)
|
|
headers = {
|
|
"Authorization": "Bearer " + self.control_token,
|
|
"Accept": "application/json",
|
|
}
|
|
if raw_body is not None:
|
|
headers["Content-Type"] = "application/json"
|
|
req = urllib.request.Request(
|
|
base.rstrip("/") + path,
|
|
method=method,
|
|
data=raw_body,
|
|
headers=headers,
|
|
)
|
|
try:
|
|
with urllib.request.urlopen(req, timeout=self.timeout) as response:
|
|
raw = response.read()
|
|
status = response.status
|
|
except urllib.error.HTTPError as exc:
|
|
raw = exc.read()
|
|
status = exc.code
|
|
except Exception as exc:
|
|
raise IngressError("UPSTREAM_TRANSPORT_FAILED", 503) from exc
|
|
try:
|
|
value = json.loads(raw.decode("utf-8"))
|
|
except Exception as exc:
|
|
raise IngressError("UPSTREAM_RESPONSE_INVALID", 502) from exc
|
|
if not isinstance(value, Mapping):
|
|
raise IngressError("UPSTREAM_RESPONSE_INVALID", 502)
|
|
return status, dict(value)
|
|
|
|
def control_snapshot(self) -> dict[str, Any]:
|
|
status, value = self._request(
|
|
self.bridge_base,
|
|
"GET",
|
|
"/v2/control-plane-snapshot",
|
|
)
|
|
if status != 200:
|
|
raise IngressError("CONTROL_SNAPSHOT_UNAVAILABLE", 503)
|
|
return value
|
|
|
|
def create_governed_task(
|
|
self,
|
|
body: Mapping[str, Any],
|
|
) -> tuple[int, dict[str, Any]]:
|
|
return self._request(
|
|
self.bridge_base,
|
|
"POST",
|
|
"/v2/governed-task",
|
|
body,
|
|
)
|
|
|
|
def execution_envelope(
|
|
self,
|
|
body: Mapping[str, Any],
|
|
) -> tuple[int, dict[str, Any]]:
|
|
return self._request(
|
|
self.bridge_base,
|
|
"POST",
|
|
"/v2/execution-envelope",
|
|
body,
|
|
)
|
|
|
|
def get_task(self, task_id: str) -> tuple[int, dict[str, Any]]:
|
|
return self._request(
|
|
self.task_base,
|
|
"GET",
|
|
"/v1/tasks/" + task_id,
|
|
)
|
|
|
|
|
|
class Store:
|
|
def __init__(self, path: Path) -> None:
|
|
self.path = Path(path)
|
|
self._lock = threading.RLock()
|
|
self.path.parent.mkdir(parents=True, exist_ok=True)
|
|
with self._connect() as con:
|
|
con.execute(
|
|
"""
|
|
CREATE TABLE IF NOT EXISTS external_task_requests(
|
|
request_id TEXT PRIMARY KEY,
|
|
request_sha256 TEXT NOT NULL,
|
|
task_id TEXT NOT NULL UNIQUE,
|
|
job_id TEXT,
|
|
base_oid TEXT,
|
|
state TEXT NOT NULL,
|
|
created_at TEXT NOT NULL,
|
|
updated_at TEXT NOT NULL,
|
|
response_json TEXT
|
|
)
|
|
"""
|
|
)
|
|
con.commit()
|
|
|
|
def _connect(self) -> sqlite3.Connection:
|
|
con = sqlite3.connect(self.path, timeout=30)
|
|
con.row_factory = sqlite3.Row
|
|
con.execute("PRAGMA busy_timeout=30000")
|
|
return con
|
|
|
|
def get(self, request_id: str) -> sqlite3.Row | None:
|
|
with self._connect() as con:
|
|
return con.execute(
|
|
"SELECT * FROM external_task_requests WHERE request_id=?",
|
|
(request_id,),
|
|
).fetchone()
|
|
|
|
def reserve(self, request: Mapping[str, Any], task_id: str) -> sqlite3.Row:
|
|
request_sha = digest(request)
|
|
timestamp = now()
|
|
with self._lock, self._connect() as con:
|
|
con.execute("BEGIN IMMEDIATE")
|
|
row = con.execute(
|
|
"SELECT * FROM external_task_requests WHERE request_id=?",
|
|
(request["request_id"],),
|
|
).fetchone()
|
|
if row is not None:
|
|
if row["request_sha256"] != request_sha:
|
|
raise IngressError("IDEMPOTENCY_CONFLICT", 409)
|
|
con.commit()
|
|
return row
|
|
con.execute(
|
|
"""
|
|
INSERT INTO external_task_requests(
|
|
request_id,request_sha256,task_id,state,created_at,updated_at
|
|
) VALUES(?,?,?,?,?,?)
|
|
""",
|
|
(
|
|
request["request_id"],
|
|
request_sha,
|
|
task_id,
|
|
"PENDING",
|
|
timestamp,
|
|
timestamp,
|
|
),
|
|
)
|
|
con.commit()
|
|
row = self.get(request["request_id"])
|
|
assert row is not None
|
|
return row
|
|
|
|
def bound(
|
|
self,
|
|
request_id: str,
|
|
*,
|
|
job_id: str,
|
|
base_oid: str,
|
|
response: Mapping[str, Any],
|
|
) -> None:
|
|
with self._lock, self._connect() as con:
|
|
con.execute(
|
|
"""
|
|
UPDATE external_task_requests
|
|
SET job_id=?,base_oid=?,state='BOUND',response_json=?,updated_at=?
|
|
WHERE request_id=?
|
|
""",
|
|
(
|
|
job_id,
|
|
base_oid,
|
|
json.dumps(response, sort_keys=True, separators=(",", ":")),
|
|
now(),
|
|
request_id,
|
|
),
|
|
)
|
|
con.commit()
|
|
|
|
|
|
class IngressService:
|
|
def __init__(self, upstream: Upstream, store: Store) -> None:
|
|
self.upstream = upstream
|
|
self.store = store
|
|
|
|
@staticmethod
|
|
def _main_oid(snapshot: Mapping[str, Any]) -> str:
|
|
candidates = [
|
|
snapshot.get("authoritative_head_oid"),
|
|
snapshot.get("head"),
|
|
(snapshot.get("aggregate") or {}).get("authoritative_head_oid")
|
|
if isinstance(snapshot.get("aggregate"), Mapping)
|
|
else None,
|
|
]
|
|
for candidate in candidates:
|
|
if isinstance(candidate, str) and OID_RE.fullmatch(candidate):
|
|
return candidate
|
|
raise IngressError("AUTHORITATIVE_HEAD_UNAVAILABLE", 503)
|
|
|
|
@staticmethod
|
|
def _public_response(
|
|
request_id: str,
|
|
task_id: str,
|
|
job_id: str,
|
|
state: str,
|
|
) -> dict[str, Any]:
|
|
return {
|
|
"schema": RESPONSE_SCHEMA,
|
|
"request_id": request_id,
|
|
"task_id": task_id,
|
|
"job_id": job_id,
|
|
"state": state,
|
|
"execution_mode": "source_capture",
|
|
"capabilities": list(READONLY_CAPABILITIES),
|
|
"write_access": False,
|
|
"official_verification_required": True,
|
|
}
|
|
|
|
def _recover_existing(
|
|
self,
|
|
request: Mapping[str, Any],
|
|
row: sqlite3.Row,
|
|
) -> dict[str, Any] | None:
|
|
task_id = str(row["task_id"])
|
|
status, task = self.upstream.get_task(task_id)
|
|
if status == 404:
|
|
return None
|
|
if status != 200:
|
|
raise IngressError("TASK_RECOVERY_FAILED", 503)
|
|
metadata = task.get("metadata")
|
|
execution = (
|
|
metadata.get("execution")
|
|
if isinstance(metadata, Mapping)
|
|
and isinstance(metadata.get("execution"), Mapping)
|
|
else {}
|
|
)
|
|
job_id = str(execution.get("job_id") or row["job_id"] or "")
|
|
base_oid = str(execution.get("base_oid") or row["base_oid"] or "")
|
|
if (
|
|
task.get("task_id") != task_id
|
|
or not JOB_ID_RE.fullmatch(job_id)
|
|
or not OID_RE.fullmatch(base_oid)
|
|
or execution.get("action") != "source_capture"
|
|
or sorted(execution.get("capability_scope") or [])
|
|
!= sorted(READONLY_CAPABILITIES)
|
|
or (execution.get("governance") or {}).get("writer_lease") is not False
|
|
):
|
|
raise IngressError("TASK_RECOVERY_BINDING_INVALID", 409)
|
|
response = self._public_response(
|
|
request["request_id"],
|
|
task_id,
|
|
job_id,
|
|
str(task.get("status") or "BOUND"),
|
|
)
|
|
self.store.bound(
|
|
request["request_id"],
|
|
job_id=job_id,
|
|
base_oid=base_oid,
|
|
response=response,
|
|
)
|
|
return response
|
|
|
|
def create(self, raw: Mapping[str, Any]) -> dict[str, Any]:
|
|
request = normalized_request(raw)
|
|
task_id = task_id_for(request["request_id"])
|
|
row = self.store.reserve(request, task_id)
|
|
if row["state"] == "BOUND" and row["response_json"]:
|
|
return json.loads(row["response_json"])
|
|
|
|
recovered = self._recover_existing(request, row)
|
|
if recovered is not None:
|
|
return recovered
|
|
|
|
snapshot = self.upstream.control_snapshot()
|
|
base_oid = self._main_oid(snapshot)
|
|
target_branch = branch_for(task_id)
|
|
binding_key = "gpt-readonly-binding-" + digest(
|
|
{"request_id": request["request_id"], "base_oid": base_oid}
|
|
)[:24]
|
|
constraints = {
|
|
**request["constraints"],
|
|
"repository_mutation": "forbidden",
|
|
"control_plane_side_effects": "forbidden",
|
|
"credential_access": "forbidden",
|
|
}
|
|
body = {
|
|
"task_id": task_id,
|
|
"repository": REPOSITORY,
|
|
"base_oid": base_oid,
|
|
"expected_branch_oid": base_oid,
|
|
"target_branch": target_branch,
|
|
"branch_mode": "CREATE_NEW",
|
|
"generation": 1,
|
|
"risk": "low",
|
|
"description": "Externally requested read-only governed Online Task.",
|
|
"idempotency_key": binding_key,
|
|
"priority": 0,
|
|
"max_attempts": 3,
|
|
"require_online_task_projection": True,
|
|
"online_task": {
|
|
"goal": request["goal"],
|
|
"instructions": request["instructions"],
|
|
"success_criteria": request["success_criteria"],
|
|
"constraints": constraints,
|
|
"required_capabilities": list(READONLY_CAPABILITIES),
|
|
"priority": 0,
|
|
"max_attempts": 3,
|
|
},
|
|
"execution": {
|
|
"action": "source_capture",
|
|
"adapter": "source_capture",
|
|
"capability_scope": list(READONLY_CAPABILITIES),
|
|
"source_signals": [
|
|
"external read-only source analysis request",
|
|
"no repository mutation no credential access no control-plane side effects",
|
|
],
|
|
},
|
|
}
|
|
status, value = self.upstream.create_governed_task(body)
|
|
if status not in {200, 201, 202}:
|
|
if status == 409:
|
|
recovered = self._recover_existing(request, row)
|
|
if recovered is not None:
|
|
return recovered
|
|
raise IngressError(
|
|
"GOVERNED_TASK_CREATE_FAILED",
|
|
409 if status == 409 else 503,
|
|
)
|
|
job_id = str(value.get("job_id") or "")
|
|
if value.get("task_id") != task_id or JOB_ID_RE.fullmatch(job_id) is None:
|
|
raise IngressError("GOVERNED_TASK_RESPONSE_INVALID", 502)
|
|
|
|
envelope_status, envelope = self.upstream.execution_envelope(
|
|
{
|
|
"task_id": task_id,
|
|
"job_id": job_id,
|
|
"required_capabilities": list(READONLY_CAPABILITIES),
|
|
"execution": body["execution"],
|
|
}
|
|
)
|
|
if envelope_status != 200:
|
|
raise IngressError("EXECUTION_PROJECTION_FAILED", 503)
|
|
if (
|
|
envelope.get("task_id") != task_id
|
|
or envelope.get("job_id") != job_id
|
|
or envelope.get("base_oid") != base_oid
|
|
or envelope.get("action") != "source_capture"
|
|
or sorted(envelope.get("capability_scope") or [])
|
|
!= sorted(READONLY_CAPABILITIES)
|
|
or (envelope.get("governance") or {}).get("writer_lease") is not False
|
|
):
|
|
raise IngressError("EXECUTION_PROJECTION_INVALID", 502)
|
|
|
|
response = self._public_response(
|
|
request["request_id"],
|
|
task_id,
|
|
job_id,
|
|
"BOUND",
|
|
)
|
|
self.store.bound(
|
|
request["request_id"],
|
|
job_id=job_id,
|
|
base_oid=base_oid,
|
|
response=response,
|
|
)
|
|
return response
|
|
|
|
def status(self, request_id: str) -> dict[str, Any]:
|
|
if REQUEST_ID_RE.fullmatch(request_id) is None:
|
|
raise IngressError("REQUEST_ID_INVALID")
|
|
row = self.store.get(request_id)
|
|
if row is None:
|
|
raise IngressError("REQUEST_NOT_FOUND", 404)
|
|
task_id = str(row["task_id"])
|
|
status, task = self.upstream.get_task(task_id)
|
|
if status == 200 and not row["job_id"]:
|
|
metadata = task.get("metadata")
|
|
execution = (
|
|
metadata.get("execution")
|
|
if isinstance(metadata, Mapping)
|
|
and isinstance(metadata.get("execution"), Mapping)
|
|
else {}
|
|
)
|
|
job_id = str(execution.get("job_id") or "")
|
|
base_oid = str(execution.get("base_oid") or "")
|
|
if (
|
|
JOB_ID_RE.fullmatch(job_id)
|
|
and OID_RE.fullmatch(base_oid)
|
|
and execution.get("action") == "source_capture"
|
|
and sorted(execution.get("capability_scope") or [])
|
|
== sorted(READONLY_CAPABILITIES)
|
|
and (execution.get("governance") or {}).get("writer_lease") is False
|
|
):
|
|
recovered = self._public_response(
|
|
request_id,
|
|
task_id,
|
|
job_id,
|
|
str(task.get("status") or "BOUND"),
|
|
)
|
|
self.store.bound(
|
|
request_id,
|
|
job_id=job_id,
|
|
base_oid=base_oid,
|
|
response=recovered,
|
|
)
|
|
row = self.store.get(request_id) or row
|
|
if status == 404:
|
|
return {
|
|
"schema": RESPONSE_SCHEMA,
|
|
"request_id": request_id,
|
|
"task_id": task_id,
|
|
"state": "PENDING",
|
|
"official_verification_required": True,
|
|
}
|
|
if status != 200:
|
|
raise IngressError("TASK_STATUS_UNAVAILABLE", 503)
|
|
|
|
result = task.get("result") if isinstance(task.get("result"), Mapping) else {}
|
|
verification = (
|
|
task.get("verification")
|
|
if isinstance(task.get("verification"), Mapping)
|
|
else {}
|
|
)
|
|
safe = {
|
|
"schema": RESPONSE_SCHEMA,
|
|
"request_id": request_id,
|
|
"task_id": task_id,
|
|
"job_id": str(row["job_id"] or ""),
|
|
"state": str(task.get("status") or "UNKNOWN"),
|
|
"attempt": int(task.get("attempt") or 0),
|
|
"claimed": bool(task.get("claimed_by")),
|
|
"result_status": result.get("status"),
|
|
"result_classification": result.get("classification"),
|
|
"official_verification": verification.get("decision"),
|
|
"official_classification": verification.get("classification"),
|
|
"write_access": False,
|
|
}
|
|
if result.get("execution_mode") == "source_capture":
|
|
snapshot = result.get("source_snapshot")
|
|
if isinstance(snapshot, Mapping):
|
|
safe["source_snapshot_sha256"] = snapshot.get("payload_sha256")
|
|
safe["source_snapshot_file_count"] = snapshot.get("file_count")
|
|
return safe
|
|
|
|
|
|
def openapi_document() -> dict[str, Any]:
|
|
return {
|
|
"openapi": OPENAPI_VERSION,
|
|
"info": {
|
|
"title": "AIOS Read-Only Online Task Ingress",
|
|
"version": "1.0.0",
|
|
},
|
|
"paths": {
|
|
"/v1/read-only-tasks": {
|
|
"post": {
|
|
"operationId": "createReadOnlyOnlineTask",
|
|
"security": [{"bearerAuth": []}],
|
|
"requestBody": {"required": True},
|
|
"responses": {"200": {"description": "Task accepted"}},
|
|
}
|
|
},
|
|
"/v1/read-only-tasks/{request_id}": {
|
|
"get": {
|
|
"operationId": "getReadOnlyOnlineTaskStatus",
|
|
"security": [{"bearerAuth": []}],
|
|
"responses": {"200": {"description": "Sanitized task status"}},
|
|
}
|
|
},
|
|
},
|
|
"components": {
|
|
"securitySchemes": {
|
|
"bearerAuth": {
|
|
"type": "http",
|
|
"scheme": "bearer",
|
|
}
|
|
}
|
|
},
|
|
}
|
|
|
|
|
|
class Handler(BaseHTTPRequestHandler):
|
|
service: IngressService | None = None
|
|
ingress_token: str = ""
|
|
|
|
def log_message(self, *_args: Any) -> None:
|
|
return
|
|
|
|
def respond(self, value: Mapping[str, Any], status: int) -> None:
|
|
raw = canonical(value)
|
|
self.send_response(status)
|
|
self.send_header("Content-Type", "application/json")
|
|
self.send_header("Cache-Control", "no-store")
|
|
self.send_header("Content-Length", str(len(raw)))
|
|
self.end_headers()
|
|
self.wfile.write(raw)
|
|
|
|
def authorized(self) -> bool:
|
|
header = self.headers.get("Authorization", "")
|
|
return (
|
|
header.startswith("Bearer ")
|
|
and bool(self.ingress_token)
|
|
and hmac.compare_digest(header[7:], self.ingress_token)
|
|
)
|
|
|
|
def body(self) -> dict[str, Any]:
|
|
try:
|
|
length = int(self.headers.get("Content-Length", "-1"))
|
|
except ValueError as exc:
|
|
raise IngressError("CONTENT_LENGTH_INVALID") from exc
|
|
if length < 1 or length > MAX_BODY:
|
|
raise IngressError("REQUEST_SIZE_INVALID", 413)
|
|
try:
|
|
value = json.loads(self.rfile.read(length).decode("utf-8"))
|
|
except Exception as exc:
|
|
raise IngressError("REQUEST_JSON_INVALID") from exc
|
|
if not isinstance(value, Mapping):
|
|
raise IngressError("REQUEST_OBJECT_REQUIRED")
|
|
return dict(value)
|
|
|
|
def do_GET(self) -> None:
|
|
if self.path == "/health":
|
|
self.respond(
|
|
{
|
|
"status": "ok",
|
|
"service": "aios-gpt-online-task-ingress",
|
|
"mode": "read-only",
|
|
},
|
|
200,
|
|
)
|
|
return
|
|
if self.path == "/openapi.json":
|
|
self.respond(openapi_document(), 200)
|
|
return
|
|
if not self.authorized():
|
|
self.respond({"error": "UNAUTHORIZED"}, 401)
|
|
return
|
|
match = re.fullmatch(r"/v1/read-only-tasks/([^/?#]+)", self.path)
|
|
if match is None:
|
|
self.respond({"error": "NOT_FOUND"}, 404)
|
|
return
|
|
try:
|
|
if self.service is None:
|
|
raise IngressError("RUNTIME_UNAVAILABLE", 503)
|
|
self.respond(self.service.status(match.group(1)), 200)
|
|
except IngressError as exc:
|
|
self.respond({"error": exc.code}, exc.status)
|
|
|
|
def do_POST(self) -> None:
|
|
if self.path != "/v1/read-only-tasks":
|
|
self.respond({"error": "NOT_FOUND"}, 404)
|
|
return
|
|
if not self.authorized():
|
|
self.respond({"error": "UNAUTHORIZED"}, 401)
|
|
return
|
|
try:
|
|
if self.service is None:
|
|
raise IngressError("RUNTIME_UNAVAILABLE", 503)
|
|
value = self.service.create(self.body())
|
|
self.respond(value, 200)
|
|
except IngressError as exc:
|
|
self.respond({"error": exc.code}, exc.status)
|
|
|
|
|
|
def serve() -> None:
|
|
ingress_token_path = Path(
|
|
os.environ.get(
|
|
"AIOS_GPT_INGRESS_TOKEN_FILE",
|
|
str(
|
|
Path(os.environ.get("CREDENTIALS_DIRECTORY", "/run/credentials"))
|
|
/ "gpt_ingress_token"
|
|
),
|
|
)
|
|
)
|
|
control_token_path = Path(
|
|
os.environ.get(
|
|
"AIOS_AGENT_TASK_CONTROL_TOKEN_FILE",
|
|
str(
|
|
Path(os.environ.get("CREDENTIALS_DIRECTORY", "/run/credentials"))
|
|
/ "agent_task_control_token"
|
|
),
|
|
)
|
|
)
|
|
ingress_token = load_secret(
|
|
ingress_token_path,
|
|
code="GPT_INGRESS_CREDENTIAL",
|
|
)
|
|
control_token = load_secret(
|
|
control_token_path,
|
|
code="CONTROL_PLANE_CREDENTIAL",
|
|
)
|
|
|
|
state_root = Path(
|
|
os.environ.get(
|
|
"AIOS_GPT_INGRESS_STATE_ROOT",
|
|
"/var/lib/aios-gpt-online-task-ingress",
|
|
)
|
|
)
|
|
host = validated_bind_host(
|
|
os.environ.get("AIOS_GPT_INGRESS_HOST", "127.0.0.1")
|
|
)
|
|
port = int(os.environ.get("AIOS_GPT_INGRESS_PORT", "18945"))
|
|
if not 1024 <= port <= 65535:
|
|
raise RuntimeError("GPT_INGRESS_PORT_INVALID")
|
|
|
|
Handler.service = IngressService(
|
|
Upstream(
|
|
bridge_base=os.environ.get(
|
|
"AIOS_GOVERNED_WRITE_API_URL",
|
|
"http://127.0.0.1:18892",
|
|
),
|
|
task_base=os.environ.get(
|
|
"AIOS_AGENT_TASK_API_URL",
|
|
"http://127.0.0.1:18940",
|
|
),
|
|
control_token=control_token,
|
|
),
|
|
Store(state_root / "state.sqlite3"),
|
|
)
|
|
Handler.ingress_token = ingress_token
|
|
ThreadingHTTPServer((host, port), Handler).serve_forever()
|
|
|
|
|
|
if __name__ == "__main__":
|
|
serve()
|