Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7f12094e58 | ||
|
|
2ae50a2a3d |
@@ -0,0 +1,816 @@
|
||||
#!/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()
|
||||
@@ -45,7 +45,7 @@
|
||||
"daily_token_budget": 250000,
|
||||
"denied_providers": [],
|
||||
"fallback_limit": 1,
|
||||
"max_input_tokens_per_request": 16000,
|
||||
"max_input_tokens_per_request": 16384,
|
||||
"max_output_tokens_per_request": 512,
|
||||
"project_id": null,
|
||||
"required_capabilities": [
|
||||
@@ -63,7 +63,7 @@
|
||||
"daily_token_budget": 250000,
|
||||
"denied_providers": [],
|
||||
"fallback_limit": 1,
|
||||
"max_input_tokens_per_request": 16000,
|
||||
"max_input_tokens_per_request": 16384,
|
||||
"max_output_tokens_per_request": 512,
|
||||
"project_id": "staging",
|
||||
"required_capabilities": [
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
[Unit]
|
||||
Description=AIOS external GPT read-only Online Task ingress
|
||||
After=network.target aios-chatgpt-bridge-v2-staging.service aios-agent-task-api.service
|
||||
Wants=aios-chatgpt-bridge-v2-staging.service aios-agent-task-api.service
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
DynamicUser=yes
|
||||
StateDirectory=aios-gpt-online-task-ingress
|
||||
StateDirectoryMode=0750
|
||||
LoadCredential=gpt_ingress_token:/etc/aios-gitops/gpt-online-task-ingress.token
|
||||
LoadCredential=agent_task_control_token:/etc/aios-gitops/agent-task-control.token
|
||||
Environment=AIOS_GPT_INGRESS_TOKEN_FILE=%d/gpt_ingress_token
|
||||
Environment=AIOS_AGENT_TASK_CONTROL_TOKEN_FILE=%d/agent_task_control_token
|
||||
Environment=AIOS_GPT_INGRESS_STATE_ROOT=%S/aios-gpt-online-task-ingress
|
||||
Environment=AIOS_GPT_INGRESS_HOST=127.0.0.1
|
||||
Environment=AIOS_GPT_INGRESS_PORT=18945
|
||||
Environment=AIOS_GOVERNED_WRITE_API_URL=http://127.0.0.1:18892
|
||||
Environment=AIOS_AGENT_TASK_API_URL=http://127.0.0.1:18940
|
||||
ExecStart=/usr/bin/python3 -B /opt/aios/current/active-runtime/app/aios_gpt_online_task_ingress.py
|
||||
Restart=on-failure
|
||||
RestartSec=2
|
||||
NoNewPrivileges=yes
|
||||
PrivateTmp=yes
|
||||
PrivateDevices=yes
|
||||
ProtectSystem=strict
|
||||
ProtectHome=yes
|
||||
ProtectKernelTunables=yes
|
||||
ProtectKernelModules=yes
|
||||
ProtectControlGroups=yes
|
||||
ProtectKernelLogs=yes
|
||||
ProtectClock=yes
|
||||
LockPersonality=yes
|
||||
RestrictSUIDSGID=yes
|
||||
RestrictRealtime=yes
|
||||
RestrictNamespaces=yes
|
||||
MemoryDenyWriteExecute=yes
|
||||
RestrictAddressFamilies=AF_UNIX AF_INET
|
||||
IPAddressDeny=any
|
||||
IPAddressAllow=127.0.0.0/8
|
||||
InaccessiblePaths=/data/aios-gitops /etc/aios-gitops /home
|
||||
UMask=0077
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
+109
@@ -0,0 +1,109 @@
|
||||
# Model Governance Policy / Profile Floor Compatibility
|
||||
|
||||
## Problem
|
||||
|
||||
The root-owned OpenClaw runtime profile reserves a minimum of 16,384 input tokens so Model
|
||||
Governance can cover OpenClaw-injected workspace, skills and system context before provider
|
||||
execution.
|
||||
|
||||
The staging scope policies still limited input to 16,000 tokens per request. Route selection
|
||||
therefore failed before reservation with:
|
||||
|
||||
```text
|
||||
MODEL_INPUT_TOKEN_LIMIT_DENY
|
||||
```
|
||||
|
||||
This was a configuration invariant violation:
|
||||
|
||||
```text
|
||||
scope max_input_tokens_per_request
|
||||
<
|
||||
root-owned profile input_reservation_floor_tokens
|
||||
```
|
||||
|
||||
The registered OpenClaw model itself supports 32,768 input tokens, so the model capability was not
|
||||
the blocker.
|
||||
|
||||
## Invariant
|
||||
|
||||
For every Model Governance scope that allows a model/runtime whose root-owned profile declares an
|
||||
input reservation floor:
|
||||
|
||||
```text
|
||||
scope.max_input_tokens_per_request
|
||||
>=
|
||||
profile.model_governance.input_reservation_floor_tokens
|
||||
```
|
||||
|
||||
The profile floor remains root-owned. Task payloads cannot alter it.
|
||||
|
||||
## Staging policy
|
||||
|
||||
The two staging policies that allow `openai/deepseek-v4-flash` are updated from:
|
||||
|
||||
```text
|
||||
16000
|
||||
```
|
||||
|
||||
to:
|
||||
|
||||
```text
|
||||
16384
|
||||
```
|
||||
|
||||
This is the minimum compatible value. It intentionally remains below the model's 32,768-token
|
||||
technical input ceiling.
|
||||
|
||||
The daily token budget is unchanged.
|
||||
|
||||
## Governance path
|
||||
|
||||
The live SQLite policy must be changed only through the repository bootstrap and
|
||||
`aios_model_governance_admin.py`.
|
||||
|
||||
The admin reconciler:
|
||||
|
||||
- reads the governed repository config;
|
||||
- uses current policy revision as CAS;
|
||||
- calls `ModelGovernanceStore.set_scope_policy`;
|
||||
- appends normal Model Governance audit evidence;
|
||||
- verifies the audit chain.
|
||||
|
||||
Direct SQL updates remain forbidden.
|
||||
|
||||
## Exhausted OpenClaw recovery
|
||||
|
||||
The fixed OpenClaw live Task has exhausted its normal three attempts.
|
||||
|
||||
After the governed policy candidate is officially verified and the live policy is reconciled:
|
||||
|
||||
```text
|
||||
attempt 3 VERIFYING/FAILED result
|
||||
→ official NEEDS_REWORK
|
||||
→ Task becomes FAILED 3/3
|
||||
→ official bounded retry extension 3 → 4
|
||||
→ same Task / same Job returns QUEUED
|
||||
→ persistent OpenClaw worker auto-claims attempt 4
|
||||
```
|
||||
|
||||
AR1 attempt-scoped external request identity ensures attempt 4 receives a new external request and a
|
||||
new Model Governance reservation.
|
||||
|
||||
No historical reservation is deleted or rewritten.
|
||||
|
||||
## Acceptance
|
||||
|
||||
Attempt 4 must prove:
|
||||
|
||||
```text
|
||||
scope max_input_tokens_per_request=16384
|
||||
profile input_reservation_floor_tokens=16384
|
||||
reserved_estimated_input_tokens>=16384
|
||||
actual input <= reserved input
|
||||
settlement=SETTLED
|
||||
usage_source=runtime
|
||||
audit_chain_valid=true
|
||||
```
|
||||
|
||||
If attempt 4 exposes a new failure, stop. No additional retry extension is authorized by this
|
||||
repair.
|
||||
@@ -0,0 +1,138 @@
|
||||
# External GPT → Online Task ingress
|
||||
|
||||
## Scope
|
||||
|
||||
This component closes the current architectural gap between an external Custom GPT and AIOS
|
||||
without exposing the internal Task API (`18940`) or canonical Bridge (`18892`) directly.
|
||||
|
||||
The first supported public-facing capability is intentionally read-only:
|
||||
|
||||
```text
|
||||
authenticated external intent
|
||||
→ host-side ingress
|
||||
→ canonical /v2/governed-task
|
||||
→ canonical /v2/execution-envelope
|
||||
→ persistent generic worker
|
||||
→ source_capture result
|
||||
→ official control-plane verification
|
||||
```
|
||||
|
||||
The ingress itself never claims, heartbeats, verifies, commits, acquires WriterLease, or writes
|
||||
protected refs.
|
||||
|
||||
## Security boundary
|
||||
|
||||
The service binds only to loopback (`127.0.0.1:18945`). A future owner-approved TLS reverse proxy
|
||||
or tunnel may expose this service externally. Direct non-loopback binding is denied by code.
|
||||
|
||||
Two independent credentials are projected by systemd:
|
||||
|
||||
- `gpt_ingress_token`: authenticates the external caller to this ingress.
|
||||
- `agent_task_control_token`: lets the ingress call the loopback AIOS control plane.
|
||||
|
||||
Neither credential is stored in Git, SQLite, task payloads, results, logs, or API responses.
|
||||
|
||||
The ingress has no repository filesystem access.
|
||||
|
||||
## Caller-controlled fields
|
||||
|
||||
Allowed:
|
||||
|
||||
```text
|
||||
request_id
|
||||
goal
|
||||
instructions
|
||||
success_criteria
|
||||
constraints.max_files
|
||||
constraints.focus_paths
|
||||
constraints.notes
|
||||
```
|
||||
|
||||
Not caller-controlled:
|
||||
|
||||
```text
|
||||
repository
|
||||
base_oid
|
||||
expected_branch_oid
|
||||
target_branch
|
||||
branch_mode
|
||||
generation
|
||||
risk
|
||||
priority
|
||||
max_attempts
|
||||
capabilities
|
||||
action/adapter
|
||||
WriterLease
|
||||
worker identity
|
||||
credentials
|
||||
verification decision
|
||||
main/production/tag/baseline permissions
|
||||
```
|
||||
|
||||
All external tasks are hard-bound to:
|
||||
|
||||
```text
|
||||
repository=aios/aios
|
||||
capabilities=[git.read, git.test]
|
||||
action=source_capture
|
||||
writer_lease=false
|
||||
repository_mutation=forbidden
|
||||
control_plane_side_effects=forbidden
|
||||
credential_access=forbidden
|
||||
```
|
||||
|
||||
## Idempotency
|
||||
|
||||
`request_id` is the durable external idempotency boundary.
|
||||
|
||||
The ingress stores only:
|
||||
|
||||
```text
|
||||
request hash
|
||||
derived AIOS task id
|
||||
canonical job id
|
||||
base oid
|
||||
sanitized receipt
|
||||
state
|
||||
```
|
||||
|
||||
An exact retry returns the same receipt. Reusing a `request_id` with different content returns
|
||||
`IDEMPOTENCY_CONFLICT`.
|
||||
|
||||
A crash after canonical task creation can recover the same binding from the Task API rather than
|
||||
creating a replacement Task.
|
||||
|
||||
## Status
|
||||
|
||||
`GET /v1/read-only-tasks/{request_id}` returns only sanitized state:
|
||||
|
||||
```text
|
||||
task/job ids
|
||||
status
|
||||
attempt
|
||||
whether claimed
|
||||
result classification
|
||||
official verification decision/classification
|
||||
source snapshot hash/count when available
|
||||
```
|
||||
|
||||
It does not proxy raw Task metadata, execution envelopes, result text, credentials, or source files.
|
||||
|
||||
## Public exposure
|
||||
|
||||
This candidate does **not** make AIOS publicly reachable. Public exposure requires a separately
|
||||
approved HTTPS ingress/reverse proxy with:
|
||||
|
||||
- TLS;
|
||||
- external authentication lifecycle;
|
||||
- rate limiting / abuse controls;
|
||||
- request size limits;
|
||||
- observability;
|
||||
- explicit owner approval for the network exposure.
|
||||
|
||||
Until that step is verified:
|
||||
|
||||
```text
|
||||
HOST_SIDE_EXTERNAL_GPT_ADAPTER=implemented/staging-candidate
|
||||
PUBLIC_CUSTOM_GPT_DIRECT_AI0S_ACCESS=not verified
|
||||
```
|
||||
@@ -0,0 +1,342 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from pathlib import Path
|
||||
import sqlite3
|
||||
import sys
|
||||
|
||||
import pytest
|
||||
|
||||
ROOT=Path(__file__).resolve().parents[1]
|
||||
APP=ROOT/"active-runtime/app"
|
||||
sys.path.insert(0,str(APP))
|
||||
|
||||
from aios_gpt_online_task_ingress import (
|
||||
IngressError,
|
||||
IngressService,
|
||||
Store,
|
||||
READONLY_CAPABILITIES,
|
||||
branch_for,
|
||||
normalized_request,
|
||||
openapi_document,
|
||||
task_id_for,
|
||||
validated_bind_host,
|
||||
)
|
||||
|
||||
|
||||
MAIN="3244f0f17f8845592ac94d585f5ebd65693c9f29"
|
||||
JOB="job-0123456789abcdef0123456789abcdef"
|
||||
|
||||
|
||||
def request(request_id="external-request-0001", **overrides):
|
||||
value={
|
||||
"schema":"aios.gpt-online-task-ingress.request.v1",
|
||||
"request_id":request_id,
|
||||
"goal":"Inspect the repository for durable worker state.",
|
||||
"instructions":"Read only. Return a concise source analysis.",
|
||||
"success_criteria":[
|
||||
{"id":"analysis","description":"Return a source-backed analysis."}
|
||||
],
|
||||
"constraints":{
|
||||
"max_files":50,
|
||||
"focus_paths":["active-runtime/app","tests"],
|
||||
"notes":"Do not mutate repository state.",
|
||||
},
|
||||
}
|
||||
value.update(overrides)
|
||||
return value
|
||||
|
||||
|
||||
class FakeUpstream:
|
||||
def __init__(self):
|
||||
self.created=[]
|
||||
self.projected=[]
|
||||
self.tasks={}
|
||||
self.snapshot={"authoritative_head_oid":MAIN}
|
||||
|
||||
def control_snapshot(self):
|
||||
return dict(self.snapshot)
|
||||
|
||||
def create_governed_task(self, body):
|
||||
self.created.append(dict(body))
|
||||
task_id=body["task_id"]
|
||||
job_id=JOB
|
||||
self.tasks[task_id]={
|
||||
"task_id":task_id,
|
||||
"status":"PROJECTED",
|
||||
"attempt":0,
|
||||
"metadata":{
|
||||
"execution":{
|
||||
"task_id":task_id,
|
||||
"job_id":job_id,
|
||||
"base_oid":body["base_oid"],
|
||||
"action":"source_capture",
|
||||
"capability_scope":list(READONLY_CAPABILITIES),
|
||||
"governance":{"writer_lease":False},
|
||||
}
|
||||
},
|
||||
}
|
||||
return 201,{"task_id":task_id,"job_id":job_id,"state":"BOUND"}
|
||||
|
||||
def execution_envelope(self, body):
|
||||
self.projected.append(dict(body))
|
||||
task_id=body["task_id"]
|
||||
return 200,{
|
||||
"schema":"aios.online-agent.execution.v2",
|
||||
"task_id":task_id,
|
||||
"job_id":body["job_id"],
|
||||
"repository":"aios/aios",
|
||||
"base_oid":MAIN,
|
||||
"action":"source_capture",
|
||||
"capability_scope":list(READONLY_CAPABILITIES),
|
||||
"governance":{"writer_lease":False},
|
||||
}
|
||||
|
||||
def get_task(self, task_id):
|
||||
value=self.tasks.get(task_id)
|
||||
return (404,{}) if value is None else (200,dict(value))
|
||||
|
||||
|
||||
def service(tmp_path):
|
||||
upstream=FakeUpstream()
|
||||
store=Store(tmp_path/"state.sqlite3")
|
||||
return IngressService(upstream,store),upstream,store
|
||||
|
||||
|
||||
def test_request_schema_is_narrow_and_rejects_capability_or_execution_control():
|
||||
value=normalized_request(request())
|
||||
assert value["goal"].startswith("Inspect")
|
||||
for field in ("capability_scope","required_capabilities","action","adapter","repository","base_oid"):
|
||||
bad=request()
|
||||
bad[field]="evil"
|
||||
with pytest.raises(IngressError,match="UNKNOWN_FIELDS"):
|
||||
normalized_request(bad)
|
||||
|
||||
|
||||
def test_constraints_are_bounded_and_path_traversal_is_denied():
|
||||
bad=request(constraints={"focus_paths":["../etc/passwd"]})
|
||||
with pytest.raises(IngressError,match="FOCUS_PATHS_INVALID"):
|
||||
normalized_request(bad)
|
||||
bad=request(constraints={"max_files":1000})
|
||||
with pytest.raises(IngressError,match="MAX_FILES_INVALID"):
|
||||
normalized_request(bad)
|
||||
|
||||
|
||||
def test_non_loopback_bind_is_denied():
|
||||
assert validated_bind_host("127.0.0.1")=="127.0.0.1"
|
||||
with pytest.raises(RuntimeError,match="GPT_INGRESS_NON_LOOPBACK_BIND_DENY"):
|
||||
validated_bind_host("0.0.0.0")
|
||||
with pytest.raises(RuntimeError,match="GPT_INGRESS_NON_LOOPBACK_BIND_DENY"):
|
||||
validated_bind_host("192.0.2.10")
|
||||
|
||||
|
||||
def test_task_and_branch_ids_are_deterministic_and_bounded():
|
||||
a=task_id_for("external-request-0001")
|
||||
b=task_id_for("external-request-0001")
|
||||
c=task_id_for("external-request-0002")
|
||||
assert a==b
|
||||
assert a!=c
|
||||
assert a.startswith("gpt-readonly-")
|
||||
branch=branch_for(a)
|
||||
assert branch.startswith("agent/external-gpt-readonly/")
|
||||
assert len(a)<180
|
||||
|
||||
|
||||
def test_create_translates_to_canonical_readonly_governed_task(tmp_path):
|
||||
svc,upstream,store=service(tmp_path)
|
||||
response=svc.create(request())
|
||||
assert response["state"]=="BOUND"
|
||||
assert response["write_access"] is False
|
||||
assert response["official_verification_required"] is True
|
||||
assert response["capabilities"]==list(READONLY_CAPABILITIES)
|
||||
|
||||
assert len(upstream.created)==1
|
||||
body=upstream.created[0]
|
||||
assert body["repository"]=="aios/aios"
|
||||
assert body["base_oid"]==MAIN
|
||||
assert body["expected_branch_oid"]==MAIN
|
||||
assert body["branch_mode"]=="CREATE_NEW"
|
||||
assert body["risk"]=="low"
|
||||
assert body["priority"]==0
|
||||
assert body["max_attempts"]==3
|
||||
assert body["require_online_task_projection"] is True
|
||||
assert body["online_task"]["required_capabilities"]==list(READONLY_CAPABILITIES)
|
||||
assert body["online_task"]["constraints"]["repository_mutation"]=="forbidden"
|
||||
assert body["online_task"]["constraints"]["credential_access"]=="forbidden"
|
||||
assert body["execution"]["action"]=="source_capture"
|
||||
assert body["execution"]["adapter"]=="source_capture"
|
||||
assert body["execution"]["capability_scope"]==list(READONLY_CAPABILITIES)
|
||||
assert "writer_lease" not in body["execution"]
|
||||
|
||||
assert len(upstream.projected)==1
|
||||
projection=upstream.projected[0]
|
||||
assert projection["task_id"]==response["task_id"]
|
||||
assert projection["job_id"]==JOB
|
||||
assert projection["required_capabilities"]==list(READONLY_CAPABILITIES)
|
||||
|
||||
row=store.get("external-request-0001")
|
||||
assert row is not None
|
||||
assert row["state"]=="BOUND"
|
||||
assert row["job_id"]==JOB
|
||||
assert row["base_oid"]==MAIN
|
||||
|
||||
|
||||
def test_exact_retry_is_idempotent_without_duplicate_bridge_call(tmp_path):
|
||||
svc,upstream,_=service(tmp_path)
|
||||
first=svc.create(request())
|
||||
second=svc.create(request())
|
||||
assert second==first
|
||||
assert len(upstream.created)==1
|
||||
assert len(upstream.projected)==1
|
||||
|
||||
|
||||
def test_same_request_id_with_different_content_is_conflict(tmp_path):
|
||||
svc,upstream,_=service(tmp_path)
|
||||
svc.create(request())
|
||||
with pytest.raises(IngressError,match="IDEMPOTENCY_CONFLICT"):
|
||||
svc.create(request(goal="Different goal."))
|
||||
assert len(upstream.created)==1
|
||||
|
||||
|
||||
def test_pending_request_recovers_existing_canonical_task_after_crash(tmp_path):
|
||||
svc,upstream,store=service(tmp_path)
|
||||
raw=normalized_request(request())
|
||||
task_id=task_id_for(raw["request_id"])
|
||||
store.reserve(raw,task_id)
|
||||
|
||||
upstream.tasks[task_id]={
|
||||
"task_id":task_id,
|
||||
"status":"PROJECTED",
|
||||
"attempt":0,
|
||||
"metadata":{
|
||||
"execution":{
|
||||
"task_id":task_id,
|
||||
"job_id":JOB,
|
||||
"base_oid":MAIN,
|
||||
"action":"source_capture",
|
||||
"capability_scope":list(READONLY_CAPABILITIES),
|
||||
"governance":{"writer_lease":False},
|
||||
}
|
||||
},
|
||||
}
|
||||
result=svc.create(request())
|
||||
assert result["job_id"]==JOB
|
||||
assert result["task_id"]==task_id
|
||||
assert len(upstream.created)==0
|
||||
row=store.get(raw["request_id"])
|
||||
assert row["state"]=="BOUND"
|
||||
|
||||
|
||||
def test_status_is_sanitized_and_never_proxies_raw_execution_or_result_text(tmp_path):
|
||||
svc,upstream,store=service(tmp_path)
|
||||
created=svc.create(request())
|
||||
task_id=created["task_id"]
|
||||
upstream.tasks[task_id]={
|
||||
"task_id":task_id,
|
||||
"status":"SUCCEEDED",
|
||||
"attempt":1,
|
||||
"claimed_by":"online-engineering-worker-cn-network-mkt-generic-20260815",
|
||||
"metadata":{
|
||||
"execution":{
|
||||
"job_id":JOB,
|
||||
"base_oid":MAIN,
|
||||
"action":"source_capture",
|
||||
"capability_scope":list(READONLY_CAPABILITIES),
|
||||
"governance":{"writer_lease":False},
|
||||
"secret_marker":"do-not-return",
|
||||
}
|
||||
},
|
||||
"result":{
|
||||
"status":"SUCCEEDED",
|
||||
"classification":"NEEDS_REWORK_SOURCE_CAPTURE",
|
||||
"execution_mode":"source_capture",
|
||||
"output_text":"raw source text that must not be proxied",
|
||||
"source_snapshot":{
|
||||
"payload_sha256":"a"*64,
|
||||
"file_count":42,
|
||||
"files":["secret/path"],
|
||||
},
|
||||
},
|
||||
"verification":{
|
||||
"decision":"SUCCEEDED",
|
||||
"classification":"GENERIC_ONLINE_TASK_AUTO_CONSUMPTION_VERIFIED",
|
||||
"private":"do-not-return",
|
||||
},
|
||||
}
|
||||
status=svc.status("external-request-0001")
|
||||
assert status["state"]=="SUCCEEDED"
|
||||
assert status["attempt"]==1
|
||||
assert status["claimed"] is True
|
||||
assert status["official_verification"]=="SUCCEEDED"
|
||||
assert status["source_snapshot_sha256"]=="a"*64
|
||||
assert status["source_snapshot_file_count"]==42
|
||||
serialized=json.dumps(status)
|
||||
assert "raw source text" not in serialized
|
||||
assert "secret_marker" not in serialized
|
||||
assert "secret/path" not in serialized
|
||||
assert "do-not-return" not in serialized
|
||||
|
||||
|
||||
def test_pending_status_can_recover_job_binding(tmp_path):
|
||||
svc,upstream,store=service(tmp_path)
|
||||
raw=normalized_request(request())
|
||||
task_id=task_id_for(raw["request_id"])
|
||||
store.reserve(raw,task_id)
|
||||
upstream.tasks[task_id]={
|
||||
"task_id":task_id,
|
||||
"status":"PROJECTED",
|
||||
"attempt":0,
|
||||
"metadata":{
|
||||
"execution":{
|
||||
"job_id":JOB,
|
||||
"base_oid":MAIN,
|
||||
"action":"source_capture",
|
||||
"capability_scope":list(READONLY_CAPABILITIES),
|
||||
"governance":{"writer_lease":False},
|
||||
}
|
||||
},
|
||||
}
|
||||
value=svc.status(raw["request_id"])
|
||||
assert value["job_id"]==JOB
|
||||
assert store.get(raw["request_id"])["state"]=="BOUND"
|
||||
|
||||
|
||||
def test_openapi_exposes_only_create_readonly_and_status_operations():
|
||||
doc=openapi_document()
|
||||
assert doc["openapi"]=="3.1.0"
|
||||
paths=doc["paths"]
|
||||
assert set(paths)=={
|
||||
"/v1/read-only-tasks",
|
||||
"/v1/read-only-tasks/{request_id}",
|
||||
}
|
||||
rendered=json.dumps(doc)
|
||||
for forbidden in ("writer-lease","commit","verify","production","tag","baseline"):
|
||||
assert forbidden not in rendered.lower()
|
||||
|
||||
|
||||
def test_systemd_unit_is_loopback_credential_isolated_and_has_no_repository_access():
|
||||
unit=(ROOT/"deploy/systemd/aios-gpt-online-task-ingress.service").read_text()
|
||||
assert "DynamicUser=yes" in unit
|
||||
assert "LoadCredential=gpt_ingress_token:" in unit
|
||||
assert "LoadCredential=agent_task_control_token:" in unit
|
||||
assert "AIOS_GPT_INGRESS_HOST=127.0.0.1" in unit
|
||||
assert "AIOS_GOVERNED_WRITE_API_URL=http://127.0.0.1:18892" in unit
|
||||
assert "AIOS_AGENT_TASK_API_URL=http://127.0.0.1:18940" in unit
|
||||
assert "IPAddressDeny=any" in unit
|
||||
assert "IPAddressAllow=127.0.0.0/8" in unit
|
||||
assert "InaccessiblePaths=/data/aios-gitops /etc/aios-gitops /home" in unit
|
||||
assert "ProtectSystem=strict" in unit
|
||||
assert "NoNewPrivileges=yes" in unit
|
||||
assert "MemoryDenyWriteExecute=yes" in unit
|
||||
assert "0.0.0.0" not in unit
|
||||
|
||||
|
||||
def test_ingress_sqlite_contains_no_request_body_or_credentials(tmp_path):
|
||||
svc,upstream,store=service(tmp_path)
|
||||
svc.create(request(instructions="sensitive-looking but non-secret user text"))
|
||||
con=sqlite3.connect(store.path)
|
||||
columns=[row[1] for row in con.execute("pragma table_info(external_task_requests)")]
|
||||
assert "request_json" not in columns
|
||||
assert "goal" not in columns
|
||||
assert "instructions" not in columns
|
||||
assert "token" not in " ".join(columns).lower()
|
||||
@@ -65,3 +65,35 @@ def test_staging_bootstrap_has_explicit_tenant_live_policy():
|
||||
}
|
||||
assert ("legacy",None,None) in keys
|
||||
assert ("tenant","model-governance-live","staging") in keys
|
||||
|
||||
|
||||
def test_staging_policy_input_limit_covers_root_owned_openclaw_reservation_floor():
|
||||
config=json.loads(
|
||||
(ROOT/"config/model-governance/model-governance.staging.json").read_text()
|
||||
)
|
||||
profile=json.loads(
|
||||
(ROOT/"config/external-agent-runtime.d/openclaw.profile.json").read_text()
|
||||
)
|
||||
floor=int(profile["model_governance"]["input_reservation_floor_tokens"])
|
||||
assert floor==16384
|
||||
openclaw_model="openai/deepseek-v4-flash"
|
||||
relevant=[
|
||||
policy for policy in config["policies"]
|
||||
if openclaw_model in policy["allowed_models"]
|
||||
]
|
||||
assert relevant
|
||||
for policy in relevant:
|
||||
assert int(policy["max_input_tokens_per_request"]) >= floor
|
||||
|
||||
|
||||
def test_staging_openclaw_scope_limit_is_least_privilege_floor_not_model_ceiling():
|
||||
config=json.loads(
|
||||
(ROOT/"config/model-governance/model-governance.staging.json").read_text()
|
||||
)
|
||||
models={item["model_id"]:item for item in config["models"]}
|
||||
model=models["openai/deepseek-v4-flash"]
|
||||
assert int(model["max_input_tokens"])==32768
|
||||
for policy in config["policies"]:
|
||||
if "openai/deepseek-v4-flash" in policy["allowed_models"]:
|
||||
assert int(policy["max_input_tokens_per_request"])==16384
|
||||
assert int(policy["max_input_tokens_per_request"]) < int(model["max_input_tokens"])
|
||||
|
||||
Reference in New Issue
Block a user