Compare commits

..
57 changed files with 8209 additions and 6212 deletions
+13 -7
View File
@@ -13,6 +13,7 @@ from hashlib import sha256
from pathlib import Path
import json
from typing import Any
from message_handoff import stable_event_id
def utc_now() -> str:
@@ -55,13 +56,18 @@ class AgentEventBus:
payload: dict[str, Any],
correlation_id: str | None = None,
) -> dict[str, Any]:
event_id = "evt_" + sha256(
json.dumps(
{"kind": kind, "source": source, "summary": summary, "payload": payload},
sort_keys=True,
ensure_ascii=False,
).encode("utf-8")
).hexdigest()[:16]
event_id = stable_event_id(
source=source, target=target, message_type=kind, payload=payload,
tenant_id=(payload.get('tenant_id') if isinstance(payload, dict) else None),
user_id=(payload.get('user_id') if isinstance(payload, dict) else None),
project_id=(payload.get('project_id') if isinstance(payload, dict) else None),
conversation_id=(payload.get('conversation_id') if isinstance(payload, dict) else None),
task_id=(payload.get('task_id') if isinstance(payload, dict) else None),
run_id=(payload.get('run_id') if isinstance(payload, dict) else None),
causation_id=(payload.get('causation_id') if isinstance(payload, dict) else None),
sequence=(payload.get('sequence') if isinstance(payload, dict) else None),
idempotency_key=(payload.get('idempotency_key') if isinstance(payload, dict) else None),
)
event = AgentEvent(event_id, kind, source, target, summary, payload, correlation_id=correlation_id)
self._write("pending", event.to_dict())
self._audit("created", event.to_dict())
-315
View File
@@ -1,315 +0,0 @@
#!/usr/bin/env python3
"""HTTP API for AIOS external-agent online task distribution.
The service is intended to bind to loopback and be published through the
existing TLS reverse proxy. Control operations use a server-managed control
Bearer token. Agents receive individual Bearer tokens at registration; only
token hashes are persisted.
"""
from __future__ import annotations
from dataclasses import dataclass
import hmac
import json
import os
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
from pathlib import Path
import re
from typing import Any, Mapping
from urllib.parse import urlparse
from aios_agent_task_protocol import TaskProtocolError, TaskStore
MAX_BODY = 1024 * 1024
TASK_ID_RE = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._:-]{2,179}$")
@dataclass(frozen=True, slots=True)
class APIConfig:
db_path: Path
control_token_path: Path
host: str = "127.0.0.1"
port: int = 18940
token_pepper: str = ""
@classmethod
def from_env(cls) -> "APIConfig":
return cls(
db_path=Path(os.getenv(
"AIOS_AGENT_TASK_DB",
"/var/lib/aios-agent-task/state.sqlite3",
)),
control_token_path=Path(os.getenv(
"AIOS_AGENT_TASK_CONTROL_TOKEN_FILE",
"/run/credentials/aios-agent-task-api.service/control_token",
)),
host=os.getenv("AIOS_AGENT_TASK_HOST", "127.0.0.1"),
port=int(os.getenv("AIOS_AGENT_TASK_PORT", "18940")),
token_pepper=os.getenv("AIOS_AGENT_TASK_TOKEN_PEPPER", ""),
)
class AgentTaskApplication:
def __init__(self, config: APIConfig) -> None:
self.config = config
self.store = TaskStore(
config.db_path,
token_pepper=config.token_pepper,
)
def _control_token(self) -> str:
path = self.config.control_token_path
if path.is_symlink() or not path.is_file():
raise TaskProtocolError("CONTROL_CREDENTIAL_UNAVAILABLE", 503)
value = path.read_text(encoding="utf-8").strip()
if len(value) < 32:
raise TaskProtocolError("CONTROL_CREDENTIAL_INVALID", 503)
return value
@staticmethod
def _bearer(headers: Mapping[str, str]) -> str:
value = str(headers.get("Authorization") or "")
if not value.startswith("Bearer "):
return ""
return value[7:].strip()
def authenticate_control(self, headers: Mapping[str, str]) -> None:
supplied = self._bearer(headers)
expected = self._control_token()
if not supplied or not hmac.compare_digest(supplied, expected):
raise TaskProtocolError("UNAUTHORIZED", 401)
def authenticate_agent(self, headers: Mapping[str, str]) -> dict[str, Any]:
return self.store.authenticate_agent(self._bearer(headers))
def principal(self, headers: Mapping[str, str]) -> tuple[str, dict[str, Any] | None]:
supplied = self._bearer(headers)
if not supplied:
raise TaskProtocolError("UNAUTHORIZED", 401)
try:
expected = self._control_token()
except TaskProtocolError:
expected = ""
if expected and hmac.compare_digest(supplied, expected):
return "control", None
return "agent", self.store.authenticate_agent(supplied)
def health(self) -> dict[str, Any]:
return {
"status": "ok",
"schema": "aios.agent-task.v1",
"task_states": [
"QUEUED", "CLAIMED", "RUNNING", "VERIFYING",
"SUCCEEDED", "FAILED", "EXPIRED", "CANCELLED",
],
"lease_recovery": True,
"atomic_claim": True,
"idempotent_result_submission": True,
"server_side_verification_required": True,
"plaintext_credentials_persisted": False,
}
def register_agent(self, headers: Mapping[str, str], body: Mapping[str, Any]) -> dict[str, Any]:
self.authenticate_control(headers)
return self.store.register_agent(
agent_id=str(body.get("agent_id") or ""),
agent_type=str(body.get("agent_type") or ""),
agent_version=str(body.get("agent_version") or ""),
capabilities=body.get("capabilities") or [],
metadata=body.get("metadata") if isinstance(body.get("metadata"), Mapping) else {},
)
def create_task(self, headers: Mapping[str, str], body: Mapping[str, Any]) -> dict[str, Any]:
self.authenticate_control(headers)
return self.store.create_task(
task_id=str(body.get("task_id") or "") or None,
goal=str(body.get("goal") or ""),
instructions=str(body.get("instructions") or ""),
success_criteria=body.get("success_criteria"),
required_capabilities=body.get("required_capabilities") or [],
constraints=body.get("constraints") if isinstance(body.get("constraints"), Mapping) else {},
metadata=body.get("metadata") if isinstance(body.get("metadata"), Mapping) else {},
deadline=str(body.get("deadline") or "") or None,
priority=body.get("priority", 0),
max_attempts=body.get("max_attempts", 3),
idempotency_key=str(body.get("idempotency_key") or ""),
)
def poll(self, headers: Mapping[str, str], body: Mapping[str, Any]) -> dict[str, Any]:
agent = self.authenticate_agent(headers)
tasks = self.store.poll_tasks(
agent_id=agent["agent_id"],
limit=int(body.get("limit", 20)),
)
return {"agent_id": agent["agent_id"], "tasks": tasks}
def claim(self, task_id: str, headers: Mapping[str, str], body: Mapping[str, Any]) -> dict[str, Any]:
agent = self.authenticate_agent(headers)
return self.store.claim_task(
task_id=task_id,
agent_id=agent["agent_id"],
lease_seconds=int(body.get("lease_seconds", 300)),
)
def heartbeat(self, task_id: str, headers: Mapping[str, str], body: Mapping[str, Any]) -> dict[str, Any]:
agent = self.authenticate_agent(headers)
return self.store.heartbeat(
task_id=task_id,
agent_id=agent["agent_id"],
lease_token=str(body.get("lease_token") or ""),
progress=body.get("progress") if isinstance(body.get("progress"), Mapping) else {},
lease_seconds=int(body.get("lease_seconds", 300)),
)
def result(self, task_id: str, headers: Mapping[str, str], body: Mapping[str, Any]) -> dict[str, Any]:
agent = self.authenticate_agent(headers)
result = body.get("result")
if not isinstance(result, Mapping):
raise TaskProtocolError("RESULT_INVALID", 400)
return self.store.submit_result(
task_id=task_id,
agent_id=agent["agent_id"],
lease_token=str(body.get("lease_token") or ""),
idempotency_key=str(body.get("idempotency_key") or ""),
result=result,
)
def verify(self, task_id: str, headers: Mapping[str, str], body: Mapping[str, Any]) -> dict[str, Any]:
self.authenticate_control(headers)
return self.store.verify_task(
task_id=task_id,
decision=str(body.get("decision") or ""),
verification=body.get("verification") if isinstance(body.get("verification"), Mapping) else {},
)
def cancel(self, task_id: str, headers: Mapping[str, str], body: Mapping[str, Any]) -> dict[str, Any]:
self.authenticate_control(headers)
return self.store.cancel_task(task_id=task_id, reason=str(body.get("reason") or ""))
def get_task(self, task_id: str, headers: Mapping[str, str]) -> dict[str, Any]:
kind, agent = self.principal(headers)
task = self.store.get_task(task_id)
if kind == "agent" and task.get("claimed_by") != agent["agent_id"]:
raise TaskProtocolError("FORBIDDEN", 403)
return task
def events(self, task_id: str, headers: Mapping[str, str]) -> dict[str, Any]:
self.authenticate_control(headers)
return {"task_id": task_id, "events": self.store.get_events(task_id)}
class Handler(BaseHTTPRequestHandler):
app: AgentTaskApplication | None = None
server_version = "AIOSAgentTaskAPI/1"
def log_message(self, _format: str, *_args: Any) -> None:
return
def _headers(self) -> dict[str, str]:
return {key: value for key, value in self.headers.items()}
def _json(self, value: Any, status: int = 200) -> None:
payload = json.dumps(value, ensure_ascii=False, sort_keys=True).encode("utf-8")
self.send_response(status)
self.send_header("Content-Type", "application/json; charset=utf-8")
self.send_header("Cache-Control", "no-store")
self.send_header("Content-Length", str(len(payload)))
self.end_headers()
self.wfile.write(payload)
def _body(self) -> dict[str, Any]:
try:
size = int(self.headers.get("Content-Length", "0"))
except ValueError as exc:
raise TaskProtocolError("CONTENT_LENGTH_INVALID", 400) from exc
if size < 0 or size > MAX_BODY:
raise TaskProtocolError("REQUEST_TOO_LARGE", 413)
raw = self.rfile.read(size) if size else b"{}"
try:
value = json.loads(raw.decode("utf-8"))
except (UnicodeDecodeError, json.JSONDecodeError) as exc:
raise TaskProtocolError("JSON_INVALID", 400) from exc
if not isinstance(value, dict):
raise TaskProtocolError("JSON_OBJECT_REQUIRED", 400)
return value
@staticmethod
def _task_route(path: str) -> tuple[str, str | None]:
match = re.fullmatch(
r"/v1/tasks/([A-Za-z0-9][A-Za-z0-9._:-]{2,179})(?:/(claim|heartbeat|result|verify|cancel|events))?",
path,
)
if match:
return match.group(1), match.group(2)
return "", None
def _run(self, callback) -> None:
try:
callback()
except TaskProtocolError as exc:
self._json({"error": exc.code}, exc.http_status)
except Exception as exc:
self._json({"error": "INTERNAL_ERROR", "detail": type(exc).__name__}, 500)
def do_GET(self) -> None:
def action() -> None:
if self.app is None:
raise TaskProtocolError("SERVICE_UNAVAILABLE", 503)
path = urlparse(self.path).path.rstrip("/") or "/"
if path == "/health":
self._json(self.app.health())
return
task_id, suffix = self._task_route(path)
if task_id and suffix is None:
self._json(self.app.get_task(task_id, self._headers()))
return
if task_id and suffix == "events":
self._json(self.app.events(task_id, self._headers()))
return
self._json({"error": "NOT_FOUND"}, 404)
self._run(action)
def do_POST(self) -> None:
def action() -> None:
if self.app is None:
raise TaskProtocolError("SERVICE_UNAVAILABLE", 503)
path = urlparse(self.path).path.rstrip("/") or "/"
body = self._body()
if path == "/v1/agents/register":
self._json(self.app.register_agent(self._headers(), body), 201)
return
if path == "/v1/tasks":
self._json(self.app.create_task(self._headers(), body), 201)
return
if path == "/v1/tasks/poll":
self._json(self.app.poll(self._headers(), body))
return
task_id, suffix = self._task_route(path)
if not task_id:
self._json({"error": "NOT_FOUND"}, 404)
return
if suffix == "claim":
self._json(self.app.claim(task_id, self._headers(), body))
elif suffix == "heartbeat":
self._json(self.app.heartbeat(task_id, self._headers(), body))
elif suffix == "result":
self._json(self.app.result(task_id, self._headers(), body))
elif suffix == "verify":
self._json(self.app.verify(task_id, self._headers(), body))
elif suffix == "cancel":
self._json(self.app.cancel(task_id, self._headers(), body))
else:
self._json({"error": "NOT_FOUND"}, 404)
self._run(action)
def serve(config: APIConfig | None = None) -> None:
config = config or APIConfig.from_env()
application = AgentTaskApplication(config)
Handler.app = application
ThreadingHTTPServer((config.host, config.port), Handler).serve_forever()
if __name__ == "__main__":
serve()
@@ -1,827 +0,0 @@
#!/usr/bin/env python3
"""Durable online task protocol for external AIOS agents.
The store is deliberately independent from Git/Gitea mutation. It provides a
small durable state machine for publishing work, atomic claim/lease ownership,
heartbeat, idempotent result submission, explicit server-side verification,
lease recovery, and audit events.
No plaintext agent credential or task lease token is persisted.
"""
from __future__ import annotations
from dataclasses import dataclass
from datetime import datetime, timedelta, timezone
import hashlib
import hmac
import json
from pathlib import Path
import secrets
import sqlite3
from typing import Any, Callable, Iterable, Mapping
import uuid
TERMINAL_STATES = frozenset({"SUCCEEDED", "FAILED", "EXPIRED", "CANCELLED"})
ACTIVE_LEASE_STATES = frozenset({"CLAIMED", "RUNNING"})
TASK_STATES = frozenset({
"QUEUED", "CLAIMED", "RUNNING", "VERIFYING",
"SUCCEEDED", "FAILED", "EXPIRED", "CANCELLED",
})
VERIFY_DECISIONS = frozenset({"SUCCEEDED", "NEEDS_REWORK", "FAILED"})
class TaskProtocolError(RuntimeError):
def __init__(self, code: str, http_status: int = 409) -> None:
super().__init__(code)
self.code = code
self.http_status = http_status
@dataclass(frozen=True, slots=True)
class Principal:
kind: str
identifier: str
def utcnow() -> datetime:
return datetime.now(timezone.utc)
def iso(value: datetime) -> str:
return value.astimezone(timezone.utc).isoformat()
def parse_time(value: str | None) -> datetime | None:
if not value:
return None
parsed = datetime.fromisoformat(value)
if parsed.tzinfo is None:
parsed = parsed.replace(tzinfo=timezone.utc)
return parsed.astimezone(timezone.utc)
def canonical_json(value: Any) -> str:
return json.dumps(value, ensure_ascii=False, sort_keys=True, separators=(",", ":"))
def digest_json(value: Any) -> str:
return hashlib.sha256(canonical_json(value).encode("utf-8")).hexdigest()
def _string_list(value: Iterable[object] | None, field: str, *, max_items: int = 128) -> list[str]:
if value is None:
return []
if isinstance(value, (str, bytes)) or not isinstance(value, Iterable):
raise TaskProtocolError(field.upper() + "_INVALID", 400)
result: list[str] = []
for item in value:
text = str(item).strip()
if not text or len(text) > 160:
raise TaskProtocolError(field.upper() + "_INVALID", 400)
result.append(text)
if len(result) > max_items:
raise TaskProtocolError(field.upper() + "_TOO_LARGE", 400)
return sorted(set(result))
def _json_object(value: object | None, field: str, *, max_bytes: int = 128 * 1024) -> dict[str, Any]:
if value is None:
return {}
if not isinstance(value, Mapping):
raise TaskProtocolError(field.upper() + "_INVALID", 400)
result = dict(value)
if len(canonical_json(result).encode("utf-8")) > max_bytes:
raise TaskProtocolError(field.upper() + "_TOO_LARGE", 413)
return result
def _criteria(value: object | None) -> list[Any]:
if value is None:
return []
if isinstance(value, (str, bytes)) or not isinstance(value, list):
raise TaskProtocolError("SUCCESS_CRITERIA_INVALID", 400)
if len(value) > 128:
raise TaskProtocolError("SUCCESS_CRITERIA_TOO_LARGE", 413)
if len(canonical_json(value).encode("utf-8")) > 128 * 1024:
raise TaskProtocolError("SUCCESS_CRITERIA_TOO_LARGE", 413)
return list(value)
class TaskStore:
"""SQLite-backed durable task store.
SQLite BEGIN IMMEDIATE serializes claims so two agents cannot successfully
claim the same queued task. WAL mode allows concurrent readers.
"""
def __init__(
self,
path: str | Path,
*,
now: Callable[[], datetime] = utcnow,
token_pepper: str = "",
) -> None:
self.path = Path(path)
self.path.parent.mkdir(parents=True, exist_ok=True)
self.now = now
self.token_pepper = token_pepper
self._initialize()
def _connect(self) -> sqlite3.Connection:
connection = sqlite3.connect(self.path, timeout=10, isolation_level=None)
connection.row_factory = sqlite3.Row
connection.execute("PRAGMA foreign_keys=ON")
connection.execute("PRAGMA journal_mode=WAL")
connection.execute("PRAGMA synchronous=FULL")
connection.execute("PRAGMA busy_timeout=10000")
return connection
def _initialize(self) -> None:
with self._connect() as connection:
connection.executescript(
"""
CREATE TABLE IF NOT EXISTS agents (
agent_id TEXT PRIMARY KEY,
agent_type TEXT NOT NULL,
agent_version TEXT NOT NULL,
capabilities_json TEXT NOT NULL,
metadata_json TEXT NOT NULL,
token_hash TEXT NOT NULL UNIQUE,
status TEXT NOT NULL,
created_at TEXT NOT NULL,
last_seen_at TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS tasks (
task_id TEXT PRIMARY KEY,
goal TEXT NOT NULL,
instructions TEXT NOT NULL,
success_criteria_json TEXT NOT NULL,
required_capabilities_json TEXT NOT NULL,
constraints_json TEXT NOT NULL,
metadata_json TEXT NOT NULL,
deadline TEXT,
priority INTEGER NOT NULL,
max_attempts INTEGER NOT NULL,
attempt INTEGER NOT NULL DEFAULT 0,
status TEXT NOT NULL,
claimed_by TEXT,
lease_token_hash TEXT,
lease_expires_at TEXT,
result_json TEXT,
result_digest TEXT,
result_idempotency_key TEXT,
verification_json TEXT,
revision INTEGER NOT NULL DEFAULT 1,
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL,
FOREIGN KEY(claimed_by) REFERENCES agents(agent_id)
);
CREATE TABLE IF NOT EXISTS idempotency (
scope TEXT NOT NULL,
key TEXT NOT NULL,
payload_digest TEXT NOT NULL,
response_json TEXT NOT NULL,
created_at TEXT NOT NULL,
PRIMARY KEY(scope, key)
);
CREATE TABLE IF NOT EXISTS events (
event_id INTEGER PRIMARY KEY AUTOINCREMENT,
task_id TEXT,
agent_id TEXT,
event_type TEXT NOT NULL,
payload_json TEXT NOT NULL,
created_at TEXT NOT NULL
);
CREATE INDEX IF NOT EXISTS idx_tasks_queue
ON tasks(status, priority DESC, created_at);
CREATE INDEX IF NOT EXISTS idx_tasks_lease
ON tasks(status, lease_expires_at);
CREATE INDEX IF NOT EXISTS idx_events_task
ON events(task_id, event_id);
"""
)
def _token_hash(self, token: str) -> str:
return hashlib.sha256((self.token_pepper + "\0" + token).encode("utf-8")).hexdigest()
@staticmethod
def _row_task(row: sqlite3.Row, *, include_result: bool = True) -> dict[str, Any]:
value: dict[str, Any] = {
"task_id": row["task_id"],
"goal": row["goal"],
"instructions": row["instructions"],
"success_criteria": json.loads(row["success_criteria_json"]),
"required_capabilities": json.loads(row["required_capabilities_json"]),
"constraints": json.loads(row["constraints_json"]),
"metadata": json.loads(row["metadata_json"]),
"deadline": row["deadline"],
"priority": row["priority"],
"max_attempts": row["max_attempts"],
"attempt": row["attempt"],
"status": row["status"],
"claimed_by": row["claimed_by"],
"lease_expires_at": row["lease_expires_at"],
"revision": row["revision"],
"created_at": row["created_at"],
"updated_at": row["updated_at"],
}
if include_result:
value["result"] = json.loads(row["result_json"]) if row["result_json"] else None
value["verification"] = (
json.loads(row["verification_json"]) if row["verification_json"] else None
)
return value
def _event(
self,
connection: sqlite3.Connection,
event_type: str,
*,
task_id: str | None = None,
agent_id: str | None = None,
payload: Mapping[str, Any] | None = None,
) -> None:
connection.execute(
"INSERT INTO events(task_id,agent_id,event_type,payload_json,created_at)"
" VALUES(?,?,?,?,?)",
(task_id, agent_id, event_type, canonical_json(dict(payload or {})), iso(self.now())),
)
def register_agent(
self,
*,
agent_id: str,
agent_type: str,
agent_version: str,
capabilities: Iterable[object],
metadata: Mapping[str, Any] | None = None,
) -> dict[str, Any]:
agent_id = agent_id.strip()
agent_type = agent_type.strip()
agent_version = agent_version.strip()
if not agent_id or len(agent_id) > 160 or not agent_type or not agent_version:
raise TaskProtocolError("AGENT_IDENTITY_INVALID", 400)
caps = _string_list(capabilities, "capabilities")
meta = _json_object(metadata, "metadata")
token = "agt_" + secrets.token_urlsafe(32)
now = iso(self.now())
try:
with self._connect() as connection:
connection.execute("BEGIN IMMEDIATE")
connection.execute(
"INSERT INTO agents(agent_id,agent_type,agent_version,capabilities_json,"
"metadata_json,token_hash,status,created_at,last_seen_at)"
" VALUES(?,?,?,?,?,?,?,?,?)",
(
agent_id,
agent_type,
agent_version,
canonical_json(caps),
canonical_json(meta),
self._token_hash(token),
"ACTIVE",
now,
now,
),
)
self._event(
connection,
"AGENT_REGISTERED",
agent_id=agent_id,
payload={"agent_type": agent_type, "agent_version": agent_version},
)
connection.commit()
except sqlite3.IntegrityError as exc:
raise TaskProtocolError("AGENT_ALREADY_EXISTS", 409) from exc
return {
"agent_id": agent_id,
"agent_type": agent_type,
"agent_version": agent_version,
"capabilities": caps,
"agent_token": token,
"credential_returned_once": True,
}
def authenticate_agent(self, token: str) -> dict[str, Any]:
if not token:
raise TaskProtocolError("UNAUTHORIZED", 401)
token_hash = self._token_hash(token)
with self._connect() as connection:
row = connection.execute(
"SELECT * FROM agents WHERE token_hash=? AND status='ACTIVE'",
(token_hash,),
).fetchone()
if row is None:
raise TaskProtocolError("UNAUTHORIZED", 401)
connection.execute(
"UPDATE agents SET last_seen_at=? WHERE agent_id=?",
(iso(self.now()), row["agent_id"]),
)
return {
"agent_id": row["agent_id"],
"agent_type": row["agent_type"],
"agent_version": row["agent_version"],
"capabilities": json.loads(row["capabilities_json"]),
"metadata": json.loads(row["metadata_json"]),
}
def _idempotent_lookup(
self,
connection: sqlite3.Connection,
*,
scope: str,
key: str,
payload_digest: str,
) -> dict[str, Any] | None:
row = connection.execute(
"SELECT payload_digest,response_json FROM idempotency WHERE scope=? AND key=?",
(scope, key),
).fetchone()
if row is None:
return None
if not hmac.compare_digest(row["payload_digest"], payload_digest):
raise TaskProtocolError("IDEMPOTENCY_CONFLICT", 409)
return json.loads(row["response_json"])
def _idempotent_store(
self,
connection: sqlite3.Connection,
*,
scope: str,
key: str,
payload_digest: str,
response: Mapping[str, Any],
) -> None:
connection.execute(
"INSERT INTO idempotency(scope,key,payload_digest,response_json,created_at)"
" VALUES(?,?,?,?,?)",
(scope, key, payload_digest, canonical_json(dict(response)), iso(self.now())),
)
def create_task(
self,
*,
task_id: str | None,
goal: str,
instructions: str,
success_criteria: object | None,
required_capabilities: Iterable[object] | None = None,
constraints: Mapping[str, Any] | None = None,
metadata: Mapping[str, Any] | None = None,
deadline: str | None = None,
priority: int = 0,
max_attempts: int = 3,
idempotency_key: str,
) -> dict[str, Any]:
task_id = (task_id or ("task-" + uuid.uuid4().hex)).strip()
goal = goal.strip()
instructions = instructions.strip()
idempotency_key = idempotency_key.strip()
if not task_id or len(task_id) > 180 or not goal or not instructions:
raise TaskProtocolError("TASK_BINDING_INVALID", 400)
if not idempotency_key or len(idempotency_key) > 160:
raise TaskProtocolError("IDEMPOTENCY_KEY_INVALID", 400)
if not isinstance(priority, int) or isinstance(priority, bool) or not -100 <= priority <= 100:
raise TaskProtocolError("PRIORITY_INVALID", 400)
if not isinstance(max_attempts, int) or isinstance(max_attempts, bool) or not 1 <= max_attempts <= 20:
raise TaskProtocolError("MAX_ATTEMPTS_INVALID", 400)
parsed_deadline = parse_time(deadline)
if parsed_deadline and parsed_deadline <= self.now():
raise TaskProtocolError("DEADLINE_INVALID", 400)
criteria = _criteria(success_criteria)
required = _string_list(required_capabilities, "required_capabilities")
constraints_value = _json_object(constraints, "constraints")
metadata_value = _json_object(metadata, "metadata")
payload = {
"task_id": task_id,
"goal": goal,
"instructions": instructions,
"success_criteria": criteria,
"required_capabilities": required,
"constraints": constraints_value,
"metadata": metadata_value,
"deadline": iso(parsed_deadline) if parsed_deadline else None,
"priority": priority,
"max_attempts": max_attempts,
}
payload_digest = digest_json(payload)
now = iso(self.now())
with self._connect() as connection:
connection.execute("BEGIN IMMEDIATE")
replay = self._idempotent_lookup(
connection,
scope="create_task",
key=idempotency_key,
payload_digest=payload_digest,
)
if replay is not None:
connection.commit()
return {**replay, "idempotent": True}
try:
connection.execute(
"INSERT INTO tasks(task_id,goal,instructions,success_criteria_json,"
"required_capabilities_json,constraints_json,metadata_json,deadline,priority,"
"max_attempts,attempt,status,revision,created_at,updated_at)"
" VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)",
(
task_id,
goal,
instructions,
canonical_json(criteria),
canonical_json(required),
canonical_json(constraints_value),
canonical_json(metadata_value),
payload["deadline"],
priority,
max_attempts,
0,
"QUEUED",
1,
now,
now,
),
)
except sqlite3.IntegrityError as exc:
raise TaskProtocolError("TASK_ID_CONFLICT", 409) from exc
row = connection.execute("SELECT * FROM tasks WHERE task_id=?", (task_id,)).fetchone()
result = self._row_task(row)
self._idempotent_store(
connection,
scope="create_task",
key=idempotency_key,
payload_digest=payload_digest,
response=result,
)
self._event(connection, "TASK_CREATED", task_id=task_id, payload={"priority": priority})
connection.commit()
return result
def _expire_locked(self, connection: sqlite3.Connection) -> None:
now_dt = self.now()
now = iso(now_dt)
rows = connection.execute(
"SELECT * FROM tasks WHERE status IN ('QUEUED','CLAIMED','RUNNING')"
).fetchall()
for row in rows:
deadline = parse_time(row["deadline"])
if deadline is not None and deadline <= now_dt:
connection.execute(
"UPDATE tasks SET status='EXPIRED',lease_token_hash=NULL,"
"lease_expires_at=NULL,revision=revision+1,updated_at=? WHERE task_id=?",
(now, row["task_id"]),
)
self._event(connection, "TASK_EXPIRED", task_id=row["task_id"])
continue
if row["status"] in ACTIVE_LEASE_STATES:
lease_expires = parse_time(row["lease_expires_at"])
if lease_expires is not None and lease_expires <= now_dt:
if row["attempt"] >= row["max_attempts"]:
next_state = "FAILED"
else:
next_state = "QUEUED"
if next_state == "FAILED":
connection.execute(
"UPDATE tasks SET status=?,lease_token_hash=NULL,"
"lease_expires_at=NULL,revision=revision+1,updated_at=? WHERE task_id=?",
(next_state, now, row["task_id"]),
)
else:
connection.execute(
"UPDATE tasks SET status=?,claimed_by=NULL,lease_token_hash=NULL,"
"lease_expires_at=NULL,revision=revision+1,updated_at=? WHERE task_id=?",
(next_state, now, row["task_id"]),
)
self._event(
connection,
"LEASE_EXPIRED",
task_id=row["task_id"],
agent_id=row["claimed_by"],
payload={"next_state": next_state, "attempt": row["attempt"]},
)
def expire_leases(self) -> int:
with self._connect() as connection:
connection.execute("BEGIN IMMEDIATE")
before = connection.total_changes
self._expire_locked(connection)
changed = connection.total_changes - before
connection.commit()
return changed
def poll_tasks(self, *, agent_id: str, limit: int = 20) -> list[dict[str, Any]]:
if not 1 <= int(limit) <= 100:
raise TaskProtocolError("LIMIT_INVALID", 400)
with self._connect() as connection:
connection.execute("BEGIN IMMEDIATE")
self._expire_locked(connection)
agent = connection.execute(
"SELECT capabilities_json FROM agents WHERE agent_id=? AND status='ACTIVE'",
(agent_id,),
).fetchone()
if agent is None:
raise TaskProtocolError("AGENT_NOT_FOUND", 404)
capabilities = set(json.loads(agent["capabilities_json"]))
rows = connection.execute(
"SELECT * FROM tasks WHERE status='QUEUED' ORDER BY priority DESC,created_at ASC"
).fetchall()
result = []
for row in rows:
required = set(json.loads(row["required_capabilities_json"]))
if required.issubset(capabilities):
result.append(self._row_task(row, include_result=False))
if len(result) >= int(limit):
break
connection.commit()
return result
def _agent_capabilities(self, connection: sqlite3.Connection, agent_id: str) -> set[str]:
row = connection.execute(
"SELECT capabilities_json FROM agents WHERE agent_id=? AND status='ACTIVE'",
(agent_id,),
).fetchone()
if row is None:
raise TaskProtocolError("AGENT_NOT_FOUND", 404)
return set(json.loads(row["capabilities_json"]))
def claim_task(
self,
*,
task_id: str,
agent_id: str,
lease_seconds: int = 300,
) -> dict[str, Any]:
if not isinstance(lease_seconds, int) or isinstance(lease_seconds, bool) or not 30 <= lease_seconds <= 3600:
raise TaskProtocolError("LEASE_SECONDS_INVALID", 400)
lease_token = "lease_" + secrets.token_urlsafe(32)
with self._connect() as connection:
connection.execute("BEGIN IMMEDIATE")
self._expire_locked(connection)
row = connection.execute("SELECT * FROM tasks WHERE task_id=?", (task_id,)).fetchone()
if row is None:
raise TaskProtocolError("TASK_NOT_FOUND", 404)
if row["status"] != "QUEUED":
raise TaskProtocolError("TASK_NOT_CLAIMABLE", 409)
capabilities = self._agent_capabilities(connection, agent_id)
required = set(json.loads(row["required_capabilities_json"]))
if not required.issubset(capabilities):
raise TaskProtocolError("AGENT_CAPABILITY_MISMATCH", 403)
attempt = int(row["attempt"]) + 1
if attempt > int(row["max_attempts"]):
raise TaskProtocolError("TASK_ATTEMPTS_EXHAUSTED", 409)
expires = self.now() + timedelta(seconds=lease_seconds)
now = iso(self.now())
updated = connection.execute(
"UPDATE tasks SET status='CLAIMED',claimed_by=?,lease_token_hash=?,"
"lease_expires_at=?,attempt=?,revision=revision+1,updated_at=? "
"WHERE task_id=? AND status='QUEUED'",
(
agent_id,
self._token_hash(lease_token),
iso(expires),
attempt,
now,
task_id,
),
)
if updated.rowcount != 1:
raise TaskProtocolError("TASK_CLAIM_CONFLICT", 409)
self._event(
connection,
"TASK_CLAIMED",
task_id=task_id,
agent_id=agent_id,
payload={"attempt": attempt, "lease_expires_at": iso(expires)},
)
row = connection.execute("SELECT * FROM tasks WHERE task_id=?", (task_id,)).fetchone()
connection.commit()
return {
**self._row_task(row, include_result=False),
"lease_token": lease_token,
"lease_token_returned_once": True,
}
def _validate_lease(
self,
connection: sqlite3.Connection,
*,
task_id: str,
agent_id: str,
lease_token: str,
allowed_states: frozenset[str] = ACTIVE_LEASE_STATES,
) -> sqlite3.Row:
self._expire_locked(connection)
row = connection.execute("SELECT * FROM tasks WHERE task_id=?", (task_id,)).fetchone()
if row is None:
raise TaskProtocolError("TASK_NOT_FOUND", 404)
if row["status"] not in allowed_states:
raise TaskProtocolError("TASK_LEASE_NOT_ACTIVE", 409)
if row["claimed_by"] != agent_id:
raise TaskProtocolError("TASK_LEASE_OWNER_MISMATCH", 403)
if not row["lease_token_hash"] or not hmac.compare_digest(
row["lease_token_hash"], self._token_hash(lease_token)
):
raise TaskProtocolError("TASK_LEASE_TOKEN_INVALID", 403)
expires = parse_time(row["lease_expires_at"])
if expires is None or expires <= self.now():
raise TaskProtocolError("TASK_LEASE_EXPIRED", 409)
return row
def heartbeat(
self,
*,
task_id: str,
agent_id: str,
lease_token: str,
progress: Mapping[str, Any] | None = None,
lease_seconds: int = 300,
) -> dict[str, Any]:
if not 30 <= int(lease_seconds) <= 3600:
raise TaskProtocolError("LEASE_SECONDS_INVALID", 400)
progress_value = _json_object(progress, "progress", max_bytes=64 * 1024)
with self._connect() as connection:
connection.execute("BEGIN IMMEDIATE")
self._validate_lease(
connection,
task_id=task_id,
agent_id=agent_id,
lease_token=lease_token,
)
expires = self.now() + timedelta(seconds=int(lease_seconds))
now = iso(self.now())
connection.execute(
"UPDATE tasks SET status='RUNNING',lease_expires_at=?,revision=revision+1,"
"updated_at=? WHERE task_id=?",
(iso(expires), now, task_id),
)
self._event(
connection,
"TASK_HEARTBEAT",
task_id=task_id,
agent_id=agent_id,
payload={"progress": progress_value, "lease_expires_at": iso(expires)},
)
row = connection.execute("SELECT * FROM tasks WHERE task_id=?", (task_id,)).fetchone()
connection.commit()
return self._row_task(row, include_result=False)
def submit_result(
self,
*,
task_id: str,
agent_id: str,
lease_token: str,
idempotency_key: str,
result: Mapping[str, Any],
) -> dict[str, Any]:
idempotency_key = idempotency_key.strip()
if not idempotency_key or len(idempotency_key) > 160:
raise TaskProtocolError("IDEMPOTENCY_KEY_INVALID", 400)
result_value = _json_object(result, "result", max_bytes=1024 * 1024)
result_digest = digest_json(result_value)
with self._connect() as connection:
connection.execute("BEGIN IMMEDIATE")
row = connection.execute("SELECT * FROM tasks WHERE task_id=?", (task_id,)).fetchone()
if row is None:
raise TaskProtocolError("TASK_NOT_FOUND", 404)
if row["result_idempotency_key"] == idempotency_key:
if not row["result_digest"] or not hmac.compare_digest(row["result_digest"], result_digest):
raise TaskProtocolError("IDEMPOTENCY_CONFLICT", 409)
connection.commit()
return {**self._row_task(row), "idempotent": True}
self._validate_lease(
connection,
task_id=task_id,
agent_id=agent_id,
lease_token=lease_token,
)
now = iso(self.now())
connection.execute(
"UPDATE tasks SET status='VERIFYING',result_json=?,result_digest=?,"
"result_idempotency_key=?,lease_token_hash=NULL,lease_expires_at=NULL,"
"revision=revision+1,updated_at=? WHERE task_id=?",
(
canonical_json(result_value),
result_digest,
idempotency_key,
now,
task_id,
),
)
self._event(
connection,
"RESULT_SUBMITTED",
task_id=task_id,
agent_id=agent_id,
payload={"result_digest": result_digest, "idempotency_key": idempotency_key},
)
row = connection.execute("SELECT * FROM tasks WHERE task_id=?", (task_id,)).fetchone()
connection.commit()
return self._row_task(row)
def verify_task(
self,
*,
task_id: str,
decision: str,
verification: Mapping[str, Any] | None = None,
) -> dict[str, Any]:
decision = decision.strip().upper()
if decision not in VERIFY_DECISIONS:
raise TaskProtocolError("VERIFICATION_DECISION_INVALID", 400)
verification_value = _json_object(verification, "verification")
verification_value = {"decision": decision, **verification_value}
with self._connect() as connection:
connection.execute("BEGIN IMMEDIATE")
row = connection.execute("SELECT * FROM tasks WHERE task_id=?", (task_id,)).fetchone()
if row is None:
raise TaskProtocolError("TASK_NOT_FOUND", 404)
if row["status"] != "VERIFYING":
raise TaskProtocolError("TASK_NOT_VERIFYING", 409)
if decision == "SUCCEEDED":
next_state = "SUCCEEDED"
elif decision == "FAILED":
next_state = "FAILED"
elif int(row["attempt"]) >= int(row["max_attempts"]):
next_state = "FAILED"
verification_value["reason"] = verification_value.get(
"reason", "MAX_ATTEMPTS_EXHAUSTED"
)
else:
next_state = "QUEUED"
now = iso(self.now())
if next_state in {"SUCCEEDED", "FAILED"}:
connection.execute(
"UPDATE tasks SET status=?,lease_token_hash=NULL,"
"lease_expires_at=NULL,verification_json=?,revision=revision+1,updated_at=? "
"WHERE task_id=?",
(next_state, canonical_json(verification_value), now, task_id),
)
else:
connection.execute(
"UPDATE tasks SET status=?,claimed_by=NULL,lease_token_hash=NULL,"
"lease_expires_at=NULL,verification_json=?,revision=revision+1,updated_at=? "
"WHERE task_id=?",
(next_state, canonical_json(verification_value), now, task_id),
)
self._event(
connection,
"TASK_VERIFIED",
task_id=task_id,
agent_id=row["claimed_by"],
payload={"decision": decision, "next_state": next_state},
)
updated = connection.execute("SELECT * FROM tasks WHERE task_id=?", (task_id,)).fetchone()
connection.commit()
return self._row_task(updated)
def cancel_task(self, *, task_id: str, reason: str = "") -> dict[str, Any]:
with self._connect() as connection:
connection.execute("BEGIN IMMEDIATE")
row = connection.execute("SELECT * FROM tasks WHERE task_id=?", (task_id,)).fetchone()
if row is None:
raise TaskProtocolError("TASK_NOT_FOUND", 404)
if row["status"] in TERMINAL_STATES:
connection.commit()
return self._row_task(row)
connection.execute(
"UPDATE tasks SET status='CANCELLED',lease_token_hash=NULL,"
"lease_expires_at=NULL,revision=revision+1,updated_at=? WHERE task_id=?",
(iso(self.now()), task_id),
)
self._event(connection, "TASK_CANCELLED", task_id=task_id, payload={"reason": reason})
updated = connection.execute("SELECT * FROM tasks WHERE task_id=?", (task_id,)).fetchone()
connection.commit()
return self._row_task(updated)
def get_task(self, task_id: str) -> dict[str, Any]:
self.expire_leases()
with self._connect() as connection:
row = connection.execute("SELECT * FROM tasks WHERE task_id=?", (task_id,)).fetchone()
if row is None:
raise TaskProtocolError("TASK_NOT_FOUND", 404)
return self._row_task(row)
def get_events(self, task_id: str, *, limit: int = 200) -> list[dict[str, Any]]:
with self._connect() as connection:
rows = connection.execute(
"SELECT * FROM events WHERE task_id=? ORDER BY event_id DESC LIMIT ?",
(task_id, max(1, min(int(limit), 1000))),
).fetchall()
return [
{
"event_id": row["event_id"],
"task_id": row["task_id"],
"agent_id": row["agent_id"],
"event_type": row["event_type"],
"payload": json.loads(row["payload_json"]),
"created_at": row["created_at"],
}
for row in reversed(rows)
]
@@ -94,6 +94,91 @@ class AuthorityGitExecutor:
self._git(["worktree", "add", "--detach", str(path), base_oid])
return path
def recover_failed_detached_worktree_exact(
self,
*,
path: Path,
expected_oid: str,
stale_oid: str,
target_ref: str,
) -> str:
"""Recreate one exact clean, detached, single-attempt worktree at its durable head."""
_oid(expected_oid, "expected_oid")
_oid(stale_oid, "stale_oid")
if not TASK_BRANCH_RE.fullmatch(target_ref):
raise AuthorityGitError("TASK_BRANCH_SCOPE_DENIED")
if self.read_ref(target_ref) is not None:
raise AuthorityGitError("AUTHORITY_TARGET_BRANCH_ALREADY_EXISTS")
if (
not path.is_absolute()
or path.parent != self.worktree_root
or path.name in {"", ".", ".."}
or path.is_symlink()
or not path.is_dir()
):
raise AuthorityGitError("WORKTREE_RECOVERY_PATH_INVALID")
head = subprocess.run(
["git", "-C", str(path), "rev-parse", "HEAD"],
text=True, capture_output=True, check=False,
)
if head.returncode != 0 or head.stdout.strip() != stale_oid or stale_oid == expected_oid:
raise AuthorityGitError("WORKTREE_RECOVERY_HEAD_INVALID")
symbolic = subprocess.run(
["git", "-C", str(path), "symbolic-ref", "-q", "HEAD"],
text=True, capture_output=True, check=False,
)
if symbolic.returncode != 1:
raise AuthorityGitError("WORKTREE_RECOVERY_NOT_DETACHED")
status = subprocess.run(
["git", "-C", str(path), "status", "--porcelain=v1", "--untracked-files=all"],
text=True, capture_output=True, check=False,
)
if status.returncode != 0 or status.stdout:
raise AuthorityGitError("WORKTREE_RECOVERY_NOT_CLEAN")
parents = self._git(["rev-list", "--parents", "-n", "1", stale_oid]).split()
if parents != [stale_oid, expected_oid]:
raise AuthorityGitError("WORKTREE_RECOVERY_LINEAGE_INVALID")
registered = [
line.removeprefix("worktree ")
for line in self._git(["worktree", "list", "--porcelain"]).splitlines()
if line.startswith("worktree ")
]
if registered.count(str(path)) != 1:
raise AuthorityGitError("WORKTREE_RECOVERY_REGISTRATION_INVALID")
self._git(["worktree", "remove", "--force", str(path)])
if path.exists() or path.is_symlink():
raise AuthorityGitError("WORKTREE_RECOVERY_REMOVE_FAILED")
recreated = self.create_detached_worktree(expected_oid, path.name, path.parent)
readback = subprocess.run(
["git", "-C", str(recreated), "rev-parse", "HEAD"],
text=True, capture_output=True, check=False,
)
detached = subprocess.run(
["git", "-C", str(recreated), "symbolic-ref", "-q", "HEAD"],
text=True, capture_output=True, check=False,
)
if readback.returncode != 0 or readback.stdout.strip() != expected_oid or detached.returncode != 1:
raise AuthorityGitError("WORKTREE_RECOVERY_READBACK_MISMATCH")
return stale_oid
def create_task_branch_CAS(self, branch: str, expected_base_oid: str, new_oid: str) -> str:
if not TASK_BRANCH_RE.fullmatch(branch):
raise AuthorityGitError("TASK_BRANCH_SCOPE_DENIED")
_oid(expected_base_oid, "expected_base_oid")
_oid(new_oid, "new_oid")
current = self.read_ref(branch)
if current is not None:
if current == new_oid:
return new_oid
raise AuthorityGitError("AUTHORITY_TARGET_BRANCH_ALREADY_EXISTS")
if not self.ancestry_check(expected_base_oid, new_oid):
raise AuthorityGitError("NEW_OID_NOT_DESCENDANT")
self._git(["update-ref", branch, new_oid, "0" * 40])
readback = self.read_ref(branch)
if readback != new_oid:
raise AuthorityGitError("AUTHORITY_CREATE_READBACK_MISMATCH")
return readback
def advance_task_branch_CAS(self, branch: str, expected_oid: str, new_oid: str) -> str:
if not TASK_BRANCH_RE.fullmatch(branch):
raise AuthorityGitError("TASK_BRANCH_SCOPE_DENIED")
@@ -1,133 +0,0 @@
"""Private Unix-socket caller projection and rehydration client.
The broker response body is deliberately public metadata only. The short-lived
credential projection is delivered in a private response header, consumed
inside the model API process, and never returned by an HTTP API or persisted.
"""
from __future__ import annotations
from dataclasses import dataclass
import hashlib
import http.client
import json
from pathlib import Path
import socket
from typing import Any, Mapping
CALLER = "chatgpt-governed-implementer-singlehost"
SCOPE = "release:candidate"
CREDENTIAL_HEADER = "X-AIOS-Caller-Projection-Credential"
class CallerProjectionError(RuntimeError):
def __init__(self, code: str, status: int = 503) -> None:
super().__init__(code)
self.code = code
self.status = status
@dataclass(frozen=True)
class ProjectionResult:
public: dict[str, Any]
credential_loaded: bool
class _UnixHTTPConnection(http.client.HTTPConnection):
def __init__(self, socket_path: Path, timeout: float) -> None:
super().__init__("localhost", timeout=timeout)
self.socket_path = socket_path
def connect(self) -> None:
sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
sock.settimeout(self.timeout)
try:
sock.connect(str(self.socket_path))
except Exception:
sock.close()
raise
self.sock = sock
class CallerProjectionClient:
def __init__(self, socket_path: Path, *, timeout: float = 2.0) -> None:
self.socket_path = Path(socket_path)
self.timeout = timeout
@staticmethod
def _idempotency_key(caller: str, scope: str) -> str:
digest = hashlib.sha256((caller + "\0" + scope).encode()).hexdigest()
return "caller-projection-" + digest[:32] + "-v1"
def rehydrate(
self,
caller: str,
scope: str,
request: Mapping[str, Any] | None = None,
) -> ProjectionResult:
if caller != CALLER:
raise CallerProjectionError("CALLER_MISMATCH", 403)
if scope != SCOPE:
raise CallerProjectionError("SCOPE_MISMATCH", 403)
body = {
"caller": caller,
"scope": scope,
"idempotency_key": self._idempotency_key(caller, scope),
"request": dict(request or {}),
}
connection = _UnixHTTPConnection(self.socket_path, self.timeout)
try:
connection.request(
"POST",
"/v1/project-caller",
body=json.dumps(body, sort_keys=True, separators=(",", ":")).encode(),
headers={
"Content-Type": "application/json",
"Accept": "application/json",
"X-AIOS-Account": caller,
},
)
response = connection.getresponse()
status = response.status
raw = response.read()
credential = response.getheader(CREDENTIAL_HEADER, "")
except (OSError, TimeoutError, http.client.HTTPException) as exc:
raise CallerProjectionError("PROJECTION_BROKER_UNAVAILABLE", 503) from exc
finally:
connection.close()
try:
value = json.loads(raw.decode("utf-8"))
except Exception as exc:
raise CallerProjectionError("PROJECTION_RESPONSE_INVALID", 503) from exc
if not isinstance(value, Mapping):
raise CallerProjectionError("PROJECTION_RESPONSE_INVALID", 503)
if status != 200:
code = str(value.get("error") or "PROJECTION_DENIED")
raise CallerProjectionError(code, 403 if status < 500 else 503)
if any(
str(key).lower() in {
"credential",
"credential_material",
"token",
"bearer",
"secret",
"password",
}
for key in value
):
raise CallerProjectionError("PROJECTION_SECRET_FIELD_REJECTED", 503)
if value.get("caller") != CALLER or value.get("scope") != SCOPE:
raise CallerProjectionError("PROJECTION_BINDING_INVALID", 503)
if value.get("projection_state") != "ACKED":
raise CallerProjectionError("PROJECTION_STATE_INVALID", 503)
if value.get("credential_state") != "REHYDRATED":
raise CallerProjectionError("CREDENTIAL_REHYDRATION_INVALID", 503)
if value.get("rehydration_state") not in {"GENERATED", "RECOVERED"}:
raise CallerProjectionError("REHYDRATION_STATE_INVALID", 503)
if value.get("effective_scopes") != [SCOPE]:
raise CallerProjectionError("PROJECTION_SCOPE_INVALID", 503)
if value.get("secret_exposed") is not False:
raise CallerProjectionError("PROJECTION_SECRET_EXPOSURE", 503)
if not isinstance(credential, str) or len(credential) < 32:
raise CallerProjectionError("CREDENTIAL_REHYDRATION_MISSING", 503)
return ProjectionResult(public=dict(value), credential_loaded=True)
@@ -12,7 +12,11 @@ BRIDGE_OPERATIONS = (
"getRuntimeStatus",
"getGovernedWriteCapabilities",
"createGovernedTask",
"projectOnlineAgentExecution",
"registerOnlineAgentExecutionPlan",
"acquireWriterLease",
"bootstrapWriterLease",
"renewWriterLease",
"commitFileGoverned",
"commitFilesGoverned",
"getGitOpsJob",
@@ -30,7 +34,14 @@ BRIDGE_OPERATIONS = (
OPERATION_REQUIRED_FIELDS = {
"createGovernedTask": ("task_id", "repository", "base_oid", "target_branch", "risk", "description", "idempotency_key"),
"projectOnlineAgentExecution": ("task_id",),
"registerOnlineAgentExecutionPlan": (
"schema", "task_id", "job_id", "source_head_oid", "generation",
"idempotency_key", "execution",
),
"acquireWriterLease": ("schema", "task_id", "repository", "branch_mode", "base_oid", "expected_branch_oid", "target_branch", "generation", "lease_seconds"),
"bootstrapWriterLease": ("schema", "task_id", "repository", "branch_mode", "base_oid", "expected_branch_oid", "target_branch", "generation", "lease_seconds"),
"renewWriterLease": ("schema", "task_id", "repository", "branch_mode", "base_oid", "expected_branch_oid", "target_branch", "generation", "lease_seconds"),
"commitFileGoverned": ("task_id", "repository", "expected_branch_oid", "target_branch", "generation", "idempotency_key", "path", "content_base64", "message"),
"commitFilesGoverned": ("task_id", "repository", "expected_branch_oid", "target_branch", "generation", "idempotency_key", "files", "message"),
"getGitOpsJob": ("job_id",),
@@ -46,6 +57,68 @@ OPERATION_REQUIRED_FIELDS = {
"getControlPlaneSnapshot": (),
}
BRIDGE_OPERATION_CONTRACTS = {
"projectOnlineAgentExecution": {
"method": "POST",
"path": "/v2/execution-envelope",
"required_fields": OPERATION_REQUIRED_FIELDS["projectOnlineAgentExecution"],
"response_fields": (
"schema", "task_id", "job_id", "repository", "base_oid",
"expected_branch_oid", "target_branch", "branch_mode", "generation",
"idempotency_key", "action", "execution_digest",
),
},
"registerOnlineAgentExecutionPlan": {
"method": "POST",
"path": "/v2/execution-plan",
"required_fields": OPERATION_REQUIRED_FIELDS["registerOnlineAgentExecutionPlan"],
"response_fields": (
"schema", "task_id", "job_id", "source_head_oid", "generation",
"idempotency_key", "plan_digest", "execution", "execution_digest",
),
},
"acquireWriterLease": {
"method": "POST",
"path": "/v2/writer-lease",
"required_fields": OPERATION_REQUIRED_FIELDS["acquireWriterLease"],
"response_fields": (
"task_id", "job_id", "generation", "lease_seconds", "lease_expires_at",
"create_target_branch", "worktree_mode", "cas_ref_update_required",
),
},
"bootstrapWriterLease": {
"method": "POST",
"path": "/v2/writer-lease/bootstrap",
"required_fields": OPERATION_REQUIRED_FIELDS["bootstrapWriterLease"],
"response_fields": (
"task_id", "job_id", "generation", "lease_seconds", "lease_expires_at",
"create_target_branch", "worktree_mode", "cas_ref_update_required",
"recovered", "bootstrapped", "runtime_rehydrated",
"durable_branch_lease_present", "idempotent",
),
},
"renewWriterLease": {
"method": "POST",
"path": "/v2/writer-lease/renew",
"required_fields": OPERATION_REQUIRED_FIELDS["renewWriterLease"],
"response_fields": (
"task_id", "job_id", "generation", "lease_seconds", "lease_expires_at",
"recovered", "bootstrapped", "runtime_rehydrated",
"durable_branch_lease_present", "idempotent",
),
},
"commitFileGoverned": {
"method": "POST", "path": "/v2/commit-file",
"required_fields": OPERATION_REQUIRED_FIELDS["commitFileGoverned"],
"response_fields": ("job_id", "commit_oid", "commit_visible", "branch_mode", "target_branch", "generation"),
},
"commitFilesGoverned": {
"method": "POST", "path": "/v2/commit-files",
"required_fields": OPERATION_REQUIRED_FIELDS["commitFilesGoverned"],
"response_fields": ("job_id", "commit_oid", "commit_visible", "branch_mode", "target_branch", "generation"),
},
}
class RuntimeDriftError(RuntimeError):
pass
@@ -122,6 +195,14 @@ class BridgeV2Adapter:
"legacy_writes_enabled": False,
"governed_writes_enabled": True,
"operations": list(BRIDGE_OPERATIONS),
"operation_contracts": {
operation: {
**contract,
"required_fields": list(contract["required_fields"]),
"response_fields": list(contract["response_fields"]),
}
for operation, contract in BRIDGE_OPERATION_CONTRACTS.items()
},
"force_push": False,
"main_write": False,
"tag_write": False,
@@ -150,7 +231,11 @@ class BridgeV2Adapter:
return handler(dict(request))
def createGovernedTask(self, request: Mapping[str, Any]) -> Any: return self._call("createGovernedTask", request)
def projectOnlineAgentExecution(self, request: Mapping[str, Any]) -> Any: return self._call("projectOnlineAgentExecution", request)
def registerOnlineAgentExecutionPlan(self, request: Mapping[str, Any]) -> Any: return self._call("registerOnlineAgentExecutionPlan", request)
def acquireWriterLease(self, request: Mapping[str, Any]) -> Any: return self._call("acquireWriterLease", request)
def bootstrapWriterLease(self, request: Mapping[str, Any]) -> Any: return self._call("bootstrapWriterLease", request)
def renewWriterLease(self, request: Mapping[str, Any]) -> Any: return self._call("renewWriterLease", request)
def commitFileGoverned(self, request: Mapping[str, Any]) -> Any: return self._call("commitFileGoverned", request)
def commitFilesGoverned(self, request: Mapping[str, Any]) -> Any: return self._call("commitFilesGoverned", request)
def getGitOpsJob(self, request: Mapping[str, Any]) -> Any: return self._call("getGitOpsJob", request)
File diff suppressed because it is too large Load Diff
+158 -35
View File
@@ -51,11 +51,13 @@ class AuthorityPort(Protocol):
def branch_exists(self, branch: str) -> bool: ...
def branch_head(self, branch: str) -> str: ...
def is_ancestor(self, older: str, newer: str) -> bool: ...
def create_task_branch_cas(self, branch: str, expected_base: str, new: str) -> str: ...
def compare_and_swap_branch(self, branch: str, expected: str, new: str) -> str: ...
class RemotePort(Protocol):
def repository_identity(self) -> str: ...
def push_new_exact(self, branch: str, new: str) -> None: ...
def push_exact(self, branch: str, expected: str, new: str) -> None: ...
def read_git_ref(self, branch: str) -> str | None: ...
def read_api_branch(self, branch: str) -> str | None: ...
@@ -103,6 +105,9 @@ class AuthorityGitAdapter:
def is_ancestor(self, older: str, newer: str) -> bool:
return self.executor.ancestry_check(older, newer)
def create_task_branch_cas(self, branch: str, expected_base: str, new: str) -> str:
return self.executor.create_task_branch_CAS(self._ref(branch), expected_base, new)
def compare_and_swap_branch(self, branch: str, expected: str, new: str) -> str:
return self.executor.advance_task_branch_CAS(self._ref(branch), expected, new)
@@ -152,7 +157,7 @@ class DetachedGitWorkspace:
def materialize_files(self, files: Mapping[str, bytes]) -> None:
dirty = self._dirty_paths()
expected_paths = set(files)
if dirty and dirty != expected_paths:
if dirty and not dirty.issubset(expected_paths):
raise ContinuationExecutionError("WORKTREE_DIRTY_SCOPE_MISMATCH")
for relative, content in files.items():
target = (self.path / relative).resolve()
@@ -258,6 +263,15 @@ class GiteaContinuationRemote:
raise ContinuationExecutionError("GITEA_GIT_READBACK_INVALID")
return oid
def push_new_exact(self, branch: str, new: str) -> None:
self._require_projected()
self.repository_identity()
if self.read_git_ref(branch) is not None:
raise ContinuationExecutionError("GITEA_TARGET_BRANCH_ALREADY_EXISTS")
_run(["git", "push", self.remote_name, f"{new}:refs/heads/{branch}"], cwd=self.workspace)
if self.read_git_ref(branch) != new:
raise ContinuationExecutionError("GITEA_CREATE_READBACK_MISMATCH")
def push_exact(self, branch: str, expected: str, new: str) -> None:
self._require_projected()
self.repository_identity()
@@ -329,6 +343,7 @@ class ContinuationPermissions:
@dataclass(frozen=True, slots=True)
class ContinuationRuntimeBinding:
request: WriterLeaseRequestV2
job_id: str
workspace_path: Path
lease_not_before: datetime
lease_expires_at: datetime
@@ -389,13 +404,20 @@ class ContinuationExecutor:
def observe_generation(self, branch: str, generation: int) -> None:
self._observed_generations[branch] = max(generation, self._observed_generations.get(branch, 0))
def _max_generation(self, branch: str) -> int:
def _max_generation(self, branch: str, *, exclude_job_id: str | None = None) -> int:
observed = self._observed_generations.get(branch, 0)
with self.controller.store._connect() as connection:
row = connection.execute(
"SELECT MAX(generation) AS generation FROM branch_leases WHERE branch=?",
(branch,),
).fetchone()
if exclude_job_id is None:
row = connection.execute(
"SELECT MAX(generation) AS generation FROM branch_leases WHERE branch=?",
(branch,),
).fetchone()
else:
row = connection.execute(
"SELECT MAX(generation) AS generation FROM branch_leases "
"WHERE branch=? AND job_id!=?",
(branch, exclude_job_id),
).fetchone()
durable = int(row["generation"] or 0)
return max(observed, durable)
@@ -404,6 +426,7 @@ class ContinuationExecutor:
return {
"schema": WriterLeaseRequestV2.schema,
"task_id": request.task_id,
"job_id": request.job_id,
"repository": request.repository,
"branch_mode": request.branch_mode,
"base_oid": request.base_oid,
@@ -413,10 +436,21 @@ class ContinuationExecutor:
"lease_seconds": request.lease_seconds,
}
def _validate_preflight(self, binding: ContinuationRuntimeBinding) -> None:
def _validate_preflight(self, binding: ContinuationRuntimeBinding, *, job_id: str | None = None) -> None:
request = binding.request
normalized = validate_writer_lease_request_v2(self._request_mapping(request), self.authority)
if normalized != {**self._request_mapping(request), "create_target_branch": False, "worktree_mode": "detached_at_expected_head", "cas_ref_update_required": True}:
request_mapping = self._request_mapping(request)
normalized = validate_writer_lease_request_v2(request_mapping, self.authority)
expected_normalized = {
**request_mapping,
"create_target_branch": request.branch_mode == "CREATE_NEW",
"worktree_mode": (
"detached_at_base_oid"
if request.branch_mode == "CREATE_NEW"
else "detached_at_expected_head"
),
"cas_ref_update_required": True,
}
if normalized != expected_normalized:
raise ContinuationExecutionError("CONTINUATION_NORMALIZATION_INVALID")
if not binding.original_receipt_valid or not binding.original_grant_consumed:
raise ContinuationExecutionError("ORIGINAL_GRANT_LINEAGE_INVALID")
@@ -426,7 +460,7 @@ class ContinuationExecutor:
raise ContinuationExecutionError("LEASE_NOT_ACTIVE")
if now >= binding.lease_expires_at:
raise ContinuationExecutionError("LEASE_EXPIRED")
if request.generation <= self._max_generation(request.target_branch):
if request.generation <= self._max_generation(request.target_branch, exclude_job_id=job_id):
raise ContinuationExecutionError("GENERATION_NOT_MONOTONIC")
if not self.workspace.is_detached():
raise ContinuationExecutionError("WORKTREE_NOT_DETACHED")
@@ -452,57 +486,127 @@ class ContinuationExecutor:
"generation": request.generation,
}
def execute(self, binding: ContinuationRuntimeBinding, commit: ContinuationCommit) -> dict[str, object]:
payload = commit.payload()
submitted = self.controller.submit(self._job_request(binding, commit, payload), payload=payload)
current = self.controller.store.read(submitted.job_id)
if JobState(current["state"]) == JobState.COMPLETED:
return self.controller.store._receipt(current, duplicate=True).as_dict()
self._validate_preflight(binding)
def execute(
self,
binding: ContinuationRuntimeBinding,
commit: ContinuationCommit,
*,
before_complete: Callable[[str], None] | None = None,
) -> dict[str, object]:
commit.payload()
if not binding.job_id or binding.request.job_id != binding.job_id:
raise ContinuationExecutionError("RUNTIME_LINEAGE_MISSING_JOB_ID")
current = self.controller.store.read(binding.job_id)
current_state = JobState(current["state"])
if current_state == JobState.COMPLETED:
cycle = self.controller.begin_continuation_cycle(
binding.job_id,
idempotency_key=commit.idempotency_key,
expected_head_oid=binding.request.expected_branch_oid,
generation=binding.request.generation,
expected_revision=int(current["revision"]),
)
if cycle.get("completed"):
return self.controller.store._receipt(
self.controller.store.read(binding.job_id), duplicate=True
).as_dict()
current = self.controller.store.read(binding.job_id)
current_state = JobState(current["state"])
if current_state == JobState.REMOTE_VERIFIED:
receipt = json.loads(str(current["receipt_json"] or "{}"))
commit_oid = str(receipt.get("authority_oid") or "")
if not OID_RE.fullmatch(commit_oid):
raise ContinuationExecutionError("GOVERNED_COMMIT_OID_INVALID")
if before_complete is not None:
before_complete(commit_oid)
result = self.controller.advance(
binding.job_id,
JobState.COMPLETED,
side_effect_state="REMOTE_VERIFIED",
receipt=receipt,
).as_dict()
self.controller.complete_continuation_cycle(
binding.job_id,
idempotency_key=commit.idempotency_key,
commit_oid=commit_oid,
)
return result
self._validate_preflight(binding, job_id=binding.job_id)
request = binding.request
projected = False
try:
self.credential.project()
projected = True
row = self.controller.store.read(submitted.job_id)
if JobState(row["state"]) == JobState.REQUESTED:
row = self.controller.store.read(binding.job_id)
state = JobState(row["state"])
if state == JobState.REQUESTED:
self.controller.advance(
submitted.job_id,
binding.job_id,
JobState.AUTHORIZED,
side_effect_state="AUTHORIZED",
receipt={"task_id": request.task_id, "original_grant_consumed": True},
expected_revision=int(row["revision"]),
)
row = self.controller.store.read(submitted.job_id)
if JobState(row["state"]) == JobState.AUTHORIZED:
row = self.controller.store.read(binding.job_id)
state = JobState(row["state"])
if state == JobState.AUTHORIZED:
self.controller.lease_job(
submitted.job_id,
lease_id=f"lease-{submitted.job_id}",
binding.job_id,
lease_id=f"lease-{binding.job_id}",
lineage_anchor_oid=request.base_oid,
current_head_oid=request.expected_branch_oid,
generation=request.generation,
lease_expires_at=binding.lease_expires_at.astimezone(timezone.utc).isoformat(),
expected_revision=int(row["revision"]),
)
self.controller.advance(submitted.job_id, JobState.WORKTREE_READY, side_effect_state="WORKTREE_READY")
self.controller.advance(submitted.job_id, JobState.EXECUTING, side_effect_state="IN_PROGRESS")
row = self.controller.store.read(binding.job_id)
state = JobState(row["state"])
if state == JobState.LEASED:
self.controller.advance(binding.job_id, JobState.WORKTREE_READY,
side_effect_state="WORKTREE_READY", expected_revision=int(row["revision"]))
row = self.controller.store.read(binding.job_id)
state = JobState(row["state"])
if state == JobState.WORKTREE_READY:
self.controller.advance(binding.job_id, JobState.EXECUTING,
side_effect_state="IN_PROGRESS", expected_revision=int(row["revision"]))
elif state != JobState.EXECUTING:
raise ContinuationExecutionError("JOB_EXECUTION_STATE_INVALID")
new_oid = self.workspace.commit_files(commit.files, commit.message)
if not OID_RE.fullmatch(new_oid) or not self.authority.is_ancestor(request.expected_branch_oid, new_oid):
raise ContinuationExecutionError("NON_FAST_FORWARD_COMMIT")
authority_oid = self.authority.compare_and_swap_branch(request.target_branch, request.expected_branch_oid, new_oid)
if request.branch_mode == "CREATE_NEW":
authority_oid = self.authority.create_task_branch_cas(request.target_branch, request.base_oid, new_oid)
else:
authority_oid = self.authority.compare_and_swap_branch(request.target_branch, request.expected_branch_oid, new_oid)
if authority_oid != new_oid:
raise ContinuationExecutionError("AUTHORITY_CAS_READBACK_MISMATCH")
self.controller.advance(
submitted.job_id,
binding.job_id,
JobState.LOCAL_VERIFIED,
side_effect_state="LOCAL_VERIFIED",
receipt={"authority_oid": authority_oid, "force_push": False},
)
try:
self.controller.advance(submitted.job_id, JobState.REMOTE_SYNCING, side_effect_state="IN_PROGRESS")
self.remote.push_exact(request.target_branch, request.expected_branch_oid, new_oid)
self.controller.advance(binding.job_id, JobState.REMOTE_SYNCING, side_effect_state="IN_PROGRESS")
if request.branch_mode == "CREATE_NEW":
self.remote.push_new_exact(request.target_branch, new_oid)
else:
self.remote.push_exact(request.target_branch, request.expected_branch_oid, new_oid)
except Exception:
return self._remote_pending(submitted.job_id, authority_oid, new_oid)
return self._finish_remote_verification(submitted.job_id, authority_oid, new_oid)
return self._remote_pending(binding.job_id, authority_oid, new_oid)
result = self._finish_remote_verification(
binding.job_id,
authority_oid,
new_oid,
before_complete=before_complete,
)
if str(result.get("state") or "") == JobState.COMPLETED.value:
self.controller.complete_continuation_cycle(
binding.job_id,
idempotency_key=commit.idempotency_key,
commit_oid=new_oid,
)
return result
finally:
if projected:
self.credential.revoke()
@@ -525,7 +629,14 @@ class ContinuationExecutor:
)
return result.as_dict()
def _finish_remote_verification(self, job_id: str, authority_oid: str, new_oid: str) -> dict[str, object]:
def _finish_remote_verification(
self,
job_id: str,
authority_oid: str,
new_oid: str,
*,
before_complete: Callable[[str], None] | None = None,
) -> dict[str, object]:
row = self.controller.store.read(job_id)
branch = row["branch"]
request = json.loads(row["request_json"])["binding"]
@@ -542,8 +653,20 @@ class ContinuationExecutor:
"reconciliation_decision": decision.action.value,
"force_push": False,
}
self.controller.advance(job_id, JobState.REMOTE_VERIFIED, side_effect_state="REMOTE_VERIFIED", receipt=receipt)
return self.controller.advance(job_id, JobState.COMPLETED, side_effect_state="REMOTE_VERIFIED", receipt=receipt).as_dict()
self.controller.advance(
job_id,
JobState.REMOTE_VERIFIED,
side_effect_state="REMOTE_VERIFIED",
receipt=receipt,
)
if before_complete is not None:
before_complete(new_oid)
return self.controller.advance(
job_id,
JobState.COMPLETED,
side_effect_state="REMOTE_VERIFIED",
receipt=receipt,
).as_dict()
def reconcile(self, job_id: str) -> dict[str, object]:
row = self.controller.store.read(job_id)
@@ -49,6 +49,7 @@ class WriterLeaseRequestV2:
target_branch: str
generation: int
lease_seconds: int = 900
job_id: str = ""
schema = "aios.task-writer-lease.v2"
accepted_schemas = frozenset({"aios.task-writer-lease.v2", "aios.task-writer-lease-request.v2"})
@@ -56,6 +57,8 @@ class WriterLeaseRequestV2:
def validate(self, probe: BranchProbe) -> None:
if not self.task_id or self.repository != "aios/aios":
raise ContinuationError("request_identity_invalid")
if not self.job_id or not self.job_id.startswith("job-"):
raise ContinuationError("job_id_invalid")
if self.branch_mode not in {"CREATE_NEW", "CONTINUE_EXISTING"}:
raise ContinuationError("branch_mode_invalid")
_oid(self.base_oid, "base_oid")
File diff suppressed because it is too large Load Diff
@@ -27,6 +27,19 @@ class ServiceUnavailable(JobError):
pass
@dataclass(frozen=True, slots=True)
class LeaseRenewalResult:
"""Durable outcome of one writer-lease recovery attempt."""
renewed: bool
idempotent: bool
durable: bool
lease_expires_at: str | None
def __bool__(self) -> bool:
return self.renewed or self.idempotent
class BindingConflict(JobError):
pass
@@ -66,6 +79,14 @@ def utc_now() -> str:
return datetime.now(timezone.utc).isoformat().replace("+00:00", "Z")
def parse_utc(value: str) -> datetime:
normalized = value[:-1] + "+00:00" if value.endswith("Z") else value
parsed = datetime.fromisoformat(normalized)
if parsed.tzinfo is None:
raise ValueError("LEASE_EXPIRY_INVALID")
return parsed.astimezone(timezone.utc)
def canonical_json(value: Any) -> bytes:
return (
json.dumps(value, sort_keys=True, separators=(",", ":"), ensure_ascii=False) + "\n"
@@ -209,7 +230,15 @@ class JobStore:
kind TEXT NOT NULL, digest TEXT NOT NULL, payload_json TEXT NOT NULL,
created_at TEXT NOT NULL, FOREIGN KEY(job_id) REFERENCES jobs(job_id)
);
INSERT OR REPLACE INTO metadata(key, value) VALUES('schema_version', '1');
CREATE TABLE IF NOT EXISTS continuation_cycles (
job_id TEXT NOT NULL, idempotency_key TEXT NOT NULL,
sequence INTEGER NOT NULL, expected_head_oid TEXT NOT NULL,
generation INTEGER NOT NULL, state TEXT NOT NULL,
commit_oid TEXT, created_at TEXT NOT NULL, updated_at TEXT NOT NULL,
PRIMARY KEY(job_id,idempotency_key), UNIQUE(job_id,sequence),
FOREIGN KEY(job_id) REFERENCES jobs(job_id)
);
INSERT OR REPLACE INTO metadata(key, value) VALUES('schema_version', '2');
"""
)
except sqlite3.Error as exc:
@@ -404,13 +433,16 @@ class GitOpsJobController:
self.store._mirror(result)
return self.store._receipt(result, duplicate=True)
def update_lease(self, lease_id: str, *, expected_revision: int, current_head_oid: str, generation: int, state: str = "ACTIVE") -> None:
def update_lease(self, lease_id: str, *, expected_revision: int, current_head_oid: str, generation: int, state: str = "ACTIVE", lease_expires_at: str | None = None) -> None:
if len(current_head_oid) != 40 or generation < 1:
raise JobError("LEASE_BINDING_INVALID")
try:
with self.store._connect() as connection:
connection.execute("BEGIN IMMEDIATE")
updated = connection.execute("UPDATE branch_leases SET current_head_oid=?,generation=?,state=?,revision=revision+1 WHERE lease_id=? AND revision=?", (current_head_oid, generation, state, lease_id, expected_revision))
if lease_expires_at is None:
updated = connection.execute("UPDATE branch_leases SET current_head_oid=?,generation=?,state=?,revision=revision+1 WHERE lease_id=? AND revision=?", (current_head_oid, generation, state, lease_id, expected_revision))
else:
updated = connection.execute("UPDATE branch_leases SET current_head_oid=?,generation=?,lease_expires_at=?,state=?,revision=revision+1 WHERE lease_id=? AND revision=?", (current_head_oid, generation, lease_expires_at, state, lease_id, expected_revision))
if updated.rowcount != 1:
raise JobError("LEASE_REVISION_CAS_CONFLICT")
connection.commit()
@@ -419,6 +451,271 @@ class GitOpsJobController:
except sqlite3.Error as exc:
raise ServiceUnavailable("JOB_DB_UNAVAILABLE") from exc
def renew_lease(
self,
*,
branch: str,
lineage_anchor_oid: str,
current_head_oid: str,
generation: int,
lease_expires_at: str,
) -> LeaseRenewalResult:
if (
not branch
or len(lineage_anchor_oid) != 40
or len(current_head_oid) != 40
or generation < 1
or not lease_expires_at.strip()
):
raise JobError("LEASE_BINDING_INVALID")
try:
with self.store._connect() as connection:
connection.execute("BEGIN IMMEDIATE")
rows = connection.execute(
"SELECT lease_id, revision, generation, state, lease_expires_at "
"FROM branch_leases "
"WHERE branch=? AND lineage_anchor_oid=? AND current_head_oid=? "
"AND generation=? AND state='ACTIVE' ORDER BY revision DESC",
(branch, lineage_anchor_oid, current_head_oid, generation),
).fetchall()
if not rows:
connection.commit()
return LeaseRenewalResult(False, False, False, None)
if len(rows) != 1:
raise JobError("LEASE_BINDING_CONFLICT")
row = rows[0]
try:
current_expires_at = parse_utc(str(row["lease_expires_at"]))
requested_expires_at = parse_utc(lease_expires_at)
except (TypeError, ValueError) as exc:
raise JobError("LEASE_EXPIRY_INVALID") from exc
now = datetime.now(timezone.utc)
if requested_expires_at <= now:
raise JobError("LEASE_EXPIRY_INVALID")
if current_expires_at > now:
connection.commit()
return LeaseRenewalResult(
False,
True,
True,
str(row["lease_expires_at"]),
)
updated = connection.execute(
"UPDATE branch_leases SET lease_expires_at=?,revision=revision+1 "
"WHERE lease_id=? AND revision=? AND generation=? "
"AND state='ACTIVE' AND lease_expires_at=?",
(
lease_expires_at,
row["lease_id"],
int(row["revision"]),
generation,
row["lease_expires_at"],
),
)
if updated.rowcount != 1:
current = connection.execute(
"SELECT lease_expires_at, generation, state FROM branch_leases "
"WHERE lease_id=?",
(row["lease_id"],),
).fetchone()
if (
current is not None
and int(current["generation"]) == generation
and current["state"] == "ACTIVE"
and parse_utc(str(current["lease_expires_at"])) > now
):
connection.commit()
return LeaseRenewalResult(
False,
True,
True,
str(current["lease_expires_at"]),
)
raise JobError("LEASE_REVISION_CAS_CONFLICT")
connection.commit()
return LeaseRenewalResult(True, False, True, lease_expires_at)
except JobError:
raise
except sqlite3.Error as exc:
raise ServiceUnavailable("JOB_DB_UNAVAILABLE") from exc
def begin_continuation_cycle(
self,
job_id: str,
*,
idempotency_key: str,
expected_head_oid: str,
generation: int,
expected_revision: int,
) -> dict[str, Any]:
if (
not idempotency_key
or len(idempotency_key) > 128
or not all(character.isalnum() or character in "._:-" for character in idempotency_key)
or len(expected_head_oid) != 40
or any(character not in "0123456789abcdef" for character in expected_head_oid)
or generation < 1
):
raise JobError("CONTINUATION_CYCLE_BINDING_INVALID")
try:
with self.store._connect() as connection:
connection.execute("BEGIN IMMEDIATE")
row = connection.execute("SELECT * FROM jobs WHERE job_id=?", (job_id,)).fetchone()
if row is None:
raise JobError("JOB_NOT_FOUND")
existing = connection.execute(
"SELECT * FROM continuation_cycles WHERE job_id=? AND idempotency_key=?",
(job_id, idempotency_key),
).fetchone()
if existing is not None:
if str(existing["expected_head_oid"]) != expected_head_oid or int(existing["generation"]) != generation:
raise JobError("CONTINUATION_CYCLE_BINDING_CONFLICT")
if str(existing["state"]) == "COMPLETED":
connection.commit()
return {"completed": True, "commit_oid": str(existing["commit_oid"] or ""), "idempotent": True}
if str(row["state"]) == JobState.COMPLETED.value:
receipt = json.loads(str(row["receipt_json"] or "{}"))
commit_oid = str(receipt.get("authority_oid") or "")
lease = connection.execute(
"SELECT current_head_oid FROM branch_leases WHERE job_id=? AND generation=? AND state='ACTIVE'",
(job_id, generation),
).fetchall()
if (
len(lease) == 1
and len(commit_oid) == 40
and str(lease[0]["current_head_oid"] or "") == commit_oid
and receipt.get("remote_verified") is True
):
now = utc_now()
connection.execute(
"UPDATE continuation_cycles SET state='COMPLETED',commit_oid=?,updated_at=? "
"WHERE job_id=? AND idempotency_key=? AND state='ACTIVE'",
(commit_oid, now, job_id, idempotency_key),
)
connection.commit()
return {"completed": True, "commit_oid": commit_oid, "idempotent": True}
raise JobError("CONTINUATION_CYCLE_RECOVERY_REQUIRED")
connection.commit()
return {"completed": False, "commit_oid": None, "idempotent": True}
if int(row["revision"]) != expected_revision:
raise JobError("REVISION_CAS_CONFLICT")
if str(row["state"]) != JobState.COMPLETED.value:
raise JobError("CONTINUATION_CYCLE_JOB_STATE_INVALID")
if str(row["branch"]) in {"main", "master"} or str(row["branch"]).startswith(("refs/", "tags/")):
raise JobError("PROTECTED_BRANCH_REJECTED")
receipt = json.loads(str(row["receipt_json"] or "{}"))
if receipt.get("remote_verified") is not True or any(
str(receipt.get(field) or "") != expected_head_oid
for field in ("authority_oid", "api_branch_oid", "git_remote_oid")
):
raise JobError("CONTINUATION_CYCLE_RECEIPT_INVALID")
leases = connection.execute(
"SELECT * FROM branch_leases WHERE job_id=? AND branch=? AND generation=? AND state='ACTIVE'",
(job_id, str(row["branch"]), generation),
).fetchall()
if len(leases) != 1 or str(leases[0]["current_head_oid"] or "") != expected_head_oid:
raise JobError("CONTINUATION_CYCLE_LEASE_INVALID")
if connection.execute(
"SELECT 1 FROM continuation_cycles WHERE job_id=? AND state='ACTIVE' LIMIT 1",
(job_id,),
).fetchone() is not None:
raise JobError("CONTINUATION_CYCLE_ACTIVE_CONFLICT")
sequence = int(connection.execute(
"SELECT COALESCE(MAX(sequence),0)+1 FROM continuation_cycles WHERE job_id=?",
(job_id,),
).fetchone()[0])
now = utc_now()
connection.execute(
"INSERT INTO continuation_cycles(job_id,idempotency_key,sequence,expected_head_oid,generation,state,commit_oid,created_at,updated_at) "
"VALUES(?,?,?,?,?,'ACTIVE',NULL,?,?)",
(job_id, idempotency_key, sequence, expected_head_oid, generation, now, now),
)
next_revision = expected_revision + 1
updated = connection.execute(
"UPDATE jobs SET state=?,side_effect_state=?,revision=?,updated_at=? WHERE job_id=? AND revision=?",
(JobState.LEASED.value, "CONTINUATION_LEASED", next_revision, now, job_id, expected_revision),
)
if updated.rowcount != 1:
raise JobError("REVISION_CAS_CONFLICT")
connection.execute(
"INSERT INTO state_transitions(job_id,from_state,to_state,revision,side_effect_state,created_at) VALUES(?,?,?,?,?,?)",
(job_id, JobState.COMPLETED.value, JobState.LEASED.value, next_revision, "CONTINUATION_LEASED", now),
)
cycle_receipt = {
"idempotency_key": idempotency_key,
"expected_head_oid": expected_head_oid,
"generation": generation,
"sequence": sequence,
}
connection.execute(
"INSERT INTO receipts(receipt_id,job_id,kind,digest,payload_json,created_at) VALUES(?,?,?,?,?,?)",
(
f"receipt-{job_id}-{next_revision}",
job_id,
"CONTINUATION_LEASED",
hashlib.sha256(canonical_json(cycle_receipt)).hexdigest(),
json.dumps(cycle_receipt, sort_keys=True),
now,
),
)
result = dict(connection.execute("SELECT * FROM jobs WHERE job_id=?", (job_id,)).fetchone())
connection.commit()
except (JobError, BindingConflict):
raise
except sqlite3.Error as exc:
raise ServiceUnavailable("JOB_DB_UNAVAILABLE") from exc
result["request"] = json.loads(result["request_json"])
self.store._mirror(result)
return {"completed": False, "commit_oid": None, "idempotent": False}
def complete_continuation_cycle(self, job_id: str, *, idempotency_key: str, commit_oid: str) -> bool:
if len(commit_oid) != 40 or any(character not in "0123456789abcdef" for character in commit_oid):
raise JobError("CONTINUATION_CYCLE_COMMIT_OID_INVALID")
try:
with self.store._connect() as connection:
connection.execute("BEGIN IMMEDIATE")
cycle = connection.execute(
"SELECT * FROM continuation_cycles WHERE job_id=? AND idempotency_key=?",
(job_id, idempotency_key),
).fetchone()
if cycle is None:
connection.commit()
return False
if str(cycle["state"]) == "COMPLETED":
if str(cycle["commit_oid"] or "") != commit_oid:
raise JobError("CONTINUATION_CYCLE_COMMIT_CONFLICT")
connection.commit()
return True
row = connection.execute("SELECT * FROM jobs WHERE job_id=?", (job_id,)).fetchone()
leases = connection.execute(
"SELECT current_head_oid FROM branch_leases WHERE job_id=? AND generation=? AND state='ACTIVE'",
(job_id, int(cycle["generation"])),
).fetchall()
receipt = json.loads(str(row["receipt_json"] or "{}")) if row is not None else {}
if (
row is None
or str(row["state"]) != JobState.COMPLETED.value
or receipt.get("remote_verified") is not True
or str(receipt.get("authority_oid") or "") != commit_oid
or len(leases) != 1
or str(leases[0]["current_head_oid"] or "") != commit_oid
):
raise JobError("CONTINUATION_CYCLE_COMPLETION_EVIDENCE_INVALID")
now = utc_now()
updated = connection.execute(
"UPDATE continuation_cycles SET state='COMPLETED',commit_oid=?,updated_at=? "
"WHERE job_id=? AND idempotency_key=? AND state='ACTIVE'",
(commit_oid, now, job_id, idempotency_key),
)
if updated.rowcount != 1:
raise JobError("CONTINUATION_CYCLE_CAS_CONFLICT")
connection.commit()
return True
except (JobError, BindingConflict):
raise
except sqlite3.Error as exc:
raise ServiceUnavailable("JOB_DB_UNAVAILABLE") from exc
def advance(self, job_id: str, state: JobState, *, side_effect_state: str, receipt: Mapping[str, Any] | None = None, expected_revision: int | None = None) -> JobReceipt:
return self._advance(
job_id,
@@ -2,6 +2,8 @@
from __future__ import annotations
from dataclasses import dataclass, asdict
from contextlib import contextmanager
import fcntl
import hashlib
import json
from pathlib import Path
@@ -9,6 +11,11 @@ import re
from typing import Any, Mapping
from aios_gitops_job_controller import GitOpsJobController, JobStore
from aios_online_agent_execution import (
GovernedExecutionBinding,
project_execution_envelope,
project_online_task,
)
OID_RE = re.compile(r"^[0-9a-f]{40}$")
BRANCH_RE = re.compile(r"^agent/[A-Za-z0-9][A-Za-z0-9._/-]{2,179}$")
@@ -36,35 +43,161 @@ class GovernedTaskBinding:
return {"task_id": self.task_id, "repository": self.repository, "base_oid": self.base_oid, "target_branch": self.target_branch, "expected_branch_oid": self.expected_branch_oid, "generation": self.generation, "branch_mode": self.branch_mode, "create_target_branch": self.branch_mode == "CREATE_NEW", "worktree_mode": "detached_at_base_oid" if self.branch_mode == "CREATE_NEW" else "detached_at_expected_head", "cas_ref_update_required": True, "idempotent": idempotent, "state": "BOUND", "job_id": self.job_id}
def _safe(value: str) -> str:
def _legacy_safe(value: str) -> str:
return re.sub(r"[^A-Za-z0-9_.-]+", "-", value).strip("-")[:120] or "task"
def _task_key(value: str) -> str:
readable = _legacy_safe(value)[:72]
return f"{readable}-{hashlib.sha256(value.encode('utf-8')).hexdigest()[:24]}"
class GovernedTaskBootstrap:
def __init__(self, authority: Any, root: Path, *, remote_name: str, token_path: Path, ssh_key_path: Path, api_base: str = "https://g.cnnetmkt.top/git/api/v1") -> None:
self.authority = authority; self.root = Path(root); self.remote_name = remote_name; self.token_path = Path(token_path); self.ssh_key_path = Path(ssh_key_path); self.api_base = api_base; self.task_meta_root = self.root / "task-bindings"; self.job_root = self.root / "jobs"; self.task_meta_root.mkdir(parents=True, exist_ok=True); self.job_root.mkdir(parents=True, exist_ok=True)
self.authority = authority; self.root = Path(root); self.remote_name = remote_name; self.token_path = Path(token_path); self.ssh_key_path = Path(ssh_key_path); self.api_base = api_base; self.task_meta_root = self.root / "task-bindings"; self.job_root = self.root / "jobs"; self.lock_root = self.root / "task-locks"; self.task_meta_root.mkdir(parents=True, exist_ok=True); self.job_root.mkdir(parents=True, exist_ok=True); self.lock_root.mkdir(parents=True, exist_ok=True)
def _store(self, task_id: str) -> tuple[JobStore, GitOpsJobController]:
root = self.job_root / _safe(task_id); store = JobStore(root, payload_root=root / "payloads"); return store, GitOpsJobController(store)
legacy = self.job_root / _legacy_safe(task_id)
keyed = self.job_root / _task_key(task_id)
root = legacy if legacy.is_dir() and not keyed.exists() else keyed
store = JobStore(root, payload_root=root / "payloads"); return store, GitOpsJobController(store)
def _metadata(self, task_id: str) -> Path:
return self.task_meta_root / (_safe(task_id) + ".json")
return self.task_meta_root / (_task_key(task_id) + ".json")
def _legacy_metadata(self, task_id: str) -> Path:
return self.task_meta_root / (_legacy_safe(task_id) + ".json")
@contextmanager
def _task_lock(self, task_id: str):
path = self.lock_root / (_task_key(task_id) + ".lock")
with path.open("a+b") as handle:
fcntl.flock(handle.fileno(), fcntl.LOCK_EX)
try:
yield
finally:
fcntl.flock(handle.fileno(), fcntl.LOCK_UN)
@contextmanager
def _branch_lock(self, repository: str, target_branch: str):
key = hashlib.sha256((repository + "\0" + target_branch).encode("utf-8")).hexdigest()
path = self.lock_root / ("branch-" + key + ".lock")
with path.open("a+b") as handle:
fcntl.flock(handle.fileno(), fcntl.LOCK_EX)
try:
yield
finally:
fcntl.flock(handle.fileno(), fcntl.LOCK_UN)
def _read_existing(self, task_id: str) -> GovernedTaskBinding | None:
path = self._metadata(task_id)
if not path.exists():
path = self._legacy_metadata(task_id)
if not path.is_file() or path.is_symlink(): return None
return GovernedTaskBinding(**json.loads(path.read_text(encoding="utf-8")))
binding = GovernedTaskBinding(**json.loads(path.read_text(encoding="utf-8")))
if binding.task_id != task_id:
raise BootstrapError("TASK_BINDING_KEY_COLLISION")
return binding
def get_binding(self, task_id: str) -> GovernedTaskBinding:
"""Return the exact durable binding; never synthesize missing lineage."""
existing = self._read_existing(str(task_id))
if existing is None or not existing.job_id:
raise BootstrapError("TASK_EXECUTION_PROJECTION_MISSING")
return existing
def execution_binding(self, binding: GovernedTaskBinding) -> GovernedExecutionBinding:
expected = binding.expected_branch_oid
mode = binding.branch_mode
if mode == "CREATE_NEW":
current = self.authority.read_ref("refs/heads/" + binding.target_branch)
if current is not None:
store, _ = self._store(binding.task_id)
with store._connect() as connection:
rows = connection.execute(
"SELECT current_head_oid,generation,state,lineage_anchor_oid "
"FROM branch_leases WHERE job_id=? AND branch=? AND state='ACTIVE' "
"ORDER BY revision DESC",
(binding.job_id, binding.target_branch),
).fetchall()
if len(rows) != 1:
raise BootstrapError("SAME_JOB_CONTINUATION_LEASE_INVALID")
row = rows[0]
if (
str(row["current_head_oid"] or "") != current
or int(row["generation"] or 0) != binding.generation
or str(row["lineage_anchor_oid"] or "") != binding.base_oid
):
raise BootstrapError("SAME_JOB_CONTINUATION_BINDING_INVALID")
expected = current
mode = "CONTINUE_EXISTING"
return GovernedExecutionBinding.from_mapping(
{
"task_id": binding.task_id,
"job_id": binding.job_id,
"repository": binding.repository,
"base_oid": binding.base_oid,
"expected_branch_oid": expected,
"target_branch": binding.target_branch,
"branch_mode": mode,
"generation": binding.generation,
"idempotency_key": binding.idempotency_key,
}
)
def execution_envelope(
self,
task_id: str,
execution_request: Mapping[str, Any] | None = None,
*,
required_capabilities: tuple[str, ...] = (),
expected_job_id: str | None = None,
) -> dict[str, Any]:
binding = self.get_binding(task_id)
if expected_job_id and binding.job_id != expected_job_id:
raise BootstrapError("JOB_BINDING_CONFLICT")
return project_execution_envelope(
self.execution_binding(binding),
execution_request,
required_capabilities=required_capabilities,
)
def online_task_projection(
self,
task_id: str,
task_request: Mapping[str, Any],
execution_request: Mapping[str, Any] | None = None,
*,
expected_job_id: str | None = None,
) -> dict[str, Any]:
binding = self.get_binding(task_id)
if expected_job_id and binding.job_id != expected_job_id:
raise BootstrapError("JOB_BINDING_CONFLICT")
return project_online_task(self.execution_binding(binding), task_request, execution_request)
def _write(self, binding: GovernedTaskBinding) -> None:
path = self._metadata(binding.task_id); temporary = path.with_suffix(".tmp"); temporary.write_text(json.dumps(asdict(binding), sort_keys=True), encoding="utf-8"); temporary.replace(path)
def create(self, request: Mapping[str, Any]) -> dict[str, Any]:
task_id = str(request.get("task_id") or "")
if not task_id:
raise BootstrapError("TASK_BINDING_INVALID")
repository = str(request.get("repository") or "")
target_branch = str(request.get("target_branch") or "")
with self._task_lock(task_id):
with self._branch_lock(repository, target_branch):
return self._create_locked(request)
def _create_locked(self, request: Mapping[str, Any]) -> dict[str, Any]:
values = {key: str(request[key]) for key in ("task_id", "repository", "base_oid", "target_branch", "risk", "description", "idempotency_key")}
if values["repository"] != "aios/aios" or not OID_RE.fullmatch(values["base_oid"]): raise BootstrapError("TASK_BINDING_INVALID")
if not BRANCH_RE.fullmatch(values["target_branch"]): raise BootstrapError("TARGET_BRANCH_INVALID")
existing = self._read_existing(values["task_id"])
if existing is not None:
if any(getattr(existing, key) != values[key] for key in ("repository", "base_oid", "target_branch", "risk", "description")): raise BootstrapError("TASK_BINDING_CONFLICT")
if any(getattr(existing, key) != values[key] for key in ("repository", "base_oid", "target_branch", "risk", "description", "idempotency_key")): raise BootstrapError("TASK_BINDING_CONFLICT")
optional = {"expected_branch_oid": existing.expected_branch_oid, "branch_mode": existing.branch_mode, "generation": existing.generation}
for key, durable in optional.items():
if key in request and str(request[key]) != str(durable): raise BootstrapError("TASK_BINDING_CONFLICT")
return existing.response(idempotent=True)
if not self.authority.commit_exists(values["base_oid"]): raise BootstrapError("BASE_COMMIT_NOT_FOUND")
current = self.authority.read_ref("refs/heads/" + values["target_branch"])
@@ -74,8 +207,24 @@ class GovernedTaskBootstrap:
if expected != values["base_oid"]: raise BootstrapError("EXPECTED_BASE_CONFLICT")
else:
if expected != current: raise BootstrapError("EXISTING_BRANCH_OID_CONFLICT")
mode, generation = "CONTINUE_EXISTING", int(request.get("generation", 2))
if generation < 1: raise BootstrapError("GENERATION_INVALID")
parent_task_id = str(request.get("parent_task_id") or "")
parent_job_id = str(request.get("parent_job_id") or "")
if not parent_task_id or not parent_job_id:
raise BootstrapError("EXISTING_BRANCH_OWNERSHIP_REQUIRED")
parent = self._read_existing(parent_task_id)
if parent is None or parent.job_id != parent_job_id or parent.repository != values["repository"] or parent.target_branch != values["target_branch"]:
raise BootstrapError("EXISTING_BRANCH_OWNERSHIP_CONFLICT")
store, _ = self._store(parent_task_id)
with store._connect() as connection:
rows = connection.execute(
"SELECT current_head_oid,generation,state FROM branch_leases WHERE job_id=? AND branch=? AND state='ACTIVE' ORDER BY revision DESC",
(parent_job_id, values["target_branch"]),
).fetchall()
if len(rows) != 1 or str(rows[0]["current_head_oid"] or "") != current:
raise BootstrapError("EXISTING_BRANCH_OWNERSHIP_CONFLICT")
mode, generation = "CONTINUE_EXISTING", int(rows[0]["generation"]) + 1
if "generation" in request and int(request["generation"]) != generation:
raise BootstrapError("GENERATION_BINDING_CONFLICT")
payload = json.dumps(values, sort_keys=True, separators=(",", ":")).encode()
_, controller = self._store(values["task_id"])
receipt = controller.submit({"idempotency_key": values["idempotency_key"], "campaign_id": values["task_id"], "task_id": values["task_id"], "repository": values["repository"], "operation": "GOVERNED_TASK_CREATE", "branch": values["target_branch"], "expected_branch_oid": expected, "payload_digest": hashlib.sha256(payload).hexdigest(), "lineage_anchor_oid": values["base_oid"]}, payload=payload)
@@ -0,0 +1,306 @@
"""Uncredentialed deterministic executor for governed Online Agent changes.
This process never receives Agent/Owner/Bridge credentials and never writes Git refs.
It operates only on an exchange bundle and returns hash-bound changed files + test evidence.
"""
from __future__ import annotations
import base64
from dataclasses import dataclass
import errno
import hashlib
import io
import json
import os
from pathlib import Path
import re
import shutil
import socket
import subprocess
import tarfile
import tempfile
import time
from typing import Any, Mapping, Sequence
ALLOWED_PROGRAMS = {"/usr/bin/python3", "/usr/bin/git", "/bin/true", "/usr/bin/true"}
MAX_ARCHIVE_BYTES = 64 * 1024 * 1024
MAX_FILE_BYTES = 4 * 1024 * 1024
SECRET_ENV_TOKENS = ("TOKEN", "SECRET", "PASSWORD", "CREDENTIAL", "AUTHORIZATION", "PRIVATE_KEY")
REQUEST_ID_RE = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._:-]{15,254}$")
SENSITIVE_VALUE_RE = re.compile(
r"(?i)(authorization\s*[:=]\s*bearer\s+\S+|authorization\s+bearer\s+\S+|"
r"(?:password|passwd|secret|token|api[_-]?key)\s*[:=]\s*\S+)"
)
class IsolatedExecutorError(RuntimeError):
pass
def sha256(data: bytes) -> str:
return hashlib.sha256(data).hexdigest()
def canonical(value: object) -> bytes:
return json.dumps(value, sort_keys=True, separators=(",", ":"), ensure_ascii=False).encode("utf-8")
def safe_relative(value: object) -> str:
if not isinstance(value, str) or not value or value.startswith("/"):
raise IsolatedExecutorError("PATH_INVALID")
path = Path(value)
if ".." in path.parts or "." in path.parts or any(part in {"", ".git"} for part in path.parts):
raise IsolatedExecutorError("PATH_INVALID")
return path.as_posix()
def resolve_under(root: Path, relative: str) -> Path:
root = root.resolve()
path = (root / safe_relative(relative)).resolve(strict=False)
if path == root or root not in path.parents:
raise IsolatedExecutorError("PATH_ESCAPE")
return path
def _credential_exposure(directory: Path) -> str | None:
"""Return an exposure code; EACCES is the intended credential isolation proof."""
try:
if not directory.exists():
return None
if directory.is_symlink():
return "CREDENTIAL_MOUNT_SYMLINK"
if os.access(directory, os.R_OK):
return "CREDENTIAL_MOUNT_ACCESSIBLE"
if directory.is_dir():
for child in directory.iterdir():
if child.is_file() and os.access(child, os.R_OK):
return "CREDENTIAL_FILE_ACCESSIBLE"
except PermissionError:
return None
return None
def assert_uncredentialed() -> None:
credentials = Path("/run/credentials")
candidates = {credentials}
credentials_directory = Path(os.environ.get("CREDENTIALS_DIRECTORY", "/run/credentials"))
candidates.add(credentials_directory)
leaked = sorted(
key for key, value in os.environ.items()
if value
and (any(token in key.upper() for token in SECRET_ENV_TOKENS) or SENSITIVE_VALUE_RE.search(str(value)))
)
if leaked:
raise IsolatedExecutorError("CREDENTIAL_ENV_EXPOSED:" + ",".join(leaked))
for directory in candidates:
exposure = _credential_exposure(directory)
if exposure is not None:
raise IsolatedExecutorError(exposure)
# In the intended PrivateNetwork namespace host loopback services are unreachable.
try:
probe = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
except OSError as exc:
if exc.errno in {errno.EACCES, errno.EPERM, errno.EAFNOSUPPORT}:
return
raise IsolatedExecutorError("NETWORK_ISOLATION_PROBE_FAILED") from exc
probe.settimeout(0.15)
try:
if probe.connect_ex(("127.0.0.1", 18892)) == 0:
raise IsolatedExecutorError("GOVERNED_BRIDGE_REACHABLE")
finally:
probe.close()
def extract_source(archive: Path, root: Path) -> None:
if archive.is_symlink() or not archive.is_file() or archive.stat().st_size > MAX_ARCHIVE_BYTES:
raise IsolatedExecutorError("SOURCE_ARCHIVE_INVALID")
with tarfile.open(archive, "r:gz") as tf:
for member in tf.getmembers():
path = Path(member.name)
if path.is_absolute() or ".." in path.parts or ".git" in path.parts:
raise IsolatedExecutorError("SOURCE_ARCHIVE_PATH_INVALID")
if member.issym() or member.islnk():
raise IsolatedExecutorError("SOURCE_ARCHIVE_LINK_DENY")
tf.extractall(root, filter="data")
def run_command(item: Mapping[str, Any], root: Path) -> dict[str, Any]:
argv = item.get("argv")
if not isinstance(argv, Sequence) or isinstance(argv, (str, bytes)) or not argv:
raise IsolatedExecutorError("COMMAND_INVALID")
argv = [str(part) for part in argv]
if argv[0] not in ALLOWED_PROGRAMS:
raise IsolatedExecutorError("COMMAND_PROGRAM_DENY")
if argv[0] == "/usr/bin/python3" and "-m" in argv:
index = argv.index("-m")
if index + 1 >= len(argv) or argv[index + 1] not in {"pytest", "py_compile"}:
raise IsolatedExecutorError("PYTHON_MODULE_DENY")
if argv[0] == "/usr/bin/git" and any(
token in {"push", "update-ref", "commit", "merge", "rebase", "reset", "clean", "branch"}
for token in argv[1:]
):
raise IsolatedExecutorError("GIT_MUTATION_DENY")
timeout = int(item.get("timeout_seconds") or 300)
if not 1 <= timeout <= 1800:
raise IsolatedExecutorError("COMMAND_TIMEOUT_INVALID")
cwd = root if item.get("cwd", ".") == "." else resolve_under(root, str(item.get("cwd")))
started = time.monotonic()
completed = subprocess.run(
argv,
cwd=cwd,
capture_output=True,
text=True,
check=False,
timeout=timeout,
env={"PATH": "/usr/bin:/bin", "LC_ALL": "C.UTF-8"},
)
result = {
"label": str(item.get("label") or "command"),
"exit_code": completed.returncode,
"stdout_sha256": sha256(completed.stdout.encode("utf-8", "replace")),
"stderr_sha256": sha256(completed.stderr.encode("utf-8", "replace")),
"duration_ms": int((time.monotonic() - started) * 1000),
}
if completed.returncode != 0:
raise IsolatedExecutorError(
"COMMAND_FAILED:" + result["label"] + ":" + str(completed.returncode)
)
return result
def apply_changes(request: Mapping[str, Any], root: Path) -> list[str]:
changes = request.get("change_set")
if not isinstance(changes, list) or not changes:
raise IsolatedExecutorError("CHANGE_SET_INVALID")
changed: list[str] = []
for change in changes:
if not isinstance(change, Mapping):
raise IsolatedExecutorError("CHANGE_INVALID")
relative = safe_relative(change.get("path"))
path = resolve_under(root, relative)
current = path.read_bytes() if path.is_file() else None
expected = change.get("expected_sha256")
if expected is not None and (current is None or sha256(current) != expected):
raise IsolatedExecutorError("SOURCE_DRIFT:" + relative)
operation = change.get("operation")
if operation == "write_file":
content = change.get("content")
if not isinstance(content, str):
raise IsolatedExecutorError("WRITE_CONTENT_INVALID")
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(content, encoding="utf-8")
elif operation == "replace_text":
if current is None:
raise IsolatedExecutorError("REPLACE_SOURCE_MISSING")
old = str(change.get("old"))
new = str(change.get("new"))
count = int(change.get("count", 1))
text = current.decode("utf-8")
if text.count(old) != count:
raise IsolatedExecutorError("REPLACE_COUNT_INVALID:" + relative)
path.write_text(text.replace(old, new), encoding="utf-8")
elif operation == "delete_file":
if path.exists():
path.unlink()
else:
raise IsolatedExecutorError("CHANGE_OPERATION_INVALID")
changed.append(relative)
return sorted(set(changed))
def changed_bundle(root: Path, changed: list[str]) -> list[dict[str, Any]]:
files = []
for relative in changed:
path = resolve_under(root, relative)
if path.exists():
data = path.read_bytes()
if len(data) > MAX_FILE_BYTES:
raise IsolatedExecutorError("CHANGED_FILE_TOO_LARGE:" + relative)
files.append({
"path": relative,
"deleted": False,
"sha256": sha256(data),
"content_b64": base64.b64encode(data).decode("ascii"),
})
else:
files.append({"path": relative, "deleted": True, "sha256": None, "content_b64": None})
return files
def execute_request(request_path: Path, result_path: Path) -> None:
assert_uncredentialed()
request = json.loads(request_path.read_text(encoding="utf-8"))
if request.get("schema") != "aios.isolated-execution.request.v1":
raise IsolatedExecutorError("REQUEST_SCHEMA_INVALID")
request_id = request.get("request_id")
if not isinstance(request_id, str) or not REQUEST_ID_RE.fullmatch(request_id):
raise IsolatedExecutorError("REQUEST_ID_INVALID")
source_archive = Path(str(request.get("source_archive") or ""))
if not source_archive.is_absolute():
raise IsolatedExecutorError("SOURCE_ARCHIVE_PATH_INVALID")
expected_archive_sha = str(request.get("source_archive_sha256") or "")
if sha256(source_archive.read_bytes()) != expected_archive_sha:
raise IsolatedExecutorError("SOURCE_ARCHIVE_SHA_MISMATCH")
with tempfile.TemporaryDirectory(prefix="aios-isolated-executor-") as temporary:
root = Path(temporary) / "workspace"
root.mkdir()
extract_source(source_archive, root)
changed = apply_changes(request, root)
commands = [
run_command(item, root)
for item in request.get("verification", [])
if isinstance(item, Mapping)
]
result = {
"schema": "aios.isolated-execution.result.v1",
"request_id": request_id,
"request_sha256": sha256(canonical(request)),
"changed_files": changed_bundle(root, changed),
"verification": commands,
"credential_isolation": {
"credential_env_absent": True,
"credential_mount_inaccessible": True,
"governed_bridge_unreachable": True,
},
}
result["result_sha256"] = sha256(canonical(result))
temporary_result = result_path.with_suffix(".tmp")
temporary_result.write_text(json.dumps(result, sort_keys=True, separators=(",", ":")), encoding="utf-8")
os.replace(temporary_result, result_path)
def serve(root: Path) -> None:
assert_uncredentialed()
requests = root / "requests"
results = root / "results"
failed = root / "failed"
for path in (requests, results, failed):
path.mkdir(parents=True, exist_ok=True)
while True:
for request in sorted(requests.glob("*.json")):
if request.is_symlink():
continue
result = results / request.name
try:
execute_request(request, result)
request.unlink()
except Exception as exc:
(failed / request.name).write_text(
json.dumps({"error": str(exc) or type(exc).__name__}, sort_keys=True),
encoding="utf-8",
)
request.unlink(missing_ok=True)
time.sleep(0.5)
if __name__ == "__main__":
import argparse
parser = argparse.ArgumentParser()
parser.add_argument("--root", default="/var/lib/aios-online-agent-executor")
parser.add_argument("--request")
parser.add_argument("--result")
args = parser.parse_args()
if args.request and args.result:
execute_request(Path(args.request), Path(args.result))
else:
serve(Path(args.root))
+24 -346
View File
@@ -3,13 +3,6 @@ from __future__ import annotations
import hashlib, hmac, json, os, re, subprocess, sys, time, urllib.parse, urllib.request, uuid
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
from pathlib import Path
from aios_caller_admission_projection import (
CALLER,
SCOPE,
CallerProjectionClient,
CallerProjectionError,
)
from baseline_authority import BaselineAuthority, BaselineAuthorityError
from typing import Any
@@ -96,11 +89,6 @@ SILENT_RELEASE_PUBLIC_KEYS = Path(os.getenv(
'AIOS_SILENT_RELEASE_PUBLIC_KEYS',
'/etc/aios-gitops/silent-release-owner-public-keys.json',
))
CALLER_PROJECTION_SOCKET = Path(os.getenv(
'AIOS_OWNER_AUTH_BROKER_SOCKET',
'/run/aios-owner-authorization-broker/broker.sock',
))
CALLER_PROJECTION_CLIENT = CallerProjectionClient(CALLER_PROJECTION_SOCKET)
monitor = ActionMonitor()
BASE_MODELS = [
@@ -124,9 +112,6 @@ READ_ONLY_GET_PATHS = frozenset({
'/admin/ai-evolution/api/data',
'/api/release-consistency', '/api/tag-publication-status',
'/api/release-acceptance',
'/api/governed-write-capabilities', '/api/control-plane-snapshot',
'/api/caller-capabilities',
'/api/online-agent-task', '/api/online-agent-task-events',
'/getReleaseCandidate', '/getReleasePlan', '/getReleaseAcceptance',
'/getApprovalEnvelope', '/getReleaseSaga', '/getCapabilityStatus',
'/getRecoveryStatus',
@@ -422,113 +407,14 @@ class H(BaseHTTPRequestHandler):
except Exception: return {'_raw':raw}
def actor(self):
auth=self.headers.get('Authorization',''); tok=auth[7:].strip() if auth.startswith('Bearer ') else self.headers.get('X-AIOS-Token','')
try:
rec=authenticate(tok)
except Exception:
rec=None
if not isinstance(rec, dict) or not rec:
return {
'ok': False,
'account': 'anonymous',
'role': 'anonymous',
'scopes': set(),
'static_scopes': set(),
'projection': None,
'credential_loaded': False,
}
scopes = set(rec.get('scopes') or [])
return {
'ok': True,
'account': safe(rec.get('account') or rec.get('name') or 'token-user'),
'role': rec.get('role', 'user'),
'scopes': scopes,
'static_scopes': set(scopes),
'projection': None,
'credential_loaded': False,
}
def need(self, scope, *, projection_request=None):
a = self.actor()
if not a['ok']:
self.sendj({'error': 'unauthorized', 'hint': 'Bearer token required'}, 401)
return None
if a['account'] == CALLER and scope == SCOPE:
try:
result = CALLER_PROJECTION_CLIENT.rehydrate(
a['account'],
scope,
projection_request,
)
except CallerProjectionError as exc:
self.sendj(
{
'error': 'caller_projection_denied',
'projection_state': 'DENIED',
'detail': exc.code,
'account': a['account'],
},
exc.status,
)
return None
projection = result.public
a['scopes'].update(projection.get('effective_scopes') or [])
a['projection'] = projection
a['credential_loaded'] = result.credential_loaded
if '*' not in a['scopes'] and scope not in a['scopes']:
self.sendj(
{
'error': 'forbidden',
'required_scope': scope,
'account': a['account'],
},
403,
)
return None
rec=authenticate(tok)
if rec: return {'ok':True,'account':safe(rec.get('account') or rec.get('name') or 'token-user'),'role':rec.get('role','user'),'scopes':set(rec.get('scopes') or [])}
return {'ok':False,'account':'anonymous','role':'anonymous','scopes':set()}
def need(self, scope):
a=self.actor()
if not a['ok']: self.sendj({'error':'unauthorized','hint':'Bearer token required'},401); return None
if '*' not in a['scopes'] and scope not in a['scopes']: self.sendj({'error':'forbidden','required_scope':scope,'account':a['account']},403); return None
return a
def caller_capabilities(self):
actor = self.actor()
if not actor['ok']:
return self.sendj(
{'error': 'unauthorized', 'hint': 'Bearer token required'},
401,
)
effective = set(actor['static_scopes'])
projection_state = 'NOT_REQUIRED'
credential_state = 'NOT_REQUIRED'
rehydration_state = 'NOT_APPLICABLE'
admitted = True
projection_error = None
if actor['account'] == CALLER:
try:
result = CALLER_PROJECTION_CLIENT.rehydrate(
actor['account'],
SCOPE,
{},
)
except CallerProjectionError as exc:
admitted = False
projection_state = 'DENIED'
credential_state = 'NOT_REHYDRATED'
rehydration_state = 'FAILED'
projection_error = exc.code
else:
effective.update(result.public.get('effective_scopes') or [])
projection_state = result.public['projection_state']
credential_state = result.public['credential_state']
rehydration_state = result.public['rehydration_state']
return self.sendj(
{
'schema': 'aios.caller-capability-introspection.v1',
'caller_identity': actor['account'],
'admitted': admitted,
'effective_scopes': sorted(effective),
'projection_state': projection_state,
'credential_state': credential_state,
'rehydration_state': rehydration_state,
'projection_error': projection_error,
'secret_exposed': False,
}
)
def bridge_release(self, path, data):
route = {
'/createTagApprovalRequest': '/v2/tag/approval-request',
@@ -560,183 +446,24 @@ class H(BaseHTTPRequestHandler):
return self.sendj(body, exc.code)
except (urllib.error.URLError, TimeoutError) as exc:
return self.sendj({'error': 'GOVERNED_BRIDGE_UNAVAILABLE', 'detail': type(exc).__name__}, 503)
def bridge_v2_json(self, route, data=None, method='POST'):
base = os.getenv('AIOS_BRIDGE_V2_URL', 'http://127.0.0.1:18892').rstrip('/')
payload = None if data is None else json.dumps(
data, sort_keys=True, separators=(',', ':')
).encode('utf-8')
request = urllib.request.Request(
base + route,
data=payload,
headers={'Content-Type': 'application/json'} if payload is not None else {},
method=method,
)
try:
with urllib.request.urlopen(request, timeout=30) as response:
result = json.loads(response.read().decode('utf-8'))
return self.sendj(result, response.status)
except urllib.error.HTTPError as exc:
try:
body = json.loads(exc.read().decode('utf-8'))
except Exception:
body = {'error': 'bridge_http_error'}
return self.sendj(body, exc.code)
except (urllib.error.URLError, TimeoutError) as exc:
return self.sendj(
{'error': 'GOVERNED_BRIDGE_UNAVAILABLE', 'detail': type(exc).__name__},
503,
)
def control_plane_snapshot(self):
"""Build a non-sensitive snapshot locally when Bridge v2 has no snapshot HTTP route."""
base = os.getenv('AIOS_BRIDGE_V2_URL', 'http://127.0.0.1:18892').rstrip('/')
def bridge_get(route):
request = urllib.request.Request(
base + route,
headers={'Accept': 'application/json'},
method='GET',
)
with urllib.request.urlopen(request, timeout=15) as response:
if response.status != 200:
raise RuntimeError('bridge_read_http_' + str(response.status))
value = json.loads(response.read().decode('utf-8'))
if not isinstance(value, dict):
raise RuntimeError('bridge_read_not_object')
return value
try:
runtime = bridge_get('/v2/runtime')
capabilities = bridge_get('/v2/capabilities')
repository = repository_snapshot(
git_dir=AUTHORITATIVE_GIT_DIR,
repository_identity=REPOSITORY_IDENTITY,
)
if not repository.get('repository_available'):
return self.sendj({
'error': 'AUTHORITATIVE_REPOSITORY_UNAVAILABLE',
'query_error': repository.get('query_error'),
}, 503)
baseline = dict(BASELINE_AUTHORITY.read())
except BaselineAuthorityError as exc:
return self.sendj({
'error': 'RECONCILIATION_REQUIRED',
'detail': str(exc),
}, 503)
except urllib.error.HTTPError as exc:
return self.sendj({
'error': 'GOVERNED_BRIDGE_UNAVAILABLE',
'detail': 'HTTP_' + str(exc.code),
}, 503)
except (urllib.error.URLError, TimeoutError, OSError, ValueError, RuntimeError, json.JSONDecodeError) as exc:
return self.sendj({
'error': 'GOVERNED_BRIDGE_UNAVAILABLE',
'detail': type(exc).__name__,
}, 503)
legacy = capabilities.get('legacy_writes_enabled')
if legacy is None:
legacy = capabilities.get('legacy_global_writes_enabled')
payload = {
'schema': 'aios.control-plane-snapshot.aggregate.v1',
'snapshot_source': 'model-api-local-aggregate',
'version': {
'target_release': runtime.get('control_plane_release_version'),
},
'runtime': {
'bridge_schema_version': runtime.get('bridge_schema_version'),
'control_plane_release_version': runtime.get('control_plane_release_version'),
'runtime_release_commit': runtime.get('runtime_release_commit'),
'runtime_path': runtime.get('runtime_path'),
'artifact_sha256': runtime.get('artifact_sha256'),
'drift_status': runtime.get('drift_status'),
'deployment_state': runtime.get('deployment_state'),
},
'repository': {
'identity': runtime.get('repository_identity') or runtime.get('repository') or REPOSITORY_IDENTITY,
'authority_git_dir': runtime.get('authoritative_git_dir') or repository.get('authoritative_git_dir'),
'authority_main_oid': repository.get('authoritative_head_oid'),
},
'production': {
'baseline_version': baseline.get('current_baseline'),
'target_oid': baseline.get('target_oid'),
'deployment_state': baseline.get('status'),
'baseline_generation': baseline.get('generation'),
'baseline_sha256': baseline.get('authority_sha256'),
},
'task': {},
'capabilities': {
'legacy_writes_enabled': legacy,
'governed_writes_enabled': capabilities.get('governed_writes_enabled'),
'force_push': capabilities.get('force_push'),
'main_write': capabilities.get('main_write'),
'tag_write': capabilities.get('tag_write'),
'baseline_write': capabilities.get('baseline_write'),
'production_write': capabilities.get('production_write'),
},
'credential_broker': {
'status': 'SERVER_SIDE_ONLY',
'provider': 'BridgeV2RuntimeCredentialProjection',
},
'secret_material_exposed': False,
}
return self.sendj(payload)
def agent_task_control(self, method, route, data=None):
token_path = Path(os.getenv(
'AIOS_AGENT_TASK_CONTROL_TOKEN_FILE',
'/run/credentials/aios-gitops-model-api-v2r2c1-staging.service/agent_task_control_token',
))
if token_path.is_symlink() or not token_path.is_file():
return self.sendj({'error': 'AGENT_TASK_CONTROL_CREDENTIAL_UNAVAILABLE'}, 503)
token = token_path.read_text(encoding='utf-8').strip()
if len(token) < 32:
return self.sendj({'error': 'AGENT_TASK_CONTROL_CREDENTIAL_INVALID'}, 503)
base = os.getenv('AIOS_AGENT_TASK_API_URL', 'http://127.0.0.1:18940').rstrip('/')
payload = None if data is None else json.dumps(
data, sort_keys=True, separators=(',', ':')
).encode('utf-8')
headers = {'Authorization': 'Bearer ' + token, 'Accept': 'application/json'}
if payload is not None:
headers['Content-Type'] = 'application/json'
request = urllib.request.Request(
base + route, data=payload, headers=headers, method=method
)
try:
with urllib.request.urlopen(request, timeout=30) as response:
result = json.loads(response.read().decode('utf-8'))
return self.sendj(result, response.status)
except urllib.error.HTTPError as exc:
try:
body = json.loads(exc.read().decode('utf-8'))
except Exception:
body = {'error': 'agent_task_http_error'}
return self.sendj(body, exc.code)
except (urllib.error.URLError, TimeoutError) as exc:
return self.sendj(
{'error': 'AGENT_TASK_API_UNAVAILABLE', 'detail': type(exc).__name__},
503,
)
def do_POST(self):
path = urllib.parse.urlparse(self.path).path
if path in {
'/createTagApprovalRequest', '/requestOrCreateTag', '/getTagPublicationStatus',
'/buildProductionArtifact', '/executeApprovedProductionRelease',
'/getReleaseConsistency', '/runReleaseAcceptance',
}:
actor = self.need('tag:create')
if not actor:
return
return self.bridge_release(path, self.body())
if reject_non_read_http(self, 'POST', path):
return
return self.sendj({'error': 'not_found', 'path': path}, 404)
def do_GET(self):
u=urllib.parse.urlparse(self.path); path=u.path.rstrip('/') if u.path != '/' else u.path
if path not in READ_ONLY_GET_PATHS: return self.sendj({'error':'not_found','path':path},404)
q=urllib.parse.parse_qs(u.query)
if path == '/api/governed-write-capabilities':
return self.bridge_v2_json('/v2/capabilities', method='GET')
if path == '/api/control-plane-snapshot':
return self.control_plane_snapshot()
if path == '/api/caller-capabilities':
return self.caller_capabilities()
if path in ('/api/online-agent-task', '/api/online-agent-task-events'):
actor = self.need('agent-task:control')
if not actor:
return
task_id = (q.get('task_id') or [''])[0]
if not re.fullmatch(r'[A-Za-z0-9][A-Za-z0-9._:-]{2,179}', task_id):
return self.sendj({'error': 'task_id_invalid'}, 400)
suffix = '/events' if path.endswith('-events') else ''
return self.agent_task_control('GET', '/v1/tasks/' + task_id + suffix)
try:
if path in ('/health','/api/health'):
return self.sendj({
@@ -973,58 +700,12 @@ class H(BaseHTTPRequestHandler):
return self.sendj({'error':'not_found','path':path},404)
def do_POST(self):
path=urllib.parse.urlparse(self.path).path.rstrip('/')
governed_routes = {
'/createGovernedTask': ('/v2/governed-task', 'branch:create'),
'/acquireWriterLease': ('/v2/writer-lease', 'file:write'),
'/commitFileGoverned': ('/v2/commit-file', 'file:write'),
'/commitFilesGoverned': ('/v2/commit-files', 'file:write'),
'/getGitOpsJob': ('/v2/job', 'file:write'),
'/resumeGitOpsJob': ('/v2/job/resume', 'file:write'),
'/reconcileGitOpsJob': ('/v2/job/reconcile', 'file:write'),
'/createTagApprovalRequest': ('/v2/tag/approval-request', 'tag:create'),
'/requestOrCreateTag': ('/v2/tag/publish', 'tag:create'),
'/getTagPublicationStatus': ('/v2/tag/status', 'tag:create'),
'/buildProductionArtifact': ('/v2/release/artifact', 'tag:create'),
'/executeApprovedProductionRelease': ('/v2/release/execute', 'tag:create'),
'/getReleaseConsistency': ('/v2/release/consistency', 'tag:create'),
'/runReleaseAcceptance': ('/v2/release/acceptance', 'tag:create'),
}
if path in governed_routes:
route, scope = governed_routes[path]
actor = self.need(scope)
if not actor:
return
return self.bridge_v2_json(route, self.body(), method='POST')
if path in {
'/createOnlineAgentTask', '/registerOnlineAgent',
'/verifyOnlineAgentTask', '/cancelOnlineAgentTask',
}:
actor = self.need('agent-task:control')
if not actor:
return
data = self.body()
if path == '/createOnlineAgentTask':
return self.agent_task_control('POST', '/v1/tasks', data)
if path == '/registerOnlineAgent':
return self.agent_task_control('POST', '/v1/agents/register', data)
task_id = str(data.pop('task_id', '') or '')
if not re.fullmatch(r'[A-Za-z0-9][A-Za-z0-9._:-]{2,179}', task_id):
return self.sendj({'error': 'task_id_invalid'}, 400)
suffix = '/verify' if path == '/verifyOnlineAgentTask' else '/cancel'
return self.agent_task_control('POST', '/v1/tasks/' + task_id + suffix, data)
if reject_non_read_http(self, 'POST', path): return
data=self.body()
try:
if path == '/createReleaseCandidate':
a = self.need(
'release:candidate',
projection_request={
'source_commit': str(data.get('source_commit') or ''),
'version': str(data.get('version') or ''),
},
)
if not a:
return
a=self.need('release:candidate')
if not a: return
return self.sendj(
silent_release_service().create_release_candidate(data)
)
@@ -1315,7 +996,7 @@ def api_spec():
return {
'version': 'V2R2C1-IMMUTABLE-STAGING',
'openapi': '/api/openapi.yaml',
'read': ['GET /api/caller-capabilities','GET /api/base','GET /api/ver','GET /api/diff','GET /api/patch','GET /api/model','GET /api/run','GET /getReleaseCandidate','GET /getApprovalEnvelope','GET /getReleaseSaga','GET /admin/ai-evolution'],
'read': ['GET /api/base','GET /api/ver','GET /api/diff','GET /api/patch','GET /api/model','GET /api/run','GET /getReleaseCandidate','GET /getApprovalEnvelope','GET /getReleaseSaga','GET /admin/ai-evolution'],
'write': ['POST /api/branch','POST /api/file','POST /api/commit','POST /api/patch','POST /api/tag','POST /api/rollback','POST /api/model','PUT /api/model-priority','POST /api/nl','POST /createReleaseCandidate','POST /submitOwnerApproval','POST /startDeployment'],
'policy': {
'bearer_required_for_write': True,
@@ -1330,9 +1011,6 @@ def api_spec():
'silent_release_shadow_default': True,
'runtime_mode': 'IMMUTABLE_STAGING',
'governed_release_routing': 'bridge-v2',
'caller_admission_projection': 'durable-owner-policy-server-side-ack',
'caller_projection_scope': SCOPE,
'caller_projection_caller': CALLER,
},
}
@@ -0,0 +1,397 @@
"""Canonical governed execution projection for Online Agent Tasks.
The durable governed binding is authoritative. Natural-language task fields and
worker configuration are never allowed to invent repository, ref, generation, or
idempotency coordinates.
"""
from __future__ import annotations
from dataclasses import asdict, dataclass
import hashlib
import json
import re
from pathlib import Path
from typing import Any, Mapping, Sequence
import urllib.error
import urllib.request
EXECUTION_SCHEMA = "aios.online-agent.execution.v2"
PROJECTION_SCHEMA = "aios.online-agent.task-projection.v1"
OID_RE = re.compile(r"^[0-9a-f]{40}$")
TASK_RE = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._:-]{2,179}$")
BRANCH_RE = re.compile(r"^agent/[A-Za-z0-9][A-Za-z0-9._/-]{2,179}$")
KEY_RE = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._:-]{7,159}$")
PROTECTED_PREFIXES = ("main", "master", "refs/", "tags/", "tag/", "release/", "baseline/", "production/")
MUTATION_ACTIONS = frozenset({"deterministic"})
SUPPORTED_ACTIONS = frozenset({"deterministic", "source_capture"})
class ExecutionProjectionError(RuntimeError):
"""A canonical fail-closed projection or validation error."""
class OnlineTaskProjectionClient:
"""Official HTTP projection client using only a control credential reference."""
def __init__(self, api_base: str, control_token_path: Path) -> None:
self.api_base = api_base.rstrip("/")
self.control_token_path = Path(control_token_path)
def _token(self) -> str:
path = self.control_token_path
if path.is_symlink() or not path.is_file():
_error("ONLINE_TASK_PROJECTION_CONTROL_UNAVAILABLE")
value = path.read_text(encoding="utf-8").strip()
if len(value) < 32:
_error("ONLINE_TASK_PROJECTION_CONTROL_INVALID")
return value
def create(
self,
projected_task: Mapping[str, Any],
*,
binding: "GovernedExecutionBinding",
) -> dict[str, Any]:
body = dict(projected_task)
payload = _canonical(body).encode("utf-8")
request = urllib.request.Request(
self.api_base + "/v1/tasks",
method="POST",
data=payload,
headers={
"Authorization": "Bearer " + self._token(),
"Content-Type": "application/json",
"Accept": "application/json",
},
)
try:
with urllib.request.urlopen(request, timeout=15) as response:
result = json.loads(response.read().decode("utf-8"))
status = response.status
except urllib.error.HTTPError as exc:
raw = exc.read(8192).decode("utf-8", "replace")
try:
error = json.loads(raw).get("error")
except (AttributeError, json.JSONDecodeError):
error = "HTTP_ERROR"
raise ExecutionProjectionError(f"ONLINE_TASK_PROJECTION_FAILED:{exc.code}:{error}") from exc
except (OSError, urllib.error.URLError, TimeoutError) as exc:
raise ExecutionProjectionError("ONLINE_TASK_PROJECTION_UNAVAILABLE") from exc
if status not in {200, 201} or not isinstance(result, dict):
_error("ONLINE_TASK_PROJECTION_RESPONSE_INVALID")
if result.get("task_id") != binding.task_id or result.get("status") not in {"QUEUED", "VERIFYING", "SUCCEEDED"}:
_error("ONLINE_TASK_PROJECTION_RESPONSE_INVALID")
metadata = result.get("metadata")
if not isinstance(metadata, Mapping):
_error("ONLINE_TASK_PROJECTION_RESPONSE_INVALID")
returned_execution = validate_execution_envelope(
_object(metadata.get("execution"), "ONLINE_TASK_PROJECTION_RESPONSE_INVALID"),
binding=binding,
)
submitted_metadata = _object(body.get("metadata"), "ONLINE_TASK_PROJECTION_REQUEST_INVALID")
submitted_execution = validate_execution_envelope(
_object(submitted_metadata.get("execution"), "ONLINE_TASK_PROJECTION_REQUEST_INVALID"),
binding=binding,
)
if _canonical(returned_execution) != _canonical(submitted_execution):
_error("ONLINE_TASK_PROJECTION_RESPONSE_BINDING_INVALID")
return result
def _error(code: str) -> None:
raise ExecutionProjectionError(code)
def _object(value: object, code: str) -> dict[str, Any]:
if not isinstance(value, Mapping):
_error(code)
return dict(value)
def _string(value: object, code: str) -> str:
if not isinstance(value, str) or not value.strip():
_error(code)
return value.strip()
def _task_id(value: object) -> str:
value = _string(value, "TASK_ID_INVALID")
if TASK_RE.fullmatch(value) is None:
_error("TASK_ID_INVALID")
return value
def _oid(value: object, code: str) -> str:
value = _string(value, code)
if OID_RE.fullmatch(value) is None:
_error(code)
return value
def _branch(value: object) -> str:
value = _string(value, "TARGET_BRANCH_INVALID")
if BRANCH_RE.fullmatch(value) is None or value.startswith(PROTECTED_PREFIXES):
_error("TARGET_BRANCH_INVALID")
return value
def _generation(value: object) -> int:
if not isinstance(value, int) or isinstance(value, bool) or value < 1:
_error("GENERATION_INVALID")
return value
def _canonical(value: object) -> str:
return json.dumps(value, ensure_ascii=False, sort_keys=True, separators=(",", ":"))
def _digest(value: object) -> str:
return hashlib.sha256(_canonical(value).encode("utf-8")).hexdigest()
def _source_signals(value: object) -> tuple[str, ...]:
"""Normalize untrusted natural-language hints before they enter a signed envelope."""
if value is None:
return ()
if not isinstance(value, Sequence) or isinstance(value, (str, bytes)):
_error("SOURCE_CAPTURE_SIGNALS_INVALID")
result: list[str] = []
for item in value:
if not isinstance(item, str):
_error("SOURCE_CAPTURE_SIGNALS_INVALID")
text = " ".join(item.split()).strip()
if not text:
continue
# Projection may accept long task prose, but execution envelopes stay bounded.
text = text[:512].rstrip()
if text not in result:
result.append(text)
if len(result) == 16:
break
return tuple(result)
def _capabilities(value: object) -> tuple[str, ...]:
if value is None:
return ()
if not isinstance(value, Sequence) or isinstance(value, (str, bytes)):
_error("CAPABILITY_SCOPE_INVALID")
result = tuple(sorted({_string(item, "CAPABILITY_SCOPE_INVALID") for item in value}))
if any(len(item) > 120 for item in result):
_error("CAPABILITY_SCOPE_INVALID")
return result
@dataclass(frozen=True, slots=True)
class GovernedExecutionBinding:
task_id: str
job_id: str
repository: str
base_oid: str
expected_branch_oid: str
target_branch: str
branch_mode: str
generation: int
idempotency_key: str
@classmethod
def from_mapping(cls, value: Mapping[str, Any]) -> "GovernedExecutionBinding":
data = dict(value)
task_id = _task_id(data.get("task_id"))
job_id = _string(data.get("job_id"), "JOB_ID_REQUIRED")
if TASK_RE.fullmatch(job_id) is None:
_error("JOB_ID_INVALID")
repository = _string(data.get("repository"), "REPOSITORY_INVALID")
if repository != "aios/aios":
_error("WRONG_REPOSITORY_ENVELOPE_DENY")
base_oid = _oid(data.get("base_oid"), "BASE_OID_INVALID")
expected = _oid(data.get("expected_branch_oid"), "EXPECTED_BRANCH_OID_INVALID")
target = _branch(data.get("target_branch"))
mode = _string(data.get("branch_mode"), "BRANCH_MODE_MISSING")
if mode not in {"CREATE_NEW", "CONTINUE_EXISTING"}:
_error("BRANCH_MODE_INVALID")
generation = _generation(data.get("generation"))
if mode == "CREATE_NEW" and expected != base_oid:
_error("EXPECTED_BASE_CONFLICT")
key = _string(data.get("idempotency_key"), "IDEMPOTENCY_KEY_INVALID")
if KEY_RE.fullmatch(key) is None:
_error("IDEMPOTENCY_KEY_INVALID")
return cls(task_id, job_id, repository, base_oid, expected, target, mode, generation, key)
def public(self) -> dict[str, Any]:
return asdict(self)
def binding_from_bootstrap(value: Mapping[str, Any]) -> GovernedExecutionBinding:
return GovernedExecutionBinding.from_mapping(value)
def validate_execution_envelope(
value: Mapping[str, Any],
*,
binding: GovernedExecutionBinding | None = None,
worker_capabilities: Sequence[str] | None = None,
) -> dict[str, Any]:
envelope = dict(value)
if envelope.get("schema") != EXECUTION_SCHEMA:
_error("EXECUTION_ENVELOPE_SCHEMA_VALIDATION")
candidate = GovernedExecutionBinding.from_mapping(envelope)
if binding is not None and candidate != binding:
comparisons = (
(candidate.task_id, binding.task_id, "TASK_BINDING_CONFLICT"),
(candidate.job_id, binding.job_id, "JOB_BINDING_CONFLICT"),
(candidate.repository, binding.repository, "WRONG_REPOSITORY_ENVELOPE_DENY"),
(candidate.base_oid, binding.base_oid, "WRONG_BASE_ENVELOPE_DENY"),
(candidate.expected_branch_oid, binding.expected_branch_oid, "WRONG_EXPECTED_HEAD_ENVELOPE_DENY"),
(candidate.target_branch, binding.target_branch, "WRONG_BRANCH_ENVELOPE_DENY"),
(candidate.generation, binding.generation, "WRONG_GENERATION_ENVELOPE_DENY"),
(candidate.branch_mode, binding.branch_mode, "BRANCH_MODE_BINDING_CONFLICT"),
(candidate.idempotency_key, binding.idempotency_key, "IDEMPOTENCY_BINDING_CONFLICT"),
)
for actual, expected, code in comparisons:
if actual != expected:
_error(code)
action = _string(envelope.get("action"), "EXECUTION_ACTION_MISSING")
if action not in SUPPORTED_ACTIONS:
_error("EXECUTION_ACTION_UNSUPPORTED")
capability_scope = _capabilities(envelope.get("capability_scope"))
if worker_capabilities is not None and not set(capability_scope).issubset(set(worker_capabilities)):
_error("CAPABILITY_MISMATCH_DENY")
workspace = _object(envelope.get("workspace"), "WORKSPACE_POLICY_REQUIRED")
if workspace.get("authority_git_dir") != "/data/aios-gitops/repo.git" or workspace.get("read_only_repository") is not True:
_error("WORKSPACE_POLICY_DENY")
governance = _object(envelope.get("governance"), "GOVERNANCE_POLICY_REQUIRED")
governed_coordinates = (
(governance.get("task_id"), candidate.task_id, "TASK_BINDING_CONFLICT"),
(governance.get("job_id"), candidate.job_id, "JOB_BINDING_CONFLICT"),
(governance.get("idempotency_key"), candidate.idempotency_key, "IDEMPOTENCY_BINDING_CONFLICT"),
)
for actual_coordinate, expected_coordinate, code in governed_coordinates:
if actual_coordinate != expected_coordinate:
_error(code)
commit_key = _string(governance.get("commit_idempotency_key"), "COMMIT_IDEMPOTENCY_KEY_INVALID")
if KEY_RE.fullmatch(commit_key) is None:
_error("COMMIT_IDEMPOTENCY_KEY_INVALID")
if governance.get("writer_lease") is not (action in MUTATION_ACTIONS):
_error("WRITER_LEASE_POLICY_DENY")
for field in ("main_write", "production_write", "tag_write", "baseline_write", "force_push", "protected_ref_bypass"):
if governance.get(field, False) is not False:
_error("POLICY_DENY")
changes = envelope.get("change_set")
verification = envelope.get("verification")
if not isinstance(changes, list) or not isinstance(verification, list):
_error("EXECUTION_OPERATIONS_INVALID")
if action == "source_capture":
if changes or verification:
_error("SOURCE_CAPTURE_MUTATION_DENY")
signals = list(_source_signals(envelope.get("source_signals") or []))
if envelope.get("source_signals") != signals:
_error("SOURCE_CAPTURE_SIGNALS_INVALID")
elif not changes:
_error("CHANGE_SET_INVALID")
declared = str(envelope.get("execution_digest") or "")
material = {key: item for key, item in envelope.items() if key != "execution_digest"}
actual = _digest(material)
if declared != actual:
_error("EXECUTION_DIGEST_INVALID")
return {**material, "execution_digest": actual}
def project_execution_envelope(
binding: GovernedExecutionBinding,
execution_request: Mapping[str, Any] | None = None,
*,
required_capabilities: Sequence[str] = (),
) -> dict[str, Any]:
request = dict(execution_request or {})
action = str(request.get("action") or "source_capture")
if action not in SUPPORTED_ACTIONS:
_error("EXECUTION_ACTION_UNSUPPORTED")
capabilities = _capabilities(request.get("capability_scope", required_capabilities))
governance = {
"task_id": binding.task_id,
"job_id": binding.job_id,
"idempotency_key": binding.idempotency_key,
"commit_idempotency_key": str(request.get("commit_idempotency_key") or f"{binding.task_id}-commit-g{binding.generation}"),
"writer_lease": action in MUTATION_ACTIONS,
"main_write": False,
"production_write": False,
"tag_write": False,
"baseline_write": False,
"force_push": False,
"protected_ref_bypass": False,
}
supplied_governance = request.get("governance")
if supplied_governance is not None:
governance.update(_object(supplied_governance, "GOVERNANCE_POLICY_REQUIRED"))
material: dict[str, Any] = {
"schema": EXECUTION_SCHEMA,
**binding.public(),
"action": action,
"adapter": str(request.get("adapter") or ("deterministic" if action == "deterministic" else "source_capture")),
"capability_scope": list(capabilities),
"change_set": list(request.get("change_set") or []),
"verification": list(request.get("verification") or []),
"source_signals": list(_source_signals(request.get("source_signals") or [])) if action == "source_capture" else [],
"governance": governance,
"workspace": {
"authority_git_dir": "/data/aios-gitops/repo.git",
"read_only_repository": True,
"cleanup_policy": "always",
},
}
envelope = {**material, "execution_digest": _digest(material)}
return validate_execution_envelope(envelope, binding=binding)
def project_online_task(
binding: GovernedExecutionBinding,
task_request: Mapping[str, Any],
execution_request: Mapping[str, Any] | None = None,
) -> dict[str, Any]:
request = dict(task_request)
required = _capabilities(request.get("required_capabilities"))
execution = project_execution_envelope(binding, execution_request, required_capabilities=required)
metadata = dict(request.get("metadata") or {})
if "execution" in metadata:
validate_execution_envelope(_object(metadata["execution"], "EXECUTION_ENVELOPE_INVALID"), binding=binding)
if metadata["execution"] != execution:
_error("EXECUTION_PROJECTION_CONFLICT")
metadata["execution"] = execution
metadata["execution_projection"] = {
"schema": PROJECTION_SCHEMA,
"task_id": binding.task_id,
"job_id": binding.job_id,
"execution_digest": execution["execution_digest"],
}
return {
**request,
"task_id": binding.task_id,
"metadata": metadata,
"required_capabilities": list(required),
"idempotency_key": str(request.get("idempotency_key") or f"online-task:{binding.idempotency_key}"),
}
def resolve_task_execution(
task: Mapping[str, Any],
binding: GovernedExecutionBinding,
*,
worker_capabilities: Sequence[str] | None = None,
) -> tuple[dict[str, Any], str]:
metadata = task.get("metadata")
if isinstance(metadata, Mapping) and "execution" in metadata:
value = validate_execution_envelope(
_object(metadata["execution"], "EXECUTION_ENVELOPE_INVALID"),
binding=binding,
worker_capabilities=worker_capabilities,
)
return value, "persisted"
projected = project_execution_envelope(
binding,
{"action": "source_capture", "capability_scope": task.get("required_capabilities") or []},
required_capabilities=task.get("required_capabilities") or [],
)
validate_execution_envelope(projected, binding=binding, worker_capabilities=worker_capabilities)
return projected, "recovered_projection"
@@ -0,0 +1,187 @@
"""Durable, authority-bound deterministic execution plans for Online Agent Tasks."""
from __future__ import annotations
from contextlib import contextmanager
from dataclasses import dataclass
import fcntl
import hashlib
import json
from pathlib import Path
from typing import Any, Mapping
from aios_online_agent_execution import (
GovernedExecutionBinding,
project_execution_envelope,
)
class ExecutionPlanError(RuntimeError):
pass
def _canonical(value: object) -> bytes:
return json.dumps(value, sort_keys=True, separators=(",", ":"), ensure_ascii=False).encode("utf-8")
def _digest(value: object) -> str:
return hashlib.sha256(_canonical(value)).hexdigest()
def _task_key(task_id: str) -> str:
readable = "".join(ch if ch.isalnum() or ch in "._-" else "-" for ch in task_id).strip("-")[:72] or "task"
return readable + "-" + hashlib.sha256(task_id.encode("utf-8")).hexdigest()[:24]
@dataclass(frozen=True)
class ExecutionPlanRecord:
task_id: str
job_id: str
source_head_oid: str
generation: int
idempotency_key: str
plan_digest: str
execution: dict[str, Any]
def public(self) -> dict[str, Any]:
return {
"schema": "aios.online-agent.plan-record.v1",
"task_id": self.task_id,
"job_id": self.job_id,
"source_head_oid": self.source_head_oid,
"generation": self.generation,
"idempotency_key": self.idempotency_key,
"plan_digest": self.plan_digest,
"execution": dict(self.execution),
}
class ExecutionPlanStore:
def __init__(self, root: Path) -> None:
self.root = Path(root)
self.root.mkdir(parents=True, exist_ok=True)
def _path(self, task_id: str) -> Path:
return self.root / (_task_key(task_id) + ".json")
@contextmanager
def _lock(self, task_id: str):
path = self.root / (_task_key(task_id) + ".lock")
with path.open("a+b") as handle:
fcntl.flock(handle.fileno(), fcntl.LOCK_EX)
try:
yield
finally:
fcntl.flock(handle.fileno(), fcntl.LOCK_UN)
@staticmethod
def _source_head(binding: GovernedExecutionBinding) -> str:
return binding.expected_branch_oid if binding.branch_mode == "CONTINUE_EXISTING" else binding.base_oid
def _read(self, task_id: str) -> ExecutionPlanRecord | None:
path = self._path(task_id)
if not path.is_file() or path.is_symlink():
return None
value = json.loads(path.read_text(encoding="utf-8"))
if not isinstance(value, Mapping) or value.get("schema") != "aios.online-agent.plan-record.v1":
raise ExecutionPlanError("EXECUTION_PLAN_RECORD_INVALID")
execution = value.get("execution")
if not isinstance(execution, Mapping):
raise ExecutionPlanError("EXECUTION_PLAN_RECORD_INVALID")
return ExecutionPlanRecord(
task_id=str(value.get("task_id") or ""),
job_id=str(value.get("job_id") or ""),
source_head_oid=str(value.get("source_head_oid") or ""),
generation=int(value.get("generation") or 0),
idempotency_key=str(value.get("idempotency_key") or ""),
plan_digest=str(value.get("plan_digest") or ""),
execution=dict(execution),
)
def register(
self,
binding: GovernedExecutionBinding,
request: Mapping[str, Any],
) -> dict[str, Any]:
with self._lock(binding.task_id):
return self._register_locked(binding, request)
def _register_locked(
self,
binding: GovernedExecutionBinding,
request: Mapping[str, Any],
) -> dict[str, Any]:
if request.get("schema") != "aios.online-agent.plan.v1":
raise ExecutionPlanError("EXECUTION_PLAN_SCHEMA_INVALID")
if request.get("task_id") != binding.task_id or request.get("job_id") != binding.job_id:
raise ExecutionPlanError("EXECUTION_PLAN_BINDING_CONFLICT")
if int(request.get("generation") or 0) != binding.generation:
raise ExecutionPlanError("EXECUTION_PLAN_GENERATION_CONFLICT")
source_head = str(request.get("source_head_oid") or "")
if source_head != self._source_head(binding):
raise ExecutionPlanError("EXECUTION_PLAN_SOURCE_HEAD_CONFLICT")
key = str(request.get("idempotency_key") or "")
if len(key) < 8:
raise ExecutionPlanError("EXECUTION_PLAN_IDEMPOTENCY_INVALID")
execution_request = request.get("execution")
if not isinstance(execution_request, Mapping) or execution_request.get("action") != "deterministic":
raise ExecutionPlanError("EXECUTION_PLAN_DETERMINISTIC_REQUIRED")
envelope = project_execution_envelope(binding, execution_request)
material = {
"task_id": binding.task_id,
"job_id": binding.job_id,
"source_head_oid": source_head,
"generation": binding.generation,
"idempotency_key": key,
"execution": dict(execution_request),
}
plan_digest = _digest(material)
existing = self._read(binding.task_id)
expected_previous = request.get("expected_previous_plan_digest")
if existing is not None:
if existing.idempotency_key == key:
if existing.plan_digest != plan_digest:
raise ExecutionPlanError("EXECUTION_PLAN_IDEMPOTENCY_CONFLICT")
return {**existing.public(), "idempotent": True, "execution_digest": envelope["execution_digest"]}
if expected_previous != existing.plan_digest:
raise ExecutionPlanError("EXECUTION_PLAN_REPLACEMENT_CAS_MISMATCH")
if existing.source_head_oid != source_head:
raise ExecutionPlanError("EXECUTION_PLAN_STALE_REPLACEMENT_DENY")
elif expected_previous not in (None, ""):
raise ExecutionPlanError("EXECUTION_PLAN_REPLACEMENT_CAS_MISMATCH")
record = ExecutionPlanRecord(
task_id=binding.task_id,
job_id=binding.job_id,
source_head_oid=source_head,
generation=binding.generation,
idempotency_key=key,
plan_digest=plan_digest,
execution=dict(execution_request),
)
path = self._path(binding.task_id)
temporary = path.with_suffix(".tmp")
temporary.write_text(
json.dumps(record.public(), sort_keys=True, separators=(",", ":")),
encoding="utf-8",
)
temporary.replace(path)
return {**record.public(), "idempotent": False, "execution_digest": envelope["execution_digest"]}
def resolve(self, binding: GovernedExecutionBinding) -> dict[str, Any] | None:
record = self._read(binding.task_id)
if record is None:
return None
if (
record.task_id != binding.task_id
or record.job_id != binding.job_id
or record.generation != binding.generation
):
raise ExecutionPlanError("EXECUTION_PLAN_BINDING_CONFLICT")
if record.source_head_oid != self._source_head(binding):
raise ExecutionPlanError("EXECUTION_PLAN_SOURCE_HEAD_DRIFT")
envelope = project_execution_envelope(binding, record.execution)
return {
"execution": dict(record.execution),
"plan_digest": record.plan_digest,
"execution_digest": envelope["execution_digest"],
}
File diff suppressed because it is too large Load Diff
@@ -964,6 +964,7 @@ def validate_writer_lease_request_v2(raw: Mapping[str, Any], probe: Any) -> dict
required = {
"schema",
"task_id",
"job_id",
"repository",
"branch_mode",
"base_oid",
@@ -976,6 +977,7 @@ def validate_writer_lease_request_v2(raw: Mapping[str, Any], probe: Any) -> dict
raise ProvisioningError("LEASE_REQUEST_V2_FIELDS_REJECTED", 409)
request = WriterLeaseRequestV2(
task_id=str(raw["task_id"]),
job_id=str(raw["job_id"]),
repository=str(raw["repository"]),
branch_mode=str(raw["branch_mode"]),
base_oid=str(raw["base_oid"]),
@@ -988,6 +990,7 @@ def validate_writer_lease_request_v2(raw: Mapping[str, Any], probe: Any) -> dict
return {
"schema": WriterLeaseRequestV2.schema,
"task_id": request.task_id,
"job_id": request.job_id,
"repository": request.repository,
"branch_mode": request.branch_mode,
"base_oid": request.base_oid,
@@ -1,46 +0,0 @@
# External agent ingress. TLS termination is assumed on the enclosing server.
# Control endpoints (/v1/agents/register, POST /v1/tasks, verify/cancel/events)
# are intentionally NOT exposed here; the AIOS control plane reaches them on
# loopback with its server-side credential.
location = /agent-tasks/health {
rewrite ^/agent-tasks/(.*)$ /$1 break;
proxy_pass http://127.0.0.1:18940;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-Proto $scheme;
}
location = /agent-tasks/v1/tasks/poll {
limit_except POST { deny all; }
rewrite ^/agent-tasks/(.*)$ /$1 break;
proxy_pass http://127.0.0.1:18940;
proxy_http_version 1.1;
proxy_set_header Authorization $http_authorization;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-Proto $scheme;
client_max_body_size 1m;
proxy_read_timeout 35s;
}
location ~ ^/agent-tasks/v1/tasks/[A-Za-z0-9][A-Za-z0-9._:-]{2,179}/(claim|heartbeat|result)$ {
limit_except POST { deny all; }
rewrite ^/agent-tasks/(.*)$ /$1 break;
proxy_pass http://127.0.0.1:18940;
proxy_http_version 1.1;
proxy_set_header Authorization $http_authorization;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-Proto $scheme;
client_max_body_size 1m;
proxy_read_timeout 35s;
}
location ~ ^/agent-tasks/v1/tasks/[A-Za-z0-9][A-Za-z0-9._:-]{2,179}$ {
limit_except GET { deny all; }
rewrite ^/agent-tasks/(.*)$ /$1 break;
proxy_pass http://127.0.0.1:18940;
proxy_http_version 1.1;
proxy_set_header Authorization $http_authorization;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_read_timeout 35s;
}
@@ -1,36 +0,0 @@
[Unit]
Description=AIOS Online Agent Task API
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
User=aios-agent-task
Group=aios-agent-task
WorkingDirectory=@RUNTIME_ROOT@
Environment=PYTHONUNBUFFERED=1
Environment=PYTHONDONTWRITEBYTECODE=1
Environment=PYTHONPATH=@RUNTIME_ROOT@/app
Environment=AIOS_AGENT_TASK_DB=/var/lib/aios-agent-task/state.sqlite3
Environment=AIOS_AGENT_TASK_HOST=127.0.0.1
Environment=AIOS_AGENT_TASK_PORT=18940
Environment=AIOS_AGENT_TASK_CONTROL_TOKEN_FILE=/run/credentials/aios-agent-task-api.service/control_token
LoadCredential=control_token:/etc/aios-gitops/agent-task-control.token
ExecStart=/usr/bin/python3 @RUNTIME_ROOT@/app/aios_agent_task_api.py
Restart=on-failure
RestartSec=2
NoNewPrivileges=true
PrivateTmp=true
ProtectHome=true
ProtectSystem=strict
ProtectKernelTunables=true
ProtectKernelModules=true
ProtectControlGroups=true
LockPersonality=true
RestrictSUIDSGID=true
RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6
ReadOnlyPaths=@RUNTIME_ROOT@
ReadWritePaths=/var/lib/aios-agent-task
[Install]
WantedBy=multi-user.target
@@ -0,0 +1,5 @@
[Service]
LoadCredential=agent_task_control_token:/etc/aios-gitops/agent-task-control.token
Environment=AIOS_AGENT_TASK_API_URL=http://127.0.0.1:18940
Environment=AIOS_ONLINE_TASK_PROJECTION_ENABLED=1
Environment=AIOS_AGENT_TASK_CONTROL_TOKEN_FILE=%d/agent_task_control_token
@@ -1,7 +0,0 @@
# Existing model API drop-in. Install only with the governed runtime package.
[Service]
SupplementaryGroups=gitops aios-executor-exchange aios-autonomous-agent-worker
Environment=AIOS_OWNER_AUTH_BROKER_SOCKET=/run/aios-owner-authorization-broker/broker.sock
Environment=AIOS_OWNER_AUTH_BROKER_ACCOUNT=chatgpt-governed-implementer-singlehost
Environment=AIOS_OWNER_AUTH_BROKER_CALLER=chatgpt-governed-implementer-singlehost
Environment=AIOS_OWNER_AUTH_BROKER_SCOPE=release:candidate
@@ -28,9 +28,6 @@ Environment=AIOS_GITOPS_MODEL_API_SERVICE=aios-gitops-model-api-v2r2c1-staging.s
Environment=AIOS_SILENT_RELEASE_EXECUTION_ENABLED=false
Environment=AIOS_SILENT_RELEASE_STATE_ROOT=/var/lib/aios-gitops/model-api-staging/silent-release
Environment=AIOS_BRIDGE_V2_URL=http://127.0.0.1:18892
Environment=AIOS_AGENT_TASK_API_URL=http://127.0.0.1:18940
Environment=AIOS_AGENT_TASK_CONTROL_TOKEN_FILE=/run/credentials/aios-gitops-model-api-v2r2c1-staging.service/agent_task_control_token
LoadCredential=agent_task_control_token:/etc/aios-gitops/agent-task-control.token
Environment=AIOS_GITOPS_HOST=127.0.0.1
Environment=AIOS_GITOPS_PORT=18893
Environment=PYTHONPATH=@RUNTIME_ROOT@/app:@RUNTIME_ROOT@/runtime-agent-os
@@ -0,0 +1,33 @@
[Unit]
Description=AIOS uncredentialed deterministic Planner/Executor
After=local-fs.target
[Service]
Type=simple
User=aios-engineering-executor
Group=aios-executor-exchange
ExecStart=/usr/bin/python3 -B /opt/aios/current/active-runtime/app/aios_isolated_executor.py --root /var/lib/aios-online-agent-executor
Restart=always
RestartSec=2
NoNewPrivileges=true
PrivateTmp=true
PrivateNetwork=true
ProtectSystem=strict
ProtectHome=true
ProtectKernelTunables=true
ProtectKernelModules=true
ProtectControlGroups=true
LockPersonality=true
RestrictSUIDSGID=true
RestrictRealtime=true
RestrictNamespaces=true
MemoryDenyWriteExecute=true
RestrictAddressFamilies=AF_UNIX AF_INET
IPAddressDeny=any
IPAddressAllow=localhost
InaccessiblePaths=/run/credentials
ReadWritePaths=/var/lib/aios-online-agent-executor
UMask=0027
[Install]
WantedBy=multi-user.target
@@ -0,0 +1,45 @@
[Unit]
Description=AIOS governed Online Agent Worker
After=aios-agent-task-api.service aios-chatgpt-bridge-v2-staging.service
Wants=aios-agent-task-api.service aios-chatgpt-bridge-v2-staging.service
[Service]
Type=simple
User=aios-engineering-worker
Group=aios-engineering-worker
SupplementaryGroups=gitops aios-executor-exchange
LoadCredentialEncrypted=agent_token:/etc/credstore.encrypted/aios-online-agent-worker-20260815/agent_token
Environment=PYTHONUNBUFFERED=1
Environment=AIOS_AGENT_TASK_API_URL=http://127.0.0.1:18940
Environment=AIOS_GOVERNED_WRITE_API_URL=http://127.0.0.1:18892
Environment=AIOS_AGENT_ID=online-engineering-worker
Environment=AIOS_AGENT_TYPE=trusted-online-engineering-worker
Environment=AIOS_AGENT_VERSION=2026-08-17.1-dynamic
Environment=AIOS_WORKER_CAPABILITIES=git.read,git.test,governed.write.branch,governed.write.file
Environment=AIOS_AUTHORITY_GIT_DIR=/data/aios-gitops/repo.git
Environment=AIOS_ISOLATED_EXECUTOR_ROOT=/var/lib/aios-online-agent-executor
Environment=AIOS_WORKER_STATE_ROOT=/var/lib/aios-online-agent-worker
Environment=AIOS_WORKER_WORKSPACE_ROOT=/var/lib/aios-online-agent-worker/workspaces
ExecStart=/usr/bin/python3 -B /opt/aios/current/active-runtime/app/aios_online_agent_worker.py
Restart=always
RestartSec=5
TimeoutStopSec=20
NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=strict
ProtectHome=true
ProtectKernelTunables=true
ProtectKernelModules=true
ProtectControlGroups=true
LockPersonality=true
RestrictSUIDSGID=true
RestrictRealtime=true
RestrictNamespaces=true
MemoryDenyWriteExecute=true
RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6
ReadOnlyPaths=/data/aios-gitops/repo.git
ReadWritePaths=/var/lib/aios-online-agent-worker
UMask=0027
[Install]
WantedBy=multi-user.target
@@ -0,0 +1,45 @@
[Unit]
Description=AIOS trusted dynamic engineering consumer
After=aios-agent-task-api.service aios-chatgpt-bridge-v2-staging.service
Wants=aios-agent-task-api.service aios-chatgpt-bridge-v2-staging.service
[Service]
Type=simple
User=aios-engineering-worker
Group=aios-engineering-worker
SupplementaryGroups=gitops aios-executor-exchange
LoadCredentialEncrypted=agent_token:/etc/credstore.encrypted/aios-online-engineering-20260815/agent_token
Environment=PYTHONUNBUFFERED=1
Environment=AIOS_AGENT_TASK_API_URL=http://127.0.0.1:18940
Environment=AIOS_GOVERNED_WRITE_API_URL=http://127.0.0.1:18892
Environment=AIOS_AGENT_ID=online-engineering-consumer
Environment=AIOS_AGENT_TYPE=trusted-online-engineering-worker
Environment=AIOS_AGENT_VERSION=2026-08-17.1-dynamic
Environment=AIOS_WORKER_CAPABILITIES=git.read,git.test,governed.write.branch,governed.write.file
Environment=AIOS_AUTHORITY_GIT_DIR=/data/aios-gitops/repo.git
Environment=AIOS_ISOLATED_EXECUTOR_ROOT=/var/lib/aios-online-agent-executor
Environment=AIOS_WORKER_STATE_ROOT=/var/lib/aios-online-engineering-consumer
Environment=AIOS_WORKER_WORKSPACE_ROOT=/var/lib/aios-online-engineering-consumer/workspaces
ExecStart=/usr/bin/python3 -B /opt/aios/current/active-runtime/app/aios_online_agent_worker.py
Restart=always
RestartSec=5
TimeoutStopSec=20
NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=strict
ProtectHome=true
ProtectKernelTunables=true
ProtectKernelModules=true
ProtectControlGroups=true
LockPersonality=true
RestrictSUIDSGID=true
RestrictRealtime=true
RestrictNamespaces=true
MemoryDenyWriteExecute=true
RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6
ReadOnlyPaths=/data/aios-gitops/repo.git
ReadWritePaths=/var/lib/aios-online-engineering-consumer
UMask=0027
[Install]
WantedBy=multi-user.target
@@ -1,49 +0,0 @@
[Unit]
Description=AIOS durable Owner authorization broker
After=network.target aios-chatgpt-bridge-v2-staging.service
Wants=aios-chatgpt-bridge-v2-staging.service
[Service]
Type=simple
User=root
Group=aios-executor-exchange
RuntimeDirectory=aios-owner-authorization-broker
RuntimeDirectoryMode=0750
StateDirectory=aios-owner-authorization-broker
StateDirectoryMode=0750
LoadCredential=agent_task_control_token:/etc/aios-gitops/agent-task-control.token
Environment=AIOS_AGENT_TASK_CONTROL_TOKEN_FILE=%d/agent_task_control_token
Environment=AIOS_OWNER_DURABLE_AUTHORIZATION_POLICY=/etc/aios-gitops/owner-durable-authorization.json
Environment=AIOS_OWNER_DURABLE_AUTHORIZATION_REVOCATIONS=/etc/aios-gitops/owner-durable-authorization-revocations.json
Environment=AIOS_OWNER_AUTH_BROKER_ACCOUNT=chatgpt-governed-implementer-singlehost
Environment=AIOS_OWNER_AUTH_BROKER_ALLOWED_USERS=aios-autonomous-agent-worker,aiman
Environment=AIOS_OWNER_AUTH_BROKER_SOCKET=%t/aios-owner-authorization-broker/broker.sock
ExecStart=/usr/bin/python3 -B @RUNTIME_ROOT@/active-runtime/app/aios_durable_owner_authorization.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
ReadOnlyPaths=/etc/aios-gitops/owner-durable-authorization.json
ReadOnlyPaths=/etc/aios-gitops/owner-durable-authorization-revocations.json
ReadOnlyPaths=@RUNTIME_ROOT@
InaccessiblePaths=/data/aios-gitops /home
UMask=0007
[Install]
WantedBy=multi-user.target
@@ -0,0 +1,4 @@
g aios-executor-exchange -
u aios-engineering-executor - "AIOS isolated engineering executor" /var/lib/aios-online-agent-executor /usr/sbin/nologin
m aios-engineering-executor aios-executor-exchange
m aios-engineering-worker aios-executor-exchange
@@ -0,0 +1,5 @@
d /var/lib/aios-online-agent-executor 2770 aios-engineering-executor aios-executor-exchange -
d /var/lib/aios-online-agent-executor/requests 2770 aios-engineering-worker aios-executor-exchange -
d /var/lib/aios-online-agent-executor/results 2770 aios-engineering-executor aios-executor-exchange -
d /var/lib/aios-online-agent-executor/failed 2770 aios-engineering-executor aios-executor-exchange -
d /var/lib/aios-online-agent-executor/sources 2770 aios-engineering-worker aios-executor-exchange -
@@ -1,88 +0,0 @@
# Caller admission projection and rehydration
This runbook covers the exact durable Owner policy and the exact API caller:
- caller: \`chatgpt-governed-implementer-singlehost\`
- scope: \`release:candidate\`
- policy source: \`/etc/aios-gitops/owner-durable-authorization.json\`
- revocation source: \`/etc/aios-gitops/owner-durable-authorization-revocations.json\`
## Admission source
The model API first authenticates the existing caller token from the static
registry. For this exact caller and exact release scope, static authentication
is not sufficient. The model API calls the existing Unix-socket Owner broker.
The broker reloads and validates the durable policy on every projection request,
including account, capability, optional expiry, and revocation.
Other callers retain their existing static-scope behavior. The target caller
does not receive a permanent \`release:candidate\` entry in the static registry,
and it is never treated as a release-worker account.
## Projection lifecycle and ACK semantics
The broker records only non-secret projection metadata in its existing durable
state database. The state sequence is:
\`PENDING\` -> \`ACKED\`
The ACK is produced by the broker's server-side projection consumer after the
exact policy and request binding have passed validation. The public response
contains projection state, effective scope, policy digest, expiry and
rehydration state. The short-lived credential material is returned only in a
private Unix-socket response header to the model API process. It is not written
to the response body, logs, diagnostics, Git, task metadata or terminal output.
## Credential rehydration, expiry and revocation
The credential projection is generated in memory for a short TTL and is
discarded after the model API admission check. The durable row stores no raw
credential and no bearer token. After a broker or model API restart, the next
admission request creates a new short-lived credential from the durable
projection metadata; this is reported as \`RECOVERED\`.
Every request reloads the policy. Missing, malformed, expired or revoked policy
fails closed. A caller mismatch, scope mismatch, invalid projection state,
broker outage, missing credential header or unexpected secret field also fails
closed. No static scope fallback is allowed for the target caller.
## Diagnosis
Use the read-only API with the caller's existing credential:
\`GET /api/caller-capabilities\`
The response is safe to display and contains:
- \`caller_identity\`
- \`admitted\`
- \`effective_scopes\`
- \`projection_state\`
- \`credential_state\`
- \`rehydration_state\`
- \`secret_exposed=false\`
It never contains a bearer, token, credential value or credential hash.
For a release candidate request, the model API also binds the exact
\`source_commit\` and \`version\` to the durable policy before invoking the
trusted release service. The caller cannot supply release evidence fields.
## Failure states and troubleshooting
- \`PROJECTION_BROKER_UNAVAILABLE\`: verify the broker unit, socket path and
the model API supplementary groups; do not add a static scope.
- \`OWNER_GRANT_REVOKED\` or \`POLICY_EXPIRED\`: restore a valid Owner policy
through governed policy management; do not edit broker state.
- \`CALLER_MISMATCH\` or \`SCOPE_MISMATCH\`: verify the authenticated account and
requested exact scope.
- \`PROJECTION_STATE_INVALID\`: inspect the durable projection through the
broker's read-only diagnostics and fail closed until it is reconciled.
- \`CREDENTIAL_REHYDRATION_MISSING\`: restart only the related control-plane
service after capturing pre-state and then repeat the read-only diagnosis.
- \`trusted candidate builder is not configured\`: Gate A admission is working,
but the separate existing TrustedCandidateBuilder runtime must be restored
before Gate B can create a candidate.
No Owner Approval, Deployment, Host Seed, Host Activation, Projector,
continuation task, Tag or Baseline action is part of this path.
-295
View File
@@ -1,295 +0,0 @@
openapi: 3.1.0
info:
title: AIOS Online Agent Task API
version: 1.0.0
description: >
Durable online task distribution for OpenClaw, Hermes, and other external
agents. Agent credentials and lease tokens are bearer credentials and must
never be committed to Git.
servers:
- url: /agent-tasks
paths:
/health:
get:
operationId: agentTaskHealth
responses:
"200":
description: Service health
/v1/agents/register:
post:
operationId: registerOnlineAgent
security:
- bearerAuth: []
description: Control-plane only. Returns the agent token once.
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/RegisterAgentRequest"
responses:
"201":
description: Registered
/v1/tasks:
post:
operationId: createOnlineAgentTask
security:
- bearerAuth: []
description: Control-plane only.
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/CreateTaskRequest"
responses:
"201":
description: Task created
/v1/tasks/poll:
post:
operationId: pollOnlineAgentTasks
security:
- bearerAuth: []
description: Agent operation. Returns capability-compatible queued tasks.
requestBody:
content:
application/json:
schema:
type: object
properties:
limit:
type: integer
minimum: 1
maximum: 100
default: 20
responses:
"200":
description: Available tasks
/v1/tasks/{task_id}:
parameters:
- $ref: "#/components/parameters/TaskId"
get:
operationId: getOnlineAgentTaskDirect
security:
- bearerAuth: []
responses:
"200":
description: Task status
/v1/tasks/{task_id}/claim:
parameters:
- $ref: "#/components/parameters/TaskId"
post:
operationId: claimOnlineAgentTask
security:
- bearerAuth: []
requestBody:
content:
application/json:
schema:
type: object
properties:
lease_seconds:
type: integer
minimum: 30
maximum: 3600
default: 300
responses:
"200":
description: >
Claim succeeded. The lease_token is returned once and must be kept
in runtime memory/secret storage, never Git.
"409":
description: Already claimed or otherwise not claimable
/v1/tasks/{task_id}/heartbeat:
parameters:
- $ref: "#/components/parameters/TaskId"
post:
operationId: heartbeatOnlineAgentTask
security:
- bearerAuth: []
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/HeartbeatRequest"
responses:
"200":
description: Lease renewed
/v1/tasks/{task_id}/result:
parameters:
- $ref: "#/components/parameters/TaskId"
post:
operationId: submitOnlineAgentResult
security:
- bearerAuth: []
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/ResultRequest"
responses:
"200":
description: Result accepted into VERIFYING
/v1/tasks/{task_id}/verify:
parameters:
- $ref: "#/components/parameters/TaskId"
post:
operationId: verifyOnlineAgentTaskDirect
security:
- bearerAuth: []
description: Control-plane only.
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/VerifyRequest"
responses:
"200":
description: Verification decision applied
/v1/tasks/{task_id}/cancel:
parameters:
- $ref: "#/components/parameters/TaskId"
post:
operationId: cancelOnlineAgentTaskDirect
security:
- bearerAuth: []
description: Control-plane only.
requestBody:
content:
application/json:
schema:
type: object
properties:
reason:
type: string
responses:
"200":
description: Cancelled or already terminal
/v1/tasks/{task_id}/events:
parameters:
- $ref: "#/components/parameters/TaskId"
get:
operationId: getOnlineAgentTaskEventsDirect
security:
- bearerAuth: []
description: Control-plane only.
responses:
"200":
description: Audit event list
components:
securitySchemes:
bearerAuth:
type: http
scheme: bearer
parameters:
TaskId:
name: task_id
in: path
required: true
schema:
type: string
pattern: "^[A-Za-z0-9][A-Za-z0-9._:-]{2,179}$"
schemas:
RegisterAgentRequest:
type: object
additionalProperties: false
required: [agent_id, agent_type, agent_version, capabilities]
properties:
agent_id:
type: string
agent_type:
type: string
examples: [openclaw]
agent_version:
type: string
capabilities:
type: array
items:
type: string
metadata:
type: object
additionalProperties: true
CreateTaskRequest:
type: object
additionalProperties: false
required:
- goal
- instructions
- success_criteria
- idempotency_key
properties:
task_id:
type: string
goal:
type: string
instructions:
type: string
success_criteria:
type: array
items: {}
required_capabilities:
type: array
items:
type: string
constraints:
type: object
additionalProperties: true
metadata:
type: object
additionalProperties: true
deadline:
type: string
format: date-time
priority:
type: integer
minimum: -100
maximum: 100
default: 0
max_attempts:
type: integer
minimum: 1
maximum: 20
default: 3
idempotency_key:
type: string
HeartbeatRequest:
type: object
additionalProperties: false
required: [lease_token]
properties:
lease_token:
type: string
lease_seconds:
type: integer
minimum: 30
maximum: 3600
default: 300
progress:
type: object
additionalProperties: true
ResultRequest:
type: object
additionalProperties: false
required: [lease_token, idempotency_key, result]
properties:
lease_token:
type: string
idempotency_key:
type: string
result:
type: object
additionalProperties: true
VerifyRequest:
type: object
additionalProperties: false
required: [decision]
properties:
decision:
type: string
enum: [SUCCEEDED, NEEDS_REWORK, FAILED]
verification:
type: object
additionalProperties: true
File diff suppressed because it is too large Load Diff
@@ -1,326 +0,0 @@
openapi: 3.1.0
info:
title: AIOS ChatGPT Governed Write and Agent Task Extension
version: 1.0.0
description: >
Merge these paths and schemas into docs/gitops/openapi-chatgpt-actions.yaml.
This extension deliberately exposes governed Bridge v2 operations rather
than reopening legacy AIOS_GITOPS_WRITES_ENABLED.
paths:
/api/governed-write-capabilities:
get:
operationId: getGovernedWriteCapabilities
responses:
"200":
description: Governed and legacy write capability status
/api/control-plane-snapshot:
get:
operationId: getControlPlaneSnapshot
responses:
"200":
description: Non-sensitive control-plane snapshot
/createGovernedTask:
post:
operationId: createGovernedTask
x-openai-isConsequential: true
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/CreateGovernedTaskRequest"
responses:
"200":
description: Governed task binding
/acquireWriterLease:
post:
operationId: acquireWriterLease
x-openai-isConsequential: true
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/WriterLeaseRequest"
responses:
"200":
description: Writer lease binding
/commitFileGoverned:
post:
operationId: commitFileGoverned
x-openai-isConsequential: true
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/CommitFileGovernedRequest"
responses:
"200":
description: Durable GitOps job receipt
/commitFilesGoverned:
post:
operationId: commitFilesGoverned
x-openai-isConsequential: true
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/CommitFilesGovernedRequest"
responses:
"200":
description: Durable GitOps job receipt
/getGitOpsJob:
post:
operationId: getGitOpsJob
requestBody:
required: true
content:
application/json:
schema:
type: object
required: [job_id]
properties:
job_id: {type: string}
responses:
"200":
description: Job receipt
/resumeGitOpsJob:
post:
operationId: resumeGitOpsJob
x-openai-isConsequential: true
requestBody:
required: true
content:
application/json:
schema:
type: object
required: [job_id, expected_revision]
properties:
job_id: {type: string}
expected_revision: {type: integer}
responses:
"200":
description: Resumed job receipt
/reconcileGitOpsJob:
post:
operationId: reconcileGitOpsJob
x-openai-isConsequential: true
requestBody:
required: true
content:
application/json:
schema:
type: object
required: [job_id, expected_revision]
properties:
job_id: {type: string}
expected_revision: {type: integer}
responses:
"200":
description: Reconciliation receipt
/createOnlineAgentTask:
post:
operationId: createOnlineAgentTask
x-openai-isConsequential: true
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/CreateOnlineAgentTaskRequest"
responses:
"200":
description: Online task created
"201":
description: Online task created
/registerOnlineAgent:
post:
operationId: registerOnlineAgent
x-openai-isConsequential: true
description: >
Control operation. The returned agent token is secret material and must
be delivered through runtime secret storage, never Git.
requestBody:
required: true
content:
application/json:
schema:
type: object
required: [agent_id, agent_type, agent_version, capabilities]
properties:
agent_id: {type: string}
agent_type: {type: string}
agent_version: {type: string}
capabilities:
type: array
items: {type: string}
metadata:
type: object
additionalProperties: true
responses:
"201":
description: Agent registered
/api/online-agent-task:
get:
operationId: getOnlineAgentTask
parameters:
- name: task_id
in: query
required: true
schema: {type: string}
responses:
"200":
description: Task status
/api/online-agent-task-events:
get:
operationId: getOnlineAgentTaskEvents
parameters:
- name: task_id
in: query
required: true
schema: {type: string}
responses:
"200":
description: Task events
/verifyOnlineAgentTask:
post:
operationId: verifyOnlineAgentTask
x-openai-isConsequential: true
requestBody:
required: true
content:
application/json:
schema:
type: object
required: [task_id, decision]
properties:
task_id: {type: string}
decision:
type: string
enum: [SUCCEEDED, NEEDS_REWORK, FAILED]
verification:
type: object
additionalProperties: true
responses:
"200":
description: Verification applied
/cancelOnlineAgentTask:
post:
operationId: cancelOnlineAgentTask
x-openai-isConsequential: true
requestBody:
required: true
content:
application/json:
schema:
type: object
required: [task_id]
properties:
task_id: {type: string}
reason: {type: string}
responses:
"200":
description: Task cancelled
components:
schemas:
Oid40:
type: string
pattern: "^[0-9a-f]{40}$"
CreateGovernedTaskRequest:
type: object
additionalProperties: false
required:
[task_id, repository, base_oid, target_branch, risk, description, idempotency_key]
properties:
task_id: {type: string}
repository: {type: string, const: aios/aios}
base_oid: {$ref: "#/components/schemas/Oid40"}
target_branch:
type: string
pattern: "^agent/"
risk:
type: string
enum: [low, medium, high, engineering]
description: {type: string}
idempotency_key: {type: string}
expected_branch_oid: {$ref: "#/components/schemas/Oid40"}
generation: {type: integer, minimum: 1}
WriterLeaseRequest:
type: object
additionalProperties: false
required:
[schema, task_id, repository, branch_mode, base_oid, expected_branch_oid,
target_branch, generation, lease_seconds]
properties:
schema: {type: string}
task_id: {type: string}
repository: {type: string, const: aios/aios}
branch_mode:
type: string
enum: [CREATE_NEW, CONTINUE_EXISTING]
base_oid: {$ref: "#/components/schemas/Oid40"}
expected_branch_oid: {$ref: "#/components/schemas/Oid40"}
target_branch: {type: string, pattern: "^agent/"}
generation: {type: integer, minimum: 1}
lease_seconds: {type: integer, minimum: 1, maximum: 86400}
CommitFileGovernedRequest:
type: object
additionalProperties: false
required:
[task_id, repository, expected_branch_oid, target_branch, generation,
idempotency_key, path, content_base64, message]
properties:
task_id: {type: string}
repository: {type: string, const: aios/aios}
expected_branch_oid: {$ref: "#/components/schemas/Oid40"}
target_branch: {type: string, pattern: "^agent/"}
generation: {type: integer, minimum: 1}
idempotency_key: {type: string}
path: {type: string}
content_base64: {type: string}
message: {type: string}
CommitFilesGovernedRequest:
type: object
additionalProperties: false
required:
[task_id, repository, expected_branch_oid, target_branch, generation,
idempotency_key, files, message]
properties:
task_id: {type: string}
repository: {type: string, const: aios/aios}
expected_branch_oid: {$ref: "#/components/schemas/Oid40"}
target_branch: {type: string, pattern: "^agent/"}
generation: {type: integer, minimum: 1}
idempotency_key: {type: string}
files:
type: object
additionalProperties:
type: string
description: Base64 encoded file bytes
message: {type: string}
CreateOnlineAgentTaskRequest:
type: object
additionalProperties: false
required: [goal, instructions, success_criteria, idempotency_key]
properties:
task_id: {type: string}
goal: {type: string}
instructions: {type: string}
success_criteria:
type: array
items: {}
required_capabilities:
type: array
items: {type: string}
constraints:
type: object
additionalProperties: true
metadata:
type: object
additionalProperties: true
deadline: {type: string, format: date-time}
priority: {type: integer, minimum: -100, maximum: 100}
max_attempts: {type: integer, minimum: 1, maximum: 20}
idempotency_key: {type: string}
@@ -0,0 +1,134 @@
# Online Agent Automatic Claim Infrastructure
## Status and fixed boundary
This candidate targets `p0-claim-online-agent-auto-execution-20260817` on governed Job
`job-2031a9e5682a4238f98e3ac7aee8a593`, base
`a642b045775b9154a00ae78543037fbef4fb447b`, target branch
`agent/p0-claim-online-agent-auto-execution-20260817`, generation 1.
It does not authorize protected-main, Production, tag, baseline, raw Git, Task DB, or
credential mutations.
## Considered approaches
1. Require every caller to write `metadata.execution`. This is simple but preserves the
current operational gap: a valid governed Job can still project an unexecutable Online
Task.
2. Let workers infer repository and WriterLease coordinates from natural-language Task
fields. This removes the skip but violates fail-closed lineage and can bind the wrong Job,
branch, base, or generation.
3. Use a canonical server-side projector plus a task-agnostic worker resolver. This is the
selected approach. The governed binding remains the only authority for repository, Job,
branch, base, expected head, generation, and idempotency. Producers can persist the
projected envelope when creating the Online Task. A worker may recover an omitted envelope
only from the same canonical projector and durable binding; it never guesses coordinates.
## Components
### Canonical execution contract
`aios_online_agent_execution.py` owns schema `aios.online-agent.execution.v2`. It validates
the complete governed lineage and supports two explicit actions:
- `deterministic`: a hash-bound change set, verification commands, and governed commit;
- `source_capture`: a read-only first attempt that returns `NEEDS_REWORK_SOURCE_CAPTURE` and
cannot acquire WriterLease or mutate Git.
Wrong task, Job, repository, base, expected head, branch, generation, capability scope, or
governance policy fails closed with a canonical diagnostic.
### Governed Job to Online Task projection
`GovernedTaskBootstrap` exposes an exact durable binding lookup and builds Online Task
projection data through the canonical contract. When the Bridge is configured with the
official Agent Task control credential reference, governed Job creation submits that complete
projection through the official Task HTTP API. The Bridge also exposes a loopback envelope
projection operation used for recovery. The persisted projection includes the versioned
envelope rather than relying on manual metadata edits.
Projection delivery is a durable outbox, not a best-effort tail call. The outbox record is
written before HTTP delivery and a Bridge reconciler retries it after transient API failure or
process restart. A response is accepted only when its complete execution envelope equals the
submitted projection. Requests that explicitly require projection are rejected before governed
bootstrap when the projection principal is unavailable.
Loopback is not treated as an identity boundary. Every POST route requires an exact
runtime-principal bearer credential supplied by systemd credential projection; unauthenticated
local callers are denied before request dispatch. The control-plane credential used to create
Online Tasks remains a separate Bridge-only reference and is never projected into the worker.
For legacy Online Tasks with an omitted envelope, the generic worker asks the same Bridge
operation for a read-only `source_capture` envelope. Projection succeeds only when the exact
durable binding exists. An omitted envelope is therefore recoverable; an invalid envelope is
never replaced or weakened.
Persisted envelopes are also reprojected through the Bridge before claim and compared by exact
digest and Job lineage. A self-consistent client-generated envelope is therefore not authority.
### Dynamic worker and trusted consumer
The repository gains a deployable generic worker and systemd unit definitions as governed
source files. The worker validates or
resolves an envelope before claim, revalidates after atomic claim, starts heartbeat, and then
executes the selected action. The trusted engineering consumer uses this same task-agnostic
entrypoint. No compiled worker contains a Task ID, base OID, branch, generation, or plan path.
Skip observability distinguishes:
- `TASK_SKIPPED_PROJECTION_MISSING`;
- `TASK_SKIPPED_ENVELOPE_INVALID`;
- `TASK_SKIPPED_CAPABILITY_MISMATCH`;
- `TASK_SKIPPED_POLICY_DENY`;
- `TASK_POLL_FAILED` / worker unavailable.
## State flow
```text
Governed Job BOUND
-> canonical durable binding
-> Online Task projection with execution.v2
-> capability-filtered poll
-> envelope and lineage validation
-> atomic claim
-> initial heartbeat/start
-> refetch Task and re-resolve envelope
-> compare execution digest and Job
-> source capture OR deterministic governed execution
-> result construction
-> result submission
-> external server-side verification gate
```
Legacy omission uses `binding -> source_capture projection` between poll and validation. It
does not write Task metadata and does not convert a malformed envelope into a valid one.
## Recovery and safety
- Projection is deterministic and digest-bound.
- Governed Task creation is serialized by a cross-process, Task-ID-digest lock. Durable paths
use collision-resistant exact Task keys while retaining read compatibility with verified
legacy bindings.
- A new Task cannot adopt an existing branch from caller-selected generation data. It must name
an exact durable parent Task/Job whose single ACTIVE lease owns the branch head; generation is
then derived from that lease.
- Claim is attempted only after validation; claimed payload drift is denied.
- Source capture has no WriterLease or commit path.
- Deterministic execution retains detached worktree, exact WriterLease, governed commit,
durable current-head CAS, and receipt convergence.
- Local deterministic recovery is keyed by Task and execution digest; live acceptance must
independently reconverge Job, lease, receipt, and branch OIDs before verification.
- An ambiguous commit transport or server outcome becomes `RECONCILIATION_REQUIRED`; the
worker does not submit a canonical failure until exact Job receipt and head evidence is
observed.
- A completed read-only `source_capture` state is ignored when a later attempt has a changed
digest and no governed state exists; all other digest drift is denied.
- Protected refs are never worker targets.
## Acceptance
The implementation must pass the named projection, schema, claimability, dynamic-binding,
wrong-lineage, capability, P0-A, and P0-B tests. Runtime acceptance additionally requires a
real Online Task to be claimed and heartbeated without per-task worker or systemd plan
injection, plus governed receipt/OID/CAS convergence for this implementation Task.
Unit tests and deployable unit definitions do not by themselves satisfy runtime acceptance.
@@ -0,0 +1,56 @@
# P0-CLAIM Scheme A follow-up
This follow-up continues the existing candidate `71523aef6e48b1e2fa516c456ca189b7a039104b`.
It does not create a replacement Task, Job, branch or generation.
## Retry extension
A versioned Owner-controlled loopback API provides
`POST /v1/tasks/{task_id}/retry-extension`.
The request binds FAILED status, revision, exhausted attempt/max_attempts, next max attempts,
previous verification SHA256, Owner authorization ID and idempotency key. The service performs one
SQLite `BEGIN IMMEDIATE` transaction containing the CAS update, durable retry-extension record and
Task event. Each authorization extends the boundary by exactly one attempt.
## Same-Job continuation
The durable GovernedTask binding remains the lineage anchor. After the first CREATE_NEW commit,
`execution_binding()` may normalize that SAME Job to `CONTINUE_EXISTING` only when exactly one
ACTIVE WriterLease for the Job owns the current authority branch head, generation and lineage
anchor. The lineage `base_oid` stays unchanged; only `expected_branch_oid` becomes the current head.
GovernedTask creation is also serialized by a branch-digest lock, preventing sibling continuation
tasks from racing generation admission.
## Projection and recovery
Natural-language source signals are normalized server-side to at most 16 unique 512-byte values.
Malformed persisted envelopes still fail closed.
Fresh commits and recovery require a COMPLETED Job, `remote_verified=true`, exact
authority/API/remote receipt OIDs, and exactly one ACTIVE WriterLease whose durable current head
matches the commit. Delayed local ref visibility becomes `RECONCILIATION_REQUIRED`, not a terminal
canonical failure.
## Credential isolation
The credentialed Coordinator performs only Task/Bridge coordination and governed commit. All
deterministic repository modification and test commands run in
`aios-online-agent-isolated-executor` under a separate system user with:
- no Agent/Owner credential projection;
- no gitops supplementary group;
- `PrivateNetwork=true`;
- `RestrictAddressFamilies=AF_UNIX AF_INET`, with `IPAddressDeny=any` and
`IPAddressAllow=localhost`, so regression tests may use an isolated loopback
HTTP server without gaining host or external network reachability;
- `InaccessiblePaths=/run/credentials`.
The executor receives a hash-bound source archive and deterministic change set through a filesystem
exchange and returns only changed file bytes/hashes plus command evidence. The Coordinator validates
that response before materializing exact allowed files and calling the governed Bridge.
Codex/Hermes mutation adapters remain disabled until a separately audited uncredentialed planner
backend exists. Under the current operating model, implementation code is authored before Online
Agent execution and is carried as a deterministic plan.
@@ -1,20 +0,0 @@
{
"execution_id": "exec_d75446452723",
"user_id": "user1",
"visible_scope": "group1",
"entry_agent": "openclaw",
"path": [
"openclaw",
"hermes",
"codex"
],
"used_hermes": true,
"closed_by": "codex",
"duration_ms": 0,
"input_tokens": 100,
"output_tokens": 200,
"total_tokens": 300,
"status": "completed",
"started_at": "2026-08-09T17:13:34.996470+00:00",
"ended_at": "2026-08-09T17:13:34.996489+00:00"
}
-2
View File
@@ -1,2 +0,0 @@
{"task_id": "task-command_watcher-CMD-test", "title": "CMD-test", "assigned_by": "command-watcher", "executor": "openclaw", "source": "command_watcher", "status": "pending", "priority": "normal", "created_at": "2026-08-09T17:13:47.285193+00:00", "updated_at": "2026-08-09T17:13:47.285198+00:00", "progress": {"percent": 5, "current_step": "pending", "completed_steps": [], "remaining_steps": [], "blocked_reason": null}, "notification": {"required": true, "sent": false, "channel": null, "sent_at": null}, "evidence": ["ops/openclaw/CMD-test.md"], "metadata": {"bucket": "active"}}
{"task_id": "task-agent_execution-e1", "title": "Agent execution e1", "assigned_by": "hermes", "executor": "hermes", "source": "agent_execution", "status": "completed", "priority": "normal", "created_at": "2026-08-09T17:13:47.285822+00:00", "updated_at": "2026-08-09T17:13:47.285826+00:00", "progress": {"percent": 100, "current_step": "completed", "completed_steps": [], "remaining_steps": [], "blocked_reason": null}, "notification": {"required": true, "sent": false, "channel": null, "sent_at": null}, "evidence": ["e1"], "metadata": {"execution_id": "e1", "entry_agent": "hermes", "status": "completed"}}
-112
View File
@@ -1,112 +0,0 @@
#!/usr/bin/env python3
"""Small CLI used by OpenClaw/Hermes operators to validate AIOS online tasks."""
from __future__ import annotations
import argparse
import json
from pathlib import Path
import sys
from urllib.error import HTTPError, URLError
from urllib.request import Request, urlopen
def read_token(value: str | None, path: str | None) -> str:
if value:
return value.strip()
if path:
return Path(path).read_text(encoding="utf-8").strip()
raise SystemExit("token or token-file is required")
def request(base: str, method: str, path: str, token: str, body=None):
payload = None if body is None else json.dumps(body, ensure_ascii=False).encode("utf-8")
req = Request(
base.rstrip("/") + path,
data=payload,
method=method,
headers={
"Authorization": "Bearer " + token,
"Accept": "application/json",
**({"Content-Type": "application/json"} if payload is not None else {}),
},
)
try:
with urlopen(req, timeout=30) as response:
return response.status, json.loads(response.read().decode("utf-8"))
except HTTPError as exc:
raw = exc.read()
try:
value = json.loads(raw.decode("utf-8"))
except Exception:
value = {"error": "HTTP_" + str(exc.code)}
return exc.code, value
except (URLError, TimeoutError) as exc:
raise SystemExit("network_error:" + type(exc).__name__) from exc
def main() -> int:
ap = argparse.ArgumentParser()
ap.add_argument("--base", required=True)
ap.add_argument("--token")
ap.add_argument("--token-file")
sub = ap.add_subparsers(dest="command", required=True)
p = sub.add_parser("poll")
p.add_argument("--limit", type=int, default=20)
p = sub.add_parser("claim")
p.add_argument("task_id")
p.add_argument("--lease-seconds", type=int, default=300)
p = sub.add_parser("heartbeat")
p.add_argument("task_id")
p.add_argument("--lease-token", required=True)
p.add_argument("--lease-seconds", type=int, default=300)
p.add_argument("--progress-json", default="{}")
p = sub.add_parser("result")
p.add_argument("task_id")
p.add_argument("--lease-token", required=True)
p.add_argument("--idempotency-key", required=True)
p.add_argument("--result-json", required=True)
p = sub.add_parser("status")
p.add_argument("task_id")
args = ap.parse_args()
token = read_token(args.token, args.token_file)
if args.command == "poll":
status, value = request(args.base, "POST", "/v1/tasks/poll", token, {"limit": args.limit})
elif args.command == "claim":
status, value = request(
args.base, "POST", f"/v1/tasks/{args.task_id}/claim", token,
{"lease_seconds": args.lease_seconds},
)
elif args.command == "heartbeat":
status, value = request(
args.base, "POST", f"/v1/tasks/{args.task_id}/heartbeat", token,
{
"lease_token": args.lease_token,
"lease_seconds": args.lease_seconds,
"progress": json.loads(args.progress_json),
},
)
elif args.command == "result":
status, value = request(
args.base, "POST", f"/v1/tasks/{args.task_id}/result", token,
{
"lease_token": args.lease_token,
"idempotency_key": args.idempotency_key,
"result": json.loads(args.result_json),
},
)
else:
status, value = request(args.base, "GET", f"/v1/tasks/{args.task_id}", token)
print(json.dumps({"http_status": status, "response": value}, ensure_ascii=False, indent=2))
return 0 if 200 <= status < 300 else 1
if __name__ == "__main__":
raise SystemExit(main())
-193
View File
@@ -1,193 +0,0 @@
#!/usr/bin/env python3
"""End-to-end online task feasibility smoke test.
Runs through the public agent path plus control operations:
create -> poll -> claim -> heartbeat -> result -> verify -> readback.
It never performs Git, tag, Baseline, or production mutations.
"""
from __future__ import annotations
import argparse
from datetime import datetime, timezone
import json
from pathlib import Path
import secrets
from urllib.error import HTTPError, URLError
from urllib.request import Request, urlopen
def token(path: Path) -> str:
value = path.read_text(encoding="utf-8").strip()
if len(value) < 32:
raise SystemExit("credential_invalid:" + str(path))
return value
def call(base: str, method: str, route: str, credential: str, body=None):
payload = None if body is None else json.dumps(body, ensure_ascii=False).encode("utf-8")
headers = {
"Authorization": "Bearer " + credential,
"Accept": "application/json",
}
if payload is not None:
headers["Content-Type"] = "application/json"
request = Request(base.rstrip("/") + route, data=payload, headers=headers, method=method)
try:
with urlopen(request, timeout=30) as response:
return response.status, json.loads(response.read().decode("utf-8"))
except HTTPError as exc:
try:
value = json.loads(exc.read().decode("utf-8"))
except Exception:
value = {"error": "HTTP_" + str(exc.code)}
return exc.code, value
except (URLError, TimeoutError) as exc:
raise SystemExit("network_error:" + type(exc).__name__) from exc
def require(step: str, status: int, value, expected_status: str | None = None):
if not 200 <= status < 300:
raise SystemExit(f"{step}_failed:http={status}:body={json.dumps(value, ensure_ascii=False)}")
if expected_status and value.get("status") != expected_status:
raise SystemExit(
f"{step}_state_failed:expected={expected_status}:"
f"observed={value.get('status')}"
)
print(json.dumps({"step": step, "http": status, "response": value}, ensure_ascii=False))
def main() -> int:
parser = argparse.ArgumentParser()
parser.add_argument("--control-base", required=True,
help="Loopback/model-control base that exposes /v1 control routes")
parser.add_argument("--agent-base", required=True,
help="Public agent base, e.g. https://host/agent-tasks")
parser.add_argument("--control-token-file", required=True, type=Path)
parser.add_argument("--agent-token-file", required=True, type=Path)
args = parser.parse_args()
control = token(args.control_token_file)
agent = token(args.agent_token_file)
task_id = "TASK-ONLINE-SMOKE-" + datetime.now(timezone.utc).strftime("%Y%m%dT%H%M%SZ") + "-" + secrets.token_hex(3)
create_key = "smoke-create:" + task_id
result_key = "smoke-result:" + task_id
s, v = call(
args.control_base, "POST", "/v1/tasks", control,
{
"task_id": task_id,
"goal": "Validate AIOS online task distribution end to end",
"instructions": (
"Do not mutate Git or production. Return a structured result "
"containing marker ONLINE_TASK_FLOW_OK."
),
"success_criteria": [
{"field": "marker", "equals": "ONLINE_TASK_FLOW_OK"},
{"field": "agent_observed_task_id", "equals": task_id},
],
"required_capabilities": ["online-task-smoke"],
"constraints": {
"git_mutation": False,
"production_mutation": False,
"network_scope": "AIOS task endpoint only",
},
"priority": 100,
"max_attempts": 3,
"idempotency_key": create_key,
},
)
require("create", s, v, "QUEUED")
s, v = call(args.agent_base, "POST", "/v1/tasks/poll", agent, {"limit": 100})
require("poll", s, v)
if task_id not in [item.get("task_id") for item in v.get("tasks", [])]:
raise SystemExit("poll_failed:created_task_not_visible")
s, v = call(
args.agent_base, "POST", f"/v1/tasks/{task_id}/claim", agent,
{"lease_seconds": 120},
)
require("claim", s, v, "CLAIMED")
lease = v.get("lease_token")
if not lease:
raise SystemExit("claim_failed:lease_token_missing")
s, v = call(
args.agent_base, "POST", f"/v1/tasks/{task_id}/heartbeat", agent,
{
"lease_token": lease,
"lease_seconds": 120,
"progress": {"stage": "executing", "percent": 50},
},
)
require("heartbeat", s, v, "RUNNING")
s, v = call(
args.agent_base, "POST", f"/v1/tasks/{task_id}/result", agent,
{
"lease_token": lease,
"idempotency_key": result_key,
"result": {
"status": "completed",
"marker": "ONLINE_TASK_FLOW_OK",
"agent_observed_task_id": task_id,
"summary": "Agent received, claimed, heartbeated and returned the online task.",
"artifacts": [],
},
},
)
require("result", s, v, "VERIFYING")
# Re-submit the identical result to prove idempotency.
s2, v2 = call(
args.agent_base, "POST", f"/v1/tasks/{task_id}/result", agent,
{
"lease_token": lease,
"idempotency_key": result_key,
"result": {
"status": "completed",
"marker": "ONLINE_TASK_FLOW_OK",
"agent_observed_task_id": task_id,
"summary": "Agent received, claimed, heartbeated and returned the online task.",
"artifacts": [],
},
},
)
require("result_idempotent_replay", s2, v2, "VERIFYING")
if v2.get("idempotent") is not True:
raise SystemExit("result_idempotency_failed")
s, v = call(
args.control_base, "POST", f"/v1/tasks/{task_id}/verify", control,
{
"decision": "SUCCEEDED",
"verification": {
"marker_match": "PASS",
"task_id_match": "PASS",
"online_round_trip": "PASS",
},
},
)
require("verify", s, v, "SUCCEEDED")
s, v = call(args.control_base, "GET", f"/v1/tasks/{task_id}", control)
require("final_control_readback", s, v, "SUCCEEDED")
# The executing Agent must also be able to observe the final terminal state.
s, v = call(args.agent_base, "GET", f"/v1/tasks/{task_id}", agent)
require("final_agent_readback", s, v, "SUCCEEDED")
print(json.dumps({
"status": "ONLINE_TASK_FLOW_VERIFIED",
"task_id": task_id,
"final_state": v.get("status"),
"force_push_used": False,
"tag_rewritten": False,
"direct_main_push": False,
"production_changed": False,
}, ensure_ascii=False, indent=2))
return 0
if __name__ == "__main__":
raise SystemExit(main())
-100
View File
@@ -1,100 +0,0 @@
#!/usr/bin/env python3
"""Build deterministic immutable staging artifact for AIOS Agent Task API."""
from __future__ import annotations
import argparse
import hashlib
import json
from pathlib import Path
import tarfile
import tempfile
REQUIRED = (
"active-runtime/app/aios_agent_task_protocol.py",
"active-runtime/app/aios_agent_task_api.py",
"docs/gitops/openapi-agent-task-v1.yaml",
)
def sha256(path: Path) -> str:
digest = hashlib.sha256()
with path.open("rb") as source:
for chunk in iter(lambda: source.read(1024 * 1024), b""):
digest.update(chunk)
return digest.hexdigest()
def main() -> int:
parser = argparse.ArgumentParser()
parser.add_argument("--source-root", required=True, type=Path)
parser.add_argument("--output-root", required=True, type=Path)
parser.add_argument("--source-oid", required=True)
args = parser.parse_args()
oid = args.source_oid.strip().lower()
if len(oid) != 40 or any(ch not in "0123456789abcdef" for ch in oid):
raise SystemExit("source oid must be exact 40 hex")
source = args.source_root.resolve(strict=True)
output = args.output_root.resolve()
for relative in REQUIRED:
if not (source / relative).is_file():
raise SystemExit("required source missing: " + relative)
output.mkdir(parents=True, exist_ok=True)
archive = output / f"aios-agent-task-api-{oid}.tar.gz"
manifest = {
"schema": "aios.agent-task-artifact.v1",
"source_oid": oid,
"repository_identity": "aios/aios",
"deployment_state": "STAGING",
"runtime_mode": "IMMUTABLE_STAGING",
"entrypoint": "app/aios_agent_task_api.py",
"listen": "127.0.0.1:18940",
"plaintext_credentials_in_artifact": False,
"included_paths": list(REQUIRED),
}
with tempfile.TemporaryDirectory(prefix="agent-task-build-", dir=str(output)) as temp:
stage = Path(temp) / "runtime"
for relative in REQUIRED:
src = source / relative
if relative.startswith("active-runtime/"):
dest = stage / Path(relative).relative_to("active-runtime")
else:
dest = stage / relative
dest.parent.mkdir(parents=True, exist_ok=True)
dest.write_bytes(src.read_bytes())
(stage / "manifest.json").write_text(
json.dumps(manifest, sort_keys=True, indent=2) + "\n",
encoding="utf-8",
)
with tarfile.open(archive, "w:gz", format=tarfile.PAX_FORMAT) as tar:
for path in sorted(stage.rglob("*")):
arcname = str(path.relative_to(stage.parent)).replace("\\", "/")
info = tar.gettarinfo(str(path), arcname=arcname)
info.mtime = 0
info.uid = info.gid = 0
info.uname = info.gname = "root"
if path.is_file():
with path.open("rb") as fh:
tar.addfile(info, fh)
else:
tar.addfile(info)
receipt = {
**manifest,
"artifact_path": str(archive),
"artifact_sha256": sha256(archive),
}
receipt_path = archive.with_suffix(archive.suffix + ".receipt.json")
receipt_path.write_text(
json.dumps(receipt, sort_keys=True, indent=2) + "\n",
encoding="utf-8",
)
print(json.dumps(receipt, sort_keys=True, indent=2))
return 0
if __name__ == "__main__":
raise SystemExit(main())
+9
View File
@@ -20,3 +20,12 @@ def test_unified_event_flow():
dashboard = AgentEventDashboard(bus).overview()
assert dashboard["audit"]["enabled"] is True
def test_target_changes_event_identity(tmp_path):
bus = AgentEventBus(tmp_path)
first = bus.publish('test', 'same', 'user-a', 'same summary', {'value': 1})
second = bus.publish('test', 'same', 'user-b', 'same summary', {'value': 1})
first_id = first.get('id') if isinstance(first, dict) else getattr(first, 'id', first)
second_id = second.get('id') if isinstance(second, dict) else getattr(second, 'id', second)
assert first_id != second_id
@@ -1,48 +0,0 @@
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
def test_model_api_exposes_governed_writer_without_legacy_global_enable():
source = (ROOT / "active-runtime/app/aios_model_api_staging.py").read_text(encoding="utf-8")
assert "'/createGovernedTask': ('/v2/governed-task'" in source
assert "'/commitFilesGoverned': ('/v2/commit-files'" in source
assert "path == '/api/governed-write-capabilities'" in source
assert "AIOS_GITOPS_WRITES_ENABLED" not in source or "legacy_global_writes_enabled" in source
assert "'writes_enabled': False" in source
assert "bridge_v2_json" in source
def test_model_api_routes_online_task_control_through_loopback_credential():
source = (ROOT / "active-runtime/app/aios_model_api_staging.py").read_text(encoding="utf-8")
assert "AIOS_AGENT_TASK_API_URL" in source
assert "AIOS_AGENT_TASK_CONTROL_TOKEN_FILE" in source
assert "/createOnlineAgentTask" in source
assert "/verifyOnlineAgentTask" in source
assert "/cancelOnlineAgentTask" in source
assert "agent-task:control" in source
def test_online_task_service_keeps_control_endpoints_off_public_nginx():
nginx = (ROOT / "deploy/nginx/aios-agent-task-api.conf.example").read_text(encoding="utf-8")
assert "/agent-tasks/v1/tasks/poll" in nginx
assert "(claim|heartbeat|result)" in nginx
assert "/v1/agents/register" not in nginx.replace(
"# Control endpoints (/v1/agents/register, POST /v1/tasks, verify/cancel/events)", ""
)
def test_online_task_systemd_uses_runtime_credential_file():
unit = (ROOT / "deploy/systemd/aios-agent-task-api.service").read_text(encoding="utf-8")
assert "LoadCredential=control_token:" in unit
assert "/run/credentials/aios-agent-task-api.service/control_token" in unit
assert "ReadWritePaths=/var/lib/aios-agent-task" in unit
assert "127.0.0.1" in unit
def test_control_plane_snapshot_is_local_aggregate_not_missing_bridge_route():
source = (ROOT / "active-runtime/app/aios_model_api_staging.py").read_text(encoding="utf-8")
assert "def control_plane_snapshot(self):" in source
assert "'snapshot_source': 'model-api-local-aggregate'" in source
assert "return self.control_plane_snapshot()" in source
assert "bridge_v2_json('/v2/control-plane-snapshot'" not in source
-317
View File
@@ -1,317 +0,0 @@
from __future__ import annotations
from datetime import datetime, timedelta, timezone
from pathlib import Path
import threading
import pytest
from aios_agent_task_protocol import TaskProtocolError, TaskStore
class Clock:
def __init__(self) -> None:
self.value = datetime(2026, 8, 9, 12, 0, tzinfo=timezone.utc)
def __call__(self) -> datetime:
return self.value
def advance(self, seconds: int) -> None:
self.value += timedelta(seconds=seconds)
@pytest.fixture
def env(tmp_path: Path):
clock = Clock()
store = TaskStore(tmp_path / "tasks.sqlite3", now=clock, token_pepper="test-pepper")
a1 = store.register_agent(
agent_id="openclaw-1",
agent_type="openclaw",
agent_version="test",
capabilities=["shell", "git", "python"],
)
a2 = store.register_agent(
agent_id="hermes-1",
agent_type="hermes",
agent_version="test",
capabilities=["shell", "git", "python"],
)
return store, clock, a1, a2
def create_task(store: TaskStore, task_id: str = "TASK-ONLINE-001"):
return store.create_task(
task_id=task_id,
goal="Create a deterministic validation artifact",
instructions="Write validation.txt containing ONLINE_TASK_OK.",
success_criteria=[
{"type": "file_exists", "path": "validation.txt"},
{"type": "contains", "path": "validation.txt", "value": "ONLINE_TASK_OK"},
],
required_capabilities=["shell"],
constraints={"network": "optional"},
metadata={"suite": "online-task"},
priority=10,
max_attempts=3,
idempotency_key="create:" + task_id,
)
def test_full_flow_requires_server_verification(env):
store, _clock, a1, _a2 = env
create_task(store)
polled = store.poll_tasks(agent_id=a1["agent_id"])
assert [item["task_id"] for item in polled] == ["TASK-ONLINE-001"]
claim = store.claim_task(
task_id="TASK-ONLINE-001",
agent_id=a1["agent_id"],
lease_seconds=300,
)
assert claim["status"] == "CLAIMED"
assert claim["lease_token"].startswith("lease_")
running = store.heartbeat(
task_id="TASK-ONLINE-001",
agent_id=a1["agent_id"],
lease_token=claim["lease_token"],
progress={"percent": 50},
lease_seconds=300,
)
assert running["status"] == "RUNNING"
submitted = store.submit_result(
task_id="TASK-ONLINE-001",
agent_id=a1["agent_id"],
lease_token=claim["lease_token"],
idempotency_key="result:1",
result={
"status": "completed",
"summary": "Created validation.txt",
"artifacts": [{"path": "validation.txt", "sha256": "0" * 64}],
},
)
assert submitted["status"] == "VERIFYING"
assert submitted["status"] != "SUCCEEDED"
verified = store.verify_task(
task_id="TASK-ONLINE-001",
decision="SUCCEEDED",
verification={"checks": {"content": "PASS"}},
)
assert verified["status"] == "SUCCEEDED"
def test_atomic_claim_allows_only_one_agent(env):
store, _clock, a1, a2 = env
create_task(store)
outcomes: list[tuple[str, str]] = []
barrier = threading.Barrier(2)
def runner(agent_id: str) -> None:
barrier.wait()
try:
store.claim_task(
task_id="TASK-ONLINE-001",
agent_id=agent_id,
lease_seconds=300,
)
outcomes.append((agent_id, "CLAIMED"))
except TaskProtocolError as exc:
outcomes.append((agent_id, exc.code))
t1 = threading.Thread(target=runner, args=(a1["agent_id"],))
t2 = threading.Thread(target=runner, args=(a2["agent_id"],))
t1.start()
t2.start()
t1.join()
t2.join()
assert sum(result == "CLAIMED" for _, result in outcomes) == 1
assert sum(result in {"TASK_NOT_CLAIMABLE", "TASK_CLAIM_CONFLICT"} for _, result in outcomes) == 1
def test_expired_lease_is_requeued_and_reclaimable(env):
store, clock, a1, a2 = env
create_task(store)
first = store.claim_task(
task_id="TASK-ONLINE-001",
agent_id=a1["agent_id"],
lease_seconds=30,
)
assert first["attempt"] == 1
clock.advance(31)
available = store.poll_tasks(agent_id=a2["agent_id"])
assert available[0]["status"] == "QUEUED"
second = store.claim_task(
task_id="TASK-ONLINE-001",
agent_id=a2["agent_id"],
lease_seconds=30,
)
assert second["attempt"] == 2
assert second["claimed_by"] == a2["agent_id"]
def test_wrong_lease_token_is_rejected(env):
store, _clock, a1, _a2 = env
create_task(store)
store.claim_task(
task_id="TASK-ONLINE-001",
agent_id=a1["agent_id"],
lease_seconds=300,
)
with pytest.raises(TaskProtocolError, match="TASK_LEASE_TOKEN_INVALID"):
store.heartbeat(
task_id="TASK-ONLINE-001",
agent_id=a1["agent_id"],
lease_token="lease_wrong",
progress={},
)
def test_result_submission_is_idempotent(env):
store, _clock, a1, _a2 = env
create_task(store)
claim = store.claim_task(
task_id="TASK-ONLINE-001",
agent_id=a1["agent_id"],
lease_seconds=300,
)
payload = {"status": "completed", "summary": "ok"}
first = store.submit_result(
task_id="TASK-ONLINE-001",
agent_id=a1["agent_id"],
lease_token=claim["lease_token"],
idempotency_key="result:same",
result=payload,
)
second = store.submit_result(
task_id="TASK-ONLINE-001",
agent_id=a1["agent_id"],
lease_token=claim["lease_token"],
idempotency_key="result:same",
result=payload,
)
assert first["status"] == "VERIFYING"
assert second["status"] == "VERIFYING"
assert second["idempotent"] is True
with pytest.raises(TaskProtocolError, match="IDEMPOTENCY_CONFLICT"):
store.submit_result(
task_id="TASK-ONLINE-001",
agent_id=a1["agent_id"],
lease_token=claim["lease_token"],
idempotency_key="result:same",
result={"status": "completed", "summary": "different"},
)
def test_needs_rework_returns_to_queue(env):
store, _clock, a1, _a2 = env
create_task(store)
claim = store.claim_task(
task_id="TASK-ONLINE-001",
agent_id=a1["agent_id"],
lease_seconds=300,
)
store.submit_result(
task_id="TASK-ONLINE-001",
agent_id=a1["agent_id"],
lease_token=claim["lease_token"],
idempotency_key="result:1",
result={"status": "completed"},
)
result = store.verify_task(
task_id="TASK-ONLINE-001",
decision="NEEDS_REWORK",
verification={"reason": "success criteria not met"},
)
assert result["status"] == "QUEUED"
assert result["claimed_by"] is None
def test_capability_filter(env):
store, _clock, a1, _a2 = env
store.create_task(
task_id="TASK-GPU-001",
goal="GPU task",
instructions="Run GPU workload",
success_criteria=[],
required_capabilities=["gpu"],
idempotency_key="create:gpu",
)
assert store.poll_tasks(agent_id=a1["agent_id"]) == []
def test_create_task_idempotency_conflict(env):
store, _clock, _a1, _a2 = env
first = create_task(store)
replay = create_task(store)
assert first["task_id"] == replay["task_id"]
assert replay["idempotent"] is True
with pytest.raises(TaskProtocolError, match="IDEMPOTENCY_CONFLICT"):
store.create_task(
task_id="TASK-ONLINE-002",
goal="different",
instructions="different",
success_criteria=[],
idempotency_key="create:TASK-ONLINE-001",
)
def test_terminal_success_retains_executing_agent_for_final_readback(env):
store, _clock, a1, _a2 = env
create_task(store, "TASK-TERMINAL-READBACK")
claim = store.claim_task(
task_id="TASK-TERMINAL-READBACK",
agent_id=a1["agent_id"],
lease_seconds=120,
)
store.submit_result(
task_id="TASK-TERMINAL-READBACK",
agent_id=a1["agent_id"],
lease_token=claim["lease_token"],
idempotency_key="terminal-result",
result={"status": "completed"},
)
final = store.verify_task(
task_id="TASK-TERMINAL-READBACK",
decision="SUCCEEDED",
verification={"criteria": "PASS"},
)
assert final["status"] == "SUCCEEDED"
assert final["claimed_by"] == a1["agent_id"]
def test_needs_rework_releases_agent_ownership_for_reclaim(env):
store, _clock, a1, a2 = env
create_task(store, "TASK-REWORK-OWNERSHIP")
claim = store.claim_task(
task_id="TASK-REWORK-OWNERSHIP",
agent_id=a1["agent_id"],
lease_seconds=120,
)
store.submit_result(
task_id="TASK-REWORK-OWNERSHIP",
agent_id=a1["agent_id"],
lease_token=claim["lease_token"],
idempotency_key="rework-owner-result",
result={"status": "completed"},
)
requeued = store.verify_task(
task_id="TASK-REWORK-OWNERSHIP",
decision="NEEDS_REWORK",
verification={"criteria": "FAIL"},
)
assert requeued["status"] == "QUEUED"
assert requeued["claimed_by"] is None
reclaimed = store.claim_task(
task_id="TASK-REWORK-OWNERSHIP",
agent_id=a2["agent_id"],
lease_seconds=120,
)
assert reclaimed["claimed_by"] == a2["agent_id"]
+152 -2
View File
@@ -2,6 +2,7 @@ import base64
from pathlib import Path
from aios_authority_git_executor import AuthorityGitExecutor
from aios_chatgpt_bridge_v2 import BridgeV2Adapter, OPERATION_REQUIRED_FIELDS
from aios_chatgpt_bridge_v2_server import BridgeHandler, RuntimeConfig
from aios_continuation_executor import GiteaContinuationRemote, ServerSideCredentialProjection
from aios_continuation_lease import WriterLeaseRequestV2
@@ -26,6 +27,7 @@ class Probe:
def test_create_new_lease_is_exact_base_and_absent():
request = WriterLeaseRequestV2(
job_id="job-bridge-runtime-lineage-test",
task_id="TASK-P0-BRIDGE-V21-DURABLE-RECONCILIATION-20260809",
repository="aios/aios",
branch_mode="CREATE_NEW",
@@ -39,6 +41,7 @@ def test_create_new_lease_is_exact_base_and_absent():
def test_create_new_lease_rejects_existing_target():
request = WriterLeaseRequestV2(
job_id="job-bridge-runtime-lineage-test",
task_id="task",
repository="aios/aios",
branch_mode="CREATE_NEW",
@@ -74,8 +77,8 @@ def test_slash_branch_uses_paginated_collection():
def test_http_route_contract_is_complete():
expected = {
"/v2/runtime", "/v2/capabilities", "/v2/governed-task",
"/v2/writer-lease", "/v2/commit-file", "/v2/commit-files",
"/v2/runtime", "/v2/capabilities", "/v2/governed-task", "/v2/execution-envelope", "/v2/execution-plan",
"/v2/writer-lease", "/v2/writer-lease/bootstrap", "/v2/writer-lease/renew", "/v2/commit-file", "/v2/commit-files",
"/v2/job", "/v2/job/resume", "/v2/job/reconcile",
"/v2/tag/approval-request",
"/v2/tag/publish",
@@ -88,3 +91,150 @@ def test_http_route_contract_is_complete():
from aios_chatgpt_bridge_v2_server import ROUTES
assert set(ROUTES) == expected
def test_renew_writer_lease_is_exposed_in_action_and_capability_contracts():
adapter = BridgeV2Adapter(runtime_status={})
capabilities = adapter.getGovernedWriteCapabilities()
contract = capabilities["operation_contracts"]["renewWriterLease"]
assert contract["method"] == "POST"
assert contract["path"] == "/v2/writer-lease/renew"
assert tuple(contract["required_fields"]) == OPERATION_REQUIRED_FIELDS["renewWriterLease"]
assert "runtime_rehydrated" in contract["response_fields"]
action_schema = Path("docs/gitops/openapi-chatgpt-actions.yaml").read_text(encoding="utf-8")
assert "/v2/writer-lease/renew:" in action_schema
assert "operationId: renewWriterLease" in action_schema
assert "BridgeWriterLeaseRecoveryRequest" in action_schema
assert "BridgeWriterLeaseRecoveryResponse" in action_schema
assert {
capabilities["operation_contracts"][operation]["path"]
for operation in (
"acquireWriterLease",
"renewWriterLease",
"commitFileGoverned",
"commitFilesGoverned",
)
} == {
"/v2/writer-lease",
"/v2/writer-lease/renew",
"/v2/commit-file",
"/v2/commit-files",
}
def test_bootstrap_writer_lease_is_exposed_in_action_and_capability_contracts():
adapter = BridgeV2Adapter(runtime_status={})
capabilities = adapter.getGovernedWriteCapabilities()
contract = capabilities["operation_contracts"]["bootstrapWriterLease"]
assert contract["method"] == "POST"
assert contract["path"] == "/v2/writer-lease/bootstrap"
assert tuple(contract["required_fields"]) == OPERATION_REQUIRED_FIELDS["bootstrapWriterLease"]
assert "bootstrapped" in contract["response_fields"]
assert "durable_branch_lease_present" in contract["response_fields"]
action_schema = Path("docs/gitops/openapi-chatgpt-actions.yaml").read_text(encoding="utf-8")
assert "/v2/writer-lease/bootstrap:" in action_schema
assert "operationId: bootstrapWriterLease" in action_schema
assert "bootstrapped: {type: boolean}" in action_schema
assert "durable_branch_lease_present: {type: boolean}" in action_schema
def test_http_allows_explicit_job_id_on_lease_and_commit_requests():
handler = BridgeHandler.__new__(BridgeHandler)
lease_body = {
"schema": WriterLeaseRequestV2.schema,
"task_id": "task",
"job_id": "job-explicit-runtime-lineage",
"repository": "aios/aios",
"branch_mode": "CONTINUE_EXISTING",
"base_oid": "a" * 40,
"expected_branch_oid": "a" * 40,
"target_branch": "agent/explicit-runtime-lineage",
"generation": 2,
"lease_seconds": 120,
}
handler._allowed("acquireWriterLease", lease_body)
handler._allowed(
"commitFilesGoverned",
{
"task_id": "task",
"job_id": "job-explicit-runtime-lineage",
"repository": "aios/aios",
"expected_branch_oid": "a" * 40,
"target_branch": "agent/explicit-runtime-lineage",
"generation": 2,
"idempotency_key": "commit-explicit-runtime-lineage",
"files": {"docs/x.txt": "eA=="},
"message": "test",
},
)
def test_http_allows_omitted_job_id_for_durable_server_resolution():
handler = BridgeHandler.__new__(BridgeHandler)
lease_body = {
"schema": WriterLeaseRequestV2.schema,
"task_id": "task",
"repository": "aios/aios",
"branch_mode": "CONTINUE_EXISTING",
"base_oid": "a" * 40,
"expected_branch_oid": "a" * 40,
"target_branch": "agent/implicit-runtime-lineage",
"generation": 2,
"lease_seconds": 120,
}
handler._allowed("acquireWriterLease", lease_body)
handler._allowed(
"commitFilesGoverned",
{
"task_id": "task",
"repository": "aios/aios",
"expected_branch_oid": "a" * 40,
"target_branch": "agent/implicit-runtime-lineage",
"generation": 2,
"idempotency_key": "commit-implicit-runtime-lineage",
"files": {"docs/x.txt": "eA=="},
"message": "test",
},
)
def test_http_mutation_routes_require_exact_runtime_principal():
handler = BridgeHandler.__new__(BridgeHandler)
runtime_principal = "runtime-" "agent-principal-token-1234"
handler.control_token = runtime_principal
handler.agent_task_api_url = "http://127.0.0.1:1"
handler.headers = {"Authorization": f"Bearer {runtime_principal}"}
assert handler._authorized("createGovernedTask", {}) is True
handler.headers = {"Authorization": "Bearer wrong-runtime-principal-token"}
assert handler._authorized("createGovernedTask", {}) is False
handler.headers = {}
assert handler._authorized("createGovernedTask", {}) is False
def test_capability_contracts_return_exact_job_id_lineage():
contracts = BridgeV2Adapter(runtime_status={}).getGovernedWriteCapabilities()["operation_contracts"]
for operation in ("acquireWriterLease", "bootstrapWriterLease", "renewWriterLease"):
assert "job_id" in contracts[operation]["response_fields"]
for operation in ("commitFileGoverned", "commitFilesGoverned"):
assert "job_id" in contracts[operation]["response_fields"]
def test_online_agent_execution_routes_and_action_projection_are_exposed():
from aios_chatgpt_bridge_v2 import BridgeV2Adapter, OPERATION_REQUIRED_FIELDS
from aios_chatgpt_bridge_v2_server import ROUTES
assert ROUTES["/v2/execution-envelope"] == "projectOnlineAgentExecution"
assert ROUTES["/v2/execution-plan"] == "registerOnlineAgentExecutionPlan"
adapter = BridgeV2Adapter(runtime_status={})
contracts = adapter.getGovernedWriteCapabilities()["operation_contracts"]
assert tuple(contracts["projectOnlineAgentExecution"]["required_fields"]) == OPERATION_REQUIRED_FIELDS["projectOnlineAgentExecution"]
assert tuple(contracts["registerOnlineAgentExecutionPlan"]["required_fields"]) == OPERATION_REQUIRED_FIELDS["registerOnlineAgentExecutionPlan"]
schema = Path("docs/gitops/openapi-chatgpt-actions.yaml").read_text(encoding="utf-8")
assert "/v2/execution-envelope:" in schema
assert "operationId: projectOnlineAgentExecution" in schema
assert "/v2/execution-plan:" in schema
assert "operationId: registerOnlineAgentExecutionPlan" in schema
-192
View File
@@ -1,192 +0,0 @@
from __future__ import annotations
from datetime import datetime, timedelta, timezone
import importlib.util
import json
from pathlib import Path
import tempfile
import unittest
from unittest.mock import patch
ROOT = Path(__file__).resolve().parents[1]
APP = ROOT / "active-runtime" / "app"
SPEC = importlib.util.spec_from_file_location(
"aios_durable_owner_authorization",
APP / "aios_durable_owner_authorization.py",
)
BROKER_MODULE = importlib.util.module_from_spec(SPEC)
import sys
sys.modules[SPEC.name] = BROKER_MODULE
assert SPEC.loader is not None
SPEC.loader.exec_module(BROKER_MODULE)
AuthorizationError = BROKER_MODULE.AuthorizationError
Broker = BROKER_MODULE.Broker
Constraints = BROKER_MODULE.Constraints
DurablePolicy = BROKER_MODULE.DurablePolicy
ReceiptStore = BROKER_MODULE.ReceiptStore
Upstream = BROKER_MODULE.Upstream
CALLER = "chatgpt-governed-implementer-singlehost"
SCOPE = "release:candidate"
SOURCE = "257c7a8df433f75b364bcaf69cbbc32e8c2e5d38"
def policy(*, expires_at=None, capabilities=None):
return DurablePolicy(
grant_id="grant-caller-projection-v1",
owner_identity="owner-durable-policy",
account=CALLER,
capabilities=frozenset(capabilities or {SCOPE}),
issued_at=datetime.now(timezone.utc) - timedelta(minutes=1),
not_before=datetime.now(timezone.utc) - timedelta(minutes=1),
persistent_until_revoked=True,
constraints=Constraints(
repository="aios/aios",
account=CALLER,
allowed_target_prefix="agent/",
release_version_prefix="V2R2C",
max_task_risk="engineering",
),
policy_sha256="a" * 64,
expires_at=expires_at,
)
def make_broker(root):
return Broker(
policy_path=root / "policy.json",
revocation_path=root / "revocations.json",
store=ReceiptStore(root / "receipts.sqlite3"),
upstream=Upstream("http://127.0.0.1:1", "not-used"),
expected_account=CALLER,
expected_peer_user="test-peer",
allowed_peer_users=frozenset({"test-peer"}),
)
class CallerProjectionTests(unittest.TestCase):
def project(self, broker, request=None):
return broker.project_caller(
CALLER,
SCOPE,
request or {"source_commit": SOURCE, "version": "V2R2C2"},
idempotency_key="caller-projection-test-v1",
)
def test_positive_projection_ack_and_effective_scope(self):
broker = make_broker(Path(tempfile.mkdtemp()))
with patch.object(broker, "policy", return_value=policy()):
value = self.project(broker)
credential = value.pop("_credential")
self.assertGreaterEqual(len(credential), 32)
self.assertEqual("ACKED", value["projection_state"])
self.assertEqual("REHYDRATED", value["credential_state"])
self.assertEqual([SCOPE], value["effective_scopes"])
self.assertFalse(value["secret_exposed"])
def test_secret_material_is_not_durable(self):
root = Path(tempfile.mkdtemp())
broker = make_broker(root)
with patch.object(broker, "policy", return_value=policy()):
value = self.project(broker)
credential = value.pop("_credential")
durable = (root / "receipts.sqlite3").read_bytes()
self.assertNotIn(credential.encode(), durable)
self.assertNotIn("credential_material", durable.decode("utf-8", "ignore"))
self.assertFalse(value["secret_exposed"])
def test_caller_mismatch_is_denied(self):
broker = make_broker(Path(tempfile.mkdtemp()))
with self.assertRaises(AuthorizationError) as caught:
broker.project_caller(
"other-caller",
SCOPE,
{},
idempotency_key="caller-projection-test-v1",
)
self.assertEqual("CALLER_MISMATCH", caught.exception.code)
def test_scope_mismatch_is_denied(self):
broker = make_broker(Path(tempfile.mkdtemp()))
with self.assertRaises(AuthorizationError) as caught:
broker.project_caller(
CALLER,
"release:deploy",
{},
idempotency_key="caller-projection-test-v1",
)
self.assertEqual("SCOPE_MISMATCH", caught.exception.code)
def test_policy_missing_revoked_and_expired_fail_closed(self):
broker = make_broker(Path(tempfile.mkdtemp()))
for code in ("POLICY_FILE_INVALID", "OWNER_GRANT_REVOKED", "POLICY_EXPIRED"):
with self.subTest(code=code):
with patch.object(
broker,
"policy",
side_effect=AuthorizationError(code, 403),
):
with self.assertRaises(AuthorizationError) as caught:
self.project(broker)
self.assertEqual(code, caught.exception.code)
def test_invalid_projection_state_fails_closed(self):
with self.assertRaises(AuthorizationError) as caught:
Broker.validate_caller_projection(
{
"caller": CALLER,
"scope": SCOPE,
"projection_state": "CORRUPT",
"credential_state": "REHYDRATED",
"rehydration_state": "RECOVERED",
"effective_scopes": [SCOPE],
"secret_exposed": False,
}
)
self.assertEqual("PROJECTION_STATE_INVALID", caught.exception.code)
def test_restart_rehydrates_from_durable_metadata(self):
root = Path(tempfile.mkdtemp())
first = make_broker(root)
with patch.object(first, "policy", return_value=policy()):
initial = self.project(first)
first_credential = initial.pop("_credential")
second = make_broker(root)
with patch.object(second, "policy", return_value=policy()):
recovered = self.project(second)
second_credential = recovered.pop("_credential")
self.assertNotEqual(first_credential, second_credential)
self.assertEqual("RECOVERED", recovered["rehydration_state"])
self.assertEqual("ACKED", recovered["projection_state"])
def test_expired_policy_loader_rejects(self):
root = Path(tempfile.mkdtemp())
policy_path = root / "policy.json"
payload = {
"schema": "aios.owner-durable-authorization.v1",
"grant_id": "grant-caller-projection-v1",
"owner_identity": "owner-durable-policy",
"account": CALLER,
"capabilities": [SCOPE],
"issued_at": "2026-08-22T00:00:00Z",
"not_before": "2026-08-22T00:00:00Z",
"expires_at": "2026-08-22T01:00:00Z",
"persistent_until_revoked": True,
"constraints": {
"repository": "aios/aios",
"allowed_target_prefix": "agent/",
"release_version_prefix": "V2R2C",
"max_task_risk": "engineering",
},
}
policy_path.write_text(json.dumps(payload), encoding="utf-8")
with patch.object(BROKER_MODULE, "validate_root_file", lambda path, **kwargs: path):
with self.assertRaises(AuthorizationError) as caught:
DurablePolicy.load(policy_path)
self.assertEqual("POLICY_EXPIRED", caught.exception.code)
if __name__ == "__main__":
unittest.main()
@@ -1,191 +0,0 @@
from __future__ import annotations
import json
import sys
import types
from pathlib import Path
from types import SimpleNamespace
from unittest.mock import patch
import pytest
ROOT = Path(__file__).resolve().parents[1]
APP = ROOT / "active-runtime" / "app"
if str(APP) not in sys.path:
sys.path.insert(0, str(APP))
try:
import orchestrator.action_monitor
except ModuleNotFoundError:
_orchestrator = types.ModuleType("orchestrator")
_action_monitor = types.ModuleType("orchestrator.action_monitor")
_action_monitor.ActionMonitor = type("ActionMonitor", (), {})
_orchestrator.__path__ = []
sys.modules["orchestrator"] = _orchestrator
sys.modules["orchestrator.action_monitor"] = _action_monitor
import aios_durable_owner_authorization as broker_api
import aios_model_api_staging as model_api
CALLER = "chatgpt-governed-implementer-singlehost"
SCOPE = "release:candidate"
def _invoke_capabilities(
headers,
*,
auth_result=None,
auth_side_effect=None,
projection_client=None,
):
handler = object.__new__(model_api.H)
handler.headers = headers
responses = []
handler.sendj = lambda value, code=200: responses.append((value, code))
if auth_side_effect is not None:
auth_patch = patch.object(
model_api, "authenticate", side_effect=auth_side_effect
)
else:
auth_patch = patch.object(
model_api, "authenticate", return_value=auth_result
)
client = (
projection_client
if projection_client is not None
else model_api.CALLER_PROJECTION_CLIENT
)
with auth_patch, patch.object(model_api, "CALLER_PROJECTION_CLIENT", client):
model_api.H.caller_capabilities(handler)
assert len(responses) == 1
return responses[0]
@pytest.mark.parametrize(
"headers",
[
{},
{"Authorization": "Bearer invalid"},
{"Authorization": "Bearer unknown"},
{"X-AIOS-Token": "rec-none"},
],
)
def test_anonymous_invalid_unknown_and_rec_none_are_structured_401(headers):
payload, status = _invoke_capabilities(headers, auth_result=None)
assert status == 401
assert payload == {"error": "unauthorized", "hint": "Bearer token required"}
assert "Traceback" not in json.dumps(payload)
def test_authenticate_exception_is_fail_closed_without_socket_abort():
payload, status = _invoke_capabilities(
{"Authorization": "Bearer malformed"},
auth_side_effect=RuntimeError("malformed"),
)
assert status == 401
assert payload["error"] == "unauthorized"
assert "Traceback" not in json.dumps(payload)
def test_authorized_capability_introspection_remains_secret_free():
row = {
"account": CALLER,
"role": "user",
"scopes": ["agent-task:control", "branch:create", "file:write"],
}
result = SimpleNamespace(
public={
"projection_state": "ACKED",
"credential_state": "ACTIVE",
"rehydration_state": "REHYDRATED",
"effective_scopes": [SCOPE],
},
credential_loaded=True,
)
class _ProjectionClient:
def rehydrate(self, caller, scope, request):
assert caller == CALLER
assert scope == SCOPE
assert request == {}
return result
payload, status = _invoke_capabilities(
{"Authorization": "Bearer valid"},
auth_result=row,
projection_client=_ProjectionClient(),
)
assert status == 200
assert payload["caller_identity"] == CALLER
assert payload["admitted"] is True
assert SCOPE in payload["effective_scopes"]
assert payload["projection_state"] == "ACKED"
assert payload["credential_state"] == "ACTIVE"
assert payload["rehydration_state"] == "REHYDRATED"
assert payload["secret_exposed"] is False
assert "_credential" not in json.dumps(payload)
class _FakeBroker:
def __init__(self):
self.calls = []
def project_caller(self, caller, scope, request, *, idempotency_key):
self.calls.append((caller, scope, request, idempotency_key))
return {
"caller": caller,
"scope": scope,
"projection_state": "ACKED",
"credential_state": "ACTIVE",
"rehydration_state": "REHYDRATED",
"effective_scopes": [SCOPE],
"_credential": "x" * 40,
}
def _dispatch_broker(path="/v1/project-caller", *, peer=True, account=True):
broker = _FakeBroker()
handler = object.__new__(broker_api.Handler)
handler.path = path
handler.broker = broker
handler.peer_authorized = lambda: peer
handler.account_authorized = lambda: account
handler.body = lambda: {
"caller": CALLER,
"scope": SCOPE,
"idempotency_key": "runtime-regression-test-20260823",
"request": {
"repository": "aios/aios",
"target_branch": "agent/runtime-regression-test",
"release_version": "V2R2C2",
},
}
responses = []
handler.send_json = lambda value, status, internal_credential=None: responses.append(
(value, status, internal_credential)
)
broker_api.Handler.do_POST(handler)
return responses, broker
def test_existing_project_caller_route_reaches_handler_without_public_secret():
responses, broker = _dispatch_broker()
assert len(responses) == 1
value, status, internal_credential = responses[0]
assert status == 200
assert broker.calls
assert broker.calls[0][0:2] == (CALLER, SCOPE)
assert internal_credential == "x" * 40
assert "_credential" not in value
assert "x" * 40 not in json.dumps(value)
def test_unauthorized_peer_is_denied_and_unregistered_route_is_not_added():
responses, broker = _dispatch_broker(peer=False)
assert responses[0][1] == 403
assert broker.calls == []
responses, broker = _dispatch_broker(path="/v1/not-registered")
assert responses[0][1] == 404
assert broker.calls == []
@@ -0,0 +1,307 @@
from __future__ import annotations
from pathlib import Path
import re
import json
from types import SimpleNamespace
import pytest
import yaml
from aios_chatgpt_bridge_v2_server import GovernedRuntime
from aios_continuation_executor import ContinuationExecutor
from aios_continuation_lease import WriterLeaseRequestV2
from aios_gitops_job_controller import JobState
TASK = "continuation-normalization-test"
JOB = "job-continuation-normalization-test"
REPO = "aios/aios"
BASE = "a" * 40
BRANCH = "agent/continuation-normalization-test"
class Authority:
def __init__(self, head: str | None) -> None:
self.head = head
def read_ref(self, ref: str) -> str | None:
assert ref == "refs/heads/" + BRANCH
return self.head
def commit_exists(self, oid: str) -> bool:
return oid == BASE
def branch_exists(self, branch: str) -> bool:
assert branch == BRANCH
return self.head is not None
def branch_head(self, branch: str) -> str:
assert branch == BRANCH and self.head is not None
return self.head
def is_ancestor(self, older: str, newer: str) -> bool:
return older == BASE and newer == BASE
def lease(**overrides: object) -> WriterLeaseRequestV2:
values = {
"task_id": TASK,
"job_id": JOB,
"repository": REPO,
"branch_mode": "CREATE_NEW",
"base_oid": BASE,
"expected_branch_oid": BASE,
"target_branch": BRANCH,
"generation": 1,
"lease_seconds": 120,
}
values.update(overrides)
return WriterLeaseRequestV2(**values)
def runtime(*, head: str | None = BASE, **binding_overrides: object) -> GovernedRuntime:
value = GovernedRuntime.__new__(GovernedRuntime)
value.authority = Authority(head)
binding = {
"task_id": TASK,
"job_id": JOB,
"repository": REPO,
"base_oid": BASE,
"expected_branch_oid": BASE,
"target_branch": BRANCH,
"generation": 1,
"branch_mode": "CREATE_NEW",
}
binding.update(binding_overrides)
value.bindings = {TASK: binding}
return value
def test_same_lineage_create_new_normalizes_to_continue_existing() -> None:
normalized = runtime()._normalize_runtime_lease(lease())
assert normalized.branch_mode == "CONTINUE_EXISTING"
assert normalized.task_id == TASK
assert normalized.job_id == JOB
assert normalized.repository == REPO
assert normalized.target_branch == BRANCH
assert normalized.expected_branch_oid == BASE
assert normalized.generation == 1
def test_unprovisioned_create_new_stays_create_new() -> None:
assert runtime(head=None)._normalize_runtime_lease(lease()).branch_mode == "CREATE_NEW"
@pytest.mark.parametrize(
("field", "value"),
(
("repository", "other/repo"),
("base_oid", "b" * 40),
("expected_branch_oid", "b" * 40),
("target_branch", "agent/foreign"),
("generation", 2),
),
)
def test_binding_mismatch_fails_closed(field: str, value: object) -> None:
with pytest.raises(RuntimeError, match="LEASE_BINDING_CONFLICT"):
runtime(**{field: value})._normalize_runtime_lease(lease())
def test_drifted_existing_head_fails_closed() -> None:
with pytest.raises(RuntimeError, match="EXPECTED_BRANCH_OID_CONFLICT"):
runtime(head="b" * 40)._normalize_runtime_lease(lease())
def test_openapi_contract_parses_and_requires_exact_commit_lineage() -> None:
document = yaml.safe_load(
Path("docs/gitops/openapi-chatgpt-actions.yaml").read_text(encoding="utf-8")
)
schema = document["components"]["schemas"]["BridgeCommitResponse"]
assert {
"job_id",
"commit_oid",
"commit_visible",
"branch_mode",
"target_branch",
"generation",
} <= set(schema["required"])
def test_changed_production_files_pass_secret_scan() -> None:
paths = [
Path("active-runtime/app/aios_authority_git_executor.py"),
Path("active-runtime/app/aios_chatgpt_bridge_v2_server.py"),
Path("active-runtime/app/aios_continuation_executor.py"),
Path("active-runtime/app/aios_continuation_lease.py"),
Path("active-runtime/app/authorized_task_writer_provisioning.py"),
Path("active-runtime/app/aios_chatgpt_bridge_v2.py"),
Path("docs/gitops/openapi-chatgpt-actions.yaml"),
]
expression = re.compile(
r"(?i)(BEGIN (RSA|OPENSSH|EC) PRIVATE KEY|"
r"authorization:\\s*bearer\\s+[A-Za-z0-9._-]{12,}|"
r"api[_-]?key\\s*[:=]\\s*['\\\"][^'\\\"]{12,})"
)
hits = []
for path in paths:
for number, line in enumerate(
path.read_text(encoding="utf-8", errors="replace").splitlines(), 1
):
if expression.search(line):
hits.append(f"{path}:{number}")
assert hits == []
def test_remote_verified_job_is_not_completed_when_head_cas_callback_fails() -> None:
class Store:
def read(self, _job_id):
return {
"branch": BRANCH,
"request_json": json.dumps(
{"binding": {"expected_branch_oid": BASE}}
),
}
class Controller:
def __init__(self):
self.store = Store()
self.states = []
def advance(self, _job_id, state, **_kwargs):
self.states.append(state)
return SimpleNamespace(as_dict=lambda: {"state": state.value})
executor = ContinuationExecutor.__new__(ContinuationExecutor)
executor.controller = Controller()
executor._observe_remote = lambda _branch, oid: (oid, oid, True)
def fail_cas(_oid):
raise RuntimeError("LEASE_CAS_CONFLICT")
with pytest.raises(RuntimeError, match="LEASE_CAS_CONFLICT"):
executor._finish_remote_verification(
JOB,
"b" * 40,
"b" * 40,
before_complete=fail_cas,
)
assert executor.controller.states == [JobState.REMOTE_VERIFIED]
@pytest.mark.parametrize("branch_mode", ("CREATE_NEW", "CONTINUE_EXISTING"))
def test_executor_preflight_accepts_the_exact_mode_dependent_normalized_shape(
branch_mode: str,
) -> None:
from datetime import datetime, timedelta, timezone
from aios_continuation_executor import (
ContinuationPermissions,
ContinuationRuntimeBinding,
)
request = lease(branch_mode=branch_mode)
now = datetime.now(timezone.utc)
executor = ContinuationExecutor.__new__(ContinuationExecutor)
executor.authority = Authority(None if branch_mode == "CREATE_NEW" else BASE)
executor.now = lambda: now
executor._max_generation = lambda _branch, *, exclude_job_id=None: 0
executor.workspace = SimpleNamespace(
is_detached=lambda: True,
head_oid=lambda: BASE,
)
executor.remote = SimpleNamespace(repository_identity=lambda: REPO)
binding = ContinuationRuntimeBinding(
request=request,
job_id=JOB,
workspace_path=Path("/tmp/continuation-normalization-test"),
lease_not_before=now - timedelta(seconds=1),
lease_expires_at=now + timedelta(seconds=60),
original_receipt_valid=True,
original_grant_consumed=True,
permissions=ContinuationPermissions(),
)
executor._validate_preflight(binding, job_id=JOB)
def test_create_new_executor_resumes_executing_and_uses_absence_bound_writers() -> None:
from datetime import datetime, timedelta, timezone
from aios_continuation_executor import (
ContinuationCommit,
ContinuationPermissions,
ContinuationRuntimeBinding,
)
new_oid = "b" * 40
class Store:
def read(self, _job_id: str) -> dict[str, object]:
return {"state": "EXECUTING", "revision": 4}
class Controller:
def __init__(self) -> None:
self.store = Store()
self.advanced: list[str] = []
def advance(self, _job_id: str, state: JobState, **_kwargs: object) -> object:
self.advanced.append(state.value)
return SimpleNamespace(as_dict=lambda: {"state": state.value})
def complete_continuation_cycle(self, *_args: object, **_kwargs: object) -> None:
return None
class Workspace:
def commit_files(self, _files: object, _message: str) -> str:
return new_oid
class CreateAuthority:
def __init__(self) -> None:
self.created: list[tuple[str, str, str]] = []
def is_ancestor(self, older: str, newer: str) -> bool:
return older == BASE and newer == new_oid
def create_task_branch_cas(self, branch: str, base: str, new: str) -> str:
self.created.append((branch, base, new))
return new
def compare_and_swap_branch(self, *_args: object) -> str:
raise AssertionError("existing-branch CAS must not serve CREATE_NEW")
class CreateRemote:
def __init__(self) -> None:
self.created: list[tuple[str, str]] = []
def push_new_exact(self, branch: str, new: str) -> None:
self.created.append((branch, new))
def push_exact(self, *_args: object) -> None:
raise AssertionError("existing-branch push must not serve CREATE_NEW")
class Credential:
def project(self) -> None: pass
def revoke(self) -> None: pass
controller=Controller(); authority=CreateAuthority(); remote=CreateRemote()
executor=ContinuationExecutor(controller,authority,Workspace(),remote,Credential())
executor._validate_preflight=lambda *_args,**_kwargs: None
executor._finish_remote_verification=lambda *_args,**_kwargs: {
"job_id":JOB,"state":"COMPLETED","receipt":{"authority_oid":new_oid,"commit_visible":True}
}
now=datetime.now(timezone.utc)
binding=ContinuationRuntimeBinding(
request=lease(branch_mode="CREATE_NEW"),job_id=JOB,
workspace_path=Path("/tmp/create-new-executor-resume"),
lease_not_before=now-timedelta(seconds=1),lease_expires_at=now+timedelta(seconds=60),
original_receipt_valid=True,original_grant_consumed=True,
permissions=ContinuationPermissions(),
)
result=executor.execute(
binding,ContinuationCommit("create-new-test",{"docs/test.txt":b"ok"},"test")
)
assert result["state"]=="COMPLETED"
assert authority.created==[(BRANCH,BASE,new_oid)]
assert remote.created==[(BRANCH,new_oid)]
assert controller.advanced==["LOCAL_VERIFIED","REMOTE_SYNCING"]
+67
View File
@@ -0,0 +1,67 @@
from pathlib import Path
import hashlib,json,os,tarfile,sys,tempfile
import pytest
ROOT=Path(__file__).resolve().parents[1]
sys.path.insert(0,str(ROOT/"active-runtime/app"))
import aios_isolated_executor as mod
def make_archive(tmp_path):
root=tmp_path/"src"; root.mkdir()
(root/"hello.py").write_text("VALUE = 1\n")
archive=tmp_path/"source.tgz"
with tarfile.open(archive,"w:gz") as tf: tf.add(root/"hello.py",arcname="hello.py")
return archive
def test_isolated_executor_applies_hash_bound_change(tmp_path,monkeypatch):
archive=make_archive(tmp_path)
monkeypatch.delenv("CREDENTIALS_DIRECTORY",raising=False)
monkeypatch.setattr(mod,"assert_uncredentialed",lambda:None)
request={
"schema":"aios.isolated-execution.request.v1",
"request_id":"task-req-0000000000000000",
"source_archive":str(archive),
"source_archive_sha256":mod.sha256(archive.read_bytes()),
"change_set":[{
"path":"hello.py","operation":"replace_text","old":"VALUE = 1","new":"VALUE = 2","count":1,
"expected_sha256":mod.sha256(b"VALUE = 1\n")
}],
"verification":[{"label":"compile","argv":["/usr/bin/python3","-m","py_compile","hello.py"],"cwd":".","timeout_seconds":30}]
}
req=tmp_path/"req.json"; res=tmp_path/"res.json"
req.write_text(json.dumps(request))
mod.execute_request(req,res)
out=json.loads(res.read_text())
assert out["request_id"]=="task-req-0000000000000000"
assert out["credential_isolation"]["credential_env_absent"] is True
changed=out["changed_files"][0]
assert changed["path"]=="hello.py" and changed["deleted"] is False
def test_isolated_executor_source_drift_denied(tmp_path,monkeypatch):
archive=make_archive(tmp_path); monkeypatch.setattr(mod,"assert_uncredentialed",lambda:None)
request={
"schema":"aios.isolated-execution.request.v1","request_id":"task-req-0000000000000001",
"source_archive":str(archive),
"source_archive_sha256":mod.sha256(archive.read_bytes()),
"change_set":[{"path":"hello.py","operation":"write_file","content":"x\n","expected_sha256":"0"*64}],
"verification":[]
}
req=tmp_path/"req.json"; res=tmp_path/"res.json"; req.write_text(json.dumps(request))
with pytest.raises(mod.IsolatedExecutorError,match="SOURCE_DRIFT"):
mod.execute_request(req,res)
def test_credential_boundary_eacces_is_accepted(monkeypatch):
class DeniedDirectory:
def exists(self): return True
def is_symlink(self): return False
def is_dir(self): return True
def iterdir(self): raise PermissionError(13,"permission denied")
monkeypatch.setattr(mod.os,"access",lambda *_args,**_kwargs:False)
assert mod._credential_exposure(DeniedDirectory()) is None
def test_systemd_unit_has_no_credentials_and_private_network():
unit=(ROOT/"deploy/systemd/aios-online-agent-isolated-executor.service").read_text()
assert "PrivateNetwork=true" in unit
assert "InaccessiblePaths=/run/credentials" in unit
assert "LoadCredential" not in unit
assert "SupplementaryGroups=gitops" not in unit
+524
View File
@@ -0,0 +1,524 @@
from __future__ import annotations
import hashlib
import json
from pathlib import Path
import sys
from types import SimpleNamespace
import pytest
ROOT = Path(__file__).resolve().parents[1]
APP = ROOT / "active-runtime" / "app"
sys.path.insert(0, str(APP))
from aios_chatgpt_bridge_v2 import ( # noqa: E402
BRIDGE_OPERATION_CONTRACTS,
BridgeV2Adapter,
)
from aios_chatgpt_bridge_v2_server import GovernedRuntime # noqa: E402
from aios_governed_task_bootstrap import BootstrapError, GovernedTaskBinding, GovernedTaskBootstrap, _task_key # noqa: E402
from aios_online_agent_execution import ( # noqa: E402
ExecutionProjectionError,
GovernedExecutionBinding,
OnlineTaskProjectionClient,
project_execution_envelope,
project_online_task,
validate_execution_envelope,
)
import aios_online_agent_worker as worker_module # noqa: E402
from aios_online_agent_worker import ExecutionEnvelope, WorkerError, WorkerSupervisor, governed_commit, load_task_state # noqa: E402
BASE = "a642b045775b9154a00ae78543037fbef4fb447b"
TASK = "p0-claim-online-agent-auto-execution-20260817"
JOB = "job-2031a9e5682a4238f98e3ac7aee8a593"
BRANCH = "agent/p0-claim-online-agent-auto-execution-20260817"
def binding() -> GovernedExecutionBinding:
return GovernedExecutionBinding.from_mapping(
{
"task_id": TASK,
"job_id": JOB,
"repository": "aios/aios",
"base_oid": BASE,
"expected_branch_oid": BASE,
"target_branch": BRANCH,
"branch_mode": "CREATE_NEW",
"generation": 1,
"idempotency_key": "p0-claim-online-agent-auto-execution-v1",
}
)
def task_without_envelope() -> dict:
return {
"task_id": TASK,
"goal": "make governed Online Tasks automatically executable",
"instructions": "project, claim, heartbeat, and preserve exact lineage",
"required_capabilities": ["git.read", "git.test"],
"metadata": {},
}
def test_project_online_task_builds_execution_metadata_from_binding() -> None:
projected = project_online_task(binding(), task_without_envelope())
execution = projected["metadata"]["execution"]
assert execution["schema"] == "aios.online-agent.execution.v2"
assert execution["job_id"] == JOB
assert execution["task_id"] == TASK
assert execution["repository"] == "aios/aios"
assert execution["base_oid"] == BASE
assert execution["expected_branch_oid"] == BASE
assert execution["target_branch"] == BRANCH
assert execution["generation"] == 1
assert projected["metadata"]["execution_projection"]["execution_digest"] == execution["execution_digest"]
def test_job_to_task_execution_projection(tmp_path: Path) -> None:
exact = binding()
durable = GovernedTaskBinding(
task_id=exact.task_id,
job_id=exact.job_id,
repository=exact.repository,
base_oid=exact.base_oid,
expected_branch_oid=exact.expected_branch_oid,
target_branch=exact.target_branch,
branch_mode=exact.branch_mode,
generation=exact.generation,
idempotency_key=exact.idempotency_key,
risk="engineering",
description="automatic claim infrastructure",
)
class Bootstrap:
def create(self, _request: dict) -> dict:
return durable.response()
def online_task_projection(self, _task_id: str, online: dict, execution: dict | None, **_kwargs: object) -> dict:
return project_online_task(exact, online, execution)
def get_binding(self, _task_id: str) -> GovernedTaskBinding:
return durable
def execution_binding(self, _binding: GovernedTaskBinding) -> GovernedExecutionBinding:
return exact
class Projector:
def __init__(self) -> None:
self.calls = 0
def create(self, projected: dict, *, binding: GovernedExecutionBinding) -> dict:
self.calls += 1
assert binding == exact
return {**projected, "status": "QUEUED"}
runtime = GovernedRuntime.__new__(GovernedRuntime)
runtime.bootstrap = Bootstrap()
runtime.bindings = {}
runtime.online_task_projector = Projector()
runtime.projection_outbox_root = tmp_path / "projection-outbox"
runtime.projection_outbox_root.mkdir()
response = GovernedRuntime.create_task(
runtime,
{
"task_id": TASK,
"repository": "aios/aios",
"base_oid": BASE,
"target_branch": BRANCH,
"risk": "engineering",
"description": "automatic claim infrastructure",
"idempotency_key": exact.idempotency_key,
"online_task": task_without_envelope(),
},
)
assert response["online_task_projection"]["status"] == "PROJECTED"
assert response["online_task_projection"]["task_status"] == "QUEUED"
assert runtime.online_task_projector.calls == 1
def test_execution_envelope_schema_validation() -> None:
envelope = project_execution_envelope(binding())
validate_execution_envelope(envelope, binding=binding())
malformed = dict(envelope)
malformed["schema"] = "aios.online-agent.execution.v1"
with pytest.raises(ExecutionProjectionError, match="EXECUTION_ENVELOPE_SCHEMA_VALIDATION"):
validate_execution_envelope(malformed, binding=binding())
def test_projection_response_must_equal_submitted_execution(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None:
submitted = project_online_task(binding(), task_without_envelope(), {"action": "source_capture", "source_signals": ["submitted"]})
returned = project_online_task(binding(), task_without_envelope(), {"action": "source_capture", "source_signals": ["different"]})
returned.update({"status": "QUEUED"})
class Response:
status = 201
def __enter__(self):
return self
def __exit__(self, *_args: object) -> None:
return None
def read(self) -> bytes:
return json.dumps(returned).encode("utf-8")
monkeypatch.setattr("aios_online_agent_execution.urllib.request.urlopen", lambda *_args, **_kwargs: Response())
token = tmp_path / "control.token"
token.write_text("x" * 40, encoding="utf-8")
with pytest.raises(ExecutionProjectionError, match="ONLINE_TASK_PROJECTION_RESPONSE_BINDING_INVALID"):
OnlineTaskProjectionClient("http://127.0.0.1:1", token).create(submitted, binding=binding())
class _ProjectedBridge:
def __init__(self, value: dict) -> None:
self.value = value
self.calls = 0
def project_execution(self, _task: dict, _execution: dict | None = None) -> tuple[int, dict]:
self.calls += 1
return 200, self.value
class _FakeAgent:
def __init__(self, tasks: list[dict]) -> None:
self.tasks = tasks
self.claimed: list[str] = []
def poll(self) -> list[dict]:
return self.tasks
def claim(self, task_id: str) -> tuple[int, dict]:
self.claimed.append(task_id)
return 200, {"task_id": task_id, "lease_token": "x" * 32, "attempt": 1, "revision": 2}
def test_task_visible_to_worker_claimable(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None:
monkeypatch.setattr(worker_module, "STATE_ROOT", tmp_path)
supervisor = WorkerSupervisor("runtime-token-not-secret")
supervisor.bridge = _ProjectedBridge(project_execution_envelope(binding()))
envelope, source = supervisor.resolve_envelope(task_without_envelope())
assert source == "recovered_projection"
assert envelope.task_id == TASK
assert envelope.job_id == JOB
assert envelope.action == "source_capture"
def test_task_claim_after_projection(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None:
monkeypatch.setattr(worker_module, "STATE_ROOT", tmp_path)
supervisor = WorkerSupervisor("runtime-token-not-secret")
fake_agent = _FakeAgent([task_without_envelope()])
supervisor.agent = fake_agent
supervisor.bridge = _ProjectedBridge(project_execution_envelope(binding()))
processed: list[tuple[str, str]] = []
supervisor.process_claim = lambda task, claim, envelope: processed.append((task["task_id"], envelope.execution_digest if hasattr(envelope, "execution_digest") else envelope.digest)) # type: ignore[method-assign]
assert supervisor.once() == 0
assert fake_agent.claimed == [TASK]
assert processed and processed[0][0] == TASK
def test_claim_heartbeat_source_capture_and_result_lifecycle(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None:
execution = project_execution_envelope(binding(), {"action": "source_capture", "source_signals": ["agent"]})
task = task_without_envelope()
task["metadata"] = {"execution": execution}
class Agent:
def __init__(self) -> None:
self.heartbeats: list[str] = []
self.results: list[dict] = []
def heartbeat(self, _task_id: str, _lease_token: str, stage: str) -> tuple[int, dict]:
self.heartbeats.append(stage)
return 200, {}
def get(self, _task_id: str) -> tuple[int, dict]:
return 200, task
def result(self, _task_id: str, _lease_token: str, _key: str, result: dict) -> tuple[int, dict]:
self.results.append(result)
return 200, {}
worktree = tmp_path / "worktree"
source = worktree / "active-runtime" / "app" / "agent_auto_fixture.py"
source.parent.mkdir(parents=True)
source.write_text("VALUE = 1\n", encoding="utf-8")
monkeypatch.setattr(worker_module, "STATE_ROOT", tmp_path / "state")
monkeypatch.setattr(worker_module, "new_run_root", lambda: tmp_path / "run")
monkeypatch.setattr(worker_module, "prepare_worktree", lambda *_args, **_kwargs: worktree)
monkeypatch.setattr(worker_module, "cleanup_run_root", lambda *_args, **_kwargs: None)
agent = Agent()
supervisor = WorkerSupervisor("runtime-token-not-secret")
supervisor.agent = agent
supervisor.bridge = _ProjectedBridge(execution)
envelope, _ = supervisor.resolve_envelope(task)
supervisor.process_claim(task, {"task_id": TASK, "lease_token": "l" * 32, "attempt": 2, "revision": 8}, envelope)
assert agent.heartbeats and agent.heartbeats[0] == "claimed"
assert len(agent.results) == 1
assert agent.results[0]["status"] == "SUCCEEDED"
assert agent.results[0]["classification"] == "NEEDS_REWORK_SOURCE_CAPTURE"
def test_dynamic_task_binding_no_hardcoded_task_id() -> None:
worker = (APP / "aios_online_agent_worker.py").read_text(encoding="utf-8")
assert TASK not in worker
assert BASE not in worker
for name in ("aios-online-agent-worker.service", "aios-online-engineering-consumer.service"):
unit = (ROOT / "deploy" / "systemd" / name).read_text(encoding="utf-8")
assert TASK not in unit
assert JOB not in unit
assert BASE not in unit
assert BRANCH not in unit
assert "AIOS_ENGINEERING_PLAN_FILE" not in unit
assert "aios_online_agent_worker.py" in unit
projection = (ROOT / "deploy" / "systemd" / "aios-chatgpt-bridge-v2-agent-task-projection.conf").read_text(encoding="utf-8")
assert "AIOS_ONLINE_TASK_PROJECTION_ENABLED=1" in projection
assert "LoadCredential=agent_task_control_token:" in projection
assert "LoadCredentialEncrypted=" not in projection
assert "Bearer " not in projection
def test_durable_task_key_has_collision_resistant_exact_identity() -> None:
assert _task_key("task:a") != _task_key("task-a")
assert _task_key("x" * 140 + "a") != _task_key("x" * 140 + "b")
def test_existing_binding_rejects_idempotency_head_and_generation_drift(tmp_path: Path) -> None:
class Authority:
@staticmethod
def commit_exists(_oid: str) -> bool:
return True
@staticmethod
def read_ref(_ref: str) -> None:
return None
bootstrap = GovernedTaskBootstrap(Authority(), tmp_path, remote_name="origin", token_path=tmp_path / "token", ssh_key_path=tmp_path / "key")
request = {
"task_id": TASK,
"repository": "aios/aios",
"base_oid": BASE,
"target_branch": BRANCH,
"risk": "engineering",
"description": "automatic claim infrastructure",
"idempotency_key": "p0-claim-online-agent-auto-execution-v1",
"expected_branch_oid": BASE,
"generation": 1,
}
bootstrap.create(request)
for drift in (
{"idempotency_key": "p0-claim-online-agent-auto-execution-v2"},
{"expected_branch_oid": "b" * 40},
{"generation": 2},
):
with pytest.raises(BootstrapError, match="TASK_BINDING_CONFLICT"):
bootstrap.create({**request, **drift})
def test_existing_branch_without_durable_parent_ownership_is_denied(tmp_path: Path) -> None:
class Authority:
@staticmethod
def commit_exists(_oid: str) -> bool:
return True
@staticmethod
def read_ref(_ref: str) -> str:
return BASE
bootstrap = GovernedTaskBootstrap(Authority(), tmp_path, remote_name="origin", token_path=tmp_path / "token", ssh_key_path=tmp_path / "key")
with pytest.raises(BootstrapError, match="EXISTING_BRANCH_OWNERSHIP_REQUIRED"):
bootstrap.create({
"task_id": TASK,
"repository": "aios/aios",
"base_oid": BASE,
"target_branch": BRANCH,
"risk": "engineering",
"description": "automatic claim infrastructure",
"idempotency_key": "p0-claim-online-agent-auto-execution-v1",
"expected_branch_oid": BASE,
"generation": 2,
})
@pytest.mark.parametrize(
("field", "value", "error"),
[
("repository", "other/repo", "WRONG_REPOSITORY_ENVELOPE_DENY"),
("target_branch", "agent/wrong-branch", "WRONG_BRANCH_ENVELOPE_DENY"),
("generation", 2, "WRONG_GENERATION_ENVELOPE_DENY"),
],
)
def test_wrong_lineage_envelope_deny(field: str, value: object, error: str) -> None:
envelope = project_execution_envelope(binding())
envelope[field] = value
with pytest.raises(ExecutionProjectionError, match=error):
validate_execution_envelope(envelope, binding=binding())
def test_capability_mismatch_deny() -> None:
envelope = project_execution_envelope(
binding(),
{"action": "source_capture", "capability_scope": ["git.read", "governed.write.file"]},
)
with pytest.raises(ExecutionProjectionError, match="CAPABILITY_MISMATCH_DENY"):
validate_execution_envelope(envelope, binding=binding(), worker_capabilities=["git.read"])
def test_governance_lineage_override_deny() -> None:
envelope = project_execution_envelope(binding())
envelope["governance"] = {**envelope["governance"], "job_id": "job-wrong-lineage"}
with pytest.raises(ExecutionProjectionError, match="JOB_BINDING_CONFLICT"):
validate_execution_envelope(envelope, binding=binding())
def test_malformed_persisted_envelope_does_not_fallback() -> None:
task = task_without_envelope()
task["metadata"] = {"execution": {"schema": "broken"}}
bridge = _ProjectedBridge(project_execution_envelope(binding()))
supervisor = WorkerSupervisor("runtime-token-not-secret")
supervisor.bridge = bridge
with pytest.raises(WorkerError, match="EXECUTION_ENVELOPE_SCHEMA_VALIDATION"):
supervisor.resolve_envelope(task)
assert bridge.calls == 0
def test_self_consistent_persisted_wrong_lineage_is_denied_by_authority() -> None:
task = task_without_envelope()
wrong = binding()
wrong = GovernedExecutionBinding.from_mapping({**wrong.public(), "job_id": "job-00000000000000000000000000000000"})
task["metadata"] = {"execution": project_execution_envelope(wrong)}
supervisor = WorkerSupervisor("runtime-token-not-secret")
supervisor.bridge = _ProjectedBridge(project_execution_envelope(binding()))
with pytest.raises(WorkerError, match="TASK_PERSISTED_BINDING_CONFLICT"):
supervisor.resolve_envelope(task)
def test_projection_control_missing_denies_before_governed_side_effect() -> None:
class Bootstrap:
called = False
def create(self, _request: dict) -> dict:
self.called = True
raise AssertionError("must not create governed state")
runtime = GovernedRuntime.__new__(GovernedRuntime)
runtime.bootstrap = Bootstrap()
runtime.online_task_projector = None
with pytest.raises(RuntimeError, match="ONLINE_TASK_PROJECTION_CONTROL_UNAVAILABLE"):
GovernedRuntime.create_task(runtime, {"task_id": TASK, "require_online_task_projection": True})
assert runtime.bootstrap.called is False
def test_source_capture_projection_cannot_write() -> None:
envelope = project_execution_envelope(binding())
assert envelope["action"] == "source_capture"
assert envelope["change_set"] == []
assert envelope["verification"] == []
assert envelope["governance"]["writer_lease"] is False
assert all(envelope["governance"][field] is False for field in ("main_write", "production_write", "tag_write", "baseline_write", "force_push", "protected_ref_bypass"))
def test_source_capture_state_allows_next_attempt_deterministic_digest(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None:
monkeypatch.setattr(worker_module, "STATE_ROOT", tmp_path)
state_path = tmp_path / "tasks" / f"{TASK}.json"
state_path.parent.mkdir(parents=True)
state_path.write_text(json.dumps({"task_id": TASK, "execution_digest": "a" * 64, "phase": "source_capture_done"}), encoding="utf-8")
assert load_task_state(TASK, "b" * 64) is None
def test_bridge_exposes_controlled_projection_operation() -> None:
seen: list[dict] = []
adapter = BridgeV2Adapter(
runtime_status={},
handlers={"projectOnlineAgentExecution": lambda request: seen.append(dict(request)) or project_execution_envelope(binding())},
)
response = adapter.projectOnlineAgentExecution({"task_id": TASK})
assert response["job_id"] == JOB
assert seen == [{"task_id": TASK}]
assert BRIDGE_OPERATION_CONTRACTS["projectOnlineAgentExecution"]["path"] == "/v2/execution-envelope"
def test_execution_digest_is_canonical() -> None:
envelope = project_execution_envelope(binding())
material = {key: value for key, value in envelope.items() if key != "execution_digest"}
expected = hashlib.sha256(json.dumps(material, ensure_ascii=False, sort_keys=True, separators=(",", ":")).encode()).hexdigest()
assert envelope["execution_digest"] == expected
def test_governed_commit_uses_existing_exact_job_without_recreating(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None:
execution = project_execution_envelope(
binding(),
{
"action": "deterministic",
"change_set": [{"operation": "write_file", "path": "result.txt", "content": "ok\n"}],
"governance": {"allowed_files": ["result.txt"]},
},
)
envelope = ExecutionEnvelope.from_task({"task_id": TASK, "metadata": {"execution": execution}})
(tmp_path / "result.txt").write_text("ok\n", encoding="utf-8")
commit_oid = "b" * 40
class Bridge:
def __init__(self) -> None:
self.routes: list[str] = []
def call(self, route: str, body: dict) -> tuple[int, dict]:
self.routes.append(route)
if route == "/v2/job":
assert body == {"job_id": JOB}
else:
assert body["task_id"] == TASK
assert body["job_id"] == JOB
if route == "/v2/writer-lease":
return 200, {
"task_id": TASK,
"job_id": JOB,
"repository": "aios/aios",
"base_oid": BASE,
"expected_branch_oid": BASE,
"branch_mode": "CREATE_NEW",
"generation": 1,
"target_branch": BRANCH,
}
if route == "/v2/commit-files":
return 200, {
"job_id": JOB,
"target_branch": BRANCH,
"generation": 1,
"branch_mode": "CONTINUE_EXISTING",
"commit_oid": commit_oid,
"commit_visible": True,
"receipt": {
"remote_verified": True,
"authority_oid": commit_oid,
"api_branch_oid": commit_oid,
"git_remote_oid": commit_oid,
},
}
if route == "/v2/job":
return 200, {
"state": "COMPLETED",
"receipt": {
"remote_verified": True,
"authority_oid": commit_oid,
"api_branch_oid": commit_oid,
"git_remote_oid": commit_oid,
},
"branch_leases": [{
"job_id": JOB,
"branch": BRANCH,
"state": "ACTIVE",
"generation": 1,
"current_head_oid": commit_oid,
}],
}
raise AssertionError(route)
bridge = Bridge()
monkeypatch.setattr(worker_module.subprocess, "run", lambda *args, **kwargs: SimpleNamespace(returncode=0, stdout=commit_oid + "\n", stderr=""))
heartbeat = SimpleNamespace(ensure=lambda: None)
result = governed_commit(envelope, tmp_path, bridge, heartbeat)
assert bridge.routes == ["/v2/writer-lease", "/v2/commit-files", "/v2/job"]
assert result["job_id"] == JOB
assert result["commit_oid"] == commit_oid
+60
View File
@@ -0,0 +1,60 @@
from pathlib import Path
import sys
import pytest
ROOT=Path(__file__).resolve().parents[1]
sys.path.insert(0,str(ROOT/"active-runtime/app"))
from aios_online_agent_execution import GovernedExecutionBinding
from aios_online_agent_plan_store import ExecutionPlanStore, ExecutionPlanError
BASE="a642b045775b9154a00ae78543037fbef4fb447b"
HEAD="71523aef6e48b1e2fa516c456ca189b7a039104b"
def binding(head=HEAD):
return GovernedExecutionBinding.from_mapping({
"task_id":"task-plan-a","job_id":"job-plan-a","repository":"aios/aios","base_oid":BASE,
"expected_branch_oid":head,"target_branch":"agent/task-plan-a","branch_mode":"CONTINUE_EXISTING",
"generation":1,"idempotency_key":"governed-plan-a",
})
def request():
return {
"schema":"aios.online-agent.plan.v1","task_id":"task-plan-a","job_id":"job-plan-a",
"source_head_oid":HEAD,"generation":1,"idempotency_key":"plan-task-a-v1",
"execution":{
"action":"deterministic",
"change_set":[{"path":"x.py","operation":"write_file","content":"x=1\n"}],
"verification":[{"label":"compile","argv":["/usr/bin/python3","-m","py_compile","x.py"],"cwd":".","timeout_seconds":30}],
"capability_scope":["git.read","git.test"],
}
}
def test_plan_register_and_resolve(tmp_path):
store=ExecutionPlanStore(tmp_path)
created=store.register(binding(),request())
assert created["idempotent"] is False
resolved=store.resolve(binding())
assert resolved["plan_digest"]==created["plan_digest"]
assert resolved["execution"]["action"]=="deterministic"
def test_plan_is_idempotent(tmp_path):
store=ExecutionPlanStore(tmp_path); req=request()
first=store.register(binding(),req); second=store.register(binding(),req)
assert second["idempotent"] is True
assert first["plan_digest"]==second["plan_digest"]
def test_plan_source_head_drift_denied(tmp_path):
store=ExecutionPlanStore(tmp_path); store.register(binding(),request())
with pytest.raises(ExecutionPlanError,match="SOURCE_HEAD_DRIFT"):
store.resolve(binding("b"*40))
def test_plan_replacement_requires_cas(tmp_path):
store=ExecutionPlanStore(tmp_path); first=store.register(binding(),request())
changed=request(); changed["idempotency_key"]="plan-task-a-v2"
changed["execution"]["change_set"][0]["content"]="x=2\n"
with pytest.raises(ExecutionPlanError,match="REPLACEMENT_CAS_MISMATCH"):
store.register(binding(),changed)
changed["expected_previous_plan_digest"]=first["plan_digest"]
second=store.register(binding(),changed)
assert second["plan_digest"]!=first["plan_digest"]
+245
View File
@@ -0,0 +1,245 @@
from __future__ import annotations
from pathlib import Path
from types import SimpleNamespace
import json
import sys
import pytest
ROOT=Path(__file__).resolve().parents[1]
APP=ROOT/"active-runtime/app"
sys.path.insert(0,str(APP))
from aios_online_agent_execution import GovernedExecutionBinding, project_execution_envelope
from aios_governed_task_bootstrap import GovernedTaskBinding, GovernedTaskBootstrap
from aios_gitops_job_controller import GitOpsJobController, JobState, JobStore
from aios_continuation_executor import ContinuationExecutionError, DetachedGitWorkspace
from aios_online_agent_worker import (
BridgeClient,
IsolatedDeterministicExecutor,
load_task_state,
_claim_order,
_isolated_executor_wait_seconds,
_verify_converged_job,
WorkerError,
)
BASE="a642b045775b9154a00ae78543037fbef4fb447b"
HEAD="71523aef6e48b1e2fa516c456ca189b7a039104b"
TASK="p0-claim-online-agent-auto-execution-20260817"
JOB="job-2031a9e5682a4238f98e3ac7aee8a593"
BRANCH="agent/p0-claim-online-agent-auto-execution-20260817"
def test_source_signals_are_bounded_by_projection():
binding=GovernedExecutionBinding.from_mapping({
"task_id":TASK,"job_id":JOB,"repository":"aios/aios","base_oid":BASE,
"expected_branch_oid":BASE,"target_branch":BRANCH,"branch_mode":"CREATE_NEW",
"generation":1,"idempotency_key":"p0-claim-online-agent-auto-execution-a642b04-v1",
})
execution=project_execution_envelope(binding,{
"action":"source_capture",
"source_signals":["x"*5000,"x"*5000,"trace credential tool "*100],
})
assert len(execution["source_signals"]) <= 16
assert all(0 < len(item) <= 512 for item in execution["source_signals"])
def test_same_job_create_new_binding_normalizes_to_current_head():
binding=GovernedTaskBinding(
task_id=TASK,job_id=JOB,repository="aios/aios",base_oid=BASE,
expected_branch_oid=BASE,target_branch=BRANCH,branch_mode="CREATE_NEW",
generation=1,idempotency_key="p0-claim-online-agent-auto-execution-a642b04-v1",
risk="engineering",description="automatic claim",
)
bootstrap=GovernedTaskBootstrap.__new__(GovernedTaskBootstrap)
bootstrap.authority=SimpleNamespace(read_ref=lambda ref: HEAD)
class Connection:
def __enter__(self): return self
def __exit__(self,*a): return False
def execute(self,*a,**k):
return SimpleNamespace(fetchall=lambda:[{
"current_head_oid":HEAD,"generation":1,"state":"ACTIVE","lineage_anchor_oid":BASE
}])
bootstrap._store=lambda _task:(SimpleNamespace(_connect=lambda:Connection()),None)
normalized=bootstrap.execution_binding(binding)
assert normalized.base_oid == BASE
assert normalized.expected_branch_oid == HEAD
assert normalized.branch_mode == "CONTINUE_EXISTING"
assert normalized.generation == 1
def test_recovery_receipt_requires_exact_oid_and_active_lease():
envelope=SimpleNamespace(job_id=JOB,target_branch=BRANCH,generation=1)
response={
"state":"COMPLETED",
"receipt":{"remote_verified":True,"authority_oid":HEAD,"api_branch_oid":HEAD,"git_remote_oid":HEAD},
"branch_leases":[{
"job_id":JOB,"branch":BRANCH,"state":"ACTIVE","generation":1,"current_head_oid":HEAD
}],
}
_verify_converged_job(response,envelope,HEAD)
bad={**response,"receipt":{**response["receipt"],"git_remote_oid":BASE}}
with pytest.raises(WorkerError,match="GOVERNED_RECEIPT_OID_MISMATCH"):
_verify_converged_job(bad,envelope,HEAD)
def test_executor_unit_is_uncredentialed():
unit=(ROOT/"deploy/systemd/aios-online-agent-isolated-executor.service").read_text()
assert "PrivateNetwork=true" in unit
assert "RestrictAddressFamilies=AF_UNIX AF_INET" in unit
assert "IPAddressDeny=any" in unit
assert "IPAddressAllow=localhost" in unit
assert "InaccessiblePaths=/run/credentials" in unit
assert "LoadCredential" not in unit
assert "SupplementaryGroups=gitops" not in unit
def test_isolated_executor_wait_budget_includes_safety_margin():
assert _isolated_executor_wait_seconds(({"timeout_seconds":1200},)) == 1230
def test_prepared_deterministic_plan_precedes_source_capture_at_same_priority():
deterministic=SimpleNamespace(action="deterministic",task_id="task-deterministic")
source_capture=SimpleNamespace(action="source_capture",task_id="task-source-capture")
task={"priority":100,"created_at":"2026-08-17T00:00:00Z"}
ordered=sorted(((task,source_capture),(task,deterministic)),key=_claim_order)
assert ordered[0][1] is deterministic
def test_plan_registry_gets_precedence_when_task_has_no_execution(monkeypatch):
client=BridgeClient("agent-token-placeholder")
seen=[]
monkeypatch.setattr(client,"call",lambda route,body:(seen.append((route,body)) or (200,{})))
task={"task_id":TASK,"required_capabilities":["git.read"]}
client.project_execution(task)
assert seen == [("/v2/execution-envelope",{"task_id":TASK,"required_capabilities":["git.read"]})]
client.project_execution(task,{"action":"source_capture","source_signals":["trace"]})
assert seen[-1][1]["execution"]["action"] == "source_capture"
def test_executor_exchange_files_are_group_readable_and_directories_setgid(tmp_path):
request=tmp_path/"request.json"
IsolatedDeterministicExecutor._atomic(request,b"{}")
assert request.stat().st_mode & 0o777 == 0o640
tmpfiles=(ROOT/"deploy/tmpfiles.d/aios-online-agent-executor.conf").read_text()
assert tmpfiles.count(" 2770 ") == 5
def test_precommit_state_recovery_requires_a_later_official_attempt(monkeypatch,tmp_path):
import aios_online_agent_worker as worker_module
monkeypatch.setattr(worker_module,"STATE_ROOT",tmp_path)
state_path=tmp_path/"tasks"/f"{TASK}.json"
state_path.parent.mkdir(parents=True)
state_path.write_text(json.dumps({"task_id":TASK,"execution_digest":"a"*64,"phase":"workspace_prepared"}))
assert load_task_state(TASK,"b"*64) is None
state_path.write_text(json.dumps({"task_id":TASK,"execution_digest":"a"*64,"phase":"verification_passed","attempt":16}))
with pytest.raises(WorkerError,match="TASK_PAYLOAD_DRIFT"):
load_task_state(TASK,"b"*64,attempt=16)
assert load_task_state(TASK,"b"*64,attempt=17) is None
def test_reconciliation_state_recovery_requires_later_attempt_and_same_job(monkeypatch,tmp_path):
import aios_online_agent_worker as worker_module
monkeypatch.setattr(worker_module,"STATE_ROOT",tmp_path)
state_path=tmp_path/"tasks"/f"{TASK}.json"
state_path.parent.mkdir(parents=True)
state_path.write_text(json.dumps({
"task_id":TASK,
"execution_digest":"a"*64,
"phase":"reconciliation_required",
"attempt":18,
"job_id":JOB,
"details":{"api_status":503,"error":"WORKTREE_DIRTY_SCOPE_MISMATCH"},
}))
with pytest.raises(WorkerError,match="TASK_PAYLOAD_DRIFT"):
load_task_state(TASK,"b"*64,attempt=18,continuation_job_id=JOB,continuation_head_oid=HEAD)
assert load_task_state(TASK,"b"*64,attempt=19,continuation_job_id=JOB,continuation_head_oid=HEAD) is None
with pytest.raises(WorkerError,match="TASK_PAYLOAD_DRIFT"):
load_task_state(TASK,"b"*64,attempt=19,continuation_job_id="job-other",continuation_head_oid=HEAD)
def test_legacy_dirty_scope_reconciliation_state_is_exactly_bounded(monkeypatch,tmp_path):
import aios_online_agent_worker as worker_module
monkeypatch.setattr(worker_module,"STATE_ROOT",tmp_path)
state_path=tmp_path/"tasks"/f"{TASK}.json"
state_path.parent.mkdir(parents=True)
state_path.write_text(json.dumps({
"task_id":TASK,
"execution_digest":"a"*64,
"phase":"reconciliation_required",
"job_id":JOB,
"details":{"api_status":503,"error":"WORKTREE_DIRTY_SCOPE_MISMATCH"},
}))
assert load_task_state(TASK,"b"*64,attempt=19,continuation_job_id=JOB,continuation_head_oid=HEAD) is None
with pytest.raises(WorkerError,match="TASK_PAYLOAD_DRIFT"):
load_task_state(TASK,"b"*64,attempt=19,continuation_job_id=JOB,continuation_head_oid="not-an-oid")
def test_completed_same_job_begins_one_idempotent_continuation_cycle(tmp_path):
controller=GitOpsJobController(JobStore(tmp_path/"jobs",payload_root=tmp_path/"payloads"))
submitted=controller.submit({
"idempotency_key":"same-job-continuation-test",
"campaign_id":TASK,
"task_id":TASK,
"repository":"aios/aios",
"operation":"GOVERNED_TASK_CREATE",
"branch":BRANCH,
"expected_branch_oid":HEAD,
"payload_digest":"a"*64,
})
job_id=submitted.job_id
controller.advance(job_id,JobState.AUTHORIZED,side_effect_state="AUTHORIZED",expected_revision=0)
controller.lease_job(
job_id,
lease_id="lease-"+job_id,
lineage_anchor_oid=BASE,
current_head_oid=HEAD,
generation=1,
lease_expires_at="2099-01-01T00:00:00+00:00",
expected_revision=1,
)
controller.advance(job_id,JobState.WORKTREE_READY,side_effect_state="WORKTREE_READY",expected_revision=2)
controller.advance(job_id,JobState.EXECUTING,side_effect_state="IN_PROGRESS",expected_revision=3)
controller.advance(job_id,JobState.LOCAL_VERIFIED,side_effect_state="LOCAL_VERIFIED",receipt={"authority_oid":HEAD},expected_revision=4)
controller.advance(job_id,JobState.REMOTE_SYNCING,side_effect_state="IN_PROGRESS",expected_revision=5)
receipt={
"remote_verified":True,
"commit_visible":True,
"authority_oid":HEAD,
"api_branch_oid":HEAD,
"git_remote_oid":HEAD,
}
controller.advance(job_id,JobState.REMOTE_VERIFIED,side_effect_state="REMOTE_VERIFIED",receipt=receipt,expected_revision=6)
controller.advance(job_id,JobState.COMPLETED,side_effect_state="REMOTE_VERIFIED",receipt=receipt,expected_revision=7)
first=controller.begin_continuation_cycle(
job_id,
idempotency_key="same-job-continuation-commit-v1",
expected_head_oid=HEAD,
generation=1,
expected_revision=8,
)
assert first == {"completed":False,"commit_oid":None,"idempotent":False}
assert controller.store.read(job_id)["state"] == "LEASED"
replay=controller.begin_continuation_cycle(
job_id,
idempotency_key="same-job-continuation-commit-v1",
expected_head_oid=HEAD,
generation=1,
expected_revision=8,
)
assert replay == {"completed":False,"commit_oid":None,"idempotent":True}
def test_deterministic_materialization_recovers_only_an_in_scope_dirty_subset(monkeypatch,tmp_path):
workspace=object.__new__(DetachedGitWorkspace)
workspace.path=tmp_path
monkeypatch.setattr(DetachedGitWorkspace,"_dirty_paths",lambda _self:{"old.py"})
workspace.materialize_files({"old.py":b"new-old\n","new.py":b"new\n"})
assert (tmp_path/"old.py").read_bytes() == b"new-old\n"
assert (tmp_path/"new.py").read_bytes() == b"new\n"
monkeypatch.setattr(DetachedGitWorkspace,"_dirty_paths",lambda _self:{"outside.py"})
with pytest.raises(ContinuationExecutionError,match="WORKTREE_DIRTY_SCOPE_MISMATCH"):
workspace.materialize_files({"old.py":b"again\n"})
+763
View File
@@ -0,0 +1,763 @@
from __future__ import annotations
from concurrent.futures import ThreadPoolExecutor
from datetime import datetime, timedelta, timezone
import hashlib
from dataclasses import asdict
from pathlib import Path
import sqlite3
import json
from types import SimpleNamespace
import threading
import pytest
from aios_chatgpt_bridge_v2_server import GovernedRuntime
from aios_continuation_executor import ContinuationPermissions, ContinuationRuntimeBinding
from aios_continuation_lease import WriterLeaseRequestV2
from aios_gitops_job_controller import GitOpsJobController, JobError, JobStore
from aios_governed_task_bootstrap import GovernedTaskBootstrap
TASK_ID = "gov-writer-lease-recovery-test"
BRANCH = "agent/writer-lease-recovery-test"
BASE = "a" * 40
JOB_ID_PREFIX = "writer-lease-runtime-lineage"
class LocalAuthority:
def __init__(self, head: str | None = None) -> None:
self.head = head
def read_ref(self, _ref: str) -> str | None:
return self.head
def commit_exists(self, _oid: str) -> bool:
return True
def ancestry_check(self, _older: str, _newer: str) -> bool:
return True
def writer_request(task_id: str = TASK_ID, **overrides: object) -> dict[str, object]:
request: dict[str, object] = {
"schema": WriterLeaseRequestV2.schema,
"task_id": task_id,
"repository": "aios/aios",
"branch_mode": "CREATE_NEW",
"base_oid": BASE,
"expected_branch_oid": BASE,
"target_branch": BRANCH,
"generation": 1,
"lease_seconds": 120,
}
request.update(overrides)
return request
def make_runtime(tmp_path: Path, task_id: str = TASK_ID, *, authority_head: str | None = None) -> GovernedRuntime:
runtime = GovernedRuntime.__new__(GovernedRuntime)
runtime.config = SimpleNamespace(repository="aios/aios", state_root=tmp_path)
runtime.authority = LocalAuthority(authority_head)
runtime.bindings = {
task_id: {
"task_id": task_id,
"repository": "aios/aios",
"base_oid": BASE,
"expected_branch_oid": BASE,
"target_branch": BRANCH,
"branch_mode": "CREATE_NEW",
"generation": 1,
}
}
runtime.runtimes = {}
runtime.controllers = {}
runtime.bootstrap = GovernedTaskBootstrap(
runtime.authority,
tmp_path,
remote_name="test",
token_path=tmp_path / "token",
ssh_key_path=tmp_path / "key",
)
runtime.bindings = {}
runtime.create_task(
{
"task_id": task_id,
"repository": "aios/aios",
"base_oid": BASE,
"expected_branch_oid": BASE,
"target_branch": BRANCH,
"risk": "engineering",
"description": "governed runtime lineage test",
"idempotency_key": JOB_ID_PREFIX + "-" + task_id,
}
)
job_root = runtime._job_root(task_id)
runtime.controllers[task_id] = GitOpsJobController(
JobStore(job_root, payload_root=job_root / "payloads")
)
def build(
lease: WriterLeaseRequestV2,
*,
job_id: str,
lease_not_before: datetime | None = None,
lease_expires_at: datetime | None = None,
) -> tuple[ContinuationRuntimeBinding, GitOpsJobController, object]:
controller = runtime.controllers.get(lease.task_id)
if controller is None:
root = runtime._job_root(lease.task_id)
controller = GitOpsJobController(JobStore(root, payload_root=root / "payloads"))
now = datetime.now(timezone.utc)
return (
ContinuationRuntimeBinding(
request=lease,
job_id=job_id,
workspace_path=tmp_path / "worktrees" / lease.task_id,
lease_not_before=lease_not_before or now - timedelta(seconds=1),
lease_expires_at=lease_expires_at or now + timedelta(seconds=lease.lease_seconds),
original_receipt_valid=True,
original_grant_consumed=True,
permissions=ContinuationPermissions(),
),
controller,
object(),
)
runtime._build_runtime = build
return runtime
def create_durable_lease(
runtime: GovernedRuntime,
*,
expires_at: str = "2020-01-01T00:00:00+00:00",
) -> GitOpsJobController:
root = runtime._job_root(TASK_ID)
controller = GitOpsJobController(JobStore(root, payload_root=root / "payloads"))
receipt = SimpleNamespace(job_id=runtime.bindings[TASK_ID]["job_id"])
controller.lease_job(
receipt.job_id,
lease_id="writer-lease-recovery",
lineage_anchor_oid=BASE,
current_head_oid=BASE,
generation=1,
lease_expires_at=expires_at,
expected_revision=0,
)
runtime.controllers[TASK_ID] = controller
return controller
def durable_row(runtime: GovernedRuntime) -> tuple[str, int]:
database = runtime._job_root(TASK_ID) / "state.sqlite3"
with sqlite3.connect(f"file:{database}?mode=ro", uri=True) as connection:
row = connection.execute(
"SELECT lease_expires_at, revision FROM branch_leases WHERE lease_id=?",
("writer-lease-recovery",),
).fetchone()
assert row is not None
return row
def test_first_bootstrap_refreshes_only_runtime_without_durable_lease(tmp_path: Path) -> None:
runtime = make_runtime(tmp_path)
result = runtime.bootstrap_writer_lease(writer_request())
assert result["bootstrapped"] is True
assert result["runtime_rehydrated"] is False
assert result["durable_branch_lease_present"] is False
assert result["idempotent"] is False
database = runtime._job_root(TASK_ID) / "state.sqlite3"
with sqlite3.connect(f"file:{database}?mode=ro", uri=True) as connection:
assert connection.execute("SELECT COUNT(*) FROM branch_leases").fetchone()[0] == 0
def test_duplicate_bootstrap_is_idempotent_without_consuming_generation(tmp_path: Path) -> None:
runtime = make_runtime(tmp_path)
first = runtime.bootstrap_writer_lease(writer_request())
second = runtime.bootstrap_writer_lease(writer_request())
assert first["bootstrapped"] is True
assert first["generation"] == 1
assert second["bootstrapped"] is False
assert second["idempotent"] is True
assert second["generation"] == 1
database = runtime._job_root(TASK_ID) / "state.sqlite3"
with sqlite3.connect(f"file:{database}?mode=ro", uri=True) as connection:
assert connection.execute("SELECT COUNT(*) FROM branch_leases").fetchone()[0] == 0
def test_commit_after_bootstrap_reuses_the_exact_runtime_binding(tmp_path: Path) -> None:
runtime = make_runtime(tmp_path)
class RecordingWorkspace:
def __init__(self) -> None:
self.files: dict[str, bytes] = {}
def materialize_files(self, files: dict[str, bytes]) -> None:
self.files = dict(files)
class RecordingExecutor:
def __init__(self) -> None:
self.workspace = RecordingWorkspace()
self.binding = None
self.commit = None
def execute(self, binding: object, commit: object) -> dict[str, object]:
self.binding = binding
self.commit = commit
return {'task_id': binding.request.task_id, 'generation': binding.request.generation, 'files': sorted(self.workspace.files)}
executor = RecordingExecutor()
original_build = runtime._build_runtime
def build(lease: WriterLeaseRequestV2, *, job_id: str, lease_not_before: datetime | None=None, lease_expires_at: datetime | None=None) -> tuple[ContinuationRuntimeBinding, GitOpsJobController, object]:
binding, controller, _ = original_build(lease, job_id=job_id, lease_not_before=lease_not_before, lease_expires_at=lease_expires_at)
return (binding, controller, executor)
runtime._build_runtime = build
runtime.bootstrap_writer_lease(writer_request())
lease_response = runtime.acquire_lease(writer_request())
binding, controller, executor = runtime.runtimes[TASK_ID, 1]
assert lease_response['job_id'] == binding.job_id
assert lease_response['durable_branch_lease_present'] is True
_original_execute = executor.execute
def _execute_with_governed_commit_contract(*args, **kwargs):
_before_complete = kwargs.pop('before_complete', None)
_legacy = dict(_original_execute(*args, **kwargs))
_result = {**_legacy, 'job_id': binding.job_id, 'state': 'COMPLETED', 'side_effect_state': 'REMOTE_VERIFIED', 'receipt': {'authority_oid': 'b' * 40, 'git_remote_oid': 'b' * 40, 'api_branch_oid': 'b' * 40, 'commit_visible': True, 'remote_verified': True, 'force_push': False}}
if _before_complete is not None:
_before_complete('b' * 40)
return _result
executor.execute = _execute_with_governed_commit_contract
_bound_job_id = binding.job_id
result = runtime.commit_files({'task_id': TASK_ID, 'repository': 'aios/aios', 'generation': 1, 'target_branch': BRANCH, 'expected_branch_oid': BASE, 'idempotency_key': 'commit-after-bootstrap-v1', 'path': 'docs/recovery.txt', 'content_base64': 'YQ==', 'message': 'test commit after bootstrap'})
assert result['task_id'] == TASK_ID
assert result['generation'] == 1
assert result['files'] == ['docs/recovery.txt']
assert executor.binding.request.task_id == TASK_ID
assert executor.binding.request.generation == 1
database = runtime._job_root(TASK_ID) / 'state.sqlite3'
with sqlite3.connect(f'file:{database}?mode=ro', uri=True) as connection:
assert connection.execute('SELECT COUNT(*) FROM branch_leases').fetchone()[0] == 1
assert connection.execute('PRAGMA foreign_key_check(branch_leases)').fetchall() == []
assert result['commit_oid'] == 'b' * 40
assert result['commit_visible'] is True
assert result['branch_mode'] == 'CONTINUE_EXISTING'
assert result['target_branch'] == BRANCH
assert int(result['generation']) == 1
with controller.store._connect() as connection:
_persisted = connection.execute('SELECT current_head_oid,revision FROM branch_leases WHERE lease_id=?', ('lease-' + str(_bound_job_id),)).fetchone()
assert _persisted is not None
assert _persisted[0] == 'b' * 40
assert int(_persisted[1]) == 1
fresh_runtime = make_runtime(tmp_path, authority_head='b' * 40)
fresh_response = fresh_runtime.bootstrap_writer_lease(writer_request(branch_mode='CONTINUE_EXISTING', expected_branch_oid='b' * 40))
fresh_binding, _, _ = fresh_runtime.runtimes[TASK_ID, 1]
assert fresh_response['job_id'] == str(_bound_job_id)
assert fresh_response['runtime_rehydrated'] is True
assert fresh_binding.job_id == str(_bound_job_id)
assert fresh_binding.request.expected_branch_oid == 'b' * 40
with fresh_runtime.controllers[TASK_ID].store._connect() as connection:
_fresh_lease = connection.execute('SELECT job_id,current_head_oid,revision FROM branch_leases WHERE lease_id=?', ('lease-' + str(_bound_job_id),)).fetchone()
assert tuple(_fresh_lease) == (str(_bound_job_id), 'b' * 40, 1)
@pytest.mark.parametrize(
("field", "value", "error"),
(
("repository", "other/repository", "LEASE_BINDING_CONFLICT"),
("base_oid", "b" * 40, "LEASE_BINDING_CONFLICT"),
("expected_branch_oid", "b" * 40, "LEASE_BINDING_CONFLICT"),
("target_branch", "agent/wrong-target", "LEASE_BINDING_CONFLICT"),
("generation", 2, "GENERATION_BINDING_CONFLICT"),
),
)
def test_bootstrap_rejects_any_nonmatching_binding_field(
tmp_path: Path, field: str, value: object, error: str
) -> None:
runtime = make_runtime(tmp_path)
with pytest.raises(RuntimeError, match=error):
runtime.bootstrap_writer_lease(writer_request(**{field: value}))
def test_bootstrap_rejects_unknown_task_ownership(tmp_path: Path) -> None:
runtime = make_runtime(tmp_path)
with pytest.raises(RuntimeError, match="TASK_OWNERSHIP_INVALID"):
runtime.bootstrap_writer_lease(writer_request(task_id="unknown-task"))
def test_renew_missing_durable_lease_is_not_bootstrap(tmp_path: Path) -> None:
runtime = make_runtime(tmp_path)
with pytest.raises(RuntimeError, match="LEASE_NOT_FOUND"):
runtime.renew_writer_lease(writer_request())
def test_renew_is_generation_scoped_before_durable_lookup(tmp_path: Path) -> None:
runtime = make_runtime(tmp_path)
create_durable_lease(runtime)
with pytest.raises(RuntimeError, match="GENERATION_BINDING_CONFLICT"):
runtime.renew_writer_lease(writer_request(generation=2))
assert durable_row(runtime) == ("2020-01-01T00:00:00+00:00", 0)
def test_restart_rehydrates_only_from_matching_durable_lease(tmp_path: Path) -> None:
runtime = make_runtime(tmp_path)
create_durable_lease(runtime)
runtime.runtimes.clear()
result = runtime.renew_writer_lease(writer_request())
expires_at, revision = durable_row(runtime)
assert result["recovered"] is True
assert result["runtime_rehydrated"] is True
assert result["durable_branch_lease_present"] is True
assert revision == 1
assert datetime.fromisoformat(expires_at) > datetime.now(timezone.utc)
def test_duplicate_renewal_is_idempotent_after_first_durable_recovery(tmp_path: Path) -> None:
runtime = make_runtime(tmp_path)
create_durable_lease(runtime)
first = runtime.renew_writer_lease(writer_request())
second = runtime.renew_writer_lease(writer_request())
assert first["recovered"] is True
assert first["idempotent"] is False
assert second["recovered"] is False
assert second["idempotent"] is True
assert durable_row(runtime)[1] == 1
def test_update_lease_keeps_expiry_without_optional_argument_and_keeps_cas(tmp_path: Path) -> None:
runtime = make_runtime(tmp_path)
controller = create_durable_lease(runtime)
controller.update_lease(
"writer-lease-recovery",
expected_revision=0,
current_head_oid=BASE,
generation=1,
)
assert durable_row(runtime) == ("2020-01-01T00:00:00+00:00", 1)
with pytest.raises(JobError, match="LEASE_REVISION_CAS_CONFLICT"):
controller.update_lease(
"writer-lease-recovery",
expected_revision=0,
current_head_oid=BASE,
generation=1,
)
def test_two_runtime_connections_resolve_renewal_cas_race_idempotently(tmp_path: Path) -> None:
first = make_runtime(tmp_path)
first_controller = create_durable_lease(first)
second = make_runtime(tmp_path)
second_controller = GitOpsJobController(
JobStore(second._job_root(TASK_ID), payload_root=second._job_root(TASK_ID) / "payloads")
)
second.controllers[TASK_ID] = second_controller
gate = threading.Barrier(2)
def gate_update(controller: GitOpsJobController) -> None:
original = controller.update_lease
def update(*args: object, **kwargs: object) -> None:
gate.wait(timeout=5)
original(*args, **kwargs)
controller.update_lease = update
gate_update(first_controller)
gate_update(second_controller)
with ThreadPoolExecutor(max_workers=2) as pool:
results = list(pool.map(lambda runtime: runtime.renew_writer_lease(writer_request()), (first, second)))
assert sorted((item["recovered"], item["idempotent"]) for item in results) == [
(False, True),
(True, False),
]
assert durable_row(first)[1] == 1
def test_bootstrap_preserves_exact_governed_job_lineage(tmp_path: Path) -> None:
runtime = make_runtime(tmp_path)
expected_job_id = runtime.bindings[TASK_ID]["job_id"]
response = runtime.bootstrap_writer_lease(writer_request())
binding, _, _ = runtime.runtimes[(TASK_ID, 1)]
assert response["job_id"] == expected_job_id
assert binding.job_id == expected_job_id
assert binding.request.job_id == expected_job_id
def test_restart_rehydrate_preserves_exact_governed_job_lineage(tmp_path: Path) -> None:
first = make_runtime(tmp_path)
expected_job_id = first.bindings[TASK_ID]["job_id"]
first.acquire_lease(writer_request())
runtime = make_runtime(tmp_path)
assert runtime is not first
assert runtime.runtimes == {}
response = runtime.bootstrap_writer_lease(writer_request())
binding, _, _ = runtime.runtimes[(TASK_ID, 1)]
assert response["job_id"] == expected_job_id
assert binding.job_id == expected_job_id
assert binding.request.job_id == expected_job_id
def test_wrong_governed_job_id_is_denied_before_runtime_or_lease_side_effect(tmp_path: Path) -> None:
runtime = make_runtime(tmp_path)
with pytest.raises(RuntimeError, match="LEASE_BINDING_CONFLICT"):
runtime.bootstrap_writer_lease(writer_request(job_id="job-wrong-lineage"))
assert runtime.runtimes == {}
database = runtime._job_root(TASK_ID) / "state.sqlite3"
with sqlite3.connect(f"file:{database}?mode=ro", uri=True) as connection:
assert connection.execute("SELECT COUNT(*) FROM branch_leases").fetchone()[0] == 0
assert connection.execute("PRAGMA foreign_key_check(branch_leases)").fetchall() == []
def test_acquire_creates_exact_governed_writer_lease_with_fk_and_cas(tmp_path: Path) -> None:
runtime = make_runtime(tmp_path)
expected_job_id = runtime.bindings[TASK_ID]["job_id"]
response = runtime.acquire_lease(writer_request())
assert response["job_id"] == expected_job_id
assert response["durable_branch_lease_present"] is True
assert response["idempotent"] is False
database = runtime._job_root(TASK_ID) / "state.sqlite3"
with sqlite3.connect(f"file:{database}?mode=ro", uri=True) as connection:
foreign_keys = connection.execute(
"PRAGMA foreign_key_list(branch_leases)"
).fetchall()
row = connection.execute(
"SELECT job_id,branch,lineage_anchor_oid,current_head_oid,generation,state,revision "
"FROM branch_leases"
).fetchone()
job = connection.execute(
"SELECT state,side_effect_state,revision FROM jobs WHERE job_id=?",
(expected_job_id,),
).fetchone()
assert connection.execute("PRAGMA foreign_key_check(branch_leases)").fetchall() == []
assert any(
fk[2] == "jobs" and fk[3] == "job_id" and fk[4] == "job_id"
for fk in foreign_keys
)
assert row == (expected_job_id, BRANCH, BASE, BASE, 1, "ACTIVE", 0)
assert job == ("LEASED", "LEASED", 2)
def test_duplicate_acquire_observes_same_durable_writer_lease(tmp_path: Path) -> None:
runtime = make_runtime(tmp_path)
expected_job_id = runtime.bindings[TASK_ID]["job_id"]
first = runtime.acquire_lease(writer_request())
second = runtime.acquire_lease(writer_request())
assert first["job_id"] == expected_job_id
assert second["job_id"] == expected_job_id
assert second["idempotent"] is True
database = runtime._job_root(TASK_ID) / "state.sqlite3"
with sqlite3.connect(f"file:{database}?mode=ro", uri=True) as connection:
leases = connection.execute(
"SELECT lease_id,job_id,revision FROM branch_leases"
).fetchall()
job = connection.execute(
"SELECT state,revision FROM jobs WHERE job_id=?",
(expected_job_id,),
).fetchone()
assert leases == [("lease-" + expected_job_id, expected_job_id, 0)]
assert job == ("LEASED", 2)
def test_wrong_job_acquire_is_denied_before_authorize_or_lease(tmp_path: Path) -> None:
runtime = make_runtime(tmp_path)
expected_job_id = runtime.bindings[TASK_ID]["job_id"]
with pytest.raises(RuntimeError, match="LEASE_BINDING_CONFLICT"):
runtime.acquire_lease(writer_request(job_id="job-wrong-lineage"))
database = runtime._job_root(TASK_ID) / "state.sqlite3"
with sqlite3.connect(f"file:{database}?mode=ro", uri=True) as connection:
job = connection.execute(
"SELECT state,side_effect_state,revision FROM jobs WHERE job_id=?",
(expected_job_id,),
).fetchone()
lease_count = connection.execute("SELECT COUNT(*) FROM branch_leases").fetchone()[0]
assert job == ("REQUESTED", "NONE", 0)
assert lease_count == 0
def test_omitted_request_job_id_resolves_the_exact_durable_parent(tmp_path: Path) -> None:
runtime = make_runtime(tmp_path)
expected_job_id = runtime.bindings[TASK_ID]["job_id"]
raw = dict(writer_request())
response = runtime.acquire_lease(raw)
binding, _, _ = runtime.runtimes[(TASK_ID, 1)]
assert response["job_id"] == expected_job_id
assert binding.job_id == expected_job_id
assert binding.request.job_id == expected_job_id
def test_durable_job_expected_head_mismatch_is_denied(tmp_path: Path) -> None:
runtime = make_runtime(tmp_path)
expected_job_id = runtime.bindings[TASK_ID]["job_id"]
controller = runtime.controllers[TASK_ID]
with controller.store._connect() as connection:
row = connection.execute(
"SELECT request_json FROM jobs WHERE job_id=?", (expected_job_id,)
).fetchone()
request = json.loads(row[0])
request["expected_branch_oid"] = "c" * 40
request["binding"]["expected_branch_oid"] = "c" * 40
connection.execute(
"UPDATE jobs SET request_json=? WHERE job_id=?",
(json.dumps(request, sort_keys=True, separators=(",", ":")), expected_job_id),
)
connection.commit()
with pytest.raises(RuntimeError, match="RUNTIME_GOVERNED_JOB_UNRESOLVED"):
runtime.bootstrap_writer_lease(writer_request())
assert runtime.runtimes == {}
def _commit_ready_runtime(tmp_path: Path):
runtime = make_runtime(tmp_path)
runtime.bootstrap_writer_lease(writer_request())
binding, controller, _ = runtime.runtimes[(TASK_ID, 1)]
class Workspace:
def materialize_files(self, _files):
return None
class Executor:
def __init__(self):
self.workspace = Workspace()
self.calls = 0
def execute(self, _binding, _commit, *, before_complete=None):
self.calls += 1
result = {
"job_id": binding.job_id,
"state": "COMPLETED",
"side_effect_state": "REMOTE_VERIFIED",
"receipt": {
"authority_oid": "b" * 40,
"git_remote_oid": "b" * 40,
"api_branch_oid": "b" * 40,
"commit_visible": True,
"remote_verified": True,
"force_push": False,
},
}
if before_complete is not None:
before_complete("b" * 40)
return result
executor = Executor()
runtime.runtimes[(TASK_ID, 1)] = (binding, controller, executor)
runtime.acquire_lease(writer_request())
return runtime, binding, controller, executor
def _commit_request():
return {
"task_id": TASK_ID,
"repository": "aios/aios",
"generation": 1,
"target_branch": BRANCH,
"expected_branch_oid": BASE,
"idempotency_key": "commit-cas-test-v1",
"path": "docs/cas.txt",
"content_base64": "YQ==",
"message": "test commit cas",
}
def test_commit_lease_revision_cas_conflict_is_preserved(tmp_path: Path) -> None:
runtime, _binding, controller, _executor = _commit_ready_runtime(tmp_path)
def conflict(*_args, **_kwargs):
raise JobError("LEASE_REVISION_CAS_CONFLICT")
controller.update_lease = conflict
with pytest.raises(RuntimeError, match="LEASE_CAS_CONFLICT"):
runtime.commit_files(_commit_request())
def test_commit_divergent_durable_head_fails_before_executor(tmp_path: Path) -> None:
runtime, binding, controller, executor = _commit_ready_runtime(tmp_path)
controller.update_lease(
"lease-" + binding.job_id,
expected_revision=0,
current_head_oid="c" * 40,
generation=1,
)
with pytest.raises(RuntimeError, match="LEASE_HEAD_RECONCILIATION_REQUIRED"):
runtime.commit_files(_commit_request())
assert executor.calls == 0
def test_remote_pending_never_advances_durable_head(tmp_path: Path) -> None:
runtime, binding, controller, executor = _commit_ready_runtime(tmp_path)
def pending(_binding, _commit, **_kwargs):
executor.calls += 1
return {
"job_id": binding.job_id,
"state": "RECONCILIATION_REQUIRED",
"side_effect_state": "REMOTE_PUSH_PENDING",
"receipt": {
"authority_oid": "b" * 40,
"desired_remote_oid": "b" * 40,
"remote_verified": False,
},
}
executor.execute = pending
with pytest.raises(RuntimeError, match="RECONCILIATION_REQUIRED"):
runtime.commit_files(_commit_request())
with controller.store._connect() as connection:
row = connection.execute(
"SELECT current_head_oid,revision FROM branch_leases WHERE lease_id=?",
("lease-" + binding.job_id,),
).fetchone()
assert tuple(row) == (BASE, 0)
def test_retry_accepts_exact_head_already_advanced_at_remote_verified_boundary(tmp_path: Path) -> None:
runtime, binding, controller, executor = _commit_ready_runtime(tmp_path)
commit_oid = "b" * 40
controller.update_lease(
"lease-" + binding.job_id,
expected_revision=0,
current_head_oid=commit_oid,
generation=1,
)
receipt = {
"authority_oid": commit_oid,
"git_remote_oid": commit_oid,
"api_branch_oid": commit_oid,
"commit_visible": True,
"remote_verified": True,
"force_push": False,
}
with controller.store._connect() as connection:
connection.execute(
"UPDATE jobs SET state='REMOTE_VERIFIED',side_effect_state='REMOTE_VERIFIED',"
"receipt_json=? WHERE job_id=?",
(json.dumps(receipt, sort_keys=True, separators=(",", ":")), binding.job_id),
)
connection.commit()
result = runtime.commit_files(_commit_request())
assert result["commit_oid"] == commit_oid
assert executor.calls == 1
with controller.store._connect() as connection:
row = connection.execute(
"SELECT current_head_oid,revision FROM branch_leases WHERE lease_id=?",
("lease-" + binding.job_id,),
).fetchone()
assert tuple(row) == (commit_oid, 1)
def test_completed_response_with_invalid_oid_uses_canonical_oid_error(tmp_path: Path) -> None:
runtime, binding, controller, executor = _commit_ready_runtime(tmp_path)
def invalid_oid(_binding, _commit, **_kwargs):
executor.calls += 1
return {
"job_id": binding.job_id,
"state": "COMPLETED",
"side_effect_state": "REMOTE_VERIFIED",
"receipt": {
"authority_oid": "not-an-oid",
"commit_visible": True,
"remote_verified": True,
},
}
executor.execute = invalid_oid
with pytest.raises(RuntimeError, match="GOVERNED_COMMIT_OID_INVALID"):
runtime.commit_files(_commit_request())
with controller.store._connect() as connection:
row = connection.execute(
"SELECT current_head_oid,revision FROM branch_leases WHERE lease_id=?",
("lease-" + binding.job_id,),
).fetchone()
assert tuple(row) == (BASE, 0)
def test_acquire_renews_an_expired_matching_durable_lease_with_revision_cas(tmp_path: Path) -> None:
runtime = make_runtime(tmp_path)
create_durable_lease(runtime)
runtime.runtimes.clear()
result = runtime.acquire_lease(writer_request())
expires_at, revision = durable_row(runtime)
assert result["recovered"] is True
assert result["runtime_rehydrated"] is False
assert result["durable_branch_lease_present"] is True
assert revision == 1
assert datetime.fromisoformat(expires_at) > datetime.now(timezone.utc)
def test_authority_recovers_only_clean_detached_single_attempt_worktree(tmp_path: Path) -> None:
import subprocess
from aios_authority_git_executor import AuthorityGitExecutor
bare = tmp_path / "authority.git"
seed = tmp_path / "seed"
subprocess.run(["git", "init", "--bare", str(bare)], check=True, capture_output=True)
subprocess.run(["git", "init", str(seed)], check=True, capture_output=True)
(seed / "base.txt").write_text("base\n", encoding="utf-8")
subprocess.run(["git", "-C", str(seed), "add", "base.txt"], check=True)
subprocess.run(
["git", "-C", str(seed), "-c", "user.name=test", "-c", "user.email=test@example.invalid",
"commit", "-m", "base"], check=True, capture_output=True,
)
base = subprocess.check_output(["git", "-C", str(seed), "rev-parse", "HEAD"], text=True).strip()
subprocess.run(["git", "-C", str(seed), "push", str(bare), "HEAD:refs/heads/main"], check=True, capture_output=True)
root = tmp_path / "worktrees"
authority = AuthorityGitExecutor(bare, root)
worktree = authority.create_detached_worktree(base, "task")
(worktree / "attempt.txt").write_text("attempt\n", encoding="utf-8")
subprocess.run(["git", "-C", str(worktree), "add", "attempt.txt"], check=True)
subprocess.run(
["git", "-C", str(worktree), "-c", "user.name=test", "-c", "user.email=test@example.invalid",
"commit", "-m", "failed attempt"], check=True, capture_output=True,
)
stale = subprocess.check_output(["git", "-C", str(worktree), "rev-parse", "HEAD"], text=True).strip()
recovered = authority.recover_failed_detached_worktree_exact(
path=worktree,
expected_oid=base,
stale_oid=stale,
target_ref="refs/heads/agent/recovery-test",
)
assert recovered == stale
assert subprocess.check_output(["git", "-C", str(worktree), "rev-parse", "HEAD"], text=True).strip() == base
assert subprocess.run(["git", "-C", str(worktree), "symbolic-ref", "-q", "HEAD"]).returncode == 1
assert subprocess.check_output(["git", "-C", str(worktree), "status", "--porcelain"], text=True) == ""
+73
View File
@@ -0,0 +1,73 @@
#!/usr/bin/env python3
from __future__ import annotations
import argparse
import ast
import pathlib
import shutil
import sys
from datetime import datetime, timezone
UTC = timezone.utc
def die(msg: str) -> None:
print(f"ERROR: {msg}", file=sys.stderr)
raise SystemExit(2)
def backup(path: pathlib.Path) -> pathlib.Path:
stamp = datetime.now(UTC).strftime("%Y%m%dT%H%M%SZ")
out = path.with_name(path.name + f".bak.{stamp}")
shutil.copy2(path, out)
return out
def main() -> int:
ap = argparse.ArgumentParser(
description="Repair misplaced subprocess import in execution-rebind runtime."
)
ap.add_argument("--rebind", required=True)
args = ap.parse_args()
path = pathlib.Path(args.rebind).resolve()
if not path.is_file():
die(f"missing rebind source: {path}")
text = path.read_text(encoding="utf-8")
marker = "from __future__ import annotations\n"
if marker not in text:
die("future import marker missing")
lines = text.splitlines(keepends=True)
filtered = [line for line in lines if line.strip() != "import subprocess"]
text2 = "".join(filtered)
future_index = text2.find(marker)
if future_index < 0:
die("future import marker missing after normalization")
insert_at = future_index + len(marker)
normalized = text2[:insert_at] + "\nimport subprocess\n" + text2[insert_at:]
if normalized == text:
print("OK import order already normalized")
else:
b = backup(path)
path.write_text(normalized, encoding="utf-8")
print(f"PATCHED import order; backup={b}")
ast.parse(path.read_text(encoding="utf-8"), filename=str(path))
print(f"OK syntax: {path}")
current = path.read_text(encoding="utf-8")
required_markers = (
"EXECUTION_REBIND_NON_FAST_FORWARD_DENY",
"old_expected_branch_oid",
"new_expected_branch_oid",
)
missing = [m for m in required_markers if m not in current]
if missing:
die("re-anchor patch markers missing: " + ",".join(missing))
print("RESULT: IMPORT_ORDER_REPAIRED")
return 0
if __name__ == "__main__":
raise SystemExit(main())
+172
View File
@@ -0,0 +1,172 @@
#!/usr/bin/env python3
from __future__ import annotations
import argparse
import ast
from datetime import datetime, timezone
import pathlib
import shutil
import sys
UTC = timezone.utc
def die(msg: str) -> None:
print(f"ERROR: {msg}", file=sys.stderr)
raise SystemExit(2)
def backup(path: pathlib.Path) -> pathlib.Path:
stamp = datetime.now(UTC).strftime("%Y%m%dT%H%M%SZ")
out = path.with_name(path.name + f".bak.{stamp}")
shutil.copy2(path, out)
return out
def ensure_import(path: pathlib.Path, import_line: str) -> None:
text = path.read_text(encoding="utf-8")
if import_line + "\n" in text:
return
lines = text.splitlines(keepends=True)
at = 0
if lines and lines[0].startswith("#!"):
at = 1
if at < len(lines) and lines[at].startswith("from __future__ import"):
at += 1
lines.insert(at, import_line + "\n")
path.write_text("".join(lines), encoding="utf-8")
def replace_once(path: pathlib.Path, old: str, new: str, label: str) -> None:
text = path.read_text(encoding="utf-8")
if new in text:
print(f"OK already patched: {label}")
return
count = text.count(old)
if count != 1:
die(f"{label}: expected exactly one match, found {count}")
b = backup(path)
path.write_text(text.replace(old, new, 1), encoding="utf-8")
print(f"PATCHED {label}; backup={b}")
def syntax_check(path: pathlib.Path) -> None:
ast.parse(path.read_text(encoding="utf-8"), filename=str(path))
print(f"OK syntax: {path}")
def main() -> int:
ap = argparse.ArgumentParser(description="Add guarded same-job execution re-anchor support.")
ap.add_argument("--rebind", required=True)
args = ap.parse_args()
path = pathlib.Path(args.rebind).resolve()
if not path.is_file():
die(f"missing rebind source: {path}")
ensure_import(path, "import subprocess")
old = ''' binding = GovernedExecutionBinding.from_mapping(old_execution)
resolved = dict(self.resolver(task_id, capabilities))
new_execution = validate_execution_envelope(resolved, binding=binding)
if new_execution.get("action") != "deterministic":
raise ExecutionRebindError("EXECUTION_REBIND_DETERMINISTIC_PLAN_REQUIRED", 409)
if new_execution["execution_digest"] != request.expected_new_execution_digest:
raise ExecutionRebindError("EXECUTION_REBIND_NEW_DIGEST_DRIFT", 409)
if new_execution["execution_digest"] == old_execution["execution_digest"]:
raise ExecutionRebindError("EXECUTION_REBIND_DIGEST_UNCHANGED", 409)
'''
new = ''' binding = GovernedExecutionBinding.from_mapping(old_execution)
resolved = dict(self.resolver(task_id, capabilities))
candidate_binding = GovernedExecutionBinding.from_mapping(resolved)
# Same-job continuation may advance only the branch head (and may
# transition CREATE_NEW -> CONTINUE_EXISTING after the branch exists).
immutable_fields = (
"task_id",
"job_id",
"repository",
"base_oid",
"target_branch",
"generation",
"idempotency_key",
)
for field in immutable_fields:
if getattr(candidate_binding, field) != getattr(binding, field):
raise ExecutionRebindError("EXECUTION_REBIND_BINDING_CONFLICT", 409)
head_advanced = candidate_binding.expected_branch_oid != binding.expected_branch_oid
if head_advanced:
allowed_modes = {
(binding.branch_mode, binding.branch_mode),
("CREATE_NEW", "CONTINUE_EXISTING"),
}
if (binding.branch_mode, candidate_binding.branch_mode) not in allowed_modes:
raise ExecutionRebindError("EXECUTION_REBIND_BRANCH_MODE_CONFLICT", 409)
authority_git_dir = "/data/aios-gitops/repo.git"
authority_ref = f"refs/heads/{binding.target_branch}"
try:
authority_head = subprocess.check_output(
[
"/usr/bin/git",
f"--git-dir={authority_git_dir}",
"rev-parse",
authority_ref,
],
text=True,
timeout=5,
).strip()
ancestry = subprocess.run(
[
"/usr/bin/git",
f"--git-dir={authority_git_dir}",
"merge-base",
"--is-ancestor",
binding.expected_branch_oid,
candidate_binding.expected_branch_oid,
],
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
timeout=5,
check=False,
)
except (OSError, subprocess.SubprocessError) as exc:
raise ExecutionRebindError("EXECUTION_REBIND_AUTHORITY_GIT_UNAVAILABLE", 503) from exc
if authority_head != candidate_binding.expected_branch_oid:
raise ExecutionRebindError("EXECUTION_REBIND_CURRENT_HEAD_MISMATCH", 409)
if ancestry.returncode != 0:
raise ExecutionRebindError("EXECUTION_REBIND_NON_FAST_FORWARD_DENY", 409)
elif candidate_binding.branch_mode != binding.branch_mode:
raise ExecutionRebindError("EXECUTION_REBIND_BRANCH_MODE_CONFLICT", 409)
# Validate the canonical envelope on its own after the continuation
# binding proof above. Worker-side strict equality remains unchanged.
new_execution = validate_execution_envelope(resolved)
# Re-anchor may legitimately remain source_capture when no deterministic
# plan is registered yet. A non-head-changing rebind must still advance
# source_capture -> deterministic.
if new_execution.get("action") not in {"source_capture", "deterministic"}:
raise ExecutionRebindError("EXECUTION_REBIND_ACTION_INVALID", 409)
if not head_advanced and new_execution.get("action") != "deterministic":
raise ExecutionRebindError("EXECUTION_REBIND_DETERMINISTIC_PLAN_REQUIRED", 409)
if new_execution["execution_digest"] != request.expected_new_execution_digest:
raise ExecutionRebindError("EXECUTION_REBIND_NEW_DIGEST_DRIFT", 409)
if new_execution["execution_digest"] == old_execution["execution_digest"]:
raise ExecutionRebindError("EXECUTION_REBIND_DIGEST_UNCHANGED", 409)
'''
replace_once(path, old, new, "same-job fast-forward execution re-anchor")
old_event = ''' "old_action": old_execution.get("action"),
"new_action": new_execution.get("action"),
"previous_revision": request.expected_revision,
'''
new_event = ''' "old_action": old_execution.get("action"),
"new_action": new_execution.get("action"),
"old_expected_branch_oid": binding.expected_branch_oid,
"new_expected_branch_oid": candidate_binding.expected_branch_oid,
"head_advanced": head_advanced,
"previous_revision": request.expected_revision,
'''
replace_once(path, old_event, new_event, "re-anchor audit evidence")
syntax_check(path)
print("RESULT: PATCH_OK")
return 0
if __name__ == "__main__":
raise SystemExit(main())
+141
View File
@@ -0,0 +1,141 @@
#!/usr/bin/env python3
from __future__ import annotations
import argparse
import ast
from datetime import datetime, timezone
import pathlib
import shutil
import sys
UTC = timezone.utc
def die(msg: str) -> None:
print(f"ERROR: {msg}", file=sys.stderr)
raise SystemExit(2)
def backup(path: pathlib.Path) -> pathlib.Path:
stamp = datetime.now(UTC).strftime("%Y%m%dT%H%M%SZ")
out = path.with_name(path.name + f".bak.{stamp}")
shutil.copy2(path, out)
return out
def ensure_datetime_import(path: pathlib.Path) -> bool:
text = path.read_text(encoding="utf-8")
target = "from datetime import datetime, timezone\n"
if target in text:
return False
lines = text.splitlines(keepends=True)
at = 0
if lines and lines[0].startswith("#!"):
at = 1
if at < len(lines) and lines[at].startswith("from __future__ import"):
at += 1
b = backup(path)
lines.insert(at, target)
path.write_text("".join(lines), encoding="utf-8")
print(f"PATCHED datetime import: {path}; backup={b}")
return True
def replace_once(path: pathlib.Path, old: str, new: str, label: str) -> bool:
text = path.read_text(encoding="utf-8")
if new in text:
print(f"OK already patched: {label}: {path}")
return False
n = text.count(old)
if n != 1:
die(f"{label}: expected exactly 1 match, found {n}: {path}")
b = backup(path)
path.write_text(text.replace(old, new, 1), encoding="utf-8")
print(f"PATCHED {label}: {path}; backup={b}")
return True
def syntax_check(path: pathlib.Path) -> None:
ast.parse(path.read_text(encoding="utf-8"), filename=str(path))
print(f"OK syntax: {path}")
def main() -> int:
ap = argparse.ArgumentParser()
ap.add_argument("--rebind", required=True)
ap.add_argument("--worker", required=True)
ap.add_argument("--bridge", required=True)
args = ap.parse_args()
rebind = pathlib.Path(args.rebind).resolve()
worker = pathlib.Path(args.worker).resolve()
bridge = pathlib.Path(args.bridge).resolve()
for p in (rebind, worker, bridge):
if not p.is_file():
die(f"missing file: {p}")
old_rebind = ''' if task["lease_token_hash"] not in (None, "") or task["lease_expires_at"] not in (None, ""):
raise ExecutionRebindError("EXECUTION_REBIND_ACTIVE_LEASE_DENY", 409)
'''
new_rebind = ''' # A persisted lease token is not by itself proof of an active lease.
lease_expires_at = task["lease_expires_at"]
if lease_expires_at not in (None, ""):
try:
parsed_lease_expires_at = datetime.fromisoformat(
str(lease_expires_at).replace("Z", "+00:00")
).astimezone(timezone.utc)
except (TypeError, ValueError) as exc:
raise ExecutionRebindError("EXECUTION_REBIND_LEASE_EXPIRY_INVALID", 409) from exc
if parsed_lease_expires_at > datetime.now(timezone.utc):
raise ExecutionRebindError("EXECUTION_REBIND_ACTIVE_LEASE_DENY", 409)
'''
old_worker = ''' active = [
item for item in leases
if isinstance(item, Mapping)
and item.get("state") == "ACTIVE"
and item.get("job_id") == envelope.job_id
and item.get("branch") == envelope.target_branch
]
'''
new_worker = ''' now = datetime.now(timezone.utc)
active = []
for item in leases:
if (
not isinstance(item, Mapping)
or item.get("state") != "ACTIVE"
or item.get("job_id") != envelope.job_id
or item.get("branch") != envelope.target_branch
):
continue
try:
expires_at = datetime.fromisoformat(
str(item.get("lease_expires_at") or "").replace("Z", "+00:00")
).astimezone(timezone.utc)
except (TypeError, ValueError):
continue
if expires_at > now:
active.append(item)
'''
old_bridge = ''' if durable_expires_at <= datetime.now(timezone.utc):
return self.renew_writer_lease(request)
'''
new_bridge = ''' # Expired durable lease is recoverable on the same task/job/generation.
if durable_expires_at <= datetime.now(timezone.utc):
return self.renew_writer_lease(request)
'''
changed = []
for p in (rebind, worker):
if ensure_datetime_import(p):
changed.append(p)
if replace_once(rebind, old_rebind, new_rebind, "execution-rebind effective lease expiry"):
changed.append(rebind)
if replace_once(worker, old_worker, new_worker, "worker effective lease expiry"):
changed.append(worker)
if replace_once(bridge, old_bridge, new_bridge, "bridge actual-runtime marker"):
changed.append(bridge)
for p in (rebind, worker, bridge):
syntax_check(p)
print("RESULT: PATCH_OK")
print("CHANGED:", ",".join(dict.fromkeys(str(p) for p in changed)) if changed else "none")
return 0
if __name__ == "__main__":
raise SystemExit(main())
+63
View File
@@ -0,0 +1,63 @@
#!/usr/bin/env python3
from __future__ import annotations
import argparse
import ast
from datetime import datetime, timezone
import pathlib
import shutil
import sys
UTC = timezone.utc
def die(msg: str) -> None:
print(f"ERROR: {msg}", file=sys.stderr)
raise SystemExit(2)
def backup(path: pathlib.Path) -> pathlib.Path:
stamp = datetime.now(UTC).strftime("%Y%m%dT%H%M%SZ")
out = path.with_name(path.name + f".bak.{stamp}")
shutil.copy2(path, out)
return out
def main() -> int:
ap = argparse.ArgumentParser(description="Allow pre-claim execution rebind at attempt=0.")
ap.add_argument("--rebind", required=True)
args = ap.parse_args()
path = pathlib.Path(args.rebind).resolve()
if not path.is_file():
die(f"missing rebind source: {path}")
text = path.read_text(encoding="utf-8")
old = " or attempt < 1\n"
new = " or attempt < 0\n"
if new in text:
print("OK already patched: pre-claim attempt=0")
else:
count = text.count(old)
if count != 1:
die(f"expected exactly one attempt<1 guard, found {count}")
b = backup(path)
path.write_text(text.replace(old, new, 1), encoding="utf-8")
print(f"PATCHED pre-claim attempt=0; backup={b}")
ast.parse(path.read_text(encoding="utf-8"), filename=str(path))
current = path.read_text(encoding="utf-8")
for marker in (
"or attempt < 0",
"EXECUTION_REBIND_NON_FAST_FORWARD_DENY",
"old_expected_branch_oid",
"new_expected_branch_oid",
):
if marker not in current:
die(f"required marker missing: {marker}")
print(f"OK syntax: {path}")
print("RESULT: PRECLAIM_REBIND_PATCH_OK")
return 0
if __name__ == "__main__":
raise SystemExit(main())
+145
View File
@@ -0,0 +1,145 @@
#!/usr/bin/env python3
from __future__ import annotations
import argparse
import ast
import datetime as dt
import pathlib
import shutil
import sys
UTC = dt.timezone.utc
def die(msg: str) -> None:
print(f"ERROR: {msg}", file=sys.stderr)
raise SystemExit(2)
def backup(path: pathlib.Path) -> pathlib.Path:
stamp = dt.datetime.now(UTC).strftime("%Y%m%dT%H%M%SZ")
out = path.with_suffix(path.suffix + f".bak.{stamp}")
shutil.copy2(path, out)
return out
def replace_once(path: pathlib.Path, old: str, new: str, label: str) -> bool:
text = path.read_text(encoding="utf-8")
if new in text:
print(f"OK already patched: {label}")
return False
count = text.count(old)
if count != 1:
die(f"{label}: expected exactly 1 match, found {count} in {path}")
backup_path = backup(path)
path.write_text(text.replace(old, new, 1), encoding="utf-8")
print(f"PATCHED {label}; backup={backup_path}")
return True
def ensure_datetime_import(path: pathlib.Path) -> bool:
text = path.read_text(encoding="utf-8")
target = "from datetime import datetime, timezone\n"
if target in text:
return False
lines = text.splitlines(keepends=True)
insert_at = 0
if lines and lines[0].startswith("#!"):
insert_at = 1
if insert_at < len(lines) and lines[insert_at].startswith("from __future__ import"):
insert_at += 1
lines.insert(insert_at, target)
backup_path = backup(path)
path.write_text("".join(lines), encoding="utf-8")
print(f"PATCHED datetime import: {path}; backup={backup_path}")
return True
def syntax_check(path: pathlib.Path) -> None:
ast.parse(path.read_text(encoding="utf-8"), filename=str(path))
print(f"OK syntax: {path}")
def main() -> int:
ap = argparse.ArgumentParser(description="Repair stale governed writer lease handling.")
ap.add_argument("--app-dir", default="/opt/aios/current/active-runtime/app")
args = ap.parse_args()
app = pathlib.Path(args.app_dir)
rebind = app / "aios_agent_task_execution_rebind.py"
worker = app / "aios_online_agent_worker.py"
bridge = app / "aios_chatgpt_bridge_v2_server.py"
for p in (rebind, worker, bridge):
if not p.is_file():
die(f"missing file: {p}")
old_rebind = ''' if task["lease_token_hash"] not in (None, "") or task["lease_expires_at"] not in (None, ""):
raise ExecutionRebindError("EXECUTION_REBIND_ACTIVE_LEASE_DENY", 409)
'''
new_rebind = ''' # A persisted lease token is not by itself proof of an active lease.
# Treat an unexpired lease as active; stale lease projection is recoverable.
lease_expires_at = task["lease_expires_at"]
if lease_expires_at not in (None, ""):
try:
parsed_lease_expires_at = datetime.fromisoformat(
str(lease_expires_at).replace("Z", "+00:00")
).astimezone(timezone.utc)
except (TypeError, ValueError) as exc:
raise ExecutionRebindError("EXECUTION_REBIND_LEASE_EXPIRY_INVALID", 409) from exc
if parsed_lease_expires_at > datetime.now(timezone.utc):
raise ExecutionRebindError("EXECUTION_REBIND_ACTIVE_LEASE_DENY", 409)
'''
old_worker = ''' active = [
item for item in leases
if isinstance(item, Mapping)
and item.get("state") == "ACTIVE"
and item.get("job_id") == envelope.job_id
and item.get("branch") == envelope.target_branch
]
'''
new_worker = ''' now = datetime.now(timezone.utc)
active = []
for item in leases:
if (
not isinstance(item, Mapping)
or item.get("state") != "ACTIVE"
or item.get("job_id") != envelope.job_id
or item.get("branch") != envelope.target_branch
):
continue
try:
expires_at = datetime.fromisoformat(
str(item.get("lease_expires_at") or "").replace("Z", "+00:00")
).astimezone(timezone.utc)
except (TypeError, ValueError):
continue
if expires_at > now:
active.append(item)
'''
old_bridge = ''' if durable_expires_at <= datetime.now(timezone.utc):
return self.renew_writer_lease(request)
'''
new_bridge = ''' # An expired durable lease is recoverable on the same governed
# task/job/generation. Renew it instead of treating stale ACTIVE state
# as a durable-binding failure.
if durable_expires_at <= datetime.now(timezone.utc):
return self.renew_writer_lease(request)
'''
changed = []
if ensure_datetime_import(rebind):
changed.append(rebind)
if ensure_datetime_import(worker):
changed.append(worker)
if replace_once(rebind, old_rebind, new_rebind, "execution-rebind effective lease expiry"):
changed.append(rebind)
if replace_once(worker, old_worker, new_worker, "worker converged-job effective lease expiry"):
changed.append(worker)
if replace_once(bridge, old_bridge, new_bridge, "bridge expired-lease recovery contract"):
changed.append(bridge)
for p in (rebind, worker, bridge):
syntax_check(p)
print("RESULT: PATCH_OK")
print("CHANGED:", ",".join(dict.fromkeys(str(p) for p in changed)) if changed else "none")
return 0
if __name__ == "__main__":
raise SystemExit(main())