Compare commits

..
55 changed files with 6272 additions and 5237 deletions
+315
View File
@@ -0,0 +1,315 @@
#!/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()
@@ -0,0 +1,827 @@
#!/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)
]
@@ -0,0 +1,133 @@
"""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)
+6 -22
View File
@@ -3,12 +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 version_registry import (
configured_authoritative_git_dir,
configured_repository_identity,
list_registry_versions,
list_registry_versions_report,
)
from typing import Any
try:
@@ -78,8 +72,8 @@ HOST = os.getenv('AIOS_GITOPS_HOST', '127.0.0.1')
PORT = int(os.getenv('AIOS_GITOPS_PORT', '18890'))
OPENAPI = WS / 'docs' / 'gitops' / 'openapi-chatgpt-actions.yaml'
BARE_REPO = Path(os.getenv('AIOS_GITOPS_BARE_REPO', str(ROOT / 'runtime' / 'gitea-bin' / 'data' / 'repositories' / 'aios' / 'aios.git')))
AUTHORITATIVE_GIT_DIR = configured_authoritative_git_dir()
REPOSITORY_IDENTITY = configured_repository_identity()
AUTHORITATIVE_GIT_DIR = Path(os.getenv('AIOS_GITOPS_AUTHORITATIVE_GIT_DIR')) if os.getenv('AIOS_GITOPS_AUTHORITATIVE_GIT_DIR') else None
REPOSITORY_IDENTITY = os.getenv('AIOS_GITOPS_REPOSITORY_IDENTITY', '')
SILENT_RELEASE_STATE_ROOT = Path(os.getenv(
'AIOS_SILENT_RELEASE_STATE_ROOT',
'/var/lib/aios-gitops/silent-release',
@@ -472,14 +466,6 @@ class H(BaseHTTPRequestHandler):
))
return self.sendj(data)
if path in ('/api/ver','/api/versions','/ver'):
registry_report = list_registry_versions_report(
WS,
baseline_file=Path(os.getenv('AIOS_GITOPS_BASELINE_AUTHORITY', '/var/lib/aios-gitops-runtime-anchor/baseline/current.json')),
# Do not invent an authoritative repository when authority
# discovery is disabled/omitted. Preserve the unavailable
# authority path (503) instead of turning it into a 404.
repo_git_dir=AUTHORITATIVE_GIT_DIR,
)
snapshot = repository_snapshot(
git_dir=AUTHORITATIVE_GIT_DIR,
repository_identity=REPOSITORY_IDENTITY,
@@ -488,8 +474,7 @@ class H(BaseHTTPRequestHandler):
return self.sendj({
'repository_available': False,
'query_error': snapshot['query_error'],
'registry_versions': registry_report['registry_versions'],
**{key: registry_report[key] for key in ('effective_registry_versions', 'runtime_registry_versions', 'attested_projection_versions', 'attested_projection_version', 'source_registry_converged', 'runtime_registry_converged', 'runtime_bundle_contains_current_version', 'runtime_bundle_contains_version', 'registry_consistency_status')},
'registry_versions': [p.name for p in (WS/'versions').glob('*')],
'local_authoritative_tags': [],
}, 503)
try:
@@ -498,11 +483,10 @@ class H(BaseHTTPRequestHandler):
return self.sendj({
'repository_available': False,
'query_error': str(exc),
'registry_versions': registry_report['registry_versions'],
**{key: registry_report[key] for key in ('effective_registry_versions', 'runtime_registry_versions', 'attested_projection_versions', 'attested_projection_version', 'source_registry_converged', 'runtime_registry_converged', 'runtime_bundle_contains_current_version', 'runtime_bundle_contains_version', 'registry_consistency_status')},
'registry_versions': [p.name for p in (WS/'versions').glob('*')],
'local_authoritative_tags': [],
}, 503)
registry = registry_report['registry_versions']
registry = [p.name for p in (WS/'versions').glob('*')]
return self.sendj({
'registry_versions': registry,
'local_authoritative_tags': tags,
@@ -512,7 +496,6 @@ class H(BaseHTTPRequestHandler):
'versions_dir': registry,
'head': snapshot['authoritative_head_oid'],
'authoritative_head_oid': snapshot['authoritative_head_oid'],
**{key: registry_report[key] for key in ('effective_registry_versions', 'runtime_registry_versions', 'attested_projection_versions', 'attested_projection_version', 'source_registry_converged', 'runtime_registry_converged', 'runtime_bundle_contains_current_version', 'runtime_bundle_contains_version', 'registry_consistency_status')},
})
if path in ('/api/diff','/diff'):
frm=(q.get('from') or [''])[0]
@@ -967,3 +950,4 @@ def main():
print(f'AIOS ChatGPT GitOps adapter listening on {HOST}:{PORT}')
ThreadingHTTPServer((HOST,PORT), H).serve_forever()
if __name__ == '__main__': main()
@@ -15,7 +15,6 @@ BRIDGE_OPERATIONS = (
"acquireWriterLease",
"commitFileGoverned",
"commitFilesGoverned",
"publishExistingBranchGoverned",
"getGitOpsJob",
"resumeGitOpsJob",
"reconcileGitOpsJob",
@@ -34,7 +33,6 @@ OPERATION_REQUIRED_FIELDS = {
"acquireWriterLease": ("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"),
"publishExistingBranchGoverned": ("repository", "branch", "expected_branch_oid", "owner_approval_reference", "idempotency_key"),
"getGitOpsJob": ("job_id",),
"resumeGitOpsJob": ("job_id", "expected_revision"),
"reconcileGitOpsJob": ("job_id", "expected_revision"),
@@ -155,7 +153,6 @@ class BridgeV2Adapter:
def acquireWriterLease(self, request: Mapping[str, Any]) -> Any: return self._call("acquireWriterLease", 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 publishExistingBranchGoverned(self, request: Mapping[str, Any]) -> Any: return self._call("publishExistingBranchGoverned", request)
def getGitOpsJob(self, request: Mapping[str, Any]) -> Any: return self._call("getGitOpsJob", request)
def resumeGitOpsJob(self, request: Mapping[str, Any]) -> Any: return self._call("resumeGitOpsJob", request)
def reconcileGitOpsJob(self, request: Mapping[str, Any]) -> Any: return self._call("reconcileGitOpsJob", request)
@@ -19,7 +19,6 @@ from aios_continuation_lease import WriterLeaseRequestV2
from aios_control_plane_snapshot import build_control_plane_snapshot
from aios_gitops_job_controller import GitOpsJobController, JobStore
from aios_governed_task_bootstrap import BootstrapError, GovernedTaskBootstrap
from aios_existing_branch_publisher import ExistingBranchPublisher
from governed_production_release import build_executor_config
MAX_BODY = 2 * 1024 * 1024
@@ -30,7 +29,6 @@ ROUTES = {
"/v2/writer-lease": "acquireWriterLease",
"/v2/commit-file": "commitFileGoverned",
"/v2/commit-files": "commitFilesGoverned",
"/v2/branch/publish": "publishExistingBranchGoverned",
"/v2/job": "getGitOpsJob",
"/v2/job/resume": "resumeGitOpsJob",
"/v2/job/reconcile": "reconcileGitOpsJob",
@@ -152,36 +150,6 @@ class GovernedRuntime:
executor.workspace.materialize_files(commit.files)
return executor.execute(binding, commit)
def publish_existing_branch(self, request: Mapping[str, Any]) -> dict[str, Any]:
expected = str(request["expected_branch_oid"])
key = _safe("existing-branch-" + str(request["idempotency_key"]))
worktree_root = self.config.state_root / "worktrees"
worktree_path = worktree_root / key
if not worktree_path.exists():
self.authority.create_detached_worktree(expected, key, worktree_root)
workspace = DetachedGitWorkspace(worktree_path)
credential = ServerSideCredentialProjection(self.config.ssh_key_path, self.config.token_path)
remote = GiteaContinuationRemote(
workspace=worktree_path,
credential=credential,
remote_name=self.config.remote_name,
repository=self.config.repository,
api_base=self.config.api_base,
)
publisher = ExistingBranchPublisher(
self.authority,
remote,
self.config.state_root / "existing-branch-publications",
)
projected = False
try:
credential.project()
projected = True
return publisher.publish(request)
finally:
if projected:
credential.revoke()
def _controller_for_job(self, job_id: str) -> GitOpsJobController:
for controller in self.controllers.values():
try:
@@ -377,7 +345,6 @@ class GovernedRuntime:
"acquireWriterLease": self.acquire_lease,
"commitFileGoverned": lambda request: self.commit_files({**request, "files": {request["path"]: request["content_base64"]}}),
"commitFilesGoverned": self.commit_files,
"publishExistingBranchGoverned": self.publish_existing_branch,
"getGitOpsJob": lambda request: self.job(request, "get"),
"resumeGitOpsJob": lambda request: self.job(request, "resume"),
"reconcileGitOpsJob": lambda request: self.job(request, "reconcile"),
@@ -258,7 +258,7 @@ class GiteaContinuationRemote:
raise ContinuationExecutionError("GITEA_GIT_READBACK_INVALID")
return oid
def push_exact(self, branch: str, expected: str | None, new: str) -> None:
def push_exact(self, branch: str, expected: str, new: str) -> None:
self._require_projected()
self.repository_identity()
if self.read_git_ref(branch) != expected:
File diff suppressed because it is too large Load Diff
@@ -1,252 +0,0 @@
"""Fail-closed governed publication of an existing Authority branch at an exact OID."""
from __future__ import annotations
from collections.abc import Mapping
from dataclasses import asdict, dataclass
from enum import Enum
import hashlib
import json
import os
import re
import tempfile
import time
from pathlib import Path
from typing import Any, Protocol
OID_RE = re.compile(r"^[0-9a-f]{40}$")
BRANCH_RE = re.compile(r"^(?!refs/)(?!tags/)[A-Za-z0-9][A-Za-z0-9._/-]{2,179}$")
IDEMPOTENCY_RE = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._:-]{7,127}$")
class ExistingBranchPublicationError(RuntimeError):
"""Stable, non-secret error for the governed existing-branch publisher."""
class ExistingBranchPublicationConflict(ExistingBranchPublicationError):
pass
class ExistingBranchPublicationState(str, Enum):
QUEUED = "QUEUED"
PUBLISHING = "PUBLISHING"
REMOTE_PUSH_PENDING = "REMOTE_PUSH_PENDING"
RECONCILIATION_REQUIRED = "RECONCILIATION_REQUIRED"
MANUAL_OWNER_REVIEW_REQUIRED = "MANUAL_OWNER_REVIEW_REQUIRED"
COMPLETED = "COMPLETED"
FAILED = "FAILED"
@dataclass(frozen=True, slots=True)
class ExistingBranchPublicationRequest:
repository: str
branch: str
expected_branch_oid: str
owner_approval_reference: str
idempotency_key: str
@classmethod
def from_mapping(cls, value: Mapping[str, Any]) -> "ExistingBranchPublicationRequest":
required = ("repository", "branch", "expected_branch_oid", "owner_approval_reference", "idempotency_key")
if not isinstance(value, Mapping):
raise ExistingBranchPublicationConflict("REQUEST_OBJECT_REQUIRED")
if any(not isinstance(value.get(field), str) or not value[field].strip() for field in required):
raise ExistingBranchPublicationConflict("REQUEST_FIELDS_MISSING")
repository = value["repository"].strip()
branch = value["branch"].strip()
expected = value["expected_branch_oid"].strip().lower()
approval = value["owner_approval_reference"].strip()
idempotency = value["idempotency_key"].strip()
if repository != "aios/aios":
raise ExistingBranchPublicationConflict("REPOSITORY_IDENTITY_MISMATCH")
if not BRANCH_RE.fullmatch(branch) or branch in {"main", "master"}:
raise ExistingBranchPublicationConflict("BRANCH_NAMESPACE_REJECTED")
if not OID_RE.fullmatch(expected):
raise ExistingBranchPublicationConflict("EXPECTED_BRANCH_OID_INVALID")
if len(approval) > 512:
raise ExistingBranchPublicationConflict("OWNER_APPROVAL_REFERENCE_INVALID")
if not IDEMPOTENCY_RE.fullmatch(idempotency):
raise ExistingBranchPublicationConflict("IDEMPOTENCY_KEY_INVALID")
return cls(repository, branch, expected, approval, idempotency)
@property
def publication_job_id(self) -> str:
return "publication-" + hashlib.sha256(self.idempotency_key.encode()).hexdigest()[:32]
@property
def digest(self) -> str:
raw = json.dumps(asdict(self), sort_keys=True, separators=(",", ":")).encode()
return hashlib.sha256(raw).hexdigest()
class ExistingBranchAuthority(Protocol):
def read_ref(self, ref: str) -> str | None: ...
class ExistingBranchRemote(Protocol):
def repository_identity(self) -> str: ...
def read_git_ref(self, branch: str) -> str | None: ...
def read_api_branch(self, branch: str) -> str | None: ...
def commit_visible(self, oid: str) -> bool: ...
def push_exact(self, branch: str, expected: str | None, new: str) -> None: ...
class ExistingBranchPublicationStore:
def __init__(self, root: Path):
self.root = Path(root)
self.root.mkdir(parents=True, exist_ok=True)
def _path(self, request: ExistingBranchPublicationRequest) -> Path:
return self.root / f"{request.publication_job_id}.json"
def load(self, request: ExistingBranchPublicationRequest) -> dict[str, Any]:
try:
value = json.loads(self._path(request).read_text(encoding="utf-8"))
except FileNotFoundError:
return {}
if not isinstance(value, dict):
raise ExistingBranchPublicationConflict("PUBLICATION_STATE_INVALID")
return value
def record(self, request: ExistingBranchPublicationRequest, state: ExistingBranchPublicationState, **evidence: Any) -> dict[str, Any]:
payload = {
"schema": "aios.existing-branch-publication.v1",
"publication_job_id": request.publication_job_id,
"repository": request.repository,
"branch": request.branch,
"expected_branch_oid": request.expected_branch_oid,
"owner_approval_reference": request.owner_approval_reference,
"idempotency_key": request.idempotency_key,
"request_digest": request.digest,
"publication_state": state.value,
"updated_at": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()),
**evidence,
}
path = self._path(request)
fd, temporary = tempfile.mkstemp(prefix=f".{path.name}.", dir=path.parent)
try:
with os.fdopen(fd, "w", encoding="utf-8") as handle:
json.dump(payload, handle, ensure_ascii=False, sort_keys=True)
handle.write("\n")
handle.flush()
os.fsync(handle.fileno())
os.replace(temporary, path)
finally:
if os.path.exists(temporary):
os.unlink(temporary)
return payload
class ExistingBranchPublisher:
"""Publish only the exact, already-authorized Authority branch tip."""
def __init__(self, authority: ExistingBranchAuthority, remote: ExistingBranchRemote, state_root: Path):
self.authority = authority
self.remote = remote
self.store = ExistingBranchPublicationStore(state_root)
@staticmethod
def _safe_error(exc: BaseException) -> str:
return type(exc).__name__
def _observe(self, request: ExistingBranchPublicationRequest) -> dict[str, Any]:
return {
"authority_branch_oid": self.authority.read_ref(f"refs/heads/{request.branch}"),
"gitea_git_branch_oid": self.remote.read_git_ref(request.branch),
"gitea_api_branch_oid": self.remote.read_api_branch(request.branch),
"commit_visible": bool(self.remote.commit_visible(request.expected_branch_oid)),
}
@staticmethod
def _has_remote_conflict(request: ExistingBranchPublicationRequest, observation: Mapping[str, Any]) -> bool:
return any(
observation.get(field) not in (None, request.expected_branch_oid)
for field in ("gitea_git_branch_oid", "gitea_api_branch_oid")
)
def _manual(self, request: ExistingBranchPublicationRequest, reason: str, observation: Mapping[str, Any]) -> dict[str, Any]:
return self.store.record(
request,
ExistingBranchPublicationState.MANUAL_OWNER_REVIEW_REQUIRED,
error_code=reason,
**dict(observation),
)
def _finish(self, request: ExistingBranchPublicationRequest) -> dict[str, Any]:
try:
observation = self._observe(request)
except Exception as exc:
return self.store.record(
request,
ExistingBranchPublicationState.RECONCILIATION_REQUIRED,
error_code=self._safe_error(exc),
)
if observation.get("authority_branch_oid") != request.expected_branch_oid:
return self._manual(request, "AUTHORITY_BRANCH_OID_CHANGED", observation)
if self._has_remote_conflict(request, observation):
return self._manual(request, "REMOTE_BRANCH_OID_CONFLICT", observation)
if (
observation.get("gitea_git_branch_oid") == request.expected_branch_oid
and observation.get("gitea_api_branch_oid") == request.expected_branch_oid
and observation.get("commit_visible") is True
):
return self.store.record(request, ExistingBranchPublicationState.COMPLETED, **dict(observation))
return self.store.record(
request,
ExistingBranchPublicationState.RECONCILIATION_REQUIRED,
error_code="REMOTE_READBACK_INCOMPLETE",
**dict(observation),
)
def publish(self, value: Mapping[str, Any]) -> dict[str, Any]:
request = ExistingBranchPublicationRequest.from_mapping(value)
previous = self.store.load(request)
if previous:
if previous.get("request_digest") != request.digest:
raise ExistingBranchPublicationConflict("IDEMPOTENCY_KEY_REUSED")
if previous.get("publication_state") == ExistingBranchPublicationState.COMPLETED.value:
return {**previous, "idempotent": True}
authority_oid = self.authority.read_ref(f"refs/heads/{request.branch}")
if authority_oid is None:
raise ExistingBranchPublicationConflict("AUTHORITY_BRANCH_MISSING")
if authority_oid != request.expected_branch_oid:
raise ExistingBranchPublicationConflict("AUTHORITY_BRANCH_OID_MISMATCH")
if self.remote.repository_identity() != request.repository:
raise ExistingBranchPublicationConflict("REMOTE_REPOSITORY_IDENTITY_MISMATCH")
self.store.record(request, ExistingBranchPublicationState.QUEUED)
try:
observation = self._observe(request)
except Exception as exc:
return self.store.record(
request,
ExistingBranchPublicationState.RECONCILIATION_REQUIRED,
error_code=self._safe_error(exc),
)
if observation.get("authority_branch_oid") != request.expected_branch_oid:
return self._manual(request, "AUTHORITY_BRANCH_OID_CHANGED", observation)
if self._has_remote_conflict(request, observation):
return self._manual(request, "REMOTE_BRANCH_OID_CONFLICT", observation)
if (
observation.get("gitea_git_branch_oid") == request.expected_branch_oid
and observation.get("gitea_api_branch_oid") == request.expected_branch_oid
and observation.get("commit_visible") is True
):
return self.store.record(request, ExistingBranchPublicationState.COMPLETED, **dict(observation))
if observation.get("gitea_git_branch_oid") is not None or observation.get("gitea_api_branch_oid") is not None:
return self.store.record(
request,
ExistingBranchPublicationState.RECONCILIATION_REQUIRED,
error_code="REMOTE_READBACK_INCOMPLETE",
**dict(observation),
)
self.store.record(request, ExistingBranchPublicationState.PUBLISHING, **dict(observation))
try:
self.remote.push_exact(request.branch, None, request.expected_branch_oid)
except Exception as exc:
return self.store.record(
request,
ExistingBranchPublicationState.REMOTE_PUSH_PENDING,
error_code=self._safe_error(exc),
desired_remote_oid=request.expected_branch_oid,
**dict(observation),
)
return self._finish(request)
+361 -63
View File
@@ -3,11 +3,12 @@ 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 version_registry import (
configured_authoritative_git_dir,
configured_repository_identity,
list_registry_versions,
list_registry_versions_report,
from aios_caller_admission_projection import (
CALLER,
SCOPE,
CallerProjectionClient,
CallerProjectionError,
)
from baseline_authority import BaselineAuthority, BaselineAuthorityError
from typing import Any
@@ -21,9 +22,6 @@ except ModuleNotFoundError:
from gitea_tag_publication import (
GiteaTagClient,
GitTagBackend,
GiteaConfigurationError,
InvalidReleaseVersion,
PublicationError,
PublicationConflict,
PublicationPending,
PublicationManualOwnerReviewRequired,
@@ -82,13 +80,10 @@ HOST = os.getenv('AIOS_GITOPS_HOST', '127.0.0.1')
PORT = int(os.getenv('AIOS_GITOPS_PORT', '18890'))
OPENAPI = WS / 'docs' / 'gitops' / 'openapi-chatgpt-actions.yaml'
BARE_REPO = Path(os.getenv('AIOS_GITOPS_BARE_REPO', str(ROOT / 'runtime' / 'gitea-bin' / 'data' / 'repositories' / 'aios' / 'aios.git')))
AUTHORITATIVE_GIT_DIR = configured_authoritative_git_dir()
REPOSITORY_IDENTITY = configured_repository_identity()
AUTHORITATIVE_GIT_DIR = Path(os.getenv('AIOS_GITOPS_AUTHORITATIVE_GIT_DIR')) if os.getenv('AIOS_GITOPS_AUTHORITATIVE_GIT_DIR') else None
REPOSITORY_IDENTITY = os.getenv('AIOS_GITOPS_REPOSITORY_IDENTITY', '')
BASELINE_AUTHORITY = BaselineAuthority()
# Retain the generic Git directory only for legacy/staging compatibility
# commands. Source-registry convergence receives the separate authority
# below and never falls back to this path.
GIT_DIR = Path(os.getenv('AIOS_GITOPS_REPO', '/data/aios-gitops/repo.git'))
GIT_DIR = Path(os.getenv('AIOS_GITOPS_AUTHORITATIVE_GIT_DIR') or os.getenv('AIOS_GITOPS_REPO', '/data/aios-gitops/repo.git'))
LEGACY_GIT_WRITE_COMMANDS = frozenset({'add','commit','checkout','branch','tag','push','fetch','reset','merge','rebase','update-ref','worktree'})
SILENT_RELEASE_STATE_ROOT = Path(os.getenv(
'AIOS_SILENT_RELEASE_STATE_ROOT',
@@ -101,6 +96,11 @@ 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,6 +124,9 @@ 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',
@@ -370,7 +373,12 @@ def publication_publisher() -> TagPublisher:
return TagPublisher(
WS,
GitTagBackend(WS, authoritative_repo=authoritative_repo),
GiteaTagClient.from_authoritative_environment(),
GiteaTagClient(
os.getenv('AIOS_GITOPS_GITEA_API_URL',''),
os.getenv('AIOS_GITOPS_GITEA_REPOSITORY',''),
Path(os.getenv('AIOS_GITOPS_GITEA_TOKEN_FILE','')),
token_line=int(os.getenv('AIOS_GITOPS_GITEA_TOKEN_LINE','1')),
),
os.getenv('AIOS_GITOPS_GITEA_REMOTE','gitea'),
)
@@ -414,14 +422,113 @@ 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','')
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
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
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',
@@ -453,24 +560,183 @@ 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 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 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_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({
@@ -502,11 +768,6 @@ class H(BaseHTTPRequestHandler):
data['legacy_global_writes_enabled'] = False
return self.sendj(data)
if path in ('/api/ver','/api/versions','/ver'):
registry_report = list_registry_versions_report(
WS,
baseline_file=BASELINE_AUTHORITY.path,
repo_git_dir=AUTHORITATIVE_GIT_DIR,
)
try:
baseline = dict(BASELINE_AUTHORITY.read())
except BaselineAuthorityError as exc:
@@ -519,8 +780,7 @@ class H(BaseHTTPRequestHandler):
return self.sendj({
'repository_available': False,
'query_error': snapshot['query_error'],
'registry_versions': registry_report['registry_versions'],
**{key: registry_report[key] for key in ('effective_registry_versions', 'runtime_registry_versions', 'attested_projection_versions', 'attested_projection_version', 'source_registry_converged', 'runtime_registry_converged', 'runtime_bundle_contains_current_version', 'runtime_bundle_contains_version', 'registry_consistency_status')},
'registry_versions': [p.name for p in (WS/'versions').glob('*')],
'local_authoritative_tags': [],
'baseline_version': baseline.get('current_baseline'),
'baseline_generation': baseline.get('generation'),
@@ -532,14 +792,13 @@ class H(BaseHTTPRequestHandler):
return self.sendj({
'repository_available': False,
'query_error': str(exc),
'registry_versions': registry_report['registry_versions'],
**{key: registry_report[key] for key in ('effective_registry_versions', 'runtime_registry_versions', 'attested_projection_versions', 'attested_projection_version', 'source_registry_converged', 'runtime_registry_converged', 'runtime_bundle_contains_current_version', 'runtime_bundle_contains_version', 'registry_consistency_status')},
'registry_versions': [p.name for p in (WS/'versions').glob('*')],
'local_authoritative_tags': [],
'baseline_version': baseline.get('current_baseline'),
'baseline_generation': baseline.get('generation'),
'baseline_sha256': baseline.get('authority_sha256'),
}, 503)
registry = registry_report['registry_versions']
registry = [p.name for p in (WS/'versions').glob('*')]
return self.sendj({
'registry_versions': registry,
'local_authoritative_tags': tags,
@@ -553,7 +812,6 @@ class H(BaseHTTPRequestHandler):
'baseline_generation': baseline.get('generation'),
'baseline_sha256': baseline.get('authority_sha256'),
'baseline': baseline,
**{key: registry_report[key] for key in ('effective_registry_versions', 'runtime_registry_versions', 'attested_projection_versions', 'attested_projection_version', 'source_registry_converged', 'runtime_registry_converged', 'runtime_bundle_contains_current_version', 'runtime_bundle_contains_version', 'registry_consistency_status')},
})
if path in ('/api/diff','/diff'):
frm=(q.get('from') or [''])[0]
@@ -649,22 +907,12 @@ class H(BaseHTTPRequestHandler):
try:
version=validate_version(version)
status=publication_publisher().get_tag_publication_status(version)
except InvalidReleaseVersion as exc:
return self.sendj({'error': exc.code},400)
except GiteaConfigurationError as exc:
return self.sendj({'error': exc.code},503)
except PublicationError as exc:
return self.sendj({'error': getattr(exc, 'code', str(exc))},503)
except ValueError:
return self.sendj({'error':'invalid release version'},400)
return self.sendj(status)
if path == '/api/release-consistency':
request=publication_request_from_query(q)
try:
result=publication_publisher().get_release_consistency(request)
except GiteaConfigurationError as exc:
return self.sendj({'error': exc.code},503)
except PublicationError as exc:
return self.sendj({'error': getattr(exc, 'code', str(exc))},503)
return self.sendj(result)
return self.sendj(publication_publisher().get_release_consistency(request))
if path == '/api/release-acceptance':
request=publication_request_from_query(q)
return self.sendj(publication_publisher().run_release_acceptance(request))
@@ -725,12 +973,58 @@ 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')
if not a: return
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
return self.sendj(
silent_release_service().create_release_candidate(data)
)
@@ -1021,7 +1315,7 @@ def api_spec():
return {
'version': 'V2R2C1-IMMUTABLE-STAGING',
'openapi': '/api/openapi.yaml',
'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'],
'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'],
'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,
@@ -1036,6 +1330,9 @@ 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,
},
}
@@ -1044,3 +1341,4 @@ def main():
print(f'AIOS ChatGPT GitOps adapter listening on {HOST}:{PORT}')
ThreadingHTTPServer((HOST,PORT), H).serve_forever()
if __name__ == '__main__': main()
-86
View File
@@ -1,86 +0,0 @@
"""Narrow, non-root client contract for the privileged signer broker."""
from __future__ import annotations
import base64
import hashlib
import json
from pathlib import Path
import re
import socket
from typing import Any, Protocol
DEFAULT_SIGNER_BROKER_SOCKET = Path("/run/aios/signer-broker.sock")
MAX_SIGNING_PAYLOAD = 1024 * 1024
CODE_RE = re.compile(r"^[A-Z0-9_.:-]{3,96}$")
class SignerBrokerError(RuntimeError):
def __init__(self, code: str):
self.code = code
super().__init__(code)
class SignerBroker(Protocol):
def self_test(self) -> None: ...
def sign(self, payload_kind: str, payload: bytes) -> tuple[bytes, str]: ...
class UnixSignerBroker:
"""AF_UNIX request client; it never accepts commands, paths, or shell text."""
def __init__(self, socket_path: Path = DEFAULT_SIGNER_BROKER_SOCKET, timeout: float = 15.0):
path = Path(socket_path)
if path != DEFAULT_SIGNER_BROKER_SOCKET or not path.is_absolute():
raise SignerBrokerError("SIGNER_BROKER_SOCKET_INVALID")
self.socket_path = path
self.timeout = timeout
def _request(self, operation: str, **payload: Any) -> dict[str, Any]:
if operation not in {"self_test", "sign"}:
raise SignerBrokerError("SIGNER_OPERATION_REJECTED")
request = {
"schema": "aios.signer-broker.request.v1",
"operation": operation,
**payload,
}
try:
with socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) as channel:
channel.settimeout(self.timeout)
channel.connect(str(self.socket_path))
channel.sendall((json.dumps(request, sort_keys=True, separators=(",", ":")) + "\n").encode())
raw = channel.makefile("rb").readline()
except (OSError, TimeoutError) as exc:
raise SignerBrokerError("SIGNER_BROKER_UNAVAILABLE") from exc
try:
response = json.loads(raw.decode())
except (UnicodeDecodeError, json.JSONDecodeError) as exc:
raise SignerBrokerError("SIGNER_BROKER_RESPONSE_INVALID") from exc
if not isinstance(response, dict) or response.get("schema") != "aios.signer-broker.response.v1":
raise SignerBrokerError("SIGNER_BROKER_RESPONSE_INVALID")
if response.get("status") != "PASS":
code = str(response.get("error_code") or "SIGNER_BROKER_REJECTED")
raise SignerBrokerError(code if CODE_RE.fullmatch(code) else "SIGNER_BROKER_REJECTED")
return response
def self_test(self) -> None:
self._request("self_test")
def sign(self, payload_kind: str, payload: bytes) -> tuple[bytes, str]:
if payload_kind not in {"approval_plan", "grant_payload"}:
raise SignerBrokerError("SIGNER_PAYLOAD_KIND_REJECTED")
if not isinstance(payload, bytes) or not payload or len(payload) > MAX_SIGNING_PAYLOAD:
raise SignerBrokerError("SIGNER_PAYLOAD_INVALID")
response = self._request(
"sign",
payload_kind=payload_kind,
payload_sha256=hashlib.sha256(payload).hexdigest(),
payload_base64=base64.b64encode(payload).decode("ascii"),
)
try:
signature = base64.b64decode(str(response["signature_base64"]), validate=True)
key_id = str(response["key_id"]).strip()
except (KeyError, ValueError) as exc:
raise SignerBrokerError("SIGNER_BROKER_RESPONSE_INVALID") from exc
if not signature or not key_id or len(key_id) > 256:
raise SignerBrokerError("SIGNER_BROKER_RESPONSE_INVALID")
return signature, key_id
@@ -1,289 +0,0 @@
"""Privileged, fixed-function signer broker service."""
from __future__ import annotations
import argparse
import base64
import binascii
from dataclasses import dataclass
from datetime import datetime, timezone
import hashlib
import json
import os
from pathlib import Path
import re
import socket
import socketserver
import stat
import struct
import subprocess
import tempfile
import threading
from typing import Any, Callable, Mapping, Protocol, Sequence
REQUEST_SCHEMA = "aios.signer-broker.request.v1"
RESPONSE_SCHEMA = "aios.signer-broker.response.v1"
AUDIT_SCHEMA = "aios.signer-broker.audit.v1"
DEFAULT_SOCKET_PATH = Path("/run/aios/signer-broker.sock")
DEFAULT_SIGNER_PATH = Path("/usr/local/libexec/aios-grant-signer-adapter")
DEFAULT_AUDIT_PATH = Path("/var/lib/aios-gitops/audit/signer-broker.jsonl")
DEFAULT_STATE_ROOT = Path("/var/lib/aios-gitops/signer-broker")
ALLOWED_OPERATIONS = frozenset({"self_test", "sign"})
ALLOWED_PAYLOAD_KINDS = frozenset({"approval_plan", "grant_payload"})
MAX_SIGNING_PAYLOAD = 1024 * 1024
MAX_REQUEST_BYTES = 2 * MAX_SIGNING_PAYLOAD + 8192
CODE_RE = re.compile(r"^[A-Z0-9_.:-]{3,96}$")
SHA256_RE = re.compile(r"^[0-9a-f]{64}$")
class SignerBrokerServiceError(RuntimeError):
def __init__(self, code: str):
self.code = code if CODE_RE.fullmatch(code) else "SIGNER_INTERNAL_ERROR"
super().__init__(self.code)
class SignerRunner(Protocol):
def self_test(self) -> None: ...
def sign(self, payload_kind: str, payload: bytes) -> tuple[bytes, str]: ...
@dataclass(frozen=True, slots=True)
class SignerBrokerPolicy:
socket_path: Path = DEFAULT_SOCKET_PATH
signer_path: Path = DEFAULT_SIGNER_PATH
audit_path: Path = DEFAULT_AUDIT_PATH
state_root: Path = DEFAULT_STATE_ROOT
allowed_uids: frozenset[int] = frozenset()
signer_timeout: float = 60.0
max_payload_bytes: int = MAX_SIGNING_PAYLOAD
def __post_init__(self) -> None:
for name in ("socket_path", "signer_path", "audit_path", "state_root"):
if not Path(getattr(self, name)).is_absolute():
raise SignerBrokerServiceError("SIGNER_POLICY_PATH_INVALID")
if not self.allowed_uids or any(not isinstance(uid, int) or uid < 0 for uid in self.allowed_uids):
raise SignerBrokerServiceError("SIGNER_POLICY_CALLER_INVALID")
if not 0 < self.signer_timeout <= 300:
raise SignerBrokerServiceError("SIGNER_POLICY_TIMEOUT_INVALID")
if not 0 < self.max_payload_bytes <= MAX_SIGNING_PAYLOAD:
raise SignerBrokerServiceError("SIGNER_POLICY_PAYLOAD_LIMIT_INVALID")
class FixedSignerRunner:
"""Use one configured signer executable and broker-owned temporary paths."""
def __init__(self, policy: SignerBrokerPolicy):
self.policy = policy
def _run(self, argv: Sequence[str]) -> None:
try:
result = subprocess.run(list(argv), capture_output=True, text=True, check=False, timeout=self.policy.signer_timeout)
except FileNotFoundError as exc:
raise SignerBrokerServiceError("SIGNER_UNAVAILABLE") from exc
except subprocess.TimeoutExpired as exc:
raise SignerBrokerServiceError("SIGNER_TIMEOUT") from exc
except OSError as exc:
raise SignerBrokerServiceError("SIGNER_UNAVAILABLE") from exc
if result.returncode != 0:
raise SignerBrokerServiceError("SIGNER_FAILED")
def self_test(self) -> None:
self._run((str(self.policy.signer_path), "self-test"))
def sign(self, payload_kind: str, payload: bytes) -> tuple[bytes, str]:
if payload_kind not in ALLOWED_PAYLOAD_KINDS:
raise SignerBrokerServiceError("SIGNER_PAYLOAD_KIND_REJECTED")
try:
self.policy.state_root.mkdir(parents=True, exist_ok=True)
os.chmod(self.policy.state_root, 0o700)
with tempfile.TemporaryDirectory(prefix="request-", dir=self.policy.state_root) as directory:
root = Path(directory)
input_path = root / "payload.canonical"
signature_path = root / "payload.sig"
key_id_path = root / "payload.key-id"
input_path.write_bytes(payload)
os.chmod(input_path, 0o600)
self._run((str(self.policy.signer_path), "sign", "--namespace", payload_kind, "--input", str(input_path), "--signature-out", str(signature_path), "--key-id-out", str(key_id_path)))
signature = signature_path.read_bytes()
key_id = key_id_path.read_text(encoding="utf-8").strip()
except SignerBrokerServiceError:
raise
except (OSError, UnicodeError) as exc:
raise SignerBrokerServiceError("SIGNER_OUTPUT_INVALID") from exc
if not signature or not key_id or len(key_id) > 256 or "\n" in key_id or "\r" in key_id:
raise SignerBrokerServiceError("SIGNER_OUTPUT_INVALID")
return signature, key_id
class SignerBrokerService:
def __init__(self, policy: SignerBrokerPolicy, runner: SignerRunner | None = None, *, now: Callable[[], datetime] | None = None) -> None:
self.policy = policy
self.runner = runner or FixedSignerRunner(policy)
self.now = now or (lambda: datetime.now(timezone.utc))
self._audit_lock = threading.Lock()
@staticmethod
def _operation(value: Any) -> str:
return value if isinstance(value, str) and re.fullmatch(r"[A-Za-z0-9_.:-]{1,64}", value) else "INVALID"
def _audit(self, *, peer_uid: int, operation: str, status: str, error_code: str | None = None, payload_sha256: str | None = None) -> None:
event: dict[str, Any] = {"schema": AUDIT_SCHEMA, "timestamp": self.now().astimezone(timezone.utc).isoformat(), "peer_uid": peer_uid, "operation": self._operation(operation), "status": status}
if isinstance(payload_sha256, str) and SHA256_RE.fullmatch(payload_sha256):
event["payload_sha256"] = payload_sha256
if error_code:
event["error_code"] = error_code if CODE_RE.fullmatch(error_code) else "SIGNER_INTERNAL_ERROR"
try:
with self._audit_lock:
self.policy.audit_path.parent.mkdir(parents=True, exist_ok=True)
with self.policy.audit_path.open("a", encoding="utf-8") as handle:
handle.write(json.dumps(event, sort_keys=True, separators=(",", ":")) + "\n")
handle.flush()
os.fsync(handle.fileno())
os.chmod(self.policy.audit_path, 0o600)
except OSError as exc:
raise SignerBrokerServiceError("SIGNER_AUDIT_UNAVAILABLE") from exc
@staticmethod
def _failure(code: str) -> dict[str, Any]:
return {"schema": RESPONSE_SCHEMA, "status": "FAIL", "error_code": code if CODE_RE.fullmatch(code) else "SIGNER_INTERNAL_ERROR"}
def _dispatch(self, request: Mapping[str, Any], peer_uid: int) -> tuple[dict[str, Any], str, str | None]:
operation = self._operation(request.get("operation"))
if peer_uid not in self.policy.allowed_uids:
raise SignerBrokerServiceError("CALLER_UNAUTHORIZED")
if request.get("schema") != REQUEST_SCHEMA:
raise SignerBrokerServiceError("REQUEST_SCHEMA_INVALID")
if operation not in ALLOWED_OPERATIONS:
raise SignerBrokerServiceError("SIGNER_OPERATION_REJECTED")
if operation == "self_test":
if set(request) != {"schema", "operation"}:
raise SignerBrokerServiceError("REQUEST_FIELDS_REJECTED")
self.runner.self_test()
return {"schema": RESPONSE_SCHEMA, "status": "PASS", "operation": operation}, operation, None
if set(request) != {"schema", "operation", "payload_kind", "payload_sha256", "payload_base64"}:
raise SignerBrokerServiceError("REQUEST_FIELDS_REJECTED")
payload_kind = request.get("payload_kind")
encoded = request.get("payload_base64")
claimed_hash = request.get("payload_sha256")
if payload_kind not in ALLOWED_PAYLOAD_KINDS or not isinstance(encoded, str) or not isinstance(claimed_hash, str) or not SHA256_RE.fullmatch(claimed_hash):
raise SignerBrokerServiceError("SIGNER_PAYLOAD_INVALID")
if len(encoded) > self.policy.max_payload_bytes * 2 + 8:
raise SignerBrokerServiceError("SIGNER_PAYLOAD_INVALID")
try:
payload = base64.b64decode(encoded.encode("ascii"), validate=True)
except (ValueError, UnicodeError, binascii.Error) as exc:
raise SignerBrokerServiceError("SIGNER_PAYLOAD_INVALID") from exc
if not payload or len(payload) > self.policy.max_payload_bytes:
raise SignerBrokerServiceError("SIGNER_PAYLOAD_INVALID")
actual_hash = hashlib.sha256(payload).hexdigest()
if actual_hash != claimed_hash:
raise SignerBrokerServiceError("SIGNER_PAYLOAD_HASH_MISMATCH")
signature, key_id = self.runner.sign(payload_kind, payload)
if not isinstance(signature, bytes) or not signature or not isinstance(key_id, str) or not key_id.strip() or len(key_id) > 256:
raise SignerBrokerServiceError("SIGNER_OUTPUT_INVALID")
return {"schema": RESPONSE_SCHEMA, "status": "PASS", "operation": operation, "signature_base64": base64.b64encode(signature).decode("ascii"), "key_id": key_id.strip()}, operation, actual_hash
def handle_request(self, request: Mapping[str, Any], peer_uid: int) -> dict[str, Any]:
operation = self._operation(request.get("operation")) if isinstance(request, Mapping) else "INVALID"
payload_hash = request.get("payload_sha256") if isinstance(request, Mapping) else None
status = "FAIL"
error_code: str | None = None
try:
if not isinstance(request, Mapping):
raise SignerBrokerServiceError("REQUEST_OBJECT_REQUIRED")
response, operation, payload_hash = self._dispatch(request, peer_uid)
status = "PASS"
except SignerBrokerServiceError as exc:
error_code = exc.code
response = self._failure(exc.code)
except Exception:
error_code = "SIGNER_INTERNAL_ERROR"
response = self._failure(error_code)
try:
self._audit(peer_uid=peer_uid, operation=operation, status=status, error_code=error_code, payload_sha256=payload_hash if isinstance(payload_hash, str) else None)
except SignerBrokerServiceError as exc:
response = self._failure(exc.code)
return response
def handle_json_line(self, raw: bytes, peer_uid: int) -> dict[str, Any]:
if len(raw) > MAX_REQUEST_BYTES:
return self.handle_request({"operation": "invalid"}, peer_uid) | {"error_code": "REQUEST_TOO_LARGE", "status": "FAIL"}
try:
request = json.loads(raw.decode("utf-8"))
except (UnicodeDecodeError, json.JSONDecodeError):
return self.handle_request({"operation": "invalid"}, peer_uid) | {"error_code": "REQUEST_JSON_INVALID", "status": "FAIL"}
return self.handle_request(request, peer_uid)
class _UnixRequestHandler(socketserver.StreamRequestHandler):
def handle(self) -> None:
service: SignerBrokerService = self.server.service # type: ignore[attr-defined]
response = service.handle_json_line(self.rfile.readline(MAX_REQUEST_BYTES + 1), _peer_uid(self.request))
self.wfile.write((json.dumps(response, sort_keys=True, separators=(",", ":")) + "\n").encode("utf-8"))
self.wfile.flush()
class SignerBrokerUnixServer(socketserver.ThreadingMixIn, socketserver.UnixStreamServer):
daemon_threads = True
allow_reuse_address = True
def __init__(self, socket_path: Path, service: SignerBrokerService):
self.socket_path = Path(socket_path)
self.service = service
super().__init__(str(self.socket_path), _UnixRequestHandler)
def server_close(self) -> None:
super().server_close()
try:
if self.socket_path.exists() and stat.S_ISSOCK(self.socket_path.stat().st_mode):
self.socket_path.unlink()
except OSError:
pass
def _peer_uid(channel: socket.socket) -> int:
if not hasattr(socket, "SO_PEERCRED"):
return -1
try:
_pid, uid, _gid = struct.unpack("3i", channel.getsockopt(socket.SOL_SOCKET, socket.SO_PEERCRED, struct.calcsize("3i")))
return int(uid)
except (OSError, struct.error):
return -1
def build_server(policy: SignerBrokerPolicy, runner: SignerRunner | None = None) -> SignerBrokerUnixServer:
socket_path = policy.socket_path
socket_path.parent.mkdir(parents=True, exist_ok=True)
if socket_path.exists():
if not stat.S_ISSOCK(socket_path.stat().st_mode):
raise SignerBrokerServiceError("SIGNER_BROKER_SOCKET_CONFLICT")
socket_path.unlink()
server = SignerBrokerUnixServer(socket_path, SignerBrokerService(policy, runner))
os.chmod(socket_path, 0o660)
return server
def _parser() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser(description="AIOS fixed-function signer broker")
parser.add_argument("command", choices=("serve",))
parser.add_argument("--socket", default=str(DEFAULT_SOCKET_PATH))
parser.add_argument("--signer", default=str(DEFAULT_SIGNER_PATH))
parser.add_argument("--audit", default=str(DEFAULT_AUDIT_PATH))
parser.add_argument("--state-root", default=str(DEFAULT_STATE_ROOT))
parser.add_argument("--allowed-uid", action="append", type=int, required=True)
parser.add_argument("--timeout", type=float, default=60.0)
return parser
def main(argv: Sequence[str] | None = None) -> int:
args = _parser().parse_args(argv)
policy = SignerBrokerPolicy(socket_path=Path(args.socket), signer_path=Path(args.signer), audit_path=Path(args.audit), state_root=Path(args.state_root), allowed_uids=frozenset(args.allowed_uid), signer_timeout=args.timeout)
server = build_server(policy)
try:
server.serve_forever()
finally:
server.server_close()
return 0
if __name__ == "__main__":
raise SystemExit(main())
+39 -445
View File
@@ -31,8 +31,6 @@ import time
from pathlib import Path
from typing import Any, Mapping, Sequence
from version_registry import configured_authoritative_git_dir, verify_authoritative_repository
OID_RE = re.compile(r"^[0-9a-f]{40}$")
SHA_RE = re.compile(r"^[0-9a-f]{64}$")
PLAN_RE = re.compile(r"^sha256:[0-9a-f]{64}$")
@@ -51,11 +49,6 @@ REQUIRED_V2R1C7_MEMBERS = {
"payload/ARTIFACT-MANIFEST.json",
"payload/ROLLBACK-METADATA.json",
}
REQUIRED_V2R2C1_MEMBERS = {
"payload/versions/V2R2C1.json",
}
V2R2C1_TAG_OID = "07fc2746b688f5f243f29b39de39f32edc3dd9d2"
DEFAULT_AUTHORITATIVE_GIT_DIR = Path("/data/aios-gitops/repo.git")
class ReleaseBlocked(RuntimeError):
pass
@@ -79,13 +72,6 @@ class Binding:
production_service: str
production_port: int
repository_identity: str = "aios/aios"
# Legacy V2R2C1 receipts intentionally omit tag_oid. Future release
# bindings must provide it explicitly and may use a versioned schema.
tag_oid: str | None = None
binding_schema: str | None = None
source_registry_main_oid: str | None = None
source_registry_path: str | None = None
source_registry_sha256: str | None = None
def __post_init__(self) -> None:
if not self.version or not OID_RE.fullmatch(self.target_oid):
@@ -104,32 +90,6 @@ class Binding:
raise ValueError("invalid production port")
if self.repository_identity != "aios/aios":
raise ValueError("unexpected repository identity")
if self.tag_oid is not None and not OID_RE.fullmatch(self.tag_oid):
raise ValueError("invalid annotated tag object OID")
if self.binding_schema is not None and not self.binding_schema.startswith(
"aios.release-binding."
):
raise ValueError("invalid release binding schema")
provenance = (
self.source_registry_main_oid,
self.source_registry_sha256,
)
if any(item is not None for item in provenance):
if not all(item is not None for item in provenance):
raise ValueError("source registry provenance must be complete")
if not OID_RE.fullmatch(str(self.source_registry_main_oid)):
raise ValueError("invalid source registry main OID")
if not SHA_RE.fullmatch(str(self.source_registry_sha256)):
raise ValueError("invalid source registry SHA-256")
path = self.source_registry_path or f"versions/{self.version}.json"
candidate = Path(path)
if (
candidate.is_absolute()
or ".." in candidate.parts
or "\\" in path
or not path.endswith(".json")
):
raise ValueError("invalid source registry path")
@property
def tag(self) -> str:
@@ -148,22 +108,10 @@ class Binding:
"production_port": self.production_port,
"repository_identity": self.repository_identity,
}
if self.tag_oid is not None:
payload["tag_oid"] = self.tag_oid
payload["binding_schema"] = self.binding_schema or "aios.release-binding.v2"
if self.source_registry_main_oid is not None:
payload.update(
{
"source_registry_main_oid": self.source_registry_main_oid,
"source_registry_path": self.source_registry_path
or f"versions/{self.version}.json",
"source_registry_sha256": self.source_registry_sha256,
}
)
return hashlib.sha256(canonical_json(payload)).hexdigest()
def request(self, operation: str) -> dict[str, Any]:
request = {
return {
"schema": "aios.production-executor-request.v1",
"operation": operation,
"binding_fingerprint": self.fingerprint,
@@ -179,19 +127,6 @@ class Binding:
"production_port": self.production_port,
"repository_identity": self.repository_identity,
}
if self.tag_oid is not None:
request["tag_oid"] = self.tag_oid
request["binding_schema"] = self.binding_schema or "aios.release-binding.v2"
if self.source_registry_main_oid is not None:
request.update(
{
"source_registry_main_oid": self.source_registry_main_oid,
"source_registry_path": self.source_registry_path
or f"versions/{self.version}.json",
"source_registry_sha256": self.source_registry_sha256,
}
)
return request
def canonical_json(value: object) -> bytes:
@@ -233,23 +168,6 @@ def run_git(git_dir: Path, *args: str, check: bool = True) -> subprocess.Complet
)
def run_git_bytes(
git_dir: Path,
*args: str,
check: bool = True,
) -> subprocess.CompletedProcess[bytes]:
completed = subprocess.run(
["git", f"--git-dir={git_dir}", *args],
check=False,
capture_output=True,
timeout=30,
env={"PATH": os.environ.get("PATH", "")},
)
if check and completed.returncode != 0:
raise ReleaseBlocked(completed.stderr.decode("utf-8", "replace").strip())
return completed
def verify_git_lineage(git_dir: Path, binding: Binding) -> dict[str, Any]:
authority_main = run_git(git_dir, "rev-parse", "refs/heads/main").stdout.strip()
if not OID_RE.fullmatch(authority_main):
@@ -291,45 +209,7 @@ def verify_git_lineage(git_dir: Path, binding: Binding) -> dict[str, Any]:
if not target_in_evidence:
raise ReleaseBlocked("approved target is not an ancestor of evidence closure")
source_main = None
if binding.source_registry_main_oid is not None:
source_main = binding.source_registry_main_oid
source_type = run_git(git_dir, "cat-file", "-t", source_main).stdout.strip()
if source_type != "commit":
raise ReleaseBlocked("source_registry_main_oid is not a commit")
target_in_source_main = run_git(
git_dir,
"merge-base",
"--is-ancestor",
binding.target_oid,
source_main,
check=False,
).returncode == 0
if not target_in_source_main:
raise ManualOwnerReviewRequired(
"approved target is not an ancestor of source registry main"
)
expected_tag_oid = binding.tag_oid
if expected_tag_oid is None and binding.version == "V2R2C1":
expected_tag_oid = V2R2C1_TAG_OID
if expected_tag_oid is not None:
local_tag = run_git(
git_dir,
"rev-parse",
"--verify",
f"refs/tags/{binding.version}",
).stdout.strip()
if local_tag != expected_tag_oid:
raise ManualOwnerReviewRequired(
"future release binding tag object differs from local tag"
)
if run_git(git_dir, "cat-file", "-t", local_tag).stdout.strip() != "tag":
raise ReleaseBlocked("release tag is not annotated")
peeled = run_git(git_dir, "rev-parse", "--verify", f"{local_tag}^{{commit}}").stdout.strip()
if peeled != binding.target_oid:
raise ManualOwnerReviewRequired("release tag peeled target mismatch")
result = {
return {
"authority_main_oid": authority_main,
"target_oid": binding.target_oid,
"target_is_ancestor_of_main": True,
@@ -337,231 +217,9 @@ def verify_git_lineage(git_dir: Path, binding: Binding) -> dict[str, Any]:
"evidence_object_type": evidence_type,
"target_is_ancestor_of_evidence": True,
}
if source_main is not None:
result["source_registry_main_oid"] = source_main
result["target_is_ancestor_of_source_registry_main"] = True
return result
def _source_registry_git_provenance(
git_dir: Path,
binding: Binding,
) -> dict[str, Any] | None:
"""Read M:path and C:path independently of the artifact metadata."""
if binding.source_registry_main_oid is None:
return None
verify_authoritative_repository(git_dir)
source_main = binding.source_registry_main_oid
source_path = binding.source_registry_path or f"versions/{binding.version}.json"
authority_main = run_git(git_dir, "rev-parse", "--verify", "refs/heads/main").stdout.strip()
if run_git(git_dir, "cat-file", "-t", source_main).stdout.strip() != "commit":
raise ReleaseBlocked("source_registry_main_oid is not a commit")
if run_git(git_dir, "cat-file", "-t", authority_main).stdout.strip() != "commit":
raise ReleaseBlocked("Authority main OID is not a commit")
if run_git(
git_dir,
"merge-base",
"--is-ancestor",
source_main,
authority_main,
check=False,
).returncode != 0:
raise ManualOwnerReviewRequired(
"source registry main is not an ancestor of current Authority main"
)
source_blob = run_git_bytes(git_dir, "show", f"{source_main}:{source_path}").stdout
current_blob = run_git_bytes(git_dir, "show", f"{authority_main}:{source_path}").stdout
if source_blob != current_blob:
raise ManualOwnerReviewRequired(
"current Authority main source registry entry differs from pinned M"
)
try:
source = json.loads(source_blob.decode("utf-8"))
except (UnicodeDecodeError, json.JSONDecodeError) as exc:
raise ReleaseBlocked("Git source registry entry is invalid") from exc
expected_tag_oid = binding.tag_oid
if expected_tag_oid is None and binding.version == "V2R2C1":
expected_tag_oid = V2R2C1_TAG_OID
expected = {
"version": binding.version,
"repository": "aios/aios",
"tag_oid": expected_tag_oid,
"target_oid": binding.target_oid,
"peeled_target_oid": binding.target_oid,
"evidence_closure_oid": binding.evidence_closure_oid,
"approval_plan_id": binding.approval_plan_id,
}
if not isinstance(source, dict) or source.get("schema") not in {
"aios.source-registry.v1",
"aios.production-registry.v2",
}:
raise ReleaseBlocked("Git source registry schema is invalid")
if expected_tag_oid is None or any(source.get(key) != value for key, value in expected.items()):
raise ReleaseBlocked("Git source registry identity mismatch")
if source.get("tag_object") != expected_tag_oid:
raise ReleaseBlocked("Git source registry tag object mismatch")
digest = hashlib.sha256(source_blob).hexdigest()
if digest != binding.source_registry_sha256:
raise ReleaseBlocked("Git source registry SHA-256 mismatch")
return {
"source_registry_main_oid": source_main,
"current_main_oid": authority_main,
"source_registry_path": source_path,
"source_registry_bytes": source_blob,
"source_registry_sha256": digest,
"source_registry_identity_verified": True,
"source_registry_main_converged": True,
}
def _verify_artifact_members(
path: Path,
required: set[str],
binding: Binding | None = None,
git_provenance: Mapping[str, Any] | None = None,
) -> dict[str, Any]:
"""Verify archive safety and independently verify packaged release data."""
# Python tarfile may not have zstd support on the target host, so
# decompress into a private temporary tar and inspect it there.
with tempfile.TemporaryDirectory(prefix="aios-artifact-verify-") as temp_dir:
tar_path = Path(temp_dir) / "release.tar"
with tar_path.open("wb") as output:
completed = subprocess.run(
["zstd", "-dc", str(path)],
check=False,
stdout=output,
stderr=subprocess.PIPE,
timeout=60,
env={"PATH": os.environ.get("PATH", "")},
)
if completed.returncode != 0:
raise ReleaseBlocked("artifact zstd decompression failed")
import tarfile
with tarfile.open(tar_path, "r:") as archive:
members = archive.getmembers()
names: set[str] = set()
for member in members:
candidate = Path(member.name)
if (
candidate.is_absolute()
or ".." in candidate.parts
or member.issym()
or member.islnk()
or not (member.isfile() or member.isdir())
):
raise ReleaseBlocked(f"unsafe artifact member: {member.name}")
if member.name in names:
raise ReleaseBlocked(f"duplicate artifact member: {member.name}")
names.add(member.name)
source_path = None
if binding is not None and binding.version != "V2R1C7":
source_path = binding.source_registry_path or f"versions/{binding.version}.json"
required_members = set(required)
if binding is not None and binding.source_registry_main_oid is not None:
required_members.add("payload/BUILD-METADATA.json")
if source_path is not None and binding is not None:
required_members.add(f"payload/{source_path}")
missing = sorted(required_members - names)
if missing:
raise ReleaseBlocked(
"artifact missing required members: " + ",".join(missing)
)
verification: dict[str, Any] = {}
if binding is not None and source_path is not None:
member_name = f"payload/{source_path}"
member = archive.getmember(member_name)
stream = archive.extractfile(member)
if stream is None:
raise ReleaseBlocked("source registry member is unreadable")
raw = stream.read()
try:
source = json.loads(raw.decode("utf-8"))
except (UnicodeDecodeError, json.JSONDecodeError) as exc:
raise ReleaseBlocked("source registry member is invalid") from exc
canonical_source = (
json.dumps(source, ensure_ascii=False, sort_keys=True, indent=2) + "\n"
).encode("utf-8")
if raw != canonical_source:
raise ReleaseBlocked("source registry member is not canonical JSON")
expected_tag_oid = binding.tag_oid
if expected_tag_oid is None and binding.version == "V2R2C1":
expected_tag_oid = V2R2C1_TAG_OID
expected = {
"version": binding.version,
"repository": "aios/aios",
"tag_oid": expected_tag_oid,
"target_oid": binding.target_oid,
"peeled_target_oid": binding.target_oid,
"evidence_closure_oid": binding.evidence_closure_oid,
"approval_plan_id": binding.approval_plan_id,
}
if expected_tag_oid is None:
raise ReleaseBlocked("future release binding tag object is required")
if not isinstance(source, dict) or any(
source.get(key) != value for key, value in expected.items()
):
raise ReleaseBlocked("packaged source registry identity mismatch")
if source.get("tag_object") != expected_tag_oid:
raise ReleaseBlocked("packaged source registry tag object mismatch")
source_digest = hashlib.sha256(raw).hexdigest()
if git_provenance is not None:
if raw != git_provenance["source_registry_bytes"]:
raise ReleaseBlocked(
"packaged source registry differs from Git M:path"
)
if source_digest != git_provenance["source_registry_sha256"]:
raise ReleaseBlocked(
"packaged source registry SHA-256 differs from Git M:path"
)
verification.update(
{
"source_registry_path": source_path,
"source_registry_sha256": source_digest,
"source_registry_identity_verified": True,
}
)
if binding.source_registry_sha256 is not None:
if source_digest != binding.source_registry_sha256:
raise ReleaseBlocked("packaged source registry SHA-256 mismatch")
metadata_member = archive.getmember("payload/BUILD-METADATA.json")
metadata_stream = archive.extractfile(metadata_member)
if metadata_stream is None:
raise ReleaseBlocked("BUILD-METADATA.json is unreadable")
try:
metadata = json.loads(metadata_stream.read().decode("utf-8"))
except (UnicodeDecodeError, json.JSONDecodeError) as exc:
raise ReleaseBlocked("BUILD-METADATA.json is invalid") from exc
metadata_expected = {
"release_target_oid": binding.target_oid,
"source_registry_main_oid": binding.source_registry_main_oid,
"source_registry_path": source_path,
"source_registry_sha256": binding.source_registry_sha256,
}
if not isinstance(metadata, dict) or any(
metadata.get(key) != value
for key, value in metadata_expected.items()
):
raise ReleaseBlocked("BUILD-METADATA source provenance mismatch")
verification["build_metadata_provenance_verified"] = True
if git_provenance is not None:
verification.update(
{
"git_source_registry_member_verified": True,
"git_source_registry_main_converged": True,
"git_source_registry_sha256": git_provenance[
"source_registry_sha256"
],
}
)
return verification
def verify_artifact(
binding: Binding,
*,
repository: Path | str | None = None,
) -> dict[str, Any]:
def verify_artifact(binding: Binding) -> dict[str, Any]:
path = Path(binding.artifact_path)
if path.stat().st_size != binding.artifact_size:
raise ReleaseBlocked("artifact size mismatch")
@@ -569,32 +227,46 @@ def verify_artifact(
if actual != binding.artifact_sha256:
raise ReleaseBlocked("artifact SHA-256 mismatch")
required = {
"V2R1C7": REQUIRED_V2R1C7_MEMBERS,
"V2R2C1": REQUIRED_V2R2C1_MEMBERS,
}.get(binding.version)
member_verification: dict[str, Any] = {}
git_provenance = None
if binding.source_registry_main_oid is not None:
git_provenance = _source_registry_git_provenance(
Path(repository) if repository is not None else Path(
configured_authoritative_git_dir()
),
binding,
)
if required or binding.source_registry_main_oid is not None:
member_verification = _verify_artifact_members(
path,
required or set(),
binding,
git_provenance,
)
if binding.version == "V2R1C7":
# Python tarfile may not have zstd support on the target host, so
# decompress into a private temporary tar and inspect it there.
with tempfile.TemporaryDirectory(prefix="aios-artifact-verify-") as temp_dir:
tar_path = Path(temp_dir) / "release.tar"
with tar_path.open("wb") as output:
completed = subprocess.run(
["zstd", "-dc", str(path)],
check=False,
stdout=output,
stderr=subprocess.PIPE,
timeout=60,
env={"PATH": os.environ.get("PATH", "")},
)
if completed.returncode != 0:
raise ReleaseBlocked("artifact zstd decompression failed")
import tarfile
with tarfile.open(tar_path, "r:") as archive:
names: set[str] = set()
for member in archive.getmembers():
candidate = Path(member.name)
if (
candidate.is_absolute()
or ".." in candidate.parts
or member.issym()
or member.islnk()
or not (member.isfile() or member.isdir())
):
raise ReleaseBlocked(f"unsafe artifact member: {member.name}")
names.add(member.name)
missing = sorted(REQUIRED_V2R1C7_MEMBERS - names)
if missing:
raise ReleaseBlocked(
"artifact missing required members: " + ",".join(missing)
)
return {
"artifact_path": str(path),
"artifact_sha256": actual,
"artifact_size": binding.artifact_size,
"verified": True,
**member_verification,
}
@@ -610,18 +282,6 @@ def verify_authorization(receipt: Mapping[str, Any], binding: Binding) -> dict[s
"artifact_sha256": binding.artifact_sha256,
"approval_plan_id": binding.approval_plan_id,
}
if binding.tag_oid is not None:
expected["tag_oid"] = binding.tag_oid
expected["binding_schema"] = binding.binding_schema or "aios.release-binding.v2"
if binding.source_registry_main_oid is not None:
expected.update(
{
"source_registry_main_oid": binding.source_registry_main_oid,
"source_registry_path": binding.source_registry_path
or f"versions/{binding.version}.json",
"source_registry_sha256": binding.source_registry_sha256,
}
)
for key, value in expected.items():
if receipt.get(key) != value:
raise ManualOwnerReviewRequired(f"Owner authorization mismatch: {key}")
@@ -773,29 +433,13 @@ def tag_step(command: Sequence[str], binding: Binding) -> dict[str, Any]:
tag_oid = probe.get("tag_oid")
if not isinstance(tag_oid, str) or not OID_RE.fullmatch(tag_oid):
raise ReleaseBlocked("verified annotated tag object OID missing")
if binding.tag_oid is not None and tag_oid != binding.tag_oid:
raise ManualOwnerReviewRequired(
"same-name tag object differs from the future release binding"
)
return dict(probe)
def deploy_step(command: Sequence[str], binding: Binding) -> dict[str, Any]:
def require_runtime_registry(result: Mapping[str, Any]) -> None:
if binding.source_registry_main_oid is None:
return
if (
result.get("runtime_bundle_contains_current_version") is not True
or result.get("runtime_registry_converged") is not True
):
raise ReconciliationRequired(
"runtime bundle/runtime registry convergence was not independently verified"
)
probe = run_adapter(command, binding.request("probe"))
require_exact_response(probe, binding)
if probe.get("status") == "ACTIVE_VERIFIED":
require_runtime_registry(probe)
return dict(probe)
if probe.get("status") == "CONFLICT":
raise ManualOwnerReviewRequired("production runtime has conflicting binding")
@@ -814,30 +458,9 @@ def deploy_step(command: Sequence[str], binding: Binding) -> dict[str, Any]:
or probe.get("production_deployed") is not True
):
raise ReconciliationRequired("production health contract is not ACTIVE")
require_runtime_registry(probe)
return dict(probe)
def verify_post_deploy_source_registry(
repository: Path,
binding: Binding,
) -> dict[str, Any]:
"""Re-read authoritative Git after runtime deployment and before closure."""
if binding.source_registry_main_oid is None:
return {
"source_registry_converged": "not_required",
"source_registry_check": "not_required_for_legacy_binding",
}
provenance = _source_registry_git_provenance(repository, binding)
if provenance is None:
raise ReconciliationRequired("authoritative source registry was not checked")
return {
**provenance,
"source_registry_converged": True,
"source_registry_check": "post_deploy_authoritative_git_read",
}
def finalize_step(
command: Sequence[str],
binding: Binding,
@@ -878,7 +501,7 @@ def execute(
) -> dict[str, Any]:
authorization = verify_authorization(secure_json(authorization_path), binding)
lineage = verify_git_lineage(repository, binding)
artifact = verify_artifact(binding, repository=repository)
artifact = verify_artifact(binding)
state = DurableState(state_root, binding)
with FileLock(state_root / "executor.lock"):
state.record(
@@ -895,13 +518,6 @@ def execute(
runtime = deploy_step(deploy_command, binding)
state.record("RUNTIME_VERIFIED", "runtime", runtime)
# The runtime adapter proves only runtime facts. Re-read M:path
# and C:path from authoritative Git immediately before Registry
# and Baseline closure so a post-preflight main change cannot be
# mistaken for source convergence.
source_registry = verify_post_deploy_source_registry(repository, binding)
state.record("SOURCE_REGISTRY_VERIFIED", "source_registry", source_registry)
registry = finalize_step(
registry_command,
binding,
@@ -931,21 +547,10 @@ def execute(
require_exact_response(value, binding)
if final_runtime.get("status") != "ACTIVE_VERIFIED":
raise ReconciliationRequired("final runtime readback failed")
if binding.source_registry_main_oid is not None and not all(
final_runtime.get(name) is True
for name in (
"runtime_bundle_contains_current_version",
"runtime_registry_converged",
)
):
raise ReconciliationRequired(
"final runtime registry convergence readback failed"
)
if final_registry.get("status") != "VERIFIED":
raise ReconciliationRequired("final Registry readback failed")
if final_baseline.get("status") != "VERIFIED":
raise ReconciliationRequired("final Baseline readback failed")
final_source_registry = verify_post_deploy_source_registry(repository, binding)
final = {
"status": "PRODUCTION_ACCEPTED",
"version": binding.version,
@@ -956,7 +561,6 @@ def execute(
"tag_oid": tag["tag_oid"],
"tag": final_tag,
"runtime": final_runtime,
"source_registry": final_source_registry,
"registry": final_registry,
"baseline": final_baseline,
"force_push_used": False,
@@ -996,11 +600,6 @@ def parser() -> argparse.ArgumentParser:
p.add_argument("--evidence-closure-oid", required=True)
p.add_argument("--artifact-sha256", required=True)
p.add_argument("--approval-plan-id", required=True)
p.add_argument("--tag-oid")
p.add_argument("--binding-schema")
p.add_argument("--source-registry-main-oid")
p.add_argument("--source-registry-path")
p.add_argument("--source-registry-sha256")
p.add_argument("--production-service", required=True)
p.add_argument("--production-port", required=True, type=int)
p.add_argument("--repository-identity", default="aios/aios")
@@ -1024,11 +623,6 @@ def main() -> int:
production_service=args.production_service,
production_port=args.production_port,
repository_identity=args.repository_identity,
tag_oid=args.tag_oid,
binding_schema=args.binding_schema,
source_registry_main_oid=args.source_registry_main_oid,
source_registry_path=args.source_registry_path,
source_registry_sha256=args.source_registry_sha256,
)
try:
result = execute(
+14 -115
View File
@@ -64,40 +64,6 @@ class PublicationError(RuntimeError):
pass
class PublicationValueError(PublicationError, ValueError):
def __init__(self, code: str, message: str):
self.code = code
super().__init__(message)
class InvalidReleaseVersion(PublicationValueError):
def __init__(self):
super().__init__("INVALID_RELEASE_VERSION", "invalid release version")
class GiteaConfigurationError(PublicationValueError):
def __init__(self, code: str):
super().__init__(code, code)
class GiteaUnavailableError(PublicationError):
def __init__(self, code: str = "GITEA_UNAVAILABLE"):
self.code = code
super().__init__(code)
class GiteaTagNotFound(PublicationError):
def __init__(self):
self.code = "TAG_NOT_FOUND"
super().__init__(self.code)
class TagTargetMismatch(PublicationError):
def __init__(self):
self.code = "TAG_TARGET_MISMATCH"
super().__init__(self.code)
class PublicationConflict(PublicationError):
pass
@@ -119,7 +85,7 @@ class PublicationManualOwnerReviewRequired(
def validate_version(version: str) -> str:
value = str(version or "")
if not VERSION_RE.fullmatch(value):
raise InvalidReleaseVersion()
raise ValueError("invalid release version")
return value
@@ -436,40 +402,6 @@ class GitTagBackend:
)
@dataclass(frozen=True)
class GiteaClientConfig:
api_url: str
repository: str
token_file: Path
remote: str = "gitea"
token_line: int = 1
@classmethod
def from_authoritative_environment(cls, environ: dict[str, str] | None = None) -> "GiteaClientConfig":
values = os.environ if environ is None else environ
api_url = str(values.get("AIOS_GITOPS_GITEA_API_URL", "")).strip()
repository = str(values.get("AIOS_GITOPS_GITEA_REPOSITORY", "")).strip()
token_ref = str(values.get("AIOS_GITOPS_GITEA_TOKEN_FILE", "")).strip()
if not api_url:
raise GiteaConfigurationError("GITEA_API_URL_MISSING")
if not api_url.startswith(("https://", "http://")):
raise GiteaConfigurationError("GITEA_API_URL_INVALID")
if not repository:
raise GiteaConfigurationError("GITEA_REPOSITORY_MISSING")
if repository.count("/") != 1 or repository.startswith("/"):
raise GiteaConfigurationError("GITEA_REPOSITORY_INVALID")
if not token_ref:
raise GiteaConfigurationError("GITEA_TOKEN_REFERENCE_MISSING")
token_file = Path(token_ref)
if not token_file.is_absolute():
raise GiteaConfigurationError("GITEA_TOKEN_REFERENCE_INVALID")
remote = str(values.get("AIOS_GITOPS_GITEA_REMOTE", "gitea")).strip() or "gitea"
token_line = int(str(values.get("AIOS_GITOPS_GITEA_TOKEN_LINE", "1")))
if token_line < 1:
raise GiteaConfigurationError("GITEA_TOKEN_REFERENCE_INVALID")
return cls(api_url, repository, token_file, remote, token_line)
class GiteaTagClient:
def __init__(
self,
@@ -479,37 +411,23 @@ class GiteaTagClient:
timeout: float = 15,
token_line: int = 1,
):
self.api_url = str(api_url or "").rstrip("/")
self.repository = str(repository or "").strip("/")
self.token_file = Path(token_file)
self.api_url = api_url.rstrip("/")
self.repository = repository.strip("/")
self.token_file = token_file
self.timeout = timeout
self.token_line = token_line
if not self.api_url:
raise GiteaConfigurationError("GITEA_API_URL_MISSING")
if not self.api_url.startswith(("https://", "http://")):
raise GiteaConfigurationError("GITEA_API_URL_INVALID")
if not self.repository:
raise GiteaConfigurationError("GITEA_REPOSITORY_MISSING")
raise ValueError("Gitea API URL must be HTTP(S)")
if self.repository.count("/") != 1:
raise GiteaConfigurationError("GITEA_REPOSITORY_INVALID")
if not str(token_file) or not self.token_file.is_absolute():
raise GiteaConfigurationError("GITEA_TOKEN_REFERENCE_MISSING")
@classmethod
def from_authoritative_environment(cls, environ: dict[str, str] | None = None) -> "GiteaTagClient":
config = GiteaClientConfig.from_authoritative_environment(environ)
return cls(config.api_url, config.repository, config.token_file, token_line=config.token_line)
raise ValueError("Gitea repository must be owner/name")
def _get(self, path: str) -> Any:
try:
lines = self.token_file.read_text(encoding="utf-8").splitlines()
except (OSError, UnicodeError) as exc:
raise GiteaConfigurationError("GITEA_TOKEN_REFERENCE_UNAVAILABLE") from exc
lines = self.token_file.read_text(encoding="utf-8").splitlines()
if self.token_line < 1 or self.token_line > len(lines):
raise GiteaConfigurationError("GITEA_TOKEN_REFERENCE_INVALID")
raise RuntimeError("Gitea credential reference has no configured token line")
token = lines[self.token_line - 1].strip()
if not token:
raise GiteaConfigurationError("GITEA_TOKEN_REFERENCE_INVALID")
raise RuntimeError("Gitea token file is empty")
request = urllib.request.Request(
f"{self.api_url}{path}",
headers={
@@ -521,12 +439,10 @@ class GiteaTagClient:
try:
with urllib.request.urlopen(request, timeout=self.timeout) as response:
return json.loads(response.read().decode("utf-8"))
except urllib.error.HTTPError as exc:
if exc.code == 404:
raise GiteaTagNotFound() from exc
raise GiteaUnavailableError() from exc
except (urllib.error.URLError, TimeoutError, OSError) as exc:
raise GiteaUnavailableError() from exc
except (urllib.error.URLError, urllib.error.HTTPError, TimeoutError) as exc:
status = getattr(exc, "code", None)
detail = f" HTTP {status}" if status else ""
raise RuntimeError(f"Gitea API unavailable{detail}") from exc
def repository_identity(self) -> str:
repository = self._get(f"/repos/{self.repository}")
@@ -1034,12 +950,6 @@ class TagPublisher:
listing = state.get("gitea_listing_visible") is True
expected_tag_oid = state.get("tag_oid")
registry_tag_oid = registry.get("tag_oid") or registry.get("tag_object")
target_mismatch = any(
isinstance(value, tuple)
and len(value) > 1
and value[1] not in (None, request.target_oid)
for value in (remote, api)
)
checks = {
"fingerprint": all(
baseline.get(field) == getattr(request, field)
@@ -1064,7 +974,7 @@ class TagPublisher:
and registry.get("enabled") is True,
}
status = "PASS" if all(checks.values()) else "FAIL"
result = {
return {
"version": request.version,
"status": status,
"checks": checks,
@@ -1076,19 +986,8 @@ class TagPublisher:
"gitea_listing_visible": listing,
"repository_identity": repository_identity,
}
if target_mismatch:
result["fail_closed"] = True
result["error"] = TagTargetMismatch().code
return result
except PublicationManualOwnerReviewRequired:
raise
except PublicationError as exc:
return {
"version": request.version,
"status": "UNKNOWN",
"fail_closed": True,
"error": str(getattr(exc, "code", exc)),
}
except Exception as exc:
return {
"version": request.version,
+15 -157
View File
@@ -29,18 +29,9 @@ from gitea_tag_publication import (
PublicationRequest,
TagPublisher,
)
from version_registry import (
RegistryConvergenceError,
assert_source_registry_convergence,
resolve_source_registry_main_oid,
source_registry_snapshot,
verify_authoritative_repository,
verify_source_registry_git_identity,
)
VERSION = "V2R2C1"
TAG_OID = "07fc2746b688f5f243f29b39de39f32edc3dd9d2"
TARGET_OID = "998d30304787e31e5ae607c0bd86b5edb2b0e5ad"
EVIDENCE_CLOSURE_OID = "a8b906d1bc24088c19647e98de6a679268f6d0a2"
APPROVAL_PLAN_ID = "sha256:c0df52008ae7f64cec5ee2d28947aa7b892066c670ab8295e4549140b1a7500e"
@@ -88,7 +79,6 @@ def atomic_json(path: Path, value: Mapping[str, Any], mode: int = 0o600) -> None
@dataclass(frozen=True)
class ReleaseBinding:
version: str = VERSION
tag_oid: str = TAG_OID
target_oid: str = TARGET_OID
evidence_closure_oid: str = EVIDENCE_CLOSURE_OID
approval_plan_id: str = APPROVAL_PLAN_ID
@@ -99,8 +89,6 @@ class ReleaseBinding:
def validate(self) -> None:
if self.version != VERSION or self.repository != REPOSITORY:
raise ManualOwnerReviewRequired("release binding is outside V2R2C1 scope")
if not OID_RE.fullmatch(self.tag_oid) or self.tag_oid != TAG_OID:
raise ManualOwnerReviewRequired("V2R2C1 tag_oid mismatch")
if not OID_RE.fullmatch(self.target_oid) or self.target_oid != TARGET_OID:
raise ManualOwnerReviewRequired("V2R2C1 target_oid mismatch")
if not OID_RE.fullmatch(self.evidence_closure_oid) or self.evidence_closure_oid != EVIDENCE_CLOSURE_OID:
@@ -204,48 +192,19 @@ def _ancestor(git_dir: Path, ancestor: str, descendant: str) -> bool:
).returncode == 0
def verify_lineage(
git_dir: Path,
binding: ReleaseBinding,
*,
authority_main_oid: str | None = None,
) -> dict[str, Any]:
def verify_lineage(git_dir: Path, binding: ReleaseBinding) -> dict[str, Any]:
binding.validate()
main_oid = authority_main_oid or _git(git_dir, "rev-parse", "refs/heads/main")
if not OID_RE.fullmatch(main_oid) or _git(git_dir, "cat-file", "-t", main_oid) != "commit":
raise ReleaseBlocked("authoritative main OID is not a commit")
if _git(git_dir, "cat-file", "-t", binding.target_oid) != "commit":
raise ReleaseBlocked("approved target_oid is not a commit")
if _git(git_dir, "cat-file", "-t", binding.evidence_closure_oid) != "commit":
raise ReleaseBlocked("evidence closure is not a commit")
if not _ancestor(git_dir, binding.target_oid, binding.evidence_closure_oid):
raise ManualOwnerReviewRequired("approved target is not an ancestor of evidence closure")
if not _ancestor(git_dir, binding.target_oid, main_oid):
raise ManualOwnerReviewRequired("approved target is not an ancestor of source registry main")
return {
"target_oid": binding.target_oid,
"evidence_closure_oid": binding.evidence_closure_oid,
"target_is_ancestor_of_evidence": True,
"main_oid": main_oid,
"target_is_ancestor_of_main": True,
}
def verify_local_annotated_tag(git_dir: Path, binding: ReleaseBinding) -> dict[str, Any]:
"""Verify the local authoritative tag object without creating or rewriting it."""
tag_oid = _git(git_dir, "rev-parse", "--verify", f"refs/tags/{binding.version}")
if tag_oid != binding.tag_oid:
raise ManualOwnerReviewRequired("local annotated tag object mismatch")
if _git(git_dir, "cat-file", "-t", tag_oid) != "tag":
raise ReleaseBlocked("release tag is not annotated")
peeled = _git(git_dir, "rev-parse", "--verify", f"{tag_oid}^{{commit}}")
if peeled != binding.target_oid:
raise ManualOwnerReviewRequired("local annotated tag target mismatch")
return {
"tag_oid": tag_oid,
"tag_object_type": "tag",
"peeled_target_oid": peeled,
"target_oid": binding.target_oid,
"main_oid": _git(git_dir, "rev-parse", "refs/heads/main"),
}
@@ -289,41 +248,9 @@ def build_production_artifact(
binding: ReleaseBinding,
output_root: Path,
) -> dict[str, Any]:
"""Build a deterministic artifact from composite immutable provenance.
Release payload files are read from target ``T``. The source registry is
read once from the pinned main/convergence commit ``M`` and its exact blob
bytes are reused for packaging and all recorded digests.
"""
"""Build a deterministic executable V2R2C1 health artifact from target_oid."""
binding.validate()
verify_authoritative_repository(git_dir)
source_registry_main_oid = resolve_source_registry_main_oid(
git_dir,
main_ref="refs/heads/main",
)
verify_lineage(git_dir, binding, authority_main_oid=source_registry_main_oid)
tag_lineage = verify_local_annotated_tag(git_dir, binding)
try:
source_snapshot = source_registry_snapshot(
git_dir,
VERSION,
source_registry_main_oid=source_registry_main_oid,
expected_tag_oid=binding.tag_oid,
expected_target_oid=binding.target_oid,
expected_evidence_closure_oid=binding.evidence_closure_oid,
expected_approval_plan_id=binding.approval_plan_id,
)
source_registry = source_snapshot["record"]
source_registry_bytes = source_snapshot["bytes"]
verify_source_registry_git_identity(git_dir, source_registry)
except RegistryConvergenceError as exc:
raise ReleaseBlocked(f"SOURCE_REGISTRY_NOT_CONVERGED:{exc}") from exc
if source_snapshot["source_registry_main_oid"] != source_registry_main_oid:
raise ReconciliationRequired("source registry main OID changed during build")
if not _ancestor(git_dir, binding.target_oid, source_registry_main_oid):
raise ManualOwnerReviewRequired("release target is not an ancestor of source registry main")
source_registry_path = source_snapshot["source_registry_path"]
source_registry_sha256 = source_snapshot["source_registry_sha256"]
verify_lineage(git_dir, binding)
epoch = _commit_epoch(git_dir, binding.target_oid)
output_root.mkdir(parents=True, exist_ok=True)
stem = f"aios-{VERSION}-{binding.target_oid[:12]}"
@@ -334,15 +261,11 @@ def build_production_artifact(
source_paths = {
"active-runtime/app/deployment_state.py": "active-runtime/app/deployment_state.py",
"active-runtime/app/v2r1c6_privacy_tools.py": "active-runtime/app/v2r1c6_privacy_tools.py",
"versions/V2R2C1.json": "versions/V2R2C1.json",
}
for destination, source in source_paths.items():
target = root / destination
target.parent.mkdir(parents=True, exist_ok=True)
if destination == source_registry_path:
target.write_bytes(source_registry_bytes)
else:
target.write_bytes(_payload_file(git_dir, binding.target_oid, source))
target.write_bytes(_payload_file(git_dir, binding.target_oid, source))
generated: dict[str, str] = {
"VERSION": VERSION + "\n",
"active-runtime/app/v2r2c1_health_service.py": (
@@ -384,6 +307,16 @@ def build_production_artifact(
"with urllib.request.urlopen(url, timeout=5) as r: print(json.dumps(json.load(r), sort_keys=True))\n"
),
"scripts/rollback_v2r2c1_release.sh": "#!/bin/sh\nset -eu\nprintf '%s\\n' 'rollback requires governed owner-approved executor'\nexit 2\n",
"release-manifests/V2R2C1.json": json.dumps({
"schema": "aios.release-source.v2",
"version": VERSION,
"repository": REPOSITORY,
"target_oid": binding.target_oid,
"evidence_closure_oid": binding.evidence_closure_oid,
"production_deployed": False,
"deployment_state": "STAGING",
"artifact_kind": "executable-production-health",
}, sort_keys=True, separators=(",", ":")) + "\n",
}
for rel, text in generated.items():
target = root / rel
@@ -395,10 +328,6 @@ def build_production_artifact(
"schema": "aios.release-artifact-build.v3",
"version": VERSION,
"repository": REPOSITORY,
"release_target_oid": binding.target_oid,
"source_registry_main_oid": source_registry_main_oid,
"source_registry_path": source_registry_path,
"source_registry_sha256": source_registry_sha256,
"source_head_oid": binding.target_oid,
"evidence_closure_oid": binding.evidence_closure_oid,
"source_date_epoch": epoch,
@@ -408,31 +337,6 @@ def build_production_artifact(
"tag_created": False,
"baseline_changed": False,
"secrets_included": False,
"source_registry": {
"path": source_registry_path,
"version": source_registry["version"],
"tag_oid": source_registry["tag_oid"],
"target_oid": source_registry["target_oid"],
"peeled_target_oid": source_registry["peeled_target_oid"],
"evidence_closure_oid": source_registry["evidence_closure_oid"],
"approval_plan_id": source_registry["approval_plan_id"],
"source_registry_main_oid": source_registry_main_oid,
"source_registry_sha256": source_registry_sha256,
},
"provenance": {
"release_payload_source": {
"commit_oid": binding.target_oid,
"role": "release_target",
},
"source_registry_source": {
"commit_oid": source_registry_main_oid,
"path": source_registry_path,
"sha256": source_registry_sha256,
"role": "source_registry_convergence",
},
"release_target_is_ancestor_of_source_registry_main": True,
"local_annotated_tag": tag_lineage,
},
}
(root / "BUILD-METADATA.json").write_bytes(canonical(metadata))
(root / "ROLLBACK-METADATA.json").write_bytes(canonical(json.loads((root / "deploy/aios-v2/v2r2c1-rollback.json").read_text(encoding="utf-8"))))
@@ -440,10 +344,6 @@ def build_production_artifact(
(root / "ARTIFACT-MANIFEST.json").write_bytes(canonical({
"schema": "aios.release-artifact-manifest.v3",
"version": VERSION,
"release_target_oid": binding.target_oid,
"source_registry_main_oid": source_registry_main_oid,
"source_registry_path": source_registry_path,
"source_registry_sha256": source_registry_sha256,
"source_head_oid": binding.target_oid,
"evidence_closure_oid": binding.evidence_closure_oid,
"repository": REPOSITORY,
@@ -475,10 +375,6 @@ def build_production_artifact(
"schema": "aios.release-artifact-receipt.v3",
"version": VERSION,
"repository": REPOSITORY,
"release_target_oid": binding.target_oid,
"source_registry_main_oid": source_registry_main_oid,
"source_registry_path": source_registry_path,
"source_registry_sha256": source_registry_sha256,
"source_head_oid": binding.target_oid,
"evidence_closure_oid": binding.evidence_closure_oid,
"artifact": str(output),
@@ -489,14 +385,6 @@ def build_production_artifact(
"production_deployed": False,
"tag_created": False,
"baseline_changed": False,
"source_registry_path": source_registry_path,
"source_registry_identity": source_registry,
"provenance": {
"release_payload_source": binding.target_oid,
"source_registry_source": source_registry_main_oid,
"release_target_is_ancestor_of_source_registry_main": True,
"local_annotated_tag": tag_lineage,
},
}
atomic_json(output.with_suffix(output.suffix + ".receipt.json"), receipt, mode=0o640)
return receipt
@@ -570,12 +458,6 @@ class GovernedProductionReleaseExecutor:
self.state.fail("RECONCILIATION_REQUIRED", str(exc))
raise ReconciliationRequired(str(exc)) from exc
result = dict(verified)
if result.get("tag_oid") != self.binding.tag_oid:
self.state.fail(
"MANUAL_OWNER_REVIEW_REQUIRED",
"same-name tag object differs from the approved V2R2C1 tag",
)
raise ManualOwnerReviewRequired("approved tag object mismatch")
self.state.record("TAG_VERIFIED", "tag", result)
return {"status": "REMOTE_VERIFIED", **result}
@@ -596,8 +478,6 @@ class GovernedProductionReleaseExecutor:
tag_state = self.state.value.get("completed", {}).get("tag")
if not isinstance(tag_state, Mapping) or tag_state.get("state") not in {"GITEA_API_CONFIRMED", "REMOTE_VERIFIED"}:
raise ReleaseBlocked("REMOTE_TAG_PROOF_REQUIRED")
if tag_state.get("tag_oid") != self.binding.tag_oid:
raise ManualOwnerReviewRequired("approved tag object mismatch")
artifact = self.state.value.get("completed", {}).get("production_artifact")
if not isinstance(artifact, Mapping):
artifact = self.build_artifact(request)
@@ -608,13 +488,6 @@ class GovernedProductionReleaseExecutor:
"artifact_size": int(artifact["artifact_size"]),
"artifact_sha256": artifact["artifact_sha256"],
}
for name in (
"source_registry_main_oid",
"source_registry_path",
"source_registry_sha256",
):
if name in artifact:
value[name] = artifact[name]
# The production adapter is privileged and is run through the root
# approved-release service boundary by the caller. This handler is
# still useful for a root-owned staging invocation and remains
@@ -634,21 +507,6 @@ class GovernedProductionReleaseExecutor:
result["production_health"] = _health(self.config.health_url, self.binding, self.state.value.get("completed", {}).get("production_artifact", {}))
result["registry"] = _read_json(Path(self.config.registry_root) / f"{VERSION}.json")
result["baseline"] = _read_json(Path(self.config.baseline_path))
try:
assert_source = Path(self.config.registry_root) / f"{VERSION}.json"
if not assert_source.is_file():
raise RegistryConvergenceError("runtime_registry_entry_missing")
assert_source_registry_convergence(
assert_source,
repo_git_dir=self.config.authoritative_git_dir,
)
source_check = {"status": "PASS", "path": str(assert_source)}
except RegistryConvergenceError as exc:
source_check = {"status": "FAIL", "error": str(exc)}
result["source_registry"] = source_check
result.setdefault("checks", {})["source_registry"] = source_check["status"] == "PASS"
if result.get("status") == "PASS" and source_check["status"] != "PASS":
result["status"] = "FAIL"
return result
def acceptance(self, request: Mapping[str, Any]) -> dict[str, Any]:
+5 -73
View File
@@ -7,7 +7,7 @@ Registry and authoritative runtime-anchor Baseline only.
Registry is finalized before Baseline. Baseline apply independently verifies:
- exact Authority annotated tag and peeled target
- exact ACTIVE production health
- exact runtime Registry record and source-registry convergence
- exact V2R1C7 Registry record
- exact previous Baseline file SHA-256 (CAS)
"""
from __future__ import annotations
@@ -25,7 +25,6 @@ import time
import urllib.error
import urllib.request
from pathlib import Path
from version_registry import assert_source_registry_convergence
from typing import Any, Mapping
OID_RE = re.compile(r"^[0-9a-f]{40}$")
@@ -98,40 +97,11 @@ def load_request() -> dict[str, Any]:
raise ValueError("approval_plan_id_invalid")
if "tag_oid" in value and not OID_RE.fullmatch(str(value["tag_oid"])):
raise ValueError("tag_oid_invalid")
if "source_registry_main_oid" in value and not OID_RE.fullmatch(
str(value["source_registry_main_oid"])
):
raise ValueError("source_registry_main_oid_invalid")
if "source_registry_sha256" in value and not SHA_RE.fullmatch(
str(value["source_registry_sha256"])
):
raise ValueError("source_registry_sha256_invalid")
if "source_registry_path" in value:
source_path = str(value["source_registry_path"])
if (
not source_path.startswith("versions/")
or ".." in Path(source_path).parts
or "\\" in source_path
or not source_path.endswith(".json")
):
raise ValueError("source_registry_path_invalid")
provenance = tuple(
value.get(name)
for name in (
"source_registry_main_oid",
"source_registry_path",
"source_registry_sha256",
)
)
if any(item is not None for item in provenance) and not all(
item is not None for item in provenance
):
raise ValueError("source_registry_provenance_incomplete")
return value
def exact_response(value: Mapping[str, Any], status: str, **extra: Any) -> dict[str, Any]:
result = {
return {
"status": status,
"version": value["version"],
"target_oid": value["target_oid"],
@@ -140,14 +110,6 @@ def exact_response(value: Mapping[str, Any], status: str, **extra: Any) -> dict[
"approval_plan_id": value["approval_plan_id"],
**extra,
}
for name in (
"source_registry_main_oid",
"source_registry_path",
"source_registry_sha256",
):
if name in value:
result[name] = value[name]
return result
def git(repository: Path, *args: str, check: bool = True) -> subprocess.CompletedProcess[str]:
@@ -192,7 +154,7 @@ def health(url: str, value: Mapping[str, Any]) -> dict[str, Any]:
def registry_record(value: Mapping[str, Any], tag_oid: str, production_health: Mapping[str, Any]) -> dict[str, Any]:
result = {
return {
"schema": "aios.production-registry.v2",
"version": value["version"],
"repository": "aios/aios",
@@ -213,18 +175,10 @@ def registry_record(value: Mapping[str, Any], tag_oid: str, production_health: M
"deployment_state": production_health["deployment_state"],
"updated_at": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()),
}
for name in (
"source_registry_main_oid",
"source_registry_path",
"source_registry_sha256",
):
if name in value:
result[name] = value[name]
return result
def registry_matches(record: Mapping[str, Any], value: Mapping[str, Any], tag_oid: str) -> bool:
matches = all(
return all(
record.get(key) == expected
for key, expected in {
"version": value["version"],
@@ -238,16 +192,11 @@ def registry_matches(record: Mapping[str, Any], value: Mapping[str, Any], tag_oi
"production_deployed": True,
}.items()
) and record.get("registered") is True and record.get("enabled") is True
return matches and all(
record.get(name) == value.get(name)
for name in ("source_registry_main_oid", "source_registry_path", "source_registry_sha256")
if name in value
)
def baseline_matches(record: Mapping[str, Any], value: Mapping[str, Any], tag_oid: str) -> bool:
current = record.get("current_baseline") or record.get("version")
matches = current == value["version"] and all(
return current == value["version"] and all(
record.get(key) == expected
for key, expected in {
"target_oid": value["target_oid"],
@@ -260,11 +209,6 @@ def baseline_matches(record: Mapping[str, Any], value: Mapping[str, Any], tag_oi
"status": "PRODUCTION_DEPLOYED",
}.items()
)
return matches and all(
record.get(name) == value.get(name)
for name in ("source_registry_main_oid", "source_registry_path", "source_registry_sha256")
if name in value
)
def registry_probe(args: argparse.Namespace, value: Mapping[str, Any]) -> dict[str, Any]:
@@ -361,11 +305,6 @@ def baseline_apply(args: argparse.Namespace, value: Mapping[str, Any]) -> dict[s
int(previous.get("generation", 0) or 0),
int(previous.get("authority_generation", 0) or 0),
) + 1
assert_source_registry_convergence(
registry_path,
repo_git_dir=args.repository,
source_registry_main_oid=value.get("source_registry_main_oid"),
)
record = {
"schema": "aios.production-baseline.v2",
"source_identity": "aios-gitops-runtime-anchor-baseline-authority-v1",
@@ -397,13 +336,6 @@ def baseline_apply(args: argparse.Namespace, value: Mapping[str, Any]) -> dict[s
"promoted_at": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()),
"promoted_by": "aios-approved-production-release-executor",
}
for name in (
"source_registry_main_oid",
"source_registry_path",
"source_registry_sha256",
):
if name in value:
record[name] = value[name]
atomic_json(args.baseline_path, record)
return baseline_probe(args, value)
@@ -15,8 +15,6 @@ import subprocess
from pathlib import Path
from typing import Any, Mapping, Sequence
from aios_signer_broker import SignerBroker, SignerBrokerError, UnixSignerBroker
_SIGNER_FLAGS = (
"--namespace",
@@ -157,36 +155,6 @@ def _sign(
raise SignerContractError("signer_output_empty")
def run_signer_self_test(broker: SignerBroker | None = None) -> dict[str, str]:
active = broker or UnixSignerBroker()
try:
active.self_test()
except SignerBrokerError as exc:
raise SignerContractError(exc.code) from exc
return {"status": "PASS", "contract": "aios.signer-broker.v1"}
def _sign_with_broker(
broker: SignerBroker,
payload_kind: str,
input_path: Path,
signature_path: Path,
key_id_path: Path,
) -> None:
if signature_path.exists() or key_id_path.exists():
raise SignerContractError("contract_output_exists")
try:
signature, key_id = broker.sign(payload_kind, input_path.read_bytes())
except SignerBrokerError as exc:
raise SignerContractError(exc.code) from exc
if not signature or not key_id.strip():
raise SignerContractError("signer_output_empty")
signature_path.write_bytes(signature)
key_id_path.write_text(key_id.strip() + "\n", encoding="utf-8")
os.chmod(signature_path, 0o600)
os.chmod(key_id_path, 0o600)
def _manifest(grant_dir: Path, names: Sequence[str]) -> None:
rows = []
for name in names:
@@ -316,8 +284,6 @@ def run_compatible_signer(
signer_command: Sequence[str],
grant_dir: Path,
result_path: Path,
*,
broker: SignerBroker | None = None,
) -> dict[str, str]:
try:
if not _supports_deployed_signer(signer_command):
@@ -352,21 +318,22 @@ def run_compatible_signer(
"signing_key_identity": signer,
}
prepared = _prepare(grant_dir)
active_broker = broker or UnixSignerBroker()
try:
active_broker.self_test()
except SignerBrokerError as exc:
raise SignerContractError(exc.code) from exc
_sign_with_broker(
active_broker,
"approval_plan",
config_path = Path(
os.environ.get("AIOS_GRANT_ROOT_CONFIG", "/etc/aios/grant-root/config.json")
)
config = _read(config_path)
grant_namespace = str(config["grant_namespace"])
approval_namespace = str(config["approval_namespace"])
_sign(
signer_command,
approval_namespace,
grant_dir / "approval-plan.canonical.json",
grant_dir / "approval-plan.sig",
grant_dir / "approval-plan.key-id",
)
_sign_with_broker(
active_broker,
"grant_payload",
_sign(
signer_command,
grant_namespace,
grant_dir / "grant-payload.canonical.json",
grant_dir / "signed-grant.sig",
grant_dir / "signed-grant.key-id",
@@ -7,7 +7,6 @@ previous V2R1C7 release available for an explicit, owner-approved rollback.
from __future__ import annotations
import json
import hashlib
import os
import shutil
import sys
@@ -30,7 +29,6 @@ ROLLBACK_ROOT = Path("/var/lib/aios/v2r2c1/rollback")
REQUIRED_MEMBERS = {
"payload/VERSION",
"payload/versions/V2R2C1.json",
"payload/active-runtime/app/deployment_state.py",
"payload/active-runtime/app/v2r1c6_privacy_tools.py",
"payload/active-runtime/app/v2r2c1_health_service.py",
@@ -89,65 +87,12 @@ def execute_request(value: Mapping[str, Any]) -> dict[str, Any]:
_backup(Path("/var/lib/aios/v2r1c7/deployment-state.json"), "v2r1c7-deployment-state.previous")
return base.install_release(_args(), value)
if value.get("operation") == "probe":
result = base.probe(_args(), value)
if result.get("status") == "ACTIVE_VERIFIED":
result.update(_runtime_registry_gate(value))
return result
return base.probe(_args(), value)
if value.get("operation") == "rollback":
return rollback(value)
raise ValueError("unsupported_operation")
def _runtime_registry_gate(value: Mapping[str, Any]) -> dict[str, Any]:
"""Prove the deployed immutable runtime contains the exact M:path blob."""
source_digest = value.get("source_registry_sha256")
source_main = value.get("source_registry_main_oid")
if not source_digest or not source_main:
return {}
source_path = str(value.get("source_registry_path") or f"versions/{VERSION}.json")
member = CURRENT_LINK / source_path
if member.is_symlink() or not member.is_file():
raise RuntimeError("RUNTIME_SOURCE_REGISTRY_MEMBER_MISSING")
raw = member.read_bytes()
digest = hashlib.sha256(raw).hexdigest()
if digest != source_digest:
raise RuntimeError("RUNTIME_SOURCE_REGISTRY_DIGEST_MISMATCH")
try:
source = json.loads(raw.decode("utf-8"))
except (UnicodeDecodeError, json.JSONDecodeError) as exc:
raise RuntimeError("RUNTIME_SOURCE_REGISTRY_INVALID") from exc
expected = {
"version": VERSION,
"repository": "aios/aios",
"tag_oid": value.get("tag_oid"),
"target_oid": value.get("target_oid"),
"peeled_target_oid": value.get("target_oid"),
"evidence_closure_oid": value.get("evidence_closure_oid"),
"approval_plan_id": value.get("approval_plan_id"),
}
if not isinstance(source, dict) or any(source.get(key) != expected_value for key, expected_value in expected.items()):
raise RuntimeError("RUNTIME_SOURCE_REGISTRY_IDENTITY_MISMATCH")
if source.get("tag_object") != expected["tag_oid"]:
raise RuntimeError("RUNTIME_SOURCE_REGISTRY_TAG_OBJECT_MISMATCH")
version_name = f"{VERSION}.json"
return {
"runtime_registry_versions": [version_name],
"attested_projection_versions": [version_name],
"effective_registry_versions": [version_name],
# The adapter can only inspect the deployed immutable runtime. It
# must not claim convergence with mutable authoritative Git; the
# executor performs that independent M/C read after deployment.
"source_registry_converged": "not_checked",
"source_registry_convergence_error": "executor_must_recheck_authoritative_git",
"runtime_registry_converged": True,
"runtime_bundle_contains_current_version": True,
"runtime_bundle_contains_version": True,
"registry_consistency_status": "RUNTIME_ONLY_VERIFIED",
"runtime_source_registry_main_oid": source_main,
"runtime_source_registry_sha256": digest,
}
def rollback(value: Mapping[str, Any]) -> dict[str, Any]:
"""Explicit rollback only; this adapter never invokes it automatically."""
import subprocess
-982
View File
@@ -1,982 +0,0 @@
"""Release registry visibility and source-convergence gates.
Two invariants are enforced:
1. listVersions may overlay exactly the current Production Baseline-attested
projection onto the immutable source bundle. Arbitrary projection files
are not surfaced.
2. release_metadata_adapter.baseline_apply may not finalize a production
baseline unless the governed repository/main already contains a source
versions/<version>.json entry matching the release identity.
The source convergence gate intentionally checks release identity, not
post-deployment runtime fields. This allows the source entry to be committed
before deployment while still preventing PRODUCTION_DEPLOYED from being
written for an uncommitted release identity.
"""
from __future__ import annotations
import hashlib
import json
import os
import re
import stat
import subprocess
from datetime import datetime, timezone
from pathlib import Path
from typing import Any
_VERSION_RE = re.compile(r"^V[0-9]+R[0-9]+C[0-9]+(?:-[A-Za-z0-9_.-]+)?$")
_OID_RE = re.compile(r"^[0-9a-f]{40}$")
_PLAN_RE = re.compile(r"^sha256:[0-9a-f]{64}$")
VERSION_RE = _VERSION_RE
OID_RE = _OID_RE
SOURCE_REGISTRY_SCHEMAS = frozenset(
{"aios.source-registry.v1", "aios.production-registry.v2"}
)
SOURCE_REGISTRY_FIELDS = (
"version",
"tag_oid",
"target_oid",
"peeled_target_oid",
"evidence_closure_oid",
"approval_plan_id",
)
REPOSITORY_IDENTITY = "aios/aios"
DEFAULT_PROJECTION_ROOT = Path(
os.getenv(
"AIOS_GITOPS_REGISTRY_PROJECTION_ROOT",
"/var/lib/aios-gitops/runtime/registry-projection",
)
)
DEFAULT_BASELINE_FILE = Path(
os.getenv(
"AIOS_GITOPS_BASELINE_FILE",
"/var/lib/aios-gitops-runtime-anchor/baseline/current.json",
)
)
DEFAULT_MAIN_REF = os.getenv("AIOS_GITOPS_MAIN_REF", "refs/heads/main")
DEFAULT_AUTHORITATIVE_GIT_DIR = Path("/data/aios-gitops/repo.git")
DEFAULT_APPROVAL_AUTHORITY_ROOT = Path("/var/lib/aios-gitops/approval-authority")
TEST_MODE_ENV = "AIOS_TEST_MODE"
AUTHORITATIVE_GIT_ENV = "AIOS_GITOPS_AUTHORITATIVE_GIT_DIR"
APPROVAL_AUTHORITY_ROOT_ENV = "AIOS_GITOPS_APPROVAL_AUTHORITY_ROOT"
REPOSITORY_IDENTITY_ENV = "AIOS_GITOPS_REPOSITORY_IDENTITY"
class RegistryConvergenceError(RuntimeError):
"""Release registry/source convergence is not proven."""
def _load_object(path: Path) -> dict[str, Any]:
obj = json.loads(path.read_text(encoding="utf-8"))
if not isinstance(obj, dict):
raise RegistryConvergenceError(f"json_not_object:{path}")
return obj
def _sha256(path: Path) -> str:
return hashlib.sha256(path.read_bytes()).hexdigest()
def _git_text(repo_git_dir: Path | str, *args: str) -> str:
completed = subprocess.run(
["git", f"--git-dir={Path(repo_git_dir)}", *args],
capture_output=True,
text=True,
check=False,
timeout=45,
env={"PATH": os.environ.get("PATH", "")},
)
if completed.returncode != 0:
raise RegistryConvergenceError(
completed.stderr.strip() or "authoritative_git_query_failed"
)
return completed.stdout.strip()
def _git_bytes(repo_git_dir: Path | str, *args: str) -> bytes:
completed = subprocess.run(
["git", f"--git-dir={Path(repo_git_dir)}", *args],
capture_output=True,
check=False,
timeout=45,
env={"PATH": os.environ.get("PATH", "")},
)
if completed.returncode != 0:
raise RegistryConvergenceError(
completed.stderr.decode("utf-8", "replace").strip()
or "authoritative_git_query_failed"
)
return completed.stdout
def test_mode_enabled() -> bool:
"""Return whether explicit dependency injection is allowed for tests."""
return os.environ.get(TEST_MODE_ENV) == "1"
def _configured_override(env_name: str, default: str, label: str) -> Path:
if env_name in os.environ:
if not test_mode_enabled():
raise RegistryConvergenceError(f"{label}_env_override_forbidden")
value = os.environ.get(env_name, "")
if not value:
raise RegistryConvergenceError(f"{label}_env_override_invalid")
return Path(value)
return Path(default)
def configured_authoritative_git_dir() -> Path:
return _configured_override(
AUTHORITATIVE_GIT_ENV,
str(DEFAULT_AUTHORITATIVE_GIT_DIR),
"authoritative_repository",
)
def configured_repository_identity() -> str:
if REPOSITORY_IDENTITY_ENV in os.environ:
if not test_mode_enabled():
raise RegistryConvergenceError("repository_identity_env_override_forbidden")
value = os.environ.get(REPOSITORY_IDENTITY_ENV, "")
if not value:
raise RegistryConvergenceError("repository_identity_env_override_invalid")
return value
return REPOSITORY_IDENTITY
def verify_authoritative_repository(
repo_git_dir: Path | str,
*,
repository_identity: str = REPOSITORY_IDENTITY,
expected_path: Path | str | None = None,
) -> dict[str, str]:
"""Prove that a Git directory is the configured AIOS authority.
A repository that merely contains internally consistent objects is not
sufficient. The resolved path must be the configured authority and the
bare repository must advertise the expected Gitea repository identity.
"""
if repository_identity != REPOSITORY_IDENTITY:
raise RegistryConvergenceError("authoritative_repository_identity_invalid")
candidate = Path(repo_git_dir)
configured = Path(expected_path) if expected_path is not None else configured_authoritative_git_dir()
if test_mode_enabled() and expected_path is None and AUTHORITATIVE_GIT_ENV not in os.environ:
# Explicit test mode may inject a temporary bare repository directly.
# Production mode always compares against the fixed authority path.
configured = candidate
try:
candidate_real = candidate.resolve(strict=True)
configured_real = configured.resolve(strict=True)
except OSError as exc:
raise RegistryConvergenceError("authoritative_repository_path_unavailable") from exc
if candidate_real != configured_real:
raise RegistryConvergenceError("authoritative_repository_path_mismatch")
if not candidate_real.is_dir():
raise RegistryConvergenceError("authoritative_repository_not_directory")
if _git_text(candidate_real, "rev-parse", "--is-bare-repository") != "true":
raise RegistryConvergenceError("authoritative_repository_not_bare")
remotes = subprocess.run(
[
"git",
f"--git-dir={candidate_real}",
"config",
"--get-regexp",
r"^remote\..*\.url$",
],
capture_output=True,
text=True,
check=False,
timeout=45,
env={"PATH": os.environ.get("PATH", "")},
)
urls = [line.split(None, 1)[1] for line in remotes.stdout.splitlines() if " " in line]
identity_suffix = repository_identity.removesuffix(".git")
identity_verified = any(
url.rstrip("/").removesuffix(".git").replace("\\", "/").split(":")[-1]
in {identity_suffix, "/" + identity_suffix}
or url.rstrip("/").removesuffix(".git").replace("\\", "/").split(":")[-1].endswith(
"/" + identity_suffix
)
for url in urls
)
if not test_mode_enabled() and not identity_verified:
raise RegistryConvergenceError("authoritative_repository_identity_unverified")
return {
"repository_identity": repository_identity,
"configured_path": str(configured_real),
"resolved_path": str(candidate_real),
}
def resolve_source_registry_main_oid(
repo_git_dir: Path | str,
*,
main_ref: str = DEFAULT_MAIN_REF,
) -> str:
"""Resolve the authoritative main ref to one immutable commit OID."""
oid = _git_text(repo_git_dir, "rev-parse", "--verify", f"{main_ref}^{{commit}}")
if not _OID_RE.fullmatch(oid):
raise RegistryConvergenceError("source_registry_main_oid_invalid")
if _git_text(repo_git_dir, "cat-file", "-t", oid) != "commit":
raise RegistryConvergenceError("source_registry_main_oid_not_commit")
return oid
def source_registry_snapshot(
repo_git_dir: Path | str,
version: str,
*,
main_ref: str = DEFAULT_MAIN_REF,
source_registry_main_oid: str | None = None,
expected_tag_oid: str | None = None,
expected_target_oid: str | None = None,
expected_evidence_closure_oid: str | None = None,
expected_approval_plan_id: str | None = None,
) -> dict[str, Any]:
"""Read one source entry and its exact bytes from one immutable commit.
Callers building an artifact should resolve ``source_registry_main_oid``
once and pass it back here. The function then never consults the mutable
ref again, so validation and packaging use the same Git blob.
"""
if not isinstance(version, str) or not _VERSION_RE.fullmatch(version):
raise RegistryConvergenceError("source_registry_version_invalid")
main_oid = source_registry_main_oid or resolve_source_registry_main_oid(
repo_git_dir,
main_ref=main_ref,
)
if not _OID_RE.fullmatch(main_oid):
raise RegistryConvergenceError("source_registry_main_oid_invalid")
if _git_text(repo_git_dir, "cat-file", "-t", main_oid) != "commit":
raise RegistryConvergenceError("source_registry_main_oid_not_commit")
path = f"versions/{version}.json"
try:
raw = _git_bytes(repo_git_dir, "show", f"{main_oid}:{path}")
except RegistryConvergenceError as exc:
if "path" in str(exc).lower() or "does not exist" in str(exc).lower():
raise RegistryConvergenceError(
f"source_registry_entry_missing:{main_oid}:{path}"
) from exc
raise
try:
source = json.loads(raw.decode("utf-8"))
except (UnicodeDecodeError, json.JSONDecodeError) as exc:
raise RegistryConvergenceError("source_registry_json_invalid") from exc
record = validate_source_registry_record(
source,
expected_version=version,
expected_tag_oid=expected_tag_oid,
expected_target_oid=expected_target_oid,
expected_evidence_closure_oid=expected_evidence_closure_oid,
expected_approval_plan_id=expected_approval_plan_id,
)
return {
"record": record,
"bytes": raw,
"source_registry_main_oid": main_oid,
"source_registry_path": path,
"source_registry_sha256": hashlib.sha256(raw).hexdigest(),
}
def validate_source_registry_record(
value: Any,
*,
expected_version: str | None = None,
expected_tag_oid: str | None = None,
expected_target_oid: str | None = None,
expected_evidence_closure_oid: str | None = None,
expected_approval_plan_id: str | None = None,
) -> dict[str, Any]:
"""Validate the immutable identity carried by versions/<version>.json."""
if not isinstance(value, dict):
raise RegistryConvergenceError("source_registry_not_object")
if value.get("schema") not in SOURCE_REGISTRY_SCHEMAS:
raise RegistryConvergenceError("source_registry_schema_invalid")
version = value.get("version")
if not isinstance(version, str) or not _VERSION_RE.fullmatch(version):
raise RegistryConvergenceError("source_registry_version_invalid")
if expected_version is not None and version != expected_version:
raise RegistryConvergenceError("source_registry_mismatch:version")
if value.get("repository") != REPOSITORY_IDENTITY:
raise RegistryConvergenceError("source_registry_repository_invalid")
for field in ("tag_oid", "target_oid", "peeled_target_oid", "evidence_closure_oid"):
item = value.get(field)
if not isinstance(item, str) or not _OID_RE.fullmatch(item):
raise RegistryConvergenceError(f"source_registry_{field}_invalid")
if value["peeled_target_oid"] != value["target_oid"]:
raise RegistryConvergenceError("source_registry_peeled_target_mismatch")
approval = value.get("approval_plan_id")
if not isinstance(approval, str) or not _PLAN_RE.fullmatch(approval):
raise RegistryConvergenceError("source_registry_approval_plan_invalid")
if value.get("tag_object", value["tag_oid"]) != value["tag_oid"]:
raise RegistryConvergenceError("source_registry_tag_object_mismatch")
expected = {
"tag_oid": expected_tag_oid,
"target_oid": expected_target_oid,
"evidence_closure_oid": expected_evidence_closure_oid,
"approval_plan_id": expected_approval_plan_id,
}
for field, wanted in expected.items():
if wanted is not None and value.get(field) != wanted:
raise RegistryConvergenceError(f"source_registry_mismatch:{field}")
return dict(value)
def build_source_registry_entry(
*,
version: str,
tag_oid: str,
target_oid: str,
peeled_target_oid: str,
evidence_closure_oid: str,
approval_plan_id: str,
repository: str = REPOSITORY_IDENTITY,
) -> dict[str, Any]:
"""Build the pre-promotion source entry; it performs no Git or runtime write."""
value = {
"schema": "aios.source-registry.v1",
"version": version,
"repository": repository,
"tag": version,
"tag_oid": tag_oid,
"tag_object": tag_oid,
"target_oid": target_oid,
"peeled_target_oid": peeled_target_oid,
"evidence_closure_oid": evidence_closure_oid,
"approval_plan_id": approval_plan_id,
}
return validate_source_registry_record(
value,
expected_version=version,
expected_tag_oid=tag_oid,
expected_target_oid=target_oid,
expected_evidence_closure_oid=evidence_closure_oid,
expected_approval_plan_id=approval_plan_id,
)
def verify_source_registry_git_identity(
repo_git_dir: Path | str,
record: dict[str, Any],
) -> dict[str, Any]:
"""Prove that every Git identity in a source entry exists and is related."""
record = validate_source_registry_record(record)
version = record["version"]
tag_oid = record["tag_oid"]
target_oid = record["target_oid"]
evidence_oid = record["evidence_closure_oid"]
if _git_text(repo_git_dir, "cat-file", "-t", tag_oid) != "tag":
raise RegistryConvergenceError("source_registry_tag_not_annotated")
named_tag = _git_text(
repo_git_dir,
"rev-parse",
"--verify",
f"refs/tags/{version}",
)
if named_tag != tag_oid:
raise RegistryConvergenceError("source_registry_named_tag_mismatch")
if _git_text(repo_git_dir, "cat-file", "-t", target_oid) != "commit":
raise RegistryConvergenceError("source_registry_target_not_commit")
peeled = _git_text(
repo_git_dir,
"rev-parse",
"--verify",
f"{tag_oid}^{{commit}}",
)
if peeled != target_oid or record["peeled_target_oid"] != target_oid:
raise RegistryConvergenceError("source_registry_tag_target_mismatch")
if _git_text(repo_git_dir, "cat-file", "-t", evidence_oid) != "commit":
raise RegistryConvergenceError("source_registry_evidence_not_commit")
ancestor = subprocess.run(
[
"git",
f"--git-dir={Path(repo_git_dir)}",
"merge-base",
"--is-ancestor",
target_oid,
evidence_oid,
],
check=False,
capture_output=True,
timeout=45,
env={"PATH": os.environ.get("PATH", "")},
)
if ancestor.returncode != 0:
raise RegistryConvergenceError("source_registry_evidence_lineage_invalid")
return {
"repository": REPOSITORY_IDENTITY,
"version": version,
"tag_oid": tag_oid,
"tag_object_type": "tag",
"target_oid": target_oid,
"peeled_target_oid": peeled,
"evidence_closure_oid": evidence_oid,
"target_is_ancestor_of_evidence": True,
}
def verify_source_registry_main_convergence(
repo_git_dir: Path | str,
version: str,
*,
main_ref: str = DEFAULT_MAIN_REF,
source_registry_main_oid: str | None = None,
expected_tag_oid: str | None = None,
expected_target_oid: str | None = None,
expected_evidence_closure_oid: str | None = None,
expected_approval_plan_id: str | None = None,
) -> dict[str, Any]:
"""Prove that pinned M is still converged with current authoritative main C."""
current_main_oid = resolve_source_registry_main_oid(repo_git_dir, main_ref=main_ref)
source_main_oid = source_registry_main_oid or current_main_oid
if not _OID_RE.fullmatch(source_main_oid):
raise RegistryConvergenceError("source_registry_main_oid_invalid")
ancestor = subprocess.run(
[
"git",
f"--git-dir={Path(repo_git_dir)}",
"merge-base",
"--is-ancestor",
source_main_oid,
current_main_oid,
],
check=False,
capture_output=True,
timeout=45,
env={"PATH": os.environ.get("PATH", "")},
)
if ancestor.returncode != 0:
raise RegistryConvergenceError("source_registry_main_not_ancestor_of_current_main")
pinned = source_registry_snapshot(
repo_git_dir,
version,
source_registry_main_oid=source_main_oid,
expected_tag_oid=expected_tag_oid,
expected_target_oid=expected_target_oid,
expected_evidence_closure_oid=expected_evidence_closure_oid,
expected_approval_plan_id=expected_approval_plan_id,
)
current = source_registry_snapshot(
repo_git_dir,
version,
source_registry_main_oid=current_main_oid,
expected_tag_oid=expected_tag_oid,
expected_target_oid=expected_target_oid,
expected_evidence_closure_oid=expected_evidence_closure_oid,
expected_approval_plan_id=expected_approval_plan_id,
)
if pinned["bytes"] != current["bytes"]:
raise RegistryConvergenceError("source_registry_main_entry_changed_after_pin")
return {
"source_registry_main_oid": source_main_oid,
"current_main_oid": current_main_oid,
"source_registry_path": pinned["source_registry_path"],
"source_registry_sha256": pinned["source_registry_sha256"],
"current_main_source_registry_sha256": current["source_registry_sha256"],
"source_registry_main_is_ancestor_of_current_main": True,
"source_registry_entry_matches_current_main": True,
}
_APPROVAL_AUTHORITY_STATES = frozenset(
{
"APPROVED",
"GITEA_API_CONFIRMED",
"REMOTE_VERIFIED",
"PRODUCTION_DEPLOYED",
}
)
_APPROVAL_AUTHORITY_SCHEMAS = frozenset(
{
"aios.v2r2c1.tag-approval-request.v1",
"aios.owner-release-approval.v1",
"aios.release-binding-approval.v2",
}
)
def configured_approval_authority_root() -> Path:
return _configured_override(
APPROVAL_AUTHORITY_ROOT_ENV,
str(DEFAULT_APPROVAL_AUTHORITY_ROOT),
"approval_authority_root",
)
def _secure_approval_authority_path(
authority_path: Path | str,
version: str,
) -> tuple[Path, Path]:
root = configured_approval_authority_root()
path = Path(authority_path)
if path.is_dir():
path = path / f"{version}.json"
try:
root_real = root.resolve(strict=True)
path_real = path.resolve(strict=True)
except OSError as exc:
raise RegistryConvergenceError("approval_authority_trusted_root_missing") from exc
try:
path_real.relative_to(root_real)
except ValueError as exc:
raise RegistryConvergenceError("approval_authority_untrusted_path") from exc
metadata = path.lstat()
if not stat.S_ISREG(metadata.st_mode) or path.is_symlink():
raise RegistryConvergenceError("approval_authority_not_regular")
if metadata.st_mode & 0o022:
raise RegistryConvergenceError("approval_authority_permissions_invalid")
if APPROVAL_AUTHORITY_ROOT_ENV not in os.environ and metadata.st_uid != 0:
raise RegistryConvergenceError("approval_authority_owner_invalid")
return path, root_real
def _canonical_json(value: object) -> bytes:
return (json.dumps(value, sort_keys=True, separators=(",", ":")) + "\n").encode(
"utf-8"
)
def _approval_is_revoked(root: Path, approval_plan_id: str, authority: dict[str, Any]) -> bool:
if any(
authority.get(name) is True
for name in ("revoked", "invalidated", "revocation")
):
return True
state = authority.get("state") or authority.get("status")
if state in {"REVOKED", "INVALIDATED", "EXPIRED", "CANCELLED"}:
return True
for marker in (
root / "revocations" / approval_plan_id,
root / "revocations" / approval_plan_id.replace(":", "_"),
):
if marker.exists():
return True
expires_at = authority.get("expires_at")
if isinstance(expires_at, str) and expires_at:
try:
instant = datetime.fromisoformat(expires_at.replace("Z", "+00:00"))
except ValueError as exc:
raise RegistryConvergenceError("approval_expiry_invalid") from exc
if instant <= datetime.now(timezone.utc):
return True
return False
def verify_approval_authority(
authority_path: Path | str,
record: dict[str, Any],
) -> dict[str, Any]:
"""Verify the existing release approval/publication receipt association.
The authority document is intentionally supplied by the governed caller;
this helper never creates or rewrites an approval. The path must be under
the configured canonical Owner-authority root and the document must use a
known release-approval schema. A caller-supplied JSON file elsewhere is
never an authority, even when its fields are internally consistent.
"""
path, trusted_root = _secure_approval_authority_path(authority_path, record["version"])
authority = _load_object(path)
schema = authority.get("schema")
if schema not in _APPROVAL_AUTHORITY_SCHEMAS:
raise RegistryConvergenceError("approval_authority_schema_invalid")
if _approval_is_revoked(trusted_root, record["approval_plan_id"], authority):
raise RegistryConvergenceError("approval_plan_revoked_or_invalidated")
nested = authority.get("release_binding") or authority.get("release_request")
if not isinstance(nested, dict):
nested = authority.get("binding")
if not isinstance(nested, dict):
raise RegistryConvergenceError("approval_authority_binding_missing")
expected = {
"version": record["version"],
"repository": REPOSITORY_IDENTITY,
"tag_oid": record["tag_oid"],
"target_oid": record["target_oid"],
"peeled_target_oid": record["peeled_target_oid"],
"evidence_closure_oid": record["evidence_closure_oid"],
"approval_plan_id": record["approval_plan_id"],
}
if authority.get("approval_plan_id") != expected["approval_plan_id"]:
raise RegistryConvergenceError("approval_plan_not_found_or_mismatched")
state = authority.get("state") or authority.get("status")
if state not in _APPROVAL_AUTHORITY_STATES and authority.get("approved") is not True:
raise RegistryConvergenceError("approval_plan_not_approved")
owner_approval = authority.get("owner_approval")
if schema == "aios.v2r2c1.tag-approval-request.v1":
if not isinstance(owner_approval, dict):
raise RegistryConvergenceError("canonical_owner_approval_missing")
if (
owner_approval.get("approved") is not True
or owner_approval.get("status") != "APPROVED"
or owner_approval.get("approved_by") != "aiman"
or owner_approval.get("approval_source") != "explicit_owner_approval_in_chat"
):
raise RegistryConvergenceError("canonical_owner_approval_invalid")
expected_digest = hashlib.sha256(_canonical_json(owner_approval)).hexdigest()
if authority.get("owner_approval_digest_sha256") != expected_digest:
raise RegistryConvergenceError("canonical_owner_approval_digest_invalid")
for field, wanted in expected.items():
observed = nested.get(field)
if field == "repository" and observed is None:
observed = nested.get("repository_identity") or authority.get(
"repository_identity"
)
if field == "peeled_target_oid" and observed is None:
observed = (
nested.get("gitea_peeled_target_oid")
or authority.get("gitea_peeled_target_oid")
or nested.get("remote_peeled_target_oid")
or authority.get("remote_peeled_target_oid")
)
if observed != wanted:
raise RegistryConvergenceError(f"approval_plan_release_binding_mismatch:{field}")
if state == "GITEA_API_CONFIRMED":
gitea_tag = authority.get("gitea_tag_oid")
gitea_peeled = authority.get("gitea_peeled_target_oid")
if gitea_tag != expected["tag_oid"] or gitea_peeled != expected["target_oid"]:
raise RegistryConvergenceError("approval_plan_remote_tag_binding_mismatch")
return {
"path": str(path),
"schema": schema,
"state": state or "APPROVED",
"approval_plan_id": expected["approval_plan_id"],
"owner_approval_verified": schema == "aios.v2r2c1.tag-approval-request.v1",
"release_binding_verified": True,
}
def source_registry_json(value: dict[str, Any]) -> bytes:
"""Return the canonical bytes used by the governed file writer."""
return (json.dumps(value, ensure_ascii=False, sort_keys=True, indent=2) + "\n").encode(
"utf-8"
)
def _validated_current_projection(
projection_root: Path,
baseline_file: Path,
) -> Path | None:
"""Return only the exact projection attested by the current Baseline."""
if not baseline_file.is_file():
return None
try:
baseline = _load_object(baseline_file)
except Exception:
return None
version = baseline.get("version")
if not isinstance(version, str) or not _VERSION_RE.fullmatch(version):
return None
if baseline.get("status") != "PRODUCTION_DEPLOYED":
return None
if baseline.get("production_deployed") is not True:
return None
source = baseline.get("registry_source")
digest = baseline.get("registry_digest")
if not isinstance(source, str) or not isinstance(digest, str):
return None
candidate = projection_root / f"{version}.json"
try:
if candidate.resolve() != Path(source).resolve():
return None
except Exception:
return None
if not candidate.is_file() or _sha256(candidate) != digest:
return None
try:
record = _load_object(candidate)
except Exception:
return None
try:
validate_source_registry_record(
record,
expected_version=version,
expected_tag_oid=baseline.get("tag_oid"),
expected_target_oid=baseline.get("target_oid"),
expected_evidence_closure_oid=baseline.get("evidence_closure_oid"),
expected_approval_plan_id=baseline.get("approval_plan_id"),
)
except RegistryConvergenceError:
return None
if record.get("schema") != "aios.production-registry.v2":
return None
if record.get("status") != "PRODUCTION_DEPLOYED":
return None
if record.get("version") != version:
return None
if record.get("repository") != REPOSITORY_IDENTITY:
return None
if record.get("production_deployed") is not True:
return None
tag_oid = record.get("tag_oid")
target_oid = record.get("target_oid")
if not isinstance(tag_oid, str) or not _OID_RE.fullmatch(tag_oid):
return None
if not isinstance(target_oid, str) or not _OID_RE.fullmatch(target_oid):
return None
if record.get("peeled_target_oid") != target_oid:
return None
return candidate
def list_registry_versions(
workspace: Path | str,
*,
projection_root: Path = DEFAULT_PROJECTION_ROOT,
baseline_file: Path = DEFAULT_BASELINE_FILE,
) -> list[str]:
"""Return the legacy display list; use the report for provenance."""
return list_registry_versions_report(
workspace,
projection_root=projection_root,
baseline_file=baseline_file,
)["registry_versions"]
def list_registry_versions_report(
workspace: Path | str,
*,
projection_root: Path = DEFAULT_PROJECTION_ROOT,
baseline_file: Path = DEFAULT_BASELINE_FILE,
repo_git_dir: Path | str | None = None,
main_ref: str = DEFAULT_MAIN_REF,
) -> dict[str, Any]:
"""Return version visibility with runtime/projection provenance.
The display list intentionally retains the historical projection overlay
for API compatibility. The explicit booleans make that overlay
non-authoritative: a projection-only version has
``runtime_bundle_contains_version == false`` and can never be reported as
converged.
"""
ws = Path(workspace)
versions_root = ws / "versions"
runtime_names = {
path.name
for path in versions_root.iterdir()
if path.is_file()
and not path.is_symlink()
and path.suffix == ".json"
and _VERSION_RE.fullmatch(path.stem)
} if versions_root.is_dir() else set()
current = _validated_current_projection(projection_root, baseline_file)
attested_version = current.stem if current is not None else None
runtime_bundle_contains = bool(
attested_version is not None
and f"{attested_version}.json" in runtime_names
)
consistency_status = "NO_ATTESTED_PROJECTION"
source_converged: bool | str = "unknown"
source_error: str | None = None
runtime_converged = False
projection_record: dict[str, Any] | None = None
if current is not None:
try:
projection_record = _load_object(current)
except (OSError, RegistryConvergenceError, json.JSONDecodeError) as exc:
source_error = f"projection_invalid:{type(exc).__name__}"
if repo_git_dir is None:
source_converged = "unknown"
source_error = "authoritative_repository_unavailable"
elif projection_record is not None:
try:
verify_authoritative_repository(repo_git_dir)
verify_source_registry_main_convergence(
repo_git_dir,
attested_version or "",
main_ref=main_ref,
expected_tag_oid=projection_record.get("tag_oid"),
expected_target_oid=projection_record.get("target_oid"),
expected_evidence_closure_oid=projection_record.get(
"evidence_closure_oid"
),
expected_approval_plan_id=projection_record.get("approval_plan_id"),
)
source_converged = True
except RegistryConvergenceError as exc:
detail = str(exc)
if detail.startswith("authoritative_repository_"):
source_converged = "unknown"
source_error = (
"authoritative_repository_unavailable"
if detail.endswith("path_unavailable")
else detail
)
else:
source_converged = False
source_error = detail
if current is not None and not runtime_bundle_contains:
consistency_status = "RUNTIME_ARTIFACT_RECONCILIATION_REQUIRED"
elif current is not None and runtime_bundle_contains:
runtime_path = versions_root / current.name
try:
projection_record = projection_record or _load_object(current)
runtime_record = _load_object(runtime_path)
identity_fields = (
"version",
"repository",
"tag_oid",
"target_oid",
"peeled_target_oid",
"evidence_closure_oid",
"approval_plan_id",
)
runtime_converged = all(
runtime_record.get(field) == projection_record.get(field)
for field in identity_fields
)
if not runtime_converged:
consistency_status = "RUNTIME_IDENTITY_MISMATCH"
except (OSError, RegistryConvergenceError, json.JSONDecodeError):
consistency_status = "RUNTIME_ENTRY_INVALID"
runtime_converged = False
if current is not None and runtime_bundle_contains and runtime_converged:
if source_converged is True:
consistency_status = "FULLY_CONSISTENT"
elif source_converged == "unknown":
consistency_status = "SOURCE_GIT_NOT_CHECKED"
else:
consistency_status = "SOURCE_REGISTRY_NOT_CONVERGED"
elif current is not None and not runtime_bundle_contains:
if source_converged is False:
consistency_status = "SOURCE_REGISTRY_NOT_CONVERGED"
else:
consistency_status = "RUNTIME_ARTIFACT_RECONCILIATION_REQUIRED"
names = set(runtime_names)
if current is not None:
names.add(current.name)
effective = sorted(names)
projection_versions = [attested_version + ".json"] if attested_version else []
return {
"registry_versions": effective,
"effective_registry_versions": effective,
"runtime_registry_versions": sorted(runtime_names),
"attested_projection_versions": projection_versions,
"attested_projection_version": attested_version,
"source_registry_converged": source_converged,
"source_registry_convergence_error": source_error,
"runtime_registry_converged": runtime_converged,
"runtime_bundle_contains_current_version": runtime_bundle_contains,
"runtime_bundle_contains_version": runtime_bundle_contains,
"registry_consistency_status": consistency_status,
}
def assert_runtime_registry_convergence(
workspace: Path | str,
version: str,
*,
projection_root: Path = DEFAULT_PROJECTION_ROOT,
baseline_file: Path = DEFAULT_BASELINE_FILE,
) -> dict[str, Any]:
"""Fail closed when a projection-only version is visible but unpackaged."""
report = list_registry_versions_report(
workspace,
projection_root=projection_root,
baseline_file=baseline_file,
)
expected_name = f"{version}.json"
if (
report.get("attested_projection_version") == version
and report.get("runtime_bundle_contains_version") is not True
):
raise RegistryConvergenceError("runtime_bundle_missing_source_registry")
if expected_name not in report.get("runtime_registry_versions", []):
raise RegistryConvergenceError("runtime_bundle_missing_source_registry")
if report.get("source_registry_converged") is not True:
raise RegistryConvergenceError("source_registry_not_converged_or_not_checked")
if report.get("registry_consistency_status") not in {"FULLY_CONSISTENT", "PASS"}:
raise RegistryConvergenceError(
f"runtime_registry_consistency:{report.get('registry_consistency_status')}"
)
return report
def source_registry_from_main(
repo_git_dir: Path | str,
version: str,
*,
main_ref: str = DEFAULT_MAIN_REF,
source_registry_main_oid: str | None = None,
expected_tag_oid: str | None = None,
expected_target_oid: str | None = None,
expected_evidence_closure_oid: str | None = None,
expected_approval_plan_id: str | None = None,
) -> dict[str, Any]:
"""Read and validate the canonical source entry from the supplied main ref."""
try:
return source_registry_snapshot(
repo_git_dir,
version,
main_ref=main_ref,
source_registry_main_oid=source_registry_main_oid,
expected_tag_oid=expected_tag_oid,
expected_target_oid=expected_target_oid,
expected_evidence_closure_oid=expected_evidence_closure_oid,
expected_approval_plan_id=expected_approval_plan_id,
)["record"]
except RegistryConvergenceError as exc:
if str(exc).startswith("fatal:") or "path" in str(exc).lower():
raise RegistryConvergenceError(
f"source_registry_entry_missing:{source_registry_main_oid or main_ref}:versions/{version}.json"
) from exc
raise
def assert_source_registry_convergence(
registry_path: Path | str,
*,
repo_git_dir: Path | str,
main_ref: str = DEFAULT_MAIN_REF,
source_registry_main_oid: str | None = None,
) -> dict[str, Any]:
"""Require governed main to contain a source entry for the release identity.
The source entry is intentionally required to match only fields that are
knowable and governable before production deployment. Post-deployment
fields such as production_service/port/status timestamps are not required.
"""
registry_path = Path(registry_path)
repo_git_dir = Path(repo_git_dir)
verify_authoritative_repository(repo_git_dir)
record = _load_object(registry_path)
version = record.get("version")
tag_oid = record.get("tag_oid")
target_oid = record.get("target_oid")
try:
validate_source_registry_record(
record,
expected_version=version,
expected_tag_oid=tag_oid,
expected_target_oid=target_oid,
expected_evidence_closure_oid=record.get("evidence_closure_oid"),
expected_approval_plan_id=record.get("approval_plan_id"),
)
except RegistryConvergenceError as exc:
raise RegistryConvergenceError(f"registry_identity_invalid:{exc}") from exc
convergence = verify_source_registry_main_convergence(
repo_git_dir,
version,
main_ref=main_ref,
source_registry_main_oid=source_registry_main_oid,
expected_tag_oid=tag_oid,
expected_target_oid=target_oid,
expected_evidence_closure_oid=record["evidence_closure_oid"],
expected_approval_plan_id=record["approval_plan_id"],
)
return {
**convergence,
}
@@ -0,0 +1,46 @@
# 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;
}
@@ -0,0 +1,36 @@
[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,7 @@
# 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,6 +28,9 @@ 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,49 @@
[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,88 @@
# 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
@@ -0,0 +1,295 @@
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
@@ -0,0 +1,326 @@
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}
@@ -1,24 +0,0 @@
{
"schema": "aios.source-registry-reconciliation.v1",
"version": "V2R2C1",
"root_cause": "PROMOTION_STATE_MACHINE_FINALIZED_BEFORE_SOURCE_REGISTRY_CONVERGENCE",
"main_before": "3244f0f17f8845592ac94d585f5ebd65693c9f29",
"tag_oid": "07fc2746b688f5f243f29b39de39f32edc3dd9d2",
"target_oid": "998d30304787e31e5ae607c0bd86b5edb2b0e5ad",
"projection_sha256": "924dfc0ec58afc4e31638f6538e551c5bc8858f804cdbee5af7dce6eba7c90fe",
"evidence_closure_oid": "a8b906d1bc24088c19647e98de6a679268f6d0a2",
"approval_plan_id": "sha256:c0df52008ae7f64cec5ee2d28947aa7b892066c670ab8295e4549140b1a7500e",
"remote_tag_verified": true,
"remote_target_verified": true,
"gitea_api_verified_by_owner_acceptance": true,
"source_entry_before": "missing",
"source_entry_added": "versions/V2R2C1.json",
"source_entry_is_exact_attested_projection": true,
"release_manifest_added": false,
"release_manifest_reason": "Historical release-manifest schema differs; no canonical V2R2C1 source-manifest generator was proven. Do not synthesize one by copying old release values.",
"runtime_backing": "immutable WS/versions bundle",
"runtime_mutated": false,
"tag_rewritten": false,
"baseline_modified": false,
"production_modified": false
}
@@ -0,0 +1,20 @@
{
"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
@@ -0,0 +1,2 @@
{"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
@@ -0,0 +1,112 @@
#!/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
@@ -0,0 +1,193 @@
#!/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())
+1 -19
View File
@@ -32,36 +32,18 @@ def build_parser() -> argparse.ArgumentParser:
parser.add_argument("--tree-oid", required=True)
parser.add_argument("--output-directory", required=True, type=Path)
parser.add_argument("--name", default="aios-autonomous-release-candidate")
parser.add_argument("--release-version")
parser.add_argument("--release-binding-json", type=Path)
parser.add_argument("--artifact-mode", choices=("release", "general"))
return parser
def main(argv: list[str] | None = None) -> int:
args = build_parser().parse_args(argv)
release_binding = (
json.loads(args.release_binding_json.read_text(encoding="utf-8"))
if args.release_binding_json is not None
else None
)
if release_binding is not None and not isinstance(release_binding, dict):
raise ValueError("release binding JSON must be an object")
output = args.output_directory
output.mkdir(parents=True, exist_ok=True, mode=0o750)
if output.is_symlink():
raise RuntimeError("artifact output directory symlink rejected")
os.chmod(output, output.stat().st_mode & 0o750)
builder = ArtifactBuilder()
built = builder.build_from_directory(
args.source_oid,
args.tree_oid,
args.source,
release_version=args.release_version,
release_binding=release_binding,
release_mode=args.release_version is not None,
artifact_mode=args.artifact_mode,
)
built = builder.build_from_directory(args.source_oid, args.tree_oid, args.source)
builder.verify(built)
artifact_path = output / f"{args.name}.tar"
manifest_path = output / f"{args.name}.manifest.json"
+100
View File
@@ -0,0 +1,100 @@
#!/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())
-124
View File
@@ -1,124 +0,0 @@
#!/usr/bin/env python3
"""Generate one canonical source entry for a governed non-main change.
This utility only creates the file in the caller's worktree. It never commits,
pushes, changes main, rewrites a tag, or updates a production projection. The
caller must submit the resulting file through the existing governed writer and
approval flow.
"""
from __future__ import annotations
import argparse
import json
import os
import sys
from pathlib import Path
APP = Path(__file__).resolve().parents[1] / "active-runtime" / "app"
sys.path.insert(0, str(APP))
from version_registry import ( # noqa: E402
build_source_registry_entry,
configured_authoritative_git_dir,
configured_repository_identity,
source_registry_json,
validate_source_registry_record,
verify_approval_authority,
verify_authoritative_repository,
verify_source_registry_git_identity,
)
def parser() -> argparse.ArgumentParser:
value = argparse.ArgumentParser()
value.add_argument("--output", required=True, type=Path)
value.add_argument("--version", required=True)
value.add_argument("--tag-oid", required=True)
value.add_argument("--target-oid", required=True)
value.add_argument("--peeled-target-oid", required=True)
value.add_argument("--evidence-closure-oid", required=True)
value.add_argument("--approval-plan-id", required=True)
value.add_argument(
"--repository",
type=Path,
default=configured_authoritative_git_dir(),
help="authoritative bare repository; required for release generation",
)
value.add_argument(
"--approval-authority",
type=Path,
default=(
Path(os.environ["AIOS_GITOPS_APPROVAL_AUTHORITY"])
if os.environ.get("AIOS_GITOPS_APPROVAL_AUTHORITY")
else None
),
help="canonical approval/publication receipt for this release request",
)
value.add_argument(
"--repository-identity",
default=configured_repository_identity(),
)
return value
def main() -> int:
args = parser().parse_args()
if args.approval_authority is None:
raise ValueError("canonical approval authority is required")
verify_authoritative_repository(
args.repository,
repository_identity=args.repository_identity,
)
tag_oid = args.tag_oid.lower()
target_oid = args.target_oid.lower()
peeled_target_oid = args.peeled_target_oid.lower()
evidence_closure_oid = args.evidence_closure_oid.lower()
expected = build_source_registry_entry(
version=args.version,
tag_oid=tag_oid,
target_oid=target_oid,
peeled_target_oid=peeled_target_oid,
evidence_closure_oid=evidence_closure_oid,
approval_plan_id=args.approval_plan_id,
repository=args.repository_identity,
)
verify_source_registry_git_identity(args.repository, expected)
verify_approval_authority(args.approval_authority, expected)
destination = args.output.resolve()
if destination.name != f"{args.version}.json" or destination.parent.name != "versions":
raise ValueError("output must be versions/<version>.json")
payload = source_registry_json(expected)
destination.parent.mkdir(parents=True, exist_ok=True)
if destination.is_symlink():
raise ValueError("existing source registry path must not be a symlink")
if destination.exists() and not destination.is_file():
raise ValueError("existing source registry path must be a regular file")
if destination.exists():
current = json.loads(destination.read_text(encoding="utf-8"))
validate_source_registry_record(
current,
expected_version=args.version,
expected_tag_oid=tag_oid,
expected_target_oid=target_oid,
expected_evidence_closure_oid=evidence_closure_oid,
expected_approval_plan_id=args.approval_plan_id,
)
print(json.dumps({"status": "EXISTING_IDENTITY_VERIFIED", "path": str(destination)}))
return 0
descriptor = os.open(
destination,
os.O_WRONLY | os.O_CREAT | os.O_EXCL | getattr(os, "O_NOFOLLOW", 0),
0o640,
)
try:
os.write(descriptor, payload)
os.fsync(descriptor)
finally:
os.close(descriptor)
print(json.dumps({"status": "GENERATED", "path": str(destination)}))
return 0
if __name__ == "__main__":
raise SystemExit(main())
@@ -8,7 +8,6 @@ import io
import json
import os
from pathlib import Path, PurePosixPath
import re
import stat
import tarfile
from typing import Mapping
@@ -76,30 +75,6 @@ class BuiltArtifact:
manifest: ArtifactManifest
@dataclass(frozen=True, slots=True)
class ReleaseIdentityBinding:
"""Immutable release identity required for a release artifact build."""
version: str
repository: str
tag_oid: str
target_oid: str
evidence_closure_oid: str
approval_plan_id: str
peeled_target_oid: str | None = None
def as_dict(self) -> dict[str, str]:
return {
"version": self.version,
"repository": self.repository,
"tag_oid": self.tag_oid,
"target_oid": self.target_oid,
"peeled_target_oid": self.peeled_target_oid or self.target_oid,
"evidence_closure_oid": self.evidence_closure_oid,
"approval_plan_id": self.approval_plan_id,
}
class ArtifactBuilder:
MAX_FILE_BYTES = 8 * 1024 * 1024
MAX_ARTIFACT_BYTES = 64 * 1024 * 1024
@@ -117,188 +92,14 @@ class ArtifactBuilder:
if not is_git_oid(value):
raise ValueError(f"{name} must be a full lowercase Git OID")
@staticmethod
def _binding_dict(
binding: ReleaseIdentityBinding | Mapping[str, str],
) -> dict[str, str]:
if isinstance(binding, ReleaseIdentityBinding):
return binding.as_dict()
values = {str(key): str(value) for key, value in binding.items()}
values.setdefault("peeled_target_oid", values.get("target_oid", ""))
return values
@classmethod
def _validate_source_registry(
cls,
root: Path,
release_version: str,
release_binding: ReleaseIdentityBinding | Mapping[str, str] | None = None,
*,
release_mode: bool = False,
) -> dict[str, str]:
if not re.fullmatch(r"V\d+R\d+C\d+(?:-[A-Za-z0-9_.-]+)?", release_version):
raise ReleaseBlocked(
"ARTIFACT_SOURCE_REGISTRY_INVALID", "release version is not canonical"
)
path = root / "versions" / f"{release_version}.json"
if path.is_symlink() or not path.is_file():
raise ReleaseBlocked(
"ARTIFACT_SOURCE_REGISTRY_MISSING",
f"artifact source is missing versions/{release_version}.json",
)
try:
value = json.loads(path.read_text(encoding="utf-8"))
except (OSError, UnicodeDecodeError, json.JSONDecodeError) as exc:
raise ReleaseBlocked(
"ARTIFACT_SOURCE_REGISTRY_INVALID",
f"artifact source registry cannot be read: {path}",
) from exc
if not isinstance(value, dict):
raise ReleaseBlocked(
"ARTIFACT_SOURCE_REGISTRY_INVALID", "source registry is not an object"
)
if value.get("version") != release_version or value.get("repository") != "aios/aios":
raise ReleaseBlocked(
"ARTIFACT_SOURCE_REGISTRY_IDENTITY_MISMATCH",
"source registry version or repository does not match the artifact",
)
if value.get("schema") not in {
"aios.source-registry.v1",
"aios.production-registry.v2",
}:
raise ReleaseBlocked(
"ARTIFACT_SOURCE_REGISTRY_INVALID", "source registry schema is invalid"
)
for field in ("tag_oid", "target_oid", "peeled_target_oid", "evidence_closure_oid"):
item = value.get(field)
if not isinstance(item, str) or not re.fullmatch(r"[0-9a-f]{40}", item):
raise ReleaseBlocked(
"ARTIFACT_SOURCE_REGISTRY_INVALID",
f"source registry field is invalid: {field}",
)
if value["peeled_target_oid"] != value["target_oid"]:
raise ReleaseBlocked(
"ARTIFACT_SOURCE_REGISTRY_IDENTITY_MISMATCH",
"source registry peeled target differs from target",
)
if not isinstance(value.get("approval_plan_id"), str) or not re.fullmatch(
r"sha256:[0-9a-f]{64}", value["approval_plan_id"]
):
raise ReleaseBlocked(
"ARTIFACT_SOURCE_REGISTRY_INVALID", "source registry approval plan is invalid"
)
if release_mode and release_binding is None:
raise ReleaseBlocked(
"ARTIFACT_RELEASE_BINDING_REQUIRED",
"release artifact builds require an expected release identity",
)
if release_binding is not None:
expected = cls._binding_dict(release_binding)
required = (
"version",
"repository",
"tag_oid",
"target_oid",
"peeled_target_oid",
"evidence_closure_oid",
"approval_plan_id",
)
if any(not expected.get(name) for name in required):
raise ReleaseBlocked(
"ARTIFACT_RELEASE_BINDING_INVALID",
"expected release identity is incomplete",
)
for name in ("tag_oid", "target_oid", "peeled_target_oid", "evidence_closure_oid"):
if not re.fullmatch(r"[0-9a-f]{40}", expected[name]):
raise ReleaseBlocked(
"ARTIFACT_RELEASE_BINDING_INVALID",
f"expected release identity field is invalid: {name}",
)
if not re.fullmatch(r"sha256:[0-9a-f]{64}", expected["approval_plan_id"]):
raise ReleaseBlocked(
"ARTIFACT_RELEASE_BINDING_INVALID",
"expected release identity approval plan is invalid",
)
for name in required:
if value.get(name) != expected[name]:
raise ReleaseBlocked(
"ARTIFACT_SOURCE_REGISTRY_IDENTITY_MISMATCH",
f"source registry does not match expected release identity: {name}",
)
return {
"version": str(value["version"]),
"repository": str(value["repository"]),
"tag_oid": str(value["tag_oid"]),
"target_oid": str(value["target_oid"]),
"peeled_target_oid": str(value["peeled_target_oid"]),
"evidence_closure_oid": str(value["evidence_closure_oid"]),
"approval_plan_id": str(value["approval_plan_id"]),
}
def build_from_directory(
self,
source_oid: str,
tree_oid: str,
source: str | Path,
release_version: str | None = None,
release_binding: ReleaseIdentityBinding | Mapping[str, str] | None = None,
release_mode: bool | None = None,
artifact_mode: str | None = None,
self, source_oid: str, tree_oid: str, source: str | Path
) -> BuiltArtifact:
root = Path(source)
if root.is_symlink() or not root.is_dir():
raise ReleaseBlocked(
"ARTIFACT_SOURCE_LINK_REJECTED", "artifact source must be a real directory"
)
if artifact_mode not in {None, "release", "general"}:
raise ValueError("artifact_mode must be release, general, or omitted")
if artifact_mode == "release" and release_mode is False:
raise ValueError("artifact_mode=release conflicts with release_mode=False")
if artifact_mode == "general" and release_mode is True:
raise ValueError("artifact_mode=general conflicts with release_mode=True")
explicit_release_version = release_version is not None
detected_canonical_version = False
if release_version is None:
version_file = root / "VERSION"
if version_file.is_file() and not version_file.is_symlink():
release_version = version_file.read_text(encoding="utf-8").strip()
detected_canonical_version = bool(
re.fullmatch(r"V\d+R\d+C\d+(?:-[A-Za-z0-9_.-]+)?", release_version)
)
elif re.fullmatch(r"V\d+R\d+C\d+(?:-[A-Za-z0-9_.-]+)?", release_version):
detected_canonical_version = True
binding_expected: dict[str, str] | None = None
if release_binding is not None:
binding_expected = self._binding_dict(release_binding)
if not binding_expected.get("version"):
raise ReleaseBlocked(
"ARTIFACT_RELEASE_BINDING_INVALID",
"release identity binding must include version",
)
release_version = release_version or binding_expected["version"]
release_requested = bool(
explicit_release_version
or detected_canonical_version
or release_binding is not None
or release_mode is True
or artifact_mode == "release"
)
if artifact_mode == "general" and release_requested:
raise ReleaseBlocked(
"ARTIFACT_GENERAL_MODE_RELEASE_VERSION",
"general artifacts cannot contain canonical release metadata",
)
if release_requested:
release_mode = True
self._validate_source_registry(
root,
release_version or "",
release_binding,
release_mode=True,
)
files: list[ArtifactFile] = []
for directory, dirnames, filenames in os.walk(root, followlinks=False):
current = Path(directory)
@@ -61,9 +61,6 @@ def build_parser() -> argparse.ArgumentParser:
artifact_build.add_argument("--source-oid", required=True)
artifact_build.add_argument("--tree-oid", required=True)
artifact_build.add_argument("--output-directory", required=True, type=Path)
artifact_build.add_argument("--release-version")
artifact_build.add_argument("--release-binding-json", type=Path)
artifact_build.add_argument("--artifact-mode", choices=("release", "general"))
artifact_verify = artifact_sub.add_parser("verify")
artifact_verify.add_argument("--artifact", required=True, type=Path)
artifact_verify.add_argument("--manifest", required=True, type=Path)
@@ -148,20 +145,7 @@ def main(argv: list[str] | None = None) -> int:
if args.command == "artifact":
builder = ArtifactBuilder()
if args.artifact_command == "build":
release_binding = (
load_json(args.release_binding_json)
if args.release_binding_json is not None
else None
)
built = builder.build_from_directory(
args.source_oid,
args.tree_oid,
args.source,
release_version=args.release_version,
release_binding=release_binding,
release_mode=args.release_version is not None,
artifact_mode=args.artifact_mode,
)
built = builder.build_from_directory(args.source_oid, args.tree_oid, args.source)
builder.verify(built)
artifact_path = args.output_directory / "candidate.tar"
manifest_path = args.output_directory / "manifest.json"
@@ -1,35 +1,11 @@
import json
from pathlib import Path
import pytest
from aios_autonomous_release_candidate.artifact import (
ArtifactBuilder,
ArtifactFile,
BuiltArtifact,
ReleaseIdentityBinding,
)
from aios_autonomous_release_candidate.artifact import ArtifactBuilder, ArtifactFile, BuiltArtifact
from aios_autonomous_release_candidate.errors import EvidenceIncomplete, ReleaseBlocked
def _source_registry(root: Path) -> None:
target = "2" * 40
value = {
"schema": "aios.source-registry.v1",
"version": "V2R2C1",
"repository": "aios/aios",
"tag_oid": "1" * 40,
"tag_object": "1" * 40,
"target_oid": target,
"peeled_target_oid": target,
"evidence_closure_oid": "3" * 40,
"approval_plan_id": "sha256:" + "4" * 64,
}
path = root / "versions" / "V2R2C1.json"
path.parent.mkdir(parents=True)
path.write_text(json.dumps(value) + "\n", encoding="utf-8")
def test_artifact_is_deterministic_and_reverified() -> None:
builder = ArtifactBuilder()
files = (
@@ -56,143 +32,6 @@ def test_source_tree_rejects_symlink_and_hardlink(tmp_path: Path) -> None:
ArtifactBuilder().build_from_directory("a" * 40, "b" * 40, root)
def test_release_source_tree_requires_source_registry_entry(tmp_path: Path) -> None:
root = tmp_path / "source"
root.mkdir()
(root / "VERSION").write_text("V2R2C1\n", encoding="utf-8")
with pytest.raises(ReleaseBlocked, match="ARTIFACT_SOURCE_REGISTRY_MISSING"):
ArtifactBuilder().build_from_directory("a" * 40, "b" * 40, root)
def test_canonical_version_without_binding_fails_closed(tmp_path: Path) -> None:
root = tmp_path / "source"
root.mkdir()
(root / "VERSION").write_text("V2R2C1\n", encoding="utf-8")
_source_registry(root)
with pytest.raises(ReleaseBlocked, match="ARTIFACT_RELEASE_BINDING_REQUIRED"):
ArtifactBuilder().build_from_directory("a" * 40, "b" * 40, root)
def test_release_source_tree_requires_expected_release_identity(tmp_path: Path) -> None:
root = tmp_path / "source"
root.mkdir()
(root / "VERSION").write_text("V2R2C1\n", encoding="utf-8")
_source_registry(root)
binding = ReleaseIdentityBinding(
version="V2R2C1",
repository="aios/aios",
tag_oid="1" * 40,
target_oid="2" * 40,
evidence_closure_oid="3" * 40,
approval_plan_id="sha256:" + "4" * 64,
)
built = ArtifactBuilder().build_from_directory(
"a" * 40,
"b" * 40,
root,
release_binding=binding,
)
ArtifactBuilder().verify(built)
def test_canonical_version_with_malformed_binding_fails_closed(tmp_path: Path) -> None:
root = tmp_path / "source"
root.mkdir()
(root / "VERSION").write_text("V2R2C1\n", encoding="utf-8")
_source_registry(root)
with pytest.raises(ReleaseBlocked, match="ARTIFACT_RELEASE_BINDING_INVALID"):
ArtifactBuilder().build_from_directory(
"a" * 40,
"b" * 40,
root,
release_binding={
"version": "V2R2C1",
"repository": "aios/aios",
"tag_oid": "not-an-oid",
},
)
def test_explicit_general_artifact_without_release_version_passes(tmp_path: Path) -> None:
root = tmp_path / "source"
root.mkdir()
(root / "README.txt").write_text("general candidate\n", encoding="utf-8")
built = ArtifactBuilder().build_from_directory(
"a" * 40,
"b" * 40,
root,
artifact_mode="general",
)
assert "README.txt" in built.manifest.files
def test_explicit_general_mode_rejects_canonical_version(tmp_path: Path) -> None:
root = tmp_path / "source"
root.mkdir()
(root / "VERSION").write_text("V2R2C1\n", encoding="utf-8")
_source_registry(root)
with pytest.raises(ReleaseBlocked, match="ARTIFACT_GENERAL_MODE_RELEASE_VERSION"):
ArtifactBuilder().build_from_directory(
"a" * 40,
"b" * 40,
root,
artifact_mode="general",
)
@pytest.mark.parametrize(
"field,wrong",
[
("repository", "other/repository"),
("version", "V2R2C2"),
("tag_oid", "9" * 40),
("target_oid", "8" * 40),
("peeled_target_oid", "7" * 40),
("evidence_closure_oid", "6" * 40),
("approval_plan_id", "sha256:" + "5" * 64),
],
)
def test_release_source_tree_rejects_every_identity_mismatch(
tmp_path: Path, field: str, wrong: str
) -> None:
root = tmp_path / "source"
root.mkdir()
(root / "VERSION").write_text("V2R2C1\n", encoding="utf-8")
_source_registry(root)
binding = {
"version": "V2R2C1",
"repository": "aios/aios",
"tag_oid": "1" * 40,
"target_oid": "2" * 40,
"peeled_target_oid": "2" * 40,
"evidence_closure_oid": "3" * 40,
"approval_plan_id": "sha256:" + "4" * 64,
}
binding[field] = wrong
with pytest.raises(ReleaseBlocked, match="ARTIFACT_SOURCE_REGISTRY"):
ArtifactBuilder().build_from_directory(
"a" * 40,
"b" * 40,
root,
release_binding=binding,
)
def test_release_mode_without_binding_fails_closed(tmp_path: Path) -> None:
root = tmp_path / "source"
root.mkdir()
(root / "VERSION").write_text("V2R2C1\n", encoding="utf-8")
_source_registry(root)
with pytest.raises(ReleaseBlocked, match="ARTIFACT_RELEASE_BINDING_REQUIRED"):
ArtifactBuilder().build_from_directory(
"a" * 40,
"b" * 40,
root,
release_version="V2R2C1",
release_mode=True,
)
def test_artifact_rejects_secret_markers() -> None:
with pytest.raises(ReleaseBlocked, match="ARTIFACT_SECRET_MARKER"):
ArtifactBuilder().build(
@@ -0,0 +1,48 @@
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
@@ -0,0 +1,317 @@
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"]
+1 -1
View File
@@ -158,7 +158,7 @@ def test_execute_orders_registry_before_baseline_and_baseline_last(
)
monkeypatch.setattr(
"approved_release_executor.verify_artifact",
lambda binding, **kwargs: {"verified": True},
lambda binding: {"verified": True},
)
calls = []
-1
View File
@@ -73,7 +73,6 @@ class AuthoritativeGitContextTests(unittest.TestCase):
json.dumps({"branch": "patch/context"}), encoding="utf-8"
)
self.env = {
"AIOS_TEST_MODE": "1",
"AIOS_GITOPS_AUTHORITATIVE_GIT_DIR": str(self.bare),
"AIOS_GITOPS_REPOSITORY_IDENTITY": "aios/aios",
}
@@ -76,7 +76,6 @@ 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/branch/publish",
"/v2/job", "/v2/job/resume", "/v2/job/reconcile",
"/v2/tag/approval-request",
"/v2/tag/publish",
+192
View File
@@ -0,0 +1,192 @@
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()
@@ -0,0 +1,191 @@
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 == []
-166
View File
@@ -1,166 +0,0 @@
from __future__ import annotations
import json
from pathlib import Path
import tempfile
from unittest.mock import patch
import pytest
from aios_existing_branch_publisher import ExistingBranchPublicationState, ExistingBranchPublisher
from gitea_tag_publication import GiteaConfigurationError, GiteaTagClient, InvalidReleaseVersion, PublicationRequest, TagPublisher, validate_version
from signer_contract_compatibility import run_compatible_signer
class Authority:
def __init__(self, oid: str):
self.oid = oid
def read_ref(self, ref: str) -> str | None:
return self.oid if ref.startswith("refs/heads/") else None
class Remote:
def __init__(self, oid: str | None = None, api_oid: str | None = None, visible: bool = True):
self.oid = oid
self.api_oid = api_oid
self.visible = visible
self.pushes: list[tuple[str, str | None, str]] = []
def repository_identity(self) -> str:
return "aios/aios"
def read_git_ref(self, branch: str) -> str | None:
return self.oid
def read_api_branch(self, branch: str) -> str | None:
return self.api_oid
def commit_visible(self, oid: str) -> bool:
return self.visible
def push_exact(self, branch: str, expected: str | None, new: str) -> None:
assert expected is None
self.pushes.append((branch, expected, new))
self.oid = new
self.api_oid = new
self.visible = True
def request(oid: str, key: str = "gate-closure-test-0001") -> dict[str, str]:
return {
"repository": "aios/aios",
"branch": "agent/gate-closure-test",
"expected_branch_oid": oid,
"owner_approval_reference": "owner-authorization:test",
"idempotency_key": key,
}
def test_existing_branch_publisher_creates_exact_missing_remote_and_replays_idempotently(tmp_path: Path):
oid = "a" * 40
remote = Remote()
publisher = ExistingBranchPublisher(Authority(oid), remote, tmp_path / "state")
first = publisher.publish(request(oid))
assert first["publication_state"] == ExistingBranchPublicationState.COMPLETED.value
assert remote.pushes == [("agent/gate-closure-test", None, oid)]
second = publisher.publish(request(oid))
assert second["publication_state"] == ExistingBranchPublicationState.COMPLETED.value
assert second["idempotent"] is True
def test_existing_branch_publisher_conflict_is_fail_closed(tmp_path: Path):
oid = "a" * 40
remote = Remote(oid="b" * 40, api_oid="b" * 40)
result = ExistingBranchPublisher(Authority(oid), remote, tmp_path / "state").publish(request(oid, "gate-closure-test-0002"))
assert result["publication_state"] == ExistingBranchPublicationState.MANUAL_OWNER_REVIEW_REQUIRED.value
assert remote.pushes == []
def test_existing_branch_publisher_rejects_main_and_wrong_authority_oid(tmp_path: Path):
oid = "a" * 40
with pytest.raises(Exception, match="BRANCH_NAMESPACE_REJECTED"):
ExistingBranchPublisher(Authority(oid), Remote(), tmp_path / "state").publish({**request(oid), "branch": "main", "idempotency_key": "gate-closure-test-0003"})
with pytest.raises(Exception, match="AUTHORITY_BRANCH_OID_MISMATCH"):
ExistingBranchPublisher(Authority("b" * 40), Remote(), tmp_path / "state").publish(request(oid, "gate-closure-test-0004"))
def test_signer_broker_path_has_no_direct_root_signer_call():
class FakeBroker:
def self_test(self):
return None
def sign(self, payload_kind, payload):
return b"signature", "test-broker-key"
request_doc = {
"schema": "aios.task-scoped-writer-grant-request.v1",
"task_id": "gate-closure-signer-unit",
"repository": "aios/aios",
"base": {"ref": "refs/heads/main", "oid": "a" * 40},
"target": {"branch": "agent/gate-closure-signer-unit"},
"owner_authorization": {"identity": "aiman", "reference": "owner-authorization:test"},
"authorization": {
"production_change_authorized": False,
"tag_change_authorized": False,
"baseline_change_authorized": False,
"main_write_authorized": False,
},
"allowed_operations": ["create_worktree"],
"denied_operations": ["direct_main_push", "force_push"],
"one_time_use": True,
"not_before": "2026-08-10T00:00:00Z",
"expires_at": "2026-08-10T00:00:00Z",
"nonce": "unit-nonce",
}
with tempfile.TemporaryDirectory() as td:
root = Path(td)
grant = root / "grant"
grant.mkdir()
(grant / "grant-request.json").write_text(json.dumps(request_doc), encoding="utf-8")
(grant / "approval-plan.json").write_text(json.dumps({"approval_plan_id": "sha256:" + "a" * 64}), encoding="utf-8")
with patch("signer_contract_compatibility._supports_deployed_signer", return_value=True):
result = run_compatible_signer(["unused-signer-command"], grant, root / "result.json", broker=FakeBroker())
assert result["signing_key_identity"] == "test-broker-key"
def test_gitea_configuration_has_explicit_error_codes():
with pytest.raises(GiteaConfigurationError, match="GITEA_API_URL_MISSING"):
GiteaTagClient.from_authoritative_environment({})
with pytest.raises(GiteaConfigurationError, match="GITEA_REPOSITORY_MISSING"):
GiteaTagClient.from_authoritative_environment({"AIOS_GITOPS_GITEA_API_URL": "https://gitea.invalid/api/v1"})
with pytest.raises(GiteaConfigurationError, match="GITEA_TOKEN_REFERENCE_MISSING"):
GiteaTagClient.from_authoritative_environment({"AIOS_GITOPS_GITEA_API_URL": "https://gitea.invalid/api/v1", "AIOS_GITOPS_GITEA_REPOSITORY": "aios/aios"})
assert validate_version("V2R2C1") == "V2R2C1"
with pytest.raises(InvalidReleaseVersion, match="invalid release version"):
validate_version("not-a-release")
def test_release_consistency_reports_tag_target_mismatch(tmp_path: Path):
target = "a" * 40
evidence = "b" * 40
tag_oid = "c" * 40
mismatched_target = "d" * 40
request = PublicationRequest("V2R2C1", target, evidence, "approval-plan:test")
(tmp_path / "baseline").mkdir()
(tmp_path / "versions").mkdir()
common = {
"version": request.version,
"target_oid": request.target_oid,
"evidence_closure_oid": request.evidence_closure_oid,
"approval_plan_id": request.approval_plan_id,
"tag_oid": tag_oid,
}
(tmp_path / "baseline/current.json").write_text(
json.dumps({**common, "current_baseline": request.version}), encoding="utf-8"
)
(tmp_path / "versions/V2R2C1.json").write_text(
json.dumps({**common, "registered": True, "enabled": True}), encoding="utf-8"
)
verified = {
**common,
"remote_tag_oid": tag_oid,
"remote_peeled_target_oid": mismatched_target,
"gitea_tag_oid": tag_oid,
"gitea_peeled_target_oid": target,
"gitea_listing_visible": True,
"repository_identity": "aios/aios",
}
result = TagPublisher(tmp_path, object(), object(), "gitea").get_release_consistency(
request, verified=verified
)
assert result["status"] == "FAIL"
assert result["error"] == "TAG_TARGET_MISMATCH"
-120
View File
@@ -1,120 +0,0 @@
from __future__ import annotations
import io
import json
import shutil
import subprocess
import tarfile
from pathlib import Path
import sys
import pytest
APP = Path(__file__).resolve().parents[1] / "active-runtime" / "app"
sys.path.insert(0, str(APP))
import governed_production_release as release # noqa: E402
from version_registry import build_source_registry_entry, source_registry_json # noqa: E402
def _git(work: Path, *args: str) -> str:
return subprocess.run(
["git", "-C", str(work), *args],
capture_output=True,
text=True,
check=True,
).stdout.strip()
def _commit(work: Path, message: str) -> str:
subprocess.run(["git", "-C", str(work), "add", "-A"], check=True)
subprocess.run(["git", "-C", str(work), "commit", "-qm", message], check=True)
return _git(work, "rev-parse", "HEAD")
def test_artifact_uses_pinned_source_registry_commit_when_main_advances(
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
if shutil.which("zstd") is None:
pytest.skip("zstd is required for governed artifact integration")
work = tmp_path / "work"
bare = tmp_path / "repo.git"
subprocess.run(["git", "init", "-q", "-b", "main", str(work)], check=True)
_git(work, "config", "user.name", "test")
_git(work, "config", "user.email", "test@example.invalid")
(work / "active-runtime/app").mkdir(parents=True)
(work / "active-runtime/app/deployment_state.py").write_text("state = 'candidate'\n")
(work / "active-runtime/app/v2r1c6_privacy_tools.py").write_text("def main(): return 0\n")
target_oid = _commit(work, "release target")
subprocess.run(
["git", "-C", str(work), "tag", "-a", "V2R2C1", target_oid, "-m", "release"],
check=True,
)
tag_oid = _git(work, "rev-parse", "refs/tags/V2R2C1")
(work / "evidence.txt").write_text("evidence\n")
evidence_oid = _commit(work, "evidence closure")
record = build_source_registry_entry(
version="V2R2C1",
tag_oid=tag_oid,
target_oid=target_oid,
peeled_target_oid=target_oid,
evidence_closure_oid=evidence_oid,
approval_plan_id="sha256:" + "4" * 64,
)
source_path = work / "versions/V2R2C1.json"
source_path.parent.mkdir(parents=True)
source_path.write_bytes(source_registry_json(record))
main_oid = _commit(work, "source registry convergence")
subprocess.run(["git", "clone", "-q", "--bare", str(work), str(bare)], check=True)
subprocess.run(["git", "-C", str(work), "remote", "add", "authority", str(bare)], check=True)
monkeypatch.setenv("AIOS_TEST_MODE", "1")
monkeypatch.setenv("AIOS_GITOPS_AUTHORITATIVE_GIT_DIR", str(bare))
monkeypatch.setattr(release, "TAG_OID", tag_oid)
monkeypatch.setattr(release, "TARGET_OID", target_oid)
monkeypatch.setattr(release, "EVIDENCE_CLOSURE_OID", evidence_oid)
monkeypatch.setattr(release, "APPROVAL_PLAN_ID", record["approval_plan_id"])
binding = release.ReleaseBinding(
tag_oid=tag_oid,
target_oid=target_oid,
evidence_closure_oid=evidence_oid,
approval_plan_id=record["approval_plan_id"],
)
original_payload = release._payload_file
advanced = False
def advance_after_first_payload(git_dir: Path, oid: str, path: str) -> bytes:
nonlocal advanced
result = original_payload(git_dir, oid, path)
if not advanced and path == "active-runtime/app/deployment_state.py":
updated = dict(record)
updated["review_marker"] = "main-advanced-after-validation"
source_path.write_bytes(source_registry_json(updated))
new_main = _commit(work, "advance main after source validation")
subprocess.run(["git", "-C", str(work), "push", "-q", "authority", "main"], check=True)
assert new_main != main_oid
advanced = True
return result
monkeypatch.setattr(release, "_payload_file", advance_after_first_payload)
receipt = release.build_production_artifact(bare, binding, tmp_path / "artifacts")
assert advanced is True
assert receipt["release_target_oid"] == target_oid
assert receipt["source_registry_main_oid"] == main_oid
assert receipt["source_registry_path"] == "versions/V2R2C1.json"
expected_bytes = subprocess.run(
["git", f"--git-dir={bare}", "show", f"{main_oid}:versions/V2R2C1.json"],
capture_output=True,
check=True,
).stdout
assert receipt["source_registry_sha256"] == __import__("hashlib").sha256(expected_bytes).hexdigest()
compressed = Path(receipt["artifact"]).read_bytes()
raw_tar = subprocess.run(["zstd", "-dc", "-"], input=compressed, capture_output=True, check=True).stdout
with tarfile.open(fileobj=io.BytesIO(raw_tar), mode="r:") as archive:
packaged = archive.extractfile("payload/versions/V2R2C1.json").read()
metadata = json.loads(archive.extractfile("payload/BUILD-METADATA.json").read())
assert packaged == expected_bytes
assert metadata["source_registry_main_oid"] == main_oid
assert metadata["source_registry_sha256"] == receipt["source_registry_sha256"]
-12
View File
@@ -25,18 +25,6 @@ def test_model_api_staging_unit_is_non_production_and_read_only():
assert 'production' not in unit.lower().replace('model-api-v2r2c1-staging', '')
def test_model_api_source_convergence_never_falls_back_to_generic_git_dir():
source = (ROOT / 'active-runtime' / 'app' / 'aios_model_api_staging.py').read_text(encoding='utf-8')
assert "repo_git_dir=AUTHORITATIVE_GIT_DIR or GIT_DIR" not in source
assert "repo_git_dir=AUTHORITATIVE_GIT_DIR" in source
def test_production_adapter_never_claims_source_git_convergence():
source = (ROOT / 'active-runtime' / 'app' / 'v2r2c1_production_adapter.py').read_text(encoding='utf-8')
assert '"source_registry_converged": True' not in source
assert '"source_registry_converged": "not_checked"' in source
def test_model_api_builder_manifest_contract():
builder = (ROOT / 'scripts' / 'build_v2r2c1_model_api_staging.py').read_text(encoding='utf-8')
assert 'aios.v2r2c1.model-api-staging-manifest.v1' in builder
-2
View File
@@ -47,8 +47,6 @@ def old_baseline(path: Path) -> str:
def test_baseline_record_uses_real_previous_production_baseline(tmp_path, monkeypatch):
# This test isolates previous-baseline semantics; convergence is tested separately.
monkeypatch.setattr(adapter, "assert_source_registry_convergence", lambda *a, **kw: None)
baseline = tmp_path / "baseline.json"
previous_sha = old_baseline(baseline)
registry_root = tmp_path / "versions"
-120
View File
@@ -1,120 +0,0 @@
from __future__ import annotations
import base64
from concurrent.futures import ThreadPoolExecutor
import hashlib
import json
from pathlib import Path
import threading
from aios_signer_broker_service import (
REQUEST_SCHEMA,
RESPONSE_SCHEMA,
SignerBrokerPolicy,
SignerBrokerService,
SignerBrokerServiceError,
)
class FakeRunner:
def __init__(self, failure: str | None = None):
self.failure = failure
self.calls: list[tuple[str, str]] = []
self.lock = threading.Lock()
def self_test(self) -> None:
with self.lock:
self.calls.append(("self_test", ""))
if self.failure:
raise SignerBrokerServiceError(self.failure)
def sign(self, payload_kind: str, payload: bytes) -> tuple[bytes, str]:
with self.lock:
self.calls.append(("sign", payload_kind))
if self.failure:
raise SignerBrokerServiceError(self.failure)
return b"signature:" + payload, "test-broker-key"
def make_policy(tmp_path: Path, *, allowed_uids: frozenset[int] = frozenset({1000})) -> SignerBrokerPolicy:
return SignerBrokerPolicy(
socket_path=tmp_path / "signer.sock",
signer_path=Path("/usr/local/libexec/aios-grant-signer-adapter"),
audit_path=tmp_path / "audit.jsonl",
state_root=tmp_path / "state",
allowed_uids=allowed_uids,
)
def sign_request(payload: bytes = b"canonical payload") -> dict[str, str]:
return {
"schema": REQUEST_SCHEMA,
"operation": "sign",
"payload_kind": "approval_plan",
"payload_sha256": hashlib.sha256(payload).hexdigest(),
"payload_base64": base64.b64encode(payload).decode("ascii"),
}
def read_audit(path: Path) -> list[dict[str, object]]:
return [json.loads(line) for line in path.read_text(encoding="utf-8").splitlines()]
def test_authorized_request_and_self_test_emit_metadata_only_audit(tmp_path: Path):
service = SignerBrokerService(make_policy(tmp_path), FakeRunner())
self_test = service.handle_request({"schema": REQUEST_SCHEMA, "operation": "self_test"}, 1000)
signed = service.handle_request(sign_request(b"secret-shaped canonical input"), 1000)
assert self_test == {"schema": RESPONSE_SCHEMA, "status": "PASS", "operation": "self_test"}
assert signed["status"] == "PASS"
assert base64.b64decode(signed["signature_base64"]) == b"signature:secret-shaped canonical input"
assert signed["key_id"] == "test-broker-key"
audit_text = (tmp_path / "audit.jsonl").read_text(encoding="utf-8")
assert "secret-shaped canonical input" not in audit_text
assert "payload_base64" not in audit_text
allowed = {"schema", "timestamp", "peer_uid", "operation", "status", "error_code", "payload_sha256"}
assert all(set(event) <= allowed for event in read_audit(tmp_path / "audit.jsonl"))
def test_unauthorized_caller_is_rejected_before_signer_use(tmp_path: Path):
runner = FakeRunner()
service = SignerBrokerService(make_policy(tmp_path), runner)
result = service.handle_request(sign_request(), 1001)
assert result["status"] == "FAIL"
assert result["error_code"] == "CALLER_UNAUTHORIZED"
assert runner.calls == []
def test_arbitrary_command_and_invalid_operation_are_rejected(tmp_path: Path):
runner = FakeRunner()
service = SignerBrokerService(make_policy(tmp_path), runner)
injected = {"schema": REQUEST_SCHEMA, "operation": "sign", "command": ["/bin/sh", "-c", "id"]}
invalid = {"schema": REQUEST_SCHEMA, "operation": "run", "command": ["id"]}
assert service.handle_request(injected, 1000)["error_code"] == "REQUEST_FIELDS_REJECTED"
assert service.handle_request(invalid, 1000)["error_code"] == "SIGNER_OPERATION_REJECTED"
assert runner.calls == []
def test_invalid_payload_and_hash_fail_closed(tmp_path: Path):
service = SignerBrokerService(make_policy(tmp_path), FakeRunner())
invalid = sign_request()
invalid["payload_base64"] = "not-base64"
assert service.handle_request(invalid, 1000)["error_code"] == "SIGNER_PAYLOAD_INVALID"
mismatch = sign_request()
mismatch["payload_sha256"] = "0" * 64
assert service.handle_request(mismatch, 1000)["error_code"] == "SIGNER_PAYLOAD_HASH_MISMATCH"
def test_signer_unavailable_and_timeout_are_stable_failures(tmp_path: Path):
for code in ("SIGNER_UNAVAILABLE", "SIGNER_TIMEOUT"):
result = SignerBrokerService(make_policy(tmp_path / code), FakeRunner(code)).handle_request(
{"schema": REQUEST_SCHEMA, "operation": "self_test"}, 1000
)
assert result == {"schema": RESPONSE_SCHEMA, "status": "FAIL", "error_code": code}
def test_concurrent_calls_are_safe_and_audited(tmp_path: Path):
service = SignerBrokerService(make_policy(tmp_path), FakeRunner())
with ThreadPoolExecutor(max_workers=8) as pool:
results = list(pool.map(lambda index: service.handle_request(sign_request(f"payload-{index}".encode()), 1000), range(24)))
assert all(result["status"] == "PASS" for result in results)
assert len(read_audit(tmp_path / "audit.jsonl")) == 24
+3 -8
View File
@@ -20,7 +20,6 @@ from signer_contract_compatibility import ( # noqa: E402
canonical_json_bytes,
deployed_signer_help,
run_compatible_signer,
run_signer_self_test,
resolve_ref_oid,
run_compatible_validator,
sha256_json,
@@ -114,13 +113,9 @@ class SignerContractCompatibilityTests(unittest.TestCase):
help_result = subprocess.run([signer, "sign", "--help"], capture_output=True, text=True, check=False)
self.assertEqual(help_result.returncode, 0)
self.assertTrue(deployed_signer_help(help_result.stdout, help_result.stderr))
try:
broker_result = run_signer_self_test()
except SignerContractError as exc:
if str(exc) == "SIGNER_BROKER_UNAVAILABLE":
self.skipTest("signer broker is not deployed in this engineering phase")
raise
self.assertEqual(broker_result["status"], "PASS")
self_test = subprocess.run([signer, "self-test"], capture_output=True, text=True, check=False)
self.assertEqual(self_test.returncode, 0)
self.assertIn("SIGNER_SELF_TEST=PASS", self_test.stdout)
def test_resolve_ref_oid_uses_full_synthetic_commit_and_fails_closed(self) -> None:
with tempfile.TemporaryDirectory() as temporary:
-392
View File
@@ -1,392 +0,0 @@
from __future__ import annotations
import hashlib
import io
import json
from pathlib import Path
import shutil
import subprocess
import tarfile
import pytest
from approved_release_executor import (
Binding,
ManualOwnerReviewRequired,
ReleaseBlocked,
ReconciliationRequired,
deploy_step,
execute,
tag_step,
verify_artifact,
verify_authorization,
)
def _binding(tmp_path: Path, **overrides) -> Binding:
artifact = tmp_path / "artifact.tar.zst"
artifact.write_bytes(b"x")
values = {
"version": "VTEST",
"target_oid": "a" * 40,
"evidence_closure_oid": "b" * 40,
"artifact_sha256": "c" * 64,
"approval_plan_id": "sha256:" + "d" * 64,
"artifact_path": str(artifact),
"artifact_size": 1,
"production_service": "test.service",
"production_port": 18927,
}
values.update(overrides)
return Binding(**values)
def _authorization(binding: Binding) -> dict[str, object]:
result: dict[str, object] = {
"accepted": True,
"binding_replaced": False,
"version": binding.version,
"target_oid": binding.target_oid,
"evidence_closure_oid": binding.evidence_closure_oid,
"artifact_sha256": binding.artifact_sha256,
"approval_plan_id": binding.approval_plan_id,
}
if binding.tag_oid is not None:
result.update(
{
"binding_schema": binding.binding_schema or "aios.release-binding.v2",
"tag_oid": binding.tag_oid,
}
)
if binding.source_registry_main_oid is not None:
result.update(
{
"source_registry_main_oid": binding.source_registry_main_oid,
"source_registry_path": binding.source_registry_path
or f"versions/{binding.version}.json",
"source_registry_sha256": binding.source_registry_sha256,
}
)
return result
def test_legacy_v2r2c1_style_authorization_remains_compatible(tmp_path: Path) -> None:
binding = _binding(tmp_path)
assert verify_authorization(_authorization(binding), binding)["accepted"] is True
def test_future_binding_requires_explicit_tag_oid(tmp_path: Path) -> None:
binding = _binding(
tmp_path,
version="V2R2C2",
tag_oid="1" * 40,
binding_schema="aios.release-binding.v2",
)
assert verify_authorization(_authorization(binding), binding)["tag_oid"] == "1" * 40
wrong = _authorization(binding)
wrong["tag_oid"] = "2" * 40
with pytest.raises(ManualOwnerReviewRequired, match="tag_oid"):
verify_authorization(wrong, binding)
def test_future_same_name_different_tag_object_requires_manual_review(monkeypatch, tmp_path: Path) -> None:
binding = _binding(
tmp_path,
version="V2R2C2",
tag_oid="1" * 40,
binding_schema="aios.release-binding.v2",
)
def fake(_command, _request):
return {
"status": "APPLIED_VERIFIED",
"version": binding.version,
"target_oid": binding.target_oid,
"artifact_sha256": binding.artifact_sha256,
"tag_oid": "2" * 40,
"peeled_target_oid": binding.target_oid,
}
monkeypatch.setattr("approved_release_executor.run_adapter", fake)
with pytest.raises(ManualOwnerReviewRequired, match="same-name tag object"):
tag_step(["/tag"], binding)
def test_executor_rechecks_source_registry_digest_and_build_provenance(
monkeypatch, tmp_path: Path
) -> None:
if shutil.which("zstd") is None:
pytest.skip("zstd is required for archive verification")
source = {
"schema": "aios.source-registry.v1",
"version": "V2R2C2",
"repository": "aios/aios",
"tag": "V2R2C2",
"tag_oid": "1" * 40,
"tag_object": "1" * 40,
"target_oid": "2" * 40,
"peeled_target_oid": "2" * 40,
"evidence_closure_oid": "3" * 40,
"approval_plan_id": "sha256:" + "4" * 64,
}
source_bytes = (json.dumps(source, ensure_ascii=False, sort_keys=True, indent=2) + "\n").encode()
source_digest = hashlib.sha256(source_bytes).hexdigest()
repo_work = tmp_path / "authority-work"
repo_bare = tmp_path / "authority.git"
(repo_work / "versions").mkdir(parents=True)
(repo_work / "versions" / "V2R2C2.json").write_bytes(source_bytes)
subprocess.run(["git", "init", "-q", str(repo_work)], check=True)
subprocess.run(["git", "-C", str(repo_work), "branch", "-M", "main"], check=True)
subprocess.run(["git", "-C", str(repo_work), "config", "user.name", "test"], check=True)
subprocess.run(
["git", "-C", str(repo_work), "config", "user.email", "test@example.invalid"],
check=True,
)
subprocess.run(["git", "-C", str(repo_work), "add", "versions"], check=True)
subprocess.run(["git", "-C", str(repo_work), "commit", "-qm", "source"], check=True)
source_main_oid = subprocess.run(
["git", "-C", str(repo_work), "rev-parse", "HEAD"],
capture_output=True,
text=True,
check=True,
).stdout.strip()
subprocess.run(["git", "clone", "-q", "--bare", str(repo_work), str(repo_bare)], check=True)
subprocess.run(
["git", f"--git-dir={repo_bare}", "config", "remote.gitea.url", "https://g.cnnetmkt.top/aios/aios.git"],
check=True,
)
monkeypatch.setenv("AIOS_TEST_MODE", "1")
monkeypatch.setenv("AIOS_GITOPS_AUTHORITATIVE_GIT_DIR", str(repo_bare))
metadata = {
"release_target_oid": "2" * 40,
"source_registry_main_oid": source_main_oid,
"source_registry_path": "versions/V2R2C2.json",
"source_registry_sha256": source_digest,
}
tar_path = tmp_path / "release.tar"
with tarfile.open(tar_path, "w") as archive:
for name, content in (
("payload/versions/V2R2C2.json", source_bytes),
("payload/BUILD-METADATA.json", (json.dumps(metadata, sort_keys=True) + "\n").encode()),
):
info = tarfile.TarInfo(name)
info.size = len(content)
archive.addfile(info, io.BytesIO(content))
artifact = tmp_path / "release.tar.zst"
subprocess.run(["zstd", "-q", "-19", "-f", str(tar_path), "-o", str(artifact)], check=True)
binding = _binding(
tmp_path,
version="V2R2C2",
tag_oid="1" * 40,
binding_schema="aios.release-binding.v2",
target_oid="2" * 40,
evidence_closure_oid="3" * 40,
approval_plan_id="sha256:" + "4" * 64,
source_registry_main_oid=source_main_oid,
source_registry_path="versions/V2R2C2.json",
source_registry_sha256=source_digest,
artifact_path=str(artifact),
artifact_size=artifact.stat().st_size,
artifact_sha256=hashlib.sha256(artifact.read_bytes()).hexdigest(),
)
result = verify_artifact(binding)
assert result["source_registry_sha256"] == source_digest
assert result["build_metadata_provenance_verified"] is True
bad_metadata = dict(metadata)
bad_metadata["source_registry_sha256"] = "0" * 64
bad_tar = tmp_path / "bad-metadata.tar"
with tarfile.open(bad_tar, "w") as archive:
for name, content in (
("payload/versions/V2R2C2.json", source_bytes),
("payload/BUILD-METADATA.json", (json.dumps(bad_metadata, sort_keys=True) + "\n").encode()),
):
info = tarfile.TarInfo(name)
info.size = len(content)
archive.addfile(info, io.BytesIO(content))
bad_artifact = tmp_path / "bad-metadata.tar.zst"
subprocess.run(["zstd", "-q", "-19", "-f", str(bad_tar), "-o", str(bad_artifact)], check=True)
bad_binding = _binding(
tmp_path,
version="V2R2C2",
tag_oid="1" * 40,
binding_schema="aios.release-binding.v2",
target_oid="2" * 40,
evidence_closure_oid="3" * 40,
approval_plan_id="sha256:" + "4" * 64,
source_registry_main_oid=source_main_oid,
source_registry_path="versions/V2R2C2.json",
source_registry_sha256=source_digest,
artifact_path=str(bad_artifact),
artifact_size=bad_artifact.stat().st_size,
artifact_sha256=hashlib.sha256(bad_artifact.read_bytes()).hexdigest(),
)
with pytest.raises(ReleaseBlocked, match="BUILD-METADATA"):
verify_artifact(bad_binding)
subprocess.run(["git", "-C", str(repo_work), "remote", "add", "origin", str(repo_bare)], check=True)
changed = dict(source)
changed["review_marker"] = "changed-after-M"
changed_bytes = (json.dumps(changed, ensure_ascii=False, sort_keys=True, indent=2) + "\n").encode()
(repo_work / "versions" / "V2R2C2.json").write_bytes(changed_bytes)
subprocess.run(["git", "-C", str(repo_work), "add", "versions"], check=True)
subprocess.run(["git", "-C", str(repo_work), "commit", "-qm", "change-source"], check=True)
subprocess.run(["git", "-C", str(repo_work), "push", "-q", "origin", "HEAD:main"], check=True)
with pytest.raises(ManualOwnerReviewRequired, match="current Authority main source registry"):
verify_artifact(binding)
def test_executor_requires_runtime_bundle_convergence_for_provenance_binding(tmp_path: Path):
binding = _binding(
tmp_path,
version="V2R2C2",
tag_oid="1" * 40,
binding_schema="aios.release-binding.v2",
source_registry_main_oid="5" * 40,
source_registry_path="versions/V2R2C2.json",
source_registry_sha256="6" * 64,
)
def fake_missing(_command, _request):
return {
"status": "ACTIVE_VERIFIED",
"version": binding.version,
"target_oid": binding.target_oid,
"evidence_closure_oid": binding.evidence_closure_oid,
"artifact_sha256": binding.artifact_sha256,
"approval_plan_id": binding.approval_plan_id,
"deployment_state": "ACTIVE",
"production_deployed": True,
}
monkeypatch = pytest.MonkeyPatch()
monkeypatch.setattr("approved_release_executor.run_adapter", fake_missing)
try:
with pytest.raises(ReconciliationRequired, match="runtime bundle/runtime registry"):
deploy_step(["/deploy"], binding)
finally:
monkeypatch.undo()
def fake_converged(_command, _request):
return {
"status": "ACTIVE_VERIFIED",
"version": binding.version,
"target_oid": binding.target_oid,
"evidence_closure_oid": binding.evidence_closure_oid,
"artifact_sha256": binding.artifact_sha256,
"approval_plan_id": binding.approval_plan_id,
"deployment_state": "ACTIVE",
"production_deployed": True,
"runtime_bundle_contains_current_version": True,
"runtime_registry_converged": True,
}
monkeypatch.setattr("approved_release_executor.run_adapter", fake_converged)
try:
assert deploy_step(["/deploy"], binding)["runtime_registry_converged"] is True
finally:
monkeypatch.undo()
def test_execute_rechecks_authoritative_git_after_runtime_before_registry(
monkeypatch, tmp_path: Path
):
work = tmp_path / "authority-work"
bare = tmp_path / "authority.git"
(work / "versions").mkdir(parents=True)
subprocess.run(["git", "init", "-q", "-b", "main", str(work)], check=True)
subprocess.run(["git", "-C", str(work), "config", "user.name", "test"], check=True)
subprocess.run(
["git", "-C", str(work), "config", "user.email", "test@example.invalid"],
check=True,
)
source = {
"schema": "aios.source-registry.v1",
"version": "VTEST",
"repository": "aios/aios",
"tag_oid": "1" * 40,
"tag_object": "1" * 40,
"target_oid": "a" * 40,
"peeled_target_oid": "a" * 40,
"evidence_closure_oid": "b" * 40,
"approval_plan_id": "sha256:" + "d" * 64,
}
source_path = work / "versions" / "VTEST.json"
source_path.write_bytes((json.dumps(source, sort_keys=True, indent=2) + "\n").encode())
subprocess.run(["git", "-C", str(work), "add", "versions"], check=True)
subprocess.run(["git", "-C", str(work), "commit", "-qm", "source"], check=True)
pinned_main = subprocess.run(
["git", "-C", str(work), "rev-parse", "HEAD"],
capture_output=True,
text=True,
check=True,
).stdout.strip()
subprocess.run(["git", "clone", "-q", "--bare", str(work), str(bare)], check=True)
source_sha = hashlib.sha256(source_path.read_bytes()).hexdigest()
binding = _binding(
tmp_path,
source_registry_main_oid=pinned_main,
source_registry_path="versions/VTEST.json",
source_registry_sha256=source_sha,
)
monkeypatch.setenv("AIOS_TEST_MODE", "1")
monkeypatch.setenv("AIOS_GITOPS_AUTHORITATIVE_GIT_DIR", str(bare))
monkeypatch.setattr("approved_release_executor.secure_json", lambda _path: _authorization(binding))
monkeypatch.setattr("approved_release_executor.verify_git_lineage", lambda *_args: {"verified": True})
monkeypatch.setattr("approved_release_executor.verify_artifact", lambda *_args, **_kwargs: {"verified": True})
deployed = False
calls = []
def fake_adapter(command, request):
nonlocal deployed
name = command[0]
operation = request["operation"]
calls.append((name, operation))
common = {
"version": binding.version,
"target_oid": binding.target_oid,
"evidence_closure_oid": binding.evidence_closure_oid,
"artifact_sha256": binding.artifact_sha256,
"approval_plan_id": binding.approval_plan_id,
}
if name == "/tag":
return {
**common,
"status": "APPLIED_VERIFIED",
"tag_oid": "1" * 40,
"peeled_target_oid": binding.target_oid,
}
if name == "/deploy":
if operation == "probe" and not deployed:
changed = dict(source)
changed["review_marker"] = "changed-after-runtime"
source_path.write_bytes((json.dumps(changed, sort_keys=True, indent=2) + "\n").encode())
subprocess.run(["git", "-C", str(work), "add", "versions"], check=True)
subprocess.run(["git", "-C", str(work), "commit", "-qm", "advance-main"], check=True)
subprocess.run(["git", "-C", str(work), "push", "-q", str(bare), "HEAD:main"], check=True)
deployed = True
return {
**common,
"status": "ACTIVE_VERIFIED",
"deployment_state": "ACTIVE",
"production_deployed": True,
"runtime_bundle_contains_current_version": True,
"runtime_registry_converged": True,
}
raise AssertionError(f"unexpected adapter call: {name}")
monkeypatch.setattr("approved_release_executor.run_adapter", fake_adapter)
with pytest.raises(ManualOwnerReviewRequired, match="current Authority main source registry"):
execute(
binding=binding,
authorization_path=tmp_path / "authorization.json",
repository=bare,
state_root=tmp_path / "state",
tag_command=["/tag"],
deploy_command=["/deploy"],
registry_command=["/registry"],
baseline_command=["/baseline"],
)
assert deployed is True
assert not any(name in {"/registry", "/baseline"} for name, _ in calls)
-850
View File
@@ -1,850 +0,0 @@
from __future__ import annotations
import hashlib
import inspect
import json
import os
import subprocess
import sys
from pathlib import Path
import pytest
APP = Path(__file__).resolve().parents[1] / "active-runtime" / "app"
sys.path.insert(0, str(APP))
import release_metadata_adapter as adapter # noqa: E402
import version_registry as registry_module # noqa: E402
from version_registry import ( # noqa: E402
RegistryConvergenceError,
assert_runtime_registry_convergence,
assert_source_registry_convergence,
build_source_registry_entry,
configured_approval_authority_root,
configured_authoritative_git_dir,
list_registry_versions,
list_registry_versions_report,
source_registry_snapshot,
verify_authoritative_repository,
verify_approval_authority,
verify_source_registry_main_convergence,
verify_source_registry_git_identity,
)
def _write_json(path: Path, obj: dict) -> None:
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(json.dumps(obj, sort_keys=True) + "\n", encoding="utf-8")
def _record(
*,
version: str = "V2R2C1",
tag_oid: str = "1" * 40,
target_oid: str = "2" * 40,
) -> dict:
return {
"schema": "aios.production-registry.v2",
"version": version,
"repository": "aios/aios",
"status": "PRODUCTION_DEPLOYED",
"production_deployed": True,
"tag_oid": tag_oid,
"target_oid": target_oid,
"peeled_target_oid": target_oid,
"evidence_closure_oid": "3" * 40,
"approval_plan_id": "sha256:" + "4" * 64,
}
def _canonical_approval(root: Path, record: dict) -> Path:
owner = {
"approved": True,
"status": "APPROVED",
"approved_by": "aiman",
"approval_source": "explicit_owner_approval_in_chat",
}
binding = {
key: record[key]
for key in (
"version",
"repository",
"tag_oid",
"target_oid",
"peeled_target_oid",
"evidence_closure_oid",
"approval_plan_id",
)
}
authority = {
"schema": "aios.v2r2c1.tag-approval-request.v1",
"state": "APPROVED",
"status": "APPROVED",
"execute": False,
"approval_plan_id": record["approval_plan_id"],
"binding": binding,
"owner_approval": owner,
"owner_approval_digest_sha256": hashlib.sha256(
(json.dumps(owner, sort_keys=True, separators=(",", ":")) + "\n").encode()
).hexdigest(),
}
path = root / f"{record['version']}.json"
_write_json(path, authority)
path.chmod(0o640)
return path
@pytest.fixture(autouse=True)
def _explicit_test_authority_mode(monkeypatch: pytest.MonkeyPatch):
"""Permit only these unit tests to inject temporary authority roots."""
monkeypatch.setenv("AIOS_TEST_MODE", "1")
def test_list_registry_versions_adds_only_baseline_attested_projection(tmp_path: Path):
ws = tmp_path / "ws"
(ws / "versions").mkdir(parents=True)
(ws / "versions" / "V2R1C7.json").write_text("{}\n")
projection_root = tmp_path / "projection"
projection = projection_root / "V2R2C1.json"
_write_json(projection, _record())
digest = hashlib.sha256(projection.read_bytes()).hexdigest()
baseline = tmp_path / "baseline" / "current.json"
_write_json(
baseline,
{
"version": "V2R2C1",
"status": "PRODUCTION_DEPLOYED",
"production_deployed": True,
"registry_source": str(projection),
"registry_digest": digest,
},
)
assert list_registry_versions(
ws,
projection_root=projection_root,
baseline_file=baseline,
) == ["V2R1C7.json", "V2R2C1.json"]
def test_list_registry_versions_rejects_projection_digest_drift(tmp_path: Path):
ws = tmp_path / "ws"
(ws / "versions").mkdir(parents=True)
projection_root = tmp_path / "projection"
projection = projection_root / "V2R2C1.json"
_write_json(projection, _record())
baseline = tmp_path / "baseline.json"
_write_json(
baseline,
{
"version": "V2R2C1",
"status": "PRODUCTION_DEPLOYED",
"production_deployed": True,
"registry_source": str(projection),
"registry_digest": "0" * 64,
},
)
assert list_registry_versions(
ws,
projection_root=projection_root,
baseline_file=baseline,
) == []
def test_projection_only_version_is_not_runtime_artifact_convergence(tmp_path: Path):
ws = tmp_path / "ws"
(ws / "versions").mkdir(parents=True)
projection_root = tmp_path / "projection"
projection = projection_root / "V2R2C1.json"
_write_json(projection, _record())
baseline = tmp_path / "baseline.json"
_write_json(
baseline,
{
"version": "V2R2C1",
"status": "PRODUCTION_DEPLOYED",
"production_deployed": True,
"registry_source": str(projection),
"registry_digest": hashlib.sha256(projection.read_bytes()).hexdigest(),
},
)
report = list_registry_versions_report(
ws,
projection_root=projection_root,
baseline_file=baseline,
)
assert report["registry_versions"] == ["V2R2C1.json"]
assert report["attested_projection_version"] == "V2R2C1"
assert report["runtime_bundle_contains_version"] is False
assert report["source_registry_converged"] == "unknown"
assert report["runtime_registry_converged"] is False
assert report["runtime_bundle_contains_current_version"] is False
assert report["registry_consistency_status"] == "RUNTIME_ARTIFACT_RECONCILIATION_REQUIRED"
with pytest.raises(RegistryConvergenceError, match="runtime_bundle_missing"):
assert_runtime_registry_convergence(
ws,
"V2R2C1",
projection_root=projection_root,
baseline_file=baseline,
)
def _attested_projection(tmp_path: Path, record: dict) -> tuple[Path, Path]:
projection_root = tmp_path / "projection"
projection = projection_root / f"{record['version']}.json"
_write_json(projection, record)
baseline = tmp_path / "baseline.json"
_write_json(
baseline,
{
"version": record["version"],
"status": "PRODUCTION_DEPLOYED",
"production_deployed": True,
"registry_source": str(projection),
"registry_digest": hashlib.sha256(projection.read_bytes()).hexdigest(),
},
)
return projection_root, baseline
def test_list_report_source_good_runtime_missing_is_not_fully_consistent(tmp_path: Path):
record = _record()
bare, branch = _init_bare_repo(tmp_path, record)
projection_root, baseline = _attested_projection(tmp_path, record)
ws = tmp_path / "runtime"
(ws / "versions").mkdir(parents=True)
report = list_registry_versions_report(
ws,
projection_root=projection_root,
baseline_file=baseline,
repo_git_dir=bare,
main_ref=f"refs/heads/{branch}",
)
assert report["source_registry_converged"] is True
assert report["runtime_registry_converged"] is False
assert report["runtime_bundle_contains_current_version"] is False
assert report["registry_consistency_status"] == "RUNTIME_ARTIFACT_RECONCILIATION_REQUIRED"
def test_list_report_source_runtime_projection_good_is_fully_consistent(tmp_path: Path):
record = _record()
bare, branch = _init_bare_repo(tmp_path, record)
projection_root, baseline = _attested_projection(tmp_path, record)
ws = tmp_path / "runtime"
versions = ws / "versions"
versions.mkdir(parents=True)
_write_json(versions / f"{record['version']}.json", record)
report = list_registry_versions_report(
ws,
projection_root=projection_root,
baseline_file=baseline,
repo_git_dir=bare,
main_ref=f"refs/heads/{branch}",
)
assert report["source_registry_converged"] is True
assert report["runtime_registry_converged"] is True
assert report["runtime_bundle_contains_current_version"] is True
assert report["registry_consistency_status"] == "FULLY_CONSISTENT"
def test_list_report_source_missing_is_not_converged(tmp_path: Path):
record = _record()
work = tmp_path / "no-source-work"
bare = tmp_path / "no-source.git"
subprocess.run(["git", "init", "-q", str(work)], check=True)
subprocess.run(["git", "-C", str(work), "config", "user.name", "test"], check=True)
subprocess.run(
["git", "-C", str(work), "config", "user.email", "test@example.invalid"],
check=True,
)
(work / "README").write_text("no source\n", encoding="utf-8")
subprocess.run(["git", "-C", str(work), "add", "README"], check=True)
subprocess.run(["git", "-C", str(work), "commit", "-qm", "no source"], check=True)
branch = subprocess.run(
["git", "-C", str(work), "symbolic-ref", "--short", "HEAD"],
capture_output=True,
text=True,
check=True,
).stdout.strip()
subprocess.run(["git", "clone", "-q", "--bare", str(work), str(bare)], check=True)
projection_root, baseline = _attested_projection(tmp_path, record)
ws = tmp_path / "runtime"
(ws / "versions").mkdir(parents=True)
_write_json(ws / "versions" / f"{record['version']}.json", record)
report = list_registry_versions_report(
ws,
projection_root=projection_root,
baseline_file=baseline,
repo_git_dir=bare,
main_ref=f"refs/heads/{branch}",
)
assert report["source_registry_converged"] is False
assert report["runtime_registry_converged"] is True
assert report["registry_consistency_status"] == "SOURCE_REGISTRY_NOT_CONVERGED"
def test_list_registry_versions_excludes_non_version_entries(tmp_path: Path):
ws = tmp_path / "ws"
versions = ws / "versions"
versions.mkdir(parents=True)
(versions / "README.txt").write_text("do not expose\n", encoding="utf-8")
(versions / "scratch.json").write_text("{}\n", encoding="utf-8")
(versions / "proposals").mkdir()
(versions / "V2R1C7.json").write_text("{}\n", encoding="utf-8")
assert list_registry_versions(
ws,
projection_root=tmp_path / "projection",
baseline_file=tmp_path / "baseline.json",
) == ["V2R1C7.json"]
def _init_bare_repo(tmp_path: Path, record: dict) -> tuple[Path, str]:
work = tmp_path / "work"
bare = tmp_path / "repo.git"
subprocess.run(["git", "init", "-q", str(work)], check=True)
subprocess.run(["git", "-C", str(work), "config", "user.name", "test"], check=True)
subprocess.run(["git", "-C", str(work), "config", "user.email", "test@example.invalid"], check=True)
_write_json(work / "versions" / f"{record['version']}.json", record)
subprocess.run(["git", "-C", str(work), "add", "versions"], check=True)
subprocess.run(["git", "-C", str(work), "commit", "-qm", "registry"], check=True)
branch = subprocess.run(
["git", "-C", str(work), "symbolic-ref", "--short", "HEAD"],
capture_output=True,
text=True,
check=True,
).stdout.strip()
subprocess.run(["git", "clone", "-q", "--bare", str(work), str(bare)], check=True)
return bare, branch
def _init_bare_repo_with_work(tmp_path: Path, record: dict) -> tuple[Path, Path, str]:
work = tmp_path / "history-work"
bare = tmp_path / "history.git"
subprocess.run(["git", "init", "-q", str(work)], check=True)
subprocess.run(["git", "-C", str(work), "config", "user.name", "test"], check=True)
subprocess.run(
["git", "-C", str(work), "config", "user.email", "test@example.invalid"],
check=True,
)
_write_json(work / "versions" / f"{record['version']}.json", record)
subprocess.run(["git", "-C", str(work), "add", "versions"], check=True)
subprocess.run(["git", "-C", str(work), "commit", "-qm", "registry"], check=True)
branch = subprocess.run(
["git", "-C", str(work), "symbolic-ref", "--short", "HEAD"],
capture_output=True,
text=True,
check=True,
).stdout.strip()
subprocess.run(["git", "clone", "-q", "--bare", str(work), str(bare)], check=True)
subprocess.run(["git", "-C", str(work), "remote", "add", "origin", str(bare)], check=True)
return work, bare, branch
def test_assert_source_registry_convergence_passes_matching_identity(tmp_path: Path):
record = _record()
bare, branch = _init_bare_repo(tmp_path, record)
projection = tmp_path / "projection.json"
_write_json(projection, record)
assert_source_registry_convergence(
projection,
repo_git_dir=bare,
main_ref=f"refs/heads/{branch}",
)
def test_assert_source_registry_convergence_fails_missing_entry(tmp_path: Path):
record = _record()
work = tmp_path / "work"
bare = tmp_path / "repo.git"
subprocess.run(["git", "init", "-q", str(work)], check=True)
subprocess.run(["git", "-C", str(work), "config", "user.name", "test"], check=True)
subprocess.run(["git", "-C", str(work), "config", "user.email", "test@example.invalid"], check=True)
(work / "README").write_text("x\n")
subprocess.run(["git", "-C", str(work), "add", "README"], check=True)
subprocess.run(["git", "-C", str(work), "commit", "-qm", "init"], check=True)
branch = subprocess.run(
["git", "-C", str(work), "symbolic-ref", "--short", "HEAD"],
capture_output=True,
text=True,
check=True,
).stdout.strip()
subprocess.run(["git", "clone", "-q", "--bare", str(work), str(bare)], check=True)
projection = tmp_path / "projection.json"
_write_json(projection, record)
with pytest.raises(RegistryConvergenceError, match="source_registry_entry_missing"):
assert_source_registry_convergence(
projection,
repo_git_dir=bare,
main_ref=f"refs/heads/{branch}",
)
def test_assert_source_registry_convergence_fails_identity_mismatch(tmp_path: Path):
source = _record(tag_oid="5" * 40)
projection_record = _record(tag_oid="1" * 40)
bare, branch = _init_bare_repo(tmp_path, source)
projection = tmp_path / "projection.json"
_write_json(projection, projection_record)
with pytest.raises(RegistryConvergenceError, match="source_registry_mismatch:tag_oid"):
assert_source_registry_convergence(
projection,
repo_git_dir=bare,
main_ref=f"refs/heads/{branch}",
)
def test_assert_source_registry_convergence_requires_all_identity_fields(tmp_path: Path):
source = _record()
source.pop("peeled_target_oid")
bare, branch = _init_bare_repo(tmp_path, source)
projection = tmp_path / "projection.json"
_write_json(projection, _record())
with pytest.raises(RegistryConvergenceError, match="peeled_target_oid"):
assert_source_registry_convergence(
projection,
repo_git_dir=bare,
main_ref=f"refs/heads/{branch}",
)
def test_assert_source_registry_convergence_rejects_wrong_repository(tmp_path: Path):
source = _record()
source["repository"] = "other/repository"
bare, branch = _init_bare_repo(tmp_path, source)
projection = tmp_path / "projection.json"
_write_json(projection, _record())
with pytest.raises(RegistryConvergenceError, match="repository"):
assert_source_registry_convergence(
projection,
repo_git_dir=bare,
main_ref=f"refs/heads/{branch}",
)
def test_source_registry_main_convergence_accepts_unrelated_later_main_commit(tmp_path: Path):
record = _record()
work, bare, branch = _init_bare_repo_with_work(tmp_path, record)
source_main = subprocess.run(
["git", "-C", str(work), "rev-parse", "HEAD"],
capture_output=True,
text=True,
check=True,
).stdout.strip()
(work / "README").write_text("unrelated later main commit\n", encoding="utf-8")
subprocess.run(["git", "-C", str(work), "add", "README"], check=True)
subprocess.run(["git", "-C", str(work), "commit", "-qm", "unrelated"], check=True)
subprocess.run(["git", "-C", str(work), "push", "-q", "origin", branch], check=True)
result = verify_source_registry_main_convergence(
bare,
record["version"],
main_ref=f"refs/heads/{branch}",
source_registry_main_oid=source_main,
expected_tag_oid=record["tag_oid"],
expected_target_oid=record["target_oid"],
expected_evidence_closure_oid=record["evidence_closure_oid"],
expected_approval_plan_id=record["approval_plan_id"],
)
assert result["source_registry_main_is_ancestor_of_current_main"] is True
def test_source_registry_main_convergence_rejects_side_branch_only_commit(tmp_path: Path):
record = _record()
work, bare, branch = _init_bare_repo_with_work(tmp_path, record)
base = subprocess.run(
["git", "-C", str(work), "rev-parse", "HEAD"],
capture_output=True,
text=True,
check=True,
).stdout.strip()
subprocess.run(["git", "-C", str(work), "checkout", "-qb", "side"], check=True)
(work / "side.txt").write_text("side\n", encoding="utf-8")
subprocess.run(["git", "-C", str(work), "add", "side.txt"], check=True)
subprocess.run(["git", "-C", str(work), "commit", "-qm", "side"], check=True)
side = subprocess.run(
["git", "-C", str(work), "rev-parse", "HEAD"],
capture_output=True,
text=True,
check=True,
).stdout.strip()
subprocess.run(["git", "-C", str(work), "checkout", "-q", branch], check=True)
(work / "main.txt").write_text("main\n", encoding="utf-8")
subprocess.run(["git", "-C", str(work), "add", "main.txt"], check=True)
subprocess.run(["git", "-C", str(work), "commit", "-qm", "main"], check=True)
subprocess.run(["git", "-C", str(work), "push", "-q", "origin", branch], check=True)
assert base != side
with pytest.raises(RegistryConvergenceError, match="not_ancestor"):
verify_source_registry_main_convergence(
bare,
record["version"],
main_ref=f"refs/heads/{branch}",
source_registry_main_oid=side,
)
def test_source_registry_main_convergence_rejects_removed_entry(tmp_path: Path):
record = _record()
work, bare, branch = _init_bare_repo_with_work(tmp_path, record)
source_main = subprocess.run(
["git", "-C", str(work), "rev-parse", "HEAD"],
capture_output=True,
text=True,
check=True,
).stdout.strip()
(work / "versions" / f"{record['version']}.json").unlink()
subprocess.run(["git", "-C", str(work), "add", "-u", "versions"], check=True)
subprocess.run(["git", "-C", str(work), "commit", "-qm", "remove"], check=True)
subprocess.run(["git", "-C", str(work), "push", "-q", "origin", branch], check=True)
with pytest.raises(RegistryConvergenceError, match="entry_missing"):
verify_source_registry_main_convergence(
bare,
record["version"],
main_ref=f"refs/heads/{branch}",
source_registry_main_oid=source_main,
)
def test_source_registry_main_convergence_rejects_altered_entry(tmp_path: Path):
record = _record()
work, bare, branch = _init_bare_repo_with_work(tmp_path, record)
source_main = subprocess.run(
["git", "-C", str(work), "rev-parse", "HEAD"],
capture_output=True,
text=True,
check=True,
).stdout.strip()
altered = dict(record)
altered["approval_plan_id"] = "sha256:" + "9" * 64
_write_json(work / "versions" / f"{record['version']}.json", altered)
subprocess.run(["git", "-C", str(work), "add", "versions"], check=True)
subprocess.run(["git", "-C", str(work), "commit", "-qm", "alter"], check=True)
subprocess.run(["git", "-C", str(work), "push", "-q", "origin", branch], check=True)
with pytest.raises(RegistryConvergenceError, match="entry_changed"):
verify_source_registry_main_convergence(
bare,
record["version"],
main_ref=f"refs/heads/{branch}",
source_registry_main_oid=source_main,
)
def test_source_registry_generator_predeclares_release_identity():
value = build_source_registry_entry(
version="V2R2C1",
tag_oid="1" * 40,
target_oid="2" * 40,
peeled_target_oid="2" * 40,
evidence_closure_oid="3" * 40,
approval_plan_id="sha256:" + "4" * 64,
)
assert set(
(
"version",
"tag_oid",
"target_oid",
"peeled_target_oid",
"evidence_closure_oid",
"approval_plan_id",
)
).issubset(value)
def test_authoritative_repository_rejects_arbitrary_repository(
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
):
record = _record()
bare, _ = _init_bare_repo(tmp_path, record)
monkeypatch.delenv("AIOS_TEST_MODE")
with pytest.raises(RegistryConvergenceError, match="path_mismatch"):
verify_authoritative_repository(bare)
def test_production_authority_defaults_are_fixed_and_env_overrides_fail(
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
):
monkeypatch.delenv("AIOS_TEST_MODE")
monkeypatch.delenv("AIOS_GITOPS_AUTHORITATIVE_GIT_DIR", raising=False)
monkeypatch.delenv("AIOS_GITOPS_APPROVAL_AUTHORITY_ROOT", raising=False)
assert configured_authoritative_git_dir() == Path("/data/aios-gitops/repo.git")
assert configured_approval_authority_root() == Path(
"/var/lib/aios-gitops/approval-authority"
)
monkeypatch.setenv("AIOS_GITOPS_AUTHORITATIVE_GIT_DIR", str(tmp_path / "fake.git"))
with pytest.raises(RegistryConvergenceError, match="override_forbidden"):
configured_authoritative_git_dir()
monkeypatch.setenv("AIOS_GITOPS_APPROVAL_AUTHORITY_ROOT", str(tmp_path / "fake-root"))
with pytest.raises(RegistryConvergenceError, match="override_forbidden"):
configured_approval_authority_root()
def test_production_canonical_repository_and_test_injected_authority(
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
):
record = _record()
bare, _ = _init_bare_repo(tmp_path, record)
subprocess.run(
[
"git",
f"--git-dir={bare}",
"config",
"remote.gitea.url",
"https://g.cnnetmkt.top/aios/aios.git",
],
check=True,
)
monkeypatch.delenv("AIOS_TEST_MODE")
monkeypatch.setattr(registry_module, "DEFAULT_AUTHORITATIVE_GIT_DIR", bare)
assert verify_authoritative_repository(bare)["repository_identity"] == "aios/aios"
monkeypatch.setenv("AIOS_TEST_MODE", "1")
monkeypatch.setenv("AIOS_GITOPS_AUTHORITATIVE_GIT_DIR", str(bare))
assert verify_authoritative_repository(bare)["configured_path"] == str(bare.resolve())
def test_authoritative_git_unavailable_stays_unknown_and_closure_fails(
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
):
record = _record()
projection_root, baseline = _attested_projection(tmp_path, record)
ws = tmp_path / "runtime"
(ws / "versions").mkdir(parents=True)
_write_json(ws / "versions" / f"{record['version']}.json", record)
missing = tmp_path / "missing-authority.git"
report = list_registry_versions_report(
ws,
projection_root=projection_root,
baseline_file=baseline,
repo_git_dir=missing,
)
assert report["source_registry_converged"] == "unknown"
assert report["source_registry_convergence_error"] == "authoritative_repository_unavailable"
monkeypatch.delenv("AIOS_TEST_MODE")
with pytest.raises(RegistryConvergenceError, match="path_unavailable"):
assert_source_registry_convergence(
projection_root / f"{record['version']}.json",
repo_git_dir=missing,
)
def test_source_registry_snapshot_stays_pinned_after_main_advances(tmp_path: Path):
work = tmp_path / "work"
bare = tmp_path / "repo.git"
subprocess.run(["git", "init", "-q", str(work)], check=True)
subprocess.run(["git", "-C", str(work), "config", "user.name", "test"], check=True)
subprocess.run(
["git", "-C", str(work), "config", "user.email", "test@example.invalid"],
check=True,
)
record = _record()
_write_json(work / "versions" / "V2R2C1.json", record)
subprocess.run(["git", "-C", str(work), "add", "versions"], check=True)
subprocess.run(["git", "-C", str(work), "commit", "-qm", "registry-1"], check=True)
branch = subprocess.run(
["git", "-C", str(work), "branch", "--show-current"],
capture_output=True,
text=True,
check=True,
).stdout.strip()
subprocess.run(["git", "clone", "-q", "--bare", str(work), str(bare)], check=True)
pinned = source_registry_snapshot(
bare,
"V2R2C1",
main_ref=f"refs/heads/{branch}",
)
updated = dict(record)
updated["review_marker"] = "main-advanced"
_write_json(work / "versions" / "V2R2C1.json", updated)
subprocess.run(["git", "-C", str(work), "add", "versions"], check=True)
subprocess.run(["git", "-C", str(work), "commit", "-qm", "registry-2"], check=True)
subprocess.run(["git", "-C", str(work), "remote", "add", "authority", str(bare)], check=True)
subprocess.run(["git", "-C", str(work), "push", "-q", "authority", branch], check=True)
repinned = source_registry_snapshot(
bare,
"V2R2C1",
main_ref=f"refs/heads/{branch}",
source_registry_main_oid=pinned["source_registry_main_oid"],
)
assert repinned["source_registry_main_oid"] == pinned["source_registry_main_oid"]
assert repinned["bytes"] == pinned["bytes"]
assert repinned["source_registry_sha256"] == pinned["source_registry_sha256"]
assert repinned["record"].get("review_marker") is None
def _git_commit_with_release_tag(tmp_path: Path, *, annotated: bool = True):
work = tmp_path / "authority-work"
bare = tmp_path / "authority.git"
subprocess.run(["git", "init", "-q", str(work)], check=True)
subprocess.run(["git", "-C", str(work), "config", "user.name", "test"], check=True)
subprocess.run(["git", "-C", str(work), "config", "user.email", "test@example.invalid"], check=True)
(work / "README").write_text("release\n", encoding="utf-8")
subprocess.run(["git", "-C", str(work), "add", "README"], check=True)
subprocess.run(["git", "-C", str(work), "commit", "-qm", "target"], check=True)
target = subprocess.run(
["git", "-C", str(work), "rev-parse", "HEAD"], capture_output=True, text=True, check=True
).stdout.strip()
tag_args = ["git", "-C", str(work), "tag"]
if annotated:
tag_args += ["-a", "V2R2C1", target, "-m", "release"]
else:
tag_args += ["V2R2C1", target]
subprocess.run(tag_args, check=True)
tag_oid = subprocess.run(
["git", "-C", str(work), "rev-parse", "refs/tags/V2R2C1"],
capture_output=True,
text=True,
check=True,
).stdout.strip()
subprocess.run(["git", "-C", str(work), "commit", "--allow-empty", "-qm", "evidence"], check=True)
evidence = subprocess.run(
["git", "-C", str(work), "rev-parse", "HEAD"], capture_output=True, text=True, check=True
).stdout.strip()
record = _record(tag_oid=tag_oid, target_oid=target)
record["evidence_closure_oid"] = evidence
_write_json(work / "versions" / "V2R2C1.json", record)
subprocess.run(["git", "-C", str(work), "add", "versions"], check=True)
subprocess.run(["git", "-C", str(work), "commit", "-qm", "source registry"], check=True)
branch = subprocess.run(
["git", "-C", str(work), "branch", "--show-current"], capture_output=True, text=True, check=True
).stdout.strip()
subprocess.run(["git", "clone", "-q", "--bare", str(work), str(bare)], check=True)
return work, bare, branch, record, target, evidence, tag_oid
def test_source_registry_git_identity_rejects_lightweight_tag(tmp_path: Path):
_, bare, _, record, _, _, _ = _git_commit_with_release_tag(tmp_path, annotated=False)
with pytest.raises(RegistryConvergenceError, match="tag_not_annotated"):
verify_source_registry_git_identity(bare, record)
def test_source_registry_git_identity_proves_annotated_tag_and_lineage(
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
):
_, bare, _, record, target, evidence, tag_oid = _git_commit_with_release_tag(tmp_path)
result = verify_source_registry_git_identity(bare, record)
assert result["tag_oid"] == tag_oid
assert result["target_oid"] == target
assert result["evidence_closure_oid"] == evidence
authority_root = tmp_path / "authority-root"
authority_root.mkdir()
monkeypatch.setenv("AIOS_GITOPS_APPROVAL_AUTHORITY_ROOT", str(authority_root))
authority = _canonical_approval(authority_root, record)
assert verify_approval_authority(authority, record)["release_binding_verified"] is True
def test_source_registry_generator_requires_authoritative_git_and_approval(
tmp_path: Path, monkeypatch: pytest.MonkeyPatch,
):
_, bare, _, record, _, _, _ = _git_commit_with_release_tag(tmp_path)
subprocess.run(
["git", f"--git-dir={bare}", "config", "remote.gitea.url", "https://g.cnnetmkt.top/aios/aios.git"],
check=True,
)
authority_root = tmp_path / "authority-root"
authority_root.mkdir()
monkeypatch.setenv("AIOS_GITOPS_AUTHORITATIVE_GIT_DIR", str(bare))
monkeypatch.setenv("AIOS_GITOPS_APPROVAL_AUTHORITY_ROOT", str(authority_root))
authority = _canonical_approval(authority_root, record)
output = tmp_path / "generated" / "versions" / "V2R2C1.json"
script = Path(__file__).resolve().parents[1] / "scripts" / "generate_source_registry.py"
command = [
sys.executable,
str(script),
"--output",
str(output),
"--version",
record["version"],
"--tag-oid",
record["tag_oid"],
"--target-oid",
record["target_oid"],
"--peeled-target-oid",
record["peeled_target_oid"],
"--evidence-closure-oid",
record["evidence_closure_oid"],
"--approval-plan-id",
record["approval_plan_id"],
"--repository",
str(bare),
"--approval-authority",
str(authority),
]
completed = subprocess.run(command, capture_output=True, text=True, check=False)
assert completed.returncode == 0, completed.stderr
assert json.loads(output.read_text(encoding="utf-8"))["tag_oid"] == record["tag_oid"]
wrong = list(command)
wrong[wrong.index("--approval-plan-id") + 1] = "sha256:" + "f" * 64
failed = subprocess.run(wrong, capture_output=True, text=True, check=False)
assert failed.returncode != 0
other = tmp_path / "other.git"
subprocess.run(["git", "clone", "-q", "--bare", str(bare), str(other)], check=True)
arbitrary = list(command)
arbitrary[arbitrary.index("--repository") + 1] = str(other)
failed_authority = subprocess.run(arbitrary, capture_output=True, text=True, check=False)
assert failed_authority.returncode != 0
def test_approval_authority_rejects_unassociated_plan(
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
):
record = _record()
authority_root = tmp_path / "authority-root"
authority_root.mkdir()
monkeypatch.setenv("AIOS_GITOPS_APPROVAL_AUTHORITY_ROOT", str(authority_root))
authority = authority_root / f"{record['version']}.json"
_write_json(
authority,
{
"schema": "aios.v2r2c1.tag-approval-request.v1",
"state": "APPROVED",
"approval_plan_id": record["approval_plan_id"],
},
)
authority.chmod(0o640)
with pytest.raises(RegistryConvergenceError, match="binding_missing"):
verify_approval_authority(authority, record)
def test_approval_authority_rejects_untrusted_path(tmp_path: Path, monkeypatch: pytest.MonkeyPatch):
authority_root = tmp_path / "authority-root"
authority_root.mkdir()
monkeypatch.setenv("AIOS_GITOPS_APPROVAL_AUTHORITY_ROOT", str(authority_root))
authority = tmp_path / "approval.json"
_write_json(authority, {"schema": "aios.v2r2c1.tag-approval-request.v1"})
with pytest.raises(RegistryConvergenceError, match="untrusted_path"):
verify_approval_authority(authority, _record())
def test_approval_authority_rejects_revoked_receipt(tmp_path: Path, monkeypatch: pytest.MonkeyPatch):
authority_root = tmp_path / "authority-root"
authority_root.mkdir()
monkeypatch.setenv("AIOS_GITOPS_APPROVAL_AUTHORITY_ROOT", str(authority_root))
record = _record()
authority = _canonical_approval(authority_root, record)
value = json.loads(authority.read_text(encoding="utf-8"))
value["revoked"] = True
authority.write_text(json.dumps(value) + "\n", encoding="utf-8")
authority.chmod(0o640)
with pytest.raises(RegistryConvergenceError, match="revoked"):
verify_approval_authority(authority, record)
def test_baseline_apply_wires_convergence_to_caller_repository():
source = inspect.getsource(adapter.baseline_apply)
assert "assert_source_registry_convergence(" in source
assert "repo_git_dir=args.repository" in source
# The gate must execute before the dict that writes final production status.
assert source.index("assert_source_registry_convergence(") < source.index(
'"status": "PRODUCTION_DEPLOYED"'
)
-21
View File
@@ -1,21 +0,0 @@
{
"approval_plan_id": "sha256:c0df52008ae7f64cec5ee2d28947aa7b892066c670ab8295e4549140b1a7500e",
"artifact_sha256": "ab4ae257346c82863f05abf5e309381a1c86612957608c3a4f4ab5cc9233f052",
"deployment_state": "ACTIVE",
"enabled": true,
"evidence_closure_oid": "a8b906d1bc24088c19647e98de6a679268f6d0a2",
"peeled_target_oid": "998d30304787e31e5ae607c0bd86b5edb2b0e5ad",
"production_deployed": true,
"production_port": 18927,
"production_service": "aios-v2r1c7-production.service",
"registered": true,
"repository": "aios/aios",
"schema": "aios.production-registry.v2",
"status": "PRODUCTION_DEPLOYED",
"tag": "V2R2C1",
"tag_object": "07fc2746b688f5f243f29b39de39f32edc3dd9d2",
"tag_oid": "07fc2746b688f5f243f29b39de39f32edc3dd9d2",
"target_oid": "998d30304787e31e5ae607c0bd86b5edb2b0e5ad",
"updated_at": "2026-08-09T09:13:13Z",
"version": "V2R2C1"
}