Compare commits

...
4 changed files with 1341 additions and 0 deletions
@@ -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()
@@ -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
@@ -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
```
+342
View File
@@ -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()