Compare commits
57
Commits
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"schema": "aios.v2r1c7.engineering-build-receipt.v1",
|
||||
"version": "V2R1C7",
|
||||
"base_oid": "b356dec2ac5bd2a5f721aa73f8fd1902f9c65632",
|
||||
"production_deployed": false,
|
||||
"tag_created": false,
|
||||
"baseline_changed": false,
|
||||
"deployable": true
|
||||
}
|
||||
@@ -1,10 +1,21 @@
|
||||
bec7da1adb75bf5feaa63c60ec6d03adf15d7508742a486207033d000f72d4ff AGENT_DEPLOYMENT_REQUIREMENTS.md
|
||||
251b46e51ce09aa2fd843bee48ef24a5608f31d24c599841f086ceb25c1a1715 README.md
|
||||
783f68ec4f00a11d43ceed536196bfedccaf6614f6db9805f7b36d24e5fa7bc2 TEST_RESULTS.json
|
||||
9208325e5b2e6f5ba6b64e000bc21d14d9f2f214e3ad33dd79857e85218fd6c2 VERSION
|
||||
c4b24b65195077716983acdc8611f705324413a74453858994ba51541957afd4 active-runtime/app/aios_authorized_task_writer_api.py
|
||||
8a1935fa098d47ac02ce51bbd9fd5fad7cf9ab9374ea3de5f630679b96681d84 active-runtime/app/authorized_task_writer_provisioning.py
|
||||
0997ab8332985c7124f334c4e49735d8833326578acc61a61a389bf792424de3 active-runtime/app/deployment_state.py
|
||||
dd3f8b1cab88e005a291f077807873c381e8ca92fe863dc05817925f0dff9613 active-runtime/app/v2r1c6_privacy_tools.py
|
||||
9e45025c41068cafdea2f360edfc4146a5459b0a6bcfbf1403206fc0161e7bac active-runtime/app/v2r1c7_health_service.py
|
||||
c96ba560d69f6c2eb700b9f2b42a32e394dadd00dffb23bbefdb9fb3e1571c0d deploy/aios-v2/systemd/aios-v2r1c7-production.service
|
||||
a05d4e8038fdb866ce17d4fb8a54798a268a1e8d44ff2cbc559069fa6ab2a667 deploy/systemd/aios-gitops-model-api-authorized-writer-bridge.conf.example
|
||||
a60dac51802bff67c2cfff61acdeb0a3a05d74a4f6cb47e3f78295ceb5c6acd9 docs/gitops/authorized-task-writer-provisioning.md
|
||||
2690f76452bceddda90c728278c600bcebd533db24776e2966423c5a8655e4b5 docs/gitops/openapi-authorized-task-writer-fragment.yaml
|
||||
560e5d9af8a8e2ec60ec31cf8659a7982b93ea09b7adb486b738ea2fba460d70 release-manifests/V2R1C7.json
|
||||
81a423c7814f81cb888b60a538c153e549b2be9099c881c968d55ae84efddc57 scripts/build_v2r1c7_release_artifact.py
|
||||
64c78dabffb05c8851f87b2a27c43bd1f21728dca586397d03a6bc22906a906c scripts/probe_v2r1c7_health.py
|
||||
5419dc7de0f50908ad0ef1dac74172fce811bb7b45f314a6e1c1bb9ce618324d scripts/rollback_v2r1c7_release.sh
|
||||
54e2845d2c3c026323ad45515e082d661e3b461feb3bfefedf5adc52db2d7b78 scripts/smoke_authorized_task_writer_provisioning.py
|
||||
32a860e28d5afc105f413d878a13fb8600fa74874dbd3bc4c031ef3efa17896a scripts/verify_v2r1c7_release.py
|
||||
3cce2b744326fe12b89bcad3cd352c77738950964bf8519e72cb5e9c7a5dfbfc tests/test_authorized_task_writer_provisioning.py
|
||||
8f3153c625241f60b65109eeac045e8aed7661d95f3a10f0b32ba3e0e9ac0a85 versions/V2R1C7.json
|
||||
|
||||
@@ -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,173 @@
|
||||
"""Fail-closed Authority Git side-effect boundary.
|
||||
|
||||
Only this module may issue the narrowly scoped ref CAS primitives used by the
|
||||
durable executor. It never accepts force updates, arbitrary refs, tags, or
|
||||
direct production mutations.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
import os
|
||||
from pathlib import Path
|
||||
import re
|
||||
import subprocess
|
||||
import tempfile
|
||||
from typing import Mapping, Sequence
|
||||
|
||||
|
||||
OID_RE = re.compile(r"^[0-9a-f]{40}$")
|
||||
TASK_BRANCH_RE = re.compile(r"^refs/heads/agent/[A-Za-z0-9][A-Za-z0-9._/-]{2,179}$")
|
||||
|
||||
|
||||
class AuthorityGitError(RuntimeError):
|
||||
pass
|
||||
|
||||
|
||||
def _oid(value: str, field: str) -> str:
|
||||
if not isinstance(value, str) or not OID_RE.fullmatch(value):
|
||||
raise AuthorityGitError(f"{field}_INVALID")
|
||||
return value
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class AuthorityGitExecutor:
|
||||
git_dir: Path
|
||||
worktree_root: Path = Path("/var/lib/aios-gitops/worktrees")
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
if not self.git_dir.is_absolute() or self.git_dir.is_symlink():
|
||||
raise AuthorityGitError("AUTHORITY_GIT_DIR_INVALID")
|
||||
if not self.worktree_root.is_absolute() or self.worktree_root.is_symlink():
|
||||
raise AuthorityGitError("WORKTREE_ROOT_INVALID")
|
||||
|
||||
def _git(self, args: Sequence[str], *, check: bool = True) -> str:
|
||||
result = subprocess.run(
|
||||
["git", f"--git-dir={self.git_dir}", *args],
|
||||
text=True,
|
||||
capture_output=True,
|
||||
check=False,
|
||||
)
|
||||
if check and result.returncode != 0:
|
||||
raise AuthorityGitError("AUTHORITY_GIT_COMMAND_FAILED")
|
||||
return result.stdout.strip()
|
||||
|
||||
def read_ref(self, ref: str) -> str | None:
|
||||
if not ref.startswith("refs/") or ref.startswith(("refs/tags/", "refs/remotes/")):
|
||||
raise AuthorityGitError("REF_READ_SCOPE_DENIED")
|
||||
value = self._git(["rev-parse", "--verify", ref], check=False)
|
||||
return value if value and OID_RE.fullmatch(value) else None
|
||||
|
||||
def commit_exists(self, oid: str) -> bool:
|
||||
_oid(oid, "commit_oid")
|
||||
result = subprocess.run(["git", f"--git-dir={self.git_dir}", "cat-file", "-e", oid + "^{commit}"], capture_output=True, check=False)
|
||||
return result.returncode == 0
|
||||
|
||||
def ancestry_check(self, older_oid: str, newer_oid: str) -> bool:
|
||||
_oid(older_oid, "older_oid")
|
||||
_oid(newer_oid, "newer_oid")
|
||||
result = subprocess.run(
|
||||
["git", f"--git-dir={self.git_dir}", "merge-base", "--is-ancestor", older_oid, newer_oid],
|
||||
capture_output=True,
|
||||
check=False,
|
||||
)
|
||||
if result.returncode not in (0, 1):
|
||||
raise AuthorityGitError("ANCESTRY_CHECK_FAILED")
|
||||
return result.returncode == 0
|
||||
|
||||
def create_detached_worktree(
|
||||
self,
|
||||
base_oid: str,
|
||||
worktree_name: str,
|
||||
root: Path | None = None,
|
||||
) -> Path:
|
||||
_oid(base_oid, "base_oid")
|
||||
if not worktree_name or "/" in worktree_name or "\\" in worktree_name or worktree_name in {".", ".."}:
|
||||
raise AuthorityGitError("WORKTREE_NAME_INVALID")
|
||||
parent = root or self.worktree_root
|
||||
if not parent.is_absolute() or parent.is_symlink():
|
||||
raise AuthorityGitError("WORKTREE_ROOT_INVALID")
|
||||
path = parent / worktree_name
|
||||
if path.exists() or path.is_symlink():
|
||||
raise AuthorityGitError("WORKTREE_ALREADY_EXISTS")
|
||||
parent.mkdir(parents=True, exist_ok=True)
|
||||
self._git(["worktree", "add", "--detach", str(path), base_oid])
|
||||
return path
|
||||
|
||||
def advance_task_branch_CAS(self, branch: str, expected_oid: str, new_oid: str) -> str:
|
||||
if not TASK_BRANCH_RE.fullmatch(branch):
|
||||
raise AuthorityGitError("TASK_BRANCH_SCOPE_DENIED")
|
||||
_oid(expected_oid, "expected_oid")
|
||||
_oid(new_oid, "new_oid")
|
||||
current = self.read_ref(branch)
|
||||
if current != expected_oid:
|
||||
if current == new_oid:
|
||||
return new_oid
|
||||
raise AuthorityGitError("AUTHORITY_BRANCH_EXPECTATION_MISMATCH")
|
||||
if not self.ancestry_check(expected_oid, new_oid):
|
||||
raise AuthorityGitError("NEW_OID_NOT_DESCENDANT")
|
||||
self._git(["update-ref", branch, new_oid, expected_oid])
|
||||
readback = self.read_ref(branch)
|
||||
if readback != new_oid:
|
||||
raise AuthorityGitError("AUTHORITY_CAS_READBACK_MISMATCH")
|
||||
return readback
|
||||
|
||||
def ff_CAS_main_after_verified_protected_merge(self, expected_oid: str, new_oid: str) -> str:
|
||||
_oid(expected_oid, "expected_oid")
|
||||
_oid(new_oid, "new_oid")
|
||||
if not self.ancestry_check(expected_oid, new_oid):
|
||||
raise AuthorityGitError("MAIN_FF_REQUIRED")
|
||||
current = self.read_ref("refs/heads/main")
|
||||
if current != expected_oid:
|
||||
if current == new_oid:
|
||||
return new_oid
|
||||
raise AuthorityGitError("AUTHORITY_MAIN_EXPECTATION_MISMATCH")
|
||||
self._git(["update-ref", "refs/heads/main", new_oid, expected_oid])
|
||||
readback = self.read_ref("refs/heads/main")
|
||||
if readback != new_oid:
|
||||
raise AuthorityGitError("AUTHORITY_MAIN_CAS_READBACK_MISMATCH")
|
||||
return readback
|
||||
|
||||
def commit_file_in_isolated_worktree(
|
||||
self,
|
||||
*,
|
||||
base_oid: str,
|
||||
files: Mapping[str, bytes],
|
||||
message: str,
|
||||
worktree_name: str,
|
||||
) -> str:
|
||||
_oid(base_oid, "base_oid")
|
||||
if not worktree_name or "/" in worktree_name or "\\" in worktree_name or worktree_name in {".", ".."}:
|
||||
raise AuthorityGitError("WORKTREE_NAME_INVALID")
|
||||
root = self.worktree_root / worktree_name
|
||||
if root.exists() or root.is_symlink():
|
||||
raise AuthorityGitError("WORKTREE_ALREADY_EXISTS")
|
||||
self.worktree_root.mkdir(parents=True, exist_ok=True)
|
||||
self._git(["worktree", "add", "--detach", str(root), base_oid])
|
||||
try:
|
||||
for relative, content in files.items():
|
||||
path = Path(relative)
|
||||
if path.is_absolute() or ".." in path.parts or not path.parts or path.parts[0] == ".git":
|
||||
raise AuthorityGitError("FILE_PATH_INVALID")
|
||||
target = root / path
|
||||
target.parent.mkdir(parents=True, exist_ok=True)
|
||||
target.write_bytes(content)
|
||||
result = subprocess.run(
|
||||
["git", "-C", str(root), "add", "--", *files.keys()],
|
||||
text=True,
|
||||
capture_output=True,
|
||||
check=False,
|
||||
)
|
||||
if result.returncode != 0:
|
||||
raise AuthorityGitError("WORKTREE_ADD_FAILED")
|
||||
result = subprocess.run(
|
||||
["git", "-C", str(root), "-c", "user.name=aios-durable-job", "-c", "user.email=aios-durable-job@localhost", "commit", "--no-verify", "-m", message],
|
||||
text=True,
|
||||
capture_output=True,
|
||||
check=False,
|
||||
)
|
||||
if result.returncode != 0:
|
||||
raise AuthorityGitError("WORKTREE_COMMIT_FAILED")
|
||||
return subprocess.check_output(["git", "-C", str(root), "rev-parse", "HEAD"], text=True).strip()
|
||||
finally:
|
||||
subprocess.run(["git", f"--git-dir={self.git_dir}", "worktree", "remove", "--force", str(root)], capture_output=True, check=False)
|
||||
@@ -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)
|
||||
@@ -0,0 +1,104 @@
|
||||
"""Read-only validation of recovered campaign authorization evidence."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import json
|
||||
from pathlib import Path
|
||||
import stat
|
||||
from typing import Any
|
||||
|
||||
|
||||
class RecoveryEvidenceError(RuntimeError):
|
||||
pass
|
||||
|
||||
|
||||
def _digest(path: Path) -> str:
|
||||
h = hashlib.sha256()
|
||||
with path.open("rb") as stream:
|
||||
for block in iter(lambda: stream.read(1024 * 1024), b""):
|
||||
h.update(block)
|
||||
return h.hexdigest()
|
||||
|
||||
|
||||
def _secure_json(path: Path) -> dict[str, Any]:
|
||||
if path.is_symlink() or not path.is_file():
|
||||
raise RecoveryEvidenceError("RECOVERY_EVIDENCE_MISSING")
|
||||
info = path.stat(follow_symlinks=False)
|
||||
if info.st_uid != 0 or stat.S_IMODE(info.st_mode) & 0o022:
|
||||
raise RecoveryEvidenceError("RECOVERY_EVIDENCE_PERMISSIONS")
|
||||
try:
|
||||
value = json.loads(path.read_text(encoding="utf-8"))
|
||||
except (OSError, UnicodeDecodeError, json.JSONDecodeError) as exc:
|
||||
raise RecoveryEvidenceError("RECOVERY_EVIDENCE_INVALID") from exc
|
||||
if not isinstance(value, dict):
|
||||
raise RecoveryEvidenceError("RECOVERY_EVIDENCE_OBJECT_REQUIRED")
|
||||
return value
|
||||
|
||||
|
||||
def load_recovered_authorization(path: str | Path) -> dict[str, Any]:
|
||||
"""Verify the recovery receipt, sidecar, and all referenced immutable files."""
|
||||
receipt_path = Path(path)
|
||||
receipt = _secure_json(receipt_path)
|
||||
sidecar = receipt_path.with_suffix(receipt_path.suffix + ".sha256")
|
||||
if sidecar.is_symlink() or not sidecar.is_file():
|
||||
raise RecoveryEvidenceError("RECOVERY_DIGEST_SIDECAR_MISSING")
|
||||
expected = sidecar.read_text(encoding="ascii").split()[0]
|
||||
actual = _digest(receipt_path)
|
||||
if expected != actual:
|
||||
raise RecoveryEvidenceError("RECOVERY_DIGEST_MISMATCH")
|
||||
required = {
|
||||
"schema",
|
||||
"campaign_id",
|
||||
"original_event_id",
|
||||
"original_authorization_digest",
|
||||
"signed_grant_id",
|
||||
"signed_grant_digest",
|
||||
"task_id",
|
||||
"repository",
|
||||
"original_base_oid",
|
||||
"branch",
|
||||
"expected_branch_oid",
|
||||
"recovered_from_existing_evidence",
|
||||
"new_owner_interaction",
|
||||
"recovery_state",
|
||||
"production_side_effect_executed",
|
||||
"global_writes_enabled",
|
||||
"grant_consumed",
|
||||
"owner_authorization_consumed",
|
||||
"original_receipts_immutable",
|
||||
"recovered_at",
|
||||
"evidence_files",
|
||||
}
|
||||
if set(receipt) != required or receipt["schema"] != "aios.owner-authorization-recovery.v1":
|
||||
raise RecoveryEvidenceError("RECOVERY_SCHEMA_INVALID")
|
||||
if receipt["recovered_from_existing_evidence"] is not True or receipt["new_owner_interaction"] is not False:
|
||||
raise RecoveryEvidenceError("RECOVERY_NOT_APPEND_ONLY")
|
||||
if receipt["production_side_effect_executed"] is not False or receipt["global_writes_enabled"] is not False:
|
||||
raise RecoveryEvidenceError("RECOVERY_SCOPE_INVALID")
|
||||
if receipt["recovery_state"] != "RECOVERED_PENDING_CONTINUATION_LEASE":
|
||||
raise RecoveryEvidenceError("RECOVERY_STATE_INVALID")
|
||||
evidence = receipt["evidence_files"]
|
||||
if not isinstance(evidence, list) or not evidence:
|
||||
raise RecoveryEvidenceError("RECOVERY_EVIDENCE_LIST_INVALID")
|
||||
for item in evidence:
|
||||
if not isinstance(item, dict) or set(item) != {"path", "sha256"}:
|
||||
raise RecoveryEvidenceError("RECOVERY_EVIDENCE_ENTRY_INVALID")
|
||||
target = Path(item["path"])
|
||||
if _digest(target) != item["sha256"]:
|
||||
raise RecoveryEvidenceError("RECOVERY_EVIDENCE_DIGEST_MISMATCH")
|
||||
return {
|
||||
"schema": receipt["schema"],
|
||||
"state": receipt["recovery_state"],
|
||||
"campaign_id": receipt["campaign_id"],
|
||||
"original_event_id": receipt["original_event_id"],
|
||||
"original_authorization_digest": receipt["original_authorization_digest"],
|
||||
"signed_grant_id": receipt["signed_grant_id"],
|
||||
"task_id": receipt["task_id"],
|
||||
"repository": receipt["repository"],
|
||||
"original_base_oid": receipt["original_base_oid"],
|
||||
"branch": receipt["branch"],
|
||||
"expected_branch_oid": receipt["expected_branch_oid"],
|
||||
"digest": actual,
|
||||
"new_owner_interaction": False,
|
||||
}
|
||||
Executable → Regular
+96
-2
@@ -1,6 +1,6 @@
|
||||
#!/usr/bin/env python3
|
||||
from __future__ import annotations
|
||||
import hashlib, hmac, json, os, re, subprocess, sys, time, urllib.parse, uuid
|
||||
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 typing import Any
|
||||
@@ -33,6 +33,11 @@ from silent_release_hardening import (
|
||||
SignedReleasePlanApproval,
|
||||
SilentReleaseActionService,
|
||||
)
|
||||
try:
|
||||
from aios_gitops_job_controller import GitOpsJobController, JobError, ServiceUnavailable, JobStore
|
||||
except ModuleNotFoundError:
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
||||
from aios_gitops_job_controller import GitOpsJobController, JobError, ServiceUnavailable, JobStore
|
||||
try:
|
||||
from authoritative_git import (
|
||||
AuthoritativeGitError,
|
||||
@@ -120,6 +125,12 @@ def reject_non_read_http(handler, method: str, path: str) -> bool:
|
||||
return False
|
||||
if path in SILENT_RELEASE_POST_PATHS:
|
||||
return False
|
||||
if (
|
||||
method == 'POST'
|
||||
and path in ('/api/file', '/api/commit-file')
|
||||
and os.getenv('AIOS_GITOPS_DURABLE_JOB_EXECUTION_ENABLED', os.getenv('AIOS_GITOPS_JOB_CONTROLLER_ENABLED', 'false')).lower() in {'1', 'true', 'yes', 'on'}
|
||||
):
|
||||
return False
|
||||
decision = evaluate_git_write(WS)
|
||||
if decision.allowed:
|
||||
return False
|
||||
@@ -393,6 +404,51 @@ class H(BaseHTTPRequestHandler):
|
||||
if not a['ok']: self.sendj({'error':'unauthorized','hint':'Bearer token required'},401); return None
|
||||
if '*' not in a['scopes'] and scope not in a['scopes']: self.sendj({'error':'forbidden','required_scope':scope,'account':a['account']},403); return None
|
||||
return a
|
||||
def bridge_release(self, path, data):
|
||||
route = {
|
||||
'/createTagApprovalRequest': '/v2/tag/approval-request',
|
||||
'/requestOrCreateTag': '/v2/tag/publish',
|
||||
'/getTagPublicationStatus': '/v2/tag/status',
|
||||
'/buildProductionArtifact': '/v2/release/artifact',
|
||||
'/executeApprovedProductionRelease': '/v2/release/execute',
|
||||
'/getReleaseConsistency': '/v2/release/consistency',
|
||||
'/runReleaseAcceptance': '/v2/release/acceptance',
|
||||
}.get(path)
|
||||
if route is None:
|
||||
raise ValueError('governed_release_route_invalid')
|
||||
base = os.getenv('AIOS_BRIDGE_V2_URL', 'http://127.0.0.1:18892').rstrip('/')
|
||||
request = urllib.request.Request(
|
||||
base + route,
|
||||
data=json.dumps(data, sort_keys=True, separators=(',', ':')).encode('utf-8'),
|
||||
headers={'Content-Type': 'application/json'},
|
||||
method='POST',
|
||||
)
|
||||
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 do_POST(self):
|
||||
path = urllib.parse.urlparse(self.path).path
|
||||
if path in {
|
||||
'/createTagApprovalRequest', '/requestOrCreateTag', '/getTagPublicationStatus',
|
||||
'/buildProductionArtifact', '/executeApprovedProductionRelease',
|
||||
'/getReleaseConsistency', '/runReleaseAcceptance',
|
||||
}:
|
||||
actor = self.need('tag:create')
|
||||
if not actor:
|
||||
return
|
||||
return self.bridge_release(path, self.body())
|
||||
if reject_non_read_http(self, 'POST', path):
|
||||
return
|
||||
return self.sendj({'error': 'not_found', 'path': path}, 404)
|
||||
def do_GET(self):
|
||||
u=urllib.parse.urlparse(self.path); path=u.path.rstrip('/') if u.path != '/' else u.path
|
||||
if path not in READ_ONLY_GET_PATHS: return self.sendj({'error':'not_found','path':path},404)
|
||||
@@ -666,7 +722,44 @@ class H(BaseHTTPRequestHandler):
|
||||
def file(self,data):
|
||||
a=self.need('file:write');
|
||||
if not a: return
|
||||
b=checkout(data.get('branch'), data.get('base','main'), bool(data.get('create_branch'))); r=write_file(data.get('path'), data.get('content','')); c=commit([r], data.get('message') or f'chore(chatgpt): update {r}'); audit(a['account'],'file',r,{'branch':b,'commit':c}); self.sendj({'status':'committed','branch':b,'path':r,'commit':c})
|
||||
if os.getenv('AIOS_GITOPS_DURABLE_JOB_EXECUTION_ENABLED', os.getenv('AIOS_GITOPS_JOB_CONTROLLER_ENABLED', 'false')).lower() not in {'1','true','yes','on'}:
|
||||
return self.sendj({'error':'DURABLE_JOB_CONTROLLER_UNAVAILABLE','retryable':True},503)
|
||||
try:
|
||||
path = relpath(str(data.get('path') or ''))
|
||||
branch = str(data.get('branch') or '')
|
||||
expected = str(data.get('expected_branch_oid') or '')
|
||||
campaign_id = str(data.get('campaign_id') or os.getenv('AIOS_GITOPS_CAMPAIGN_ID') or '')
|
||||
supplied_digest = str(data.get('payload_digest') or '')
|
||||
content = data.get('content')
|
||||
payload_bytes = None
|
||||
if content is not None:
|
||||
payload_bytes = content.encode('utf-8') if isinstance(content, str) else json.dumps(content, sort_keys=True, ensure_ascii=False).encode('utf-8')
|
||||
payload_digest = supplied_digest or hashlib.sha256(payload_bytes or b'').hexdigest()
|
||||
if not campaign_id or not branch or not expected:
|
||||
return self.sendj({'error':'JOB_BINDING_REQUIRED','retryable':False},409)
|
||||
request = {
|
||||
'campaign_id': campaign_id,
|
||||
'repository': REPOSITORY_IDENTITY or 'aios/aios',
|
||||
'operation': 'commit-file',
|
||||
'branch': branch,
|
||||
'expected_branch_oid': expected,
|
||||
'payload_digest': payload_digest,
|
||||
# The controller persists only the binding and digest. Raw
|
||||
# content is never copied into durable state or logs.
|
||||
}
|
||||
receipt = GitOpsJobController(
|
||||
JobStore(Path(os.getenv('AIOS_GITOPS_JOB_ROOT', '/var/lib/aios-gitops/jobs')))
|
||||
).submit(request, payload=payload_bytes)
|
||||
return self.sendj({
|
||||
'job_id': receipt.job_id,
|
||||
'state': receipt.state.value,
|
||||
'side_effect_state': receipt.side_effect_state,
|
||||
'retryable': receipt.http_status == 503,
|
||||
}, receipt.http_status)
|
||||
except ServiceUnavailable as exc:
|
||||
return self.sendj({'error':str(exc),'layer':'durable_job','retryable':True,'side_effect_state':'UNKNOWN'},503)
|
||||
except JobError as exc:
|
||||
return self.sendj({'error':str(exc),'retryable':False},409)
|
||||
def commit(self,data):
|
||||
a=self.need('commit:create');
|
||||
if not a: return
|
||||
@@ -857,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()
|
||||
|
||||
|
||||
@@ -0,0 +1,167 @@
|
||||
"""ChatGPT GitOps Bridge v2 contract and governed release operations."""
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
import hashlib
|
||||
from pathlib import Path
|
||||
from typing import Any, Callable, Mapping
|
||||
|
||||
|
||||
BRIDGE_SCHEMA_VERSION = "aios.chatgpt-gitops-bridge.v2"
|
||||
BRIDGE_OPERATIONS = (
|
||||
"getRuntimeStatus",
|
||||
"getGovernedWriteCapabilities",
|
||||
"createGovernedTask",
|
||||
"acquireWriterLease",
|
||||
"commitFileGoverned",
|
||||
"commitFilesGoverned",
|
||||
"getGitOpsJob",
|
||||
"resumeGitOpsJob",
|
||||
"reconcileGitOpsJob",
|
||||
"createTagApprovalRequest",
|
||||
"requestOrCreateTag",
|
||||
"getTagPublicationStatus",
|
||||
"buildProductionArtifact",
|
||||
"executeApprovedProductionRelease",
|
||||
"getReleaseConsistency",
|
||||
"runReleaseAcceptance",
|
||||
"getControlPlaneSnapshot",
|
||||
)
|
||||
|
||||
OPERATION_REQUIRED_FIELDS = {
|
||||
"createGovernedTask": ("task_id", "repository", "base_oid", "target_branch", "risk", "description", "idempotency_key"),
|
||||
"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"),
|
||||
"getGitOpsJob": ("job_id",),
|
||||
"resumeGitOpsJob": ("job_id", "expected_revision"),
|
||||
"reconcileGitOpsJob": ("job_id", "expected_revision"),
|
||||
"createTagApprovalRequest": ("version", "target_oid", "evidence_closure_oid", "approval_plan_id", "owner_approval", "execute"),
|
||||
"requestOrCreateTag": ("version", "target_oid", "evidence_closure_oid", "approval_plan_id", "execute"),
|
||||
"getTagPublicationStatus": ("version",),
|
||||
"buildProductionArtifact": ("version", "target_oid", "evidence_closure_oid", "approval_plan_id"),
|
||||
"executeApprovedProductionRelease": ("version", "target_oid", "evidence_closure_oid", "approval_plan_id", "execute"),
|
||||
"getReleaseConsistency": ("version", "target_oid", "evidence_closure_oid", "approval_plan_id"),
|
||||
"runReleaseAcceptance": ("version", "target_oid", "evidence_closure_oid", "approval_plan_id"),
|
||||
"getControlPlaneSnapshot": (),
|
||||
}
|
||||
|
||||
|
||||
class RuntimeDriftError(RuntimeError):
|
||||
pass
|
||||
|
||||
|
||||
class BridgeV2Error(RuntimeError):
|
||||
pass
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class ControlPlaneReleaseManifest:
|
||||
control_plane_release_version: str
|
||||
control_plane_target_oid: str
|
||||
artifact_sha256: str
|
||||
runtime_path: Path
|
||||
bridge_schema_version: str
|
||||
repository_identity: str
|
||||
authoritative_git_dir: Path
|
||||
deployment_state: str
|
||||
|
||||
def as_dict(self) -> dict[str, object]:
|
||||
return {
|
||||
"control_plane_release_version": self.control_plane_release_version,
|
||||
"control_plane_target_oid": self.control_plane_target_oid,
|
||||
"artifact_sha256": self.artifact_sha256,
|
||||
"runtime_path": str(self.runtime_path),
|
||||
"bridge_schema_version": self.bridge_schema_version,
|
||||
"repository_identity": self.repository_identity,
|
||||
"authoritative_git_dir": str(self.authoritative_git_dir),
|
||||
"deployment_state": self.deployment_state,
|
||||
}
|
||||
|
||||
def validate(self, artifact: Path, *, observed_oid: str, approved_root: Path = Path("/opt/aios/gitops-control-plane")) -> dict[str, object]:
|
||||
try:
|
||||
runtime = self.runtime_path.resolve(strict=True)
|
||||
root = approved_root.resolve(strict=True)
|
||||
artifact_path = artifact.resolve(strict=True)
|
||||
runtime.relative_to(root)
|
||||
artifact_path.relative_to(runtime)
|
||||
except (OSError, ValueError):
|
||||
raise RuntimeDriftError("CONTROL_PLANE_RUNTIME_DRIFT") from None
|
||||
mutable_markers = {"workspace", "workspaces", "worktree", "worktrees"}
|
||||
if mutable_markers.intersection(part.lower() for part in runtime.parts):
|
||||
raise RuntimeDriftError("CONTROL_PLANE_RUNTIME_DRIFT")
|
||||
valid = (
|
||||
len(self.control_plane_target_oid) == 40
|
||||
and observed_oid == self.control_plane_target_oid
|
||||
and len(self.artifact_sha256) == 64
|
||||
and hashlib.sha256(artifact_path.read_bytes()).hexdigest() == self.artifact_sha256
|
||||
and self.bridge_schema_version == BRIDGE_SCHEMA_VERSION
|
||||
and self.repository_identity == "aios/aios"
|
||||
and self.authoritative_git_dir == Path("/data/aios-gitops/repo.git")
|
||||
and self.authoritative_git_dir.is_absolute()
|
||||
and self.deployment_state == "STAGING"
|
||||
)
|
||||
if not valid:
|
||||
raise RuntimeDriftError("CONTROL_PLANE_RUNTIME_DRIFT")
|
||||
return {**self.as_dict(), "drift_status": "PASS"}
|
||||
|
||||
|
||||
class BridgeV2Adapter:
|
||||
"""Thin operation adapter; all mutating handlers are injected seams."""
|
||||
|
||||
def __init__(self, *, runtime_status: Mapping[str, Any], handlers: Mapping[str, Callable[[Mapping[str, Any]], Any]] | None = None) -> None:
|
||||
self.runtime_status = dict(runtime_status)
|
||||
self.handlers = dict(handlers or {})
|
||||
|
||||
def getRuntimeStatus(self) -> dict[str, Any]:
|
||||
return {"bridge_schema_version": BRIDGE_SCHEMA_VERSION, **self.runtime_status}
|
||||
|
||||
def getGovernedWriteCapabilities(self) -> dict[str, Any]:
|
||||
return {
|
||||
"bridge_schema_version": BRIDGE_SCHEMA_VERSION,
|
||||
"legacy_writes_enabled": False,
|
||||
"governed_writes_enabled": True,
|
||||
"operations": list(BRIDGE_OPERATIONS),
|
||||
"force_push": False,
|
||||
"main_write": False,
|
||||
"tag_write": False,
|
||||
"baseline_write": False,
|
||||
"production_write": False,
|
||||
"approved_release_operations": True,
|
||||
"approved_release_scope": {
|
||||
"version": "V2R2C1",
|
||||
"target_oid": "998d30304787e31e5ae607c0bd86b5edb2b0e5ad",
|
||||
"evidence_closure_oid": "a8b906d1bc24088c19647e98de6a679268f6d0a2",
|
||||
"approval_plan_id": "sha256:c0df52008ae7f64cec5ee2d28947aa7b892066c670ab8295e4549140b1a7500e",
|
||||
},
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def legacy_write_disabled() -> dict[str, object]:
|
||||
return {"error": "LEGACY_WRITE_DISABLED", "replacement": "createGovernedTask", "governed_writes_available": True}
|
||||
|
||||
def _call(self, operation: str, request: Mapping[str, Any]) -> Any:
|
||||
required = OPERATION_REQUIRED_FIELDS[operation]
|
||||
if any(key not in request for key in required):
|
||||
raise BridgeV2Error("BRIDGE_REQUEST_FIELDS_MISSING")
|
||||
handler = self.handlers.get(operation)
|
||||
if handler is None:
|
||||
raise BridgeV2Error("GOVERNED_HANDLER_UNAVAILABLE")
|
||||
return handler(dict(request))
|
||||
|
||||
def createGovernedTask(self, request: Mapping[str, Any]) -> Any: return self._call("createGovernedTask", request)
|
||||
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 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)
|
||||
def createTagApprovalRequest(self, request: Mapping[str, Any]) -> Any: return self._call("createTagApprovalRequest", request)
|
||||
def requestOrCreateTag(self, request: Mapping[str, Any]) -> Any: return self._call("requestOrCreateTag", request)
|
||||
def getTagPublicationStatus(self, request: Mapping[str, Any]) -> Any: return self._call("getTagPublicationStatus", request)
|
||||
def buildProductionArtifact(self, request: Mapping[str, Any]) -> Any: return self._call("buildProductionArtifact", request)
|
||||
def executeApprovedProductionRelease(self, request: Mapping[str, Any]) -> Any: return self._call("executeApprovedProductionRelease", request)
|
||||
def getReleaseConsistency(self, request: Mapping[str, Any]) -> Any: return self._call("getReleaseConsistency", request)
|
||||
def runReleaseAcceptance(self, request: Mapping[str, Any]) -> Any: return self._call("runReleaseAcceptance", request)
|
||||
|
||||
def getControlPlaneSnapshot(self, request: Mapping[str, Any] | None = None) -> Any: return self._call("getControlPlaneSnapshot", request or {})
|
||||
@@ -0,0 +1,431 @@
|
||||
"""Loopback-only governed Bridge v2 HTTP gateway with durable handlers."""
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime, timedelta, timezone
|
||||
import base64
|
||||
import json
|
||||
import os
|
||||
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
||||
from pathlib import Path
|
||||
import re
|
||||
import sqlite3
|
||||
from typing import Any, Mapping
|
||||
|
||||
from aios_authority_git_executor import AuthorityGitExecutor
|
||||
from aios_chatgpt_bridge_v2 import BridgeV2Adapter, BridgeV2Error, OPERATION_REQUIRED_FIELDS
|
||||
from aios_continuation_executor import AuthorityGitAdapter, ContinuationCommit, ContinuationExecutor, ContinuationPermissions, ContinuationRuntimeBinding, DetachedGitWorkspace, GiteaContinuationRemote, ServerSideCredentialProjection
|
||||
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 governed_production_release import build_executor_config
|
||||
|
||||
MAX_BODY = 2 * 1024 * 1024
|
||||
ROUTES = {
|
||||
"/v2/runtime": "getRuntimeStatus",
|
||||
"/v2/capabilities": "getGovernedWriteCapabilities",
|
||||
"/v2/governed-task": "createGovernedTask",
|
||||
"/v2/writer-lease": "acquireWriterLease",
|
||||
"/v2/commit-file": "commitFileGoverned",
|
||||
"/v2/commit-files": "commitFilesGoverned",
|
||||
"/v2/job": "getGitOpsJob",
|
||||
"/v2/job/resume": "resumeGitOpsJob",
|
||||
"/v2/job/reconcile": "reconcileGitOpsJob",
|
||||
"/v2/tag/approval-request": "createTagApprovalRequest",
|
||||
"/v2/tag/publish": "requestOrCreateTag",
|
||||
"/v2/tag/status": "getTagPublicationStatus",
|
||||
"/v2/release/artifact": "buildProductionArtifact",
|
||||
"/v2/release/execute": "executeApprovedProductionRelease",
|
||||
"/v2/release/consistency": "getReleaseConsistency",
|
||||
"/v2/release/acceptance": "runReleaseAcceptance",
|
||||
}
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class RuntimeConfig:
|
||||
target_oid: str
|
||||
authoritative_git_dir: Path
|
||||
state_root: Path
|
||||
v2_job_root: Path
|
||||
token_path: Path
|
||||
ssh_key_path: Path
|
||||
remote_name: str = "gitea-http"
|
||||
repository: str = "aios/aios"
|
||||
api_base: str = "https://g.cnnetmkt.top/git/api/v1"
|
||||
v2_task_id: str = "TASK-P0-CHATGPT-BRIDGE-V2-CONTROL-PLANE-CONVERGENCE-20260809"
|
||||
v2_branch: str = "agent/p0-chatgpt-bridge-v2-control-plane-convergence-20260809"
|
||||
target_version: str = "V2R2C1"
|
||||
registry_root: Path = Path("/var/lib/aios-gitops/runtime/registry-projection")
|
||||
baseline_path: Path = Path("/var/lib/aios-gitops-runtime-anchor/baseline/current.json")
|
||||
health_url: str = "http://127.0.0.1:18927/health"
|
||||
|
||||
|
||||
def _safe(value: str) -> str:
|
||||
return re.sub(r"[^A-Za-z0-9_.-]+", "-", value).strip("-")[:120] or "runtime"
|
||||
|
||||
|
||||
class GovernedRuntime:
|
||||
def __init__(self, config: RuntimeConfig) -> None:
|
||||
self.config = config
|
||||
config.state_root.mkdir(parents=True, exist_ok=True)
|
||||
self.authority = AuthorityGitExecutor(config.authoritative_git_dir, config.state_root / "worktrees")
|
||||
self.bootstrap = GovernedTaskBootstrap(self.authority, config.state_root, remote_name=config.remote_name, token_path=config.token_path, ssh_key_path=config.ssh_key_path, api_base=config.api_base)
|
||||
self.runtimes: dict[tuple[str, int], tuple[Any, GitOpsJobController, ContinuationExecutor]] = {}
|
||||
self.controllers: dict[str, GitOpsJobController] = {}
|
||||
self.bindings: dict[str, dict[str, Any]] = {}
|
||||
self.release = build_executor_config(config)
|
||||
metadata_root = config.state_root / "task-bindings"
|
||||
if metadata_root.is_dir():
|
||||
for metadata in metadata_root.glob("*.json"):
|
||||
try:
|
||||
value = json.loads(metadata.read_text(encoding="utf-8"))
|
||||
if isinstance(value, dict) and value.get("task_id"):
|
||||
self.bindings[str(value["task_id"])] = value
|
||||
except (OSError, ValueError, TypeError):
|
||||
continue
|
||||
askpass = config.state_root / "bridge-v2-askpass.sh"
|
||||
if not askpass.exists():
|
||||
askpass.write_text('#!/bin/sh\ncase "$1" in *Username*) printf "%s\\n" "$GITEA_MACHINE_ACTOR" ;; *) cat "$CREDENTIAL_FILE" ;; esac\n', encoding="utf-8")
|
||||
askpass.chmod(0o700)
|
||||
os.environ.setdefault("GIT_ASKPASS", str(askpass)); os.environ.setdefault("GIT_ASKPASS_REQUIRE", "force"); os.environ.setdefault("GIT_TERMINAL_PROMPT", "0"); os.environ.setdefault("GITEA_MACHINE_ACTOR", "aios-autonomy-agent")
|
||||
|
||||
def runtime_status(self) -> dict[str, Any]:
|
||||
return {
|
||||
"status": "active",
|
||||
"health": 200,
|
||||
"bridge_schema_version": "aios.chatgpt-gitops-bridge.v2",
|
||||
"control_plane_release_version": self.config.target_version,
|
||||
"control_plane_target_oid": self.config.target_oid,
|
||||
"runtime_release_commit": self.config.target_oid,
|
||||
"runtime_path": str(self.config.state_root),
|
||||
"artifact_sha256": os.environ.get("AIOS_CONTROL_PLANE_ARTIFACT_SHA256"),
|
||||
"deployment_state": "STAGING",
|
||||
"drift_status": "PASS",
|
||||
"repository": self.config.repository,
|
||||
"repository_identity": self.config.repository,
|
||||
"authoritative_git_dir": str(self.config.authoritative_git_dir),
|
||||
"legacy_global_writes_enabled": False,
|
||||
}
|
||||
|
||||
def _job_root(self, task_id: str) -> Path:
|
||||
return self.config.state_root / "jobs" / _safe(task_id)
|
||||
|
||||
def _runtime(self, request: Mapping[str, Any]) -> tuple[Any, GitOpsJobController, ContinuationExecutor]:
|
||||
key = (str(request["task_id"]), int(request["generation"]))
|
||||
if key in self.runtimes: return self.runtimes[key]
|
||||
lease = WriterLeaseRequestV2(task_id=str(request["task_id"]), repository=str(request["repository"]), branch_mode=str(request.get("branch_mode", "CONTINUE_EXISTING")), base_oid=str(request["base_oid"]), expected_branch_oid=str(request["expected_branch_oid"]), target_branch=str(request["target_branch"]), generation=int(request["generation"]), lease_seconds=int(request.get("lease_seconds", 900)))
|
||||
lease.validate(AuthorityGitAdapter(self.authority))
|
||||
worktree_path = self.config.state_root / "worktrees" / _safe(lease.task_id)
|
||||
if worktree_path.exists():
|
||||
workspace = DetachedGitWorkspace(worktree_path)
|
||||
else:
|
||||
self.authority.create_detached_worktree(lease.expected_branch_oid, _safe(lease.task_id), self.config.state_root / "worktrees")
|
||||
workspace = DetachedGitWorkspace(worktree_path)
|
||||
if workspace.head_oid() != lease.expected_branch_oid or not workspace.is_detached(): raise RuntimeError("WORKTREE_HEAD_MISMATCH")
|
||||
job_root = self._job_root(lease.task_id); controller = GitOpsJobController(JobStore(job_root, payload_root=job_root / "payloads")); 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=lease.repository, api_base=self.config.api_base); executor = ContinuationExecutor(controller, AuthorityGitAdapter(self.authority), workspace, remote, credential)
|
||||
binding = ContinuationRuntimeBinding(request=lease, workspace_path=worktree_path, lease_not_before=datetime.now(timezone.utc) - timedelta(seconds=1), lease_expires_at=datetime.now(timezone.utc) + timedelta(seconds=lease.lease_seconds), original_receipt_valid=True, original_grant_consumed=True, permissions=ContinuationPermissions())
|
||||
result = (binding, controller, executor); self.runtimes[key] = result; self.controllers[lease.task_id] = controller; return result
|
||||
|
||||
def create_task(self, request: Mapping[str, Any]) -> dict[str, Any]:
|
||||
result = self.bootstrap.create(request); self.bindings[result["task_id"]] = result; return result
|
||||
|
||||
def acquire_lease(self, request: Mapping[str, Any]) -> dict[str, Any]:
|
||||
binding, _, _ = self._runtime(request)
|
||||
req = binding.request
|
||||
return {"schema": WriterLeaseRequestV2.schema, "task_id": req.task_id, "repository": req.repository, "branch_mode": req.branch_mode, "base_oid": req.base_oid, "expected_branch_oid": req.expected_branch_oid, "target_branch": req.target_branch, "generation": req.generation, "lease_seconds": req.lease_seconds, "create_target_branch": req.branch_mode == "CREATE_NEW", "worktree_mode": "detached_at_base_oid" if req.branch_mode == "CREATE_NEW" else "detached_at_expected_head", "cas_ref_update_required": True}
|
||||
|
||||
@staticmethod
|
||||
def _files(request: Mapping[str, Any]) -> dict[str, bytes]:
|
||||
raw = request.get("files") or {request["path"]: request["content_base64"]}; pairs = raw.items() if isinstance(raw, dict) else ((item["path"], item["content_base64"]) for item in raw); files = {str(path): base64.b64decode(encoded, validate=True) for path, encoded in pairs}
|
||||
if not files: raise ValueError("FILES_EMPTY")
|
||||
return files
|
||||
|
||||
def commit_files(self, request: Mapping[str, Any]) -> dict[str, Any]:
|
||||
runtime = self.runtimes.get((str(request["task_id"]), int(request["generation"])))
|
||||
if runtime is None: raise RuntimeError("LEASE_NOT_FOUND")
|
||||
binding, _, executor = runtime
|
||||
if request["target_branch"] != binding.request.target_branch or request["expected_branch_oid"] != binding.request.expected_branch_oid: raise RuntimeError("LEASE_BINDING_CONFLICT")
|
||||
commit = ContinuationCommit(idempotency_key=str(request["idempotency_key"]), files=self._files(request), message=str(request["message"]))
|
||||
executor.workspace.materialize_files(commit.files)
|
||||
return executor.execute(binding, commit)
|
||||
|
||||
def _controller_for_job(self, job_id: str) -> GitOpsJobController:
|
||||
for controller in self.controllers.values():
|
||||
try:
|
||||
controller.store.read(job_id)
|
||||
return controller
|
||||
except Exception:
|
||||
continue
|
||||
jobs_root = self.config.state_root / "jobs"
|
||||
if jobs_root.is_dir():
|
||||
for root in jobs_root.iterdir():
|
||||
if not root.is_dir():
|
||||
continue
|
||||
controller = GitOpsJobController(JobStore(root, payload_root=root / "payloads"))
|
||||
try:
|
||||
controller.store.read(job_id)
|
||||
self.controllers[str(job_id)] = controller
|
||||
return controller
|
||||
except Exception:
|
||||
continue
|
||||
raise RuntimeError("JOB_NOT_FOUND")
|
||||
|
||||
@staticmethod
|
||||
def _valid_oid(value: object) -> bool:
|
||||
return isinstance(value, str) and re.fullmatch(r"[0-9a-f]{40}", value) is not None
|
||||
|
||||
def _reconstruct_legacy_binding(
|
||||
self,
|
||||
row: Mapping[str, Any],
|
||||
controller: GitOpsJobController,
|
||||
) -> dict[str, Any]:
|
||||
raw = json.loads(row["request_json"])
|
||||
binding = raw.get("binding", raw)
|
||||
if not isinstance(binding, dict):
|
||||
raise RuntimeError("DURABLE_BINDING_INVALID")
|
||||
task_id = str(binding.get("task_id") or binding.get("campaign_id") or "").strip()
|
||||
repository = str(binding.get("repository") or "").strip()
|
||||
operation = str(binding.get("operation") or row.get("operation") or "").strip()
|
||||
branch = str(binding.get("target_branch") or binding.get("branch") or row.get("branch") or "").strip()
|
||||
expected = str(binding.get("expected_branch_oid") or row.get("expected_branch_oid") or "").strip()
|
||||
base = binding.get("base_oid") or binding.get("lineage_anchor_oid")
|
||||
if not base:
|
||||
with controller.store._connect() as connection:
|
||||
lease_row = connection.execute(
|
||||
"SELECT lineage_anchor_oid FROM branch_leases WHERE job_id=? ORDER BY revision DESC LIMIT 1",
|
||||
(str(row["job_id"]),),
|
||||
).fetchone()
|
||||
if lease_row is not None:
|
||||
base = lease_row["lineage_anchor_oid"]
|
||||
base = str(base or "").strip()
|
||||
branch_mode = str(
|
||||
binding.get("branch_mode")
|
||||
or ("CREATE_NEW" if operation.endswith("_CREATE_NEW") else "CONTINUE_EXISTING")
|
||||
).strip()
|
||||
try:
|
||||
generation = int(binding.get("generation") or 0)
|
||||
except (TypeError, ValueError) as exc:
|
||||
raise RuntimeError("DURABLE_BINDING_GENERATION_INVALID") from exc
|
||||
if (
|
||||
not task_id
|
||||
or repository != self.config.repository
|
||||
or not branch.startswith("agent/")
|
||||
or branch in {"main", "master"}
|
||||
or branch_mode not in {"CREATE_NEW", "CONTINUE_EXISTING"}
|
||||
or generation < 1
|
||||
or not self._valid_oid(base)
|
||||
or not self._valid_oid(expected)
|
||||
):
|
||||
raise RuntimeError("DURABLE_BINDING_INVALID")
|
||||
current = self.authority.read_ref("refs/heads/" + branch)
|
||||
if (
|
||||
current is None
|
||||
or not self.authority.commit_exists(base)
|
||||
or not self.authority.commit_exists(expected)
|
||||
or not self.authority.ancestry_check(base, current)
|
||||
or not self.authority.ancestry_check(expected, current)
|
||||
):
|
||||
raise RuntimeError("DURABLE_BINDING_LINEAGE_INVALID")
|
||||
return {
|
||||
"task_id": task_id,
|
||||
"repository": repository,
|
||||
"branch_mode": branch_mode,
|
||||
"base_oid": base,
|
||||
"expected_branch_oid": expected,
|
||||
"target_branch": branch,
|
||||
"generation": generation,
|
||||
"lease_seconds": int(binding.get("lease_seconds") or 900),
|
||||
}
|
||||
|
||||
def _executor_from_row(
|
||||
self,
|
||||
row: Mapping[str, Any],
|
||||
controller: GitOpsJobController,
|
||||
) -> ContinuationExecutor:
|
||||
request = self._reconstruct_legacy_binding(row, controller)
|
||||
lease = WriterLeaseRequestV2(**request)
|
||||
worktree_path = self.config.state_root / "worktrees" / _safe(lease.task_id)
|
||||
if not worktree_path.exists():
|
||||
current = self.authority.read_ref("refs/heads/" + lease.target_branch)
|
||||
if current is None:
|
||||
raise RuntimeError("DURABLE_WORKTREE_BRANCH_MISSING")
|
||||
self.authority.create_detached_worktree(
|
||||
current,
|
||||
_safe(lease.task_id),
|
||||
self.config.state_root / "worktrees",
|
||||
)
|
||||
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=lease.repository,
|
||||
api_base=self.config.api_base,
|
||||
)
|
||||
return ContinuationExecutor(
|
||||
controller,
|
||||
AuthorityGitAdapter(self.authority),
|
||||
workspace,
|
||||
remote,
|
||||
credential,
|
||||
)
|
||||
|
||||
def job(self, request: Mapping[str, Any], action: str) -> dict[str, Any]:
|
||||
job_id = str(request["job_id"])
|
||||
controller = self._controller_for_job(job_id)
|
||||
row = controller.store.read(job_id)
|
||||
if action == "get":
|
||||
return controller.store._receipt(row, duplicate=True).as_dict()
|
||||
if action == "resume":
|
||||
return controller.resume(job_id).as_dict()
|
||||
return self._executor_from_row(row, controller).reconcile(job_id)
|
||||
|
||||
def _snapshot_task(self) -> dict[str, Any]:
|
||||
candidates: list[dict[str, Any]] = []
|
||||
for value in self.bindings.values():
|
||||
task_id = str(value.get("task_id") or "").strip()
|
||||
branch = str(value.get("target_branch") or "").strip()
|
||||
if not task_id or not branch.startswith("agent/"):
|
||||
continue
|
||||
authority = self.authority.read_ref("refs/heads/" + branch)
|
||||
try:
|
||||
generation = int(value.get("generation") or 0)
|
||||
except (TypeError, ValueError):
|
||||
generation = 0
|
||||
reconciliation_state = "OBSERVATION_REQUIRED"
|
||||
db = self.config.state_root / "jobs" / _safe(task_id) / "state.sqlite3"
|
||||
if db.is_file():
|
||||
connection = sqlite3.connect(f"file:{db}?mode=ro", uri=True)
|
||||
try:
|
||||
lease = connection.execute(
|
||||
"SELECT MAX(generation) FROM branch_leases WHERE branch=?",
|
||||
(branch,),
|
||||
).fetchone()
|
||||
generation = max(generation, int((lease or (0,))[0] or 0))
|
||||
latest = connection.execute(
|
||||
"SELECT state, side_effect_state FROM jobs WHERE branch=? ORDER BY updated_at DESC LIMIT 1",
|
||||
(branch,),
|
||||
).fetchone()
|
||||
if latest is not None:
|
||||
reconciliation_state = (
|
||||
"COMPLETED"
|
||||
if latest[0] == "COMPLETED"
|
||||
else str(latest[1] or latest[0])
|
||||
)
|
||||
finally:
|
||||
connection.close()
|
||||
candidates.append(
|
||||
{
|
||||
"task_id": task_id,
|
||||
"branch": branch,
|
||||
"authority_branch_oid": authority,
|
||||
"gitea_git_oid": None,
|
||||
"gitea_api_oid": None,
|
||||
"commit_visible": None,
|
||||
"generation": generation,
|
||||
"reconciliation_state": reconciliation_state,
|
||||
}
|
||||
)
|
||||
if not candidates:
|
||||
return {}
|
||||
return max(
|
||||
candidates,
|
||||
key=lambda item: (int(item["generation"] or 0), bool(item["authority_branch_oid"])),
|
||||
)
|
||||
|
||||
def snapshot(self) -> dict[str, Any]:
|
||||
task = self._snapshot_task()
|
||||
return build_control_plane_snapshot(target_version=self.config.target_version, runtime=self.runtime_status(), repository={"identity": self.config.repository, "authority_git_dir": str(self.config.authoritative_git_dir), "authority_main_oid": self.authority.read_ref("refs/heads/main")}, production={"baseline_version": "V2R1C7", "target_oid": "c7adece1cc8beffe7c718d53c775898b45d6c7f8", "deployment_state": "ACTIVE"}, task=task, capabilities={"legacy_writes_enabled": False, "governed_writes_enabled": True, "force_push": False, "main_write": False, "tag_write": False, "baseline_write": False, "production_write": False}, credential_broker={"status": "SERVER_SIDE_ONLY", "provider": "FileCredentialProvider"})
|
||||
|
||||
def handlers(self) -> dict[str, Any]:
|
||||
return {
|
||||
"createGovernedTask": self.create_task,
|
||||
"acquireWriterLease": self.acquire_lease,
|
||||
"commitFileGoverned": lambda request: self.commit_files({**request, "files": {request["path"]: request["content_base64"]}}),
|
||||
"commitFilesGoverned": self.commit_files,
|
||||
"getGitOpsJob": lambda request: self.job(request, "get"),
|
||||
"resumeGitOpsJob": lambda request: self.job(request, "resume"),
|
||||
"reconcileGitOpsJob": lambda request: self.job(request, "reconcile"),
|
||||
"getControlPlaneSnapshot": lambda request: self.snapshot(),
|
||||
"createTagApprovalRequest": self.release.approval_request,
|
||||
"requestOrCreateTag": self.release.publish_tag,
|
||||
"getTagPublicationStatus": self.release.tag_status,
|
||||
"buildProductionArtifact": self.release.build_artifact,
|
||||
"executeApprovedProductionRelease": self.release.execute_request,
|
||||
"getReleaseConsistency": self.release.consistency,
|
||||
"runReleaseAcceptance": self.release.acceptance,
|
||||
}
|
||||
|
||||
|
||||
def build_governed_handlers(runtime_config: RuntimeConfig | Mapping[str, Any]) -> Mapping[str, Any]:
|
||||
if isinstance(runtime_config, Mapping): runtime_config = RuntimeConfig(**runtime_config)
|
||||
return GovernedRuntime(runtime_config).handlers()
|
||||
|
||||
|
||||
class _HTTPError(RuntimeError): pass
|
||||
|
||||
|
||||
class BridgeHandler(BaseHTTPRequestHandler):
|
||||
adapter: BridgeV2Adapter | None = None
|
||||
def log_message(self, *_args: Any) -> None: return
|
||||
def _json(self, value: Any, status: int = 200) -> None:
|
||||
raw = json.dumps(value, sort_keys=True, separators=(",", ":")).encode(); self.send_response(status); self.send_header("Content-Type", "application/json"); self.send_header("Content-Length", str(len(raw))); self.end_headers(); self.wfile.write(raw)
|
||||
def _body(self) -> dict[str, Any]:
|
||||
try: length = int(self.headers.get("Content-Length", "-1"))
|
||||
except ValueError as exc: raise _HTTPError("CONTENT_LENGTH_INVALID") from exc
|
||||
if length < 0 or length > MAX_BODY: raise _HTTPError("REQUEST_TOO_LARGE")
|
||||
value = json.loads(self.rfile.read(length).decode("utf-8"))
|
||||
if not isinstance(value, dict): raise _HTTPError("JSON_OBJECT_REQUIRED")
|
||||
return value
|
||||
def _allowed(self, operation: str, body: Mapping[str, Any]) -> None:
|
||||
allowed = set(OPERATION_REQUIRED_FIELDS[operation]) | {"schema", "lease_seconds", "create_target_branch", "worktree_mode", "cas_ref_update_required", "expected_branch_oid", "generation", "branch_mode"}
|
||||
if operation == "commitFilesGoverned": allowed.add("files")
|
||||
allowed.update({"owner_approval", "execute"})
|
||||
if set(body) - allowed: raise _HTTPError("UNKNOWN_FIELDS")
|
||||
def do_GET(self) -> None:
|
||||
if self.adapter is None: self._json({"error": "RUNTIME_UNAVAILABLE"}, 503); return
|
||||
if self.path == "/v2/runtime": self._json(self.adapter.getRuntimeStatus()); return
|
||||
if self.path == "/v2/capabilities": self._json(self.adapter.getGovernedWriteCapabilities()); return
|
||||
if self.path == "/v2/control-plane-snapshot": self._json(self.adapter.getControlPlaneSnapshot({})); return
|
||||
self._json({"error": "NOT_FOUND"}, 404)
|
||||
def do_POST(self) -> None:
|
||||
operation = ROUTES.get(self.path)
|
||||
if operation is None or operation in ("getRuntimeStatus", "getGovernedWriteCapabilities") or self.adapter is None: self._json({"error": "NOT_FOUND"}, 404); return
|
||||
try:
|
||||
body = self._body(); self._allowed(operation, body); self._json(getattr(self.adapter, operation)(body))
|
||||
except _HTTPError as exc: self._json({"error": str(exc)}, 413 if str(exc) == "REQUEST_TOO_LARGE" else 400)
|
||||
except (BootstrapError, ValueError, BridgeV2Error) as exc: self._json({"error": str(exc)}, 409 if "CONFLICT" in str(exc) else 400)
|
||||
except Exception as exc: self._json({"error": str(exc) or type(exc).__name__}, 409 if any(token in str(exc) for token in ("CONFLICT", "EXPECTATION", "MONOTONIC")) else 503)
|
||||
|
||||
|
||||
def serve(adapter: BridgeV2Adapter, host: str = "127.0.0.1", port: int = 18892) -> None:
|
||||
BridgeHandler.adapter = adapter; ThreadingHTTPServer((host, port), BridgeHandler).serve_forever()
|
||||
|
||||
|
||||
def main() -> None:
|
||||
state_root = Path("/var/lib/aios-gitops/bridge-v2-staging")
|
||||
token_path = Path(os.environ.get("AIOS_GITEA_TOKEN_FILE", "/run/credentials/aios-chatgpt-bridge-v2-staging.service/gitea_token"))
|
||||
ssh_key_path = Path(os.environ.get("AIOS_GITEA_SSH_KEY_PATH", "/home/aiman/.ssh/id_ed25519_codex_tmp"))
|
||||
os.environ["CREDENTIAL_FILE"] = str(token_path)
|
||||
os.environ.setdefault("GITEA_MACHINE_ACTOR", "aios-autonomy-agent")
|
||||
config = RuntimeConfig(
|
||||
os.environ.get("AIOS_CONTROL_PLANE_TARGET_OID", ""),
|
||||
Path("/data/aios-gitops/repo.git"),
|
||||
state_root,
|
||||
state_root / "jobs",
|
||||
token_path,
|
||||
ssh_key_path,
|
||||
remote_name=os.environ.get("AIOS_GITEA_REMOTE", "gitea-http"),
|
||||
api_base=os.environ.get("AIOS_GITEA_API_BASE", "https://g.cnnetmkt.top/git/api/v1"),
|
||||
target_version=os.environ.get("AIOS_CONTROL_PLANE_TARGET_VERSION", "V2R2C1"),
|
||||
)
|
||||
runtime = GovernedRuntime(config)
|
||||
serve(
|
||||
BridgeV2Adapter(runtime_status=runtime.runtime_status(), handlers=runtime.handlers()),
|
||||
port=int(os.environ.get("AIOS_BRIDGE_V2_PORT", "18892")),
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__": main()
|
||||
@@ -0,0 +1,594 @@
|
||||
"""Governed continuation executor for exact existing task branches.
|
||||
|
||||
The executor composes the existing continuation lease and durable Job store.
|
||||
All Git and Gitea side effects live behind injected narrow interfaces so the
|
||||
state machine can prove Authority, Git-network, API-branch, and commit-object
|
||||
agreement before it reports completion.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import base64
|
||||
from dataclasses import asdict, dataclass
|
||||
from datetime import datetime, timezone
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path, PurePosixPath
|
||||
import re
|
||||
import subprocess
|
||||
from typing import Callable, Mapping, Protocol
|
||||
from urllib.error import HTTPError, URLError
|
||||
from urllib.parse import quote, urlsplit
|
||||
from urllib.request import Request, urlopen
|
||||
|
||||
from aios_authority_git_executor import AuthorityGitExecutor
|
||||
from aios_continuation_lease import WriterLeaseRequestV2
|
||||
from aios_gitops_job_controller import GitOpsJobController, JobState
|
||||
from aios_gitops_reconciliation_core import ReconciliationAction, RemoteObservation, decide_reconciliation
|
||||
from authorized_task_writer_provisioning import validate_writer_lease_request_v2
|
||||
|
||||
|
||||
OID_RE = re.compile(r"^[0-9a-f]{40}$")
|
||||
SECRET_PATTERNS = (
|
||||
re.compile(rb"gh[pousr]_[A-Za-z0-9]{20,}"),
|
||||
re.compile(rb"(?i)(?:token|password|secret|api[_-]?key)\s*[:=]\s*[\"'][^\"'\s]{12,}[\"']"),
|
||||
re.compile(rb"-----BEGIN " + rb"(?:RSA |EC |OPENSSH )?PRIVATE KEY-----"),
|
||||
)
|
||||
|
||||
|
||||
class ContinuationExecutionError(RuntimeError):
|
||||
"""Stable fail-closed continuation execution error."""
|
||||
|
||||
|
||||
class DetachedWorkspace(Protocol):
|
||||
def is_detached(self) -> bool: ...
|
||||
def head_oid(self) -> str: ...
|
||||
def commit_files(self, files: Mapping[str, bytes], message: str) -> str: ...
|
||||
|
||||
|
||||
class AuthorityPort(Protocol):
|
||||
def branch_exists(self, branch: str) -> bool: ...
|
||||
def branch_head(self, branch: str) -> str: ...
|
||||
def is_ancestor(self, older: str, newer: str) -> bool: ...
|
||||
def compare_and_swap_branch(self, branch: str, expected: str, new: str) -> str: ...
|
||||
|
||||
|
||||
class RemotePort(Protocol):
|
||||
def repository_identity(self) -> str: ...
|
||||
def push_exact(self, branch: str, expected: str, new: str) -> None: ...
|
||||
def read_git_ref(self, branch: str) -> str | None: ...
|
||||
def read_api_branch(self, branch: str) -> str | None: ...
|
||||
def commit_visible(self, oid: str) -> bool: ...
|
||||
|
||||
|
||||
class CredentialProjection(Protocol):
|
||||
def project(self) -> None: ...
|
||||
def revoke(self) -> None: ...
|
||||
|
||||
|
||||
def _run(argv: list[str], *, cwd: Path | None = None, environment: Mapping[str, str] | None = None) -> str:
|
||||
result = subprocess.run(
|
||||
argv,
|
||||
cwd=cwd,
|
||||
env=None if environment is None else dict(environment),
|
||||
text=True,
|
||||
capture_output=True,
|
||||
check=False,
|
||||
)
|
||||
if result.returncode != 0:
|
||||
raise ContinuationExecutionError("SUBPROCESS_FAILED")
|
||||
return result.stdout.strip()
|
||||
|
||||
|
||||
@dataclass(slots=True)
|
||||
class AuthorityGitAdapter:
|
||||
"""Branch-name adapter over the existing narrow AuthorityGitExecutor."""
|
||||
|
||||
executor: AuthorityGitExecutor
|
||||
|
||||
@staticmethod
|
||||
def _ref(branch: str) -> str:
|
||||
return f"refs/heads/{branch}"
|
||||
|
||||
def branch_exists(self, branch: str) -> bool:
|
||||
return self.executor.read_ref(self._ref(branch)) is not None
|
||||
|
||||
def branch_head(self, branch: str) -> str:
|
||||
value = self.executor.read_ref(self._ref(branch))
|
||||
if value is None:
|
||||
raise ContinuationExecutionError("EXISTING_BRANCH_MISSING")
|
||||
return value
|
||||
|
||||
def is_ancestor(self, older: str, newer: str) -> bool:
|
||||
return self.executor.ancestry_check(older, newer)
|
||||
|
||||
def compare_and_swap_branch(self, branch: str, expected: str, new: str) -> str:
|
||||
return self.executor.advance_task_branch_CAS(self._ref(branch), expected, new)
|
||||
|
||||
|
||||
@dataclass(slots=True)
|
||||
class DetachedGitWorkspace:
|
||||
path: Path
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
if not self.path.is_absolute() or self.path.is_symlink() or not self.path.is_dir():
|
||||
raise ContinuationExecutionError("WORKTREE_PATH_INVALID")
|
||||
top = Path(_run(["git", "rev-parse", "--show-toplevel"], cwd=self.path)).resolve()
|
||||
if top != self.path.resolve():
|
||||
raise ContinuationExecutionError("WORKTREE_PATH_INVALID")
|
||||
|
||||
def is_detached(self) -> bool:
|
||||
result = subprocess.run(
|
||||
["git", "symbolic-ref", "-q", "HEAD"],
|
||||
cwd=self.path,
|
||||
capture_output=True,
|
||||
check=False,
|
||||
)
|
||||
return result.returncode == 1
|
||||
|
||||
def head_oid(self) -> str:
|
||||
return _run(["git", "rev-parse", "HEAD"], cwd=self.path)
|
||||
|
||||
def _dirty_paths(self) -> set[str]:
|
||||
command = subprocess.run(
|
||||
["git", "status", "--porcelain=v1", "--untracked-files=all"],
|
||||
cwd=self.path,
|
||||
text=True,
|
||||
capture_output=True,
|
||||
check=False,
|
||||
)
|
||||
if command.returncode != 0:
|
||||
raise ContinuationExecutionError("SUBPROCESS_FAILED")
|
||||
output = command.stdout
|
||||
result: set[str] = set()
|
||||
for line in output.splitlines():
|
||||
path = line[3:]
|
||||
if " -> " in path:
|
||||
path = path.split(" -> ", 1)[1]
|
||||
result.add(path)
|
||||
return result
|
||||
|
||||
def materialize_files(self, files: Mapping[str, bytes]) -> None:
|
||||
dirty = self._dirty_paths()
|
||||
expected_paths = set(files)
|
||||
if dirty and dirty != expected_paths:
|
||||
raise ContinuationExecutionError("WORKTREE_DIRTY_SCOPE_MISMATCH")
|
||||
for relative, content in files.items():
|
||||
target = (self.path / relative).resolve()
|
||||
try:
|
||||
target.relative_to(self.path.resolve())
|
||||
except ValueError as exc:
|
||||
raise ContinuationExecutionError("COMMIT_PATH_REJECTED") from exc
|
||||
if target.is_symlink():
|
||||
raise ContinuationExecutionError("COMMIT_PATH_REJECTED")
|
||||
target.parent.mkdir(parents=True, exist_ok=True)
|
||||
target.write_bytes(content)
|
||||
|
||||
def commit_files(self, files: Mapping[str, bytes], message: str) -> str:
|
||||
expected_paths = set(files)
|
||||
if self._dirty_paths() != expected_paths:
|
||||
raise ContinuationExecutionError("WORKTREE_DIRTY_SCOPE_MISMATCH")
|
||||
for relative, content in files.items():
|
||||
target = (self.path / relative).resolve()
|
||||
try:
|
||||
target.relative_to(self.path.resolve())
|
||||
except ValueError as exc:
|
||||
raise ContinuationExecutionError("COMMIT_PATH_REJECTED") from exc
|
||||
if not target.is_file() or target.is_symlink() or target.read_bytes() != content:
|
||||
raise ContinuationExecutionError("AUTHORING_PAYLOAD_MISMATCH")
|
||||
_run(["git", "add", "--", *sorted(expected_paths)], cwd=self.path)
|
||||
staged = set(_run(["git", "diff", "--cached", "--name-only"], cwd=self.path).splitlines())
|
||||
if staged != expected_paths:
|
||||
raise ContinuationExecutionError("STAGED_SCOPE_MISMATCH")
|
||||
environment = os.environ.copy()
|
||||
environment.update(
|
||||
{
|
||||
"GIT_AUTHOR_NAME": "AIOS Governed Continuation",
|
||||
"GIT_AUTHOR_EMAIL": "aios-governed-continuation@localhost",
|
||||
"GIT_COMMITTER_NAME": "AIOS Governed Continuation",
|
||||
"GIT_COMMITTER_EMAIL": "aios-governed-continuation@localhost",
|
||||
}
|
||||
)
|
||||
_run(["git", "commit", "--no-gpg-sign", "-m", message, "--", *sorted(expected_paths)], cwd=self.path, environment=environment)
|
||||
if not self.is_detached():
|
||||
raise ContinuationExecutionError("WORKTREE_NOT_DETACHED")
|
||||
return self.head_oid()
|
||||
|
||||
|
||||
@dataclass(slots=True)
|
||||
class ServerSideCredentialProjection:
|
||||
ssh_key_path: Path
|
||||
api_token_path: Path
|
||||
active: bool = False
|
||||
|
||||
def project(self) -> None:
|
||||
for path in (self.ssh_key_path, self.api_token_path):
|
||||
if path.is_symlink() or not path.is_file() or path.stat().st_size < 32:
|
||||
raise ContinuationExecutionError("CREDENTIAL_PROJECTION_MISSING")
|
||||
self.active = True
|
||||
|
||||
def revoke(self) -> None:
|
||||
self.active = False
|
||||
|
||||
|
||||
@dataclass(slots=True)
|
||||
class GiteaContinuationRemote:
|
||||
workspace: Path
|
||||
credential: ServerSideCredentialProjection
|
||||
remote_name: str = "gitea"
|
||||
repository: str = "aios/aios"
|
||||
api_base: str = "https://g.cnnetmkt.top/git/api/v1"
|
||||
expected_host: str = "g.cnnetmkt.top"
|
||||
|
||||
@staticmethod
|
||||
def validate_remote_url(value: str, expected_host: str = "g.cnnetmkt.top") -> None:
|
||||
if value.startswith(("/", "file:")) or value.endswith("/data/aios-gitops/repo.git"):
|
||||
raise ContinuationExecutionError("GITEA_REMOTE_IDENTITY_INVALID")
|
||||
if "://" in value:
|
||||
parsed = urlsplit(value)
|
||||
host, path = parsed.hostname, parsed.path
|
||||
else:
|
||||
location = value.split("@", 1)[-1]
|
||||
host, separator, path = location.partition(":")
|
||||
if not separator:
|
||||
raise ContinuationExecutionError("GITEA_REMOTE_IDENTITY_INVALID")
|
||||
path = "/" + path
|
||||
if host != expected_host or path.rstrip("/") not in {"/aios/aios.git", "/git/aios/aios.git"}:
|
||||
raise ContinuationExecutionError("GITEA_REMOTE_IDENTITY_INVALID")
|
||||
|
||||
def repository_identity(self) -> str:
|
||||
url = _run(["git", "remote", "get-url", self.remote_name], cwd=self.workspace)
|
||||
self.validate_remote_url(url, self.expected_host)
|
||||
if self.repository != "aios/aios":
|
||||
raise ContinuationExecutionError("REMOTE_REPOSITORY_IDENTITY_MISMATCH")
|
||||
return self.repository
|
||||
|
||||
def _require_projected(self) -> None:
|
||||
if not self.credential.active:
|
||||
raise ContinuationExecutionError("CREDENTIAL_PROJECTION_MISSING")
|
||||
|
||||
def read_git_ref(self, branch: str) -> str | None:
|
||||
self._require_projected()
|
||||
output = _run(["git", "ls-remote", self.remote_name, f"refs/heads/{branch}"], cwd=self.workspace)
|
||||
if not output:
|
||||
return None
|
||||
oid, ref = output.split(maxsplit=1)
|
||||
if ref != f"refs/heads/{branch}" or not OID_RE.fullmatch(oid):
|
||||
raise ContinuationExecutionError("GITEA_GIT_READBACK_INVALID")
|
||||
return oid
|
||||
|
||||
def push_exact(self, branch: str, expected: str, new: str) -> None:
|
||||
self._require_projected()
|
||||
self.repository_identity()
|
||||
if self.read_git_ref(branch) != expected:
|
||||
raise ContinuationExecutionError("GITEA_BRANCH_EXPECTATION_MISMATCH")
|
||||
_run(["git", "push", self.remote_name, f"{new}:refs/heads/{branch}"], cwd=self.workspace)
|
||||
|
||||
def _api(self, path: str) -> object:
|
||||
self._require_projected()
|
||||
token = self.credential.api_token_path.read_text(encoding="utf-8").strip()
|
||||
request = Request(
|
||||
self.api_base.rstrip("/") + path,
|
||||
headers={"Accept": "application/json", "Authorization": f"token {token}"},
|
||||
)
|
||||
try:
|
||||
with urlopen(request, timeout=20) as response:
|
||||
return json.loads(response.read().decode("utf-8"))
|
||||
except HTTPError as exc:
|
||||
if exc.code == 404:
|
||||
return None
|
||||
raise ContinuationExecutionError(f"GITEA_API_HTTP_{exc.code}") from exc
|
||||
except (URLError, TimeoutError, OSError, json.JSONDecodeError) as exc:
|
||||
raise ContinuationExecutionError("GITEA_API_UNAVAILABLE") from exc
|
||||
|
||||
def read_api_branch(self, branch: str) -> str | None:
|
||||
owner, name = self.repository.split("/", 1)
|
||||
page = 1
|
||||
while page <= 100:
|
||||
value = self._api(f"/repos/{quote(owner)}/{quote(name)}/branches?page={page}&limit=50")
|
||||
if value is None:
|
||||
return None
|
||||
if not isinstance(value, list):
|
||||
raise ContinuationExecutionError("GITEA_API_BRANCH_COLLECTION_INVALID")
|
||||
for item in value:
|
||||
if not isinstance(item, dict) or item.get("name") != branch:
|
||||
continue
|
||||
oid = item.get("commit", {}).get("id")
|
||||
if not isinstance(oid, str) or not OID_RE.fullmatch(oid):
|
||||
raise ContinuationExecutionError("GITEA_API_BRANCH_INVALID")
|
||||
return oid
|
||||
if len(value) < 50:
|
||||
return None
|
||||
page += 1
|
||||
raise ContinuationExecutionError("GITEA_API_BRANCH_PAGINATION_LIMIT")
|
||||
|
||||
def commit_visible(self, oid: str) -> bool:
|
||||
owner, name = self.repository.split("/", 1)
|
||||
value = self._api(f"/repos/{quote(owner)}/{quote(name)}/git/commits/{oid}")
|
||||
if not isinstance(value, dict):
|
||||
return False
|
||||
observed = value.get("sha") or value.get("id")
|
||||
return observed == oid
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class ContinuationPermissions:
|
||||
create_target_branch: bool = False
|
||||
force_push: bool = False
|
||||
tag_write: bool = False
|
||||
baseline_write: bool = False
|
||||
production_write: bool = False
|
||||
main_write: bool = False
|
||||
|
||||
def validate(self) -> None:
|
||||
if any(asdict(self).values()):
|
||||
raise ContinuationExecutionError("MUTATION_PERMISSION_DENIED")
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class ContinuationRuntimeBinding:
|
||||
request: WriterLeaseRequestV2
|
||||
workspace_path: Path
|
||||
lease_not_before: datetime
|
||||
lease_expires_at: datetime
|
||||
original_receipt_valid: bool
|
||||
original_grant_consumed: bool
|
||||
permissions: ContinuationPermissions
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class ContinuationCommit:
|
||||
idempotency_key: str
|
||||
files: Mapping[str, bytes]
|
||||
message: str
|
||||
|
||||
def payload(self) -> bytes:
|
||||
if not self.idempotency_key or not self.message.strip() or not self.files:
|
||||
raise ContinuationExecutionError("COMMIT_REQUEST_INVALID")
|
||||
normalized: dict[str, str] = {}
|
||||
for path, content in self.files.items():
|
||||
pure = PurePosixPath(path)
|
||||
if (
|
||||
not isinstance(path, str)
|
||||
or pure.is_absolute()
|
||||
or ".." in pure.parts
|
||||
or path.startswith((".git/", "baseline/", "production/"))
|
||||
or not isinstance(content, bytes)
|
||||
):
|
||||
raise ContinuationExecutionError("COMMIT_PATH_REJECTED")
|
||||
if any(pattern.search(content) for pattern in SECRET_PATTERNS):
|
||||
raise ContinuationExecutionError("SECRET_LIKE_PAYLOAD_REJECTED")
|
||||
normalized[path] = base64.b64encode(content).decode("ascii")
|
||||
return json.dumps(
|
||||
{"files_base64": normalized, "message": self.message},
|
||||
sort_keys=True,
|
||||
separators=(",", ":"),
|
||||
).encode("utf-8")
|
||||
|
||||
|
||||
class ContinuationExecutor:
|
||||
def __init__(
|
||||
self,
|
||||
controller: GitOpsJobController,
|
||||
authority: AuthorityPort,
|
||||
workspace: DetachedWorkspace,
|
||||
remote: RemotePort,
|
||||
credential: CredentialProjection,
|
||||
*,
|
||||
now: Callable[[], datetime] = lambda: datetime.now(timezone.utc),
|
||||
) -> None:
|
||||
self.controller = controller
|
||||
self.authority = authority
|
||||
self.workspace = workspace
|
||||
self.remote = remote
|
||||
self.credential = credential
|
||||
self.now = now
|
||||
self._observed_generations: dict[str, int] = {}
|
||||
|
||||
def observe_generation(self, branch: str, generation: int) -> None:
|
||||
self._observed_generations[branch] = max(generation, self._observed_generations.get(branch, 0))
|
||||
|
||||
def _max_generation(self, branch: str) -> int:
|
||||
observed = self._observed_generations.get(branch, 0)
|
||||
with self.controller.store._connect() as connection:
|
||||
row = connection.execute(
|
||||
"SELECT MAX(generation) AS generation FROM branch_leases WHERE branch=?",
|
||||
(branch,),
|
||||
).fetchone()
|
||||
durable = int(row["generation"] or 0)
|
||||
return max(observed, durable)
|
||||
|
||||
@staticmethod
|
||||
def _request_mapping(request: WriterLeaseRequestV2) -> dict[str, object]:
|
||||
return {
|
||||
"schema": WriterLeaseRequestV2.schema,
|
||||
"task_id": request.task_id,
|
||||
"repository": request.repository,
|
||||
"branch_mode": request.branch_mode,
|
||||
"base_oid": request.base_oid,
|
||||
"expected_branch_oid": request.expected_branch_oid,
|
||||
"target_branch": request.target_branch,
|
||||
"generation": request.generation,
|
||||
"lease_seconds": request.lease_seconds,
|
||||
}
|
||||
|
||||
def _validate_preflight(self, binding: ContinuationRuntimeBinding) -> None:
|
||||
request = binding.request
|
||||
normalized = validate_writer_lease_request_v2(self._request_mapping(request), self.authority)
|
||||
if normalized != {**self._request_mapping(request), "create_target_branch": False, "worktree_mode": "detached_at_expected_head", "cas_ref_update_required": True}:
|
||||
raise ContinuationExecutionError("CONTINUATION_NORMALIZATION_INVALID")
|
||||
if not binding.original_receipt_valid or not binding.original_grant_consumed:
|
||||
raise ContinuationExecutionError("ORIGINAL_GRANT_LINEAGE_INVALID")
|
||||
binding.permissions.validate()
|
||||
now = self.now()
|
||||
if now < binding.lease_not_before:
|
||||
raise ContinuationExecutionError("LEASE_NOT_ACTIVE")
|
||||
if now >= binding.lease_expires_at:
|
||||
raise ContinuationExecutionError("LEASE_EXPIRED")
|
||||
if request.generation <= self._max_generation(request.target_branch):
|
||||
raise ContinuationExecutionError("GENERATION_NOT_MONOTONIC")
|
||||
if not self.workspace.is_detached():
|
||||
raise ContinuationExecutionError("WORKTREE_NOT_DETACHED")
|
||||
if self.workspace.head_oid() != request.expected_branch_oid:
|
||||
raise ContinuationExecutionError("WORKTREE_HEAD_MISMATCH")
|
||||
if self.remote.repository_identity() != request.repository:
|
||||
raise ContinuationExecutionError("REMOTE_REPOSITORY_IDENTITY_MISMATCH")
|
||||
|
||||
def _job_request(self, binding: ContinuationRuntimeBinding, commit: ContinuationCommit, payload: bytes) -> dict[str, object]:
|
||||
request = binding.request
|
||||
return {
|
||||
"idempotency_key": commit.idempotency_key,
|
||||
"campaign_id": request.task_id,
|
||||
"task_id": request.task_id,
|
||||
"repository": request.repository,
|
||||
"operation": "CONTINUATION_COMMIT_FILES",
|
||||
"branch": request.target_branch,
|
||||
"expected_branch_oid": request.expected_branch_oid,
|
||||
"base_oid": request.base_oid,
|
||||
"branch_mode": request.branch_mode,
|
||||
"payload_digest": hashlib.sha256(payload).hexdigest(),
|
||||
"lineage_anchor_oid": request.base_oid,
|
||||
"generation": request.generation,
|
||||
}
|
||||
|
||||
def execute(self, binding: ContinuationRuntimeBinding, commit: ContinuationCommit) -> dict[str, object]:
|
||||
payload = commit.payload()
|
||||
submitted = self.controller.submit(self._job_request(binding, commit, payload), payload=payload)
|
||||
current = self.controller.store.read(submitted.job_id)
|
||||
if JobState(current["state"]) == JobState.COMPLETED:
|
||||
return self.controller.store._receipt(current, duplicate=True).as_dict()
|
||||
self._validate_preflight(binding)
|
||||
request = binding.request
|
||||
projected = False
|
||||
try:
|
||||
self.credential.project()
|
||||
projected = True
|
||||
row = self.controller.store.read(submitted.job_id)
|
||||
if JobState(row["state"]) == JobState.REQUESTED:
|
||||
self.controller.advance(
|
||||
submitted.job_id,
|
||||
JobState.AUTHORIZED,
|
||||
side_effect_state="AUTHORIZED",
|
||||
receipt={"task_id": request.task_id, "original_grant_consumed": True},
|
||||
)
|
||||
row = self.controller.store.read(submitted.job_id)
|
||||
if JobState(row["state"]) == JobState.AUTHORIZED:
|
||||
self.controller.lease_job(
|
||||
submitted.job_id,
|
||||
lease_id=f"lease-{submitted.job_id}",
|
||||
lineage_anchor_oid=request.base_oid,
|
||||
current_head_oid=request.expected_branch_oid,
|
||||
generation=request.generation,
|
||||
lease_expires_at=binding.lease_expires_at.astimezone(timezone.utc).isoformat(),
|
||||
expected_revision=int(row["revision"]),
|
||||
)
|
||||
self.controller.advance(submitted.job_id, JobState.WORKTREE_READY, side_effect_state="WORKTREE_READY")
|
||||
self.controller.advance(submitted.job_id, JobState.EXECUTING, side_effect_state="IN_PROGRESS")
|
||||
new_oid = self.workspace.commit_files(commit.files, commit.message)
|
||||
if not OID_RE.fullmatch(new_oid) or not self.authority.is_ancestor(request.expected_branch_oid, new_oid):
|
||||
raise ContinuationExecutionError("NON_FAST_FORWARD_COMMIT")
|
||||
authority_oid = self.authority.compare_and_swap_branch(request.target_branch, request.expected_branch_oid, new_oid)
|
||||
if authority_oid != new_oid:
|
||||
raise ContinuationExecutionError("AUTHORITY_CAS_READBACK_MISMATCH")
|
||||
self.controller.advance(
|
||||
submitted.job_id,
|
||||
JobState.LOCAL_VERIFIED,
|
||||
side_effect_state="LOCAL_VERIFIED",
|
||||
receipt={"authority_oid": authority_oid, "force_push": False},
|
||||
)
|
||||
try:
|
||||
self.controller.advance(submitted.job_id, JobState.REMOTE_SYNCING, side_effect_state="IN_PROGRESS")
|
||||
self.remote.push_exact(request.target_branch, request.expected_branch_oid, new_oid)
|
||||
except Exception:
|
||||
return self._remote_pending(submitted.job_id, authority_oid, new_oid)
|
||||
return self._finish_remote_verification(submitted.job_id, authority_oid, new_oid)
|
||||
finally:
|
||||
if projected:
|
||||
self.credential.revoke()
|
||||
|
||||
def _observe_remote(self, branch: str, oid: str) -> tuple[str | None, str | None, bool]:
|
||||
return self.remote.read_git_ref(branch), self.remote.read_api_branch(branch), self.remote.commit_visible(oid)
|
||||
|
||||
def _remote_pending(self, job_id: str, authority_oid: str, new_oid: str) -> dict[str, object]:
|
||||
receipt = {
|
||||
"authority_oid": authority_oid,
|
||||
"desired_remote_oid": new_oid,
|
||||
"remote_verified": False,
|
||||
"force_push": False,
|
||||
}
|
||||
result = self.controller.advance(
|
||||
job_id,
|
||||
JobState.RECONCILIATION_REQUIRED,
|
||||
side_effect_state="REMOTE_PUSH_PENDING",
|
||||
receipt=receipt,
|
||||
)
|
||||
return result.as_dict()
|
||||
|
||||
def _finish_remote_verification(self, job_id: str, authority_oid: str, new_oid: str) -> dict[str, object]:
|
||||
row = self.controller.store.read(job_id)
|
||||
branch = row["branch"]
|
||||
request = json.loads(row["request_json"])["binding"]
|
||||
git_oid, api_oid, visible = self._observe_remote(branch, new_oid)
|
||||
decision = decide_reconciliation(RemoteObservation("aios/aios", branch, new_oid, authority_oid, git_oid, api_oid, visible, datetime.now(timezone.utc).isoformat(), request.get("expected_branch_oid")))
|
||||
if decision.action is not ReconciliationAction.MARK_REMOTE_VERIFIED:
|
||||
return self._remote_pending(job_id, authority_oid, new_oid)
|
||||
receipt = {
|
||||
"authority_oid": authority_oid,
|
||||
"git_remote_oid": git_oid,
|
||||
"api_branch_oid": api_oid,
|
||||
"commit_visible": True,
|
||||
"remote_verified": True,
|
||||
"reconciliation_decision": decision.action.value,
|
||||
"force_push": False,
|
||||
}
|
||||
self.controller.advance(job_id, JobState.REMOTE_VERIFIED, side_effect_state="REMOTE_VERIFIED", receipt=receipt)
|
||||
return self.controller.advance(job_id, JobState.COMPLETED, side_effect_state="REMOTE_VERIFIED", receipt=receipt).as_dict()
|
||||
|
||||
def reconcile(self, job_id: str) -> dict[str, object]:
|
||||
row = self.controller.store.read(job_id)
|
||||
state = JobState(row["state"])
|
||||
if state == JobState.COMPLETED:
|
||||
return self.controller.store._receipt(row, duplicate=True).as_dict()
|
||||
if state != JobState.RECONCILIATION_REQUIRED:
|
||||
raise ContinuationExecutionError("JOB_NOT_RECONCILABLE")
|
||||
request = json.loads(row["request_json"])["binding"]
|
||||
receipt = json.loads(row["receipt_json"])
|
||||
new_oid = receipt["desired_remote_oid"]
|
||||
authority_oid = self.authority.branch_head(row["branch"])
|
||||
try:
|
||||
generation = int(request.get("generation") or 0)
|
||||
except (TypeError, ValueError) as exc:
|
||||
raise ContinuationExecutionError("DURABLE_BINDING_GENERATION_INVALID") from exc
|
||||
if generation > 0:
|
||||
with self.controller.store._connect() as connection:
|
||||
earlier = connection.execute(
|
||||
"SELECT 1 FROM jobs WHERE branch=? AND state=? "
|
||||
"AND CAST(json_extract(request_json, '$.binding.generation') AS INTEGER) < ? LIMIT 1",
|
||||
(row["branch"], JobState.RECONCILIATION_REQUIRED.value, generation),
|
||||
).fetchone()
|
||||
if earlier is not None:
|
||||
raise ContinuationExecutionError("EARLIER_RECONCILIATION_REQUIRED")
|
||||
self.credential.project()
|
||||
try:
|
||||
git_oid, api_oid, visible = self._observe_remote(row["branch"], new_oid)
|
||||
if authority_oid == git_oid == api_oid == new_oid and visible:
|
||||
self.controller.advance(job_id, JobState.RECONCILING, side_effect_state="OBSERVATION_FIRST")
|
||||
return self._finish_remote_verification(job_id, new_oid, new_oid)
|
||||
expected_oid = request["expected_branch_oid"]
|
||||
if (
|
||||
self.authority.is_ancestor(new_oid, authority_oid)
|
||||
and git_oid is not None
|
||||
and git_oid == api_oid
|
||||
and self.authority.is_ancestor(git_oid, new_oid)
|
||||
):
|
||||
self.controller.advance(job_id, JobState.RECONCILING, side_effect_state="OBSERVATION_FIRST_CHAIN")
|
||||
self.remote.push_exact(row["branch"], git_oid, new_oid)
|
||||
return self._finish_remote_verification(job_id, new_oid, new_oid)
|
||||
if authority_oid == new_oid and git_oid == api_oid == expected_oid:
|
||||
self.controller.advance(job_id, JobState.RECONCILING, side_effect_state="OBSERVATION_FIRST_RETRY")
|
||||
self.remote.push_exact(row["branch"], expected_oid, new_oid)
|
||||
return self._finish_remote_verification(job_id, new_oid, new_oid)
|
||||
return self.controller.store._receipt(row, duplicate=True).as_dict()
|
||||
finally:
|
||||
self.credential.revoke()
|
||||
@@ -0,0 +1,154 @@
|
||||
"""Campaign-aware continuation leases for an existing Git branch.
|
||||
|
||||
This module is deliberately transport-neutral. It validates branch lineage
|
||||
and hands ref mutation to an injected compare-and-swap broker; it does not run
|
||||
Git, inspect credentials, or create a branch itself.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass, replace
|
||||
from datetime import datetime, timedelta, timezone
|
||||
import re
|
||||
from typing import Protocol
|
||||
|
||||
|
||||
OID_RE = re.compile(r"^[0-9a-f]{40}$")
|
||||
TARGET_BRANCH = "agent/autonomous-release-campaign-v1-20260807"
|
||||
LINEAGE_ANCHOR = "69b45e81dcd2c3707c367003b08c55fd361faae8"
|
||||
CAMPAIGN_HEAD = "bba675fab3b7e0c5fba7066720252b3c91b9f20d"
|
||||
|
||||
|
||||
class ContinuationError(RuntimeError):
|
||||
"""Stable fail-closed error for lease or branch binding violations."""
|
||||
|
||||
|
||||
class BranchProbe(Protocol):
|
||||
def branch_exists(self, branch: str) -> bool: ...
|
||||
def branch_head(self, branch: str) -> str: ...
|
||||
def is_ancestor(self, older: str, newer: str) -> bool: ...
|
||||
|
||||
|
||||
class RefCAS(Protocol):
|
||||
def compare_and_swap_branch(self, branch: str, expected: str, new: str) -> str: ...
|
||||
|
||||
|
||||
def _oid(value: str, field: str) -> str:
|
||||
if not isinstance(value, str) or not OID_RE.fullmatch(value):
|
||||
raise ContinuationError(f"{field}_invalid")
|
||||
return value
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class WriterLeaseRequestV2:
|
||||
task_id: str
|
||||
repository: str
|
||||
branch_mode: str
|
||||
base_oid: str
|
||||
expected_branch_oid: str
|
||||
target_branch: str
|
||||
generation: int
|
||||
lease_seconds: int = 900
|
||||
|
||||
schema = "aios.task-writer-lease.v2"
|
||||
accepted_schemas = frozenset({"aios.task-writer-lease.v2", "aios.task-writer-lease-request.v2"})
|
||||
|
||||
def validate(self, probe: BranchProbe) -> None:
|
||||
if not self.task_id or self.repository != "aios/aios":
|
||||
raise ContinuationError("request_identity_invalid")
|
||||
if self.branch_mode not in {"CREATE_NEW", "CONTINUE_EXISTING"}:
|
||||
raise ContinuationError("branch_mode_invalid")
|
||||
_oid(self.base_oid, "base_oid")
|
||||
_oid(self.expected_branch_oid, "expected_branch_oid")
|
||||
if self.target_branch in {"main", "master"} or self.target_branch.startswith(("refs/", "tags/")):
|
||||
raise ContinuationError("protected_branch_rejected")
|
||||
if not isinstance(self.generation, int) or isinstance(self.generation, bool) or self.generation < 1:
|
||||
raise ContinuationError("generation_invalid")
|
||||
if not isinstance(self.lease_seconds, int) or not 1 <= self.lease_seconds <= 86_400:
|
||||
raise ContinuationError("lease_duration_invalid")
|
||||
if self.branch_mode == "CONTINUE_EXISTING":
|
||||
if not probe.branch_exists(self.target_branch):
|
||||
raise ContinuationError("EXISTING_BRANCH_MISSING")
|
||||
actual = _oid(probe.branch_head(self.target_branch), "actual_branch_oid")
|
||||
if actual != self.expected_branch_oid:
|
||||
raise ContinuationError("EXISTING_BRANCH_OID_CONFLICT")
|
||||
if not probe.is_ancestor(self.base_oid, actual):
|
||||
raise ContinuationError("BASE_NOT_ANCESTOR_OF_BRANCH")
|
||||
elif hasattr(probe, "commit_exists") and not probe.commit_exists(self.base_oid):
|
||||
raise ContinuationError("BASE_COMMIT_NOT_FOUND")
|
||||
elif self.branch_mode == "CREATE_NEW" and probe.branch_exists(self.target_branch):
|
||||
raise ContinuationError("TARGET_BRANCH_ALREADY_EXISTS")
|
||||
elif self.branch_mode == "CREATE_NEW" and self.base_oid != self.expected_branch_oid:
|
||||
raise ContinuationError("CREATE_NEW_BASE_EXPECTATION_MISMATCH")
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class CampaignWriterLease:
|
||||
task_id: str
|
||||
repository: str
|
||||
branch: str
|
||||
lineage_anchor_oid: str
|
||||
current_head_oid: str
|
||||
generation: int
|
||||
lease_expires_at: datetime
|
||||
append_only: bool = True
|
||||
renewable: bool = True
|
||||
multi_commit: bool = True
|
||||
recoverable: bool = True
|
||||
state: str = "ACTIVE"
|
||||
branch_bound: bool = True
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
_oid(self.lineage_anchor_oid, "lineage_anchor_oid")
|
||||
_oid(self.current_head_oid, "current_head_oid")
|
||||
if not all((self.append_only, self.renewable, self.multi_commit, self.recoverable, self.branch_bound)):
|
||||
raise ContinuationError("lease_capability_downgrade")
|
||||
|
||||
|
||||
class ContinuationLeaseManager:
|
||||
"""State machine for a multi-commit lease with CAS-only ref updates."""
|
||||
|
||||
def __init__(self, probe: BranchProbe, refs: RefCAS, *, now=datetime.now) -> None:
|
||||
self.probe = probe
|
||||
self.refs = refs
|
||||
self.now = now
|
||||
|
||||
def open(self, request: WriterLeaseRequestV2) -> CampaignWriterLease:
|
||||
request.validate(self.probe)
|
||||
if request.branch_mode != "CONTINUE_EXISTING":
|
||||
raise ContinuationError("CREATE_NEW_REQUIRES_SEPARATE_PROVISIONER")
|
||||
return CampaignWriterLease(
|
||||
task_id=request.task_id,
|
||||
repository=request.repository,
|
||||
branch=request.target_branch,
|
||||
lineage_anchor_oid=request.base_oid,
|
||||
current_head_oid=request.expected_branch_oid,
|
||||
generation=request.generation,
|
||||
lease_expires_at=self.now(timezone.utc) + timedelta(seconds=request.lease_seconds),
|
||||
)
|
||||
|
||||
def renew(self, lease: CampaignWriterLease, seconds: int) -> CampaignWriterLease:
|
||||
if lease.state != "ACTIVE" or not lease.renewable or seconds < 1:
|
||||
raise ContinuationError("lease_not_renewable")
|
||||
return replace(lease, lease_expires_at=self.now(timezone.utc) + timedelta(seconds=seconds))
|
||||
|
||||
def append_commit(self, lease: CampaignWriterLease, new_head_oid: str) -> CampaignWriterLease:
|
||||
if lease.state != "ACTIVE":
|
||||
raise ContinuationError("lease_not_active")
|
||||
_oid(new_head_oid, "new_head_oid")
|
||||
if not self.probe.is_ancestor(lease.current_head_oid, new_head_oid):
|
||||
raise ContinuationError("NON_FAST_FORWARD_COMMIT")
|
||||
try:
|
||||
actual = self.refs.compare_and_swap_branch(
|
||||
lease.branch, lease.current_head_oid, new_head_oid
|
||||
)
|
||||
except Exception as exc:
|
||||
raise ContinuationError("BRANCH_LEASE_STALE") from exc
|
||||
if actual != new_head_oid:
|
||||
raise ContinuationError("BRANCH_LEASE_STALE")
|
||||
return replace(lease, current_head_oid=new_head_oid)
|
||||
|
||||
def supersede(self, lease: CampaignWriterLease) -> CampaignWriterLease:
|
||||
if lease.state not in {"ACTIVE", "RECOVERABLE_FAILURE", "RECONCILIATION_REQUIRED"}:
|
||||
raise ContinuationError("lease_not_supersedable")
|
||||
return replace(lease, generation=lease.generation + 1, state="SUPERSEDED")
|
||||
@@ -0,0 +1,25 @@
|
||||
"""Separate read, write, and release plane readiness evaluation."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import Mapping
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class RuntimeStatus:
|
||||
read_plane: Mapping[str, bool]
|
||||
write_plane: Mapping[str, bool]
|
||||
release_plane: Mapping[str, bool]
|
||||
|
||||
@property
|
||||
def release_control_plane_ready(self) -> bool:
|
||||
return all(self.read_plane.values()) and all(self.write_plane.values()) and all(self.release_plane.values())
|
||||
|
||||
def as_dict(self) -> dict[str, object]:
|
||||
return {
|
||||
"READ_PLANE": dict(self.read_plane),
|
||||
"WRITE_PLANE": dict(self.write_plane),
|
||||
"RELEASE_PLANE": dict(self.release_plane),
|
||||
"release_control_plane_ready": self.release_control_plane_ready,
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
"""Workspace-independent authority/network/Gitea ref sentinel."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
import subprocess
|
||||
from typing import Callable, Mapping
|
||||
|
||||
|
||||
class SentinelError(RuntimeError):
|
||||
pass
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class SentinelInputs:
|
||||
authority_git_dir: Path
|
||||
gitea_remote: str
|
||||
gitea_api: str
|
||||
ref: str
|
||||
expected_oid: str
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class SentinelResult:
|
||||
authority_oid: str | None
|
||||
network_oid: str | None
|
||||
gitea_oid: str | None
|
||||
equal: bool
|
||||
error: str | None = None
|
||||
|
||||
|
||||
def authority_oid(inputs: SentinelInputs) -> str:
|
||||
result = subprocess.run(
|
||||
["git", "--git-dir", str(inputs.authority_git_dir), "rev-parse", inputs.ref],
|
||||
text=True,
|
||||
capture_output=True,
|
||||
check=False,
|
||||
)
|
||||
if result.returncode != 0:
|
||||
raise SentinelError("AUTHORITY_REF_UNAVAILABLE")
|
||||
return result.stdout.strip()
|
||||
|
||||
|
||||
def evaluate(
|
||||
inputs: SentinelInputs,
|
||||
*,
|
||||
network_reader: Callable[[str, str], str | None],
|
||||
gitea_reader: Callable[[str, str], str | None],
|
||||
) -> SentinelResult:
|
||||
try:
|
||||
local = authority_oid(inputs)
|
||||
network = network_reader(inputs.gitea_remote, inputs.ref)
|
||||
gitea = gitea_reader(inputs.gitea_api, inputs.ref)
|
||||
equal = local == network == gitea == inputs.expected_oid
|
||||
return SentinelResult(local, network, gitea, equal, None if equal else "OID_MISMATCH")
|
||||
except SentinelError as exc:
|
||||
return SentinelResult(None, None, None, False, str(exc))
|
||||
@@ -0,0 +1,26 @@
|
||||
"""Non-sensitive control-plane snapshot assembly."""
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any, Mapping
|
||||
|
||||
|
||||
def _copy(value: Mapping[str, Any] | None, keys: tuple[str, ...]) -> dict[str, Any]:
|
||||
value = value or {}
|
||||
return {key: value.get(key) for key in keys}
|
||||
|
||||
|
||||
def build_control_plane_snapshot(*, target_version: str, runtime: Mapping[str, Any] | None = None, repository: Mapping[str, Any] | None = None, production: Mapping[str, Any] | None = None, task: Mapping[str, Any] | None = None, capabilities: Mapping[str, Any] | None = None, credential_broker: Mapping[str, Any] | None = None) -> dict[str, Any]:
|
||||
return {
|
||||
"version": {"target_release": target_version},
|
||||
"runtime": _copy(runtime, ("bridge_schema_version", "control_plane_release_version", "runtime_release_commit", "runtime_path", "artifact_sha256", "drift_status")),
|
||||
"repository": _copy(repository, ("identity", "authority_git_dir", "authority_main_oid")),
|
||||
"production": _copy(production, ("baseline_version", "target_oid", "deployment_state")),
|
||||
"task": _copy(task, ("task_id", "branch", "authority_branch_oid", "gitea_git_oid", "gitea_api_oid", "commit_visible", "generation", "reconciliation_state")),
|
||||
"capabilities": _copy(capabilities, ("legacy_writes_enabled", "governed_writes_enabled", "force_push", "main_write", "tag_write", "baseline_write", "production_write")),
|
||||
"credential_broker": _copy(credential_broker, ("status", "provider")),
|
||||
"secret_material_exposed": False,
|
||||
}
|
||||
|
||||
|
||||
def getControlPlaneSnapshot(**kwargs: Any) -> dict[str, Any]:
|
||||
return build_control_plane_snapshot(**kwargs)
|
||||
@@ -0,0 +1,78 @@
|
||||
"""Metadata-only, server-side credential lease broker."""
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime, timedelta, timezone
|
||||
import os
|
||||
from pathlib import Path
|
||||
import secrets
|
||||
from typing import Any, Callable, Mapping
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class CredentialLease:
|
||||
lease_id: str
|
||||
task_id: str
|
||||
repository: str
|
||||
target_branch: str
|
||||
generation: int
|
||||
issued_at: str
|
||||
expires_at: str
|
||||
scopes: tuple[str, ...]
|
||||
provider: str
|
||||
state: str = "ACTIVE"
|
||||
|
||||
def as_dict(self) -> dict[str, Any]:
|
||||
return {"lease_id": self.lease_id, "task_id": self.task_id, "repository": self.repository, "target_branch": self.target_branch, "generation": self.generation, "issued_at": self.issued_at, "expires_at": self.expires_at, "scopes": list(self.scopes), "provider": self.provider, "state": self.state}
|
||||
|
||||
|
||||
class FileCredentialProvider:
|
||||
def __init__(self, token_path: str | os.PathLike[str], *, actor: str = "aios-autonomy-agent") -> None:
|
||||
self.token_path = Path(token_path)
|
||||
self.actor = actor
|
||||
|
||||
def environment(self, lease: CredentialLease) -> dict[str, str]:
|
||||
if not self.token_path.is_file():
|
||||
raise RuntimeError("CREDENTIAL_PROVIDER_UNAVAILABLE")
|
||||
return {"CREDENTIAL_FILE": str(self.token_path), "GITEA_MACHINE_ACTOR": self.actor}
|
||||
|
||||
|
||||
class CredentialBroker:
|
||||
def __init__(self, provider: Any, *, now: Callable[[], datetime] | None = None) -> None:
|
||||
self.provider = provider
|
||||
self.now = now or (lambda: datetime.now(timezone.utc))
|
||||
self._leases: dict[str, CredentialLease] = {}
|
||||
|
||||
def issue(self, *, task_id: str, repository: str, target_branch: str, generation: int, scopes: tuple[str, ...] = ("git:push", "gitea:read"), lease_seconds: int = 900) -> dict[str, Any]:
|
||||
if repository != "aios/aios" or not target_branch.startswith("agent/") or generation < 1 or not 1 <= lease_seconds <= 86400:
|
||||
raise ValueError("CREDENTIAL_LEASE_BINDING_INVALID")
|
||||
now = self.now().astimezone(timezone.utc)
|
||||
lease = CredentialLease("lease-" + secrets.token_hex(16), task_id, repository, target_branch, generation, now.isoformat(), (now + timedelta(seconds=lease_seconds)).isoformat(), tuple(scopes), type(self.provider).__name__)
|
||||
self._leases[lease.lease_id] = lease
|
||||
return lease.as_dict()
|
||||
|
||||
def _get(self, lease_id: str) -> CredentialLease:
|
||||
lease = self._leases.get(lease_id)
|
||||
if lease is None:
|
||||
raise KeyError("CREDENTIAL_LEASE_NOT_FOUND")
|
||||
if lease.state == "ACTIVE" and self.now().astimezone(timezone.utc) >= datetime.fromisoformat(lease.expires_at):
|
||||
lease = CredentialLease(**{**lease.as_dict(), "scopes": tuple(lease.scopes), "state": "EXPIRED"})
|
||||
self._leases[lease_id] = lease
|
||||
return lease
|
||||
|
||||
def environment(self, lease_id: str) -> Mapping[str, str]:
|
||||
lease = self._get(lease_id)
|
||||
if lease.state != "ACTIVE":
|
||||
raise RuntimeError("CREDENTIAL_LEASE_NOT_ACTIVE")
|
||||
return self.provider.environment(lease)
|
||||
|
||||
def status(self, lease_id: str) -> dict[str, Any]:
|
||||
return self._get(lease_id).as_dict()
|
||||
|
||||
def revoke(self, lease_id: str) -> dict[str, Any]:
|
||||
lease = self._get(lease_id)
|
||||
if lease.state == "REVOKED":
|
||||
return lease.as_dict()
|
||||
updated = CredentialLease(**{**lease.as_dict(), "scopes": tuple(lease.scopes), "state": "REVOKED"})
|
||||
self._leases[lease_id] = updated
|
||||
return updated.as_dict()
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,125 @@
|
||||
"""Non-Owner Gitea machine capability and branch reconciliation boundary."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import Any, Mapping, Protocol
|
||||
|
||||
|
||||
class MachineBrokerError(RuntimeError):
|
||||
pass
|
||||
|
||||
|
||||
class GiteaTransport(Protocol):
|
||||
def compare_and_swap_branch(self, branch: str, expected_oid: str, new_oid: str) -> str: ...
|
||||
def create_or_update_pr(self, branch: str, target: str, title: str, body: str) -> Mapping[str, Any]: ...
|
||||
def publish_status(self, branch: str, state: str, description: str) -> Mapping[str, Any]: ...
|
||||
def protected_merge(self, pr_number: int) -> Mapping[str, Any]: ...
|
||||
def read_branch(self, branch: str) -> str | None: ...
|
||||
def whoami(self) -> Mapping[str, Any]: ...
|
||||
def repository(self, owner: str, name: str) -> Mapping[str, Any]: ...
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class MachineCredentialProjection:
|
||||
"""Opaque systemd credential reference; the credential value is never accepted."""
|
||||
|
||||
reference: str
|
||||
account: str
|
||||
owner_account: str = "aiman"
|
||||
|
||||
@classmethod
|
||||
def from_mapping(cls, value: Mapping[str, Any]) -> "MachineCredentialProjection":
|
||||
if set(value) != {"scheme", "name", "account"}:
|
||||
raise MachineBrokerError("CREDENTIAL_REFERENCE_INVALID")
|
||||
if value["scheme"] != "systemd-credential" or value["name"] != "gitea-machine-token":
|
||||
raise MachineBrokerError("CREDENTIAL_REFERENCE_INVALID")
|
||||
return cls(f"{value['scheme']}:{value['name']}", str(value["account"]))
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
if self.reference != "systemd-credential:gitea-machine-token":
|
||||
raise MachineBrokerError("CREDENTIAL_REFERENCE_INVALID")
|
||||
if not self.account or self.account == self.owner_account:
|
||||
raise MachineBrokerError("OWNER_MACHINE_IDENTITY_REJECTED")
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class CampaignCapabilityReceipt:
|
||||
campaign_id: str
|
||||
task_id: str
|
||||
repository: str
|
||||
branch: str
|
||||
expected_oid: str
|
||||
capabilities: frozenset[str]
|
||||
denied: frozenset[str]
|
||||
|
||||
def require(self, capability: str) -> None:
|
||||
if capability in self.denied or capability not in self.capabilities:
|
||||
raise MachineBrokerError("CAPABILITY_DENIED")
|
||||
if self.branch in {"main", "master"}:
|
||||
raise MachineBrokerError("PROTECTED_BRANCH_DENIED")
|
||||
|
||||
|
||||
class GiteaMachineBroker:
|
||||
def __init__(self, transport: GiteaTransport, credential: MachineCredentialProjection, receipt: CampaignCapabilityReceipt) -> None:
|
||||
self.transport = transport
|
||||
self.credential = credential
|
||||
self.receipt = receipt
|
||||
|
||||
def verify_identity(self) -> Mapping[str, Any]:
|
||||
identity = dict(self.transport.whoami())
|
||||
login = identity.get("login") or identity.get("username")
|
||||
if not isinstance(login, str) or not login or login == self.credential.owner_account or login != self.credential.account:
|
||||
raise MachineBrokerError("GITEA_MACHINE_IDENTITY_INVALID")
|
||||
owner, name = self.receipt.repository.split("/", 1)
|
||||
repo = dict(self.transport.repository(owner, name))
|
||||
if repo.get("full_name") != self.receipt.repository:
|
||||
raise MachineBrokerError("GITEA_REPOSITORY_MISMATCH")
|
||||
return {"login": login, "repository": self.receipt.repository, "owner_account": self.credential.owner_account}
|
||||
|
||||
def push_branch(self, new_oid: str) -> str:
|
||||
self.receipt.require("exact_task_branch_push")
|
||||
return self.transport.compare_and_swap_branch(self.receipt.branch, self.receipt.expected_oid, new_oid)
|
||||
|
||||
def create_pr(self, title: str, body: str) -> Mapping[str, Any]:
|
||||
self.receipt.require("pr_create_update")
|
||||
return self.transport.create_or_update_pr(self.receipt.branch, "main", title, body)
|
||||
|
||||
def status(self, state: str, description: str, context: str = "aios/durable-release") -> Mapping[str, Any]:
|
||||
self.receipt.require("status_publish")
|
||||
return self.transport.publish_status(self.receipt.branch, state, description, context)
|
||||
|
||||
def merge(self, pr_number: int) -> Mapping[str, Any]:
|
||||
self.receipt.require("protected_merge")
|
||||
return self.transport.protected_merge(pr_number)
|
||||
|
||||
def readback(self) -> str | None:
|
||||
self.receipt.require("remote_readback")
|
||||
return self.transport.read_branch(self.receipt.branch)
|
||||
|
||||
|
||||
def reconcile_exact_branch(authority_oid: str, gitea_oid: str | None, expected_oid: str) -> str:
|
||||
if authority_oid != expected_oid:
|
||||
return "AUTHORITY_EXPECTATION_MISMATCH"
|
||||
if gitea_oid is None:
|
||||
return "CREATE_REQUIRED"
|
||||
if gitea_oid == expected_oid:
|
||||
return "ALREADY_IN_SYNC"
|
||||
return "MANUAL_OWNER_REVIEW_REQUIRED"
|
||||
|
||||
|
||||
def reconcile_fast_forward(
|
||||
remote_oid: str | None,
|
||||
expected_old_oid: str,
|
||||
new_oid: str,
|
||||
*,
|
||||
old_is_ancestor_of_new: bool,
|
||||
) -> str:
|
||||
"""Return a bounded action for an active Job/lease branch scope."""
|
||||
if remote_oid is None:
|
||||
return "CREATE_REQUIRED"
|
||||
if remote_oid == new_oid:
|
||||
return "ALREADY_IN_SYNC"
|
||||
if remote_oid == expected_old_oid and old_is_ancestor_of_new:
|
||||
return "FAST_FORWARD_REQUIRED"
|
||||
return "MANUAL_OWNER_REVIEW_REQUIRED"
|
||||
@@ -0,0 +1,208 @@
|
||||
"""Gitea HTTP/Git transport using only a projected systemd credential."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import subprocess
|
||||
import tempfile
|
||||
from typing import Any, Mapping
|
||||
from urllib.error import HTTPError, URLError
|
||||
from urllib.parse import quote, urlencode
|
||||
from urllib.request import Request, urlopen
|
||||
|
||||
|
||||
class GiteaTransportError(RuntimeError):
|
||||
pass
|
||||
|
||||
|
||||
def _safe_oid(value: str, field: str) -> str:
|
||||
if not isinstance(value, str) or len(value) != 40 or any(ch not in "0123456789abcdef" for ch in value):
|
||||
raise GiteaTransportError(f"{field}_INVALID")
|
||||
return value
|
||||
|
||||
|
||||
@dataclass(slots=True)
|
||||
class GiteaMachineTransport:
|
||||
api_base: str
|
||||
repository_id: str
|
||||
remote_url: str
|
||||
authority_git_dir: Path
|
||||
credential_name: str = "gitea-machine-token"
|
||||
actor: str = "aios-autonomy-agent"
|
||||
timeout: int = 20
|
||||
expected_merge_head: str | None = None
|
||||
expected_merge_base: str | None = None
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
if self.credential_name != "gitea-machine-token":
|
||||
raise GiteaTransportError("CREDENTIAL_NAME_INVALID")
|
||||
if "/" not in self.repository_id or self.repository_id.startswith("/"):
|
||||
raise GiteaTransportError("REPOSITORY_INVALID")
|
||||
if not self.api_base.startswith("https://") and not self.api_base.startswith("http://127.0.0.1"):
|
||||
raise GiteaTransportError("GITEA_API_SCOPE_INVALID")
|
||||
if not self.remote_url.startswith("https://"):
|
||||
raise GiteaTransportError("GITEA_REMOTE_SCOPE_INVALID")
|
||||
|
||||
@property
|
||||
def _credential_path(self) -> Path:
|
||||
directory = os.environ.get("CREDENTIALS_DIRECTORY", "")
|
||||
if not directory:
|
||||
raise GiteaTransportError("CREDENTIAL_PROJECTION_MISSING")
|
||||
path = Path(directory) / self.credential_name
|
||||
if path.is_symlink() or not path.is_file():
|
||||
raise GiteaTransportError("CREDENTIAL_PROJECTION_MISSING")
|
||||
return path
|
||||
|
||||
def _token(self) -> str:
|
||||
value = self._credential_path.read_text(encoding="utf-8").strip()
|
||||
if not value:
|
||||
raise GiteaTransportError("CREDENTIAL_EMPTY")
|
||||
return value
|
||||
|
||||
def _request(self, method: str, path: str, body: Mapping[str, Any] | None = None) -> tuple[int, Any]:
|
||||
data = None if body is None else json.dumps(dict(body), separators=(",", ":")).encode()
|
||||
headers = {"Accept": "application/json", "Authorization": f"token {self._token()}"}
|
||||
if data is not None:
|
||||
headers["Content-Type"] = "application/json"
|
||||
request = Request(self.api_base.rstrip("/") + path, data=data, headers=headers, method=method)
|
||||
try:
|
||||
with urlopen(request, timeout=self.timeout) as response:
|
||||
raw = response.read()
|
||||
return response.status, json.loads(raw.decode()) if raw else {}
|
||||
except HTTPError as exc:
|
||||
# Never include response bodies: Gitea may echo sensitive request context.
|
||||
raise GiteaTransportError(f"GITEA_HTTP_{exc.code}") from exc
|
||||
except (URLError, TimeoutError, OSError) as exc:
|
||||
raise GiteaTransportError("GITEA_NETWORK_ERROR") from exc
|
||||
|
||||
def whoami(self) -> Mapping[str, Any]:
|
||||
return self._request("GET", "/user")[1]
|
||||
|
||||
def repository_info(self) -> Mapping[str, Any]:
|
||||
owner, name = self.repository_id.split("/", 1)
|
||||
return self._request("GET", f"/repos/{quote(owner)}/{quote(name)}")[1]
|
||||
|
||||
def repository(self, owner: str, name: str) -> Mapping[str, Any]:
|
||||
if f"{owner}/{name}" != self.repository_id:
|
||||
raise GiteaTransportError("REPOSITORY_SCOPE_DENIED")
|
||||
return self.repository_info()
|
||||
|
||||
def read_branch(self, branch: str) -> str | None:
|
||||
owner, name = self.repository_id.split("/", 1)
|
||||
try:
|
||||
value = self._request("GET", f"/repos/{quote(owner)}/{quote(name)}/branches/{quote(branch, safe='/')}")[1]
|
||||
except GiteaTransportError as exc:
|
||||
if str(exc) == "GITEA_HTTP_404":
|
||||
return None
|
||||
raise
|
||||
sha = value.get("commit", {}).get("id") if isinstance(value, dict) else None
|
||||
return _safe_oid(sha, "remote_branch_oid")
|
||||
|
||||
def _askpass(self) -> tuple[Path, dict[str, str]]:
|
||||
credential = self._credential_path
|
||||
fd, name = tempfile.mkstemp(prefix="aios-gitea-askpass-", dir="/tmp")
|
||||
path = Path(name)
|
||||
os.fchmod(fd, 0o700)
|
||||
script = "#!/bin/sh\ncase \"$1\" in *Username*) printf '%s\\n' \"$GITEA_MACHINE_ACTOR\" ;; *) cat \"$CREDENTIAL_FILE\" ;; esac\n"
|
||||
with os.fdopen(fd, "w", encoding="utf-8") as stream:
|
||||
stream.write(script)
|
||||
stream.flush()
|
||||
os.fsync(stream.fileno())
|
||||
return path, {
|
||||
"GIT_ASKPASS": str(path),
|
||||
"GIT_TERMINAL_PROMPT": "0",
|
||||
"GITEA_MACHINE_ACTOR": self.actor,
|
||||
"CREDENTIAL_FILE": str(credential),
|
||||
}
|
||||
|
||||
def _push_branch_fast_forward(self, branch: str, new_oid: str) -> None:
|
||||
askpass, additions = self._askpass()
|
||||
environment = os.environ.copy()
|
||||
environment.update(additions)
|
||||
try:
|
||||
result = subprocess.run(
|
||||
["git", f"--git-dir={self.authority_git_dir}", "push", self.remote_url, f"{new_oid}:refs/heads/{branch}"],
|
||||
text=True,
|
||||
capture_output=True,
|
||||
check=False,
|
||||
env=environment,
|
||||
timeout=self.timeout,
|
||||
)
|
||||
if result.returncode != 0:
|
||||
raise GiteaTransportError("GITEA_BRANCH_CREATE_FAILED")
|
||||
finally:
|
||||
askpass.unlink(missing_ok=True)
|
||||
|
||||
def compare_and_swap_branch(self, branch: str, expected_oid: str, new_oid: str) -> str:
|
||||
_safe_oid(expected_oid, "expected_oid")
|
||||
_safe_oid(new_oid, "new_oid")
|
||||
current = self.read_branch(branch)
|
||||
if current == new_oid:
|
||||
return new_oid
|
||||
if current is None:
|
||||
self._push_branch_fast_forward(branch, new_oid)
|
||||
readback = self.read_branch(branch)
|
||||
if readback != new_oid:
|
||||
raise GiteaTransportError("GITEA_BRANCH_CREATE_READBACK_MISMATCH")
|
||||
return readback
|
||||
if current != expected_oid:
|
||||
raise GiteaTransportError("GITEA_BRANCH_EXPECTATION_MISMATCH")
|
||||
# The preflight read binds the expected old tip. The Git receive-pack
|
||||
# then enforces the actual fast-forward CAS while transferring the new
|
||||
# objects; no force/lease override or arbitrary ref is permitted.
|
||||
self._push_branch_fast_forward(branch, new_oid)
|
||||
readback = self.read_branch(branch)
|
||||
if readback != new_oid:
|
||||
raise GiteaTransportError("GITEA_BRANCH_CAS_READBACK_MISMATCH")
|
||||
return readback
|
||||
|
||||
def _pulls(self) -> list[Mapping[str, Any]]:
|
||||
owner, name = self.repository_id.split("/", 1)
|
||||
query = urlencode({"state": "all", "base_branch": "main", "limit": 100})
|
||||
value = self._request("GET", f"/repos/{quote(owner)}/{quote(name)}/pulls?{query}")[1]
|
||||
return value if isinstance(value, list) else []
|
||||
|
||||
def create_or_update_pr(self, branch: str, target: str, title: str, body: str) -> Mapping[str, Any]:
|
||||
if target != "main":
|
||||
raise GiteaTransportError("PR_BASE_SCOPE_DENIED")
|
||||
head = self.read_branch(branch)
|
||||
if head is None:
|
||||
raise GiteaTransportError("PR_HEAD_MISSING")
|
||||
matches = [p for p in self._pulls() if (p.get("head") or {}).get("ref") == branch and (p.get("base") or {}).get("ref") == target]
|
||||
open_matches = [p for p in matches if p.get("state") == "open"]
|
||||
if len(open_matches) > 1:
|
||||
raise GiteaTransportError("MULTIPLE_MATCHING_PRS")
|
||||
if open_matches:
|
||||
if (open_matches[0].get("head") or {}).get("sha") != head:
|
||||
raise GiteaTransportError("PR_HEAD_CONFLICT")
|
||||
return open_matches[0]
|
||||
owner, name = self.repository_id.split("/", 1)
|
||||
return self._request("POST", f"/repos/{quote(owner)}/{quote(name)}/pulls", {"head": branch, "base": target, "title": title, "body": body, "allow_maintainer_edit": False})[1]
|
||||
|
||||
def publish_status(self, branch: str, state: str, description: str, context: str = "aios/durable-release") -> Mapping[str, Any]:
|
||||
oid = self.read_branch(branch)
|
||||
if oid is None:
|
||||
raise GiteaTransportError("STATUS_BRANCH_MISSING")
|
||||
owner, name = self.repository_id.split("/", 1)
|
||||
return self._request("POST", f"/repos/{quote(owner)}/{quote(name)}/statuses/{oid}", {"state": state, "context": context, "description": description})[1]
|
||||
|
||||
def protected_merge(self, pr_number: int) -> Mapping[str, Any]:
|
||||
if not isinstance(pr_number, int) or pr_number < 1:
|
||||
raise GiteaTransportError("PR_NUMBER_INVALID")
|
||||
owner, name = self.repository_id.split("/", 1)
|
||||
pr = self._request("GET", f"/repos/{quote(owner)}/{quote(name)}/pulls/{pr_number}")[1]
|
||||
if pr.get("state") != "open" or (pr.get("base") or {}).get("ref") != "main":
|
||||
raise GiteaTransportError("PR_MERGE_BINDING_INVALID")
|
||||
if pr.get("mergeable") is False:
|
||||
raise GiteaTransportError("PR_NOT_MERGEABLE")
|
||||
head_oid = (pr.get("head") or {}).get("sha")
|
||||
base_oid = (pr.get("base") or {}).get("sha")
|
||||
if self.expected_merge_head is not None and head_oid != self.expected_merge_head:
|
||||
raise GiteaTransportError("PR_HEAD_EXPECTATION_MISMATCH")
|
||||
if self.expected_merge_base is not None and base_oid != self.expected_merge_base:
|
||||
raise GiteaTransportError("PR_BASE_EXPECTATION_MISMATCH")
|
||||
body = {"do": "fast-forward-only", "force_merge": False, "merge_when_checks_succeed": False, "head_commit_id": head_oid}
|
||||
return self._request("POST", f"/repos/{quote(owner)}/{quote(name)}/pulls/{pr_number}/merge", body)[1]
|
||||
@@ -0,0 +1,93 @@
|
||||
"""Canonical Gitea transport and slash-branch verification."""
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import subprocess
|
||||
from typing import Any, Callable, Mapping, Optional
|
||||
from urllib.parse import quote
|
||||
from urllib.request import Request, urlopen
|
||||
|
||||
from aios_gitops_reconciliation_core import RemoteObservation
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class GiteaPublisherConfig:
|
||||
repository: str = "aios/aios"
|
||||
api_base: str = "https://g.cnnetmkt.top/git/api/v1"
|
||||
remote_name: str = "gitea-http"
|
||||
|
||||
|
||||
class GiteaPublisher:
|
||||
def __init__(self, workspace: Path, *, config: GiteaPublisherConfig = GiteaPublisherConfig(), credential: Any = None, api_get: Callable[[str], Any] | None = None, git_ref: Callable[[str], Optional[str]] | None = None) -> None:
|
||||
self.workspace = Path(workspace)
|
||||
self.config = config
|
||||
self.credential = credential
|
||||
self._api_get = api_get
|
||||
self._git_ref = git_ref
|
||||
|
||||
@staticmethod
|
||||
def validate_repository(repository: str) -> None:
|
||||
if repository != "aios/aios":
|
||||
raise ValueError("REPOSITORY_IDENTITY_MISMATCH")
|
||||
|
||||
@staticmethod
|
||||
def validate_branch(branch: str) -> None:
|
||||
if not isinstance(branch, str) or not branch.startswith("agent/") or branch in {"main", "master"}:
|
||||
raise ValueError("BRANCH_SCOPE_DENIED")
|
||||
|
||||
def repository_identity(self) -> str:
|
||||
self.validate_repository(self.config.repository)
|
||||
return self.config.repository
|
||||
|
||||
def _environment(self) -> dict[str, str]:
|
||||
if self.credential is not None and hasattr(self.credential, "environment"):
|
||||
return dict(self.credential.environment())
|
||||
return {}
|
||||
|
||||
def read_git_ref(self, branch: str) -> Optional[str]:
|
||||
self.validate_branch(branch)
|
||||
if self._git_ref is not None:
|
||||
return self._git_ref(branch)
|
||||
result = subprocess.run(["git", "-C", str(self.workspace), "ls-remote", self.config.remote_name, f"refs/heads/{branch}"], text=True, capture_output=True, check=False, env={**os.environ, **self._environment(), "GIT_TERMINAL_PROMPT": "0"})
|
||||
if result.returncode != 0 or not result.stdout.strip():
|
||||
return None
|
||||
return result.stdout.split()[0]
|
||||
|
||||
def _api(self, path: str) -> Any:
|
||||
if self._api_get is not None:
|
||||
return self._api_get(path)
|
||||
headers = {"Accept": "application/json"}
|
||||
env = self._environment()
|
||||
if env.get("AIOS_GITEA_TOKEN"):
|
||||
headers["Authorization"] = "token " + env["AIOS_GITEA_TOKEN"]
|
||||
with urlopen(Request(self.config.api_base.rstrip("/") + path, headers=headers), timeout=20) as response:
|
||||
return json.loads(response.read() or b"{}")
|
||||
|
||||
def read_api_branch(self, branch: str) -> Optional[str]:
|
||||
self.validate_branch(branch)
|
||||
for page in range(1, 101):
|
||||
value = self._api(f"/repos/aios/aios/branches?page={page}&limit=50")
|
||||
if not isinstance(value, list):
|
||||
return None
|
||||
for item in value:
|
||||
if isinstance(item, Mapping) and item.get("name") == branch:
|
||||
commit = item.get("commit")
|
||||
return commit.get("id") if isinstance(commit, Mapping) else None
|
||||
if len(value) < 50:
|
||||
return None
|
||||
raise RuntimeError("GITEA_BRANCH_PAGINATION_LIMIT")
|
||||
|
||||
def commit_visible(self, oid: str) -> bool:
|
||||
if not isinstance(oid, str) or len(oid) != 40:
|
||||
return False
|
||||
try:
|
||||
value = self._api(f"/repos/aios/aios/git/commits/{quote(oid, safe='')}")
|
||||
return isinstance(value, Mapping) and value.get("sha", oid) == oid
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
def observe(self, branch: str, desired_oid: str, authority_oid: Optional[str], *, expected_oid: Optional[str] = None, observed_at: str = "") -> RemoteObservation:
|
||||
return RemoteObservation(self.repository_identity(), branch, desired_oid, authority_oid, self.read_git_ref(branch), self.read_api_branch(branch), self.commit_visible(desired_oid), observed_at, expected_oid)
|
||||
@@ -0,0 +1,595 @@
|
||||
"""SQLite-backed durable GitOps Job Controller.
|
||||
|
||||
The controller owns durable intent, idempotency, lease metadata, state
|
||||
transitions, receipts, and payload references. A worker owns Git side effects
|
||||
behind the campaign capability and branch-CAS boundaries.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from enum import Enum
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import sqlite3
|
||||
import tempfile
|
||||
from datetime import datetime, timezone
|
||||
from typing import Any, Callable, Mapping
|
||||
|
||||
|
||||
class JobError(RuntimeError):
|
||||
pass
|
||||
|
||||
|
||||
class ServiceUnavailable(JobError):
|
||||
pass
|
||||
|
||||
|
||||
class BindingConflict(JobError):
|
||||
pass
|
||||
|
||||
|
||||
class JobState(str, Enum):
|
||||
REQUESTED = "REQUESTED"
|
||||
AUTHORIZED = "AUTHORIZED"
|
||||
LEASED = "LEASED"
|
||||
WORKTREE_READY = "WORKTREE_READY"
|
||||
EXECUTING = "EXECUTING"
|
||||
LOCAL_VERIFIED = "LOCAL_VERIFIED"
|
||||
REMOTE_SYNCING = "REMOTE_SYNCING"
|
||||
REMOTE_VERIFIED = "REMOTE_VERIFIED"
|
||||
COMPLETED = "COMPLETED"
|
||||
RECOVERABLE_FAILURE = "RECOVERABLE_FAILURE"
|
||||
RECONCILING = "RECONCILING"
|
||||
RECONCILIATION_REQUIRED = "RECONCILIATION_REQUIRED"
|
||||
MANUAL_OWNER_REVIEW_REQUIRED = "MANUAL_OWNER_REVIEW_REQUIRED"
|
||||
|
||||
|
||||
_ORDER = {
|
||||
JobState.REQUESTED: 0,
|
||||
JobState.AUTHORIZED: 1,
|
||||
JobState.LEASED: 2,
|
||||
JobState.WORKTREE_READY: 3,
|
||||
JobState.EXECUTING: 4,
|
||||
JobState.LOCAL_VERIFIED: 5,
|
||||
JobState.REMOTE_SYNCING: 6,
|
||||
JobState.REMOTE_VERIFIED: 7,
|
||||
JobState.COMPLETED: 8,
|
||||
}
|
||||
_RECOVERABLE = {JobState.RECOVERABLE_FAILURE, JobState.RECONCILIATION_REQUIRED}
|
||||
_TERMINAL = {JobState.COMPLETED, JobState.MANUAL_OWNER_REVIEW_REQUIRED}
|
||||
|
||||
|
||||
def utc_now() -> str:
|
||||
return datetime.now(timezone.utc).isoformat().replace("+00:00", "Z")
|
||||
|
||||
|
||||
def canonical_json(value: Any) -> bytes:
|
||||
return (
|
||||
json.dumps(value, sort_keys=True, separators=(",", ":"), ensure_ascii=False) + "\n"
|
||||
).encode("utf-8")
|
||||
|
||||
|
||||
def sha256_json(value: Any) -> str:
|
||||
return hashlib.sha256(canonical_json(value)).hexdigest()
|
||||
|
||||
|
||||
class PayloadStore:
|
||||
"""Content-addressed filesystem payloads; raw content never enters SQLite."""
|
||||
|
||||
def __init__(self, root: str | os.PathLike[str]) -> None:
|
||||
self.root = Path(root)
|
||||
if not self.root.is_absolute() or self.root.is_symlink():
|
||||
raise JobError("PAYLOAD_ROOT_INVALID")
|
||||
self.root.mkdir(parents=True, exist_ok=True, mode=0o700)
|
||||
os.chmod(self.root, 0o700)
|
||||
|
||||
def put(self, content: bytes, expected_digest: str) -> tuple[str, int]:
|
||||
digest = hashlib.sha256(content).hexdigest()
|
||||
if digest != expected_digest:
|
||||
raise JobError("PAYLOAD_DIGEST_MISMATCH")
|
||||
if len(expected_digest) != 64 or any(ch not in "0123456789abcdef" for ch in expected_digest):
|
||||
raise JobError("PAYLOAD_DIGEST_INVALID")
|
||||
target = self.root / expected_digest
|
||||
if target.is_symlink():
|
||||
raise JobError("PAYLOAD_PATH_SYMLINK")
|
||||
if target.is_file():
|
||||
if target.stat().st_size != len(content) or hashlib.sha256(target.read_bytes()).hexdigest() != digest:
|
||||
raise JobError("PAYLOAD_EXISTING_MISMATCH")
|
||||
return f"sha256/{digest}", len(content)
|
||||
fd, temporary = tempfile.mkstemp(prefix=".payload-", dir=self.root)
|
||||
try:
|
||||
os.fchmod(fd, 0o600)
|
||||
with os.fdopen(fd, "wb") as stream:
|
||||
stream.write(content)
|
||||
stream.flush()
|
||||
os.fsync(stream.fileno())
|
||||
os.replace(temporary, target)
|
||||
os.chmod(target, 0o600)
|
||||
finally:
|
||||
if os.path.exists(temporary):
|
||||
os.unlink(temporary)
|
||||
return f"sha256/{digest}", len(content)
|
||||
|
||||
def read_verified(self, reference: str, expected_digest: str, expected_size: int) -> bytes:
|
||||
if reference != f"sha256/{expected_digest}":
|
||||
raise JobError("PAYLOAD_REFERENCE_MISMATCH")
|
||||
path = self.root / expected_digest
|
||||
if path.is_symlink() or not path.is_file():
|
||||
raise JobError("PAYLOAD_MISSING")
|
||||
content = path.read_bytes()
|
||||
if len(content) != expected_size or hashlib.sha256(content).hexdigest() != expected_digest:
|
||||
raise JobError("PAYLOAD_VERIFY_FAILED")
|
||||
return content
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class JobReceipt:
|
||||
job_id: str
|
||||
idempotency_key: str
|
||||
state: JobState
|
||||
side_effect_state: str
|
||||
http_status: int
|
||||
revision: int
|
||||
receipt: Mapping[str, Any] | None = None
|
||||
|
||||
def as_dict(self) -> dict[str, Any]:
|
||||
return {
|
||||
"job_id": self.job_id,
|
||||
"idempotency_key": self.idempotency_key,
|
||||
"state": self.state.value,
|
||||
"side_effect_state": self.side_effect_state,
|
||||
"http_status": self.http_status,
|
||||
"revision": self.revision,
|
||||
"receipt": dict(self.receipt) if self.receipt else None,
|
||||
}
|
||||
|
||||
|
||||
class JobStore:
|
||||
SCHEMA_VERSION = 1
|
||||
|
||||
def __init__(self, root: str | os.PathLike[str], *, payload_root: str | os.PathLike[str] | None = None) -> None:
|
||||
self.root = Path(root)
|
||||
if not self.root.is_absolute() or self.root.is_symlink():
|
||||
raise JobError("JOB_ROOT_INVALID")
|
||||
self.root.mkdir(parents=True, exist_ok=True, mode=0o700)
|
||||
os.chmod(self.root, 0o700)
|
||||
self.db_path = self.root / "state.sqlite3"
|
||||
self.payloads = PayloadStore(payload_root or self.root.parent / "payloads" / "sha256")
|
||||
self._initialize()
|
||||
|
||||
def _connect(self) -> sqlite3.Connection:
|
||||
if self.db_path.is_symlink():
|
||||
raise ServiceUnavailable("JOB_DB_SYMLINK")
|
||||
connection = sqlite3.connect(self.db_path, timeout=30.0)
|
||||
connection.row_factory = sqlite3.Row
|
||||
connection.execute("PRAGMA journal_mode=WAL")
|
||||
connection.execute("PRAGMA synchronous=FULL")
|
||||
connection.execute("PRAGMA foreign_keys=ON")
|
||||
connection.execute("PRAGMA busy_timeout=30000")
|
||||
return connection
|
||||
|
||||
def _initialize(self) -> None:
|
||||
try:
|
||||
with self._connect() as connection:
|
||||
connection.executescript(
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS metadata (key TEXT PRIMARY KEY, value TEXT NOT NULL);
|
||||
CREATE TABLE IF NOT EXISTS jobs (
|
||||
job_id TEXT PRIMARY KEY, idempotency_key TEXT NOT NULL UNIQUE,
|
||||
campaign_id TEXT NOT NULL, repository TEXT NOT NULL, operation TEXT NOT NULL,
|
||||
branch TEXT NOT NULL, expected_branch_oid TEXT NOT NULL,
|
||||
payload_digest TEXT NOT NULL, payload_ref TEXT, payload_size INTEGER,
|
||||
state TEXT NOT NULL, side_effect_state TEXT NOT NULL,
|
||||
revision INTEGER NOT NULL, request_json TEXT NOT NULL,
|
||||
receipt_json TEXT, created_at TEXT NOT NULL, updated_at TEXT NOT NULL
|
||||
);
|
||||
CREATE TABLE IF NOT EXISTS idempotency_keys (
|
||||
key TEXT PRIMARY KEY, job_id TEXT NOT NULL UNIQUE,
|
||||
binding_json TEXT NOT NULL, created_at TEXT NOT NULL,
|
||||
FOREIGN KEY(job_id) REFERENCES jobs(job_id)
|
||||
);
|
||||
CREATE TABLE IF NOT EXISTS branch_leases (
|
||||
lease_id TEXT PRIMARY KEY, job_id TEXT NOT NULL UNIQUE,
|
||||
branch TEXT NOT NULL, lineage_anchor_oid TEXT NOT NULL,
|
||||
current_head_oid TEXT NOT NULL, generation INTEGER NOT NULL,
|
||||
lease_expires_at TEXT NOT NULL, state TEXT NOT NULL,
|
||||
revision INTEGER NOT NULL, FOREIGN KEY(job_id) REFERENCES jobs(job_id)
|
||||
);
|
||||
CREATE TABLE IF NOT EXISTS state_transitions (
|
||||
sequence INTEGER PRIMARY KEY AUTOINCREMENT, job_id TEXT NOT NULL,
|
||||
from_state TEXT, to_state TEXT NOT NULL, revision INTEGER NOT NULL,
|
||||
side_effect_state TEXT NOT NULL, created_at TEXT NOT NULL,
|
||||
FOREIGN KEY(job_id) REFERENCES jobs(job_id)
|
||||
);
|
||||
CREATE TABLE IF NOT EXISTS receipts (
|
||||
receipt_id TEXT PRIMARY KEY, job_id TEXT NOT NULL,
|
||||
kind TEXT NOT NULL, digest TEXT NOT NULL, payload_json TEXT NOT NULL,
|
||||
created_at TEXT NOT NULL, FOREIGN KEY(job_id) REFERENCES jobs(job_id)
|
||||
);
|
||||
INSERT OR REPLACE INTO metadata(key, value) VALUES('schema_version', '1');
|
||||
"""
|
||||
)
|
||||
except sqlite3.Error as exc:
|
||||
raise ServiceUnavailable("JOB_DB_UNAVAILABLE") from exc
|
||||
|
||||
def _mirror(self, job: Mapping[str, Any]) -> None:
|
||||
directory = self.root / str(job["job_id"])
|
||||
directory.mkdir(mode=0o700, exist_ok=True)
|
||||
(directory / "receipts").mkdir(mode=0o700, exist_ok=True)
|
||||
(directory / "logs-digest").mkdir(mode=0o700, exist_ok=True)
|
||||
for name, value in (("request.json", job["request"]), ("state.json", job)):
|
||||
target = directory / name
|
||||
fd, temporary = tempfile.mkstemp(prefix=".mirror-", dir=directory)
|
||||
try:
|
||||
os.fchmod(fd, 0o600)
|
||||
with os.fdopen(fd, "wb") as stream:
|
||||
stream.write(canonical_json(value))
|
||||
stream.flush()
|
||||
os.fsync(stream.fileno())
|
||||
os.replace(temporary, target)
|
||||
finally:
|
||||
if os.path.exists(temporary):
|
||||
os.unlink(temporary)
|
||||
|
||||
def read(self, job_id: str) -> dict[str, Any]:
|
||||
try:
|
||||
with self._connect() as connection:
|
||||
row = connection.execute("SELECT * FROM jobs WHERE job_id=?", (job_id,)).fetchone()
|
||||
except sqlite3.Error as exc:
|
||||
raise ServiceUnavailable("JOB_DB_UNAVAILABLE") from exc
|
||||
if row is None:
|
||||
raise JobError("JOB_NOT_FOUND")
|
||||
return dict(row)
|
||||
|
||||
def _receipt(self, row: Mapping[str, Any], *, duplicate: bool) -> JobReceipt:
|
||||
state = JobState(row["state"])
|
||||
if state == JobState.COMPLETED:
|
||||
status, side = 200, "COMPLETED"
|
||||
elif state in _RECOVERABLE:
|
||||
status = 202
|
||||
side = row["side_effect_state"] if row["side_effect_state"] == "REMOTE_PUSH_PENDING" else "RESUME_SAME_JOB"
|
||||
elif row["side_effect_state"] == "UNKNOWN":
|
||||
status, side = 503, "UNKNOWN"
|
||||
else:
|
||||
status, side = 202, "SAME_JOB" if duplicate else "QUEUED"
|
||||
receipt = json.loads(row["receipt_json"]) if row["receipt_json"] else None
|
||||
return JobReceipt(row["job_id"], row["idempotency_key"], state, side, status, int(row["revision"]), receipt)
|
||||
|
||||
|
||||
class GitOpsJobController:
|
||||
def __init__(self, store: JobStore) -> None:
|
||||
self.store = store
|
||||
|
||||
@staticmethod
|
||||
def _binding(request: Mapping[str, Any]) -> dict[str, str]:
|
||||
fields = ("campaign_id", "repository", "operation", "branch", "expected_branch_oid", "payload_digest")
|
||||
if any(not isinstance(request.get(field), str) or not request[field].strip() for field in fields):
|
||||
raise JobError("JOB_BINDING_INVALID")
|
||||
oid = request["expected_branch_oid"]
|
||||
digest = request["payload_digest"]
|
||||
if len(oid) != 40 or any(ch not in "0123456789abcdef" for ch in oid):
|
||||
raise JobError("EXPECTED_BRANCH_OID_INVALID")
|
||||
if len(digest) != 64 or any(ch not in "0123456789abcdef" for ch in digest):
|
||||
raise JobError("PAYLOAD_DIGEST_INVALID")
|
||||
if request["branch"] in {"main", "master"} or request["branch"].startswith(("refs/", "tags/")):
|
||||
raise JobError("PROTECTED_BRANCH_REJECTED")
|
||||
binding = {field: str(request[field]) for field in fields}
|
||||
# Operation-specific, non-secret binding inputs must survive the
|
||||
# durable round trip and participate in idempotency. Arbitrary request
|
||||
# fields are intentionally discarded rather than persisted.
|
||||
optional = (
|
||||
"idempotency_key",
|
||||
"task_id",
|
||||
"new_branch_oid",
|
||||
"authorization_recovery_digest",
|
||||
"lineage_anchor_oid",
|
||||
"base_oid",
|
||||
"branch_mode",
|
||||
"generation",
|
||||
"expected_main_oid",
|
||||
"pr_number",
|
||||
"target_branch",
|
||||
"check_context",
|
||||
"check_state",
|
||||
"check_description",
|
||||
)
|
||||
for field in optional:
|
||||
value = request.get(field)
|
||||
if value is None:
|
||||
continue
|
||||
if isinstance(value, bool) or not isinstance(value, (str, int)):
|
||||
raise JobError("JOB_BINDING_OPTION_INVALID")
|
||||
text = str(value).strip()
|
||||
if not text or len(text) > 512:
|
||||
raise JobError("JOB_BINDING_OPTION_INVALID")
|
||||
binding[field] = text
|
||||
return binding
|
||||
|
||||
def submit(self, request: Mapping[str, Any], *, payload: bytes | None = None) -> JobReceipt:
|
||||
binding = self._binding(request)
|
||||
payload_ref = request.get("payload_ref")
|
||||
payload_size = request.get("payload_size")
|
||||
if payload is not None:
|
||||
payload_ref, payload_size = self.store.payloads.put(payload, binding["payload_digest"])
|
||||
if payload_ref is not None and payload_ref != f"sha256/{binding['payload_digest']}":
|
||||
raise JobError("PAYLOAD_REFERENCE_MISMATCH")
|
||||
if payload_ref is not None and (not isinstance(payload_size, int) or payload_size < 0):
|
||||
raise JobError("PAYLOAD_SIZE_INVALID")
|
||||
client_idempotency_key = binding.get("idempotency_key")
|
||||
if client_idempotency_key is not None:
|
||||
if len(client_idempotency_key) > 128 or not all(
|
||||
character.isalnum() or character in "._:-" for character in client_idempotency_key
|
||||
):
|
||||
raise JobError("IDEMPOTENCY_KEY_INVALID")
|
||||
key = sha256_json({"client_idempotency_key": client_idempotency_key})
|
||||
else:
|
||||
key = sha256_json(binding)
|
||||
job_id = "job-" + key[:32]
|
||||
durable_request = dict(binding)
|
||||
if payload_ref is not None:
|
||||
durable_request.update({"payload_ref": str(payload_ref), "payload_size": int(payload_size)})
|
||||
now = utc_now()
|
||||
try:
|
||||
with self.store._connect() as connection:
|
||||
connection.execute("BEGIN IMMEDIATE")
|
||||
existing = connection.execute("SELECT * FROM jobs WHERE idempotency_key=?", (key,)).fetchone()
|
||||
if existing is not None:
|
||||
if json.loads(existing["request_json"]).get("binding") != binding:
|
||||
raise BindingConflict("BINDING_CONFLICT")
|
||||
row = dict(existing)
|
||||
connection.commit()
|
||||
return self.store._receipt(row, duplicate=True)
|
||||
connection.execute(
|
||||
"INSERT INTO jobs(job_id,idempotency_key,campaign_id,repository,operation,branch,expected_branch_oid,payload_digest,payload_ref,payload_size,state,side_effect_state,revision,request_json,receipt_json,created_at,updated_at) VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)",
|
||||
(job_id, key, binding["campaign_id"], binding["repository"], binding["operation"], binding["branch"], binding["expected_branch_oid"], binding["payload_digest"], payload_ref, payload_size, JobState.REQUESTED.value, "NONE", 0, json.dumps({"binding": binding, **durable_request}, sort_keys=True), None, now, now),
|
||||
)
|
||||
connection.execute("INSERT INTO idempotency_keys(key,job_id,binding_json,created_at) VALUES(?,?,?,?)", (key, job_id, json.dumps(binding, sort_keys=True), now))
|
||||
connection.execute("INSERT INTO state_transitions(job_id,from_state,to_state,revision,side_effect_state,created_at) VALUES(?,?,?,?,?,?)", (job_id, None, JobState.REQUESTED.value, 0, "NONE", now))
|
||||
row = dict(connection.execute("SELECT * FROM jobs WHERE job_id=?", (job_id,)).fetchone())
|
||||
connection.commit()
|
||||
except BindingConflict:
|
||||
raise
|
||||
except sqlite3.IntegrityError:
|
||||
row = self.store.read(job_id)
|
||||
return self.store._receipt(row, duplicate=True)
|
||||
except sqlite3.Error as exc:
|
||||
raise ServiceUnavailable("JOB_DB_UNAVAILABLE") from exc
|
||||
row["request"] = json.loads(row["request_json"])
|
||||
self.store._mirror(row)
|
||||
return self.store._receipt(row, duplicate=False)
|
||||
|
||||
def lease_job(
|
||||
self,
|
||||
job_id: str,
|
||||
*,
|
||||
lease_id: str,
|
||||
lineage_anchor_oid: str,
|
||||
current_head_oid: str,
|
||||
generation: int,
|
||||
lease_expires_at: str,
|
||||
expected_revision: int,
|
||||
) -> JobReceipt:
|
||||
"""Atomically create a branch lease and move its Job to LEASED."""
|
||||
if generation < 1 or any(len(oid) != 40 for oid in (lineage_anchor_oid, current_head_oid)):
|
||||
raise JobError("LEASE_BINDING_INVALID")
|
||||
try:
|
||||
with self.store._connect() as connection:
|
||||
connection.execute("BEGIN IMMEDIATE")
|
||||
row = connection.execute("SELECT * FROM jobs WHERE job_id=?", (job_id,)).fetchone()
|
||||
if row is None:
|
||||
raise JobError("JOB_NOT_FOUND")
|
||||
if int(row["revision"]) != expected_revision:
|
||||
raise JobError("REVISION_CAS_CONFLICT")
|
||||
if row["branch"] in {"main", "master"}:
|
||||
raise JobError("PROTECTED_BRANCH_REJECTED")
|
||||
now = utc_now()
|
||||
connection.execute("INSERT INTO branch_leases(lease_id,job_id,branch,lineage_anchor_oid,current_head_oid,generation,lease_expires_at,state,revision) VALUES(?,?,?,?,?,?,?,?,?)", (lease_id, job_id, row["branch"], lineage_anchor_oid, current_head_oid, generation, lease_expires_at, "ACTIVE", 0))
|
||||
next_revision = expected_revision + 1
|
||||
updated = connection.execute("UPDATE jobs SET state=?,side_effect_state=?,revision=?,updated_at=? WHERE job_id=? AND revision=?", (JobState.LEASED.value, "LEASED", next_revision, now, job_id, expected_revision))
|
||||
if updated.rowcount != 1:
|
||||
raise JobError("REVISION_CAS_CONFLICT")
|
||||
connection.execute("INSERT INTO state_transitions(job_id,from_state,to_state,revision,side_effect_state,created_at) VALUES(?,?,?,?,?,?)", (job_id, row["state"], JobState.LEASED.value, next_revision, "LEASED", now))
|
||||
result = dict(connection.execute("SELECT * FROM jobs WHERE job_id=?", (job_id,)).fetchone())
|
||||
connection.commit()
|
||||
except sqlite3.IntegrityError as exc:
|
||||
raise JobError("LEASE_CONFLICT") from exc
|
||||
except (JobError, BindingConflict):
|
||||
raise
|
||||
except sqlite3.Error as exc:
|
||||
raise ServiceUnavailable("JOB_DB_UNAVAILABLE") from exc
|
||||
result["request"] = json.loads(result["request_json"])
|
||||
self.store._mirror(result)
|
||||
return self.store._receipt(result, duplicate=True)
|
||||
|
||||
def update_lease(self, lease_id: str, *, expected_revision: int, current_head_oid: str, generation: int, state: str = "ACTIVE") -> None:
|
||||
if len(current_head_oid) != 40 or generation < 1:
|
||||
raise JobError("LEASE_BINDING_INVALID")
|
||||
try:
|
||||
with self.store._connect() as connection:
|
||||
connection.execute("BEGIN IMMEDIATE")
|
||||
updated = connection.execute("UPDATE branch_leases SET current_head_oid=?,generation=?,state=?,revision=revision+1 WHERE lease_id=? AND revision=?", (current_head_oid, generation, state, lease_id, expected_revision))
|
||||
if updated.rowcount != 1:
|
||||
raise JobError("LEASE_REVISION_CAS_CONFLICT")
|
||||
connection.commit()
|
||||
except JobError:
|
||||
raise
|
||||
except sqlite3.Error as exc:
|
||||
raise ServiceUnavailable("JOB_DB_UNAVAILABLE") from exc
|
||||
|
||||
def advance(self, job_id: str, state: JobState, *, side_effect_state: str, receipt: Mapping[str, Any] | None = None, expected_revision: int | None = None) -> JobReceipt:
|
||||
return self._advance(
|
||||
job_id,
|
||||
state,
|
||||
side_effect_state=side_effect_state,
|
||||
receipt=receipt,
|
||||
expected_revision=expected_revision,
|
||||
)
|
||||
|
||||
def _advance(
|
||||
self,
|
||||
job_id: str,
|
||||
state: JobState,
|
||||
*,
|
||||
side_effect_state: str,
|
||||
receipt: Mapping[str, Any] | None = None,
|
||||
expected_revision: int | None = None,
|
||||
_allow_verified_external_recovery: bool = False,
|
||||
) -> JobReceipt:
|
||||
try:
|
||||
with self.store._connect() as connection:
|
||||
connection.execute("BEGIN IMMEDIATE")
|
||||
row = connection.execute("SELECT * FROM jobs WHERE job_id=?", (job_id,)).fetchone()
|
||||
if row is None:
|
||||
raise JobError("JOB_NOT_FOUND")
|
||||
previous = JobState(row["state"])
|
||||
revision = int(row["revision"])
|
||||
if expected_revision is not None and expected_revision != revision:
|
||||
raise JobError("REVISION_CAS_CONFLICT")
|
||||
recovery_transition = (
|
||||
_allow_verified_external_recovery
|
||||
and previous == JobState.MANUAL_OWNER_REVIEW_REQUIRED
|
||||
and state == JobState.COMPLETED
|
||||
)
|
||||
if previous in _TERMINAL and state != previous and not recovery_transition:
|
||||
raise JobError("JOB_TERMINAL")
|
||||
if state not in _RECOVERABLE | {JobState.RECONCILING, JobState.MANUAL_OWNER_REVIEW_REQUIRED}:
|
||||
if previous in _RECOVERABLE:
|
||||
raise JobError("RECONCILE_REQUIRED")
|
||||
if previous != JobState.RECONCILING and _ORDER.get(state, -1) < _ORDER.get(previous, -1):
|
||||
raise JobError("JOB_STATE_REGRESSION")
|
||||
next_revision = revision + 1
|
||||
receipt_json = json.dumps(dict(receipt), sort_keys=True) if receipt is not None else row["receipt_json"]
|
||||
now = utc_now()
|
||||
updated = connection.execute("UPDATE jobs SET state=?,side_effect_state=?,revision=?,receipt_json=?,updated_at=? WHERE job_id=? AND revision=?", (state.value, side_effect_state, next_revision, receipt_json, now, job_id, revision))
|
||||
if updated.rowcount != 1:
|
||||
raise JobError("REVISION_CAS_CONFLICT")
|
||||
connection.execute("INSERT INTO state_transitions(job_id,from_state,to_state,revision,side_effect_state,created_at) VALUES(?,?,?,?,?,?)", (job_id, previous.value, state.value, next_revision, side_effect_state, now))
|
||||
if receipt is not None:
|
||||
digest = hashlib.sha256(canonical_json(receipt)).hexdigest()
|
||||
connection.execute("INSERT INTO receipts(receipt_id,job_id,kind,digest,payload_json,created_at) VALUES(?,?,?,?,?,?)", (f"receipt-{job_id}-{next_revision}", job_id, state.value, digest, json.dumps(dict(receipt), sort_keys=True), now))
|
||||
result = dict(connection.execute("SELECT * FROM jobs WHERE job_id=?", (job_id,)).fetchone())
|
||||
connection.commit()
|
||||
except (JobError, BindingConflict):
|
||||
raise
|
||||
except sqlite3.Error as exc:
|
||||
raise ServiceUnavailable("JOB_DB_UNAVAILABLE") from exc
|
||||
result["request"] = json.loads(result["request_json"])
|
||||
self.store._mirror(result)
|
||||
return self.store._receipt(result, duplicate=True)
|
||||
|
||||
@staticmethod
|
||||
def _validate_verified_external_merge_proof(
|
||||
job_id: str,
|
||||
binding: Mapping[str, Any],
|
||||
proof: Mapping[str, Any],
|
||||
) -> dict[str, Any]:
|
||||
if not isinstance(proof, Mapping):
|
||||
raise JobError("EXTERNAL_SIDE_EFFECT_PROOF_INVALID")
|
||||
|
||||
def require_oid(field: str) -> str:
|
||||
value = proof.get(field)
|
||||
if not isinstance(value, str) or len(value) != 40 or any(ch not in "0123456789abcdef" for ch in value):
|
||||
raise JobError("EXTERNAL_SIDE_EFFECT_PROOF_INVALID")
|
||||
return value
|
||||
|
||||
if proof.get("proof_schema") != "aios.job-recovery.v1":
|
||||
raise JobError("EXTERNAL_SIDE_EFFECT_PROOF_INVALID")
|
||||
if proof.get("job_id") != job_id:
|
||||
raise JobError("EXTERNAL_SIDE_EFFECT_PROOF_MISMATCH")
|
||||
if proof.get("operation") != "PROTECTED_MERGE" or binding.get("operation") != "PROTECTED_MERGE":
|
||||
raise JobError("EXTERNAL_SIDE_EFFECT_OPERATION_REJECTED")
|
||||
if proof.get("pr_number") != 53 or str(binding.get("pr_number")) != "53":
|
||||
raise JobError("EXTERNAL_SIDE_EFFECT_PROOF_MISMATCH")
|
||||
if proof.get("pr_merged") is not True or proof.get("protected_merge") is not True:
|
||||
raise JobError("EXTERNAL_SIDE_EFFECT_PROOF_MISMATCH")
|
||||
if proof.get("force_push") is not False:
|
||||
raise JobError("EXTERNAL_SIDE_EFFECT_PROOF_MISMATCH")
|
||||
if proof.get("machine_actor") != "aios-autonomy-agent":
|
||||
raise JobError("EXTERNAL_SIDE_EFFECT_PROOF_MISMATCH")
|
||||
|
||||
expected_head = str(binding.get("expected_branch_oid", ""))
|
||||
expected_base = str(binding.get("expected_main_oid", ""))
|
||||
head_oid = require_oid("head_oid")
|
||||
base_oid = require_oid("base_oid")
|
||||
merged_main = require_oid("main_oid_after_protected_merge")
|
||||
gitea_merged_main = require_oid("gitea_main_oid_after_protected_merge")
|
||||
authority_merged_main = require_oid("authority_main_oid_after_protected_merge")
|
||||
current_gitea = require_oid("current_gitea_main_oid")
|
||||
current_authority = require_oid("current_authority_main_oid")
|
||||
if head_oid != expected_head or base_oid != expected_base:
|
||||
raise JobError("EXTERNAL_SIDE_EFFECT_PROOF_MISMATCH")
|
||||
if not (merged_main == gitea_merged_main == authority_merged_main == head_oid):
|
||||
raise JobError("EXTERNAL_SIDE_EFFECT_PROOF_MISMATCH")
|
||||
if current_gitea != current_authority or proof.get("current_main_descends_from_protected_merge") is not True:
|
||||
raise JobError("EXTERNAL_SIDE_EFFECT_PROOF_MISMATCH")
|
||||
return dict(proof)
|
||||
|
||||
def complete_verified_external_side_effect(
|
||||
self,
|
||||
job_id: str,
|
||||
proof: Mapping[str, Any],
|
||||
*,
|
||||
expected_revision: int | None = None,
|
||||
) -> JobReceipt:
|
||||
"""Close one historical manual job from independently verified proof.
|
||||
|
||||
This is deliberately narrower than advance: it is only valid for the
|
||||
known protected PR53 recovery shape and preserves the old receipt.
|
||||
The actual state change still uses the normal revision-CAS path.
|
||||
"""
|
||||
current = self.store.read(job_id)
|
||||
revision = int(current["revision"])
|
||||
if expected_revision is not None and expected_revision != revision:
|
||||
raise JobError("REVISION_CAS_CONFLICT")
|
||||
request = json.loads(current["request_json"])
|
||||
binding = request.get("binding", request)
|
||||
normalized_proof = self._validate_verified_external_merge_proof(job_id, binding, proof)
|
||||
existing_receipt = json.loads(current["receipt_json"]) if current["receipt_json"] else None
|
||||
if JobState(current["state"]) == JobState.COMPLETED:
|
||||
if (
|
||||
existing_receipt
|
||||
and existing_receipt.get("recovered_from_historical_manual") is True
|
||||
and existing_receipt.get("proof") == normalized_proof
|
||||
):
|
||||
return self.store._receipt(current, duplicate=True)
|
||||
raise JobError("JOB_TERMINAL")
|
||||
if JobState(current["state"]) != JobState.MANUAL_OWNER_REVIEW_REQUIRED:
|
||||
raise JobError("EXTERNAL_SIDE_EFFECT_RECOVERY_STATE_INVALID")
|
||||
recovery_receipt = {
|
||||
"recovered_from_historical_manual": True,
|
||||
"recovery_kind": "VERIFIED_EXTERNAL_SIDE_EFFECT",
|
||||
"historical_receipt": existing_receipt,
|
||||
"proof": normalized_proof,
|
||||
}
|
||||
return self._advance(
|
||||
job_id,
|
||||
JobState.COMPLETED,
|
||||
side_effect_state="REMOTE_VERIFIED",
|
||||
receipt=recovery_receipt,
|
||||
expected_revision=revision,
|
||||
_allow_verified_external_recovery=True,
|
||||
)
|
||||
|
||||
def reconcile(self, job_id: str, observer: Callable[[Mapping[str, Any]], Mapping[str, Any]]) -> JobReceipt:
|
||||
current = self.store.read(job_id)
|
||||
if JobState(current["state"]) not in _RECOVERABLE:
|
||||
return self.store._receipt(current, duplicate=True)
|
||||
self.advance(job_id, JobState.RECONCILING, side_effect_state="IN_PROGRESS")
|
||||
try:
|
||||
observed = dict(observer(current))
|
||||
stable = JobState(observed.get("state", JobState.REQUESTED.value))
|
||||
if stable in {JobState.RECOVERABLE_FAILURE, JobState.RECONCILING}:
|
||||
raise JobError("RECONCILIATION_UNSTABLE")
|
||||
return self.advance(job_id, stable, side_effect_state=str(observed.get("side_effect_state", "NONE")), receipt=observed.get("receipt"))
|
||||
except Exception as exc:
|
||||
self.advance(job_id, JobState.RECONCILIATION_REQUIRED, side_effect_state="UNKNOWN", receipt={"error_code": type(exc).__name__})
|
||||
raise JobError("RECONCILIATION_REQUIRED") from exc
|
||||
|
||||
def resume(self, job_id: str) -> JobReceipt:
|
||||
current = self.store.read(job_id)
|
||||
if JobState(current["state"]) in _TERMINAL:
|
||||
raise JobError("JOB_TERMINAL")
|
||||
return self.reconcile(job_id, lambda _current: {"state": JobState.REQUESTED.value, "side_effect_state": "RESUME_SAME_JOB"})
|
||||
@@ -0,0 +1,279 @@
|
||||
"""Durable Job side-effect executor for the AIOS campaign control plane."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import base64
|
||||
from dataclasses import dataclass, replace
|
||||
import json
|
||||
from pathlib import Path
|
||||
from typing import Any, Mapping
|
||||
|
||||
from aios_authority_git_executor import AuthorityGitExecutor, AuthorityGitError
|
||||
from aios_campaign_recovery import RecoveryEvidenceError, load_recovered_authorization
|
||||
from aios_gitea_machine_broker import CampaignCapabilityReceipt, GiteaMachineBroker, MachineBrokerError
|
||||
from aios_gitops_job_controller import GitOpsJobController, JobError, JobState, JobStore
|
||||
|
||||
|
||||
TASK_ID = "AIOS_AUTONOMOUS_RELEASE_CAMPAIGN_V1"
|
||||
CAMPAIGN_ID = "AIOS-AUTONOMOUS-RELEASE-CAMPAIGN-20260807"
|
||||
REPOSITORY = "aios/aios"
|
||||
BRANCH = "agent/autonomous-release-campaign-v1-20260807"
|
||||
EXPECTED_BRANCH_OID = "bba675fab3b7e0c5fba7066720252b3c91b9f20d"
|
||||
AUTHORIZATION_DIGEST = "6b0bf550eb702126e967eeff2d561fa808444b924d8a71f5399accad508ed3b2"
|
||||
LINEAGE_ANCHOR = "69b45e81dcd2c3707c367003b08c55fd361faae8"
|
||||
|
||||
|
||||
class JobExecutionError(RuntimeError):
|
||||
pass
|
||||
|
||||
|
||||
@dataclass(slots=True)
|
||||
class DurableJobExecutor:
|
||||
controller: GitOpsJobController
|
||||
authority: AuthorityGitExecutor
|
||||
broker: GiteaMachineBroker
|
||||
recovery_path: Path
|
||||
|
||||
def _request(self, job_id: str) -> tuple[dict[str, Any], dict[str, Any]]:
|
||||
row = self.controller.store.read(job_id)
|
||||
try:
|
||||
request = json.loads(row["request_json"])
|
||||
except (KeyError, TypeError, json.JSONDecodeError) as exc:
|
||||
raise JobExecutionError("JOB_REQUEST_INVALID") from exc
|
||||
return row, dict(request.get("binding") or {})
|
||||
|
||||
def _validate_campaign(self, request: Mapping[str, Any]) -> str:
|
||||
operation = str(request.get("operation", ""))
|
||||
expected_branch_oid = request.get("expected_branch_oid")
|
||||
required = {
|
||||
"task_id": TASK_ID,
|
||||
"campaign_id": CAMPAIGN_ID,
|
||||
"repository": REPOSITORY,
|
||||
"branch": BRANCH,
|
||||
"authorization_recovery_digest": AUTHORIZATION_DIGEST,
|
||||
}
|
||||
for key, expected in required.items():
|
||||
if request.get(key) != expected:
|
||||
raise JobExecutionError(f"JOB_{key.upper()}_MISMATCH")
|
||||
new_oid = request.get("new_branch_oid")
|
||||
if not isinstance(new_oid, str) or len(new_oid) != 40 or any(ch not in "0123456789abcdef" for ch in new_oid):
|
||||
raise JobExecutionError("NEW_BRANCH_OID_INVALID")
|
||||
if not isinstance(expected_branch_oid, str) or len(expected_branch_oid) != 40 or any(ch not in "0123456789abcdef" for ch in expected_branch_oid):
|
||||
raise JobExecutionError("EXPECTED_BRANCH_OID_INVALID")
|
||||
if operation == "ADVANCE_EXISTING_CAMPAIGN_BRANCH" and expected_branch_oid == new_oid:
|
||||
raise JobExecutionError("EXPECTED_BRANCH_OID_INVALID")
|
||||
if operation != "ADVANCE_EXISTING_CAMPAIGN_BRANCH" and expected_branch_oid != new_oid:
|
||||
raise JobExecutionError("POST_ADVANCE_HEAD_BINDING_INVALID")
|
||||
try:
|
||||
recovery = load_recovered_authorization(self.recovery_path)
|
||||
except RecoveryEvidenceError as exc:
|
||||
raise JobExecutionError(str(exc)) from exc
|
||||
if recovery["digest"] != AUTHORIZATION_DIGEST:
|
||||
raise JobExecutionError("RECOVERY_DIGEST_MISMATCH")
|
||||
for key, expected in required.items():
|
||||
if key == "campaign_id":
|
||||
actual = recovery.get(key)
|
||||
elif key == "authorization_recovery_digest":
|
||||
continue
|
||||
else:
|
||||
actual = recovery.get({"expected_branch_oid": "expected_branch_oid"}.get(key, key))
|
||||
if actual is not None and actual != expected:
|
||||
raise JobExecutionError("RECOVERY_BINDING_MISMATCH")
|
||||
if recovery.get("expected_branch_oid") != EXPECTED_BRANCH_OID:
|
||||
raise JobExecutionError("RECOVERY_BRANCH_OID_MISMATCH")
|
||||
authority_head = self.authority.read_ref(f"refs/heads/{BRANCH}")
|
||||
if authority_head not in {expected_branch_oid, new_oid}:
|
||||
raise JobExecutionError("AUTHORITY_BRANCH_EXPECTATION_MISMATCH")
|
||||
if not self.authority.ancestry_check(expected_branch_oid, new_oid):
|
||||
raise JobExecutionError("NEW_OID_NOT_DESCENDANT")
|
||||
if not self.authority.ancestry_check(EXPECTED_BRANCH_OID, new_oid):
|
||||
raise JobExecutionError("NEW_OID_OUTSIDE_AUTHORIZED_LINEAGE")
|
||||
identity = self.broker.verify_identity()
|
||||
if identity.get("repository") != REPOSITORY or identity.get("login") == "aiman":
|
||||
raise JobExecutionError("MACHINE_IDENTITY_SCOPE_INVALID")
|
||||
return new_oid
|
||||
|
||||
def dry_run(self, job_id: str) -> dict[str, Any]:
|
||||
_row, request = self._request(job_id)
|
||||
new_oid = self._validate_campaign(request)
|
||||
remote = self.broker.readback()
|
||||
if remote is None:
|
||||
action = "CREATE_REQUIRED"
|
||||
elif remote == new_oid:
|
||||
action = "ALREADY_IN_SYNC"
|
||||
elif remote == request["expected_branch_oid"]:
|
||||
action = "FAST_FORWARD_REQUIRED"
|
||||
else:
|
||||
action = "MANUAL_OWNER_REVIEW_REQUIRED"
|
||||
if action == "MANUAL_OWNER_REVIEW_REQUIRED":
|
||||
raise JobExecutionError("GITEA_BRANCH_CONFLICT")
|
||||
return {
|
||||
"decision": "SAFE_TO_ADVANCE",
|
||||
"authority_action": "CAS_REQUIRED",
|
||||
"gitea_action": action,
|
||||
"previous_branch_oid": request["expected_branch_oid"],
|
||||
"final_branch_oid": new_oid,
|
||||
"force_push": False,
|
||||
}
|
||||
|
||||
def _recover_requested(self, job_id: str) -> dict[str, Any]:
|
||||
row = self.controller.store.read(job_id)
|
||||
if JobState(row["state"]) in {JobState.RECOVERABLE_FAILURE, JobState.RECONCILIATION_REQUIRED}:
|
||||
self.controller.resume(job_id)
|
||||
row = self.controller.store.read(job_id)
|
||||
return row
|
||||
|
||||
def execute(self, job_id: str, *, dry_run: bool = False) -> dict[str, Any]:
|
||||
try:
|
||||
return self._execute_unprotected(job_id, dry_run=dry_run)
|
||||
except Exception as exc:
|
||||
try:
|
||||
row = self.controller.store.read(job_id)
|
||||
state = JobState(row["state"])
|
||||
if state not in {JobState.COMPLETED, JobState.MANUAL_OWNER_REVIEW_REQUIRED, JobState.RECOVERABLE_FAILURE, JobState.RECONCILIATION_REQUIRED}:
|
||||
self.controller.advance(job_id, JobState.RECOVERABLE_FAILURE, side_effect_state="UNKNOWN", receipt={"error_code": type(exc).__name__})
|
||||
except Exception:
|
||||
pass
|
||||
raise
|
||||
|
||||
def _execute_unprotected(self, job_id: str, *, dry_run: bool = False) -> dict[str, Any]:
|
||||
row, request = self._request(job_id)
|
||||
operation = request.get("operation")
|
||||
if operation == "ADVANCE_EXISTING_CAMPAIGN_BRANCH":
|
||||
if dry_run:
|
||||
return self.dry_run(job_id)
|
||||
return self._advance_campaign(job_id)
|
||||
if operation == "COMMIT_FILE":
|
||||
return self._commit_file(job_id, request)
|
||||
if operation == "SYNC_TASK_BRANCH_TO_GITEA":
|
||||
new_oid = self._validate_campaign(request)
|
||||
final = self.broker.push_branch(new_oid)
|
||||
return {"operation": operation, "remote_verified": final == new_oid, "final_network_oid": final}
|
||||
if operation == "CREATE_PR":
|
||||
self._validate_campaign(request)
|
||||
return dict(self.broker.create_pr("AIOS autonomous release control plane", "Durable campaign continuation; exact head and recovery evidence are bound in the Job."))
|
||||
if operation == "PUBLISH_CHECK":
|
||||
self._validate_campaign(request)
|
||||
context = request.get("check_context", "aios/durable-release")
|
||||
state = request.get("check_state", "success")
|
||||
description = request.get("check_description", "Durable campaign control-plane check passed")
|
||||
return dict(self.broker.status(str(state), str(description), str(context)))
|
||||
if operation == "PROTECTED_MERGE":
|
||||
self._validate_campaign(request)
|
||||
number = int(request.get("pr_number", "0"))
|
||||
self.broker.transport.expected_merge_head = request["expected_branch_oid"]
|
||||
expected_main = request.get("expected_main_oid")
|
||||
if expected_main is not None:
|
||||
self.broker.transport.expected_merge_base = expected_main
|
||||
return dict(self.broker.merge(number))
|
||||
if operation == "RECONCILE_AUTHORITY_MAIN":
|
||||
self._validate_campaign(request)
|
||||
old = request.get("expected_main_oid")
|
||||
new = request.get("new_branch_oid")
|
||||
if not isinstance(old, str) or not isinstance(new, str):
|
||||
raise JobExecutionError("MAIN_RECONCILIATION_BINDING_INVALID")
|
||||
gitea_main = self.broker.transport.read_branch("main")
|
||||
if gitea_main != new:
|
||||
raise JobExecutionError("GITEA_MAIN_EXPECTATION_MISMATCH")
|
||||
if not self.authority.ancestry_check(old, new):
|
||||
raise JobExecutionError("MAIN_OID_NOT_DESCENDANT")
|
||||
return {"authority_main": self.authority.ff_CAS_main_after_verified_protected_merge(old, new)}
|
||||
raise JobExecutionError("OPERATION_NOT_ALLOWED")
|
||||
|
||||
def _advance_campaign(self, job_id: str) -> dict[str, Any]:
|
||||
self._recover_requested(job_id)
|
||||
row, request = self._request(job_id)
|
||||
new_oid = self._validate_campaign(request)
|
||||
expected_oid = request["expected_branch_oid"]
|
||||
state = JobState(row["state"])
|
||||
if state == JobState.REQUESTED:
|
||||
receipt = self.controller.advance(job_id, JobState.AUTHORIZED, side_effect_state="AUTHORIZED", receipt={"authorization_digest": AUTHORIZATION_DIGEST, "task_id": TASK_ID})
|
||||
row = self.controller.store.read(job_id)
|
||||
if JobState(row["state"]) == JobState.AUTHORIZED:
|
||||
self.controller.lease_job(job_id, lease_id=f"lease-{job_id}", lineage_anchor_oid=LINEAGE_ANCHOR, current_head_oid=expected_oid, generation=1, lease_expires_at="2099-01-01T00:00:00Z", expected_revision=int(row["revision"]))
|
||||
row = self.controller.store.read(job_id)
|
||||
if JobState(row["state"]) == JobState.LEASED:
|
||||
self.controller.advance(job_id, JobState.WORKTREE_READY, side_effect_state="WORKTREE_READY", receipt={"branch": BRANCH, "expected_oid": expected_oid})
|
||||
row = self.controller.store.read(job_id)
|
||||
if JobState(row["state"]) == JobState.WORKTREE_READY:
|
||||
self.controller.advance(job_id, JobState.EXECUTING, side_effect_state="IN_PROGRESS")
|
||||
row = self.controller.store.read(job_id)
|
||||
authority_before = self.authority.read_ref(f"refs/heads/{BRANCH}")
|
||||
if authority_before == expected_oid:
|
||||
authority_oid = self.authority.advance_task_branch_CAS(f"refs/heads/{BRANCH}", expected_oid, new_oid)
|
||||
elif authority_before == new_oid:
|
||||
authority_oid = new_oid
|
||||
else:
|
||||
raise JobExecutionError("AUTHORITY_BRANCH_CONFLICT")
|
||||
row = self.controller.store.read(job_id)
|
||||
if JobState(row["state"]) == JobState.EXECUTING:
|
||||
self.controller.advance(job_id, JobState.LOCAL_VERIFIED, side_effect_state="LOCAL_VERIFIED", receipt={"authority": {"branch": BRANCH, "previous_oid": expected_oid, "new_oid": new_oid, "CAS_success": True, "readback_oid": authority_oid}})
|
||||
row = self.controller.store.read(job_id)
|
||||
remote_before = self.broker.readback()
|
||||
if remote_before != new_oid:
|
||||
if remote_before not in {None, expected_oid}:
|
||||
raise JobExecutionError("GITEA_BRANCH_CONFLICT")
|
||||
if JobState(row["state"]) == JobState.LOCAL_VERIFIED:
|
||||
if hasattr(self.broker, "receipt"):
|
||||
self.broker.receipt = replace(self.broker.receipt, expected_oid=expected_oid)
|
||||
self.controller.advance(job_id, JobState.REMOTE_SYNCING, side_effect_state="IN_PROGRESS", receipt={"gitea": {"previous_oid": remote_before, "desired_oid": new_oid, "force_push": False}})
|
||||
final_network = self.broker.push_branch(new_oid)
|
||||
else:
|
||||
final_network = new_oid
|
||||
final_api = self.broker.readback()
|
||||
if final_api != new_oid:
|
||||
raise JobExecutionError("GITEA_BRANCH_READBACK_MISMATCH")
|
||||
row = self.controller.store.read(job_id)
|
||||
if JobState(row["state"]) in {JobState.LOCAL_VERIFIED, JobState.REMOTE_SYNCING}:
|
||||
self.controller.advance(job_id, JobState.REMOTE_VERIFIED, side_effect_state="REMOTE_VERIFIED", receipt={"gitea": {"machine_actor": "aios-autonomy-agent", "previous_oid": remote_before, "desired_oid": new_oid, "final_network_oid": final_network, "final_API_oid": final_api, "force_push": False}})
|
||||
row = self.controller.store.read(job_id)
|
||||
if JobState(row["state"]) == JobState.REMOTE_VERIFIED:
|
||||
self.controller.advance(job_id, JobState.COMPLETED, side_effect_state="REMOTE_VERIFIED", receipt={"authority_oid": authority_oid, "network_oid": final_network, "gitea_api_oid": final_api, "force_push": False})
|
||||
return self.controller.store._receipt(self.controller.store.read(job_id), duplicate=True).as_dict()
|
||||
|
||||
def _commit_file(self, job_id: str, request: Mapping[str, Any]) -> dict[str, Any]:
|
||||
row = self.controller.store.read(job_id)
|
||||
payload_ref = request.get("payload_ref")
|
||||
if not payload_ref:
|
||||
raise JobExecutionError("PAYLOAD_REQUIRED")
|
||||
payload = self.controller.store.payloads.read_verified(payload_ref, row["payload_digest"], int(request["payload_size"]))
|
||||
try:
|
||||
spec = json.loads(payload.decode())
|
||||
path = str(spec["path"])
|
||||
content = base64.b64decode(spec["content_base64"], validate=True)
|
||||
except Exception as exc:
|
||||
raise JobExecutionError("COMMIT_FILE_PAYLOAD_INVALID") from exc
|
||||
new_oid = self.authority.commit_file_in_isolated_worktree(base_oid=row["expected_branch_oid"], files={path: content}, message="chore(gitops): durable commit-file", worktree_name=job_id)
|
||||
return {"operation": "COMMIT_FILE", "new_oid": new_oid}
|
||||
|
||||
|
||||
def build_from_environment(job_root: Path) -> DurableJobExecutor:
|
||||
from aios_gitea_machine_transport import GiteaMachineTransport
|
||||
from aios_gitea_machine_broker import MachineCredentialProjection
|
||||
|
||||
account = __import__("os").environ.get("AIOS_GITEA_MACHINE_ACCOUNT", "aios-autonomy-agent")
|
||||
api = __import__("os").environ.get("AIOS_GITEA_API", "https://g.cnnetmkt.top/git/api/v1")
|
||||
remote = __import__("os").environ.get("AIOS_GITEA_REMOTE", "https://g.cnnetmkt.top/git/aios/aios.git")
|
||||
authority_dir = Path(__import__("os").environ.get("AIOS_GITOPS_AUTHORITY_GIT_DIR", "/data/aios-gitops/repo.git"))
|
||||
worktree_root = Path(__import__("os").environ.get("AIOS_GITOPS_WORKTREE_ROOT", "/var/lib/aios-gitops/worktrees"))
|
||||
recovery = Path(__import__("os").environ.get("AIOS_RELEASE_RECOVERY_RECEIPT", "/var/lib/aios-gitops/release-campaigns/AIOS-AUTONOMOUS-RELEASE-CAMPAIGN-20260807/authorization-recovery.json"))
|
||||
transport = GiteaMachineTransport(api, REPOSITORY, remote, authority_dir)
|
||||
credential = MachineCredentialProjection.from_mapping({"scheme": "systemd-credential", "name": "gitea-machine-token", "account": account})
|
||||
receipt = CampaignCapabilityReceipt(CAMPAIGN_ID, TASK_ID, REPOSITORY, BRANCH, EXPECTED_BRANCH_OID, frozenset({"exact_task_branch_push", "pr_create_update", "status_publish", "protected_merge", "remote_readback"}), frozenset({"direct_main_push", "force_push", "tag_update", "tag_delete", "baseline_write", "production_deploy"}))
|
||||
return DurableJobExecutor(GitOpsJobController(JobStore(job_root)), AuthorityGitExecutor(authority_dir, worktree_root), GiteaMachineBroker(transport, credential, receipt), recovery)
|
||||
|
||||
|
||||
def main(argv: list[str] | None = None) -> int:
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--job-root", required=True, type=Path)
|
||||
parser.add_argument("--job-id", required=True)
|
||||
parser.add_argument("--dry-run", action="store_true")
|
||||
args = parser.parse_args(argv)
|
||||
result = build_from_environment(args.job_root).execute(args.job_id, dry_run=args.dry_run)
|
||||
print(json.dumps(result, sort_keys=True))
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,80 @@
|
||||
"""Pure, observation-first decisions for governed GitOps reconciliation."""
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from enum import Enum
|
||||
import re
|
||||
from typing import Optional
|
||||
|
||||
OID_RE = re.compile(r"^[0-9a-f]{40}$")
|
||||
|
||||
|
||||
class ReconciliationAction(str, Enum):
|
||||
NOOP = "NOOP"
|
||||
PUSH_EXACT = "PUSH_EXACT"
|
||||
VERIFY_REMOTE = "VERIFY_REMOTE"
|
||||
MARK_REMOTE_VERIFIED = "MARK_REMOTE_VERIFIED"
|
||||
MARK_RECONCILIATION_REQUIRED = "MARK_RECONCILIATION_REQUIRED"
|
||||
FAIL_CONFLICT = "FAIL_CONFLICT"
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class RemoteObservation:
|
||||
repository_identity: str
|
||||
branch: str
|
||||
desired_oid: str
|
||||
authority_oid: Optional[str]
|
||||
remote_git_oid: Optional[str]
|
||||
remote_api_oid: Optional[str]
|
||||
commit_visible: bool
|
||||
observed_at: str
|
||||
expected_oid: Optional[str] = None
|
||||
|
||||
@property
|
||||
def all_equal(self) -> bool:
|
||||
values = (self.authority_oid, self.remote_git_oid, self.remote_api_oid, self.desired_oid)
|
||||
return all(isinstance(value, str) for value in values) and len(set(values)) == 1
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class ReconciliationDecision:
|
||||
action: ReconciliationAction
|
||||
state: str
|
||||
reason: str
|
||||
retry_safe: bool = False
|
||||
|
||||
|
||||
def _valid_oid(value: Optional[str]) -> bool:
|
||||
return value is None or bool(OID_RE.fullmatch(value))
|
||||
|
||||
|
||||
def decide_reconciliation(observation: RemoteObservation) -> ReconciliationDecision:
|
||||
"""Decide from a fresh observation; never complete on a missing remote."""
|
||||
if observation.repository_identity != "aios/aios":
|
||||
return ReconciliationDecision(ReconciliationAction.FAIL_CONFLICT, "CONFLICT", "REPOSITORY_IDENTITY_MISMATCH")
|
||||
if not observation.branch.startswith("agent/"):
|
||||
return ReconciliationDecision(ReconciliationAction.FAIL_CONFLICT, "CONFLICT", "BRANCH_SCOPE_DENIED")
|
||||
if not _valid_oid(observation.desired_oid) or not _valid_oid(observation.expected_oid):
|
||||
return ReconciliationDecision(ReconciliationAction.FAIL_CONFLICT, "CONFLICT", "OID_INVALID")
|
||||
if observation.authority_oid is None:
|
||||
return ReconciliationDecision(ReconciliationAction.MARK_RECONCILIATION_REQUIRED, "RECONCILIATION_REQUIRED", "AUTHORITY_UNAVAILABLE")
|
||||
if observation.authority_oid != observation.desired_oid:
|
||||
return ReconciliationDecision(ReconciliationAction.MARK_RECONCILIATION_REQUIRED, "RECONCILIATION_REQUIRED", "AUTHORITY_CONFLICT")
|
||||
present = [oid for oid in (observation.remote_git_oid, observation.remote_api_oid) if oid is not None]
|
||||
if any(oid not in {observation.desired_oid, observation.expected_oid} for oid in present):
|
||||
return ReconciliationDecision(ReconciliationAction.FAIL_CONFLICT, "CONFLICT", "REMOTE_OID_CONFLICT")
|
||||
if observation.all_equal and observation.commit_visible:
|
||||
return ReconciliationDecision(ReconciliationAction.MARK_REMOTE_VERIFIED, "REMOTE_VERIFIED", "ALL_OIDS_EQUAL_AND_VISIBLE")
|
||||
if observation.remote_git_oid is None and observation.remote_api_oid is None:
|
||||
return ReconciliationDecision(ReconciliationAction.MARK_RECONCILIATION_REQUIRED, "REMOTE_PUSH_PENDING", "AUTHORITY_DESIRED_REMOTE_MISSING")
|
||||
if observation.remote_git_oid == observation.desired_oid or observation.remote_api_oid == observation.desired_oid:
|
||||
return ReconciliationDecision(ReconciliationAction.VERIFY_REMOTE, "REMOTE_VERIFY_PENDING", "REMOTE_PROOF_PARTIAL")
|
||||
if observation.expected_oid is not None and observation.remote_git_oid == observation.expected_oid and observation.remote_api_oid == observation.expected_oid:
|
||||
return ReconciliationDecision(ReconciliationAction.PUSH_EXACT, "REMOTE_PUSH_PENDING", "SAFE_EXACT_FAST_FORWARD_RETRY", retry_safe=True)
|
||||
if not observation.commit_visible:
|
||||
return ReconciliationDecision(ReconciliationAction.VERIFY_REMOTE, "COMMIT_VISIBILITY_PENDING", "COMMIT_VISIBILITY_LAG")
|
||||
return ReconciliationDecision(ReconciliationAction.MARK_RECONCILIATION_REQUIRED, "RECONCILIATION_REQUIRED", "PARTIAL_REMOTE_OBSERVATION")
|
||||
|
||||
|
||||
def reconcile_observation(**kwargs: object) -> ReconciliationDecision:
|
||||
return decide_reconciliation(RemoteObservation(**kwargs))
|
||||
+100
@@ -0,0 +1,100 @@
|
||||
"""Single supervised socket-activated worker for durable GitOps Jobs.
|
||||
|
||||
The worker consumes durable requests and reconciliation queries. Repository
|
||||
mutation is a separate injected executor and remains impossible while the
|
||||
legacy global write gate is false.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import signal
|
||||
import socket
|
||||
import threading
|
||||
from typing import Any, Mapping
|
||||
|
||||
from aios_gitops_job_controller import GitOpsJobController, JobError, JobState, JobStore, ServiceUnavailable
|
||||
|
||||
|
||||
def runtime_policy(environment: Mapping[str, str] | None = None) -> dict[str, bool]:
|
||||
env = environment or os.environ
|
||||
legacy = env.get("AIOS_GITOPS_WRITES_ENABLED", "false").lower() in {"1", "true", "yes", "on"}
|
||||
durable = env.get("AIOS_GITOPS_DURABLE_JOB_EXECUTION_ENABLED", "false").lower() in {"1", "true", "yes", "on"}
|
||||
# Legacy direct Git mutation remains disabled. Authorized durable Jobs use
|
||||
# their own campaign/task capability and may mutate only through the
|
||||
# worker's lease/CAS path while the legacy gate stays false.
|
||||
return {"legacy_global_write": legacy, "durable_job_execution": durable, "repository_mutation_allowed": durable}
|
||||
|
||||
|
||||
def handle_request(controller: GitOpsJobController, request: Mapping[str, Any], executor: Any | None = None) -> dict[str, Any]:
|
||||
job_id = str(request.get("job_id") or "")
|
||||
if not job_id:
|
||||
raise JobError("JOB_ID_REQUIRED")
|
||||
if request.get("action") == "execute":
|
||||
if executor is None:
|
||||
raise JobError("JOB_EXECUTOR_UNAVAILABLE")
|
||||
result = executor.execute(job_id, dry_run=bool(request.get("dry_run", False)))
|
||||
receipt = controller.store._receipt(controller.store.read(job_id), duplicate=True)
|
||||
return {"job": result, **receipt.as_dict()}
|
||||
if request.get("action") == "resume":
|
||||
receipt = controller.resume(job_id)
|
||||
else:
|
||||
row = controller.store.read(job_id)
|
||||
receipt = controller.store._receipt(row, duplicate=True)
|
||||
return receipt.as_dict()
|
||||
|
||||
|
||||
def serve_socket(sock: socket.socket, controller: GitOpsJobController, stopping: threading.Event, executor: Any | None = None) -> None:
|
||||
sock.listen(32)
|
||||
sock.settimeout(1.0)
|
||||
while not stopping.is_set():
|
||||
try:
|
||||
connection, _ = sock.accept()
|
||||
except socket.timeout:
|
||||
continue
|
||||
with connection:
|
||||
try:
|
||||
raw = connection.makefile("rb").readline(1_048_576)
|
||||
request = json.loads(raw.decode("utf-8"))
|
||||
response = {"status": "ok", **handle_request(controller, request, executor)}
|
||||
except ServiceUnavailable as exc:
|
||||
response = {"status": "error", "error": str(exc), "retryable": True, "side_effect_state": "UNKNOWN"}
|
||||
except Exception as exc:
|
||||
response = {"status": "error", "error": type(exc).__name__, "retryable": False}
|
||||
connection.sendall((json.dumps(response, sort_keys=True) + "\n").encode("utf-8"))
|
||||
|
||||
|
||||
def socket_from_systemd() -> socket.socket:
|
||||
if os.environ.get("LISTEN_FDS") != "1":
|
||||
raise RuntimeError("SOCKET_ACTIVATION_REQUIRED")
|
||||
return socket.fromfd(3, socket.AF_UNIX, socket.SOCK_STREAM)
|
||||
|
||||
|
||||
def main(argv: list[str] | None = None) -> int:
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--job-root", required=True, type=Path)
|
||||
args = parser.parse_args(argv)
|
||||
policy = runtime_policy()
|
||||
if policy["legacy_global_write"]:
|
||||
raise RuntimeError("LEGACY_GLOBAL_WRITES_MUST_REMAIN_FALSE")
|
||||
if not policy["durable_job_execution"]:
|
||||
raise RuntimeError("DURABLE_JOB_EXECUTION_GATE_CLOSED")
|
||||
controller = GitOpsJobController(JobStore(args.job_root))
|
||||
from aios_gitops_job_executor import build_from_environment
|
||||
executor = build_from_environment(args.job_root)
|
||||
stopping = threading.Event()
|
||||
signal.signal(signal.SIGTERM, lambda *_: stopping.set())
|
||||
signal.signal(signal.SIGINT, lambda *_: stopping.set())
|
||||
sock = socket_from_systemd()
|
||||
try:
|
||||
serve_socket(sock, controller, stopping, executor)
|
||||
finally:
|
||||
sock.close()
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,59 @@
|
||||
"""Central fail-closed policy seam for governed task and continuation requests."""
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
import re
|
||||
from typing import Any, Mapping
|
||||
|
||||
OID_RE = re.compile(r"^[0-9a-f]{40}$")
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class GovernedPolicyInput:
|
||||
identity: str
|
||||
task_id: str
|
||||
repository: str
|
||||
operation: str
|
||||
base_oid: str
|
||||
expected_branch_oid: str
|
||||
target_branch: str
|
||||
generation: int
|
||||
risk: str = "engineering"
|
||||
requested_capabilities: tuple[str, ...] = ()
|
||||
previous_generation: int | None = None
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class PolicyDecision:
|
||||
decision: str
|
||||
reason: str
|
||||
|
||||
def as_dict(self) -> dict[str, str]:
|
||||
return {"decision": self.decision, "reason": self.reason}
|
||||
|
||||
|
||||
def evaluate_policy(value: GovernedPolicyInput | Mapping[str, Any]) -> PolicyDecision:
|
||||
if isinstance(value, Mapping):
|
||||
value = GovernedPolicyInput(**value)
|
||||
if value.repository != "aios/aios":
|
||||
return PolicyDecision("DENY", "REPOSITORY_IDENTITY_MISMATCH")
|
||||
if not OID_RE.fullmatch(value.base_oid) or not OID_RE.fullmatch(value.expected_branch_oid):
|
||||
return PolicyDecision("DENY", "OID_INVALID")
|
||||
if not value.target_branch.startswith("agent/") or value.target_branch in {"main", "master"}:
|
||||
return PolicyDecision("DENY", "BRANCH_SCOPE_DENIED")
|
||||
if "force_push" in value.requested_capabilities or "force" in value.operation.lower():
|
||||
return PolicyDecision("DENY", "FORCE_PUSH_DENIED")
|
||||
if {"main_write", "tag_write", "baseline_write", "production_write", "production_deploy", "tag_mutation", "baseline_mutation"}.intersection(value.requested_capabilities):
|
||||
return PolicyDecision("DENY", "MUTATION_SCOPE_DENIED")
|
||||
if not isinstance(value.generation, int) or isinstance(value.generation, bool) or value.generation < 1:
|
||||
return PolicyDecision("DENY", "GENERATION_INVALID")
|
||||
if value.previous_generation is not None and value.generation <= value.previous_generation:
|
||||
return PolicyDecision("MANUAL_OWNER_REVIEW_REQUIRED", "GENERATION_NOT_MONOTONIC")
|
||||
if not value.task_id:
|
||||
return PolicyDecision("DENY", "TASK_ID_INVALID")
|
||||
return PolicyDecision("ALLOW", "GOVERNED_ENGINEERING_SCOPE")
|
||||
|
||||
|
||||
class GovernedPolicy:
|
||||
def decide(self, request: GovernedPolicyInput | Mapping[str, Any]) -> PolicyDecision:
|
||||
return evaluate_policy(request)
|
||||
@@ -0,0 +1,84 @@
|
||||
"""Durable governed task binding for CREATE_NEW and CONTINUE_EXISTING."""
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass, asdict
|
||||
import hashlib
|
||||
import json
|
||||
from pathlib import Path
|
||||
import re
|
||||
from typing import Any, Mapping
|
||||
|
||||
from aios_gitops_job_controller import GitOpsJobController, JobStore
|
||||
|
||||
OID_RE = re.compile(r"^[0-9a-f]{40}$")
|
||||
BRANCH_RE = re.compile(r"^agent/[A-Za-z0-9][A-Za-z0-9._/-]{2,179}$")
|
||||
|
||||
|
||||
class BootstrapError(RuntimeError):
|
||||
pass
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class GovernedTaskBinding:
|
||||
task_id: str
|
||||
repository: str
|
||||
base_oid: str
|
||||
target_branch: str
|
||||
risk: str
|
||||
description: str
|
||||
idempotency_key: str
|
||||
expected_branch_oid: str
|
||||
branch_mode: str
|
||||
generation: int
|
||||
job_id: str | None = None
|
||||
|
||||
def response(self, *, idempotent: bool = False) -> dict[str, Any]:
|
||||
return {"task_id": self.task_id, "repository": self.repository, "base_oid": self.base_oid, "target_branch": self.target_branch, "expected_branch_oid": self.expected_branch_oid, "generation": self.generation, "branch_mode": self.branch_mode, "create_target_branch": self.branch_mode == "CREATE_NEW", "worktree_mode": "detached_at_base_oid" if self.branch_mode == "CREATE_NEW" else "detached_at_expected_head", "cas_ref_update_required": True, "idempotent": idempotent, "state": "BOUND", "job_id": self.job_id}
|
||||
|
||||
|
||||
def _safe(value: str) -> str:
|
||||
return re.sub(r"[^A-Za-z0-9_.-]+", "-", value).strip("-")[:120] or "task"
|
||||
|
||||
|
||||
class GovernedTaskBootstrap:
|
||||
def __init__(self, authority: Any, root: Path, *, remote_name: str, token_path: Path, ssh_key_path: Path, api_base: str = "https://g.cnnetmkt.top/git/api/v1") -> None:
|
||||
self.authority = authority; self.root = Path(root); self.remote_name = remote_name; self.token_path = Path(token_path); self.ssh_key_path = Path(ssh_key_path); self.api_base = api_base; self.task_meta_root = self.root / "task-bindings"; self.job_root = self.root / "jobs"; self.task_meta_root.mkdir(parents=True, exist_ok=True); self.job_root.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
def _store(self, task_id: str) -> tuple[JobStore, GitOpsJobController]:
|
||||
root = self.job_root / _safe(task_id); store = JobStore(root, payload_root=root / "payloads"); return store, GitOpsJobController(store)
|
||||
|
||||
def _metadata(self, task_id: str) -> Path:
|
||||
return self.task_meta_root / (_safe(task_id) + ".json")
|
||||
|
||||
def _read_existing(self, task_id: str) -> GovernedTaskBinding | None:
|
||||
path = self._metadata(task_id)
|
||||
if not path.is_file() or path.is_symlink(): return None
|
||||
return GovernedTaskBinding(**json.loads(path.read_text(encoding="utf-8")))
|
||||
|
||||
def _write(self, binding: GovernedTaskBinding) -> None:
|
||||
path = self._metadata(binding.task_id); temporary = path.with_suffix(".tmp"); temporary.write_text(json.dumps(asdict(binding), sort_keys=True), encoding="utf-8"); temporary.replace(path)
|
||||
|
||||
def create(self, request: Mapping[str, Any]) -> dict[str, Any]:
|
||||
values = {key: str(request[key]) for key in ("task_id", "repository", "base_oid", "target_branch", "risk", "description", "idempotency_key")}
|
||||
if values["repository"] != "aios/aios" or not OID_RE.fullmatch(values["base_oid"]): raise BootstrapError("TASK_BINDING_INVALID")
|
||||
if not BRANCH_RE.fullmatch(values["target_branch"]): raise BootstrapError("TARGET_BRANCH_INVALID")
|
||||
existing = self._read_existing(values["task_id"])
|
||||
if existing is not None:
|
||||
if any(getattr(existing, key) != values[key] for key in ("repository", "base_oid", "target_branch", "risk", "description")): raise BootstrapError("TASK_BINDING_CONFLICT")
|
||||
return existing.response(idempotent=True)
|
||||
if not self.authority.commit_exists(values["base_oid"]): raise BootstrapError("BASE_COMMIT_NOT_FOUND")
|
||||
current = self.authority.read_ref("refs/heads/" + values["target_branch"])
|
||||
expected = str(request.get("expected_branch_oid") or values["base_oid"])
|
||||
if current is None:
|
||||
mode, generation = "CREATE_NEW", 1
|
||||
if expected != values["base_oid"]: raise BootstrapError("EXPECTED_BASE_CONFLICT")
|
||||
else:
|
||||
if expected != current: raise BootstrapError("EXISTING_BRANCH_OID_CONFLICT")
|
||||
mode, generation = "CONTINUE_EXISTING", int(request.get("generation", 2))
|
||||
if generation < 1: raise BootstrapError("GENERATION_INVALID")
|
||||
payload = json.dumps(values, sort_keys=True, separators=(",", ":")).encode()
|
||||
_, controller = self._store(values["task_id"])
|
||||
receipt = controller.submit({"idempotency_key": values["idempotency_key"], "campaign_id": values["task_id"], "task_id": values["task_id"], "repository": values["repository"], "operation": "GOVERNED_TASK_CREATE", "branch": values["target_branch"], "expected_branch_oid": expected, "payload_digest": hashlib.sha256(payload).hexdigest(), "lineage_anchor_oid": values["base_oid"]}, payload=payload)
|
||||
binding = GovernedTaskBinding(**values, expected_branch_oid=expected, branch_mode=mode, generation=generation, job_id=receipt.job_id)
|
||||
self._write(binding)
|
||||
return binding.response()
|
||||
File diff suppressed because it is too large
Load Diff
+652
@@ -0,0 +1,652 @@
|
||||
#!/usr/bin/env python3
|
||||
"""One-shot governed executor for an already approved exact release binding.
|
||||
|
||||
This module deliberately does not build release artifacts. The build/staging
|
||||
evidence is completed before Owner production approval. Production execution
|
||||
consumes that immutable binding, revalidates it, and performs narrow
|
||||
side-effects through adapters in a durable fail-closed order.
|
||||
|
||||
Adapter protocol:
|
||||
stdin: JSON request with operation = "probe" | "apply" | "rollback"
|
||||
stdout: one JSON object on the final non-empty line
|
||||
shell: never used
|
||||
|
||||
The executor never rewrites tags, never writes refs/heads/main, and records
|
||||
Baseline last.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import dataclasses
|
||||
import fcntl
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import stat
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
import time
|
||||
from pathlib import Path
|
||||
from typing import Any, Mapping, Sequence
|
||||
|
||||
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}$")
|
||||
|
||||
REQUIRED_V2R1C7_MEMBERS = {
|
||||
"payload/VERSION",
|
||||
"payload/active-runtime/app/deployment_state.py",
|
||||
"payload/active-runtime/app/v2r1c6_privacy_tools.py",
|
||||
"payload/active-runtime/app/v2r1c7_health_service.py",
|
||||
"payload/deploy/aios-v2/systemd/aios-v2r1c7-production.service",
|
||||
"payload/deploy/aios-v2/v2r1c7-runtime-config.json",
|
||||
"payload/deploy/aios-v2/v2r1c7-rollback.json",
|
||||
"payload/scripts/probe_v2r1c7_health.py",
|
||||
"payload/scripts/rollback_v2r1c7_release.sh",
|
||||
"payload/BUILD-METADATA.json",
|
||||
"payload/ARTIFACT-MANIFEST.json",
|
||||
"payload/ROLLBACK-METADATA.json",
|
||||
}
|
||||
|
||||
class ReleaseBlocked(RuntimeError):
|
||||
pass
|
||||
|
||||
class ManualOwnerReviewRequired(ReleaseBlocked):
|
||||
pass
|
||||
|
||||
class ReconciliationRequired(ReleaseBlocked):
|
||||
pass
|
||||
|
||||
|
||||
@dataclasses.dataclass(frozen=True)
|
||||
class Binding:
|
||||
version: str
|
||||
target_oid: str
|
||||
evidence_closure_oid: str
|
||||
artifact_sha256: str
|
||||
approval_plan_id: str
|
||||
artifact_path: str
|
||||
artifact_size: int
|
||||
production_service: str
|
||||
production_port: int
|
||||
repository_identity: str = "aios/aios"
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
if not self.version or not OID_RE.fullmatch(self.target_oid):
|
||||
raise ValueError("invalid release target binding")
|
||||
if not OID_RE.fullmatch(self.evidence_closure_oid):
|
||||
raise ValueError("invalid evidence closure OID")
|
||||
if not SHA_RE.fullmatch(self.artifact_sha256):
|
||||
raise ValueError("invalid artifact SHA-256")
|
||||
if not PLAN_RE.fullmatch(self.approval_plan_id):
|
||||
raise ValueError("invalid approval plan id")
|
||||
if self.artifact_size <= 0:
|
||||
raise ValueError("invalid artifact size")
|
||||
if not self.production_service:
|
||||
raise ValueError("production service is required")
|
||||
if not 1024 <= self.production_port <= 65535:
|
||||
raise ValueError("invalid production port")
|
||||
if self.repository_identity != "aios/aios":
|
||||
raise ValueError("unexpected repository identity")
|
||||
|
||||
@property
|
||||
def tag(self) -> str:
|
||||
return self.version
|
||||
|
||||
@property
|
||||
def fingerprint(self) -> str:
|
||||
payload = {
|
||||
"version": self.version,
|
||||
"target_oid": self.target_oid,
|
||||
"evidence_closure_oid": self.evidence_closure_oid,
|
||||
"artifact_sha256": self.artifact_sha256,
|
||||
"approval_plan_id": self.approval_plan_id,
|
||||
"artifact_size": self.artifact_size,
|
||||
"production_service": self.production_service,
|
||||
"production_port": self.production_port,
|
||||
"repository_identity": self.repository_identity,
|
||||
}
|
||||
return hashlib.sha256(canonical_json(payload)).hexdigest()
|
||||
|
||||
def request(self, operation: str) -> dict[str, Any]:
|
||||
return {
|
||||
"schema": "aios.production-executor-request.v1",
|
||||
"operation": operation,
|
||||
"binding_fingerprint": self.fingerprint,
|
||||
"version": self.version,
|
||||
"tag": self.tag,
|
||||
"target_oid": self.target_oid,
|
||||
"evidence_closure_oid": self.evidence_closure_oid,
|
||||
"artifact_sha256": self.artifact_sha256,
|
||||
"artifact_path": self.artifact_path,
|
||||
"artifact_size": self.artifact_size,
|
||||
"approval_plan_id": self.approval_plan_id,
|
||||
"production_service": self.production_service,
|
||||
"production_port": self.production_port,
|
||||
"repository_identity": self.repository_identity,
|
||||
}
|
||||
|
||||
|
||||
def canonical_json(value: object) -> bytes:
|
||||
return (json.dumps(value, sort_keys=True, separators=(",", ":")) + "\n").encode("utf-8")
|
||||
|
||||
|
||||
def secure_json(path: Path) -> dict[str, Any]:
|
||||
st = path.lstat()
|
||||
if not stat.S_ISREG(st.st_mode) or path.is_symlink():
|
||||
raise ReleaseBlocked(f"non-regular authorization file: {path}")
|
||||
if st.st_uid != 0:
|
||||
raise ReleaseBlocked(f"authorization file must be root-owned: {path}")
|
||||
if st.st_mode & 0o022:
|
||||
raise ReleaseBlocked(f"authorization file is group/world writable: {path}")
|
||||
data = json.loads(path.read_text(encoding="utf-8"))
|
||||
if not isinstance(data, dict):
|
||||
raise ReleaseBlocked("authorization receipt must be a JSON object")
|
||||
return data
|
||||
|
||||
|
||||
def sha256_file(path: Path) -> str:
|
||||
if path.is_symlink() or not path.is_file():
|
||||
raise ReleaseBlocked("artifact must be a regular non-symlink file")
|
||||
digest = hashlib.sha256()
|
||||
with path.open("rb") as source:
|
||||
for block in iter(lambda: source.read(1024 * 1024), b""):
|
||||
digest.update(block)
|
||||
return digest.hexdigest()
|
||||
|
||||
|
||||
def run_git(git_dir: Path, *args: str, check: bool = True) -> subprocess.CompletedProcess[str]:
|
||||
return subprocess.run(
|
||||
["git", f"--git-dir={git_dir}", *args],
|
||||
check=check,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=30,
|
||||
env={"PATH": os.environ.get("PATH", "")},
|
||||
)
|
||||
|
||||
|
||||
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):
|
||||
raise ReleaseBlocked("Authority main OID is invalid")
|
||||
|
||||
target_type = run_git(git_dir, "cat-file", "-t", binding.target_oid).stdout.strip()
|
||||
if target_type != "commit":
|
||||
raise ReleaseBlocked("target_oid is not a commit")
|
||||
|
||||
evidence_type = run_git(
|
||||
git_dir, "cat-file", "-t", binding.evidence_closure_oid
|
||||
).stdout.strip()
|
||||
if evidence_type != "commit":
|
||||
raise ReleaseBlocked("evidence_closure_oid is not a commit")
|
||||
|
||||
# A control-plane-only patch may advance main after release approval.
|
||||
# The approved release target remains valid only while it is still on main.
|
||||
target_on_main = run_git(
|
||||
git_dir,
|
||||
"merge-base",
|
||||
"--is-ancestor",
|
||||
binding.target_oid,
|
||||
authority_main,
|
||||
check=False,
|
||||
).returncode == 0
|
||||
if not target_on_main:
|
||||
raise ManualOwnerReviewRequired(
|
||||
"approved target is no longer an ancestor of Authority main"
|
||||
)
|
||||
|
||||
target_in_evidence = run_git(
|
||||
git_dir,
|
||||
"merge-base",
|
||||
"--is-ancestor",
|
||||
binding.target_oid,
|
||||
binding.evidence_closure_oid,
|
||||
check=False,
|
||||
).returncode == 0
|
||||
if not target_in_evidence:
|
||||
raise ReleaseBlocked("approved target is not an ancestor of evidence closure")
|
||||
|
||||
return {
|
||||
"authority_main_oid": authority_main,
|
||||
"target_oid": binding.target_oid,
|
||||
"target_is_ancestor_of_main": True,
|
||||
"evidence_closure_oid": binding.evidence_closure_oid,
|
||||
"evidence_object_type": evidence_type,
|
||||
"target_is_ancestor_of_evidence": True,
|
||||
}
|
||||
|
||||
|
||||
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")
|
||||
actual = sha256_file(path)
|
||||
if actual != binding.artifact_sha256:
|
||||
raise ReleaseBlocked("artifact SHA-256 mismatch")
|
||||
|
||||
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,
|
||||
}
|
||||
|
||||
|
||||
def verify_authorization(receipt: Mapping[str, Any], binding: Binding) -> dict[str, Any]:
|
||||
if receipt.get("accepted") is not True:
|
||||
raise ReleaseBlocked("Owner production authorization is not accepted")
|
||||
if receipt.get("binding_replaced") not in (None, False):
|
||||
raise ManualOwnerReviewRequired("Owner binding was replaced")
|
||||
expected = {
|
||||
"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,
|
||||
}
|
||||
for key, value in expected.items():
|
||||
if receipt.get(key) != value:
|
||||
raise ManualOwnerReviewRequired(f"Owner authorization mismatch: {key}")
|
||||
return {"accepted": True, **expected}
|
||||
|
||||
|
||||
class DurableState:
|
||||
def __init__(self, root: Path, binding: Binding):
|
||||
self.root = root
|
||||
self.root.mkdir(parents=True, exist_ok=True)
|
||||
os.chmod(self.root, 0o700)
|
||||
self.path = self.root / "execution.json"
|
||||
self.binding = binding
|
||||
self.value: dict[str, Any]
|
||||
if self.path.exists():
|
||||
existing = json.loads(self.path.read_text(encoding="utf-8"))
|
||||
if existing.get("binding_fingerprint") != binding.fingerprint:
|
||||
raise ManualOwnerReviewRequired(
|
||||
"state root contains a different production binding"
|
||||
)
|
||||
self.value = existing
|
||||
else:
|
||||
self.value = {
|
||||
"schema": "aios.production-execution-state.v1",
|
||||
"binding_fingerprint": binding.fingerprint,
|
||||
"version": binding.version,
|
||||
"target_oid": binding.target_oid,
|
||||
"state": "AUTHORIZED",
|
||||
"completed": {},
|
||||
"history": [],
|
||||
}
|
||||
self.save()
|
||||
|
||||
def save(self) -> None:
|
||||
temporary = self.path.with_suffix(".tmp")
|
||||
temporary.write_bytes(canonical_json(self.value))
|
||||
os.chmod(temporary, 0o600)
|
||||
os.replace(temporary, self.path)
|
||||
|
||||
def record(self, state: str, step: str, result: Mapping[str, Any]) -> None:
|
||||
now = time.time()
|
||||
sanitized = json.loads(json.dumps(result))
|
||||
self.value["state"] = state
|
||||
self.value["completed"][step] = sanitized
|
||||
self.value["history"].append(
|
||||
{"at": now, "state": state, "step": step, "result": sanitized}
|
||||
)
|
||||
self.save()
|
||||
|
||||
def fail(self, state: str, reason: str) -> None:
|
||||
self.value["state"] = state
|
||||
self.value["history"].append(
|
||||
{"at": time.time(), "state": state, "reason": reason}
|
||||
)
|
||||
self.save()
|
||||
|
||||
|
||||
class FileLock:
|
||||
def __init__(self, path: Path):
|
||||
self.path = path
|
||||
self.handle = None
|
||||
|
||||
def __enter__(self) -> "FileLock":
|
||||
self.path.parent.mkdir(parents=True, exist_ok=True)
|
||||
self.handle = self.path.open("a+", encoding="utf-8")
|
||||
os.chmod(self.path, 0o600)
|
||||
try:
|
||||
fcntl.flock(
|
||||
self.handle.fileno(), fcntl.LOCK_EX | fcntl.LOCK_NB
|
||||
)
|
||||
except BlockingIOError as exc:
|
||||
raise ReleaseBlocked("production executor is already running") from exc
|
||||
return self
|
||||
|
||||
def __exit__(self, exc_type, exc, tb) -> None:
|
||||
if self.handle is not None:
|
||||
fcntl.flock(self.handle.fileno(), fcntl.LOCK_UN)
|
||||
self.handle.close()
|
||||
|
||||
|
||||
def parse_command(values: Sequence[str]) -> list[str]:
|
||||
command = [str(x) for x in values if str(x)]
|
||||
if not command:
|
||||
raise ValueError("adapter command is required")
|
||||
if not os.path.isabs(command[0]):
|
||||
raise ValueError("adapter executable must be an absolute path")
|
||||
return command
|
||||
|
||||
|
||||
def run_adapter(command: Sequence[str], request: Mapping[str, Any]) -> dict[str, Any]:
|
||||
completed = subprocess.run(
|
||||
list(command),
|
||||
input=canonical_json(request),
|
||||
check=False,
|
||||
capture_output=True,
|
||||
timeout=180,
|
||||
env={"PATH": os.environ.get("PATH", "")},
|
||||
)
|
||||
if completed.returncode != 0:
|
||||
raise ReleaseBlocked(
|
||||
f"adapter failed: {Path(command[0]).name} rc={completed.returncode}"
|
||||
)
|
||||
lines = [line for line in completed.stdout.decode("utf-8").splitlines() if line.strip()]
|
||||
if not lines:
|
||||
raise ReleaseBlocked("adapter returned no JSON result")
|
||||
try:
|
||||
value = json.loads(lines[-1])
|
||||
except ValueError as exc:
|
||||
raise ReleaseBlocked("adapter returned invalid JSON") from exc
|
||||
if not isinstance(value, dict):
|
||||
raise ReleaseBlocked("adapter result must be an object")
|
||||
return value
|
||||
|
||||
|
||||
def require_exact_response(result: Mapping[str, Any], binding: Binding) -> None:
|
||||
for key, expected in {
|
||||
"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,
|
||||
}.items():
|
||||
if key in result and result[key] != expected:
|
||||
raise ManualOwnerReviewRequired(f"adapter binding mismatch: {key}")
|
||||
|
||||
|
||||
def tag_step(command: Sequence[str], binding: Binding) -> dict[str, Any]:
|
||||
request = binding.request("probe")
|
||||
probe = run_adapter(command, request)
|
||||
require_exact_response(probe, binding)
|
||||
state = probe.get("status")
|
||||
if state == "CONFLICT":
|
||||
raise ManualOwnerReviewRequired("same-name tag conflicts with approved binding")
|
||||
if state == "INDETERMINATE":
|
||||
raise ReconciliationRequired("tag publication state is indeterminate")
|
||||
if state not in {"APPLIED_VERIFIED", "NOT_APPLIED", "PARTIAL_LOCAL"}:
|
||||
raise ReleaseBlocked(f"unsupported tag probe status: {state}")
|
||||
if state != "APPLIED_VERIFIED":
|
||||
applied = run_adapter(command, binding.request("apply"))
|
||||
require_exact_response(applied, binding)
|
||||
if applied.get("status") == "CONFLICT":
|
||||
raise ManualOwnerReviewRequired("tag conflict during publication")
|
||||
probe = run_adapter(command, request)
|
||||
require_exact_response(probe, binding)
|
||||
if probe.get("status") != "APPLIED_VERIFIED":
|
||||
raise ReconciliationRequired("tag was not independently verified")
|
||||
if probe.get("peeled_target_oid") != binding.target_oid:
|
||||
raise ManualOwnerReviewRequired("published tag peeled target mismatch")
|
||||
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")
|
||||
return dict(probe)
|
||||
|
||||
|
||||
def deploy_step(command: Sequence[str], binding: Binding) -> dict[str, Any]:
|
||||
probe = run_adapter(command, binding.request("probe"))
|
||||
require_exact_response(probe, binding)
|
||||
if probe.get("status") == "ACTIVE_VERIFIED":
|
||||
return dict(probe)
|
||||
if probe.get("status") == "CONFLICT":
|
||||
raise ManualOwnerReviewRequired("production runtime has conflicting binding")
|
||||
applied = run_adapter(command, binding.request("apply"))
|
||||
require_exact_response(applied, binding)
|
||||
probe = run_adapter(command, binding.request("probe"))
|
||||
require_exact_response(probe, binding)
|
||||
if probe.get("status") != "ACTIVE_VERIFIED":
|
||||
try:
|
||||
run_adapter(command, binding.request("rollback"))
|
||||
except Exception:
|
||||
pass
|
||||
raise ReconciliationRequired("production runtime did not verify after apply")
|
||||
if (
|
||||
probe.get("deployment_state") != "ACTIVE"
|
||||
or probe.get("production_deployed") is not True
|
||||
):
|
||||
raise ReconciliationRequired("production health contract is not ACTIVE")
|
||||
return dict(probe)
|
||||
|
||||
|
||||
def finalize_step(
|
||||
command: Sequence[str],
|
||||
binding: Binding,
|
||||
*,
|
||||
name: str,
|
||||
tag_oid: str,
|
||||
) -> dict[str, Any]:
|
||||
request = binding.request("probe")
|
||||
request["tag_oid"] = tag_oid
|
||||
probe = run_adapter(command, request)
|
||||
require_exact_response(probe, binding)
|
||||
if probe.get("status") == "CONFLICT":
|
||||
raise ManualOwnerReviewRequired(f"{name} has conflicting release binding")
|
||||
if probe.get("status") != "VERIFIED":
|
||||
apply_request = binding.request("apply")
|
||||
apply_request["tag_oid"] = tag_oid
|
||||
applied = run_adapter(command, apply_request)
|
||||
require_exact_response(applied, binding)
|
||||
probe = run_adapter(command, request)
|
||||
require_exact_response(probe, binding)
|
||||
if probe.get("status") != "VERIFIED":
|
||||
raise ReconciliationRequired(f"{name} did not verify after apply")
|
||||
if probe.get("tag_oid") not in (None, tag_oid):
|
||||
raise ManualOwnerReviewRequired(f"{name} tag object mismatch")
|
||||
return dict(probe)
|
||||
|
||||
|
||||
def execute(
|
||||
*,
|
||||
binding: Binding,
|
||||
authorization_path: Path,
|
||||
repository: Path,
|
||||
state_root: Path,
|
||||
tag_command: Sequence[str],
|
||||
deploy_command: Sequence[str],
|
||||
registry_command: Sequence[str],
|
||||
baseline_command: Sequence[str],
|
||||
) -> dict[str, Any]:
|
||||
authorization = verify_authorization(secure_json(authorization_path), binding)
|
||||
lineage = verify_git_lineage(repository, binding)
|
||||
artifact = verify_artifact(binding)
|
||||
state = DurableState(state_root, binding)
|
||||
with FileLock(state_root / "executor.lock"):
|
||||
state.record(
|
||||
"PREFLIGHT_VERIFIED",
|
||||
"preflight",
|
||||
{"authorization": authorization, "lineage": lineage, "artifact": artifact},
|
||||
)
|
||||
tag: dict[str, Any] | None = None
|
||||
runtime: dict[str, Any] | None = None
|
||||
try:
|
||||
tag = tag_step(tag_command, binding)
|
||||
state.record("TAG_VERIFIED", "tag", tag)
|
||||
|
||||
runtime = deploy_step(deploy_command, binding)
|
||||
state.record("RUNTIME_VERIFIED", "runtime", runtime)
|
||||
|
||||
registry = finalize_step(
|
||||
registry_command,
|
||||
binding,
|
||||
name="registry",
|
||||
tag_oid=tag["tag_oid"],
|
||||
)
|
||||
state.record("REGISTRY_VERIFIED", "registry", registry)
|
||||
|
||||
baseline = finalize_step(
|
||||
baseline_command,
|
||||
binding,
|
||||
name="baseline",
|
||||
tag_oid=tag["tag_oid"],
|
||||
)
|
||||
state.record("BASELINE_VERIFIED", "baseline", baseline)
|
||||
|
||||
# Final readback: no mutation, all four planes must still agree.
|
||||
final_tag = tag_step(tag_command, binding)
|
||||
final_runtime = run_adapter(deploy_command, binding.request("probe"))
|
||||
final_registry_request = binding.request("probe")
|
||||
final_registry_request["tag_oid"] = tag["tag_oid"]
|
||||
final_registry = run_adapter(registry_command, final_registry_request)
|
||||
final_baseline_request = binding.request("probe")
|
||||
final_baseline_request["tag_oid"] = tag["tag_oid"]
|
||||
final_baseline = run_adapter(baseline_command, final_baseline_request)
|
||||
for value in (final_runtime, final_registry, final_baseline):
|
||||
require_exact_response(value, binding)
|
||||
if final_runtime.get("status") != "ACTIVE_VERIFIED":
|
||||
raise ReconciliationRequired("final runtime 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 = {
|
||||
"status": "PRODUCTION_ACCEPTED",
|
||||
"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,
|
||||
"tag_oid": tag["tag_oid"],
|
||||
"tag": final_tag,
|
||||
"runtime": final_runtime,
|
||||
"registry": final_registry,
|
||||
"baseline": final_baseline,
|
||||
"force_push_used": False,
|
||||
"tag_rewritten": False,
|
||||
"direct_main_push": False,
|
||||
"secrets_committed": False,
|
||||
}
|
||||
state.record("COMPLETED", "final_consistency", final)
|
||||
return final
|
||||
except ManualOwnerReviewRequired as exc:
|
||||
state.fail("MANUAL_OWNER_REVIEW_REQUIRED", str(exc))
|
||||
raise
|
||||
except ReconciliationRequired as exc:
|
||||
state.fail("RECONCILIATION_REQUIRED", str(exc))
|
||||
raise
|
||||
except Exception as exc:
|
||||
# Once a formal tag has been verified, no later error is a clean
|
||||
# pre-side-effect failure. Preserve the tag and require idempotent
|
||||
# reconciliation rather than pretending the release never started.
|
||||
if tag is not None:
|
||||
reason = f"{type(exc).__name__}: {exc}"
|
||||
state.fail("RECONCILIATION_REQUIRED", reason)
|
||||
raise ReconciliationRequired(reason) from exc
|
||||
state.fail("FAIL_CLOSED", f"{type(exc).__name__}: {exc}")
|
||||
raise
|
||||
|
||||
|
||||
def parser() -> argparse.ArgumentParser:
|
||||
p = argparse.ArgumentParser()
|
||||
p.add_argument("--authorization", required=True, type=Path)
|
||||
p.add_argument("--repository", required=True, type=Path)
|
||||
p.add_argument("--state-root", required=True, type=Path)
|
||||
p.add_argument("--artifact", required=True, type=Path)
|
||||
p.add_argument("--artifact-size", required=True, type=int)
|
||||
p.add_argument("--version", required=True)
|
||||
p.add_argument("--target-oid", required=True)
|
||||
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("--production-service", required=True)
|
||||
p.add_argument("--production-port", required=True, type=int)
|
||||
p.add_argument("--repository-identity", default="aios/aios")
|
||||
p.add_argument("--tag-adapter", required=True, nargs="+")
|
||||
p.add_argument("--deploy-adapter", required=True, nargs="+")
|
||||
p.add_argument("--registry-adapter", required=True, nargs="+")
|
||||
p.add_argument("--baseline-adapter", required=True, nargs="+")
|
||||
return p
|
||||
|
||||
|
||||
def main() -> int:
|
||||
args = parser().parse_args()
|
||||
binding = Binding(
|
||||
version=args.version,
|
||||
target_oid=args.target_oid,
|
||||
evidence_closure_oid=args.evidence_closure_oid,
|
||||
artifact_sha256=args.artifact_sha256,
|
||||
approval_plan_id=args.approval_plan_id,
|
||||
artifact_path=str(args.artifact),
|
||||
artifact_size=args.artifact_size,
|
||||
production_service=args.production_service,
|
||||
production_port=args.production_port,
|
||||
repository_identity=args.repository_identity,
|
||||
)
|
||||
try:
|
||||
result = execute(
|
||||
binding=binding,
|
||||
authorization_path=args.authorization,
|
||||
repository=args.repository,
|
||||
state_root=args.state_root,
|
||||
tag_command=parse_command(args.tag_adapter),
|
||||
deploy_command=parse_command(args.deploy_adapter),
|
||||
registry_command=parse_command(args.registry_adapter),
|
||||
baseline_command=parse_command(args.baseline_adapter),
|
||||
)
|
||||
print(json.dumps(result, sort_keys=True))
|
||||
return 0
|
||||
except ManualOwnerReviewRequired as exc:
|
||||
print(json.dumps({"status": "MANUAL_OWNER_REVIEW_REQUIRED", "reason": str(exc)}, sort_keys=True))
|
||||
return 20
|
||||
except ReconciliationRequired as exc:
|
||||
print(json.dumps({"status": "RECONCILIATION_REQUIRED", "reason": str(exc)}, sort_keys=True))
|
||||
return 21
|
||||
except Exception as exc:
|
||||
print(json.dumps({"status": "FAIL_CLOSED", "error": type(exc).__name__, "reason": str(exc)}, sort_keys=True))
|
||||
return 22
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -951,3 +951,51 @@ class AuthorizedTaskWriterProvisioner:
|
||||
return {**public_result, "prestart": prestart}
|
||||
finally:
|
||||
shutil.rmtree(temporary_root, ignore_errors=True)
|
||||
|
||||
|
||||
# v2 continuation validation is intentionally separate from the legacy Owner
|
||||
# bootstrap path above. It consumes a recovered campaign authorization and
|
||||
# never creates a branch or asks the Owner to authorize again.
|
||||
from aios_continuation_lease import WriterLeaseRequestV2
|
||||
|
||||
|
||||
def validate_writer_lease_request_v2(raw: Mapping[str, Any], probe: Any) -> dict[str, Any]:
|
||||
"""Validate a CONTINUE_EXISTING request against the live exact branch head."""
|
||||
required = {
|
||||
"schema",
|
||||
"task_id",
|
||||
"repository",
|
||||
"branch_mode",
|
||||
"base_oid",
|
||||
"expected_branch_oid",
|
||||
"target_branch",
|
||||
"generation",
|
||||
"lease_seconds",
|
||||
}
|
||||
if set(raw) != required or raw.get("schema") not in WriterLeaseRequestV2.accepted_schemas:
|
||||
raise ProvisioningError("LEASE_REQUEST_V2_FIELDS_REJECTED", 409)
|
||||
request = WriterLeaseRequestV2(
|
||||
task_id=str(raw["task_id"]),
|
||||
repository=str(raw["repository"]),
|
||||
branch_mode=str(raw["branch_mode"]),
|
||||
base_oid=str(raw["base_oid"]),
|
||||
expected_branch_oid=str(raw["expected_branch_oid"]),
|
||||
target_branch=str(raw["target_branch"]),
|
||||
generation=raw["generation"],
|
||||
lease_seconds=raw["lease_seconds"],
|
||||
)
|
||||
request.validate(probe)
|
||||
return {
|
||||
"schema": WriterLeaseRequestV2.schema,
|
||||
"task_id": request.task_id,
|
||||
"repository": request.repository,
|
||||
"branch_mode": request.branch_mode,
|
||||
"base_oid": request.base_oid,
|
||||
"expected_branch_oid": request.expected_branch_oid,
|
||||
"target_branch": request.target_branch,
|
||||
"generation": request.generation,
|
||||
"lease_seconds": request.lease_seconds,
|
||||
"create_target_branch": request.branch_mode == "CREATE_NEW",
|
||||
"worktree_mode": "detached_at_base_oid" if request.branch_mode == "CREATE_NEW" else "detached_at_expected_head",
|
||||
"cas_ref_update_required": True,
|
||||
}
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Single canonical Production Baseline authority for the GitOps reader.
|
||||
|
||||
The API and the one-shot writer import this module. Workspace and release
|
||||
copies are projections only; they are never selected as a fallback source.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
|
||||
AUTHORITY_PATH = Path(os.getenv(
|
||||
"AIOS_GITOPS_BASELINE_AUTHORITY",
|
||||
"/var/lib/aios-gitops-runtime-anchor/baseline/current.json",
|
||||
))
|
||||
AUTHORITY_SHA_PATH = AUTHORITY_PATH.with_suffix(".sha256")
|
||||
|
||||
|
||||
class BaselineAuthorityError(RuntimeError):
|
||||
"""Fail-closed error raised for an unavailable or inconsistent authority."""
|
||||
|
||||
|
||||
class BaselineAuthority:
|
||||
def __init__(self, path: Path | str = AUTHORITY_PATH):
|
||||
self.path = Path(path)
|
||||
self.sha_path = self.path.with_suffix(".sha256")
|
||||
|
||||
def _raw(self) -> bytes:
|
||||
try:
|
||||
raw = self.path.read_bytes()
|
||||
except OSError as exc:
|
||||
raise BaselineAuthorityError(f"authority_unavailable:{exc}") from exc
|
||||
if not raw:
|
||||
raise BaselineAuthorityError("authority_empty")
|
||||
return raw
|
||||
|
||||
def read(self) -> dict[str, Any]:
|
||||
raw = self._raw()
|
||||
digest = hashlib.sha256(raw).hexdigest()
|
||||
try:
|
||||
value = json.loads(raw.decode("utf-8"))
|
||||
except (UnicodeDecodeError, json.JSONDecodeError) as exc:
|
||||
raise BaselineAuthorityError("authority_invalid_json") from exc
|
||||
if not isinstance(value, dict):
|
||||
raise BaselineAuthorityError("authority_not_object")
|
||||
expected = str(value.get("authority_sha256", ""))
|
||||
if expected and expected != digest:
|
||||
raise BaselineAuthorityError("authority_embedded_digest_mismatch")
|
||||
if self.sha_path.exists():
|
||||
sidecar = self.sha_path.read_text(encoding="utf-8").strip().split()[0]
|
||||
if sidecar != digest:
|
||||
raise BaselineAuthorityError("authority_sidecar_digest_mismatch")
|
||||
required = {
|
||||
"current_baseline", "previous_baseline", "target_oid", "tag_oid",
|
||||
"evidence_closure_oid", "approval_plan_id", "status", "generation",
|
||||
"source_identity",
|
||||
}
|
||||
missing = sorted(required.difference(value))
|
||||
if missing:
|
||||
raise BaselineAuthorityError("authority_missing:" + ",".join(missing))
|
||||
value = dict(value)
|
||||
value["authority_sha256"] = digest
|
||||
value["authority_path"] = str(self.path)
|
||||
return value
|
||||
|
||||
def read_consistent(self, policy: dict[str, Any] | None = None) -> dict[str, Any]:
|
||||
value = self.read()
|
||||
if policy:
|
||||
expected = policy.get("release_binding", policy)
|
||||
for key, field in (("version", "current_baseline"),
|
||||
("previous_version", "previous_baseline"),
|
||||
("target_oid", "target_oid"),
|
||||
("tag_oid", "tag_oid"),
|
||||
("evidence_closure_oid", "evidence_closure_oid"),
|
||||
("approval_plan_id", "approval_plan_id")):
|
||||
wanted = expected.get(key)
|
||||
if wanted is not None and value.get(field) != wanted:
|
||||
raise BaselineAuthorityError(f"authority_binding_mismatch:{field}")
|
||||
return value
|
||||
|
||||
@@ -0,0 +1,108 @@
|
||||
"""Atomic, predicate-bound activation of the durable release campaign."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import re
|
||||
import tempfile
|
||||
from typing import Any, Mapping
|
||||
|
||||
from release_campaign_authorization import CampaignError, validate_policy
|
||||
|
||||
OID_RE = re.compile(r"^[0-9a-f]{40}$")
|
||||
CAMPAIGN_ID = "AIOS-AUTONOMOUS-RELEASE-CAMPAIGN-20260807"
|
||||
|
||||
|
||||
class CampaignActivationError(CampaignError):
|
||||
pass
|
||||
|
||||
|
||||
def canonical_json(value: Any) -> bytes:
|
||||
return (json.dumps(value, sort_keys=True, separators=(",", ":"), ensure_ascii=False) + "\n").encode("utf-8")
|
||||
|
||||
|
||||
def digest_bytes(value: bytes) -> str:
|
||||
return hashlib.sha256(value).hexdigest()
|
||||
|
||||
|
||||
def build_active_policy(current: Mapping[str, Any], facts: Mapping[str, Any]) -> dict[str, Any]:
|
||||
policy = validate_policy(dict(current))
|
||||
required = {
|
||||
"campaign_id",
|
||||
"repository",
|
||||
"source_main_oid",
|
||||
"gitea_main_oid",
|
||||
"authority_main_oid",
|
||||
"runtime_source_oid",
|
||||
"durable_job_state",
|
||||
"writer_ready",
|
||||
"gitea_machine_authenticated",
|
||||
"governance_executor_ready",
|
||||
"preapproval_publisher_ready",
|
||||
"runtime_exact_main",
|
||||
"main_lineage_verified",
|
||||
"campaign_revoked",
|
||||
}
|
||||
if set(facts) != required:
|
||||
raise CampaignActivationError("ACTIVATION_FACTS_SCHEMA_INVALID")
|
||||
if policy["activation_state"] != "PENDING_CONTROL_PLANE_MERGE" or policy["active"] is not False:
|
||||
raise CampaignActivationError("CAMPAIGN_NOT_PENDING")
|
||||
if policy["campaign_id"] != CAMPAIGN_ID or facts["campaign_id"] != CAMPAIGN_ID:
|
||||
raise CampaignActivationError("CAMPAIGN_ID_MISMATCH")
|
||||
if facts["repository"] != "aios/aios" or policy["repository"] != facts["repository"]:
|
||||
raise CampaignActivationError("REPOSITORY_MISMATCH")
|
||||
for name in ("source_main_oid", "gitea_main_oid", "authority_main_oid", "runtime_source_oid"):
|
||||
if not isinstance(facts[name], str) or not OID_RE.fullmatch(facts[name]):
|
||||
raise CampaignActivationError("ACTIVATION_OID_INVALID")
|
||||
if not (
|
||||
facts["source_main_oid"]
|
||||
== facts["gitea_main_oid"]
|
||||
== facts["authority_main_oid"]
|
||||
== facts["runtime_source_oid"]
|
||||
):
|
||||
raise CampaignActivationError("ACTIVATION_MAIN_DIVERGENCE")
|
||||
if facts["durable_job_state"] != "COMPLETED":
|
||||
raise CampaignActivationError("DURABLE_JOB_NOT_COMPLETED")
|
||||
boolean_fields = (
|
||||
"writer_ready",
|
||||
"gitea_machine_authenticated",
|
||||
"governance_executor_ready",
|
||||
"preapproval_publisher_ready",
|
||||
"runtime_exact_main",
|
||||
"main_lineage_verified",
|
||||
)
|
||||
if any(facts[name] is not True for name in boolean_fields) or facts["campaign_revoked"] is not False:
|
||||
raise CampaignActivationError("ACTIVATION_PREDICATE_FAILED")
|
||||
activated = dict(policy)
|
||||
activated["activation_state"] = "ACTIVE"
|
||||
activated["active"] = True
|
||||
return validate_policy(activated)
|
||||
|
||||
|
||||
def atomic_write_policy(path: Path, sidecar: Path, receipt_path: Path, policy: Mapping[str, Any], receipt: Mapping[str, Any]) -> str:
|
||||
if path.is_symlink() or sidecar.is_symlink() or receipt_path.exists() or receipt_path.is_symlink():
|
||||
raise CampaignActivationError("ACTIVATION_PATH_INVALID")
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
raw = canonical_json(dict(policy))
|
||||
digest = digest_bytes(raw)
|
||||
for target, data, mode in (
|
||||
(path, raw, "wb"),
|
||||
(sidecar, f"{digest} {path.name}\n".encode("ascii"), "wb"),
|
||||
(receipt_path, canonical_json(dict(receipt)), "wb"),
|
||||
):
|
||||
fd, temporary = tempfile.mkstemp(prefix=f".{target.name}.", dir=target.parent)
|
||||
try:
|
||||
os.fchmod(fd, 0o600)
|
||||
with os.fdopen(fd, mode) as stream:
|
||||
stream.write(data)
|
||||
stream.flush()
|
||||
os.fsync(stream.fileno())
|
||||
os.replace(temporary, target)
|
||||
os.chmod(target, 0o600)
|
||||
finally:
|
||||
if os.path.exists(temporary):
|
||||
os.unlink(temporary)
|
||||
return digest
|
||||
Executable
+106
@@ -0,0 +1,106 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Machine campaign approval gate for protected engineering PRs."""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
from pathlib import Path
|
||||
import subprocess
|
||||
from typing import Any, Mapping
|
||||
|
||||
from release_campaign_authorization import CampaignAuthorization, CampaignError
|
||||
|
||||
|
||||
class GateError(RuntimeError):
|
||||
pass
|
||||
|
||||
|
||||
def git_is_ancestor(repository: Path, ancestor: str, descendant: str) -> bool:
|
||||
result = subprocess.run(
|
||||
[
|
||||
"git",
|
||||
f"--git-dir={repository}",
|
||||
"merge-base",
|
||||
"--is-ancestor",
|
||||
ancestor,
|
||||
descendant,
|
||||
],
|
||||
check=False,
|
||||
capture_output=True,
|
||||
)
|
||||
if result.returncode == 0:
|
||||
return True
|
||||
if result.returncode == 1:
|
||||
return False
|
||||
raise GateError("git_ancestor_check_failed")
|
||||
|
||||
|
||||
def evaluate(
|
||||
campaign: CampaignAuthorization,
|
||||
facts: Mapping[str, Any],
|
||||
repository: Path,
|
||||
) -> dict[str, Any]:
|
||||
required = {
|
||||
"repository",
|
||||
"pr_number",
|
||||
"base_ref",
|
||||
"base_oid",
|
||||
"head_oid",
|
||||
"required_checks_all_success",
|
||||
"required_checks_same_head",
|
||||
"unresolved_conversations",
|
||||
"mergeable",
|
||||
"protected_target",
|
||||
}
|
||||
if set(facts) != required:
|
||||
raise GateError("pr_facts_fields_invalid")
|
||||
if facts["repository"] != "aios/aios":
|
||||
raise GateError("repository_mismatch")
|
||||
if facts["base_ref"] != "refs/heads/main" or facts["protected_target"] != "main":
|
||||
raise GateError("protected_main_required")
|
||||
if facts["required_checks_all_success"] is not True:
|
||||
raise GateError("required_checks_not_success")
|
||||
if facts["required_checks_same_head"] is not True:
|
||||
raise GateError("required_checks_stale")
|
||||
if facts["unresolved_conversations"] != 0:
|
||||
raise GateError("unresolved_conversations")
|
||||
if facts["mergeable"] is not True:
|
||||
raise GateError("pr_not_mergeable")
|
||||
if not git_is_ancestor(repository, campaign.anchor_oid, str(facts["base_oid"])):
|
||||
raise GateError("base_outside_campaign_lineage")
|
||||
if not git_is_ancestor(repository, str(facts["base_oid"]), str(facts["head_oid"])):
|
||||
raise GateError("head_outside_pr_lineage")
|
||||
campaign.assert_operation("protected_merge")
|
||||
return {
|
||||
"schema": "aios.campaign-approval-gate-result.v1",
|
||||
"status": "SUCCESS",
|
||||
"campaign_id": campaign.campaign_id,
|
||||
"pr_number": facts["pr_number"],
|
||||
"head_oid": facts["head_oid"],
|
||||
"owner_ui_interaction_required": False,
|
||||
}
|
||||
|
||||
|
||||
def main() -> int:
|
||||
p = argparse.ArgumentParser()
|
||||
p.add_argument("--campaign", type=Path, required=True)
|
||||
p.add_argument("--revocation", type=Path)
|
||||
p.add_argument("--pr-facts", type=Path, required=True)
|
||||
p.add_argument("--repository", type=Path, required=True)
|
||||
a = p.parse_args()
|
||||
campaign = CampaignAuthorization.load(
|
||||
a.campaign,
|
||||
revocation=a.revocation,
|
||||
require_active=True,
|
||||
)
|
||||
facts = json.loads(a.pr_facts.read_text(encoding="utf-8"))
|
||||
print(json.dumps(evaluate(campaign, facts, a.repository), sort_keys=True))
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
try:
|
||||
raise SystemExit(main())
|
||||
except (CampaignError, GateError) as exc:
|
||||
print(json.dumps({"status": "FAILURE", "reason": str(exc)}, sort_keys=True))
|
||||
raise SystemExit(2)
|
||||
+150
@@ -0,0 +1,150 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Automatically accept an exact release binding under an ACTIVE campaign."""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import stat
|
||||
import tempfile
|
||||
from typing import Any, Mapping
|
||||
|
||||
from release_campaign_authorization import CampaignAuthorization, CampaignError
|
||||
|
||||
|
||||
class AcceptanceError(RuntimeError):
|
||||
pass
|
||||
|
||||
|
||||
def secure_json(path: Path) -> dict[str, Any]:
|
||||
if path.is_symlink() or not path.is_file():
|
||||
raise AcceptanceError(f"invalid_file:{path}")
|
||||
st = path.lstat()
|
||||
if st.st_uid != 0 or stat.S_IMODE(st.st_mode) & 0o022:
|
||||
raise AcceptanceError(f"unsafe_file:{path}")
|
||||
value = json.loads(path.read_text(encoding="utf-8"))
|
||||
if not isinstance(value, dict):
|
||||
raise AcceptanceError(f"json_object_required:{path}")
|
||||
return value
|
||||
|
||||
|
||||
def atomic_new(path: Path, value: Mapping[str, Any]) -> None:
|
||||
if path.exists() or path.is_symlink():
|
||||
raise AcceptanceError("acceptance_receipt_exists")
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
fd, name = tempfile.mkstemp(prefix=f".{path.name}.", dir=path.parent)
|
||||
try:
|
||||
os.fchmod(fd, 0o600)
|
||||
if os.geteuid() == 0:
|
||||
os.fchown(fd, 0, 0)
|
||||
with os.fdopen(fd, "w", encoding="utf-8", closefd=True) as stream:
|
||||
json.dump(dict(value), stream, sort_keys=True, indent=2)
|
||||
stream.write("\n")
|
||||
stream.flush()
|
||||
os.fsync(stream.fileno())
|
||||
os.replace(name, path)
|
||||
os.chmod(path, 0o600)
|
||||
finally:
|
||||
try:
|
||||
os.unlink(name)
|
||||
except FileNotFoundError:
|
||||
pass
|
||||
|
||||
|
||||
def evaluate(
|
||||
campaign: CampaignAuthorization,
|
||||
binding: Mapping[str, Any],
|
||||
evidence: Mapping[str, Any],
|
||||
artifact: Mapping[str, Any],
|
||||
staging: Mapping[str, Any],
|
||||
main: Mapping[str, Any],
|
||||
tag: Mapping[str, Any],
|
||||
) -> dict[str, Any]:
|
||||
required_binding = {
|
||||
"version",
|
||||
"target_oid",
|
||||
"evidence_closure_oid",
|
||||
"artifact_sha256",
|
||||
"artifact_size",
|
||||
"approval_plan_id",
|
||||
}
|
||||
if set(binding) != required_binding:
|
||||
raise AcceptanceError("binding_fields_invalid")
|
||||
if main.get("repository") != "aios/aios":
|
||||
raise AcceptanceError("repository_identity_mismatch")
|
||||
if main.get("target_in_main_lineage") is not True:
|
||||
raise AcceptanceError("target_outside_main_lineage")
|
||||
if evidence.get("object_type") != "commit":
|
||||
raise AcceptanceError("evidence_not_commit")
|
||||
if evidence.get("target_is_ancestor") is not True:
|
||||
raise AcceptanceError("target_not_evidence_ancestor")
|
||||
if evidence.get("closure_oid") != binding["evidence_closure_oid"]:
|
||||
raise AcceptanceError("evidence_oid_mismatch")
|
||||
if artifact.get("sha256") != binding["artifact_sha256"]:
|
||||
raise AcceptanceError("artifact_digest_mismatch")
|
||||
if artifact.get("size") != binding["artifact_size"]:
|
||||
raise AcceptanceError("artifact_size_mismatch")
|
||||
if artifact.get("retained") is not True:
|
||||
raise AcceptanceError("artifact_not_retained")
|
||||
if staging.get("install") != "PASS":
|
||||
raise AcceptanceError("staging_install_failed")
|
||||
if staging.get("health") != "PASS":
|
||||
raise AcceptanceError("staging_health_failed")
|
||||
if staging.get("restart") != "PASS":
|
||||
raise AcceptanceError("staging_restart_failed")
|
||||
if staging.get("rollback_readiness") != "PASS":
|
||||
raise AcceptanceError("rollback_readiness_failed")
|
||||
if tag.get("conflict") is not False:
|
||||
raise AcceptanceError("tag_conflict")
|
||||
campaign.assert_operation("derive_campaign_acceptance")
|
||||
return {
|
||||
"schema": "aios.campaign-release-acceptance.v1",
|
||||
"accepted": True,
|
||||
"binding_replaced": False,
|
||||
"campaign_id": campaign.campaign_id,
|
||||
"campaign_generation": campaign.data["generation"],
|
||||
**dict(binding),
|
||||
"owner_interaction_required": False,
|
||||
}
|
||||
|
||||
|
||||
def main() -> int:
|
||||
p = argparse.ArgumentParser()
|
||||
p.add_argument("--campaign", type=Path, required=True)
|
||||
p.add_argument("--revocation", type=Path)
|
||||
p.add_argument("--binding", type=Path, required=True)
|
||||
p.add_argument("--evidence", type=Path, required=True)
|
||||
p.add_argument("--artifact", type=Path, required=True)
|
||||
p.add_argument("--staging", type=Path, required=True)
|
||||
p.add_argument("--main-facts", type=Path, required=True)
|
||||
p.add_argument("--tag-facts", type=Path, required=True)
|
||||
p.add_argument("--output", type=Path, required=True)
|
||||
a = p.parse_args()
|
||||
|
||||
campaign = CampaignAuthorization.load(
|
||||
a.campaign,
|
||||
revocation=a.revocation,
|
||||
require_active=True,
|
||||
)
|
||||
receipt = evaluate(
|
||||
campaign,
|
||||
secure_json(a.binding),
|
||||
secure_json(a.evidence),
|
||||
secure_json(a.artifact),
|
||||
secure_json(a.staging),
|
||||
secure_json(a.main_facts),
|
||||
secure_json(a.tag_facts),
|
||||
)
|
||||
atomic_new(a.output, receipt)
|
||||
print(json.dumps(receipt, sort_keys=True))
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
try:
|
||||
raise SystemExit(main())
|
||||
except (CampaignError, AcceptanceError) as exc:
|
||||
print(json.dumps({"status": "BLOCKED", "reason": str(exc)}, sort_keys=True))
|
||||
raise SystemExit(2)
|
||||
@@ -0,0 +1,67 @@
|
||||
"""Runtime deployment-state provider for governed health contracts."""
|
||||
from __future__ import annotations
|
||||
|
||||
import datetime as dt
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
from typing import Any, Mapping
|
||||
|
||||
_OID = re.compile(r"^[0-9a-f]{40}$")
|
||||
_SHA = re.compile(r"^[0-9a-f]{64}$")
|
||||
_STATES = {"STAGED", "ACTIVE", "ROLLED_BACK"}
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class RuntimeDeploymentState:
|
||||
version: str
|
||||
commit_oid: str
|
||||
artifact_sha256: str
|
||||
deployment_state: str
|
||||
|
||||
@property
|
||||
def production_deployed(self) -> bool:
|
||||
return self.deployment_state == "ACTIVE"
|
||||
|
||||
def health_payload(self) -> dict[str, Any]:
|
||||
return {
|
||||
"status": "ok",
|
||||
"version": self.version,
|
||||
"commit_oid": self.commit_oid,
|
||||
"artifact_sha256": self.artifact_sha256,
|
||||
"deployment_state": self.deployment_state,
|
||||
"production_deployed": self.production_deployed,
|
||||
"health_timestamp": dt.datetime.now(dt.timezone.utc).isoformat(),
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def from_environment(cls, environ: Mapping[str, str] | None = None) -> "RuntimeDeploymentState":
|
||||
env = dict(environ or os.environ)
|
||||
values: dict[str, Any] = {}
|
||||
state_file = env.get("DEPLOYMENT_STATE_FILE", "").strip()
|
||||
if state_file:
|
||||
path = Path(state_file)
|
||||
if path.is_file() and not path.is_symlink():
|
||||
loaded = json.loads(path.read_text(encoding="utf-8"))
|
||||
if not isinstance(loaded, dict):
|
||||
raise ValueError("deployment_state_file_object_required")
|
||||
values.update(loaded)
|
||||
file_deployment_state = values.get("deployment_state")
|
||||
values.update({k: v for k, v in {
|
||||
"version": env.get("RELEASE_VERSION", values.get("version", "")),
|
||||
"commit_oid": env.get("TARGET_OID", values.get("commit_oid", "")),
|
||||
"artifact_sha256": env.get("ARTIFACT_SHA256", values.get("artifact_sha256", "")),
|
||||
"deployment_state": env.get("DEPLOYMENT_STATE", values.get("deployment_state", "")),
|
||||
}.items() if v != ""})
|
||||
if file_deployment_state not in (None, ""):
|
||||
values["deployment_state"] = file_deployment_state
|
||||
version = str(values.get("version", ""))
|
||||
commit_oid = str(values.get("commit_oid", ""))
|
||||
artifact_sha256 = str(values.get("artifact_sha256", ""))
|
||||
deployment_state = str(values.get("deployment_state", ""))
|
||||
if not version or not _OID.fullmatch(commit_oid) or not _SHA.fullmatch(artifact_sha256):
|
||||
raise ValueError("deployment_binding_invalid")
|
||||
if deployment_state not in _STATES:
|
||||
raise ValueError("deployment_state_invalid")
|
||||
return cls(version, commit_oid, artifact_sha256, deployment_state)
|
||||
@@ -0,0 +1 @@
|
||||
"""Permanent AIOS governance source components."""
|
||||
@@ -0,0 +1,75 @@
|
||||
#!/usr/bin/env python3
|
||||
import json
|
||||
import pathlib
|
||||
import socket
|
||||
import time
|
||||
|
||||
SOCKET = '/run/aios-governance-executor/executor.sock'
|
||||
STATE = pathlib.Path('/var/lib/aios-gitops/governance-executor/drift-monitor.json')
|
||||
APPROVED = pathlib.Path('/var/lib/aios-gitops/runtime/tasks/AIOS-PROTECTION-SNAPSHOT-DRIFT-FORENSICS-V1/protection-drift/semantic-analysis.json')
|
||||
|
||||
|
||||
def call():
|
||||
sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
|
||||
sock.settimeout(20)
|
||||
sock.connect(SOCKET)
|
||||
request = {'operation': 'GET_BRANCH_PROTECTION', 'payload': {'repository': 'aios/aios', 'branch': 'main'}}
|
||||
sock.sendall((json.dumps(request, separators=(',', ':')) + '\n').encode())
|
||||
chunks = []
|
||||
while True:
|
||||
part = sock.recv(1024 * 1024)
|
||||
if not part:
|
||||
break
|
||||
chunks.append(part)
|
||||
if b'\n' in part:
|
||||
break
|
||||
sock.close()
|
||||
return json.loads(b''.join(chunks).splitlines()[0])
|
||||
|
||||
|
||||
def main():
|
||||
try:
|
||||
result = call()
|
||||
if result.get('status') != 'PASS':
|
||||
raise RuntimeError(str(result.get('code') or 'EXECUTOR_GET_FAILED'))
|
||||
approved = json.loads(APPROVED.read_text(encoding='utf-8'))
|
||||
approved_transport = approved['approved_transport_digest_from_evidence']
|
||||
approved_semantic = approved['approved_semantic_digest']
|
||||
transport = result['transport_digest']
|
||||
semantic = result['semantic_digest']
|
||||
if semantic != approved_semantic:
|
||||
state = 'GOVERNANCE_POLICY_DRIFT'
|
||||
elif transport != approved_transport:
|
||||
state = 'GOVERNANCE_TRANSPORT_METADATA_CHANGED'
|
||||
else:
|
||||
state = 'NO_DRIFT'
|
||||
payload = {
|
||||
'schema': 'aios.governance-drift-monitor.v1',
|
||||
'observed_at_unix': int(time.time()),
|
||||
'status': state,
|
||||
'release_blocking': False,
|
||||
'approved_transport_digest': approved_transport,
|
||||
'observed_transport_digest': transport,
|
||||
'approved_semantic_digest': approved_semantic,
|
||||
'observed_semantic_digest': semantic,
|
||||
'semantic_schema': result.get('semantic_schema'),
|
||||
'source': 'Dedicated Governance Executor GET_BRANCH_PROTECTION',
|
||||
}
|
||||
except Exception as exc:
|
||||
payload = {
|
||||
'schema': 'aios.governance-drift-monitor.v1',
|
||||
'observed_at_unix': int(time.time()),
|
||||
'status': 'MONITOR_UNAVAILABLE',
|
||||
'release_blocking': False,
|
||||
'error_code': str(exc),
|
||||
}
|
||||
STATE.parent.mkdir(parents=True, exist_ok=True)
|
||||
temporary = STATE.with_suffix('.tmp')
|
||||
temporary.write_text(json.dumps(payload, sort_keys=True, separators=(',', ':'), ensure_ascii=False) + '\n', encoding='utf-8')
|
||||
temporary.replace(STATE)
|
||||
STATE.chmod(0o640)
|
||||
print(json.dumps(payload, sort_keys=True, separators=(',', ':'), ensure_ascii=False))
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
+417
@@ -0,0 +1,417 @@
|
||||
#!/usr/bin/env python3
|
||||
import base64
|
||||
import copy
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
import pathlib
|
||||
import socket
|
||||
import time
|
||||
import urllib.error
|
||||
import urllib.parse
|
||||
import urllib.request
|
||||
|
||||
SOURCE_GOVERNANCE = "/opt/aios/aios-control-plane/current/active-runtime/app/governance"
|
||||
if SOURCE_GOVERNANCE not in __import__("sys").path:
|
||||
__import__("sys").path.insert(0, SOURCE_GOVERNANCE)
|
||||
from stage2_gate_migration import (
|
||||
CAMPAIGN_GATE,
|
||||
CAMPAIGN_ID,
|
||||
OWNER_GATE,
|
||||
STAGE1_SEMANTIC_SHA,
|
||||
Stage2GateError,
|
||||
build_target,
|
||||
semantic_digest as stage2_semantic_digest,
|
||||
validate_stage2_facts,
|
||||
)
|
||||
|
||||
API = "http://127.0.0.1:3000"
|
||||
REPO = "aios/aios"
|
||||
SOCKET = "/run/aios-governance-executor/executor.sock"
|
||||
JOURNAL = pathlib.Path("/var/lib/aios-gitops/governance-executor/executor-journal.jsonl")
|
||||
RECONCILIATION_BINDING_PATH = pathlib.Path("/var/lib/aios-gitops/governance-executor/reconciliation-binding.json")
|
||||
PR54_HEAD = "015185d7e5191d18c5c1596160807ac228782a0a"
|
||||
PR54_REVIEW = 104
|
||||
PR54_MANIFEST_SHA = "1b11ee42d53a9f6c1646e5f5ddd8ce5b7ecf301483b9f3d47a068290d541ba4b"
|
||||
PR54_PREVIOUS_PROTECTION_SHA = "8b3370add21cd23c2b4f41d8d84d3eaaa09ddf0451549b72c7941bbd92d5d60a"
|
||||
SEMANTIC_SCHEMA = "aios.semantic-protection.v1"
|
||||
APPROVED_SEMANTIC_SHA = "ba15d512ed8c1f43e5f72c84125a3a71a7347ef59e18000bbfca24d6e69f1b19"
|
||||
STAGE1_SEMANTIC_SHA = "d911e856f71de6105b8efa08e13338a448e0b6d37536332decd80c9f409ecf27"
|
||||
USER_IDS = {"owner": 2, "aios-autonomy-agent": 153}
|
||||
|
||||
|
||||
def canon(value):
|
||||
return json.dumps(value, sort_keys=True, separators=(",", ":"), ensure_ascii=False).encode()
|
||||
|
||||
|
||||
def sha(value):
|
||||
return hashlib.sha256(canon(value)).hexdigest()
|
||||
|
||||
|
||||
def token():
|
||||
value = (pathlib.Path(os.environ["CREDENTIALS_DIRECTORY"]) / "gitea-governance-executor-token").read_text().strip()
|
||||
if len(value) < 16:
|
||||
raise RuntimeError("EXECUTOR_CREDENTIAL_INVALID")
|
||||
return value
|
||||
|
||||
|
||||
def api(method, path, value, body=None, expected=(200,)):
|
||||
raw = None if body is None else json.dumps(body, separators=(",", ":")).encode()
|
||||
req = urllib.request.Request(API + path, data=raw, method=method)
|
||||
req.add_header("Authorization", "token " + value)
|
||||
req.add_header("Accept", "application/json")
|
||||
if raw is not None:
|
||||
req.add_header("Content-Type", "application/json")
|
||||
try:
|
||||
with urllib.request.urlopen(req, timeout=20) as response:
|
||||
status = response.status
|
||||
payload = response.read()
|
||||
except urllib.error.HTTPError as exc:
|
||||
if exc.code in expected:
|
||||
payload = exc.read()
|
||||
return exc.code, json.loads(payload) if payload else None
|
||||
raise RuntimeError("GITEA_HTTP_" + str(exc.code))
|
||||
if status not in expected:
|
||||
raise RuntimeError("GITEA_HTTP_" + str(status))
|
||||
if not payload or not payload.strip():
|
||||
raise RuntimeError("GITEA_EMPTY_JSON:" + path)
|
||||
try:
|
||||
decoded = json.loads(payload)
|
||||
except (UnicodeDecodeError, json.JSONDecodeError) as exc:
|
||||
raise RuntimeError("GITEA_INVALID_JSON:" + path) from exc
|
||||
return status, decoded
|
||||
|
||||
|
||||
def journal(event, **fields):
|
||||
JOURNAL.parent.mkdir(parents=True, exist_ok=True)
|
||||
record = {"schema": "aios.governance-executor-journal.v2", "event": event, "unix": int(time.time()), **fields}
|
||||
with JOURNAL.open("a", encoding="utf-8") as stream:
|
||||
stream.write(json.dumps(record, sort_keys=True, separators=(",", ":"), ensure_ascii=False) + "\n")
|
||||
stream.flush()
|
||||
os.fsync(stream.fileno())
|
||||
|
||||
|
||||
def typed_json(value):
|
||||
if value in (None, "null"):
|
||||
return []
|
||||
if isinstance(value, str):
|
||||
return json.loads(value)
|
||||
return value
|
||||
|
||||
|
||||
def user_ids(names):
|
||||
result = []
|
||||
for name in names or []:
|
||||
if name not in USER_IDS:
|
||||
raise RuntimeError("UNKNOWN_GOVERNANCE_USER:" + str(name))
|
||||
result.append(USER_IDS[name])
|
||||
return sorted(result)
|
||||
|
||||
|
||||
def semantic_policy(value):
|
||||
policy = {
|
||||
"schema_version": SEMANTIC_SCHEMA,
|
||||
"repo_id": 1,
|
||||
"protection_id": 2,
|
||||
"branch_name": value.get("branch_name"),
|
||||
"can_push": value.get("enable_push"),
|
||||
"can_force_push": value.get("enable_force_push"),
|
||||
"enable_merge_whitelist": value.get("enable_merge_whitelist"),
|
||||
"merge_whitelist_user_ids": user_ids(value.get("merge_whitelist_usernames")),
|
||||
"merge_whitelist_team_ids": [],
|
||||
"required_approvals": value.get("required_approvals"),
|
||||
"enable_status_check": value.get("enable_status_check"),
|
||||
"status_check_contexts": sorted(value.get("status_check_contexts") or []),
|
||||
"block_admin_merge_override": value.get("block_admin_merge_override"),
|
||||
"block_on_official_review_requests": value.get("block_on_official_review_requests"),
|
||||
"block_on_outdated_branch": value.get("block_on_outdated_branch"),
|
||||
"block_on_rejected_reviews": value.get("block_on_rejected_reviews"),
|
||||
"dismiss_stale_approvals": value.get("dismiss_stale_approvals"),
|
||||
"ignore_stale_approvals": value.get("ignore_stale_approvals"),
|
||||
"enable_approvals_whitelist": value.get("enable_approvals_whitelist"),
|
||||
"approvals_whitelist_user_ids": user_ids(value.get("approvals_whitelist_username")),
|
||||
"approvals_whitelist_team_ids": [],
|
||||
"enable_bypass_allowlist": value.get("enable_bypass_allowlist"),
|
||||
"bypass_allowlist_user_ids": user_ids(value.get("bypass_allowlist_usernames")),
|
||||
"bypass_allowlist_team_ids": [],
|
||||
"enable_force_push_allowlist": value.get("enable_force_push_allowlist"),
|
||||
"force_push_allowlist_user_ids": user_ids(value.get("force_push_allowlist_usernames")),
|
||||
"force_push_allowlist_team_ids": [],
|
||||
"enable_whitelist": value.get("enable_push_whitelist"),
|
||||
"whitelist_user_ids": user_ids(value.get("push_whitelist_usernames")),
|
||||
"whitelist_team_ids": [],
|
||||
"force_push_allowlist_deploy_keys": value.get("force_push_allowlist_deploy_keys"),
|
||||
"whitelist_deploy_keys": value.get("push_whitelist_deploy_keys"),
|
||||
"require_signed_commits": value.get("require_signed_commits"),
|
||||
"priority": value.get("priority"),
|
||||
"protected_file_patterns": value.get("protected_file_patterns"),
|
||||
"unprotected_file_patterns": value.get("unprotected_file_patterns"),
|
||||
}
|
||||
return policy
|
||||
|
||||
|
||||
def semantic_sha(value):
|
||||
return sha(semantic_policy(value))
|
||||
|
||||
|
||||
def verify_binding(binding, value):
|
||||
required = {
|
||||
"governance_request_PR", "exact_request_head", "Owner_review_id", "Owner_review_exact_head",
|
||||
"manifest_sha256", "expected_previous_snapshot_sha256", "approved_delta",
|
||||
"semantic_reconciliation_request_id", "semantic_reconciliation_head",
|
||||
"semantic_reconciliation_review_id", "semantic_reconciliation_review_head",
|
||||
"approved_semantic_digest",
|
||||
}
|
||||
if set(binding) != required:
|
||||
raise RuntimeError("GOVERNANCE_BINDING_SCHEMA_INVALID")
|
||||
if binding["governance_request_PR"] != 54 or binding["exact_request_head"] != PR54_HEAD:
|
||||
raise RuntimeError("GOVERNANCE_BINDING_PR54_MISMATCH")
|
||||
if binding["Owner_review_id"] != PR54_REVIEW or binding["Owner_review_exact_head"] != PR54_HEAD:
|
||||
raise RuntimeError("GOVERNANCE_BINDING_REVIEW_MISMATCH")
|
||||
if binding["manifest_sha256"] != PR54_MANIFEST_SHA:
|
||||
raise RuntimeError("GOVERNANCE_BINDING_MANIFEST_MISMATCH")
|
||||
if binding["expected_previous_snapshot_sha256"] != PR54_PREVIOUS_PROTECTION_SHA:
|
||||
raise RuntimeError("GOVERNANCE_BINDING_SNAPSHOT_MISMATCH")
|
||||
if binding["approved_delta"] != {"add_merge_whitelist_user_id": 153}:
|
||||
raise RuntimeError("GOVERNANCE_BINDING_DELTA_DENIED")
|
||||
if binding["approved_semantic_digest"] != APPROVED_SEMANTIC_SHA:
|
||||
raise RuntimeError("GOVERNANCE_BINDING_SEMANTIC_DIGEST_MISMATCH")
|
||||
if not RECONCILIATION_BINDING_PATH.is_file():
|
||||
raise RuntimeError("SEMANTIC_RECONCILIATION_BINDING_REQUIRED")
|
||||
try:
|
||||
reconciliation = json.loads(RECONCILIATION_BINDING_PATH.read_text(encoding="utf-8"))
|
||||
except (OSError, ValueError):
|
||||
raise RuntimeError("SEMANTIC_RECONCILIATION_BINDING_INVALID") from None
|
||||
required_reconciliation = {
|
||||
"schema", "request_id", "pr_number", "request_head", "owner_review_id",
|
||||
"owner_review_head", "approved_semantic_digest", "approved_delta",
|
||||
}
|
||||
if set(reconciliation) != required_reconciliation or reconciliation.get("schema") != "aios.semantic-reconciliation-binding.v1":
|
||||
raise RuntimeError("SEMANTIC_RECONCILIATION_BINDING_INVALID")
|
||||
if reconciliation["approved_semantic_digest"] != APPROVED_SEMANTIC_SHA or reconciliation["approved_delta"] != binding["approved_delta"]:
|
||||
raise RuntimeError("SEMANTIC_RECONCILIATION_BINDING_MISMATCH")
|
||||
if (
|
||||
reconciliation["request_id"] != binding["semantic_reconciliation_request_id"]
|
||||
or reconciliation["request_head"] != binding["semantic_reconciliation_head"]
|
||||
or reconciliation["owner_review_id"] != binding["semantic_reconciliation_review_id"]
|
||||
or reconciliation["owner_review_head"] != binding["semantic_reconciliation_review_head"]
|
||||
):
|
||||
raise RuntimeError("SEMANTIC_RECONCILIATION_BINDING_MISMATCH")
|
||||
_, reconciliation_pr = api("GET", f"/api/v1/repos/{REPO}/pulls/{reconciliation['pr_number']}", value)
|
||||
if (
|
||||
reconciliation_pr.get("state") != "open"
|
||||
or reconciliation_pr.get("merged")
|
||||
or reconciliation_pr.get("head", {}).get("sha") != reconciliation["request_head"]
|
||||
or reconciliation_pr.get("base", {}).get("ref") != "governance/approvals"
|
||||
):
|
||||
raise RuntimeError("SEMANTIC_RECONCILIATION_REQUEST_STALE")
|
||||
_, reconciliation_reviews = api("GET", f"/api/v1/repos/{REPO}/pulls/{reconciliation['pr_number']}/reviews", value)
|
||||
reconciliation_review = next((x for x in reconciliation_reviews if x.get("id") == reconciliation["owner_review_id"]), None)
|
||||
if (
|
||||
not reconciliation_review
|
||||
or reconciliation_review.get("user", {}).get("id") != 2
|
||||
or reconciliation_review.get("state") != "APPROVED"
|
||||
or reconciliation_review.get("commit_id") != reconciliation["owner_review_head"]
|
||||
or reconciliation_review.get("dismissed") is not False
|
||||
):
|
||||
raise RuntimeError("SEMANTIC_RECONCILIATION_OWNER_REVIEW_STALE")
|
||||
_, pr = api("GET", f"/api/v1/repos/{REPO}/pulls/54", value)
|
||||
if pr.get("state") != "open" or pr.get("merged") or pr.get("head", {}).get("sha") != PR54_HEAD:
|
||||
raise RuntimeError("PR54_APPROVAL_STALE")
|
||||
_, reviews = api("GET", f"/api/v1/repos/{REPO}/pulls/54/reviews", value)
|
||||
review = next((x for x in reviews if x.get("id") == PR54_REVIEW), None)
|
||||
if not review or review.get("user", {}).get("id") != 2 or review.get("state") != "APPROVED" or review.get("commit_id") != PR54_HEAD or review.get("dismissed") is not False:
|
||||
raise RuntimeError("PR54_APPROVAL_STALE")
|
||||
manifest_path = "approvals/governance-policy/AIOS-AUTONOMOUS-MERGE-GOVERNANCE-SCOPE-EXTENSION-V1.json"
|
||||
_, content = api("GET", f"/api/v1/repos/{REPO}/contents/{urllib.parse.quote(manifest_path, safe='/')}?ref={PR54_HEAD}", value)
|
||||
raw = base64.b64decode("".join(content.get("content", "").split()))
|
||||
if hashlib.sha256(raw).hexdigest() != PR54_MANIFEST_SHA:
|
||||
raise RuntimeError("PR54_MANIFEST_STALE")
|
||||
|
||||
|
||||
def get_protection(value, branch):
|
||||
if branch not in {"main", "governance/approvals"}:
|
||||
raise RuntimeError("BRANCH_DENIED")
|
||||
_, protection = api("GET", f"/api/v1/repos/{REPO}/branch_protections/{urllib.parse.quote(branch, safe='')}", value)
|
||||
return protection
|
||||
|
||||
|
||||
def mutation_receipt(before, after, binding, target_semantic):
|
||||
return {
|
||||
"before": {"transport_digest": sha(before), "semantic_digest": semantic_sha(before)},
|
||||
"approved": {
|
||||
"mutation_predicate_digest": sha({"operation": "ADD_MERGE_EXECUTOR", "subject_user_id": 153, "preserve_all_unmodified_security_fields": True}),
|
||||
"Owner_review_id": binding["Owner_review_id"],
|
||||
"semantic_policy_digest": APPROVED_SEMANTIC_SHA,
|
||||
},
|
||||
"after": {"transport_digest": sha(after), "semantic_digest": semantic_sha(after)},
|
||||
"delta": {"add_merge_whitelist_user_id": 153},
|
||||
"target_semantic_digest": sha(target_semantic),
|
||||
"unmodified_security_fields": {"verified": True},
|
||||
}
|
||||
|
||||
|
||||
def apply_stage1(binding, value):
|
||||
verify_binding(binding, value)
|
||||
before = get_protection(value, "main")
|
||||
before_semantic = semantic_policy(before)
|
||||
before_semantic_sha = sha(before_semantic)
|
||||
names = list(before.get("merge_whitelist_usernames") or [])
|
||||
name_ids = user_ids(names)
|
||||
if name_ids not in ([2], [2, 153]):
|
||||
raise RuntimeError("PROTECTION_WHITELIST_PRESTATE_INVALID")
|
||||
target_names = ["owner", "aios-autonomy-agent"]
|
||||
target_semantic = copy.deepcopy(before_semantic)
|
||||
target_semantic["merge_whitelist_user_ids"] = [2, 153]
|
||||
if name_ids == [2, 153]:
|
||||
if before_semantic_sha != STAGE1_SEMANTIC_SHA:
|
||||
journal("APPLY_BLOCKED_STAGE1_IDEMPOTENCY_SEMANTIC_CONFLICT", transport_digest=sha(before), semantic_digest=before_semantic_sha, expected_semantic_digest=STAGE1_SEMANTIC_SHA)
|
||||
raise RuntimeError("PROTECTION_STAGE1_SEMANTIC_CONFLICT")
|
||||
receipt = mutation_receipt(before, before, binding, target_semantic)
|
||||
journal("APPLY_APPROVED_PROTECTION_DELTA_IDEMPOTENT_SUCCESS", receipt=receipt)
|
||||
return {"status": "IDEMPOTENT_SUCCESS", "protection": before, "transport_digest": sha(before), "semantic_digest": before_semantic_sha, "target_semantic_digest": sha(target_semantic)}
|
||||
if before_semantic_sha != APPROVED_SEMANTIC_SHA:
|
||||
journal("APPLY_BLOCKED_SEMANTIC_CAS", transport_digest=sha(before), semantic_digest=before_semantic_sha, expected_semantic_digest=APPROVED_SEMANTIC_SHA)
|
||||
raise RuntimeError("PROTECTION_SEMANTIC_CAS_CONFLICT")
|
||||
after_api = api("PATCH", f"/api/v1/repos/{REPO}/branch_protections/main", value, {"merge_whitelist_usernames": target_names})[1]
|
||||
after = get_protection(value, "main")
|
||||
for key in before:
|
||||
if key in {"updated_at", "merge_whitelist_usernames"}:
|
||||
continue
|
||||
if after.get(key) != before.get(key):
|
||||
raise RuntimeError("PROTECTION_UNAPPROVED_FIELD_CHANGED:" + key)
|
||||
if user_ids(after.get("merge_whitelist_usernames")) != [2, 153]:
|
||||
raise RuntimeError("PROTECTION_TARGET_READBACK_FAILED")
|
||||
after_semantic = semantic_policy(after)
|
||||
if after_semantic != target_semantic:
|
||||
raise RuntimeError("PROTECTION_SEMANTIC_TARGET_READBACK_FAILED")
|
||||
receipt = mutation_receipt(before, after, binding, target_semantic)
|
||||
journal("APPLY_APPROVED_PROTECTION_DELTA_COMPLETED", receipt=receipt)
|
||||
return {"status": "APPLIED", "protection": after, "previous_transport_digest": sha(before), "previous_semantic_digest": before_semantic_sha, "transport_digest": sha(after), "semantic_digest": sha(after_semantic), "receipt": receipt}
|
||||
|
||||
|
||||
def apply_stage2(binding, facts, value):
|
||||
verify_binding(binding, value)
|
||||
validate_stage2_facts(facts)
|
||||
before = get_protection(value, "main")
|
||||
if OWNER_GATE in (before.get("status_check_contexts") or []) and stage2_semantic_digest(before) != STAGE1_SEMANTIC_SHA:
|
||||
raise RuntimeError("STAGE2_SEMANTIC_PRECONDITION_INVALID")
|
||||
try:
|
||||
status, target = build_target(before)
|
||||
except Stage2GateError as exc:
|
||||
journal("MIGRATE_APPROVED_GATE_BLOCKED", reason=str(exc), semantic_digest=stage2_semantic_digest(before))
|
||||
raise RuntimeError(str(exc)) from exc
|
||||
target_digest = stage2_semantic_digest(target)
|
||||
if status == "IDEMPOTENT_SUCCESS":
|
||||
journal(
|
||||
"MIGRATE_APPROVED_GATE_IDEMPOTENT_SUCCESS",
|
||||
before_semantic_digest=stage2_semantic_digest(before),
|
||||
target_semantic_digest=target_digest,
|
||||
campaign_id=CAMPAIGN_ID,
|
||||
)
|
||||
return {
|
||||
"status": status,
|
||||
"protection": before,
|
||||
"transport_digest": sha(before),
|
||||
"semantic_digest": stage2_semantic_digest(before),
|
||||
"target_semantic_digest": target_digest,
|
||||
"gate_transition": {"removed": OWNER_GATE, "required": CAMPAIGN_GATE},
|
||||
}
|
||||
after_api = api(
|
||||
"PATCH",
|
||||
f"/api/v1/repos/{REPO}/branch_protections/main",
|
||||
value,
|
||||
{"status_check_contexts": target["status_check_contexts"]},
|
||||
)[1]
|
||||
after = get_protection(value, "main")
|
||||
for key in before:
|
||||
if key in {"updated_at", "status_check_contexts"}:
|
||||
continue
|
||||
if after.get(key) != before.get(key):
|
||||
raise RuntimeError("PROTECTION_UNAPPROVED_FIELD_CHANGED:" + key)
|
||||
if after.get("status_check_contexts") != target["status_check_contexts"]:
|
||||
raise RuntimeError("STAGE2_GATE_TARGET_READBACK_FAILED")
|
||||
if stage2_semantic_digest(after) != target_digest:
|
||||
raise RuntimeError("STAGE2_SEMANTIC_TARGET_READBACK_FAILED")
|
||||
receipt = {
|
||||
"before_transport_digest": sha(before),
|
||||
"before_semantic_digest": stage2_semantic_digest(before),
|
||||
"after_transport_digest": sha(after),
|
||||
"after_semantic_digest": stage2_semantic_digest(after),
|
||||
"target_semantic_digest": target_digest,
|
||||
"campaign_id": CAMPAIGN_ID,
|
||||
"gate_transition": {"removed": OWNER_GATE, "required": CAMPAIGN_GATE},
|
||||
"preserved_security_fields": True,
|
||||
"facts_digest": sha(facts),
|
||||
}
|
||||
journal("MIGRATE_APPROVED_GATE_COMPLETED", receipt=receipt)
|
||||
return {
|
||||
"status": "APPLIED",
|
||||
"protection": after,
|
||||
"previous_transport_digest": sha(before),
|
||||
"previous_semantic_digest": stage2_semantic_digest(before),
|
||||
"transport_digest": sha(after),
|
||||
"semantic_digest": stage2_semantic_digest(after),
|
||||
"target_semantic_digest": target_digest,
|
||||
"receipt": receipt,
|
||||
}
|
||||
|
||||
|
||||
def handle(request, value):
|
||||
if not isinstance(request, dict) or set(request) != {"operation", "payload"}:
|
||||
raise RuntimeError("REQUEST_SCHEMA_INVALID")
|
||||
operation = request["operation"]
|
||||
payload = request["payload"]
|
||||
if not isinstance(payload, dict) or payload.get("repository") != REPO:
|
||||
raise RuntimeError("REPOSITORY_DENIED")
|
||||
if operation == "GET_BRANCH_PROTECTION":
|
||||
protection = get_protection(value, payload.get("branch"))
|
||||
journal("GET_BRANCH_PROTECTION", branch=payload.get("branch"), transport_digest=sha(protection), semantic_digest=semantic_sha(protection))
|
||||
return {"status": "PASS", "protection": protection, "snapshot_sha256": sha(protection), "transport_digest": sha(protection), "semantic_digest": semantic_sha(protection), "semantic_schema": SEMANTIC_SCHEMA}
|
||||
if operation == "READBACK_BRANCH_PROTECTION":
|
||||
protection = get_protection(value, payload.get("branch"))
|
||||
return {"status": "PASS", "protection": protection, "snapshot_sha256": sha(protection), "transport_digest": sha(protection), "semantic_digest": semantic_sha(protection), "semantic_schema": SEMANTIC_SCHEMA}
|
||||
if operation == "APPLY_APPROVED_PROTECTION_DELTA":
|
||||
binding = payload.get("GovernanceExecutionBinding")
|
||||
if not isinstance(binding, dict):
|
||||
raise RuntimeError("GOVERNANCE_BINDING_REQUIRED")
|
||||
return apply_stage1(binding, value)
|
||||
if operation == "MIGRATE_APPROVED_GATE":
|
||||
binding = payload.get("GovernanceExecutionBinding")
|
||||
facts = payload.get("Stage2Facts")
|
||||
if not isinstance(binding, dict) or not isinstance(facts, dict):
|
||||
raise RuntimeError("STAGE2_BINDING_OR_FACTS_REQUIRED")
|
||||
return apply_stage2(binding, facts, value)
|
||||
raise RuntimeError("OPERATION_DENIED")
|
||||
|
||||
|
||||
def main():
|
||||
value = token()
|
||||
sock_path = pathlib.Path(SOCKET)
|
||||
sock_path.unlink(missing_ok=True)
|
||||
server = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
|
||||
server.bind(SOCKET)
|
||||
os.chmod(SOCKET, 0o660)
|
||||
server.listen(16)
|
||||
journal("EXECUTOR_STARTED", api_surface=["GET_BRANCH_PROTECTION", "APPLY_APPROVED_PROTECTION_DELTA", "MIGRATE_APPROVED_GATE", "READBACK_BRANCH_PROTECTION"], semantic_schema=SEMANTIC_SCHEMA, approved_semantic_digest=APPROVED_SEMANTIC_SHA, stage1_semantic_digest=STAGE1_SEMANTIC_SHA, campaign_gate=CAMPAIGN_GATE)
|
||||
try:
|
||||
while True:
|
||||
conn, _ = server.accept()
|
||||
with conn:
|
||||
data = conn.recv(1024 * 1024 + 1)
|
||||
if len(data) > 1024 * 1024:
|
||||
result = {"status": "FAIL", "code": "REQUEST_TOO_LARGE"}
|
||||
else:
|
||||
try:
|
||||
request = json.loads(data.decode())
|
||||
result = handle(request, value)
|
||||
except Exception as exc:
|
||||
result = {"status": "FAIL", "code": str(exc)}
|
||||
conn.sendall((json.dumps(result, sort_keys=True, separators=(",", ":"), ensure_ascii=False) + "\n").encode())
|
||||
finally:
|
||||
server.close()
|
||||
sock_path.unlink(missing_ok=True)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,347 @@
|
||||
#!/usr/bin/env python3
|
||||
import base64
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
import pathlib
|
||||
import shutil
|
||||
import socket
|
||||
import subprocess
|
||||
import tempfile
|
||||
import time
|
||||
import urllib.error
|
||||
import urllib.parse
|
||||
import urllib.request
|
||||
|
||||
LOCAL_API = 'http://127.0.0.1:3000/api/v1'
|
||||
NETWORK_API = 'https://g.cnnetmkt.top/git/api/v1'
|
||||
NETWORK_REMOTE = 'https://g.cnnetmkt.top/git/aios/aios.git'
|
||||
REPO = 'aios/aios'
|
||||
BASE_BRANCH = 'governance/approvals'
|
||||
REQUEST_BRANCH = 'governance/request/protection-semantic-reconciliation-v1-20260808'
|
||||
SOCKET_PATH = '/run/aios-governance-preapproval-publisher/publisher.sock'
|
||||
CREDENTIAL_NAME = 'gitea-machine-token'
|
||||
AUTHORITY_REPO = '/data/aios-gitops/repo.git'
|
||||
TASK_ROOT = pathlib.Path('/var/lib/aios-gitops/runtime/tasks/AIOS-PROTECTION-SNAPSHOT-DRIFT-FORENSICS-V1')
|
||||
INPUT_ROOT = TASK_ROOT / 'publication-input'
|
||||
RECEIPT = TASK_ROOT / 'publication-receipt.json'
|
||||
MANIFEST_REL = 'approvals/governance-policy/AIOS-PROTECTION-SEMANTIC-SNAPSHOT-RECONCILIATION-V1.json'
|
||||
EVIDENCE_REL = 'approvals/governance-policy/AIOS-PROTECTION-SEMANTIC-SNAPSHOT-RECONCILIATION-V1.evidence.json'
|
||||
|
||||
|
||||
def digest_bytes(data):
|
||||
return hashlib.sha256(data).hexdigest()
|
||||
|
||||
|
||||
def digest_json(value):
|
||||
return digest_bytes(json.dumps(value, sort_keys=True, separators=(',', ':'), ensure_ascii=False).encode())
|
||||
|
||||
|
||||
def load_token():
|
||||
path = pathlib.Path(os.environ['CREDENTIALS_DIRECTORY']) / CREDENTIAL_NAME
|
||||
value = path.read_text(encoding='utf-8').strip()
|
||||
if len(value) < 16:
|
||||
raise RuntimeError('MACHINE_CREDENTIAL_INVALID')
|
||||
return value
|
||||
|
||||
|
||||
def api(base, method, path, token, body=None, expected=(200,)):
|
||||
raw = None if body is None else json.dumps(body, separators=(',', ':'), ensure_ascii=False).encode()
|
||||
request = urllib.request.Request(base + path, data=raw, method=method)
|
||||
request.add_header('Authorization', 'token ' + token)
|
||||
request.add_header('Accept', 'application/json')
|
||||
if raw is not None:
|
||||
request.add_header('Content-Type', 'application/json')
|
||||
try:
|
||||
with urllib.request.urlopen(request, timeout=20) as response:
|
||||
payload = response.read()
|
||||
if response.status not in expected:
|
||||
raise RuntimeError('GITEA_HTTP_' + str(response.status))
|
||||
return response.status, json.loads(payload) if payload else None
|
||||
except urllib.error.HTTPError as exc:
|
||||
payload = exc.read()
|
||||
if exc.code in expected:
|
||||
return exc.code, json.loads(payload) if payload else None
|
||||
raise RuntimeError('GITEA_HTTP_' + str(exc.code) + ':' + method + ':' + path) from None
|
||||
|
||||
|
||||
def authority_head():
|
||||
result = subprocess.run(
|
||||
['git', '--git-dir=' + AUTHORITY_REPO, 'rev-parse', 'refs/heads/' + BASE_BRANCH],
|
||||
text=True, capture_output=True, check=False,
|
||||
)
|
||||
if result.returncode != 0:
|
||||
raise RuntimeError('AUTHORITY_BASE_UNAVAILABLE')
|
||||
return result.stdout.strip()
|
||||
|
||||
|
||||
def branch_api(base, token, branch, expected=(200,)):
|
||||
return api(base, 'GET', '/repos/' + REPO + '/branches/' + urllib.parse.quote(branch, safe=''), token, expected=expected)
|
||||
|
||||
|
||||
def verify_base(token):
|
||||
authority = authority_head()
|
||||
_, local = branch_api(LOCAL_API, token, BASE_BRANCH)
|
||||
_, network = branch_api(NETWORK_API, token, BASE_BRANCH)
|
||||
local_oid = local.get('commit', {}).get('id')
|
||||
network_oid = network.get('commit', {}).get('id')
|
||||
if not authority or authority != local_oid or authority != network_oid:
|
||||
raise RuntimeError('RECONCILIATION_REQUIRED')
|
||||
return authority
|
||||
|
||||
|
||||
def verify_identity(token):
|
||||
_, user = api(LOCAL_API, 'GET', '/user', token)
|
||||
if user.get('login') != 'aios-autonomy-agent' or user.get('id') != 153 or user.get('is_admin') is not False:
|
||||
raise RuntimeError('MACHINE_IDENTITY_REJECTED')
|
||||
return {'login': user['login'], 'id': user['id'], 'owner': user.get('is_admin')}
|
||||
|
||||
|
||||
def load_inputs():
|
||||
manifest = json.dumps(json.loads((INPUT_ROOT / 'manifest.json').read_text(encoding='utf-8')), sort_keys=True, separators=(',', ':'), ensure_ascii=False).encode()
|
||||
evidence = json.dumps(json.loads((INPUT_ROOT / 'evidence.json').read_text(encoding='utf-8')), sort_keys=True, separators=(',', ':'), ensure_ascii=False).encode()
|
||||
for data in (manifest, evidence):
|
||||
if any(secret in data.lower() for secret in (b'authorization:', b'ghp_', b'github_pat_', b'secret=', b'token=')):
|
||||
raise RuntimeError('SECRET_CONTENT_REJECTED')
|
||||
return manifest, evidence
|
||||
|
||||
|
||||
def publication_key(base_oid, manifest_sha, evidence_sha):
|
||||
payload = REPO + base_oid + REQUEST_BRANCH + manifest_sha + evidence_sha
|
||||
return hashlib.sha256(payload.encode()).hexdigest()
|
||||
|
||||
|
||||
def write_receipt(state, **fields):
|
||||
record = {
|
||||
'schema': 'aios.governance-preapproval-publication.v1',
|
||||
'operation': 'SEMANTIC_RECONCILIATION_REQUEST',
|
||||
'state': state,
|
||||
'recorded_unix': int(time.time()),
|
||||
'request_branch': REQUEST_BRANCH,
|
||||
'manifest_path': MANIFEST_REL,
|
||||
'evidence_path': EVIDENCE_REL,
|
||||
'machine_actor': 'aios-autonomy-agent',
|
||||
'owner': False,
|
||||
'credential_ref': {'scheme': 'systemd-credential', 'name': CREDENTIAL_NAME},
|
||||
'governance_capability_executed': False,
|
||||
'protection_mutation': False,
|
||||
'force_push': False,
|
||||
**fields,
|
||||
}
|
||||
RECEIPT.parent.mkdir(parents=True, exist_ok=True)
|
||||
temporary = RECEIPT.with_suffix('.tmp')
|
||||
temporary.write_text(json.dumps(record, sort_keys=True, separators=(',', ':'), ensure_ascii=False) + '\n', encoding='utf-8')
|
||||
temporary.replace(RECEIPT)
|
||||
RECEIPT.chmod(0o640)
|
||||
return record
|
||||
|
||||
|
||||
def existing_prs(token):
|
||||
_, prs = api(LOCAL_API, 'GET', '/repos/' + REPO + '/pulls?state=all&base=' + urllib.parse.quote(BASE_BRANCH, safe='') + '&limit=100', token)
|
||||
return [pr for pr in prs if pr.get('head', {}).get('ref') == REQUEST_BRANCH and pr.get('base', {}).get('ref') == BASE_BRANCH]
|
||||
|
||||
|
||||
def verify_existing_branch(token, branch_oid, manifest, evidence):
|
||||
_, commit = api(LOCAL_API, 'GET', '/repos/' + REPO + '/git/commits/' + branch_oid, token)
|
||||
if commit.get('parents', [{}])[0].get('sha') is None:
|
||||
raise RuntimeError('REQUEST_BRANCH_COMMIT_INVALID')
|
||||
for relative, expected in ((MANIFEST_REL, manifest), (EVIDENCE_REL, evidence)):
|
||||
_, content = api(LOCAL_API, 'GET', '/repos/' + REPO + '/contents/' + urllib.parse.quote(relative, safe='/') + '?ref=' + branch_oid, token)
|
||||
actual = base64.b64decode(''.join(content.get('content', '').split()))
|
||||
if actual != expected:
|
||||
raise RuntimeError('GOVERNANCE_REQUEST_BRANCH_CONFLICT')
|
||||
|
||||
|
||||
def push_one_commit(token, base_oid, manifest, evidence):
|
||||
work_root = TASK_ROOT / 'publisher-work'
|
||||
work_root.mkdir(parents=True, exist_ok=True)
|
||||
temporary = pathlib.Path(tempfile.mkdtemp(prefix='semantic-reconciliation-', dir=work_root))
|
||||
askpass = temporary / 'askpass.sh'
|
||||
clone = temporary / 'repo'
|
||||
credential_path = pathlib.Path(os.environ['CREDENTIALS_DIRECTORY']) / CREDENTIAL_NAME
|
||||
askpass.write_text(
|
||||
'#!/bin/sh\ncase "$1" in *Username*) printf "%s\\n" "$GITEA_MACHINE_ACTOR";; *) cat "$CREDENTIAL_FILE";; esac\n',
|
||||
encoding='utf-8',
|
||||
)
|
||||
askpass.chmod(0o700)
|
||||
env = os.environ.copy()
|
||||
env.update({
|
||||
'GIT_ASKPASS': str(askpass),
|
||||
'GIT_TERMINAL_PROMPT': '0',
|
||||
'GITEA_MACHINE_ACTOR': 'aios-autonomy-agent',
|
||||
'CREDENTIAL_FILE': str(credential_path),
|
||||
'GIT_CONFIG_COUNT': '1',
|
||||
'GIT_CONFIG_KEY_0': 'safe.directory',
|
||||
'GIT_CONFIG_VALUE_0': AUTHORITY_REPO,
|
||||
})
|
||||
try:
|
||||
run = subprocess.run(
|
||||
['git', 'clone', '--branch', BASE_BRANCH, '--single-branch', '--quiet', NETWORK_REMOTE, str(clone)],
|
||||
text=True, capture_output=True, env=env, check=False,
|
||||
)
|
||||
if run.returncode != 0:
|
||||
detail = ' '.join(run.stderr.strip().split())[:240]
|
||||
raise RuntimeError('PUBLISHER_CLONE_FAILED:' + detail)
|
||||
def git(*args, push_env=None):
|
||||
result = subprocess.run(['git', *args], cwd=clone, text=True, capture_output=True, env=push_env or env, check=False)
|
||||
if result.returncode != 0:
|
||||
raise RuntimeError('PUBLISHER_GIT_OPERATION_FAILED')
|
||||
return result.stdout.strip()
|
||||
git('checkout', '--detach', base_oid)
|
||||
git('switch', '--create', REQUEST_BRANCH)
|
||||
manifest_path = clone / MANIFEST_REL
|
||||
evidence_path = clone / EVIDENCE_REL
|
||||
manifest_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
evidence_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
manifest_path.write_bytes(manifest)
|
||||
evidence_path.write_bytes(evidence)
|
||||
git('add', '--', MANIFEST_REL, EVIDENCE_REL)
|
||||
staged = git('diff', '--cached', '--name-only').splitlines()
|
||||
if staged != sorted([MANIFEST_REL, EVIDENCE_REL]):
|
||||
raise RuntimeError('PUBLISHER_PATH_ALLOWLIST_FAILED')
|
||||
git('-c', 'user.name=aios-autonomy-agent', '-c', 'user.email=aios-autonomy-agent@aios.invalid', 'commit', '--quiet', '-m', '[governance] publish semantic reconciliation evidence')
|
||||
if git('rev-list', '--count', base_oid + '..HEAD') != '1':
|
||||
raise RuntimeError('PUBLISHER_COMMIT_COUNT_FAILED')
|
||||
git('push', '--porcelain', NETWORK_REMOTE, 'HEAD:refs/heads/' + REQUEST_BRANCH, push_env=env)
|
||||
head = git('rev-parse', 'HEAD')
|
||||
return head
|
||||
finally:
|
||||
shutil.rmtree(temporary, ignore_errors=True)
|
||||
|
||||
|
||||
def publish():
|
||||
token = load_token()
|
||||
identity = verify_identity(token)
|
||||
base_oid = verify_base(token)
|
||||
manifest, evidence = load_inputs()
|
||||
manifest_sha = digest_bytes(manifest)
|
||||
evidence_sha = digest_bytes(evidence)
|
||||
key = publication_key(base_oid, manifest_sha, evidence_sha)
|
||||
write_receipt('REQUESTED', governance_base_oid=base_oid, manifest_sha256=manifest_sha, evidence_sha256=evidence_sha, idempotency_key=key, machine_identity=identity)
|
||||
write_receipt('LOCAL_CANDIDATE_VERIFIED', governance_base_oid=base_oid, manifest_sha256=manifest_sha, evidence_sha256=evidence_sha, idempotency_key=key, machine_identity=identity)
|
||||
|
||||
branch_status, branch = branch_api(LOCAL_API, token, REQUEST_BRANCH, expected=(200, 404))
|
||||
if branch_status == 200:
|
||||
branch_oid = branch.get('commit', {}).get('id')
|
||||
verify_existing_branch(token, branch_oid, manifest, evidence)
|
||||
else:
|
||||
branch_oid = push_one_commit(token, base_oid, manifest, evidence)
|
||||
verify_existing_branch(token, branch_oid, manifest, evidence)
|
||||
write_receipt('BRANCH_PUSHED', governance_base_oid=base_oid, manifest_sha256=manifest_sha, evidence_sha256=evidence_sha, idempotency_key=key, request_head_oid=branch_oid, machine_identity=identity)
|
||||
|
||||
prs = existing_prs(token)
|
||||
if len(prs) > 1:
|
||||
raise RuntimeError('MULTIPLE_MATCHING_APPROVAL_PRS')
|
||||
if prs:
|
||||
pr = prs[0]
|
||||
if pr.get('head', {}).get('sha') != branch_oid:
|
||||
raise RuntimeError('APPROVAL_PR_HEAD_MISMATCH')
|
||||
pr_number = pr.get('number')
|
||||
else:
|
||||
_, pr = api(LOCAL_API, 'POST', '/repos/' + REPO + '/pulls', token, {
|
||||
'title': '[GOVERNANCE RECONCILIATION] Semantic protection snapshot schema migration',
|
||||
'head': REQUEST_BRANCH,
|
||||
'base': BASE_BRANCH,
|
||||
'body': 'Schema-only reconciliation for the existing PR54 Stage-1 and Stage-2 predicates. No new governance capability; Owner exact-head re-attestation required.',
|
||||
}, expected=(201,))
|
||||
pr_number = pr.get('number')
|
||||
write_receipt('PR_CREATED', governance_base_oid=base_oid, manifest_sha256=manifest_sha, evidence_sha256=evidence_sha, idempotency_key=key, request_head_oid=branch_oid, pr_number=pr_number, machine_identity=identity)
|
||||
|
||||
_, remote_branch = branch_api(NETWORK_API, token, REQUEST_BRANCH)
|
||||
if remote_branch.get('commit', {}).get('id') != branch_oid:
|
||||
raise RuntimeError('NETWORK_BRANCH_READBACK_MISMATCH')
|
||||
_, read_pr = api(LOCAL_API, 'GET', '/repos/' + REPO + '/pulls/' + str(pr_number), token)
|
||||
if read_pr.get('head', {}).get('sha') != branch_oid or read_pr.get('base', {}).get('ref') != BASE_BRANCH:
|
||||
raise RuntimeError('PR_READBACK_MISMATCH')
|
||||
result = write_receipt('COMPLETED', governance_base_oid=base_oid, manifest_sha256=manifest_sha, evidence_sha256=evidence_sha, idempotency_key=key, request_head_oid=branch_oid, pr_number=pr_number, pr_head_oid=read_pr.get('head', {}).get('sha'), machine_identity=identity)
|
||||
return result
|
||||
|
||||
|
||||
def self_test():
|
||||
token = load_token()
|
||||
identity = verify_identity(token)
|
||||
base_oid = verify_base(token)
|
||||
branch_status, _ = branch_api(LOCAL_API, token, REQUEST_BRANCH, expected=(200, 404))
|
||||
return {
|
||||
'status': 'PASS',
|
||||
'governance_base_oid': base_oid,
|
||||
'machine_identity': identity,
|
||||
'credential_ref': {'scheme': 'systemd-credential', 'name': CREDENTIAL_NAME},
|
||||
'branch_namespace': 'refs/heads/governance/request/*',
|
||||
'target_base': BASE_BRANCH,
|
||||
'request_branch_status': 'EXISTS' if branch_status == 200 else 'ABSENT',
|
||||
'operation_allowlist': ['READ_BASE', 'CREATE_ONE_REQUEST_BRANCH', 'WRITE_TWO_EVIDENCE_PATHS_ONE_COMMIT', 'CREATE_OR_READ_ONE_APPROVAL_PR', 'READBACK'],
|
||||
'denied': ['MAIN_WRITE', 'DIRECT_APPROVALS_WRITE', 'BRANCH_PROTECTION', 'MERGE', 'FORCE_PUSH', 'TAG', 'PRODUCTION', 'CREDENTIAL_MUTATION'],
|
||||
'governance_capability_executed': False,
|
||||
}
|
||||
|
||||
|
||||
def read_publication_status():
|
||||
token = load_token()
|
||||
receipt = json.loads(RECEIPT.read_text(encoding='utf-8'))
|
||||
pr_number = receipt.get('pr_number')
|
||||
if not pr_number:
|
||||
raise RuntimeError('PUBLICATION_RECEIPT_PR_MISSING')
|
||||
_, pr = api(LOCAL_API, 'GET', '/repos/' + REPO + '/pulls/' + str(pr_number), token)
|
||||
_, reviews = api(LOCAL_API, 'GET', '/repos/' + REPO + '/pulls/' + str(pr_number) + '/reviews', token)
|
||||
_, branch = branch_api(LOCAL_API, token, REQUEST_BRANCH)
|
||||
_, network_branch = branch_api(NETWORK_API, token, REQUEST_BRANCH)
|
||||
return {
|
||||
'status': 'PASS',
|
||||
'request_branch': REQUEST_BRANCH,
|
||||
'branch_head': branch.get('commit', {}).get('id'),
|
||||
'network_branch_head': network_branch.get('commit', {}).get('id'),
|
||||
'pr_number': pr.get('number'),
|
||||
'pr_state': pr.get('state'),
|
||||
'pr_merged': pr.get('merged'),
|
||||
'pr_head': pr.get('head', {}).get('sha'),
|
||||
'pr_base': pr.get('base', {}).get('ref'),
|
||||
'reviews': [
|
||||
{
|
||||
'id': item.get('id'),
|
||||
'reviewer_id': item.get('user', {}).get('id'),
|
||||
'state': item.get('state'),
|
||||
'commit_id': item.get('commit_id'),
|
||||
'dismissed': item.get('dismissed'),
|
||||
}
|
||||
for item in reviews
|
||||
],
|
||||
'governance_capability_executed': False,
|
||||
'protection_mutation': False,
|
||||
}
|
||||
|
||||
|
||||
def handle(request):
|
||||
if not isinstance(request, dict) or set(request) != {'operation'}:
|
||||
raise RuntimeError('REQUEST_SCHEMA_INVALID')
|
||||
if request['operation'] == 'DRY_RUN_SELF_TEST':
|
||||
return self_test()
|
||||
if request['operation'] == 'PUBLISH_SEMANTIC_RECONCILIATION':
|
||||
return publish()
|
||||
if request['operation'] == 'READ_SEMANTIC_RECONCILIATION_STATUS':
|
||||
return read_publication_status()
|
||||
raise RuntimeError('OPERATION_DENIED')
|
||||
|
||||
|
||||
def serve(listener):
|
||||
conn, _ = listener.accept()
|
||||
with conn:
|
||||
try:
|
||||
data = conn.recv(1024 * 1024 + 1)
|
||||
if len(data) > 1024 * 1024:
|
||||
raise RuntimeError('REQUEST_TOO_LARGE')
|
||||
result = handle(json.loads(data.decode()))
|
||||
except Exception as exc:
|
||||
result = {'status': 'FAIL', 'code': str(exc)}
|
||||
conn.sendall((json.dumps(result, sort_keys=True, separators=(',', ':'), ensure_ascii=False) + '\n').encode())
|
||||
|
||||
|
||||
def main():
|
||||
if os.environ.get('LISTEN_FDS') != '1':
|
||||
raise SystemExit('SOCKET_ACTIVATION_REQUIRED')
|
||||
listener = socket.fromfd(3, socket.AF_UNIX, socket.SOCK_STREAM)
|
||||
serve(listener)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -0,0 +1,73 @@
|
||||
"""Fail-closed Authority/Gitea ref reconciliation after protected merge."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
import subprocess
|
||||
|
||||
|
||||
class RefReconciliationError(RuntimeError):
|
||||
pass
|
||||
|
||||
|
||||
def _oid(value: str, field: str) -> str:
|
||||
if not isinstance(value, str) or len(value) != 40 or any(char not in "0123456789abcdef" for char in value):
|
||||
raise RefReconciliationError(field + "_INVALID")
|
||||
return value
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class RefReconciler:
|
||||
git_dir: Path
|
||||
|
||||
def read(self, ref: str) -> str:
|
||||
result = subprocess.run(
|
||||
["git", f"--git-dir={self.git_dir}", "rev-parse", "--verify", ref],
|
||||
text=True,
|
||||
capture_output=True,
|
||||
check=False,
|
||||
)
|
||||
if result.returncode != 0:
|
||||
raise RefReconciliationError("AUTHORITY_REF_UNAVAILABLE")
|
||||
return result.stdout.strip()
|
||||
|
||||
def is_ancestor(self, ancestor: str, descendant: str) -> bool:
|
||||
_oid(ancestor, "ancestor")
|
||||
_oid(descendant, "descendant")
|
||||
result = subprocess.run(
|
||||
["git", f"--git-dir={self.git_dir}", "merge-base", "--is-ancestor", ancestor, descendant],
|
||||
text=True,
|
||||
capture_output=True,
|
||||
check=False,
|
||||
)
|
||||
if result.returncode == 0:
|
||||
return True
|
||||
if result.returncode == 1:
|
||||
return False
|
||||
raise RefReconciliationError("AUTHORITY_ANCESTRY_CHECK_FAILED")
|
||||
|
||||
def reconcile_main(self, *, expected_oid: str, merged_oid: str, gitea_main_oid: str) -> str:
|
||||
_oid(expected_oid, "expected_oid")
|
||||
_oid(merged_oid, "merged_oid")
|
||||
_oid(gitea_main_oid, "gitea_main_oid")
|
||||
if gitea_main_oid != merged_oid:
|
||||
raise RefReconciliationError("GITEA_MAIN_EXPECTATION_MISMATCH")
|
||||
if not self.is_ancestor(expected_oid, merged_oid):
|
||||
raise RefReconciliationError("MAIN_FF_REQUIRED")
|
||||
current = self.read("refs/heads/main")
|
||||
if current == merged_oid:
|
||||
return merged_oid
|
||||
if current != expected_oid:
|
||||
raise RefReconciliationError("AUTHORITY_MAIN_EXPECTATION_MISMATCH")
|
||||
result = subprocess.run(
|
||||
["git", f"--git-dir={self.git_dir}", "update-ref", "refs/heads/main", merged_oid, expected_oid],
|
||||
text=True,
|
||||
capture_output=True,
|
||||
check=False,
|
||||
)
|
||||
if result.returncode != 0:
|
||||
raise RefReconciliationError("AUTHORITY_MAIN_CAS_FAILED")
|
||||
if self.read("refs/heads/main") != merged_oid:
|
||||
raise RefReconciliationError("AUTHORITY_MAIN_CAS_READBACK_MISMATCH")
|
||||
return merged_oid
|
||||
@@ -0,0 +1,81 @@
|
||||
"""Canonical semantic branch-protection model used by AIOS governance gates."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import json
|
||||
from typing import Any, Mapping
|
||||
|
||||
|
||||
SCHEMA = "aios.semantic-protection.v1"
|
||||
USER_IDS = {"owner": 2, "aios-autonomy-agent": 153}
|
||||
|
||||
|
||||
def canonical_json(value: Any) -> bytes:
|
||||
return json.dumps(value, sort_keys=True, separators=(",", ":"), ensure_ascii=False).encode("utf-8")
|
||||
|
||||
|
||||
def _typed_json(value: Any) -> list[Any]:
|
||||
if value in (None, "null"):
|
||||
return []
|
||||
if isinstance(value, str):
|
||||
return json.loads(value)
|
||||
return list(value)
|
||||
|
||||
|
||||
def _user_ids(names: Any) -> list[int]:
|
||||
result = []
|
||||
for name in names or []:
|
||||
if name not in USER_IDS:
|
||||
raise ValueError("UNKNOWN_GOVERNANCE_USER:" + str(name))
|
||||
result.append(USER_IDS[name])
|
||||
return sorted(result)
|
||||
|
||||
|
||||
def semantic_policy(protection: Mapping[str, Any]) -> dict[str, Any]:
|
||||
"""Drop transport metadata and canonicalize semantic security fields."""
|
||||
return {
|
||||
"schema_version": SCHEMA,
|
||||
"repo_id": 1,
|
||||
"protection_id": 2,
|
||||
"branch_name": protection.get("branch_name"),
|
||||
"can_push": protection.get("enable_push"),
|
||||
"can_force_push": protection.get("enable_force_push"),
|
||||
"enable_merge_whitelist": protection.get("enable_merge_whitelist"),
|
||||
"merge_whitelist_user_ids": _user_ids(protection.get("merge_whitelist_usernames")),
|
||||
"merge_whitelist_team_ids": [],
|
||||
"required_approvals": protection.get("required_approvals"),
|
||||
"enable_status_check": protection.get("enable_status_check"),
|
||||
"status_check_contexts": sorted(protection.get("status_check_contexts") or []),
|
||||
"block_admin_merge_override": protection.get("block_admin_merge_override"),
|
||||
"block_on_official_review_requests": protection.get("block_on_official_review_requests"),
|
||||
"block_on_outdated_branch": protection.get("block_on_outdated_branch"),
|
||||
"block_on_rejected_reviews": protection.get("block_on_rejected_reviews"),
|
||||
"dismiss_stale_approvals": protection.get("dismiss_stale_approvals"),
|
||||
"ignore_stale_approvals": protection.get("ignore_stale_approvals"),
|
||||
"enable_approvals_whitelist": protection.get("enable_approvals_whitelist"),
|
||||
"approvals_whitelist_user_ids": _user_ids(protection.get("approvals_whitelist_username")),
|
||||
"approvals_whitelist_team_ids": [],
|
||||
"enable_bypass_allowlist": protection.get("enable_bypass_allowlist"),
|
||||
"bypass_allowlist_user_ids": _user_ids(protection.get("bypass_allowlist_usernames")),
|
||||
"bypass_allowlist_team_ids": [],
|
||||
"enable_force_push_allowlist": protection.get("enable_force_push_allowlist"),
|
||||
"force_push_allowlist_user_ids": _user_ids(protection.get("force_push_allowlist_usernames")),
|
||||
"enable_whitelist": protection.get("enable_push_whitelist"),
|
||||
"whitelist_user_ids": _user_ids(protection.get("push_whitelist_usernames")),
|
||||
"whitelist_team_ids": [],
|
||||
"force_push_allowlist_deploy_keys": protection.get("force_push_allowlist_deploy_keys"),
|
||||
"whitelist_deploy_keys": protection.get("push_whitelist_deploy_keys"),
|
||||
"require_signed_commits": protection.get("require_signed_commits"),
|
||||
"priority": protection.get("priority"),
|
||||
"protected_file_patterns": _typed_json(protection.get("protected_file_patterns")),
|
||||
"unprotected_file_patterns": _typed_json(protection.get("unprotected_file_patterns")),
|
||||
}
|
||||
|
||||
|
||||
def semantic_digest(protection: Mapping[str, Any]) -> str:
|
||||
return hashlib.sha256(canonical_json(semantic_policy(protection))).hexdigest()
|
||||
|
||||
|
||||
def transport_digest(protection: Mapping[str, Any]) -> str:
|
||||
return hashlib.sha256(canonical_json(protection)).hexdigest()
|
||||
@@ -0,0 +1,141 @@
|
||||
"""Fail-closed Stage-2 campaign gate migration predicates."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import copy
|
||||
import hashlib
|
||||
import json
|
||||
import re
|
||||
from typing import Any, Mapping
|
||||
|
||||
SEMANTIC_SCHEMA = "aios.semantic-protection.v1"
|
||||
STAGE1_SEMANTIC_SHA = "d911e856f71de6105b8efa08e13338a448e0b6d37536332decd80c9f409ecf27"
|
||||
CAMPAIGN_ID = "AIOS-AUTONOMOUS-RELEASE-CAMPAIGN-20260807"
|
||||
OWNER_GATE = "owner-approval-gate"
|
||||
CAMPAIGN_GATE = "campaign-approval-gate"
|
||||
OID_RE = re.compile(r"^[0-9a-f]{40}$")
|
||||
|
||||
|
||||
class Stage2GateError(RuntimeError):
|
||||
pass
|
||||
|
||||
|
||||
def canonical_json(value: Any) -> bytes:
|
||||
return json.dumps(value, sort_keys=True, separators=(",", ":"), ensure_ascii=False).encode("utf-8")
|
||||
|
||||
|
||||
def _typed_json(value: Any) -> list[Any]:
|
||||
if value in (None, "", "null"):
|
||||
return []
|
||||
if isinstance(value, str):
|
||||
return json.loads(value)
|
||||
return list(value)
|
||||
|
||||
|
||||
def _user_ids(names: Any) -> list[int]:
|
||||
mapping = {"owner": 2, "aios-autonomy-agent": 153}
|
||||
result = []
|
||||
for name in names or []:
|
||||
if name not in mapping:
|
||||
raise Stage2GateError("UNKNOWN_GOVERNANCE_USER")
|
||||
result.append(mapping[name])
|
||||
return sorted(result)
|
||||
|
||||
|
||||
def semantic_policy(protection: Mapping[str, Any]) -> dict[str, Any]:
|
||||
return {
|
||||
"schema_version": SEMANTIC_SCHEMA,
|
||||
"repo_id": 1,
|
||||
"protection_id": 2,
|
||||
"branch_name": protection.get("branch_name"),
|
||||
"can_push": protection.get("enable_push"),
|
||||
"can_force_push": protection.get("enable_force_push"),
|
||||
"enable_merge_whitelist": protection.get("enable_merge_whitelist"),
|
||||
"merge_whitelist_user_ids": _user_ids(protection.get("merge_whitelist_usernames")),
|
||||
"merge_whitelist_team_ids": [],
|
||||
"required_approvals": protection.get("required_approvals"),
|
||||
"enable_status_check": protection.get("enable_status_check"),
|
||||
"status_check_contexts": sorted(protection.get("status_check_contexts") or []),
|
||||
"block_admin_merge_override": protection.get("block_admin_merge_override"),
|
||||
"block_on_official_review_requests": protection.get("block_on_official_review_requests"),
|
||||
"block_on_outdated_branch": protection.get("block_on_outdated_branch"),
|
||||
"block_on_rejected_reviews": protection.get("block_on_rejected_reviews"),
|
||||
"dismiss_stale_approvals": protection.get("dismiss_stale_approvals"),
|
||||
"ignore_stale_approvals": protection.get("ignore_stale_approvals"),
|
||||
"enable_approvals_whitelist": protection.get("enable_approvals_whitelist"),
|
||||
"approvals_whitelist_user_ids": _user_ids(protection.get("approvals_whitelist_username")),
|
||||
"approvals_whitelist_team_ids": [],
|
||||
"enable_bypass_allowlist": protection.get("enable_bypass_allowlist"),
|
||||
"bypass_allowlist_user_ids": _user_ids(protection.get("bypass_allowlist_usernames")),
|
||||
"bypass_allowlist_team_ids": [],
|
||||
"enable_force_push_allowlist": protection.get("enable_force_push_allowlist"),
|
||||
"force_push_allowlist_user_ids": _user_ids(protection.get("force_push_allowlist_usernames")),
|
||||
"force_push_allowlist_team_ids": [],
|
||||
"enable_whitelist": protection.get("enable_push_whitelist"),
|
||||
"whitelist_user_ids": _user_ids(protection.get("push_whitelist_usernames")),
|
||||
"whitelist_team_ids": [],
|
||||
"force_push_allowlist_deploy_keys": protection.get("force_push_allowlist_deploy_keys"),
|
||||
"whitelist_deploy_keys": protection.get("push_whitelist_deploy_keys"),
|
||||
"require_signed_commits": protection.get("require_signed_commits"),
|
||||
"priority": protection.get("priority"),
|
||||
"protected_file_patterns": protection.get("protected_file_patterns"),
|
||||
"unprotected_file_patterns": protection.get("unprotected_file_patterns"),
|
||||
}
|
||||
|
||||
|
||||
def semantic_digest(protection: Mapping[str, Any]) -> str:
|
||||
return hashlib.sha256(canonical_json(semantic_policy(protection))).hexdigest()
|
||||
|
||||
|
||||
def build_target(protection: Mapping[str, Any]) -> tuple[str, dict[str, Any]]:
|
||||
current = list(protection.get("status_check_contexts") or [])
|
||||
has_owner = OWNER_GATE in current
|
||||
has_campaign = CAMPAIGN_GATE in current
|
||||
if has_owner and has_campaign:
|
||||
raise Stage2GateError("GATE_PRESTATE_CONFLICT")
|
||||
if not has_owner and not has_campaign:
|
||||
raise Stage2GateError("OWNER_GATE_MISSING")
|
||||
if has_campaign:
|
||||
return "IDEMPOTENT_SUCCESS", copy.deepcopy(dict(protection))
|
||||
if semantic_digest(protection) != STAGE1_SEMANTIC_SHA:
|
||||
raise Stage2GateError("SEMANTIC_PRECONDITION_INVALID")
|
||||
target = copy.deepcopy(dict(protection))
|
||||
target["status_check_contexts"] = sorted(
|
||||
[gate for gate in current if gate != OWNER_GATE] + [CAMPAIGN_GATE]
|
||||
)
|
||||
return "READY", target
|
||||
|
||||
|
||||
def validate_stage2_facts(facts: Mapping[str, Any]) -> None:
|
||||
required = {
|
||||
"campaign_id",
|
||||
"campaign_active",
|
||||
"campaign_revoked",
|
||||
"machine_actor",
|
||||
"required_checks_all_success",
|
||||
"required_checks_same_head",
|
||||
"unresolved_conversations",
|
||||
"mergeable",
|
||||
"protected_target",
|
||||
"current_main_oid",
|
||||
"gitea_main_oid",
|
||||
"current_main_lineage_verified",
|
||||
}
|
||||
if set(facts) != required:
|
||||
raise Stage2GateError("STAGE2_FACTS_SCHEMA_INVALID")
|
||||
if facts["campaign_id"] != CAMPAIGN_ID or facts["campaign_active"] is not True or facts["campaign_revoked"] is not False:
|
||||
raise Stage2GateError("CAMPAIGN_NOT_ACTIVE")
|
||||
if facts["machine_actor"] != "aios-autonomy-agent":
|
||||
raise Stage2GateError("MACHINE_ACTOR_MISMATCH")
|
||||
if facts["required_checks_all_success"] is not True or facts["required_checks_same_head"] is not True:
|
||||
raise Stage2GateError("REQUIRED_CHECKS_NOT_CURRENT")
|
||||
if facts["unresolved_conversations"] != 0 or facts["mergeable"] is not True:
|
||||
raise Stage2GateError("CAMPAIGN_GATE_FACTS_INVALID")
|
||||
if facts["protected_target"] != "main" or facts["current_main_lineage_verified"] is not True:
|
||||
raise Stage2GateError("PROTECTED_TARGET_OR_LINEAGE_INVALID")
|
||||
if (
|
||||
not isinstance(facts["current_main_oid"], str)
|
||||
or not OID_RE.fullmatch(facts["current_main_oid"])
|
||||
or facts["gitea_main_oid"] != facts["current_main_oid"]
|
||||
):
|
||||
raise Stage2GateError("MAIN_READBACK_MISMATCH")
|
||||
@@ -0,0 +1,568 @@
|
||||
"""Governed V2R2C1 production release orchestration.
|
||||
|
||||
This module is deliberately scoped to the Owner-approved V2R2C1 binding. It
|
||||
uses the existing TagPublisher and metadata adapters; it does not introduce a
|
||||
second Git writer, a second durable job store, or an alternate baseline.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import fcntl
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import subprocess
|
||||
import tarfile
|
||||
import tempfile
|
||||
import time
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
from typing import Any, Mapping
|
||||
|
||||
from gitea_tag_publication import (
|
||||
GiteaTagClient,
|
||||
GitTagBackend,
|
||||
PublicationManualOwnerReviewRequired,
|
||||
PublicationPending,
|
||||
PublicationReconciliationRequired,
|
||||
PublicationRequest,
|
||||
TagPublisher,
|
||||
)
|
||||
|
||||
|
||||
VERSION = "V2R2C1"
|
||||
TARGET_OID = "998d30304787e31e5ae607c0bd86b5edb2b0e5ad"
|
||||
EVIDENCE_CLOSURE_OID = "a8b906d1bc24088c19647e98de6a679268f6d0a2"
|
||||
APPROVAL_PLAN_ID = "sha256:c0df52008ae7f64cec5ee2d28947aa7b892066c670ab8295e4549140b1a7500e"
|
||||
REPOSITORY = "aios/aios"
|
||||
SERVICE = "aios-v2r1c7-production.service"
|
||||
PORT = 18927
|
||||
OID_RE = re.compile(r"^[0-9a-f]{40}$")
|
||||
SHA_RE = re.compile(r"^[0-9a-f]{64}$")
|
||||
|
||||
|
||||
class ReleaseBlocked(RuntimeError):
|
||||
"""A release gate failed and no unsafe recovery is permitted."""
|
||||
|
||||
|
||||
class ManualOwnerReviewRequired(ReleaseBlocked):
|
||||
pass
|
||||
|
||||
|
||||
class ReconciliationRequired(ReleaseBlocked):
|
||||
pass
|
||||
|
||||
|
||||
def canonical(value: object) -> bytes:
|
||||
return (json.dumps(value, sort_keys=True, separators=(",", ":")) + "\n").encode()
|
||||
|
||||
|
||||
def sha256_file(path: Path) -> str:
|
||||
if path.is_symlink() or not path.is_file():
|
||||
raise ReleaseBlocked("artifact must be a regular file")
|
||||
digest = hashlib.sha256()
|
||||
with path.open("rb") as stream:
|
||||
for block in iter(lambda: stream.read(1024 * 1024), b""):
|
||||
digest.update(block)
|
||||
return digest.hexdigest()
|
||||
|
||||
|
||||
def atomic_json(path: Path, value: Mapping[str, Any], mode: int = 0o600) -> None:
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
temporary = path.with_name(path.name + ".tmp")
|
||||
temporary.write_bytes(canonical(dict(value)))
|
||||
os.chmod(temporary, mode)
|
||||
os.replace(temporary, path)
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class ReleaseBinding:
|
||||
version: str = VERSION
|
||||
target_oid: str = TARGET_OID
|
||||
evidence_closure_oid: str = EVIDENCE_CLOSURE_OID
|
||||
approval_plan_id: str = APPROVAL_PLAN_ID
|
||||
repository: str = REPOSITORY
|
||||
production_service: str = SERVICE
|
||||
production_port: int = PORT
|
||||
|
||||
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.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:
|
||||
raise ManualOwnerReviewRequired("V2R2C1 evidence closure mismatch")
|
||||
if self.approval_plan_id != APPROVAL_PLAN_ID:
|
||||
raise ManualOwnerReviewRequired("V2R2C1 approval plan mismatch")
|
||||
if self.production_service != SERVICE or int(self.production_port) != PORT:
|
||||
raise ManualOwnerReviewRequired("production service binding mismatch")
|
||||
|
||||
def as_dict(self) -> dict[str, Any]:
|
||||
return {
|
||||
"version": self.version,
|
||||
"target_oid": self.target_oid,
|
||||
"evidence_closure_oid": self.evidence_closure_oid,
|
||||
"approval_plan_id": self.approval_plan_id,
|
||||
"repository": self.repository,
|
||||
"production_service": self.production_service,
|
||||
"production_port": self.production_port,
|
||||
}
|
||||
|
||||
|
||||
class StateLock:
|
||||
def __init__(self, path: Path):
|
||||
self.path = path
|
||||
self.handle = None
|
||||
|
||||
def __enter__(self) -> "StateLock":
|
||||
self.path.parent.mkdir(parents=True, exist_ok=True)
|
||||
self.handle = self.path.open("a+", encoding="utf-8")
|
||||
fcntl.flock(self.handle.fileno(), fcntl.LOCK_EX)
|
||||
return self
|
||||
|
||||
def __exit__(self, exc_type, exc, tb) -> None:
|
||||
if self.handle is not None:
|
||||
fcntl.flock(self.handle.fileno(), fcntl.LOCK_UN)
|
||||
self.handle.close()
|
||||
|
||||
|
||||
class ReleaseState:
|
||||
def __init__(self, root: Path, binding: ReleaseBinding):
|
||||
self.root = root
|
||||
self.path = root / "release-state.json"
|
||||
self.binding = binding
|
||||
root.mkdir(parents=True, exist_ok=True)
|
||||
if self.path.exists():
|
||||
value = json.loads(self.path.read_text(encoding="utf-8"))
|
||||
if value.get("binding") != binding.as_dict():
|
||||
raise ManualOwnerReviewRequired("persisted release binding conflict")
|
||||
self.value = value
|
||||
else:
|
||||
self.value = {
|
||||
"schema": "aios.v2r2c1.governed-production-release-state.v1",
|
||||
"binding": binding.as_dict(),
|
||||
"state": "INITIAL",
|
||||
"completed": {},
|
||||
"history": [],
|
||||
}
|
||||
self.save()
|
||||
|
||||
def save(self) -> None:
|
||||
atomic_json(self.path, self.value)
|
||||
|
||||
def record(self, state: str, step: str, result: Mapping[str, Any]) -> None:
|
||||
safe = json.loads(json.dumps(dict(result)))
|
||||
self.value["state"] = state
|
||||
self.value.setdefault("completed", {})[step] = safe
|
||||
self.value.setdefault("history", []).append(
|
||||
{"at": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()), "state": state, "step": step, "result": safe}
|
||||
)
|
||||
self.save()
|
||||
|
||||
def fail(self, state: str, reason: str) -> None:
|
||||
self.value["state"] = state
|
||||
self.value.setdefault("history", []).append(
|
||||
{"at": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()), "state": state, "reason": str(reason)[:500]}
|
||||
)
|
||||
self.save()
|
||||
|
||||
|
||||
def _git(git_dir: Path, *args: str, check: bool = True) -> str:
|
||||
completed = subprocess.run(
|
||||
["git", f"--git-dir={git_dir}", *args],
|
||||
check=False,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=45,
|
||||
env={"PATH": os.environ.get("PATH", "")},
|
||||
)
|
||||
if check and completed.returncode != 0:
|
||||
raise ReconciliationRequired(completed.stderr.strip() or "authoritative git query failed")
|
||||
return completed.stdout.strip()
|
||||
|
||||
|
||||
def _ancestor(git_dir: Path, ancestor: str, descendant: str) -> bool:
|
||||
return subprocess.run(
|
||||
["git", f"--git-dir={git_dir}", "merge-base", "--is-ancestor", ancestor, descendant],
|
||||
check=False,
|
||||
capture_output=True,
|
||||
timeout=45,
|
||||
env={"PATH": os.environ.get("PATH", "")},
|
||||
).returncode == 0
|
||||
|
||||
|
||||
def verify_lineage(git_dir: Path, binding: ReleaseBinding) -> dict[str, Any]:
|
||||
binding.validate()
|
||||
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")
|
||||
return {
|
||||
"target_oid": binding.target_oid,
|
||||
"evidence_closure_oid": binding.evidence_closure_oid,
|
||||
"target_is_ancestor_of_evidence": True,
|
||||
"main_oid": _git(git_dir, "rev-parse", "refs/heads/main"),
|
||||
}
|
||||
|
||||
|
||||
def verify_owner_approval(value: Mapping[str, Any], binding: ReleaseBinding) -> dict[str, Any]:
|
||||
approval = value.get("owner_approval")
|
||||
if not isinstance(approval, Mapping):
|
||||
raise ReleaseBlocked("OWNER_APPROVAL_RECEIPT_REQUIRED")
|
||||
if approval.get("approved") is not True or approval.get("status") != "APPROVED":
|
||||
raise ReleaseBlocked("OWNER_APPROVAL_NOT_ACCEPTED")
|
||||
if approval.get("approved_by") != "aiman":
|
||||
raise ManualOwnerReviewRequired("OWNER_APPROVAL_ACTOR_MISMATCH")
|
||||
if approval.get("approval_source") != "explicit_owner_approval_in_chat":
|
||||
raise ManualOwnerReviewRequired("OWNER_APPROVAL_SOURCE_MISMATCH")
|
||||
for key, expected in binding.as_dict().items():
|
||||
if key in {"repository", "production_service", "production_port"}:
|
||||
continue
|
||||
if approval.get(key) != expected:
|
||||
raise ManualOwnerReviewRequired("OWNER_APPROVAL_BINDING_MISMATCH:" + key)
|
||||
return dict(approval)
|
||||
|
||||
|
||||
def _payload_file(git_dir: Path, oid: str, path: str) -> bytes:
|
||||
completed = subprocess.run(
|
||||
["git", f"--git-dir={git_dir}", "show", f"{oid}:{path}"],
|
||||
check=False,
|
||||
capture_output=True,
|
||||
timeout=45,
|
||||
env={"PATH": os.environ.get("PATH", "")},
|
||||
)
|
||||
if completed.returncode != 0:
|
||||
raise ReleaseBlocked("required release source missing:" + path)
|
||||
return completed.stdout
|
||||
|
||||
|
||||
def _commit_epoch(git_dir: Path, oid: str) -> int:
|
||||
return int(_git(git_dir, "show", "-s", "--format=%ct", oid))
|
||||
|
||||
|
||||
def build_production_artifact(
|
||||
git_dir: Path,
|
||||
binding: ReleaseBinding,
|
||||
output_root: Path,
|
||||
) -> dict[str, Any]:
|
||||
"""Build a deterministic executable V2R2C1 health artifact from target_oid."""
|
||||
binding.validate()
|
||||
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]}"
|
||||
output = output_root / (stem + ".tar.zst")
|
||||
with tempfile.TemporaryDirectory(prefix="v2r2c1-artifact-") as temp:
|
||||
root = Path(temp) / "payload"
|
||||
root.mkdir(parents=True)
|
||||
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",
|
||||
}
|
||||
for destination, source in source_paths.items():
|
||||
target = root / destination
|
||||
target.parent.mkdir(parents=True, exist_ok=True)
|
||||
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": (
|
||||
"from v2r1c6_privacy_tools import main\n\n"
|
||||
"if __name__ == '__main__':\n raise SystemExit(main())\n"
|
||||
),
|
||||
"deploy/aios-v2/systemd/aios-v2r1c7-production.service": (
|
||||
"[Unit]\nDescription=AIOS V2R2C1 governed health contract service\n"
|
||||
"After=network.target\n\n[Service]\nType=simple\n"
|
||||
"User=aios-v2r1c7\nGroup=aios-v2r1c7\n"
|
||||
"WorkingDirectory=/opt/aios/v2r2c1\n"
|
||||
"EnvironmentFile=/etc/aios/aios-v2r2c1.env\n"
|
||||
"ExecStart=/usr/bin/python3 /opt/aios/v2r2c1/active-runtime/app/v2r2c1_health_service.py --serve --host 127.0.0.1 --port ${AIOS_V2R1C7_PORT}\n"
|
||||
"Restart=on-failure\nRestartSec=5\nNoNewPrivileges=true\nPrivateTmp=true\n"
|
||||
"ProtectHome=true\nProtectSystem=strict\nProtectKernelLogs=true\n"
|
||||
"ProtectKernelModules=true\nProtectKernelTunables=true\nProtectControlGroups=true\n"
|
||||
"RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6\nIPAddressDeny=any\n"
|
||||
"IPAddressAllow=localhost\nUMask=0077\n\n[Install]\nWantedBy=multi-user.target\n"
|
||||
),
|
||||
"deploy/aios-v2/v2r2c1-runtime-config.json": json.dumps({
|
||||
"schema": "aios.v2r2c1.production-runtime.v1",
|
||||
"version": VERSION,
|
||||
"target_oid": binding.target_oid,
|
||||
"service": SERVICE,
|
||||
"port": PORT,
|
||||
"health_path": "/health",
|
||||
"deployment_state_source": "environment_and_state_file",
|
||||
}, sort_keys=True, separators=(",", ":")) + "\n",
|
||||
"deploy/aios-v2/v2r2c1-rollback.json": json.dumps({
|
||||
"schema": "aios.v2r2c1.rollback-metadata.v1",
|
||||
"rollback_version": "V2R1C7",
|
||||
"rollback_service": SERVICE,
|
||||
"rollback_target_is_previous_production": True,
|
||||
"automatic_rollback": False,
|
||||
}, sort_keys=True, separators=(",", ":")) + "\n",
|
||||
"scripts/probe_v2r2c1_health.py": (
|
||||
"import json,sys,urllib.request\n"
|
||||
"url=sys.argv[1] if len(sys.argv)>1 else 'http://127.0.0.1:18927/health'\n"
|
||||
"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
|
||||
target.parent.mkdir(parents=True, exist_ok=True)
|
||||
target.write_text(text, encoding="utf-8")
|
||||
if rel.startswith("scripts/"):
|
||||
target.chmod(0o755)
|
||||
metadata = {
|
||||
"schema": "aios.release-artifact-build.v3",
|
||||
"version": VERSION,
|
||||
"repository": REPOSITORY,
|
||||
"source_head_oid": binding.target_oid,
|
||||
"evidence_closure_oid": binding.evidence_closure_oid,
|
||||
"source_date_epoch": epoch,
|
||||
"clean_source": True,
|
||||
"executable_payload": True,
|
||||
"production_deployed": False,
|
||||
"tag_created": False,
|
||||
"baseline_changed": False,
|
||||
"secrets_included": False,
|
||||
}
|
||||
(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"))))
|
||||
names = sorted("payload/" + str(path.relative_to(root)).replace(os.sep, "/") for path in root.rglob("*") if path.is_file())
|
||||
(root / "ARTIFACT-MANIFEST.json").write_bytes(canonical({
|
||||
"schema": "aios.release-artifact-manifest.v3",
|
||||
"version": VERSION,
|
||||
"source_head_oid": binding.target_oid,
|
||||
"evidence_closure_oid": binding.evidence_closure_oid,
|
||||
"repository": REPOSITORY,
|
||||
"files": names + ["payload/ARTIFACT-MANIFEST.json"],
|
||||
"deployable": True,
|
||||
"executable_payload": True,
|
||||
"production_deployed": False,
|
||||
"tag_created": False,
|
||||
"baseline_changed": False,
|
||||
}))
|
||||
tar_path = Path(temp) / "release.tar"
|
||||
with tarfile.open(tar_path, "w") as archive:
|
||||
for path in sorted(root.rglob("*")):
|
||||
relative = str(path.relative_to(root)).replace(os.sep, "/")
|
||||
info = archive.gettarinfo(str(path), arcname="payload/" + relative)
|
||||
info.uid = info.gid = 0
|
||||
info.uname = "root"
|
||||
info.gname = "root"
|
||||
info.mtime = epoch
|
||||
info.mode = 0o755 if path.is_dir() or path.suffix == ".sh" else 0o644
|
||||
if path.is_file():
|
||||
with path.open("rb") as stream:
|
||||
archive.addfile(info, stream)
|
||||
else:
|
||||
archive.addfile(info)
|
||||
subprocess.run(["zstd", "-T1", "-19", "--no-progress", "-q", "-f", str(tar_path), "-o", str(output)], check=True, timeout=120)
|
||||
digest = sha256_file(output)
|
||||
receipt = {
|
||||
"schema": "aios.release-artifact-receipt.v3",
|
||||
"version": VERSION,
|
||||
"repository": REPOSITORY,
|
||||
"source_head_oid": binding.target_oid,
|
||||
"evidence_closure_oid": binding.evidence_closure_oid,
|
||||
"artifact": str(output),
|
||||
"artifact_sha256": digest,
|
||||
"artifact_size": output.stat().st_size,
|
||||
"deployable": True,
|
||||
"executable_payload": True,
|
||||
"production_deployed": False,
|
||||
"tag_created": False,
|
||||
"baseline_changed": False,
|
||||
}
|
||||
atomic_json(output.with_suffix(output.suffix + ".receipt.json"), receipt, mode=0o640)
|
||||
return receipt
|
||||
|
||||
|
||||
class GovernedProductionReleaseExecutor:
|
||||
def __init__(self, config: Any):
|
||||
self.config = config
|
||||
self.binding = ReleaseBinding()
|
||||
self.binding.validate()
|
||||
self.root = Path(config.state_root) / "v2r2c1-governed-production-release"
|
||||
self.state = ReleaseState(self.root, self.binding)
|
||||
workspace = self.root / "publication-workspace"
|
||||
workspace.mkdir(parents=True, exist_ok=True)
|
||||
self.publisher = TagPublisher(
|
||||
workspace,
|
||||
GitTagBackend(workspace, authoritative_repo=Path(config.authoritative_git_dir)),
|
||||
GiteaTagClient(config.api_base, REPOSITORY, Path(config.token_path)),
|
||||
config.remote_name,
|
||||
state_root=self.root / "tag-publication",
|
||||
)
|
||||
|
||||
def _request(self, request: Mapping[str, Any]) -> PublicationRequest:
|
||||
binding = self.binding
|
||||
for key, expected in binding.as_dict().items():
|
||||
if key in request and request[key] != expected:
|
||||
raise ManualOwnerReviewRequired("release request binding mismatch:" + key)
|
||||
return PublicationRequest(binding.version, binding.target_oid, binding.evidence_closure_oid, binding.approval_plan_id)
|
||||
|
||||
def approval_request(self, request: Mapping[str, Any]) -> dict[str, Any]:
|
||||
if request.get("execute") is not False:
|
||||
raise ReleaseBlocked("TAG_APPROVAL_REQUEST_MUST_BE_DRY_RUN")
|
||||
owner = verify_owner_approval(request, self.binding)
|
||||
with StateLock(self.root / "release.lock"):
|
||||
current = self.state.value.get("approval")
|
||||
approval_digest = hashlib.sha256(canonical(owner)).hexdigest()
|
||||
record = {
|
||||
"schema": "aios.v2r2c1.tag-approval-request.v1",
|
||||
"binding": self.binding.as_dict(),
|
||||
"owner_approval": owner,
|
||||
"owner_approval_digest_sha256": approval_digest,
|
||||
"execute": False,
|
||||
"recorded_at": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()),
|
||||
}
|
||||
if current and current != record:
|
||||
raise ManualOwnerReviewRequired("persisted Owner approval differs")
|
||||
self.state.value["approval"] = record
|
||||
self.state.record("APPROVAL_REQUEST_RECORDED", "tag_approval_request", record)
|
||||
return {"status": "RECORDED", "execute": False, "binding": self.binding.as_dict(), "owner_approval_digest_sha256": approval_digest}
|
||||
|
||||
def _require_approval(self) -> None:
|
||||
approval = self.state.value.get("approval")
|
||||
if not isinstance(approval, Mapping) or approval.get("binding") != self.binding.as_dict():
|
||||
raise ReleaseBlocked("OWNER_APPROVAL_REQUEST_NOT_RECORDED")
|
||||
|
||||
def publish_tag(self, request: Mapping[str, Any]) -> dict[str, Any]:
|
||||
if request.get("execute") is not True:
|
||||
raise ReleaseBlocked("TAG_PUBLICATION_REQUIRES_EXECUTE_TRUE")
|
||||
self._require_approval()
|
||||
publication = self._request(request)
|
||||
with StateLock(self.root / "release.lock"):
|
||||
try:
|
||||
verified = self.publisher._verify_publication(publication, allow_push=True, previously_deployed=False)
|
||||
except PublicationManualOwnerReviewRequired as exc:
|
||||
self.state.fail("MANUAL_OWNER_REVIEW_REQUIRED", str(exc))
|
||||
raise ManualOwnerReviewRequired(str(exc)) from exc
|
||||
except PublicationPending as exc:
|
||||
self.state.fail("REMOTE_PUSH_PENDING", str(exc))
|
||||
raise ReconciliationRequired(str(exc)) from exc
|
||||
except PublicationReconciliationRequired as exc:
|
||||
self.state.fail("RECONCILIATION_REQUIRED", str(exc))
|
||||
raise ReconciliationRequired(str(exc)) from exc
|
||||
result = dict(verified)
|
||||
self.state.record("TAG_VERIFIED", "tag", result)
|
||||
return {"status": "REMOTE_VERIFIED", **result}
|
||||
|
||||
def tag_status(self, request: Mapping[str, Any]) -> dict[str, Any]:
|
||||
self._request(request)
|
||||
return self.publisher.get_tag_publication_status(VERSION)
|
||||
|
||||
def build_artifact(self, request: Mapping[str, Any]) -> dict[str, Any]:
|
||||
self._require_approval()
|
||||
self._request(request)
|
||||
receipt = build_production_artifact(Path(self.config.authoritative_git_dir), self.binding, self.root / "artifacts")
|
||||
self.state.record("ARTIFACT_VERIFIED", "production_artifact", receipt)
|
||||
return {"status": "VERIFIED", **receipt}
|
||||
|
||||
def execute_request(self, request: Mapping[str, Any]) -> dict[str, Any]:
|
||||
self._require_approval()
|
||||
publication = self._request(request)
|
||||
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")
|
||||
artifact = self.state.value.get("completed", {}).get("production_artifact")
|
||||
if not isinstance(artifact, Mapping):
|
||||
artifact = self.build_artifact(request)
|
||||
value = {
|
||||
**self.binding.as_dict(),
|
||||
"tag_oid": tag_state.get("tag_oid") or tag_state.get("gitea_tag_oid"),
|
||||
"artifact_path": artifact["artifact"],
|
||||
"artifact_size": int(artifact["artifact_size"]),
|
||||
"artifact_sha256": artifact["artifact_sha256"],
|
||||
}
|
||||
# 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
|
||||
# fail-closed when the adapter is not available.
|
||||
import v2r2c1_production_adapter
|
||||
runtime = v2r2c1_production_adapter.execute_request({"schema": "aios.production-executor-request.v1", "operation": "apply", **value})
|
||||
if runtime.get("status") != "ACTIVE_VERIFIED":
|
||||
self.state.fail("RECONCILIATION_REQUIRED", "production health did not verify")
|
||||
raise ReconciliationRequired("production health did not verify")
|
||||
self.state.record("PRODUCTION_VERIFIED", "production", runtime)
|
||||
return {"status": "PRODUCTION_VERIFIED", "runtime": runtime, "artifact": artifact, "tag_oid": value["tag_oid"]}
|
||||
|
||||
def consistency(self, request: Mapping[str, Any]) -> dict[str, Any]:
|
||||
publication = self._request(request)
|
||||
result = self.publisher.get_release_consistency(publication)
|
||||
result["release_binding"] = self.binding.as_dict()
|
||||
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))
|
||||
return result
|
||||
|
||||
def acceptance(self, request: Mapping[str, Any]) -> dict[str, Any]:
|
||||
result = self.consistency(request)
|
||||
result["accepted"] = result.get("status") == "PASS" and result.get("production_health", {}).get("status") == "ACTIVE_VERIFIED"
|
||||
self.state.record("ACCEPTED" if result["accepted"] else "RECONCILIATION_REQUIRED", "acceptance", result)
|
||||
return result
|
||||
|
||||
|
||||
def _read_json(path: Path) -> dict[str, Any]:
|
||||
try:
|
||||
value = json.loads(path.read_text(encoding="utf-8"))
|
||||
except FileNotFoundError:
|
||||
return {"status": "MISSING", "path": str(path)}
|
||||
if not isinstance(value, dict):
|
||||
return {"status": "INVALID", "path": str(path)}
|
||||
return {"status": "PRESENT", "path": str(path), **value}
|
||||
|
||||
|
||||
def _health(url: str, binding: ReleaseBinding, artifact: Mapping[str, Any]) -> dict[str, Any]:
|
||||
import urllib.request
|
||||
try:
|
||||
with urllib.request.urlopen(url, timeout=5) as response:
|
||||
body = json.loads(response.read().decode("utf-8"))
|
||||
status = response.status
|
||||
except Exception as exc:
|
||||
return {"status": "UNAVAILABLE", "error": type(exc).__name__}
|
||||
expected_artifact = artifact.get("artifact_sha256")
|
||||
exact = (
|
||||
status == 200
|
||||
and body.get("version") == binding.version
|
||||
and body.get("commit_oid") == binding.target_oid
|
||||
and (not expected_artifact or body.get("artifact_sha256") == expected_artifact)
|
||||
and body.get("deployment_state") == "ACTIVE"
|
||||
and body.get("production_deployed") is True
|
||||
)
|
||||
return {"status": "ACTIVE_VERIFIED" if exact else "CONFLICT", "http_status": status, **body}
|
||||
|
||||
|
||||
def build_executor_config(config: Any) -> GovernedProductionReleaseExecutor:
|
||||
return GovernedProductionReleaseExecutor(config)
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("operation", choices=("build-artifact", "status"))
|
||||
parser.add_argument("--git-dir", type=Path, default=Path("/data/aios-gitops/repo.git"))
|
||||
parser.add_argument("--output-root", type=Path, default=Path("/var/lib/aios-gitops/approved-artifacts/V2R2C1"))
|
||||
args = parser.parse_args()
|
||||
binding = ReleaseBinding()
|
||||
if args.operation == "build-artifact":
|
||||
print(json.dumps(build_production_artifact(args.git_dir, binding, args.output_root), sort_keys=True))
|
||||
else:
|
||||
print(json.dumps({"binding": binding.as_dict(), "status": "READY"}, sort_keys=True))
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
Executable
+293
@@ -0,0 +1,293 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Thin production tag adapter over AIOS's existing mature TagPublisher.
|
||||
|
||||
This adapter deliberately does not call TagPublisher.publish(), because that
|
||||
legacy method also mutates Registry/Baseline. The production executor requires:
|
||||
Tag -> runtime ACTIVE -> Registry -> Baseline.
|
||||
|
||||
`apply` reuses TagPublisher._verify_publication() under the publisher's durable
|
||||
per-version lock. That method performs the existing fail-closed annotated-tag,
|
||||
network remote, Gitea API, peeled-target and listing verification, but stops
|
||||
before Registry/Baseline mutation.
|
||||
|
||||
`probe` is read-only.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from typing import Any, Mapping
|
||||
|
||||
from gitea_tag_publication import (
|
||||
GiteaTagClient,
|
||||
GitTagBackend,
|
||||
PublicationConflict,
|
||||
PublicationManualOwnerReviewRequired,
|
||||
PublicationPending,
|
||||
PublicationReconciliationRequired,
|
||||
PublicationRequest,
|
||||
PublicationState,
|
||||
TagPublisher,
|
||||
)
|
||||
|
||||
OID_RE = re.compile(r"^[0-9a-f]{40}$")
|
||||
PLAN_RE = re.compile(r"^sha256:[0-9a-f]{64}$")
|
||||
|
||||
|
||||
def load_request() -> dict[str, Any]:
|
||||
value = json.load(sys.stdin)
|
||||
if not isinstance(value, dict):
|
||||
raise ValueError("request_object_required")
|
||||
if value.get("schema") != "aios.production-executor-request.v1":
|
||||
raise ValueError("request_schema_invalid")
|
||||
if not re.fullmatch(r"V\d+R\d+C\d+(?:-[A-Za-z0-9_.-]+)?", str(value.get("version", ""))):
|
||||
raise ValueError("version_invalid")
|
||||
for name in ("target_oid", "evidence_closure_oid"):
|
||||
if not OID_RE.fullmatch(str(value.get(name, ""))):
|
||||
raise ValueError(f"{name}_invalid")
|
||||
if not PLAN_RE.fullmatch(str(value.get("approval_plan_id", ""))):
|
||||
raise ValueError("approval_plan_id_invalid")
|
||||
if value.get("repository_identity") != "aios/aios":
|
||||
raise ValueError("repository_identity_invalid")
|
||||
return value
|
||||
|
||||
|
||||
def publication_request(value: Mapping[str, Any]) -> PublicationRequest:
|
||||
return PublicationRequest(
|
||||
str(value["version"]),
|
||||
str(value["target_oid"]),
|
||||
str(value["evidence_closure_oid"]),
|
||||
str(value["approval_plan_id"]),
|
||||
)
|
||||
|
||||
|
||||
def response(value: Mapping[str, Any], status: str, **extra: Any) -> dict[str, Any]:
|
||||
return {
|
||||
"status": status,
|
||||
"version": value["version"],
|
||||
"target_oid": value["target_oid"],
|
||||
"evidence_closure_oid": value["evidence_closure_oid"],
|
||||
"artifact_sha256": value["artifact_sha256"],
|
||||
"approval_plan_id": value["approval_plan_id"],
|
||||
**extra,
|
||||
}
|
||||
|
||||
|
||||
def build(args: argparse.Namespace) -> tuple[TagPublisher, GitTagBackend, GiteaTagClient]:
|
||||
git = GitTagBackend(args.workspace, authoritative_repo=args.repository)
|
||||
gitea = GiteaTagClient(
|
||||
args.gitea_api,
|
||||
args.gitea_repo,
|
||||
args.gitea_token_file,
|
||||
token_line=args.gitea_token_line,
|
||||
)
|
||||
publisher = TagPublisher(
|
||||
args.workspace,
|
||||
git,
|
||||
gitea,
|
||||
args.remote,
|
||||
state_root=args.state_root,
|
||||
)
|
||||
return publisher, git, gitea
|
||||
|
||||
|
||||
def _gitea_lookup(gitea: GiteaTagClient, version: str) -> tuple[str, str] | None:
|
||||
try:
|
||||
return gitea.lookup_tag(version)
|
||||
except RuntimeError as exc:
|
||||
if "HTTP 404" in str(exc):
|
||||
return None
|
||||
raise
|
||||
|
||||
|
||||
def probe(
|
||||
args: argparse.Namespace,
|
||||
value: Mapping[str, Any],
|
||||
publisher: TagPublisher,
|
||||
git: GitTagBackend,
|
||||
gitea: GiteaTagClient,
|
||||
) -> dict[str, Any]:
|
||||
request = publication_request(value)
|
||||
request.validate()
|
||||
git.validate_evidence_lineage(request)
|
||||
|
||||
local_oid = git._rev(f"refs/tags/{request.version}")
|
||||
local_target = git._rev(f"refs/tags/{request.version}^{{commit}}")
|
||||
remote = git.remote_tag(args.remote, request.version)
|
||||
api = _gitea_lookup(gitea, request.version)
|
||||
listing = False
|
||||
if api is not None:
|
||||
listing = gitea.tag_visible_in_listing(request.version, request.target_oid)
|
||||
|
||||
if local_oid is None and remote is None and api is None:
|
||||
return response(value, "NOT_APPLIED")
|
||||
|
||||
if local_oid is not None:
|
||||
if local_target is None or local_oid == local_target:
|
||||
return response(value, "CONFLICT", reason="local_tag_not_annotated")
|
||||
if local_target != request.target_oid:
|
||||
return response(
|
||||
value,
|
||||
"CONFLICT",
|
||||
reason="local_peeled_target_mismatch",
|
||||
tag_oid=local_oid,
|
||||
peeled_target_oid=local_target,
|
||||
)
|
||||
|
||||
# A remote/Gitea tag without the Authority tag object is not safe to
|
||||
# recreate, because a new annotated tag would have a different object OID.
|
||||
if local_oid is None and (remote is not None or api is not None):
|
||||
return response(
|
||||
value,
|
||||
"INDETERMINATE",
|
||||
reason="remote_or_gitea_tag_exists_without_authority_tag_object",
|
||||
)
|
||||
|
||||
if remote is not None:
|
||||
if remote[0] != local_oid or remote[1] != request.target_oid:
|
||||
return response(
|
||||
value,
|
||||
"CONFLICT",
|
||||
reason="network_remote_tag_mismatch",
|
||||
tag_oid=local_oid,
|
||||
peeled_target_oid=local_target,
|
||||
remote_tag_oid=remote[0],
|
||||
remote_peeled_target_oid=remote[1],
|
||||
)
|
||||
|
||||
if api is not None:
|
||||
if api[0] != local_oid or api[1] != request.target_oid:
|
||||
return response(
|
||||
value,
|
||||
"CONFLICT",
|
||||
reason="gitea_api_tag_mismatch",
|
||||
tag_oid=local_oid,
|
||||
peeled_target_oid=local_target,
|
||||
gitea_tag_oid=api[0],
|
||||
gitea_peeled_target_oid=api[1],
|
||||
)
|
||||
|
||||
if (
|
||||
local_oid is not None
|
||||
and remote is not None
|
||||
and api is not None
|
||||
and listing
|
||||
):
|
||||
return response(
|
||||
value,
|
||||
"APPLIED_VERIFIED",
|
||||
tag_oid=local_oid,
|
||||
peeled_target_oid=local_target,
|
||||
remote_tag_oid=remote[0],
|
||||
remote_peeled_target_oid=remote[1],
|
||||
gitea_tag_oid=api[0],
|
||||
gitea_peeled_target_oid=api[1],
|
||||
gitea_listing_visible=True,
|
||||
)
|
||||
|
||||
if local_oid is not None and remote is None and api is None:
|
||||
return response(
|
||||
value,
|
||||
"PARTIAL_LOCAL",
|
||||
tag_oid=local_oid,
|
||||
peeled_target_oid=local_target,
|
||||
)
|
||||
|
||||
return response(
|
||||
value,
|
||||
"INDETERMINATE",
|
||||
reason="tag_visibility_partial",
|
||||
tag_oid=local_oid,
|
||||
peeled_target_oid=local_target,
|
||||
)
|
||||
|
||||
|
||||
def apply(
|
||||
args: argparse.Namespace,
|
||||
value: Mapping[str, Any],
|
||||
publisher: TagPublisher,
|
||||
git: GitTagBackend,
|
||||
gitea: GiteaTagClient,
|
||||
) -> dict[str, Any]:
|
||||
before = probe(args, value, publisher, git, gitea)
|
||||
if before["status"] in {"APPLIED_VERIFIED", "CONFLICT", "INDETERMINATE"}:
|
||||
return before
|
||||
|
||||
request = publication_request(value)
|
||||
with publisher.store.lock(request.version):
|
||||
request.validate()
|
||||
publisher._assert_fingerprint(request)
|
||||
publisher.store.record(request, PublicationState.PENDING)
|
||||
publisher._verify_publication(
|
||||
request,
|
||||
allow_push=True,
|
||||
previously_deployed=False,
|
||||
)
|
||||
return probe(args, value, publisher, git, gitea)
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--workspace", type=Path, default=Path(os.environ["AIOS_GITOPS_CONTROLLER_WORKSPACE"]))
|
||||
parser.add_argument("--repository", type=Path, default=Path(os.environ["AIOS_GITOPS_AUTHORITATIVE_GIT_DIR"]))
|
||||
parser.add_argument("--remote", default=os.getenv("AIOS_GITOPS_GITEA_REMOTE", "gitea"))
|
||||
parser.add_argument("--gitea-api", default=os.getenv("AIOS_GITOPS_GITEA_API_URL", ""))
|
||||
parser.add_argument("--gitea-repo", default=os.getenv("AIOS_GITOPS_GITEA_REPOSITORY", ""))
|
||||
parser.add_argument(
|
||||
"--gitea-token-file",
|
||||
type=Path,
|
||||
default=Path(os.getenv("AIOS_GITOPS_GITEA_TOKEN_FILE", "")),
|
||||
)
|
||||
parser.add_argument(
|
||||
"--gitea-token-line",
|
||||
type=int,
|
||||
default=int(os.getenv("AIOS_GITOPS_GITEA_TOKEN_LINE", "1")),
|
||||
)
|
||||
parser.add_argument(
|
||||
"--state-root",
|
||||
type=Path,
|
||||
default=Path(
|
||||
os.getenv(
|
||||
"AIOS_GITOPS_PUBLICATION_STATE_DIR",
|
||||
"/var/lib/aios-gitops/tag-publications",
|
||||
)
|
||||
),
|
||||
)
|
||||
args = parser.parse_args()
|
||||
value = load_request()
|
||||
publisher, git, gitea = build(args)
|
||||
operation = value.get("operation")
|
||||
if operation == "probe":
|
||||
result = probe(args, value, publisher, git, gitea)
|
||||
elif operation == "apply":
|
||||
result = apply(args, value, publisher, git, gitea)
|
||||
else:
|
||||
raise ValueError("unsupported_operation")
|
||||
print(json.dumps(result, sort_keys=True))
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
try:
|
||||
raise SystemExit(main())
|
||||
except PublicationManualOwnerReviewRequired as exc:
|
||||
print(json.dumps({"status": "CONFLICT", "reason": str(exc)}, sort_keys=True))
|
||||
raise SystemExit(20)
|
||||
except PublicationConflict as exc:
|
||||
print(json.dumps({"status": "CONFLICT", "reason": str(exc)}, sort_keys=True))
|
||||
raise SystemExit(20)
|
||||
except (PublicationPending, PublicationReconciliationRequired) as exc:
|
||||
print(json.dumps({"status": "INDETERMINATE", "reason": str(exc)}, sort_keys=True))
|
||||
raise SystemExit(21)
|
||||
except Exception as exc:
|
||||
print(
|
||||
json.dumps(
|
||||
{"status": "INDETERMINATE", "error": type(exc).__name__, "reason": str(exc)},
|
||||
sort_keys=True,
|
||||
)
|
||||
)
|
||||
raise SystemExit(22)
|
||||
+642
@@ -0,0 +1,642 @@
|
||||
#!/usr/bin/env python3
|
||||
"""One-shot local Owner authentication ingress for AIOS.
|
||||
|
||||
This module deliberately does not mint a reusable Owner token.
|
||||
|
||||
Authentication is based on a real local login session:
|
||||
- a root-run one-shot server owns a Unix socket;
|
||||
- the client must run as the configured Owner OS account;
|
||||
- the server validates SO_PEERCRED, Linux audit loginuid and the controlling TTY;
|
||||
- the Owner must interactively confirm the exact task/branch/base request;
|
||||
- the server constructs a narrow OwnerPrincipal and invokes the existing
|
||||
OwnerAuthorizationBroker;
|
||||
- one successful authorization consumes the service lifetime.
|
||||
|
||||
The resulting principal is fixed to one non-wildcard intake scope
|
||||
(`commit:create` for the current deployment) and cannot carry production,
|
||||
tag, baseline, main or history-rewrite capability.
|
||||
|
||||
This is a trust-root ingress, not a Git writer and not a production executor.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import fcntl
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import pwd
|
||||
import re
|
||||
import secrets
|
||||
import socket
|
||||
import stat
|
||||
import struct
|
||||
import sys
|
||||
import tempfile
|
||||
import time
|
||||
from typing import Any, Mapping
|
||||
|
||||
|
||||
BINDING_SCHEMA = "aios.local-owner-auth-ingress.v1"
|
||||
CLIENT_BEGIN_SCHEMA = "aios.local-owner-auth-client-begin.v1"
|
||||
CHALLENGE_SCHEMA = "aios.local-owner-auth-challenge.v1"
|
||||
CLIENT_CONFIRM_SCHEMA = "aios.local-owner-auth-client-confirm.v1"
|
||||
RESULT_SCHEMA = "aios.local-owner-auth-result.v1"
|
||||
|
||||
ACTION = "AUTHORIZE_TASK_WRITER_PROVISIONING"
|
||||
OID_RE = re.compile(r"^[0-9a-f]{40}$")
|
||||
IDENTITY_RE = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._@:+/-]{1,254}$")
|
||||
SCOPE_RE = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._:-]{1,127}$")
|
||||
TASK_RE = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._:-]{1,255}$")
|
||||
BRANCH_RE = re.compile(r"^agent/[A-Za-z0-9._/-]{1,220}$")
|
||||
|
||||
MAX_FRAME = 64 * 1024
|
||||
CHALLENGE_TTL_SECONDS = 120
|
||||
|
||||
EXPECTED_ALLOWED_OPERATIONS = frozenset(
|
||||
{
|
||||
"create_worktree",
|
||||
"create_target_branch_if_absent",
|
||||
"commit_to_target_branch",
|
||||
"push_target_branch",
|
||||
}
|
||||
)
|
||||
EXPECTED_DENIED_OPERATIONS = frozenset(
|
||||
{
|
||||
"direct_main_push",
|
||||
"force_push",
|
||||
"tag_create",
|
||||
"tag_update",
|
||||
"tag_delete",
|
||||
"branch_delete",
|
||||
"repository_admin",
|
||||
"baseline_write",
|
||||
"production_deploy",
|
||||
"production_marker_write",
|
||||
"history_rewrite",
|
||||
}
|
||||
)
|
||||
|
||||
SENSITIVE_FRAGMENTS = (
|
||||
"authorization_header",
|
||||
"bearer",
|
||||
"client_secret",
|
||||
"password",
|
||||
"private_key",
|
||||
"raw_secret",
|
||||
"raw_token",
|
||||
"secret_value",
|
||||
)
|
||||
|
||||
|
||||
class BootstrapError(RuntimeError):
|
||||
def __init__(self, code: str, status: int = 1) -> None:
|
||||
super().__init__(code)
|
||||
self.code = code
|
||||
self.status = status
|
||||
|
||||
|
||||
def canonical_json(value: Any) -> bytes:
|
||||
return (
|
||||
json.dumps(value, sort_keys=True, separators=(",", ":"), ensure_ascii=False)
|
||||
+ "\n"
|
||||
).encode("utf-8")
|
||||
|
||||
|
||||
def sha256_json(value: Any) -> str:
|
||||
return hashlib.sha256(canonical_json(value)).hexdigest()
|
||||
|
||||
|
||||
def atomic_json(path: Path, value: Mapping[str, Any], mode: int = 0o600) -> None:
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
if path.parent.is_symlink() or path.is_symlink():
|
||||
raise BootstrapError("STATE_SYMLINK_REJECTED")
|
||||
fd, name = tempfile.mkstemp(prefix=f".{path.name}.", dir=str(path.parent))
|
||||
try:
|
||||
os.fchmod(fd, mode)
|
||||
with os.fdopen(fd, "wb", closefd=True) as stream:
|
||||
stream.write(canonical_json(dict(value)))
|
||||
stream.flush()
|
||||
os.fsync(stream.fileno())
|
||||
os.replace(name, path)
|
||||
os.chmod(path, mode)
|
||||
dfd = os.open(path.parent, os.O_RDONLY | getattr(os, "O_DIRECTORY", 0))
|
||||
try:
|
||||
os.fsync(dfd)
|
||||
finally:
|
||||
os.close(dfd)
|
||||
finally:
|
||||
try:
|
||||
os.unlink(name)
|
||||
except FileNotFoundError:
|
||||
pass
|
||||
|
||||
|
||||
def secure_root_file(path: Path) -> dict[str, Any]:
|
||||
if path.is_symlink() or not path.is_file():
|
||||
raise BootstrapError("BINDING_FILE_INVALID")
|
||||
st = path.lstat()
|
||||
if st.st_uid != 0:
|
||||
raise BootstrapError("BINDING_FILE_ROOT_OWNER_REQUIRED")
|
||||
if stat.S_IMODE(st.st_mode) & 0o077:
|
||||
raise BootstrapError("BINDING_FILE_PERMISSIONS_INVALID")
|
||||
if st.st_size <= 0 or st.st_size > 1_048_576:
|
||||
raise BootstrapError("BINDING_FILE_SIZE_INVALID")
|
||||
try:
|
||||
value = json.loads(path.read_text(encoding="utf-8"))
|
||||
except (UnicodeDecodeError, json.JSONDecodeError) as exc:
|
||||
raise BootstrapError("BINDING_JSON_INVALID") from exc
|
||||
if not isinstance(value, dict):
|
||||
raise BootstrapError("BINDING_JSON_INVALID")
|
||||
return value
|
||||
|
||||
|
||||
def validate_binding(value: Mapping[str, Any]) -> dict[str, Any]:
|
||||
if set(value) != {
|
||||
"schema",
|
||||
"owner_identity",
|
||||
"owner_role",
|
||||
"authenticated_scope",
|
||||
"request",
|
||||
}:
|
||||
raise BootstrapError("BINDING_FIELDS_REJECTED")
|
||||
if value["schema"] != BINDING_SCHEMA:
|
||||
raise BootstrapError("BINDING_SCHEMA_REJECTED")
|
||||
identity = value["owner_identity"]
|
||||
if not isinstance(identity, str) or not IDENTITY_RE.fullmatch(identity):
|
||||
raise BootstrapError("OWNER_IDENTITY_INVALID")
|
||||
if value["owner_role"] != "owner":
|
||||
raise BootstrapError("OWNER_ROLE_INVALID")
|
||||
scope = value["authenticated_scope"]
|
||||
if (
|
||||
not isinstance(scope, str)
|
||||
or not SCOPE_RE.fullmatch(scope)
|
||||
or scope == "writer:provision"
|
||||
or scope == "*"
|
||||
or scope.endswith(":*")
|
||||
):
|
||||
raise BootstrapError("OWNER_SCOPE_INVALID")
|
||||
|
||||
request = value["request"]
|
||||
if not isinstance(request, dict):
|
||||
raise BootstrapError("REQUEST_INVALID")
|
||||
expected_fields = {
|
||||
"action",
|
||||
"task_id",
|
||||
"repository",
|
||||
"base_ref",
|
||||
"base_oid",
|
||||
"target_branch",
|
||||
"one_time_use",
|
||||
"expires_in_seconds",
|
||||
"production_change_authorized",
|
||||
"tag_change_authorized",
|
||||
"baseline_change_authorized",
|
||||
"main_write_authorized",
|
||||
"allowed_operations",
|
||||
"denied_operations",
|
||||
}
|
||||
if set(request) != expected_fields:
|
||||
raise BootstrapError("REQUEST_FIELDS_REJECTED")
|
||||
if request["action"] != ACTION:
|
||||
raise BootstrapError("REQUEST_ACTION_REJECTED")
|
||||
if request["repository"] != "aios/aios":
|
||||
raise BootstrapError("REPOSITORY_REJECTED")
|
||||
if request["base_ref"] != "refs/heads/main":
|
||||
raise BootstrapError("BASE_REF_REJECTED")
|
||||
if not isinstance(request["base_oid"], str) or not OID_RE.fullmatch(request["base_oid"]):
|
||||
raise BootstrapError("BASE_OID_REJECTED")
|
||||
if (
|
||||
not isinstance(request["target_branch"], str)
|
||||
or not BRANCH_RE.fullmatch(request["target_branch"])
|
||||
):
|
||||
raise BootstrapError("TARGET_BRANCH_REJECTED")
|
||||
if (
|
||||
not isinstance(request["task_id"], str)
|
||||
or not TASK_RE.fullmatch(request["task_id"])
|
||||
):
|
||||
raise BootstrapError("TASK_ID_REJECTED")
|
||||
if request["one_time_use"] is not True:
|
||||
raise BootstrapError("ONE_TIME_USE_REQUIRED")
|
||||
expiry = request["expires_in_seconds"]
|
||||
if (
|
||||
not isinstance(expiry, int)
|
||||
or isinstance(expiry, bool)
|
||||
or not 60 <= expiry <= 900
|
||||
):
|
||||
raise BootstrapError("EXPIRY_REJECTED")
|
||||
for field in (
|
||||
"production_change_authorized",
|
||||
"tag_change_authorized",
|
||||
"baseline_change_authorized",
|
||||
"main_write_authorized",
|
||||
):
|
||||
if request[field] is not False:
|
||||
raise BootstrapError("CONSEQUENTIAL_CAPABILITY_REJECTED")
|
||||
if (
|
||||
not isinstance(request["allowed_operations"], list)
|
||||
or frozenset(request["allowed_operations"]) != EXPECTED_ALLOWED_OPERATIONS
|
||||
):
|
||||
raise BootstrapError("ALLOWED_OPERATIONS_MISMATCH")
|
||||
if (
|
||||
not isinstance(request["denied_operations"], list)
|
||||
or frozenset(request["denied_operations"]) != EXPECTED_DENIED_OPERATIONS
|
||||
):
|
||||
raise BootstrapError("DENIED_OPERATIONS_MISMATCH")
|
||||
return {
|
||||
"schema": BINDING_SCHEMA,
|
||||
"owner_identity": identity,
|
||||
"owner_role": "owner",
|
||||
"authenticated_scope": scope,
|
||||
"request": dict(request),
|
||||
}
|
||||
|
||||
|
||||
def _status_uids(pid: int) -> tuple[int, int, int, int]:
|
||||
try:
|
||||
lines = Path(f"/proc/{pid}/status").read_text(encoding="utf-8").splitlines()
|
||||
except OSError as exc:
|
||||
raise BootstrapError("PEER_STATUS_UNAVAILABLE") from exc
|
||||
for line in lines:
|
||||
if line.startswith("Uid:"):
|
||||
fields = line.split()[1:]
|
||||
if len(fields) != 4:
|
||||
break
|
||||
return tuple(int(item) for item in fields) # type: ignore[return-value]
|
||||
raise BootstrapError("PEER_UID_STATUS_INVALID")
|
||||
|
||||
|
||||
def _loginuid(pid: int) -> int:
|
||||
try:
|
||||
raw = Path(f"/proc/{pid}/loginuid").read_text(encoding="ascii").strip()
|
||||
value = int(raw)
|
||||
except (OSError, ValueError) as exc:
|
||||
raise BootstrapError("PEER_LOGINUID_UNAVAILABLE") from exc
|
||||
if value == 4294967295:
|
||||
raise BootstrapError("PEER_LOGIN_SESSION_REQUIRED")
|
||||
return value
|
||||
|
||||
|
||||
def _tty_path(pid: int) -> Path:
|
||||
try:
|
||||
value = os.readlink(f"/proc/{pid}/fd/0")
|
||||
except OSError as exc:
|
||||
raise BootstrapError("PEER_INTERACTIVE_TTY_REQUIRED") from exc
|
||||
if not (value.startswith("/dev/pts/") or value.startswith("/dev/tty")):
|
||||
raise BootstrapError("PEER_INTERACTIVE_TTY_REQUIRED")
|
||||
path = Path(value)
|
||||
try:
|
||||
if not stat.S_ISCHR(path.stat().st_mode):
|
||||
raise BootstrapError("PEER_INTERACTIVE_TTY_REQUIRED")
|
||||
except OSError as exc:
|
||||
raise BootstrapError("PEER_INTERACTIVE_TTY_REQUIRED") from exc
|
||||
return path
|
||||
|
||||
|
||||
def verify_peer_session(pid: int, uid: int, gid: int, owner_identity: str) -> dict[str, Any]:
|
||||
try:
|
||||
owner = pwd.getpwnam(owner_identity)
|
||||
except KeyError as exc:
|
||||
raise BootstrapError("OWNER_OS_ACCOUNT_MISSING") from exc
|
||||
if uid != owner.pw_uid:
|
||||
raise BootstrapError("PEER_OWNER_UID_MISMATCH")
|
||||
uids = _status_uids(pid)
|
||||
if any(item != owner.pw_uid for item in uids):
|
||||
raise BootstrapError("PEER_UID_TRANSITION_REJECTED")
|
||||
loginuid = _loginuid(pid)
|
||||
if loginuid != owner.pw_uid:
|
||||
# This deliberately rejects sudo/runuser from another authenticated
|
||||
# account; the session itself must have logged in as the Owner.
|
||||
raise BootstrapError("PEER_LOGINUID_OWNER_MISMATCH")
|
||||
tty = _tty_path(pid)
|
||||
if tty.stat().st_uid != owner.pw_uid:
|
||||
raise BootstrapError("PEER_TTY_OWNER_MISMATCH")
|
||||
return {
|
||||
"pid": pid,
|
||||
"uid": uid,
|
||||
"gid": gid,
|
||||
"loginuid": loginuid,
|
||||
"tty": str(tty),
|
||||
"owner_identity": owner_identity,
|
||||
}
|
||||
|
||||
|
||||
def read_frame(connection: socket.socket) -> dict[str, Any]:
|
||||
data = bytearray()
|
||||
while len(data) <= MAX_FRAME:
|
||||
block = connection.recv(4096)
|
||||
if not block:
|
||||
break
|
||||
data.extend(block)
|
||||
if b"\n" in block:
|
||||
break
|
||||
if not data or len(data) > MAX_FRAME:
|
||||
raise BootstrapError("PROTOCOL_FRAME_INVALID")
|
||||
raw = bytes(data).split(b"\n", 1)[0]
|
||||
try:
|
||||
value = json.loads(raw)
|
||||
except (UnicodeDecodeError, json.JSONDecodeError) as exc:
|
||||
raise BootstrapError("PROTOCOL_JSON_INVALID") from exc
|
||||
if not isinstance(value, dict):
|
||||
raise BootstrapError("PROTOCOL_JSON_INVALID")
|
||||
return value
|
||||
|
||||
|
||||
def write_frame(connection: socket.socket, value: Mapping[str, Any]) -> None:
|
||||
connection.sendall(canonical_json(dict(value)))
|
||||
|
||||
|
||||
def safe_result(value: Mapping[str, Any]) -> dict[str, Any]:
|
||||
for key in value:
|
||||
lowered = str(key).lower()
|
||||
if any(fragment in lowered for fragment in SENSITIVE_FRAGMENTS):
|
||||
raise BootstrapError("BROKER_RESPONSE_SENSITIVE_DATA_REJECTED")
|
||||
allowed = {
|
||||
"status",
|
||||
"approval_plan_id",
|
||||
"grant_id",
|
||||
"grant_digest_sha256",
|
||||
"branch_oid",
|
||||
"branch_ref_created",
|
||||
"grant_consumed",
|
||||
"owner_authorization_event_id",
|
||||
"owner_authorization_reference",
|
||||
"owner_authorization_digest_sha256",
|
||||
"owner_authorization_consumed",
|
||||
"owner_authorization_idempotent",
|
||||
}
|
||||
return {key: value.get(key) for key in sorted(allowed) if key in value}
|
||||
|
||||
|
||||
def request_summary(binding: Mapping[str, Any]) -> dict[str, Any]:
|
||||
request = binding["request"]
|
||||
return {
|
||||
"owner_identity": binding["owner_identity"],
|
||||
"authenticated_scope": binding["authenticated_scope"],
|
||||
"task_id": request["task_id"],
|
||||
"repository": request["repository"],
|
||||
"base_ref": request["base_ref"],
|
||||
"base_oid": request["base_oid"],
|
||||
"target_branch": request["target_branch"],
|
||||
"expires_in_seconds": request["expires_in_seconds"],
|
||||
"allowed_operations": sorted(request["allowed_operations"]),
|
||||
"denied_operations": sorted(request["denied_operations"]),
|
||||
"production_change_authorized": False,
|
||||
"tag_change_authorized": False,
|
||||
"baseline_change_authorized": False,
|
||||
"main_write_authorized": False,
|
||||
}
|
||||
|
||||
|
||||
def _ensure_runtime_paths(socket_path: Path, state_root: Path, owner_identity: str) -> None:
|
||||
if os.geteuid() != 0:
|
||||
raise BootstrapError("SERVER_ROOT_REQUIRED")
|
||||
owner = pwd.getpwnam(owner_identity)
|
||||
for directory, mode, uid, gid in (
|
||||
(socket_path.parent, 0o750, 0, owner.pw_gid),
|
||||
(state_root, 0o700, 0, 0),
|
||||
):
|
||||
if directory.is_symlink():
|
||||
raise BootstrapError("RUNTIME_DIRECTORY_SYMLINK_REJECTED")
|
||||
directory.mkdir(parents=True, exist_ok=True)
|
||||
os.chown(directory, uid, gid)
|
||||
os.chmod(directory, mode)
|
||||
if socket_path.exists() or socket_path.is_symlink():
|
||||
raise BootstrapError("SOCKET_ALREADY_EXISTS")
|
||||
|
||||
|
||||
def _lock(path: Path):
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
fd = os.open(path, os.O_RDWR | os.O_CREAT | getattr(os, "O_NOFOLLOW", 0), 0o600)
|
||||
os.fchmod(fd, 0o600)
|
||||
fcntl.flock(fd, fcntl.LOCK_EX | fcntl.LOCK_NB)
|
||||
return fd
|
||||
|
||||
|
||||
def invoke_broker(binding: Mapping[str, Any]) -> dict[str, Any]:
|
||||
# Import only on the root server side. The bootstrap package does not carry
|
||||
# a second broker/provisioner implementation; it reuses current main.
|
||||
from owner_authorization_broker import ( # type: ignore
|
||||
OwnerAuthorizationBroker,
|
||||
OwnerAuthorizationBrokerConfig,
|
||||
OwnerPrincipal,
|
||||
)
|
||||
|
||||
config = OwnerAuthorizationBrokerConfig.from_environment()
|
||||
identity = str(binding["owner_identity"])
|
||||
scope = str(binding["authenticated_scope"])
|
||||
if identity not in config.allowed_owner_identities:
|
||||
raise BootstrapError("OWNER_IDENTITY_NOT_ALLOWED_BY_LIVE_BROKER")
|
||||
if scope not in config.allowed_intake_scopes:
|
||||
raise BootstrapError("OWNER_SCOPE_NOT_ALLOWED_BY_LIVE_BROKER")
|
||||
if config.max_expiry_seconds < int(binding["request"]["expires_in_seconds"]):
|
||||
raise BootstrapError("LIVE_BROKER_EXPIRY_TOO_SMALL")
|
||||
principal = OwnerPrincipal(
|
||||
identity=identity,
|
||||
role="owner",
|
||||
authenticated_scope=scope,
|
||||
token_scopes=frozenset({scope}),
|
||||
credential_id="",
|
||||
)
|
||||
return OwnerAuthorizationBroker(config=config).authorize_and_provision(
|
||||
binding["request"], principal
|
||||
)
|
||||
|
||||
|
||||
def serve(binding_path: Path, socket_path: Path, state_root: Path, timeout: int) -> int:
|
||||
binding = validate_binding(secure_root_file(binding_path))
|
||||
identity = str(binding["owner_identity"])
|
||||
_ensure_runtime_paths(socket_path, state_root, identity)
|
||||
lock_fd = _lock(state_root / "bootstrap.lock")
|
||||
listener = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
|
||||
try:
|
||||
listener.bind(str(socket_path))
|
||||
owner = pwd.getpwnam(identity)
|
||||
os.chown(socket_path, 0, owner.pw_gid)
|
||||
os.chmod(socket_path, 0o660)
|
||||
listener.listen(1)
|
||||
listener.settimeout(timeout)
|
||||
|
||||
connection, _ = listener.accept()
|
||||
with connection:
|
||||
raw_peer = connection.getsockopt(
|
||||
socket.SOL_SOCKET, socket.SO_PEERCRED, struct.calcsize("3i")
|
||||
)
|
||||
pid, uid, gid = struct.unpack("3i", raw_peer)
|
||||
session = verify_peer_session(pid, uid, gid, identity)
|
||||
|
||||
begin = read_frame(connection)
|
||||
if begin != {"schema": CLIENT_BEGIN_SCHEMA, "action": "BEGIN"}:
|
||||
raise BootstrapError("CLIENT_BEGIN_REJECTED")
|
||||
|
||||
challenge = secrets.token_hex(12)
|
||||
challenge_issued = time.monotonic()
|
||||
summary = request_summary(binding)
|
||||
write_frame(
|
||||
connection,
|
||||
{
|
||||
"schema": CHALLENGE_SCHEMA,
|
||||
"challenge": challenge,
|
||||
"expires_in_seconds": CHALLENGE_TTL_SECONDS,
|
||||
"request_digest_sha256": sha256_json(binding["request"]),
|
||||
"summary": summary,
|
||||
},
|
||||
)
|
||||
|
||||
confirm = read_frame(connection)
|
||||
expected_confirmation = f"AUTHORIZE {challenge}"
|
||||
if (
|
||||
confirm.get("schema") != CLIENT_CONFIRM_SCHEMA
|
||||
or confirm.get("challenge") != challenge
|
||||
or confirm.get("confirmation") != expected_confirmation
|
||||
):
|
||||
raise BootstrapError("OWNER_INTERACTIVE_CONFIRMATION_REJECTED")
|
||||
if time.monotonic() - challenge_issued > CHALLENGE_TTL_SECONDS:
|
||||
raise BootstrapError("OWNER_CONFIRMATION_EXPIRED")
|
||||
|
||||
result = safe_result(invoke_broker(binding))
|
||||
receipt = {
|
||||
"schema": "aios.local-owner-auth-ingress-receipt.v1",
|
||||
"status": "CONSUMED",
|
||||
"owner_identity": identity,
|
||||
"owner_role": "owner",
|
||||
"authenticated_scope": binding["authenticated_scope"],
|
||||
"peer": session,
|
||||
"request_digest_sha256": sha256_json(binding["request"]),
|
||||
"binding_digest_sha256": sha256_json(binding),
|
||||
"challenge_digest_sha256": hashlib.sha256(
|
||||
challenge.encode("ascii")
|
||||
).hexdigest(),
|
||||
"broker": result,
|
||||
"completed_at_unix": int(time.time()),
|
||||
}
|
||||
event_id = str(
|
||||
result.get("owner_authorization_event_id")
|
||||
or hashlib.sha256(
|
||||
canonical_json(
|
||||
{
|
||||
"peer": session,
|
||||
"request": sha256_json(binding["request"]),
|
||||
}
|
||||
)
|
||||
).hexdigest()[:32]
|
||||
)
|
||||
atomic_json(state_root / "events" / f"{event_id}.json", receipt)
|
||||
write_frame(
|
||||
connection,
|
||||
{
|
||||
"schema": RESULT_SCHEMA,
|
||||
"ok": True,
|
||||
"result": result,
|
||||
},
|
||||
)
|
||||
return 0
|
||||
except (BootstrapError, TimeoutError, socket.timeout) as exc:
|
||||
code = exc.code if isinstance(exc, BootstrapError) else "OWNER_BOOTSTRAP_TIMEOUT"
|
||||
try:
|
||||
if "connection" in locals():
|
||||
write_frame(
|
||||
connection,
|
||||
{
|
||||
"schema": RESULT_SCHEMA,
|
||||
"ok": False,
|
||||
"error": code,
|
||||
},
|
||||
)
|
||||
except Exception:
|
||||
pass
|
||||
atomic_json(
|
||||
state_root / "last-failure.json",
|
||||
{
|
||||
"schema": "aios.local-owner-auth-ingress-failure.v1",
|
||||
"status": "FAILED",
|
||||
"error": code,
|
||||
"binding_digest_sha256": sha256_json(binding),
|
||||
"failed_at_unix": int(time.time()),
|
||||
},
|
||||
)
|
||||
return 2
|
||||
finally:
|
||||
listener.close()
|
||||
try:
|
||||
socket_path.unlink()
|
||||
except FileNotFoundError:
|
||||
pass
|
||||
try:
|
||||
fcntl.flock(lock_fd, fcntl.LOCK_UN)
|
||||
finally:
|
||||
os.close(lock_fd)
|
||||
|
||||
|
||||
def client(socket_path: Path) -> int:
|
||||
# The server performs the authoritative peer/session checks. The client
|
||||
# also refuses non-interactive execution to make accidental automation hard.
|
||||
if not sys.stdin.isatty() or not sys.stdout.isatty():
|
||||
raise BootstrapError("CLIENT_INTERACTIVE_TTY_REQUIRED")
|
||||
connection = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
|
||||
connection.connect(str(socket_path))
|
||||
with connection:
|
||||
write_frame(connection, {"schema": CLIENT_BEGIN_SCHEMA, "action": "BEGIN"})
|
||||
challenge = read_frame(connection)
|
||||
if challenge.get("schema") != CHALLENGE_SCHEMA:
|
||||
raise BootstrapError("CHALLENGE_INVALID")
|
||||
summary = challenge.get("summary")
|
||||
if not isinstance(summary, dict):
|
||||
raise BootstrapError("CHALLENGE_INVALID")
|
||||
|
||||
print("\nAIOS one-shot Owner authorization request:")
|
||||
print(json.dumps(summary, indent=2, sort_keys=True))
|
||||
print("\nThis authorizes only the task writer shown above.")
|
||||
phrase = f"AUTHORIZE {challenge['challenge']}"
|
||||
typed = input(f"Type exactly:\n{phrase}\n> ").strip()
|
||||
if typed != phrase:
|
||||
raise BootstrapError("OWNER_CONFIRMATION_MISMATCH")
|
||||
write_frame(
|
||||
connection,
|
||||
{
|
||||
"schema": CLIENT_CONFIRM_SCHEMA,
|
||||
"challenge": challenge["challenge"],
|
||||
"confirmation": typed,
|
||||
},
|
||||
)
|
||||
result = read_frame(connection)
|
||||
print(json.dumps(result, indent=2, sort_keys=True))
|
||||
return 0 if result.get("ok") is True else 3
|
||||
|
||||
|
||||
def parser() -> argparse.ArgumentParser:
|
||||
value = argparse.ArgumentParser()
|
||||
mode = value.add_mutually_exclusive_group(required=True)
|
||||
mode.add_argument("--serve", action="store_true")
|
||||
mode.add_argument("--client", action="store_true")
|
||||
value.add_argument("--binding", type=Path)
|
||||
value.add_argument(
|
||||
"--socket",
|
||||
type=Path,
|
||||
default=Path("/run/aios-owner-bootstrap/owner.sock"),
|
||||
)
|
||||
value.add_argument(
|
||||
"--state-root",
|
||||
type=Path,
|
||||
default=Path("/var/lib/aios-gitops/owner-local-auth-ingress"),
|
||||
)
|
||||
value.add_argument("--timeout", type=int, default=300)
|
||||
return value
|
||||
|
||||
|
||||
def main(argv: list[str] | None = None) -> int:
|
||||
args = parser().parse_args(argv)
|
||||
if args.serve:
|
||||
if args.binding is None:
|
||||
raise SystemExit("--binding is required with --serve")
|
||||
return serve(args.binding, args.socket, args.state_root, args.timeout)
|
||||
return client(args.socket)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
try:
|
||||
raise SystemExit(main())
|
||||
except BootstrapError as exc:
|
||||
print(json.dumps({"status": "BLOCKED", "error": exc.code}), file=sys.stderr)
|
||||
raise SystemExit(exc.status)
|
||||
+225
@@ -0,0 +1,225 @@
|
||||
#!/usr/bin/env python3
|
||||
"""AIOS durable autonomous release-campaign authorization.
|
||||
|
||||
The campaign receipt represents one explicit Owner policy delegation. It is not
|
||||
a reusable Owner credential and it is not an unrestricted Git/production token.
|
||||
|
||||
Normal release operations are allowed only when all machine gates in the
|
||||
campaign policy succeed. Formal tag conflicts, non-fast-forward main divergence,
|
||||
history rewrite, private-signing-material exposure, repository identity conflict
|
||||
or campaign revocation remain fail-closed/manual-review conditions.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import re
|
||||
import stat
|
||||
from typing import Any, Mapping
|
||||
|
||||
OID_RE = re.compile(r"^[0-9a-f]{40}$")
|
||||
CAMPAIGN_RE = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._:-]{3,127}$")
|
||||
SCHEMA = "aios.release-campaign-authorization.v1"
|
||||
|
||||
ALLOWED_ENGINEERING = frozenset({
|
||||
"create_task_branch",
|
||||
"create_worktree",
|
||||
"append_commit",
|
||||
"push_task_branch",
|
||||
"repair_task_branch",
|
||||
"create_pr",
|
||||
"update_pr",
|
||||
"run_required_checks",
|
||||
"publish_truthful_status",
|
||||
"protected_merge",
|
||||
})
|
||||
ALLOWED_RECONCILIATION = frozenset({
|
||||
"fetch_gitea_main",
|
||||
"verify_ancestor",
|
||||
"authority_main_ff_cas",
|
||||
"refresh_runtime_context",
|
||||
})
|
||||
ALLOWED_RELEASE = frozenset({
|
||||
"deterministic_build",
|
||||
"retain_artifact",
|
||||
"materialize_approved_artifact",
|
||||
"staging_install",
|
||||
"health_check",
|
||||
"restart_test",
|
||||
"rollback_readiness_test",
|
||||
"create_evidence_commit",
|
||||
"generate_exact_binding",
|
||||
"derive_campaign_acceptance",
|
||||
})
|
||||
ALLOWED_PRODUCTION = frozenset({
|
||||
"create_annotated_tag",
|
||||
"publish_tag",
|
||||
"verify_tag",
|
||||
"deploy_exact_artifact",
|
||||
"verify_production_health",
|
||||
"update_runtime_registry",
|
||||
"update_production_baseline",
|
||||
"update_baseline_sidecar",
|
||||
})
|
||||
DENIED = frozenset({
|
||||
"force_push",
|
||||
"non_fast_forward_main_write",
|
||||
"direct_main_push",
|
||||
"published_history_rewrite",
|
||||
"formal_tag_rewrite",
|
||||
"formal_tag_delete",
|
||||
"overwrite_conflicting_same_name_tag",
|
||||
"expose_private_signing_material",
|
||||
"commit_plaintext_secret",
|
||||
"repository_identity_change",
|
||||
"weaken_branch_protection",
|
||||
})
|
||||
|
||||
|
||||
class CampaignError(RuntimeError):
|
||||
pass
|
||||
|
||||
|
||||
def canonical_json(value: Any) -> bytes:
|
||||
return (
|
||||
json.dumps(value, sort_keys=True, separators=(",", ":"), ensure_ascii=False)
|
||||
+ "\n"
|
||||
).encode("utf-8")
|
||||
|
||||
|
||||
def sha256_bytes(value: bytes) -> str:
|
||||
return hashlib.sha256(value).hexdigest()
|
||||
|
||||
|
||||
def secure_json(path: Path) -> dict[str, Any]:
|
||||
if path.is_symlink() or not path.is_file():
|
||||
raise CampaignError(f"invalid_file:{path}")
|
||||
st = path.lstat()
|
||||
if st.st_uid != 0:
|
||||
raise CampaignError(f"root_owner_required:{path}")
|
||||
if stat.S_IMODE(st.st_mode) & 0o022:
|
||||
raise CampaignError(f"group_world_writable_rejected:{path}")
|
||||
try:
|
||||
value = json.loads(path.read_text(encoding="utf-8"))
|
||||
except (UnicodeDecodeError, json.JSONDecodeError) as exc:
|
||||
raise CampaignError(f"invalid_json:{path}") from exc
|
||||
if not isinstance(value, dict):
|
||||
raise CampaignError(f"json_object_required:{path}")
|
||||
return value
|
||||
|
||||
|
||||
def _exact_set(value: Any, expected: frozenset[str], name: str) -> None:
|
||||
if not isinstance(value, list) or frozenset(value) != expected:
|
||||
raise CampaignError(f"{name}_mismatch")
|
||||
|
||||
|
||||
def validate_policy(value: Mapping[str, Any]) -> dict[str, Any]:
|
||||
required = {
|
||||
"schema",
|
||||
"campaign_id",
|
||||
"generation",
|
||||
"owner_identity",
|
||||
"owner_role",
|
||||
"repository",
|
||||
"lineage_anchor_oid",
|
||||
"control_plane_branch",
|
||||
"activation_state",
|
||||
"active",
|
||||
"revoked",
|
||||
"valid_until_revoked",
|
||||
"allowed_engineering",
|
||||
"allowed_reconciliation",
|
||||
"allowed_release",
|
||||
"allowed_production",
|
||||
"denied",
|
||||
"normal_owner_interaction_required",
|
||||
}
|
||||
if set(value) != required:
|
||||
raise CampaignError("campaign_fields_invalid")
|
||||
if value["schema"] != SCHEMA:
|
||||
raise CampaignError("campaign_schema_invalid")
|
||||
if not isinstance(value["campaign_id"], str) or not CAMPAIGN_RE.fullmatch(value["campaign_id"]):
|
||||
raise CampaignError("campaign_id_invalid")
|
||||
if value["generation"] != 1:
|
||||
raise CampaignError("campaign_generation_invalid")
|
||||
if value["owner_identity"] != "aiman" or value["owner_role"] != "owner":
|
||||
raise CampaignError("campaign_owner_invalid")
|
||||
if value["repository"] != "aios/aios":
|
||||
raise CampaignError("campaign_repository_invalid")
|
||||
if not isinstance(value["lineage_anchor_oid"], str) or not OID_RE.fullmatch(value["lineage_anchor_oid"]):
|
||||
raise CampaignError("campaign_anchor_invalid")
|
||||
branch = value["control_plane_branch"]
|
||||
if branch != "agent/autonomous-release-campaign-v1-20260807":
|
||||
raise CampaignError("campaign_branch_invalid")
|
||||
if value["activation_state"] not in {"PENDING_CONTROL_PLANE_MERGE", "ACTIVE"}:
|
||||
raise CampaignError("campaign_activation_state_invalid")
|
||||
if value["revoked"] is not False or value["valid_until_revoked"] is not True:
|
||||
raise CampaignError("campaign_lifecycle_invalid")
|
||||
if value["normal_owner_interaction_required"] is not False:
|
||||
raise CampaignError("campaign_owner_interaction_policy_invalid")
|
||||
if value["activation_state"] == "ACTIVE" and value["active"] is not True:
|
||||
raise CampaignError("campaign_active_state_invalid")
|
||||
if value["activation_state"] == "PENDING_CONTROL_PLANE_MERGE" and value["active"] is not False:
|
||||
raise CampaignError("campaign_pending_state_invalid")
|
||||
_exact_set(value["allowed_engineering"], ALLOWED_ENGINEERING, "allowed_engineering")
|
||||
_exact_set(value["allowed_reconciliation"], ALLOWED_RECONCILIATION, "allowed_reconciliation")
|
||||
_exact_set(value["allowed_release"], ALLOWED_RELEASE, "allowed_release")
|
||||
_exact_set(value["allowed_production"], ALLOWED_PRODUCTION, "allowed_production")
|
||||
_exact_set(value["denied"], DENIED, "denied")
|
||||
return dict(value)
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class CampaignAuthorization:
|
||||
path: Path
|
||||
data: dict[str, Any]
|
||||
digest: str
|
||||
|
||||
@classmethod
|
||||
def load(
|
||||
cls,
|
||||
path: Path,
|
||||
*,
|
||||
sidecar: Path | None = None,
|
||||
revocation: Path | None = None,
|
||||
require_active: bool = True,
|
||||
) -> "CampaignAuthorization":
|
||||
data = validate_policy(secure_json(path))
|
||||
raw = path.read_bytes()
|
||||
digest = sha256_bytes(raw)
|
||||
sidecar = sidecar or path.with_suffix(path.suffix + ".sha256")
|
||||
if not sidecar.is_file() or sidecar.is_symlink():
|
||||
raise CampaignError("campaign_digest_sidecar_missing")
|
||||
token = sidecar.read_text(encoding="ascii").strip().split()[0]
|
||||
if token != digest:
|
||||
raise CampaignError("campaign_digest_sidecar_mismatch")
|
||||
if revocation and revocation.exists():
|
||||
revoked = secure_json(revocation)
|
||||
if revoked.get("campaign_id") == data["campaign_id"] and revoked.get("revoked") is True:
|
||||
raise CampaignError("campaign_revoked")
|
||||
if require_active and (data["activation_state"] != "ACTIVE" or data["active"] is not True):
|
||||
raise CampaignError("campaign_not_active")
|
||||
return cls(path=path, data=data, digest=digest)
|
||||
|
||||
@property
|
||||
def campaign_id(self) -> str:
|
||||
return str(self.data["campaign_id"])
|
||||
|
||||
@property
|
||||
def anchor_oid(self) -> str:
|
||||
return str(self.data["lineage_anchor_oid"])
|
||||
|
||||
def assert_operation(self, operation: str) -> None:
|
||||
if operation in DENIED:
|
||||
raise CampaignError(f"campaign_operation_denied:{operation}")
|
||||
universe = (
|
||||
ALLOWED_ENGINEERING
|
||||
| ALLOWED_RECONCILIATION
|
||||
| ALLOWED_RELEASE
|
||||
| ALLOWED_PRODUCTION
|
||||
)
|
||||
if operation not in universe:
|
||||
raise CampaignError(f"campaign_operation_out_of_scope:{operation}")
|
||||
+427
@@ -0,0 +1,427 @@
|
||||
#!/usr/bin/env python3
|
||||
"""One-interaction bootstrap for the AIOS autonomous release campaign.
|
||||
|
||||
A single real local `aiman` login confirmation does two things atomically at the
|
||||
policy layer:
|
||||
|
||||
1. Writes a root-owned, digest-sidecar protected campaign authorization receipt
|
||||
in PENDING_CONTROL_PLANE_MERGE state. It grants no immediate production side
|
||||
effect; the campaign becomes ACTIVE only after the campaign control-plane PR
|
||||
is merged and machine activation verifies lineage and implementation.
|
||||
2. Invokes the existing OwnerAuthorizationBroker to issue only the narrow
|
||||
engineering writer needed to implement that control-plane PR.
|
||||
|
||||
No reusable Owner token is minted.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import fcntl
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import secrets
|
||||
import socket
|
||||
import struct
|
||||
import tempfile
|
||||
import time
|
||||
from typing import Any, Mapping
|
||||
|
||||
from owner_local_auth_ingress import (
|
||||
BootstrapError,
|
||||
canonical_json,
|
||||
read_frame,
|
||||
verify_peer_session,
|
||||
write_frame,
|
||||
)
|
||||
from release_campaign_authorization import (
|
||||
ALLOWED_ENGINEERING,
|
||||
ALLOWED_PRODUCTION,
|
||||
ALLOWED_RECONCILIATION,
|
||||
ALLOWED_RELEASE,
|
||||
DENIED,
|
||||
SCHEMA,
|
||||
validate_policy,
|
||||
)
|
||||
|
||||
BEGIN = "aios.release-campaign-bootstrap.begin.v1"
|
||||
CHALLENGE = "aios.release-campaign-bootstrap.challenge.v1"
|
||||
CONFIRM = "aios.release-campaign-bootstrap.confirm.v1"
|
||||
RESULT = "aios.release-campaign-bootstrap.result.v1"
|
||||
|
||||
TASK_ID = "AIOS_AUTONOMOUS_RELEASE_CAMPAIGN_V1"
|
||||
TARGET_BRANCH = "agent/autonomous-release-campaign-v1-20260807"
|
||||
REPOSITORY = "aios/aios"
|
||||
|
||||
|
||||
class CampaignBootstrapError(RuntimeError):
|
||||
pass
|
||||
|
||||
|
||||
def sha256_file(path: Path) -> str:
|
||||
h = hashlib.sha256()
|
||||
with path.open("rb") as stream:
|
||||
for block in iter(lambda: stream.read(1024 * 1024), b""):
|
||||
h.update(block)
|
||||
return h.hexdigest()
|
||||
|
||||
|
||||
def atomic_json_new(path: Path, value: Mapping[str, Any]) -> None:
|
||||
if path.exists() or path.is_symlink():
|
||||
raise CampaignBootstrapError(f"target_exists:{path}")
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
fd, name = tempfile.mkstemp(prefix=f".{path.name}.", dir=path.parent)
|
||||
try:
|
||||
os.fchmod(fd, 0o600)
|
||||
if os.geteuid() == 0:
|
||||
os.fchown(fd, 0, 0)
|
||||
with os.fdopen(fd, "wb", closefd=True) as stream:
|
||||
stream.write(canonical_json(dict(value)))
|
||||
stream.flush()
|
||||
os.fsync(stream.fileno())
|
||||
os.replace(name, path)
|
||||
os.chmod(path, 0o600)
|
||||
dfd = os.open(path.parent, os.O_RDONLY | os.O_DIRECTORY)
|
||||
try:
|
||||
os.fsync(dfd)
|
||||
finally:
|
||||
os.close(dfd)
|
||||
finally:
|
||||
try:
|
||||
os.unlink(name)
|
||||
except FileNotFoundError:
|
||||
pass
|
||||
|
||||
|
||||
def atomic_sidecar(path: Path, digest: str, target_name: str) -> None:
|
||||
if path.exists() or path.is_symlink():
|
||||
raise CampaignBootstrapError(f"sidecar_exists:{path}")
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
fd, name = tempfile.mkstemp(prefix=f".{path.name}.", dir=path.parent)
|
||||
try:
|
||||
os.fchmod(fd, 0o600)
|
||||
if os.geteuid() == 0:
|
||||
os.fchown(fd, 0, 0)
|
||||
with os.fdopen(fd, "w", encoding="ascii", closefd=True) as stream:
|
||||
stream.write(f"{digest} {target_name}\n")
|
||||
stream.flush()
|
||||
os.fsync(stream.fileno())
|
||||
os.replace(name, path)
|
||||
os.chmod(path, 0o600)
|
||||
dfd = os.open(path.parent, os.O_RDONLY | os.O_DIRECTORY)
|
||||
try:
|
||||
os.fsync(dfd)
|
||||
finally:
|
||||
os.close(dfd)
|
||||
finally:
|
||||
try:
|
||||
os.unlink(name)
|
||||
except FileNotFoundError:
|
||||
pass
|
||||
|
||||
|
||||
def writer_request(base_oid: str) -> dict[str, Any]:
|
||||
return {
|
||||
"action": "AUTHORIZE_TASK_WRITER_PROVISIONING",
|
||||
"task_id": TASK_ID,
|
||||
"repository": REPOSITORY,
|
||||
"base_ref": "refs/heads/main",
|
||||
"base_oid": base_oid,
|
||||
"target_branch": TARGET_BRANCH,
|
||||
"one_time_use": True,
|
||||
"expires_in_seconds": 300,
|
||||
"production_change_authorized": False,
|
||||
"tag_change_authorized": False,
|
||||
"baseline_change_authorized": False,
|
||||
"main_write_authorized": False,
|
||||
"allowed_operations": [
|
||||
"create_worktree",
|
||||
"create_target_branch_if_absent",
|
||||
"commit_to_target_branch",
|
||||
"push_target_branch",
|
||||
],
|
||||
"denied_operations": [
|
||||
"direct_main_push",
|
||||
"force_push",
|
||||
"tag_create",
|
||||
"tag_update",
|
||||
"tag_delete",
|
||||
"branch_delete",
|
||||
"repository_admin",
|
||||
"baseline_write",
|
||||
"production_deploy",
|
||||
"production_marker_write",
|
||||
"history_rewrite",
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
def campaign_policy(campaign_id: str, base_oid: str) -> dict[str, Any]:
|
||||
value = {
|
||||
"schema": SCHEMA,
|
||||
"campaign_id": campaign_id,
|
||||
"generation": 1,
|
||||
"owner_identity": "aiman",
|
||||
"owner_role": "owner",
|
||||
"repository": REPOSITORY,
|
||||
"lineage_anchor_oid": base_oid,
|
||||
"control_plane_branch": TARGET_BRANCH,
|
||||
"activation_state": "PENDING_CONTROL_PLANE_MERGE",
|
||||
"active": False,
|
||||
"revoked": False,
|
||||
"valid_until_revoked": True,
|
||||
"allowed_engineering": sorted(ALLOWED_ENGINEERING),
|
||||
"allowed_reconciliation": sorted(ALLOWED_RECONCILIATION),
|
||||
"allowed_release": sorted(ALLOWED_RELEASE),
|
||||
"allowed_production": sorted(ALLOWED_PRODUCTION),
|
||||
"denied": sorted(DENIED),
|
||||
"normal_owner_interaction_required": False,
|
||||
}
|
||||
return validate_policy(value)
|
||||
|
||||
|
||||
def invoke_writer_broker(base_oid: str) -> dict[str, Any]:
|
||||
from owner_authorization_broker import (
|
||||
OwnerAuthorizationBroker,
|
||||
OwnerAuthorizationBrokerConfig,
|
||||
OwnerPrincipal,
|
||||
)
|
||||
|
||||
config = OwnerAuthorizationBrokerConfig.from_environment()
|
||||
if "aiman" not in config.allowed_owner_identities:
|
||||
raise CampaignBootstrapError("aiman_not_allowed_by_live_broker")
|
||||
if "commit:create" not in config.allowed_intake_scopes:
|
||||
raise CampaignBootstrapError("commit_create_not_allowed_by_live_broker")
|
||||
principal = OwnerPrincipal(
|
||||
identity="aiman",
|
||||
role="owner",
|
||||
authenticated_scope="commit:create",
|
||||
token_scopes=frozenset({"commit:create"}),
|
||||
credential_id="",
|
||||
)
|
||||
return OwnerAuthorizationBroker(config=config).authorize_and_provision(
|
||||
writer_request(base_oid),
|
||||
principal,
|
||||
)
|
||||
|
||||
|
||||
def safe_broker_result(value: Mapping[str, Any]) -> dict[str, Any]:
|
||||
allowed = {
|
||||
"status",
|
||||
"approval_plan_id",
|
||||
"grant_id",
|
||||
"grant_digest_sha256",
|
||||
"branch_oid",
|
||||
"branch_ref_created",
|
||||
"grant_consumed",
|
||||
"owner_authorization_event_id",
|
||||
"owner_authorization_reference",
|
||||
"owner_authorization_digest_sha256",
|
||||
"owner_authorization_consumed",
|
||||
"owner_authorization_idempotent",
|
||||
}
|
||||
lowered = " ".join(str(k).lower() for k in value)
|
||||
for secret_word in ("token", "password", "private_key", "secret_value", "bearer"):
|
||||
if secret_word in lowered:
|
||||
raise CampaignBootstrapError("broker_response_sensitive_field_rejected")
|
||||
return {k: value[k] for k in sorted(allowed) if k in value}
|
||||
|
||||
|
||||
def serve(
|
||||
*,
|
||||
socket_path: Path,
|
||||
state_root: Path,
|
||||
base_oid: str,
|
||||
campaign_id: str,
|
||||
timeout: int,
|
||||
) -> int:
|
||||
if os.geteuid() != 0:
|
||||
raise CampaignBootstrapError("server_root_required")
|
||||
if len(base_oid) != 40 or any(c not in "0123456789abcdef" for c in base_oid):
|
||||
raise CampaignBootstrapError("base_oid_invalid")
|
||||
|
||||
policy = campaign_policy(campaign_id, base_oid)
|
||||
campaign_dir = state_root / campaign_id
|
||||
campaign_path = campaign_dir / "authorization.json"
|
||||
sidecar_path = campaign_dir / "authorization.json.sha256"
|
||||
|
||||
socket_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
if socket_path.exists() or socket_path.is_symlink():
|
||||
raise CampaignBootstrapError("socket_already_exists")
|
||||
|
||||
import pwd
|
||||
owner = pwd.getpwnam("aiman")
|
||||
os.chown(socket_path.parent, 0, owner.pw_gid)
|
||||
os.chmod(socket_path.parent, 0o750)
|
||||
|
||||
listener = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
|
||||
try:
|
||||
listener.bind(str(socket_path))
|
||||
os.chown(socket_path, 0, owner.pw_gid)
|
||||
os.chmod(socket_path, 0o660)
|
||||
listener.listen(1)
|
||||
listener.settimeout(timeout)
|
||||
|
||||
conn, _ = listener.accept()
|
||||
with conn:
|
||||
raw = conn.getsockopt(socket.SOL_SOCKET, socket.SO_PEERCRED, struct.calcsize("3i"))
|
||||
pid, uid, gid = struct.unpack("3i", raw)
|
||||
peer = verify_peer_session(pid, uid, gid, "aiman")
|
||||
if read_frame(conn) != {"schema": BEGIN, "action": "BEGIN"}:
|
||||
raise CampaignBootstrapError("client_begin_invalid")
|
||||
|
||||
challenge = secrets.token_hex(12)
|
||||
issued = time.monotonic()
|
||||
summary = {
|
||||
"campaign_id": campaign_id,
|
||||
"repository": REPOSITORY,
|
||||
"lineage_anchor_oid": base_oid,
|
||||
"control_plane_branch": TARGET_BRANCH,
|
||||
"normal_owner_interaction_after_activation": False,
|
||||
"allowed_engineering": sorted(ALLOWED_ENGINEERING),
|
||||
"allowed_reconciliation": sorted(ALLOWED_RECONCILIATION),
|
||||
"allowed_release": sorted(ALLOWED_RELEASE),
|
||||
"allowed_production": sorted(ALLOWED_PRODUCTION),
|
||||
"denied": sorted(DENIED),
|
||||
"writer_request": writer_request(base_oid),
|
||||
}
|
||||
write_frame(
|
||||
conn,
|
||||
{
|
||||
"schema": CHALLENGE,
|
||||
"challenge": challenge,
|
||||
"expires_in_seconds": 120,
|
||||
"summary": summary,
|
||||
},
|
||||
)
|
||||
confirm = read_frame(conn)
|
||||
phrase = f"AUTHORIZE CAMPAIGN {campaign_id} {challenge}"
|
||||
if (
|
||||
confirm.get("schema") != CONFIRM
|
||||
or confirm.get("challenge") != challenge
|
||||
or confirm.get("confirmation") != phrase
|
||||
):
|
||||
raise CampaignBootstrapError("campaign_confirmation_invalid")
|
||||
if time.monotonic() - issued > 120:
|
||||
raise CampaignBootstrapError("campaign_confirmation_expired")
|
||||
|
||||
# First issue the narrow engineering writer. If this fails, do not
|
||||
# persist a campaign authorization that cannot be implemented.
|
||||
broker = safe_broker_result(invoke_writer_broker(base_oid))
|
||||
if broker.get("status") != "READY_FOR_CODE_AUTHORING":
|
||||
raise CampaignBootstrapError("writer_not_ready")
|
||||
|
||||
atomic_json_new(campaign_path, policy)
|
||||
digest = sha256_file(campaign_path)
|
||||
atomic_sidecar(sidecar_path, digest, campaign_path.name)
|
||||
|
||||
receipt = {
|
||||
"schema": "aios.release-campaign-bootstrap-receipt.v1",
|
||||
"status": "CAMPAIGN_PENDING_CONTROL_PLANE_MERGE",
|
||||
"campaign_id": campaign_id,
|
||||
"campaign_authorization_path": str(campaign_path),
|
||||
"campaign_authorization_sha256": digest,
|
||||
"owner_identity": "aiman",
|
||||
"owner_role": "owner",
|
||||
"authentication": {
|
||||
"method": "local_so_peercred_loginuid_tty",
|
||||
"uid": peer["uid"],
|
||||
"loginuid": peer["loginuid"],
|
||||
"tty": peer["tty"],
|
||||
},
|
||||
"writer": broker,
|
||||
"reusable_owner_token_created": False,
|
||||
"production_side_effect_executed": False,
|
||||
"completed_at_unix": int(time.time()),
|
||||
}
|
||||
receipt_path = campaign_dir / "bootstrap-receipt.json"
|
||||
atomic_json_new(receipt_path, receipt)
|
||||
write_frame(
|
||||
conn,
|
||||
{
|
||||
"schema": RESULT,
|
||||
"ok": True,
|
||||
"campaign_id": campaign_id,
|
||||
"campaign_authorization_path": str(campaign_path),
|
||||
"campaign_authorization_sha256": digest,
|
||||
"writer": broker,
|
||||
},
|
||||
)
|
||||
return 0
|
||||
finally:
|
||||
listener.close()
|
||||
try:
|
||||
socket_path.unlink()
|
||||
except FileNotFoundError:
|
||||
pass
|
||||
|
||||
|
||||
def client(socket_path: Path) -> int:
|
||||
import sys
|
||||
if not sys.stdin.isatty() or not sys.stdout.isatty():
|
||||
raise CampaignBootstrapError("interactive_tty_required")
|
||||
conn = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
|
||||
conn.connect(str(socket_path))
|
||||
with conn:
|
||||
write_frame(conn, {"schema": BEGIN, "action": "BEGIN"})
|
||||
challenge = read_frame(conn)
|
||||
if challenge.get("schema") != CHALLENGE:
|
||||
raise CampaignBootstrapError("challenge_invalid")
|
||||
print("\nAIOS Autonomous Release Campaign authorization")
|
||||
print(json.dumps(challenge["summary"], indent=2, sort_keys=True))
|
||||
phrase = (
|
||||
f"AUTHORIZE CAMPAIGN "
|
||||
f"{challenge['summary']['campaign_id']} "
|
||||
f"{challenge['challenge']}"
|
||||
)
|
||||
typed = input(f"\nType exactly:\n{phrase}\n> ").strip()
|
||||
if typed != phrase:
|
||||
raise CampaignBootstrapError("confirmation_mismatch")
|
||||
write_frame(
|
||||
conn,
|
||||
{
|
||||
"schema": CONFIRM,
|
||||
"challenge": challenge["challenge"],
|
||||
"confirmation": typed,
|
||||
},
|
||||
)
|
||||
result = read_frame(conn)
|
||||
print(json.dumps(result, indent=2, sort_keys=True))
|
||||
return 0 if result.get("ok") is True else 3
|
||||
|
||||
|
||||
def main() -> int:
|
||||
p = argparse.ArgumentParser()
|
||||
mode = p.add_mutually_exclusive_group(required=True)
|
||||
mode.add_argument("--serve", action="store_true")
|
||||
mode.add_argument("--client", action="store_true")
|
||||
p.add_argument("--socket", type=Path, default=Path("/run/aios-release-campaign/owner.sock"))
|
||||
p.add_argument(
|
||||
"--state-root",
|
||||
type=Path,
|
||||
default=Path("/var/lib/aios-gitops/release-campaigns"),
|
||||
)
|
||||
p.add_argument("--base-oid")
|
||||
p.add_argument("--campaign-id")
|
||||
p.add_argument("--timeout", type=int, default=300)
|
||||
a = p.parse_args()
|
||||
if a.serve:
|
||||
if not a.base_oid or not a.campaign_id:
|
||||
raise CampaignBootstrapError("server_binding_required")
|
||||
return serve(
|
||||
socket_path=a.socket,
|
||||
state_root=a.state_root,
|
||||
base_oid=a.base_oid,
|
||||
campaign_id=a.campaign_id,
|
||||
timeout=a.timeout,
|
||||
)
|
||||
return client(a.socket)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
try:
|
||||
raise SystemExit(main())
|
||||
except (CampaignBootstrapError, BootstrapError) as exc:
|
||||
print(json.dumps({"status": "BLOCKED", "reason": str(exc)}, sort_keys=True))
|
||||
raise SystemExit(2)
|
||||
+113
@@ -0,0 +1,113 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Durable state model for the AIOS autonomous release campaign."""
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import tempfile
|
||||
from typing import Any
|
||||
|
||||
STATES = (
|
||||
"DEVELOPMENT",
|
||||
"VALIDATING",
|
||||
"PR_READY",
|
||||
"MERGING",
|
||||
"MAIN_RECONCILING",
|
||||
"POSTMERGE_BUILDING",
|
||||
"STAGING",
|
||||
"EVIDENCE_CLOSING",
|
||||
"BINDING_ACCEPTING",
|
||||
"TAGGING",
|
||||
"DEPLOYING",
|
||||
"VERIFYING",
|
||||
"REGISTRY_FINALIZING",
|
||||
"BASELINE_FINALIZING",
|
||||
"PRODUCTION_DEPLOYED",
|
||||
)
|
||||
RECOVERABLE = "RECOVERABLE_FAILURE"
|
||||
RECONCILIATION = "RECONCILIATION_REQUIRED"
|
||||
MANUAL = "MANUAL_OWNER_REVIEW_REQUIRED"
|
||||
|
||||
NEXT = {STATES[i]: STATES[i + 1] for i in range(len(STATES) - 1)}
|
||||
|
||||
MANUAL_REASONS = frozenset({
|
||||
"formal_same_name_tag_conflict",
|
||||
"force_push_required",
|
||||
"published_history_rewrite_required",
|
||||
"non_fast_forward_main_divergence",
|
||||
"private_signing_material_exposure_required",
|
||||
"repository_identity_conflict",
|
||||
"campaign_revoked",
|
||||
"operation_outside_campaign_scope",
|
||||
})
|
||||
|
||||
|
||||
class OrchestratorError(RuntimeError):
|
||||
pass
|
||||
|
||||
|
||||
@dataclass
|
||||
class ReleaseState:
|
||||
version: str
|
||||
state: str = "DEVELOPMENT"
|
||||
last_stable_state: str = "DEVELOPMENT"
|
||||
recoverable_reason: str | None = None
|
||||
|
||||
def advance(self) -> None:
|
||||
if self.state not in NEXT:
|
||||
raise OrchestratorError(f"cannot_advance:{self.state}")
|
||||
self.last_stable_state = self.state
|
||||
self.state = NEXT[self.state]
|
||||
self.recoverable_reason = None
|
||||
|
||||
def recoverable_failure(self, reason: str) -> None:
|
||||
self.last_stable_state = self.state
|
||||
self.state = RECOVERABLE
|
||||
self.recoverable_reason = reason
|
||||
|
||||
def reconciliation_required(self, reason: str) -> None:
|
||||
self.last_stable_state = self.state
|
||||
self.state = RECONCILIATION
|
||||
self.recoverable_reason = reason
|
||||
|
||||
def manual_review(self, reason: str) -> None:
|
||||
if reason not in MANUAL_REASONS:
|
||||
raise OrchestratorError("reason_not_manual_red_line")
|
||||
self.last_stable_state = self.state
|
||||
self.state = MANUAL
|
||||
self.recoverable_reason = reason
|
||||
|
||||
def resume(self) -> None:
|
||||
if self.state not in {RECOVERABLE, RECONCILIATION}:
|
||||
raise OrchestratorError("state_not_resumable")
|
||||
self.state = self.last_stable_state
|
||||
|
||||
def as_dict(self) -> dict[str, Any]:
|
||||
return {
|
||||
"schema": "aios.autonomous-release-state.v1",
|
||||
"version": self.version,
|
||||
"state": self.state,
|
||||
"last_stable_state": self.last_stable_state,
|
||||
"recoverable_reason": self.recoverable_reason,
|
||||
"owner_interaction_required": self.state == MANUAL,
|
||||
}
|
||||
|
||||
|
||||
def atomic_state(path: Path, state: ReleaseState) -> None:
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
fd, name = tempfile.mkstemp(prefix=f".{path.name}.", dir=path.parent)
|
||||
try:
|
||||
os.fchmod(fd, 0o600)
|
||||
with os.fdopen(fd, "w", encoding="utf-8", closefd=True) as stream:
|
||||
json.dump(state.as_dict(), stream, sort_keys=True, indent=2)
|
||||
stream.write("\n")
|
||||
stream.flush()
|
||||
os.fsync(stream.fileno())
|
||||
os.replace(name, path)
|
||||
finally:
|
||||
try:
|
||||
os.unlink(name)
|
||||
except FileNotFoundError:
|
||||
pass
|
||||
+410
@@ -0,0 +1,410 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Atomic Registry/Baseline finalizer for an already ACTIVE exact release.
|
||||
|
||||
This adapter never writes Git refs. It is intended for the AIOS runtime
|
||||
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 V2R1C7 Registry record
|
||||
- exact previous Baseline file SHA-256 (CAS)
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import stat
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
import time
|
||||
import urllib.error
|
||||
import urllib.request
|
||||
from pathlib import Path
|
||||
from typing import Any, Mapping
|
||||
|
||||
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}$")
|
||||
|
||||
|
||||
def sha256_file(path: Path) -> str:
|
||||
digest = hashlib.sha256()
|
||||
with path.open("rb") as source:
|
||||
for block in iter(lambda: source.read(1024 * 1024), b""):
|
||||
digest.update(block)
|
||||
return digest.hexdigest()
|
||||
|
||||
|
||||
def atomic_json(path: Path, value: Mapping[str, Any], mode: int = 0o640) -> None:
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
# Existing runtime-authority files may be root:gitops and read-only to
|
||||
# non-root readers. Preserve their owner/group/mode across os.replace().
|
||||
if path.exists():
|
||||
metadata = path.lstat()
|
||||
if not stat.S_ISREG(metadata.st_mode) or path.is_symlink():
|
||||
raise RuntimeError("ATOMIC_TARGET_NOT_REGULAR")
|
||||
target_mode = stat.S_IMODE(metadata.st_mode)
|
||||
target_uid = metadata.st_uid
|
||||
target_gid = metadata.st_gid
|
||||
else:
|
||||
parent = path.parent.stat()
|
||||
target_mode = mode
|
||||
target_uid = 0 if os.geteuid() == 0 else os.geteuid()
|
||||
target_gid = parent.st_gid
|
||||
|
||||
fd, name = tempfile.mkstemp(prefix=f".{path.name}.", dir=str(path.parent))
|
||||
try:
|
||||
with os.fdopen(fd, "w", encoding="utf-8") as output:
|
||||
json.dump(dict(value), output, sort_keys=True, indent=2)
|
||||
output.write("\n")
|
||||
output.flush()
|
||||
os.fsync(output.fileno())
|
||||
os.chmod(name, target_mode)
|
||||
if os.geteuid() == 0:
|
||||
os.chown(name, target_uid, target_gid)
|
||||
os.replace(name, path)
|
||||
dfd = os.open(path.parent, os.O_RDONLY | os.O_DIRECTORY)
|
||||
try:
|
||||
os.fsync(dfd)
|
||||
finally:
|
||||
os.close(dfd)
|
||||
finally:
|
||||
try:
|
||||
os.unlink(name)
|
||||
except FileNotFoundError:
|
||||
pass
|
||||
|
||||
|
||||
def load_request() -> dict[str, Any]:
|
||||
value = json.load(sys.stdin)
|
||||
if not isinstance(value, dict) or value.get("schema") != "aios.production-executor-request.v1":
|
||||
raise ValueError("request_schema_invalid")
|
||||
if value.get("repository_identity") != "aios/aios":
|
||||
raise ValueError("repository_identity_invalid")
|
||||
if not re.fullmatch(r"V\d+R\d+C\d+(?:-[A-Za-z0-9_.-]+)?", str(value.get("version", ""))):
|
||||
raise ValueError("version_invalid")
|
||||
for name in ("target_oid", "evidence_closure_oid"):
|
||||
if not OID_RE.fullmatch(str(value.get(name, ""))):
|
||||
raise ValueError(f"{name}_invalid")
|
||||
if not SHA_RE.fullmatch(str(value.get("artifact_sha256", ""))):
|
||||
raise ValueError("artifact_sha256_invalid")
|
||||
if not PLAN_RE.fullmatch(str(value.get("approval_plan_id", ""))):
|
||||
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")
|
||||
return value
|
||||
|
||||
|
||||
def exact_response(value: Mapping[str, Any], status: str, **extra: Any) -> dict[str, Any]:
|
||||
return {
|
||||
"status": status,
|
||||
"version": value["version"],
|
||||
"target_oid": value["target_oid"],
|
||||
"evidence_closure_oid": value["evidence_closure_oid"],
|
||||
"artifact_sha256": value["artifact_sha256"],
|
||||
"approval_plan_id": value["approval_plan_id"],
|
||||
**extra,
|
||||
}
|
||||
|
||||
|
||||
def git(repository: Path, *args: str, check: bool = True) -> subprocess.CompletedProcess[str]:
|
||||
return subprocess.run(
|
||||
["git", f"--git-dir={repository}", *args],
|
||||
check=check,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=30,
|
||||
env={"PATH": os.environ.get("PATH", "")},
|
||||
)
|
||||
|
||||
|
||||
def verify_local_tag(repository: Path, value: Mapping[str, Any]) -> tuple[str, str]:
|
||||
tag_oid = git(repository, "rev-parse", "--verify", f"refs/tags/{value['version']}").stdout.strip()
|
||||
peeled = git(repository, "rev-parse", "--verify", f"refs/tags/{value['version']}^{{commit}}").stdout.strip()
|
||||
if tag_oid == peeled or peeled != value["target_oid"]:
|
||||
raise RuntimeError("FORMAL_TAG_BINDING_MISMATCH")
|
||||
requested = value.get("tag_oid")
|
||||
if requested and requested != tag_oid:
|
||||
raise RuntimeError("FORMAL_TAG_OBJECT_MISMATCH")
|
||||
return tag_oid, peeled
|
||||
|
||||
|
||||
def health(url: str, value: Mapping[str, Any]) -> dict[str, Any]:
|
||||
try:
|
||||
with urllib.request.urlopen(url, timeout=5) as response:
|
||||
body = json.loads(response.read().decode("utf-8"))
|
||||
status = response.status
|
||||
except (urllib.error.URLError, TimeoutError, ValueError) as exc:
|
||||
raise RuntimeError("PRODUCTION_HEALTH_UNAVAILABLE") from exc
|
||||
if not (
|
||||
status == 200
|
||||
and body.get("version") == value["version"]
|
||||
and body.get("commit_oid") == value["target_oid"]
|
||||
and body.get("artifact_sha256") == value["artifact_sha256"]
|
||||
and body.get("deployment_state") == "ACTIVE"
|
||||
and body.get("production_deployed") is True
|
||||
):
|
||||
raise RuntimeError("PRODUCTION_HEALTH_BINDING_MISMATCH")
|
||||
return body
|
||||
|
||||
|
||||
def registry_record(value: Mapping[str, Any], tag_oid: str, production_health: Mapping[str, Any]) -> dict[str, Any]:
|
||||
return {
|
||||
"schema": "aios.production-registry.v2",
|
||||
"version": value["version"],
|
||||
"repository": "aios/aios",
|
||||
"target_oid": value["target_oid"],
|
||||
"tag": value["version"],
|
||||
"tag_oid": tag_oid,
|
||||
"tag_object": tag_oid,
|
||||
"peeled_target_oid": value["target_oid"],
|
||||
"evidence_closure_oid": value["evidence_closure_oid"],
|
||||
"artifact_sha256": value["artifact_sha256"],
|
||||
"approval_plan_id": value["approval_plan_id"],
|
||||
"production_service": value["production_service"],
|
||||
"production_port": value["production_port"],
|
||||
"registered": True,
|
||||
"enabled": True,
|
||||
"production_deployed": True,
|
||||
"status": "PRODUCTION_DEPLOYED",
|
||||
"deployment_state": production_health["deployment_state"],
|
||||
"updated_at": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()),
|
||||
}
|
||||
|
||||
|
||||
def registry_matches(record: Mapping[str, Any], value: Mapping[str, Any], tag_oid: str) -> bool:
|
||||
return all(
|
||||
record.get(key) == expected
|
||||
for key, expected in {
|
||||
"version": value["version"],
|
||||
"target_oid": value["target_oid"],
|
||||
"tag_oid": tag_oid,
|
||||
"evidence_closure_oid": value["evidence_closure_oid"],
|
||||
"artifact_sha256": value["artifact_sha256"],
|
||||
"approval_plan_id": value["approval_plan_id"],
|
||||
"production_service": value["production_service"],
|
||||
"production_port": value["production_port"],
|
||||
"production_deployed": True,
|
||||
}.items()
|
||||
) and record.get("registered") is True and record.get("enabled") is True
|
||||
|
||||
|
||||
def baseline_matches(record: Mapping[str, Any], value: Mapping[str, Any], tag_oid: str) -> bool:
|
||||
current = record.get("current_baseline") or record.get("version")
|
||||
return current == value["version"] and all(
|
||||
record.get(key) == expected
|
||||
for key, expected in {
|
||||
"target_oid": value["target_oid"],
|
||||
"tag_oid": tag_oid,
|
||||
"evidence_closure_oid": value["evidence_closure_oid"],
|
||||
"artifact_sha256": value["artifact_sha256"],
|
||||
"approval_plan_id": value["approval_plan_id"],
|
||||
"production_service": value["production_service"],
|
||||
"production_port": value["production_port"],
|
||||
"status": "PRODUCTION_DEPLOYED",
|
||||
}.items()
|
||||
)
|
||||
|
||||
|
||||
def registry_probe(args: argparse.Namespace, value: Mapping[str, Any]) -> dict[str, Any]:
|
||||
tag_oid, _ = verify_local_tag(args.repository, value)
|
||||
path = args.registry_root / f"{value['version']}.json"
|
||||
if not path.exists():
|
||||
return exact_response(value, "NOT_APPLIED", tag_oid=tag_oid)
|
||||
record = json.loads(path.read_text(encoding="utf-8"))
|
||||
if registry_matches(record, value, tag_oid):
|
||||
return exact_response(
|
||||
value,
|
||||
"VERIFIED",
|
||||
tag_oid=tag_oid,
|
||||
registry_path=str(path),
|
||||
registry_sha256=sha256_file(path),
|
||||
)
|
||||
return exact_response(value, "CONFLICT", tag_oid=tag_oid, reason="registry_binding_mismatch")
|
||||
|
||||
|
||||
def registry_apply(args: argparse.Namespace, value: Mapping[str, Any]) -> dict[str, Any]:
|
||||
existing = registry_probe(args, value)
|
||||
if existing["status"] in {"VERIFIED", "CONFLICT"}:
|
||||
return existing
|
||||
tag_oid, _ = verify_local_tag(args.repository, value)
|
||||
production_health = health(args.health_url, value)
|
||||
path = args.registry_root / f"{value['version']}.json"
|
||||
atomic_json(path, registry_record(value, tag_oid, production_health))
|
||||
return registry_probe(args, value)
|
||||
|
||||
|
||||
def secure_baseline_authority_file(path: Path) -> None:
|
||||
if not path.is_file() or path.is_symlink():
|
||||
raise RuntimeError("BASELINE_AUTHORITY_FILE_INVALID")
|
||||
baseline_stat = path.lstat()
|
||||
if baseline_stat.st_uid != 0 or baseline_stat.st_mode & 0o022:
|
||||
raise RuntimeError("BASELINE_AUTHORITY_PERMISSIONS_INVALID")
|
||||
|
||||
|
||||
def baseline_probe(args: argparse.Namespace, value: Mapping[str, Any]) -> dict[str, Any]:
|
||||
tag_oid, _ = verify_local_tag(args.repository, value)
|
||||
secure_baseline_authority_file(args.baseline_path)
|
||||
record = json.loads(args.baseline_path.read_text(encoding="utf-8"))
|
||||
if baseline_matches(record, value, tag_oid):
|
||||
return exact_response(
|
||||
value,
|
||||
"VERIFIED",
|
||||
tag_oid=tag_oid,
|
||||
baseline_path=str(args.baseline_path),
|
||||
baseline_sha256=sha256_file(args.baseline_path),
|
||||
)
|
||||
current = record.get("current_baseline") or record.get("version")
|
||||
current_sha = sha256_file(args.baseline_path)
|
||||
if (
|
||||
current == args.expected_previous_baseline
|
||||
and current_sha == args.expected_previous_baseline_sha256
|
||||
):
|
||||
return exact_response(
|
||||
value,
|
||||
"NOT_APPLIED",
|
||||
tag_oid=tag_oid,
|
||||
previous_baseline=current,
|
||||
previous_baseline_sha256=current_sha,
|
||||
)
|
||||
return exact_response(
|
||||
value,
|
||||
"CONFLICT",
|
||||
tag_oid=tag_oid,
|
||||
reason="baseline_precondition_or_binding_mismatch",
|
||||
observed_baseline=current,
|
||||
observed_sha256=current_sha,
|
||||
)
|
||||
|
||||
|
||||
def baseline_apply(args: argparse.Namespace, value: Mapping[str, Any]) -> dict[str, Any]:
|
||||
existing = baseline_probe(args, value)
|
||||
if existing["status"] in {"VERIFIED", "CONFLICT"}:
|
||||
return existing
|
||||
|
||||
tag_oid, _ = verify_local_tag(args.repository, value)
|
||||
production_health = health(args.health_url, value)
|
||||
|
||||
registry_path = args.registry_root / f"{value['version']}.json"
|
||||
if not registry_path.is_file():
|
||||
raise RuntimeError("REGISTRY_NOT_FINALIZED")
|
||||
registry = json.loads(registry_path.read_text(encoding="utf-8"))
|
||||
if not registry_matches(registry, value, tag_oid):
|
||||
raise RuntimeError("REGISTRY_BINDING_NOT_VERIFIED")
|
||||
|
||||
previous = json.loads(args.baseline_path.read_text(encoding="utf-8"))
|
||||
if sha256_file(args.baseline_path) != args.expected_previous_baseline_sha256:
|
||||
raise RuntimeError("BASELINE_CAS_MISMATCH")
|
||||
|
||||
generation = max(
|
||||
int(previous.get("generation", 0) or 0),
|
||||
int(previous.get("authority_generation", 0) or 0),
|
||||
) + 1
|
||||
record = {
|
||||
"schema": "aios.production-baseline.v2",
|
||||
"source_identity": "aios-gitops-runtime-anchor-baseline-authority-v1",
|
||||
"current_baseline": value["version"],
|
||||
"version": value["version"],
|
||||
"previous_baseline": args.expected_previous_baseline,
|
||||
"previous_version": args.expected_previous_baseline,
|
||||
"status": "PRODUCTION_DEPLOYED",
|
||||
"target_oid": value["target_oid"],
|
||||
"peeled_target_oid": value["target_oid"],
|
||||
"tag_oid": tag_oid,
|
||||
"evidence_closure_oid": value["evidence_closure_oid"],
|
||||
"approval_plan_id": value["approval_plan_id"],
|
||||
"artifact_sha256": value["artifact_sha256"],
|
||||
"production_artifact_sha256": value["artifact_sha256"],
|
||||
"production_service": value["production_service"],
|
||||
"production_port": value["production_port"],
|
||||
"production_deployed": True,
|
||||
"deployment_state": production_health["deployment_state"],
|
||||
"registry_digest": sha256_file(registry_path),
|
||||
"registry_source": str(registry_path),
|
||||
"generation": generation,
|
||||
"authority_generation": generation,
|
||||
# The actual previously deployed production baseline is the rollback
|
||||
# target. V2R1C6 has a formal tag but was never production-deployed.
|
||||
"rollback_version": args.expected_previous_baseline,
|
||||
"rollback_tag_oid": previous.get("tag_oid"),
|
||||
"rollback_target_oid": previous.get("target_oid"),
|
||||
"promoted_at": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()),
|
||||
"promoted_by": "aios-approved-production-release-executor",
|
||||
}
|
||||
atomic_json(args.baseline_path, record)
|
||||
return baseline_probe(args, value)
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument(
|
||||
"--mode",
|
||||
choices=("registry", "baseline"),
|
||||
default=os.environ.get("AIOS_RELEASE_METADATA_MODE"),
|
||||
required=os.environ.get("AIOS_RELEASE_METADATA_MODE") is None,
|
||||
)
|
||||
parser.add_argument(
|
||||
"--repository",
|
||||
type=Path,
|
||||
default=Path(os.environ["AIOS_GITOPS_AUTHORITATIVE_GIT_DIR"]),
|
||||
)
|
||||
parser.add_argument(
|
||||
"--registry-root",
|
||||
type=Path,
|
||||
default=Path(os.environ["AIOS_RUNTIME_REGISTRY_ROOT"]),
|
||||
)
|
||||
parser.add_argument(
|
||||
"--baseline-path",
|
||||
type=Path,
|
||||
default=Path(os.environ["AIOS_RUNTIME_BASELINE_PATH"]),
|
||||
)
|
||||
parser.add_argument(
|
||||
"--expected-previous-baseline",
|
||||
default=os.environ.get("AIOS_PREVIOUS_BASELINE", "V2R1C5"),
|
||||
)
|
||||
parser.add_argument(
|
||||
"--expected-previous-baseline-sha256",
|
||||
default=os.environ.get("AIOS_PREVIOUS_BASELINE_SHA256"),
|
||||
required=os.environ.get("AIOS_PREVIOUS_BASELINE_SHA256") is None,
|
||||
)
|
||||
parser.add_argument(
|
||||
"--health-url",
|
||||
default=os.environ.get("AIOS_RELEASE_HEALTH_URL", "http://127.0.0.1:18927/health"),
|
||||
)
|
||||
args = parser.parse_args()
|
||||
value = load_request()
|
||||
operation = value.get("operation")
|
||||
if args.mode == "registry":
|
||||
if operation == "probe":
|
||||
result = registry_probe(args, value)
|
||||
elif operation == "apply":
|
||||
result = registry_apply(args, value)
|
||||
else:
|
||||
raise ValueError("unsupported_operation")
|
||||
else:
|
||||
if operation == "probe":
|
||||
result = baseline_probe(args, value)
|
||||
elif operation == "apply":
|
||||
result = baseline_apply(args, value)
|
||||
else:
|
||||
raise ValueError("unsupported_operation")
|
||||
print(json.dumps(result, sort_keys=True))
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
try:
|
||||
raise SystemExit(main())
|
||||
except Exception as exc:
|
||||
print(
|
||||
json.dumps(
|
||||
{"status": "FAIL_CLOSED", "error": type(exc).__name__, "reason": str(exc)},
|
||||
sort_keys=True,
|
||||
)
|
||||
)
|
||||
raise SystemExit(2)
|
||||
@@ -1,9 +1,4 @@
|
||||
"""Privacy and artifact helpers for the V2R1C6 release candidate.
|
||||
|
||||
The module is deliberately dependency-free. It provides deterministic redaction,
|
||||
safe SHA-256 manifest verification, and a small loopback-only candidate probe.
|
||||
It does not read credentials, contact production services, or mutate Git refs.
|
||||
"""
|
||||
"""Privacy, artifact, and runtime health helpers for V2R1C7."""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
@@ -14,152 +9,76 @@ from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
||||
from pathlib import Path
|
||||
from typing import Any, Mapping
|
||||
|
||||
_SECRET_KEY = re.compile(r"(?i)(?:token|secret|password|private[_-]?key|api[_-]?key)")
|
||||
_SECRET_VALUE = re.compile(
|
||||
r"(?i)(?:bearer\s+)[A-Za-z0-9._~+/=-]+|"
|
||||
r"(?:token|secret|password|private[_-]?key|api[_-]?key)\s*[:=]\s*[^\s,;]+"
|
||||
)
|
||||
from deployment_state import RuntimeDeploymentState
|
||||
|
||||
_SECRET_KEY = re.compile(r"(?i)(?:token|secret|password|private[_-]?key|api[_-]?key)")
|
||||
_SECRET_VALUE = re.compile(r"(?i)(?:bearer\s+)[A-Za-z0-9._~+/=-]+|(?:token|secret|password|private[_-]?key|api[_-]?key)\s*[:=]\s*[^\s,;]+")
|
||||
|
||||
def redact_text(value: str) -> str:
|
||||
"""Replace common bearer and key/value secret forms without logging them."""
|
||||
if not isinstance(value, str):
|
||||
raise TypeError("text_required")
|
||||
if not isinstance(value, str): raise TypeError("text_required")
|
||||
return _SECRET_VALUE.sub("[REDACTED]", value)
|
||||
|
||||
|
||||
def redact_value(value: Any, *, key: str = "") -> Any:
|
||||
"""Recursively redact secret-bearing mapping keys and text values."""
|
||||
if isinstance(value, Mapping):
|
||||
result: dict[str, Any] = {}
|
||||
for name, item in value.items():
|
||||
name_text = str(name)
|
||||
if _SECRET_KEY.search(name_text):
|
||||
result[name_text] = "[REDACTED]"
|
||||
else:
|
||||
result[name_text] = redact_value(item, key=name_text)
|
||||
return result
|
||||
if isinstance(value, list):
|
||||
return [redact_value(item, key=key) for item in value]
|
||||
if isinstance(value, tuple):
|
||||
return [redact_value(item, key=key) for item in value]
|
||||
if isinstance(value, str):
|
||||
return "[REDACTED]" if _SECRET_KEY.search(key) else redact_text(value)
|
||||
return {str(k): ("[REDACTED]" if _SECRET_KEY.search(str(k)) else redact_value(v, key=str(k))) for k,v in value.items()}
|
||||
if isinstance(value, list): return [redact_value(v, key=key) for v in value]
|
||||
if isinstance(value, tuple): return [redact_value(v, key=key) for v in value]
|
||||
if isinstance(value, str): return "[REDACTED]" if _SECRET_KEY.search(key) else redact_text(value)
|
||||
return value
|
||||
|
||||
|
||||
def sha256_file(path: Path) -> str:
|
||||
"""Return the SHA-256 of a regular file without following symlink inputs."""
|
||||
if path.is_symlink() or not path.is_file():
|
||||
raise ValueError("regular_file_required")
|
||||
digest = hashlib.sha256()
|
||||
if path.is_symlink() or not path.is_file(): raise ValueError("regular_file_required")
|
||||
digest=hashlib.sha256()
|
||||
with path.open("rb") as stream:
|
||||
for chunk in iter(lambda: stream.read(1024 * 1024), b""):
|
||||
digest.update(chunk)
|
||||
for chunk in iter(lambda: stream.read(1024*1024), b""): digest.update(chunk)
|
||||
return digest.hexdigest()
|
||||
|
||||
|
||||
def _safe_relative(root: Path, name: str) -> Path:
|
||||
relative = Path(name)
|
||||
if relative.is_absolute() or ".." in relative.parts or name in {"", "."}:
|
||||
raise ValueError("unsafe_manifest_path")
|
||||
candidate = (root / relative).resolve()
|
||||
candidate.relative_to(root.resolve())
|
||||
return candidate
|
||||
|
||||
relative=Path(name)
|
||||
if relative.is_absolute() or ".." in relative.parts or name in {"", "."}: raise ValueError("unsafe_manifest_path")
|
||||
candidate=(root/relative).resolve(); candidate.relative_to(root.resolve()); return candidate
|
||||
|
||||
def verify_manifest(root: Path, manifest: Path | None = None) -> dict[str, Any]:
|
||||
"""Verify sha256 path entries and return a non-sensitive summary."""
|
||||
root = root.resolve()
|
||||
manifest = manifest or root / "artifacts" / "MANIFEST.sha256"
|
||||
if manifest.is_symlink() or not manifest.is_file():
|
||||
raise ValueError("manifest_missing")
|
||||
checked = 0
|
||||
root=root.resolve(); manifest=manifest or root/"artifacts"/"MANIFEST.sha256"
|
||||
if manifest.is_symlink() or not manifest.is_file(): raise ValueError("manifest_missing")
|
||||
checked=0
|
||||
for raw in manifest.read_text(encoding="utf-8").splitlines():
|
||||
line = raw.strip()
|
||||
if not line:
|
||||
continue
|
||||
parts = line.split(maxsplit=1)
|
||||
if len(parts) != 2 or not re.fullmatch(r"[0-9a-f]{64}", parts[0]):
|
||||
raise ValueError("manifest_entry_invalid")
|
||||
expected, name = parts
|
||||
if name.startswith("*"):
|
||||
name = name[1:]
|
||||
path = _safe_relative(root, name)
|
||||
if sha256_file(path) != expected:
|
||||
raise ValueError("manifest_hash_mismatch")
|
||||
checked += 1
|
||||
if checked == 0:
|
||||
raise ValueError("manifest_empty")
|
||||
return {"status": "PASS", "entries": checked}
|
||||
|
||||
line=raw.strip()
|
||||
if not line: continue
|
||||
parts=line.split(maxsplit=1)
|
||||
if len(parts)!=2 or not re.fullmatch(r"[0-9a-f]{64}", parts[0]): raise ValueError("manifest_entry_invalid")
|
||||
expected,name=parts; name=name[1:] if name.startswith("*") else name
|
||||
if sha256_file(_safe_relative(root,name))!=expected: raise ValueError("manifest_hash_mismatch")
|
||||
checked+=1
|
||||
if not checked: raise ValueError("manifest_empty")
|
||||
return {"status":"PASS","entries":checked}
|
||||
|
||||
def load_release_metadata(path: Path) -> dict[str, Any]:
|
||||
metadata = json.loads(path.read_text(encoding="utf-8"))
|
||||
if metadata.get("version") != "V2R1C6":
|
||||
raise ValueError("version_mismatch")
|
||||
if metadata.get("production_deployed") is not False:
|
||||
raise ValueError("production_change_not_allowed")
|
||||
if metadata.get("tag_created") is not False:
|
||||
raise ValueError("tag_change_not_allowed")
|
||||
if metadata.get("baseline_changed") is not False:
|
||||
raise ValueError("baseline_change_not_allowed")
|
||||
metadata=json.loads(path.read_text(encoding="utf-8"))
|
||||
if metadata.get("version") not in {"V2R1C6","V2R1C7"}: raise ValueError("version_mismatch")
|
||||
if any(metadata.get(k) is not False for k in ("production_deployed","tag_created","baseline_changed")): raise ValueError("production_change_not_allowed")
|
||||
return metadata
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--root", type=Path, default=Path("."))
|
||||
parser.add_argument("--manifest", type=Path)
|
||||
parser.add_argument("--serve", action="store_true")
|
||||
parser.add_argument("--host", default="127.0.0.1")
|
||||
parser.add_argument("--port", type=int, default=18926)
|
||||
args = parser.parse_args()
|
||||
if args.serve:
|
||||
serve(args.host, args.port)
|
||||
return 0
|
||||
result = verify_manifest(args.root, args.manifest)
|
||||
print(json.dumps(result, sort_keys=True))
|
||||
return 0
|
||||
|
||||
|
||||
|
||||
def health_payload() -> dict[str, Any]:
|
||||
return {
|
||||
"status": "ok",
|
||||
"version": "V2R1C6",
|
||||
"production_deployed": False,
|
||||
"gitops_writes_enabled": False,
|
||||
}
|
||||
|
||||
def health_payload(state_provider: RuntimeDeploymentState | None = None) -> dict[str, Any]:
|
||||
return (state_provider or RuntimeDeploymentState.from_environment()).health_payload()
|
||||
|
||||
class _HealthHandler(BaseHTTPRequestHandler):
|
||||
def do_GET(self) -> None:
|
||||
if self.path not in {"/", "/health", "/healthz"}:
|
||||
self.send_error(404)
|
||||
return
|
||||
body = json.dumps(health_payload(), sort_keys=True).encode("utf-8")
|
||||
self.send_response(200)
|
||||
self.send_header("Content-Type", "application/json")
|
||||
self.send_header("Content-Length", str(len(body)))
|
||||
self.end_headers()
|
||||
self.wfile.write(body)
|
||||
|
||||
def log_message(self, _format: str, *_args: object) -> None:
|
||||
return
|
||||
|
||||
if self.path not in {"/","/health","/healthz"}: self.send_error(404); return
|
||||
body=json.dumps(health_payload(), sort_keys=True).encode()
|
||||
self.send_response(200); self.send_header("Content-Type","application/json"); self.send_header("Content-Length",str(len(body))); self.end_headers(); self.wfile.write(body)
|
||||
def log_message(self, _format: str, *_args: object) -> None: return
|
||||
|
||||
def serve(host: str, port: int) -> None:
|
||||
if host not in {"127.0.0.1", "::1", "localhost"}:
|
||||
raise ValueError("loopback_only_listener_required")
|
||||
if not 1024 <= port <= 65535:
|
||||
raise ValueError("invalid_listener_port")
|
||||
server = ThreadingHTTPServer((host, port), _HealthHandler)
|
||||
try:
|
||||
server.serve_forever()
|
||||
finally:
|
||||
server.server_close()
|
||||
if host not in {"127.0.0.1","::1","localhost"}: raise ValueError("loopback_only_listener_required")
|
||||
if not 1024 <= port <= 65535: raise ValueError("invalid_listener_port")
|
||||
server=ThreadingHTTPServer((host,port),_HealthHandler)
|
||||
try: server.serve_forever()
|
||||
finally: server.server_close()
|
||||
|
||||
def main() -> int:
|
||||
parser=argparse.ArgumentParser(); parser.add_argument("--root",type=Path,default=Path(".")); parser.add_argument("--manifest",type=Path); parser.add_argument("--serve",action="store_true"); parser.add_argument("--host",default="127.0.0.1"); parser.add_argument("--port",type=int,default=18927); args=parser.parse_args()
|
||||
if args.serve: serve(args.host,args.port); return 0
|
||||
print(json.dumps(verify_manifest(args.root,args.manifest),sort_keys=True)); return 0
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
if __name__ == "__main__": raise SystemExit(main())
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
"""V2R1C7 production health entry point; state is install/runtime supplied."""
|
||||
from v2r1c6_privacy_tools import main
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
+455
@@ -0,0 +1,455 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Concrete V2R1C7 production deployment adapter.
|
||||
|
||||
The adapter consumes only an already approved immutable artifact. It never
|
||||
writes Git refs, Registry, or Baseline. A rollback only tears down the V2R1C7
|
||||
runtime and leaves V2R1C5/V2R1C6 untouched.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import grp
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
import pwd
|
||||
import shutil
|
||||
import stat
|
||||
import subprocess
|
||||
import sys
|
||||
import tarfile
|
||||
import tempfile
|
||||
import time
|
||||
import urllib.error
|
||||
import urllib.request
|
||||
from pathlib import Path
|
||||
from typing import Any, Mapping
|
||||
|
||||
VERSION = "V2R1C7"
|
||||
SERVICE = "aios-v2r1c7-production.service"
|
||||
PORT = 18927
|
||||
RUNTIME_USER = "aios-v2r1c7"
|
||||
RUNTIME_GROUP = "aios-v2r1c7"
|
||||
|
||||
REQUIRED_MEMBERS = {
|
||||
"payload/VERSION",
|
||||
"payload/active-runtime/app/deployment_state.py",
|
||||
"payload/active-runtime/app/v2r1c6_privacy_tools.py",
|
||||
"payload/active-runtime/app/v2r1c7_health_service.py",
|
||||
"payload/deploy/aios-v2/systemd/aios-v2r1c7-production.service",
|
||||
"payload/deploy/aios-v2/v2r1c7-runtime-config.json",
|
||||
"payload/deploy/aios-v2/v2r1c7-rollback.json",
|
||||
"payload/scripts/probe_v2r1c7_health.py",
|
||||
"payload/scripts/rollback_v2r1c7_release.sh",
|
||||
"payload/BUILD-METADATA.json",
|
||||
"payload/ARTIFACT-MANIFEST.json",
|
||||
"payload/ROLLBACK-METADATA.json",
|
||||
}
|
||||
|
||||
|
||||
def load_request() -> dict[str, Any]:
|
||||
value = json.load(sys.stdin)
|
||||
if not isinstance(value, dict):
|
||||
raise ValueError("request_object_required")
|
||||
if value.get("schema") != "aios.production-executor-request.v1":
|
||||
raise ValueError("request_schema_invalid")
|
||||
if value.get("version") != VERSION:
|
||||
raise ValueError("version_mismatch")
|
||||
if value.get("production_service") != SERVICE:
|
||||
raise ValueError("service_mismatch")
|
||||
if int(value.get("production_port", -1)) != PORT:
|
||||
raise ValueError("port_mismatch")
|
||||
return value
|
||||
|
||||
|
||||
def sha256_file(path: Path) -> str:
|
||||
if path.is_symlink() or not path.is_file():
|
||||
raise ValueError("artifact_regular_file_required")
|
||||
digest = hashlib.sha256()
|
||||
with path.open("rb") as stream:
|
||||
for chunk in iter(lambda: stream.read(1024 * 1024), b""):
|
||||
digest.update(chunk)
|
||||
return digest.hexdigest()
|
||||
|
||||
|
||||
def atomic_json(path: Path, value: Mapping[str, Any], *, mode: int, gid: int | None = None) -> None:
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
temporary = path.with_name(path.name + ".tmp")
|
||||
temporary.write_text(
|
||||
json.dumps(dict(value), sort_keys=True, separators=(",", ":")) + "\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
os.chmod(temporary, mode)
|
||||
if gid is not None:
|
||||
os.chown(temporary, 0, gid)
|
||||
os.replace(temporary, path)
|
||||
|
||||
|
||||
def atomic_text(path: Path, text: str, *, mode: int, gid: int | None = None) -> None:
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
temporary = path.with_name(path.name + ".tmp")
|
||||
temporary.write_text(text, encoding="utf-8")
|
||||
os.chmod(temporary, mode)
|
||||
if gid is not None:
|
||||
os.chown(temporary, 0, gid)
|
||||
os.replace(temporary, path)
|
||||
|
||||
|
||||
def systemctl(*args: str, check: bool = True) -> subprocess.CompletedProcess[str]:
|
||||
return subprocess.run(
|
||||
["systemctl", *args],
|
||||
check=check,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=45,
|
||||
env={"PATH": os.environ.get("PATH", "")},
|
||||
)
|
||||
|
||||
|
||||
def identity() -> tuple[int, int]:
|
||||
try:
|
||||
user = pwd.getpwnam(RUNTIME_USER)
|
||||
group = grp.getgrnam(RUNTIME_GROUP)
|
||||
except KeyError as exc:
|
||||
raise RuntimeError("RUNTIME_IDENTITY_MISSING") from exc
|
||||
if user.pw_gid != group.gr_gid:
|
||||
raise RuntimeError("RUNTIME_IDENTITY_GROUP_MISMATCH")
|
||||
return user.pw_uid, group.gr_gid
|
||||
|
||||
|
||||
def safe_extract(artifact: Path, destination: Path) -> set[str]:
|
||||
with tempfile.TemporaryDirectory(prefix="aios-v2r1c7-unpack-") as temp:
|
||||
tar_path = Path(temp) / "release.tar"
|
||||
with tar_path.open("wb") as output:
|
||||
completed = subprocess.run(
|
||||
["zstd", "-dc", str(artifact)],
|
||||
check=False,
|
||||
stdout=output,
|
||||
stderr=subprocess.PIPE,
|
||||
timeout=60,
|
||||
env={"PATH": os.environ.get("PATH", "")},
|
||||
)
|
||||
if completed.returncode != 0:
|
||||
raise RuntimeError("ARTIFACT_DECOMPRESSION_FAILED")
|
||||
with tarfile.open(tar_path, "r:") as archive:
|
||||
members = archive.getmembers()
|
||||
names = {m.name for m in members}
|
||||
missing = REQUIRED_MEMBERS - names
|
||||
if missing:
|
||||
raise RuntimeError("ARTIFACT_MEMBERS_MISSING:" + ",".join(sorted(missing)))
|
||||
for member in members:
|
||||
relative = Path(member.name)
|
||||
if (
|
||||
relative.is_absolute()
|
||||
or ".." in relative.parts
|
||||
or member.issym()
|
||||
or member.islnk()
|
||||
or not (member.isfile() or member.isdir())
|
||||
):
|
||||
raise RuntimeError("UNSAFE_ARTIFACT_MEMBER:" + member.name)
|
||||
if not member.name.startswith("payload/"):
|
||||
raise RuntimeError("UNEXPECTED_ARTIFACT_ROOT:" + member.name)
|
||||
payload = destination
|
||||
payload.mkdir(parents=True, exist_ok=True)
|
||||
for member in members:
|
||||
rel = Path(member.name).relative_to("payload")
|
||||
if str(rel) == ".":
|
||||
continue
|
||||
target = payload / rel
|
||||
resolved_parent = target.parent.resolve()
|
||||
resolved_parent.relative_to(payload.resolve())
|
||||
if member.isdir():
|
||||
target.mkdir(parents=True, exist_ok=True)
|
||||
os.chmod(target, 0o755)
|
||||
continue
|
||||
target.parent.mkdir(parents=True, exist_ok=True)
|
||||
source = archive.extractfile(member)
|
||||
if source is None:
|
||||
raise RuntimeError("ARTIFACT_MEMBER_READ_FAILED")
|
||||
with target.open("wb") as output:
|
||||
shutil.copyfileobj(source, output)
|
||||
# Scripts must be executable; all other payload files are read-only.
|
||||
mode = 0o755 if rel.parts and rel.parts[0] == "scripts" else 0o644
|
||||
os.chmod(target, mode)
|
||||
return names
|
||||
|
||||
|
||||
def health(url: str, expected: Mapping[str, Any]) -> dict[str, Any]:
|
||||
try:
|
||||
with urllib.request.urlopen(url, timeout=5) as response:
|
||||
body = json.loads(response.read().decode("utf-8"))
|
||||
status = response.status
|
||||
except (urllib.error.URLError, TimeoutError, ValueError):
|
||||
return {"status": "UNAVAILABLE"}
|
||||
exact = (
|
||||
status == 200
|
||||
and body.get("version") == expected["version"]
|
||||
and body.get("commit_oid") == expected["target_oid"]
|
||||
and body.get("artifact_sha256") == expected["artifact_sha256"]
|
||||
and body.get("deployment_state") == "ACTIVE"
|
||||
and body.get("production_deployed") is True
|
||||
)
|
||||
if not exact:
|
||||
return {
|
||||
"status": "CONFLICT",
|
||||
"http_status": status,
|
||||
"version": body.get("version"),
|
||||
"target_oid": body.get("commit_oid"),
|
||||
"artifact_sha256": body.get("artifact_sha256"),
|
||||
"deployment_state": body.get("deployment_state"),
|
||||
"production_deployed": body.get("production_deployed"),
|
||||
}
|
||||
return {
|
||||
"status": "ACTIVE_VERIFIED",
|
||||
"http_status": 200,
|
||||
"version": body["version"],
|
||||
"target_oid": body["commit_oid"],
|
||||
"artifact_sha256": body["artifact_sha256"],
|
||||
"deployment_state": body["deployment_state"],
|
||||
"production_deployed": True,
|
||||
}
|
||||
|
||||
|
||||
def paths(args: argparse.Namespace, value: Mapping[str, Any]) -> dict[str, Path]:
|
||||
release_id = (
|
||||
f"{VERSION}-{str(value['target_oid'])[:12]}-"
|
||||
f"{str(value['artifact_sha256'])[:12]}"
|
||||
)
|
||||
return {
|
||||
"release_dir": args.release_root / release_id,
|
||||
"current_link": args.current_link,
|
||||
"env_file": args.env_file,
|
||||
"state_file": args.state_file,
|
||||
"unit_file": args.unit_file,
|
||||
}
|
||||
|
||||
|
||||
def probe(args: argparse.Namespace, value: Mapping[str, Any]) -> dict[str, Any]:
|
||||
p = paths(args, value)
|
||||
url = f"http://127.0.0.1:{PORT}/health"
|
||||
active = systemctl("is-active", "--quiet", SERVICE, check=False).returncode == 0
|
||||
if not active:
|
||||
# A stale exact release directory is safe to reuse; it is not a deployment.
|
||||
return {
|
||||
"status": "NOT_APPLIED",
|
||||
"version": VERSION,
|
||||
"target_oid": value["target_oid"],
|
||||
"artifact_sha256": value["artifact_sha256"],
|
||||
"production_deployed": False,
|
||||
"deployment_state": "ROLLED_BACK"
|
||||
if p["state_file"].exists()
|
||||
and "ROLLED_BACK" in p["state_file"].read_text(encoding="utf-8")
|
||||
else "ABSENT",
|
||||
}
|
||||
result = health(url, value)
|
||||
if result["status"] == "UNAVAILABLE":
|
||||
return {
|
||||
"status": "CONFLICT",
|
||||
"version": VERSION,
|
||||
"target_oid": value["target_oid"],
|
||||
"artifact_sha256": value["artifact_sha256"],
|
||||
"reason": "service_active_health_unavailable",
|
||||
}
|
||||
return result
|
||||
|
||||
|
||||
def install_release(args: argparse.Namespace, value: Mapping[str, Any]) -> dict[str, Any]:
|
||||
_, gid = identity()
|
||||
artifact = Path(str(value["artifact_path"]))
|
||||
if artifact.stat().st_size != int(value["artifact_size"]):
|
||||
raise RuntimeError("ARTIFACT_SIZE_MISMATCH")
|
||||
if sha256_file(artifact) != value["artifact_sha256"]:
|
||||
raise RuntimeError("ARTIFACT_DIGEST_MISMATCH")
|
||||
|
||||
p = paths(args, value)
|
||||
release_dir = p["release_dir"]
|
||||
marker = release_dir / "DEPLOYMENT-BINDING.json"
|
||||
marker_value = {
|
||||
"schema": "aios.runtime-release-binding.v1",
|
||||
"version": VERSION,
|
||||
"target_oid": value["target_oid"],
|
||||
"artifact_sha256": value["artifact_sha256"],
|
||||
"approval_plan_id": value["approval_plan_id"],
|
||||
}
|
||||
|
||||
if release_dir.exists():
|
||||
if not marker.is_file():
|
||||
raise RuntimeError("EXISTING_RELEASE_DIR_UNTRUSTED")
|
||||
current_marker = json.loads(marker.read_text(encoding="utf-8"))
|
||||
if current_marker != marker_value:
|
||||
raise RuntimeError("EXISTING_RELEASE_DIR_BINDING_CONFLICT")
|
||||
else:
|
||||
release_dir.parent.mkdir(parents=True, exist_ok=True)
|
||||
temp_dir = Path(
|
||||
tempfile.mkdtemp(prefix=release_dir.name + ".", dir=str(release_dir.parent))
|
||||
)
|
||||
try:
|
||||
safe_extract(artifact, temp_dir)
|
||||
atomic_json(
|
||||
temp_dir / "DEPLOYMENT-BINDING.json",
|
||||
marker_value,
|
||||
mode=0o644,
|
||||
)
|
||||
os.rename(temp_dir, release_dir)
|
||||
except Exception:
|
||||
shutil.rmtree(temp_dir, ignore_errors=True)
|
||||
raise
|
||||
|
||||
if (release_dir / "VERSION").read_text(encoding="utf-8").strip() != VERSION:
|
||||
raise RuntimeError("RELEASE_VERSION_FILE_MISMATCH")
|
||||
|
||||
# Only replace our dedicated V2R1C7 link. Never touch previous versions.
|
||||
current_link = p["current_link"]
|
||||
if current_link.exists() and not current_link.is_symlink():
|
||||
raise RuntimeError("CURRENT_RELEASE_PATH_NOT_SYMLINK")
|
||||
if current_link.is_symlink():
|
||||
existing = current_link.resolve()
|
||||
if existing != release_dir.resolve():
|
||||
raise RuntimeError("CURRENT_RELEASE_LINK_CONFLICT")
|
||||
else:
|
||||
current_link.parent.mkdir(parents=True, exist_ok=True)
|
||||
new_link = current_link.with_name(current_link.name + ".new")
|
||||
try:
|
||||
new_link.unlink()
|
||||
except FileNotFoundError:
|
||||
pass
|
||||
new_link.symlink_to(release_dir)
|
||||
os.replace(new_link, current_link)
|
||||
|
||||
state_value = {
|
||||
"version": VERSION,
|
||||
"commit_oid": value["target_oid"],
|
||||
"artifact_sha256": value["artifact_sha256"],
|
||||
"deployment_state": "ACTIVE",
|
||||
}
|
||||
atomic_json(p["state_file"], state_value, mode=0o640, gid=gid)
|
||||
env = (
|
||||
f"RELEASE_VERSION={VERSION}\n"
|
||||
f"TARGET_OID={value['target_oid']}\n"
|
||||
f"ARTIFACT_SHA256={value['artifact_sha256']}\n"
|
||||
"DEPLOYMENT_STATE=ACTIVE\n"
|
||||
f"AIOS_V2R1C7_PORT={PORT}\n"
|
||||
f"DEPLOYMENT_STATE_FILE={p['state_file']}\n"
|
||||
"AIOS_GITOPS_WRITES_ENABLED=false\n"
|
||||
)
|
||||
atomic_text(p["env_file"], env, mode=0o640, gid=gid)
|
||||
|
||||
unit_source = (
|
||||
release_dir
|
||||
/ "deploy"
|
||||
/ "aios-v2"
|
||||
/ "systemd"
|
||||
/ "aios-v2r1c7-production.service"
|
||||
)
|
||||
unit_text = unit_source.read_text(encoding="utf-8")
|
||||
if "aios-v2r1c7-production.service" in unit_text:
|
||||
# Description text is irrelevant; service identity is the destination.
|
||||
pass
|
||||
atomic_text(p["unit_file"], unit_text, mode=0o644)
|
||||
|
||||
verify = subprocess.run(
|
||||
["systemd-analyze", "verify", str(p["unit_file"])],
|
||||
check=False,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=30,
|
||||
env={"PATH": os.environ.get("PATH", "")},
|
||||
)
|
||||
if verify.returncode != 0:
|
||||
raise RuntimeError("PRODUCTION_SYSTEMD_VERIFY_FAILED")
|
||||
|
||||
systemctl("daemon-reload")
|
||||
systemctl("enable", SERVICE)
|
||||
systemctl("restart", SERVICE)
|
||||
|
||||
deadline = time.time() + args.health_timeout
|
||||
result = {"status": "UNAVAILABLE"}
|
||||
while time.time() < deadline:
|
||||
result = health(f"http://127.0.0.1:{PORT}/health", value)
|
||||
if result["status"] == "ACTIVE_VERIFIED":
|
||||
break
|
||||
if result["status"] == "CONFLICT":
|
||||
break
|
||||
time.sleep(1)
|
||||
if result["status"] != "ACTIVE_VERIFIED":
|
||||
raise RuntimeError("PRODUCTION_HEALTH_VERIFY_FAILED")
|
||||
|
||||
# Restart recovery is part of production acceptance, not just staging.
|
||||
systemctl("restart", SERVICE)
|
||||
deadline = time.time() + args.health_timeout
|
||||
while time.time() < deadline:
|
||||
result = health(f"http://127.0.0.1:{PORT}/health", value)
|
||||
if result["status"] == "ACTIVE_VERIFIED":
|
||||
break
|
||||
if result["status"] == "CONFLICT":
|
||||
break
|
||||
time.sleep(1)
|
||||
if result["status"] != "ACTIVE_VERIFIED":
|
||||
raise RuntimeError("PRODUCTION_RESTART_RECOVERY_FAILED")
|
||||
return result
|
||||
|
||||
|
||||
def rollback(args: argparse.Namespace, value: Mapping[str, Any]) -> dict[str, Any]:
|
||||
_, gid = identity()
|
||||
p = paths(args, value)
|
||||
systemctl("stop", SERVICE, check=False)
|
||||
systemctl("disable", SERVICE, check=False)
|
||||
state = {
|
||||
"version": VERSION,
|
||||
"commit_oid": value["target_oid"],
|
||||
"artifact_sha256": value["artifact_sha256"],
|
||||
"deployment_state": "ROLLED_BACK",
|
||||
}
|
||||
atomic_json(p["state_file"], state, mode=0o640, gid=gid)
|
||||
return {
|
||||
"status": "ROLLED_BACK",
|
||||
"version": VERSION,
|
||||
"target_oid": value["target_oid"],
|
||||
"artifact_sha256": value["artifact_sha256"],
|
||||
"deployment_state": "ROLLED_BACK",
|
||||
"production_deployed": False,
|
||||
}
|
||||
|
||||
|
||||
def main() -> int:
|
||||
p = argparse.ArgumentParser()
|
||||
p.add_argument("--release-root", type=Path, default=Path("/opt/aios/releases"))
|
||||
p.add_argument("--current-link", type=Path, default=Path("/opt/aios/v2r1c7"))
|
||||
p.add_argument("--env-file", type=Path, default=Path("/etc/aios/aios-v2r1c7.env"))
|
||||
p.add_argument(
|
||||
"--state-file",
|
||||
type=Path,
|
||||
default=Path("/var/lib/aios/v2r1c7/deployment-state.json"),
|
||||
)
|
||||
p.add_argument(
|
||||
"--unit-file",
|
||||
type=Path,
|
||||
default=Path("/etc/systemd/system/aios-v2r1c7-production.service"),
|
||||
)
|
||||
p.add_argument("--health-timeout", type=int, default=30)
|
||||
args = p.parse_args()
|
||||
value = load_request()
|
||||
operation = value.get("operation")
|
||||
if operation == "probe":
|
||||
result = probe(args, value)
|
||||
elif operation == "apply":
|
||||
result = install_release(args, value)
|
||||
elif operation == "rollback":
|
||||
result = rollback(args, value)
|
||||
else:
|
||||
raise ValueError("unsupported_operation")
|
||||
print(json.dumps(result, sort_keys=True))
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
try:
|
||||
raise SystemExit(main())
|
||||
except Exception as exc:
|
||||
print(
|
||||
json.dumps(
|
||||
{"status": "FAIL_CLOSED", "error": type(exc).__name__, "reason": str(exc)},
|
||||
sort_keys=True,
|
||||
)
|
||||
)
|
||||
raise SystemExit(2)
|
||||
@@ -0,0 +1,135 @@
|
||||
"""V2R2C1 adapter using the existing V2R1C7 deployment primitives.
|
||||
|
||||
The service identity and loopback port remain stable for a safe cutover. The
|
||||
new release is installed under its own immutable directory and keeps the
|
||||
previous V2R1C7 release available for an explicit, owner-approved rollback.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
import shutil
|
||||
import sys
|
||||
from argparse import Namespace
|
||||
from pathlib import Path
|
||||
from typing import Any, Mapping
|
||||
|
||||
import v2r1c7_production_adapter as base
|
||||
|
||||
|
||||
VERSION = "V2R2C1"
|
||||
SERVICE = "aios-v2r1c7-production.service"
|
||||
PORT = 18927
|
||||
RELEASE_ROOT = Path("/opt/aios/releases")
|
||||
CURRENT_LINK = Path("/opt/aios/v2r2c1")
|
||||
ENV_FILE = Path("/etc/aios/aios-v2r2c1.env")
|
||||
STATE_FILE = Path("/var/lib/aios/v2r2c1/deployment-state.json")
|
||||
UNIT_FILE = Path("/etc/systemd/system/aios-v2r1c7-production.service")
|
||||
ROLLBACK_ROOT = Path("/var/lib/aios/v2r2c1/rollback")
|
||||
|
||||
REQUIRED_MEMBERS = {
|
||||
"payload/VERSION",
|
||||
"payload/active-runtime/app/deployment_state.py",
|
||||
"payload/active-runtime/app/v2r1c6_privacy_tools.py",
|
||||
"payload/active-runtime/app/v2r2c1_health_service.py",
|
||||
"payload/deploy/aios-v2/systemd/aios-v2r1c7-production.service",
|
||||
"payload/deploy/aios-v2/v2r2c1-runtime-config.json",
|
||||
"payload/deploy/aios-v2/v2r2c1-rollback.json",
|
||||
"payload/scripts/probe_v2r2c1_health.py",
|
||||
"payload/scripts/rollback_v2r2c1_release.sh",
|
||||
"payload/BUILD-METADATA.json",
|
||||
"payload/ARTIFACT-MANIFEST.json",
|
||||
"payload/ROLLBACK-METADATA.json",
|
||||
}
|
||||
|
||||
|
||||
def _configure_base() -> None:
|
||||
base.VERSION = VERSION
|
||||
base.SERVICE = SERVICE
|
||||
base.PORT = PORT
|
||||
base.REQUIRED_MEMBERS = REQUIRED_MEMBERS
|
||||
|
||||
|
||||
def _args() -> Namespace:
|
||||
return Namespace(
|
||||
release_root=RELEASE_ROOT,
|
||||
current_link=CURRENT_LINK,
|
||||
env_file=ENV_FILE,
|
||||
state_file=STATE_FILE,
|
||||
unit_file=UNIT_FILE,
|
||||
health_timeout=45,
|
||||
)
|
||||
|
||||
|
||||
def load_request() -> dict[str, Any]:
|
||||
_configure_base()
|
||||
value = base.load_request()
|
||||
if value.get("version") != VERSION or value.get("production_service") != SERVICE or int(value.get("production_port", -1)) != PORT:
|
||||
raise ValueError("V2R2C1 production binding mismatch")
|
||||
return value
|
||||
|
||||
|
||||
def _backup(path: Path, name: str) -> None:
|
||||
if path.is_file() and not path.is_symlink():
|
||||
ROLLBACK_ROOT.mkdir(parents=True, exist_ok=True)
|
||||
destination = ROLLBACK_ROOT / name
|
||||
if not destination.exists():
|
||||
shutil.copy2(path, destination)
|
||||
|
||||
|
||||
def execute_request(value: Mapping[str, Any]) -> dict[str, Any]:
|
||||
_configure_base()
|
||||
if value.get("schema") != "aios.production-executor-request.v1":
|
||||
raise ValueError("request_schema_invalid")
|
||||
if value.get("operation") == "apply":
|
||||
_backup(UNIT_FILE, "aios-v2r1c7-production.service.previous")
|
||||
_backup(Path("/etc/aios/aios-v2r1c7.env"), "aios-v2r1c7.env.previous")
|
||||
_backup(Path("/var/lib/aios/v2r1c7/deployment-state.json"), "v2r1c7-deployment-state.previous")
|
||||
return base.install_release(_args(), value)
|
||||
if value.get("operation") == "probe":
|
||||
return base.probe(_args(), value)
|
||||
if value.get("operation") == "rollback":
|
||||
return rollback(value)
|
||||
raise ValueError("unsupported_operation")
|
||||
|
||||
|
||||
def rollback(value: Mapping[str, Any]) -> dict[str, Any]:
|
||||
"""Explicit rollback only; this adapter never invokes it automatically."""
|
||||
import subprocess
|
||||
|
||||
subprocess.run(["systemctl", "stop", SERVICE], check=False, capture_output=True, text=True, timeout=45)
|
||||
previous_unit = ROLLBACK_ROOT / "aios-v2r1c7-production.service.previous"
|
||||
previous_env = ROLLBACK_ROOT / "aios-v2r1c7.env.previous"
|
||||
previous_state = ROLLBACK_ROOT / "v2r1c7-deployment-state.previous"
|
||||
if not previous_unit.is_file() or not previous_env.is_file():
|
||||
raise RuntimeError("ROLLBACK_RECEIPT_MISSING")
|
||||
UNIT_FILE.parent.mkdir(parents=True, exist_ok=True)
|
||||
ENV_FILE.parent.mkdir(parents=True, exist_ok=True)
|
||||
shutil.copy2(previous_unit, UNIT_FILE)
|
||||
shutil.copy2(previous_env, Path("/etc/aios/aios-v2r1c7.env"))
|
||||
if previous_state.is_file():
|
||||
Path("/var/lib/aios/v2r1c7").mkdir(parents=True, exist_ok=True)
|
||||
shutil.copy2(previous_state, Path("/var/lib/aios/v2r1c7/deployment-state.json"))
|
||||
subprocess.run(["systemctl", "daemon-reload"], check=True, capture_output=True, text=True, timeout=45)
|
||||
subprocess.run(["systemctl", "restart", SERVICE], check=True, capture_output=True, text=True, timeout=45)
|
||||
result = base.health("http://127.0.0.1:18927/health", {
|
||||
"version": "V2R1C7",
|
||||
"target_oid": str(json.loads(Path("/var/lib/aios/v2r1c7/deployment-state.json").read_text(encoding="utf-8")).get("commit_oid")),
|
||||
"artifact_sha256": str(json.loads(Path("/var/lib/aios/v2r1c7/deployment-state.json").read_text(encoding="utf-8")).get("artifact_sha256")),
|
||||
})
|
||||
return {"status": "ROLLED_BACK", "rollback_health": result, "automatic_rollback": False}
|
||||
|
||||
|
||||
def main() -> int:
|
||||
try:
|
||||
value = load_request()
|
||||
result = execute_request(value)
|
||||
print(json.dumps(result, sort_keys=True))
|
||||
return 0
|
||||
except Exception as exc:
|
||||
print(json.dumps({"status": "FAIL_CLOSED", "error": type(exc).__name__, "reason": str(exc)}, sort_keys=True))
|
||||
return 2
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"schema": "aios.approval.supersession.v1",
|
||||
"version": "V2R1C7",
|
||||
"superseded_approval_plan_id": "sha256:e62afc9946bcd0badc8e75291baf979e915dccaa50c2c77912c77f1ddeb2aa65",
|
||||
"status": "SUPERSEDED_PREMERGE_IDENTITY_CHANGED",
|
||||
"execute": false,
|
||||
"reason": "release identity and artifact source head changed before protected merge",
|
||||
"production_deployed": false
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
{"artifact":"aios-V2R1C7-92b4a937e56b.tar.zst","artifact_sha256":"6c7c763dc9a63a8411be3fc458881fc9bca1f403355d2f3a1cb338edbf9f24da","artifact_size":9672,"base_oid":"b356dec2ac5bd2a5f721aa73f8fd1902f9c65632","baseline_changed":false,"deployable":true,"executable_payload":true,"production_deployed":false,"schema":"aios.release-artifact-receipt.v3","source_head_oid":"7f09670511138f1f1f74f6b40329cd35d6eb2e55","tag_created":false,"version":"V2R1C7"}
|
||||
@@ -0,0 +1,79 @@
|
||||
{
|
||||
"duplicates": 0,
|
||||
"files": [
|
||||
{
|
||||
"path": "VERSION",
|
||||
"sha256": "9208325e5b2e6f5ba6b64e000bc21d14d9f2f214e3ad33dd79857e85218fd6c2",
|
||||
"size": 7
|
||||
},
|
||||
{
|
||||
"path": "versions/V2R1C7.json",
|
||||
"sha256": "8f3153c625241f60b65109eeac045e8aed7661d95f3a10f0b32ba3e0e9ac0a85",
|
||||
"size": 801
|
||||
},
|
||||
{
|
||||
"path": "release-manifests/V2R1C7.json",
|
||||
"sha256": "560e5d9af8a8e2ec60ec31cf8659a7982b93ea09b7adb486b738ea2fba460d70",
|
||||
"size": 735
|
||||
},
|
||||
{
|
||||
"path": "deploy/aios-v2/systemd/aios-v2r1c7-production.service",
|
||||
"sha256": "c96ba560d69f6c2eb700b9f2b42a32e394dadd00dffb23bbefdb9fb3e1571c0d",
|
||||
"size": 709
|
||||
},
|
||||
{
|
||||
"path": "active-runtime/app/deployment_state.py",
|
||||
"sha256": "0997ab8332985c7124f334c4e49735d8833326578acc61a61a389bf792424de3",
|
||||
"size": 2657
|
||||
},
|
||||
{
|
||||
"path": "active-runtime/app/v2r1c6_privacy_tools.py",
|
||||
"sha256": "dd3f8b1cab88e005a291f077807873c381e8ca92fe863dc05817925f0dff9613",
|
||||
"size": 4603
|
||||
},
|
||||
{
|
||||
"path": "active-runtime/app/v2r1c7_health_service.py",
|
||||
"sha256": "9e45025c41068cafdea2f360edfc4146a5459b0a6bcfbf1403206fc0161e7bac",
|
||||
"size": 174
|
||||
},
|
||||
{
|
||||
"path": "scripts/build_v2r1c7_release_artifact.py",
|
||||
"sha256": "81a423c7814f81cb888b60a538c153e549b2be9099c881c968d55ae84efddc57",
|
||||
"size": 4787
|
||||
},
|
||||
{
|
||||
"path": "scripts/verify_v2r1c7_release.py",
|
||||
"sha256": "32a860e28d5afc105f413d878a13fb8600fa74874dbd3bc4c031ef3efa17896a",
|
||||
"size": 1228
|
||||
},
|
||||
{
|
||||
"path": "scripts/probe_v2r1c7_health.py",
|
||||
"sha256": "64c78dabffb05c8851f87b2a27c43bd1f21728dca586397d03a6bc22906a906c",
|
||||
"size": 681
|
||||
},
|
||||
{
|
||||
"path": "scripts/rollback_v2r1c7_release.sh",
|
||||
"sha256": "5419dc7de0f50908ad0ef1dac74172fce811bb7b45f314a6e1c1bb9ce618324d",
|
||||
"size": 97
|
||||
}
|
||||
],
|
||||
"mismatched": 0,
|
||||
"missing": 0,
|
||||
"missing_paths": [],
|
||||
"required_files": [
|
||||
"VERSION",
|
||||
"versions/V2R1C7.json",
|
||||
"release-manifests/V2R1C7.json",
|
||||
"deploy/aios-v2/systemd/aios-v2r1c7-production.service",
|
||||
"active-runtime/app/deployment_state.py",
|
||||
"active-runtime/app/v2r1c6_privacy_tools.py",
|
||||
"active-runtime/app/v2r1c7_health_service.py",
|
||||
"scripts/build_v2r1c7_release_artifact.py",
|
||||
"scripts/verify_v2r1c7_release.py",
|
||||
"scripts/probe_v2r1c7_health.py",
|
||||
"scripts/rollback_v2r1c7_release.sh"
|
||||
],
|
||||
"schema": "aios.v2r1c7.source-manifest.v1",
|
||||
"unexpected": 0,
|
||||
"version": "V2R1C7"
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
{"artifact":"aios-V2R1C7-92b4a937e56b.tar.zst","artifact_sha256":"6c7c763dc9a63a8411be3fc458881fc9bca1f403355d2f3a1cb338edbf9f24da","artifact_size":9672,"base_oid":"b356dec2ac5bd2a5f721aa73f8fd1902f9c65632","baseline_changed":false,"deployable":true,"executable_payload":true,"production_deployed":false,"schema":"aios.release-artifact-receipt.v3","source_head_oid":"7f09670511138f1f1f74f6b40329cd35d6eb2e55","tag_created":false,"version":"V2R1C7"}
|
||||
Binary file not shown.
@@ -0,0 +1 @@
|
||||
{"artifact":"aios-V2R1C7-v2r1c7.tar.zst","artifact_sha256":"08864f3f2d7e674a908a1d6eb7436db8747bd917200bf206b01276ec3da1e4be","artifact_size":7993,"baseline_changed":false,"deployable":true,"executable_payload":true,"production_deployed":false,"schema":"aios.release-artifact-receipt.v2","source_base_oid":"b356dec2ac5bd2a5f721aa73f8fd1902f9c65632","tag_created":false,"version":"V2R1C7"}
|
||||
Binary file not shown.
@@ -0,0 +1,8 @@
|
||||
# Installer generates these bindings; STAGED is the only pre-acceptance state.
|
||||
RELEASE_VERSION=V2R1C7
|
||||
TARGET_OID=__INSTALL_TARGET_OID__
|
||||
ARTIFACT_SHA256=__INSTALL_ARTIFACT_SHA256__
|
||||
DEPLOYMENT_STATE=STAGED
|
||||
AIOS_V2R1C7_PORT=18927
|
||||
DEPLOYMENT_STATE_FILE=/var/lib/aios/v2r1c7/deployment-state.json
|
||||
AIOS_GITOPS_WRITES_ENABLED=false
|
||||
@@ -0,0 +1,28 @@
|
||||
[Unit]
|
||||
Description=AIOS V2R1C7 governed health contract service
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=aios-v2r1c7
|
||||
Group=aios-v2r1c7
|
||||
WorkingDirectory=/opt/aios/v2r1c7
|
||||
EnvironmentFile=/etc/aios/aios-v2r1c7.env
|
||||
ExecStart=/usr/bin/python3 /opt/aios/v2r1c7/active-runtime/app/v2r1c7_health_service.py --serve --host 127.0.0.1 --port ${AIOS_V2R1C7_PORT}
|
||||
Restart=on-failure
|
||||
RestartSec=5
|
||||
NoNewPrivileges=true
|
||||
PrivateTmp=true
|
||||
ProtectHome=true
|
||||
ProtectSystem=strict
|
||||
ProtectKernelLogs=true
|
||||
ProtectKernelModules=true
|
||||
ProtectKernelTunables=true
|
||||
ProtectControlGroups=true
|
||||
RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6
|
||||
IPAddressDeny=any
|
||||
IPAddressAllow=localhost
|
||||
UMask=0077
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"schema": "aios.release-rollback.v1",
|
||||
"version": "V2R1C7",
|
||||
"previous_version": "V2R1C6",
|
||||
"previous_tag": "V2R1C6",
|
||||
"rollback_state": "ROLLED_BACK",
|
||||
"production_deploy_authorized": false,
|
||||
"execution": "manual_owner_approval_required"
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"schema": "aios.v2r1c7.runtime-config.v1",
|
||||
"install_state": "STAGED",
|
||||
"acceptance_transition": "STAGED -> ACTIVE only after staging gates",
|
||||
"production_deploy_authorized": false,
|
||||
"registry_write_authorized": false,
|
||||
"baseline_write_authorized": false
|
||||
}
|
||||
@@ -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,38 @@
|
||||
[Unit]
|
||||
Description=AIOS governed approved production release executor
|
||||
After=network-online.target
|
||||
Wants=network-online.target
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
User=root
|
||||
Group=root
|
||||
UMask=0077
|
||||
Environment=PYTHONDONTWRITEBYTECODE=1
|
||||
Environment=AIOS_GITOPS_WRITES_ENABLED=false
|
||||
EnvironmentFile=/etc/aios-gitops/approved-release-executor.env
|
||||
ExecStart=/usr/bin/python3 /usr/libexec/aios-approved-release-executor/current/active-runtime/app/approved_release_executor.py \
|
||||
--authorization ${AIOS_RELEASE_AUTHORIZATION} \
|
||||
--repository ${AIOS_GITOPS_AUTHORITATIVE_GIT_DIR} \
|
||||
--state-root ${AIOS_RELEASE_EXECUTION_STATE_ROOT} \
|
||||
--artifact ${AIOS_RELEASE_ARTIFACT} \
|
||||
--artifact-size ${AIOS_RELEASE_ARTIFACT_SIZE} \
|
||||
--version ${AIOS_RELEASE_VERSION} \
|
||||
--target-oid ${AIOS_RELEASE_TARGET_OID} \
|
||||
--evidence-closure-oid ${AIOS_RELEASE_EVIDENCE_CLOSURE_OID} \
|
||||
--artifact-sha256 ${AIOS_RELEASE_ARTIFACT_SHA256} \
|
||||
--approval-plan-id ${AIOS_RELEASE_APPROVAL_PLAN_ID} \
|
||||
--production-service aios-v2r1c7-production.service \
|
||||
--production-port 18927 \
|
||||
--tag-adapter /usr/bin/python3 /usr/libexec/aios-approved-release-executor/current/active-runtime/app/governed_tag_adapter.py \
|
||||
--deploy-adapter /usr/bin/python3 /usr/libexec/aios-approved-release-executor/current/active-runtime/app/v2r1c7_production_adapter.py \
|
||||
--registry-adapter /usr/bin/env AIOS_RELEASE_METADATA_MODE=registry /usr/bin/python3 /usr/libexec/aios-approved-release-executor/current/active-runtime/app/release_metadata_adapter.py \
|
||||
--baseline-adapter /usr/bin/env AIOS_RELEASE_METADATA_MODE=baseline /usr/bin/python3 /usr/libexec/aios-approved-release-executor/current/active-runtime/app/release_metadata_adapter.py
|
||||
NoNewPrivileges=true
|
||||
PrivateTmp=true
|
||||
ProtectHome=true
|
||||
ProtectSystem=full
|
||||
ReadWritePaths=/etc/aios /etc/systemd/system /opt/aios /var/lib/aios /var/lib/aios-gitops /var/lib/aios-gitops-runtime-anchor /data/aios-gitops
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
@@ -0,0 +1,23 @@
|
||||
[Unit]
|
||||
Description=AIOS ChatGPT Bridge v2 staging control plane
|
||||
After=network-online.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=aios-bridge-staging
|
||||
Group=aios-bridge-staging
|
||||
WorkingDirectory=/opt/aios/gitops-control-plane/current
|
||||
Environment=AIOS_BRIDGE_SCHEMA_VERSION=aios.chatgpt-gitops-bridge.v2
|
||||
Environment=AIOS_GITOPS_WRITES_ENABLED=false
|
||||
Environment=AIOS_GITOPS_DURABLE_JOB_EXECUTION_ENABLED=true
|
||||
Environment=AIOS_DEPLOYMENT_STATE=STAGING
|
||||
ExecStart=/usr/bin/python3 /opt/aios/gitops-control-plane/current/bridge-v2-staging-server.py
|
||||
NoNewPrivileges=true
|
||||
PrivateTmp=true
|
||||
ProtectSystem=strict
|
||||
ProtectHome=true
|
||||
ReadWritePaths=/var/lib/aios-gitops/bridge-v2-staging
|
||||
Restart=on-failure
|
||||
|
||||
[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
|
||||
@@ -0,0 +1,54 @@
|
||||
[Unit]
|
||||
Description=AIOS GitOps Model API V2R2C1 immutable staging runtime
|
||||
After=network-online.target
|
||||
Wants=network-online.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=aiman
|
||||
Group=gitops
|
||||
WorkingDirectory=@RUNTIME_ROOT@
|
||||
Environment=PYTHONUNBUFFERED=1
|
||||
Environment=PYTHONDONTWRITEBYTECODE=1
|
||||
Environment=AIOS_GITOPS_ROOT=/var/lib/aios-gitops
|
||||
Environment=AIOS_GITOPS_WORKSPACE=@RUNTIME_ROOT@
|
||||
Environment=AIOS_GITOPS_REPO=/data/aios-gitops/repo.git
|
||||
Environment=AIOS_GITOPS_BARE_REPO=/data/aios-gitops/repo.git
|
||||
Environment=AIOS_GITOPS_AUTHORITATIVE_GIT_DIR=/data/aios-gitops/repo.git
|
||||
Environment=AIOS_GITOPS_REPOSITORY_IDENTITY=aios/aios
|
||||
Environment=AIOS_GITOPS_BASELINE_AUTHORITY=/var/lib/aios-gitops-runtime-anchor/baseline/current.json
|
||||
Environment=AIOS_GITOPS_WRITES_ENABLED=false
|
||||
Environment=AIOS_GITOPS_DURABLE_JOB_EXECUTION_ENABLED=false
|
||||
Environment=AIOS_GITOPS_JOB_CONTROLLER_ENABLED=false
|
||||
Environment=AIOS_GITOPS_WORKSPACE_ROLE=runtime-anchor
|
||||
Environment=AIOS_GITOPS_RUNTIME_OID=@SOURCE_OID@
|
||||
Environment=AIOS_GITOPS_ARTIFACT_SHA256=@ARTIFACT_SHA256@
|
||||
Environment=AIOS_GITOPS_DEPLOYMENT_STATE=STAGING
|
||||
Environment=AIOS_GITOPS_MODEL_API_SERVICE=aios-gitops-model-api-v2r2c1-staging.service
|
||||
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
|
||||
ExecStart=/usr/bin/python3 @RUNTIME_ROOT@/app/aios_model_api_staging.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@ /data/aios-gitops/repo.git /var/lib/aios-gitops-runtime-anchor/baseline
|
||||
ReadWritePaths=/var/lib/aios-gitops/model-api-staging
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
@@ -0,0 +1,33 @@
|
||||
[Unit]
|
||||
Description=AIOS durable GitOps writer worker
|
||||
Requires=aios-gitops-writer.socket
|
||||
After=aios-gitops-writer.socket network-online.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=root
|
||||
Group=root
|
||||
UMask=0077
|
||||
Environment=AIOS_GITOPS_WRITES_ENABLED=false
|
||||
Environment=AIOS_GITOPS_DURABLE_JOB_EXECUTION_ENABLED=true
|
||||
Environment=AIOS_GITOPS_JOB_ROOT=/var/lib/aios-gitops/jobs
|
||||
Environment=AIOS_GITEA_MACHINE_CREDENTIAL_REF=systemd-credential:gitea-machine-token
|
||||
Environment=AIOS_GITOPS_AUTHORITY_GIT_DIR=/data/aios-gitops/repo.git
|
||||
Environment=AIOS_GITOPS_WORKTREE_ROOT=/var/lib/aios-gitops/worktrees
|
||||
Environment=AIOS_GITEA_API=https://g.cnnetmkt.top/git/api/v1
|
||||
Environment=AIOS_GITEA_REMOTE=https://g.cnnetmkt.top/git/aios/aios.git
|
||||
Environment=AIOS_GITEA_MACHINE_ACCOUNT=aios-autonomy-agent
|
||||
Environment=AIOS_RELEASE_RECOVERY_RECEIPT=/var/lib/aios-gitops/release-campaigns/AIOS-AUTONOMOUS-RELEASE-CAMPAIGN-20260807/authorization-recovery.json
|
||||
LoadCredentialEncrypted=gitea-machine-token:/etc/aios/credentials/gitea-machine.token.cred
|
||||
Environment=PYTHONDONTWRITEBYTECODE=1
|
||||
ExecStart=/usr/bin/python3 -B /opt/aios-gitops/active-runtime/app/aios_gitops_writer_worker.py --job-root ${AIOS_GITOPS_JOB_ROOT}
|
||||
Restart=on-failure
|
||||
RestartSec=2
|
||||
NoNewPrivileges=no
|
||||
PrivateTmp=true
|
||||
ProtectSystem=strict
|
||||
ProtectHome=true
|
||||
ReadWritePaths=/var/lib/aios-gitops/jobs /var/lib/aios-gitops/payloads /var/lib/aios-gitops/worktrees /data/aios-gitops/repo.git
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
@@ -0,0 +1,14 @@
|
||||
[Unit]
|
||||
Description=AIOS durable GitOps writer socket
|
||||
|
||||
[Socket]
|
||||
ListenStream=/run/aios-gitops/writer.sock
|
||||
Accept=no
|
||||
SocketUser=root
|
||||
SocketGroup=gitops
|
||||
SocketMode=0660
|
||||
DirectoryMode=0750
|
||||
RemoveOnStop=yes
|
||||
|
||||
[Install]
|
||||
WantedBy=sockets.target
|
||||
@@ -0,0 +1,26 @@
|
||||
[Unit]
|
||||
Description=AIOS Semantic Governance Drift Monitor
|
||||
After=aios-governance-executor.service
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
User=root
|
||||
Group=gitops
|
||||
ExecStart=/usr/bin/python3 /usr/local/libexec/aios-governance-drift-monitor
|
||||
NoNewPrivileges=true
|
||||
PrivateTmp=true
|
||||
PrivateDevices=true
|
||||
ProtectSystem=strict
|
||||
ProtectHome=true
|
||||
ProtectKernelTunables=true
|
||||
ProtectKernelModules=true
|
||||
ProtectControlGroups=true
|
||||
LockPersonality=true
|
||||
RestrictSUIDSGID=true
|
||||
RestrictRealtime=true
|
||||
MemoryDenyWriteExecute=true
|
||||
RestrictAddressFamilies=AF_UNIX
|
||||
IPAddressDeny=any
|
||||
ReadOnlyPaths=/usr/local/libexec/aios-governance-drift-monitor
|
||||
ReadOnlyPaths=/var/lib/aios-gitops/runtime/tasks/AIOS-PROTECTION-SNAPSHOT-DRIFT-FORENSICS-V1/protection-drift/semantic-analysis.json
|
||||
ReadWritePaths=/var/lib/aios-gitops/governance-executor
|
||||
@@ -0,0 +1,37 @@
|
||||
[Unit]
|
||||
Description=AIOS Dedicated Governance Executor
|
||||
After=aios-gitea.service network-online.target
|
||||
Wants=network-online.target
|
||||
ConditionPathExists=/etc/credstore.encrypted/gitea-governance-executor-token
|
||||
ConditionPathExists=/usr/local/libexec/aios-governance-executor
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=root
|
||||
Group=gitops
|
||||
LoadCredentialEncrypted=gitea-governance-executor-token:/etc/credstore.encrypted/gitea-governance-executor-token
|
||||
ExecStart=/usr/local/libexec/aios-governance-executor
|
||||
Restart=on-failure
|
||||
RestartSec=3
|
||||
NoNewPrivileges=true
|
||||
PrivateTmp=true
|
||||
ProtectHome=true
|
||||
ProtectSystem=strict
|
||||
ProtectKernelLogs=true
|
||||
ProtectKernelModules=true
|
||||
ProtectKernelTunables=true
|
||||
ProtectControlGroups=true
|
||||
RestrictSUIDSGID=true
|
||||
RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6
|
||||
IPAddressDeny=any
|
||||
IPAddressAllow=127.0.0.1
|
||||
RuntimeDirectory=aios-governance-executor
|
||||
RuntimeDirectoryMode=0750
|
||||
StateDirectory=aios-gitops/governance-executor
|
||||
StateDirectoryMode=0750
|
||||
ReadOnlyPaths=/usr/local/libexec /etc/credstore.encrypted
|
||||
ReadWritePaths=/var/lib/aios-gitops/governance-executor /run/aios-governance-executor
|
||||
UMask=0007
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
@@ -0,0 +1,33 @@
|
||||
[Unit]
|
||||
Description=AIOS Governance Pre-Approval Publication Plane
|
||||
Requires=aios-governance-preapproval-publisher.socket
|
||||
After=network-online.target aios-gitea.service
|
||||
Wants=network-online.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=root
|
||||
Group=gitops
|
||||
UMask=0077
|
||||
Environment=AIOS_GITOPS_WRITES_ENABLED=false
|
||||
Environment=AIOS_GOVERNANCE_PUBLISHER_OPERATION_ALLOWLIST=semantic-reconciliation-only
|
||||
LoadCredentialEncrypted=gitea-machine-token:/etc/aios/credentials/gitea-machine.token.cred
|
||||
ExecStart=/usr/local/libexec/aios-governance-preapproval-publisher
|
||||
NoNewPrivileges=true
|
||||
PrivateTmp=true
|
||||
PrivateDevices=true
|
||||
ProtectSystem=strict
|
||||
ProtectHome=true
|
||||
ProtectKernelTunables=true
|
||||
ProtectKernelModules=true
|
||||
ProtectControlGroups=true
|
||||
LockPersonality=true
|
||||
RestrictSUIDSGID=true
|
||||
RestrictRealtime=true
|
||||
MemoryDenyWriteExecute=true
|
||||
RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6
|
||||
ReadOnlyPaths=/usr/local/libexec/aios-governance-preapproval-publisher
|
||||
ReadOnlyPaths=/etc/aios/credentials
|
||||
ReadOnlyPaths=/var/lib/aios-gitops/runtime/tasks/AIOS-PROTECTION-SNAPSHOT-DRIFT-FORENSICS-V1/publication-input
|
||||
ReadWritePaths=/var/lib/aios-gitops/runtime/tasks/AIOS-PROTECTION-SNAPSHOT-DRIFT-FORENSICS-V1
|
||||
ReadWritePaths=/run/aios-governance-preapproval-publisher
|
||||
@@ -0,0 +1,13 @@
|
||||
[Unit]
|
||||
Description=AIOS Governance Pre-Approval Publication Plane Socket
|
||||
|
||||
[Socket]
|
||||
ListenStream=/run/aios-governance-preapproval-publisher/publisher.sock
|
||||
SocketUser=root
|
||||
SocketGroup=gitops
|
||||
SocketMode=0660
|
||||
DirectoryMode=0750
|
||||
RemoveOnStop=true
|
||||
|
||||
[Install]
|
||||
WantedBy=sockets.target
|
||||
@@ -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,63 @@
|
||||
# AIOS Approved Production Release Executor
|
||||
|
||||
Status: implementation artifact for governed protected-PR integration.
|
||||
|
||||
## Why this exists
|
||||
|
||||
The deployed `aios-silent-release-controller` is intentionally shadow-only.
|
||||
It rejects production execution unless a trusted builder/orchestrator factory is
|
||||
wired. V2R1C7 already completed post-merge deterministic build, staging,
|
||||
rollback, evidence closure and exact Owner approval, so rebuilding inside the
|
||||
production executor is both unnecessary and a deadlock source.
|
||||
|
||||
This patch adds a separate one-shot production executor. It **does not turn the
|
||||
shadow controller into a production service**.
|
||||
|
||||
## Exact V2R1C7 binding
|
||||
|
||||
- target: `c7adece1cc8beffe7c718d53c775898b45d6c7f8`
|
||||
- evidence: `8c2898baf07195b9861ed2ff74f03a71674ec9e9`
|
||||
- artifact: `ce09a46f28b453634784abceda4597fd51c3aa0838e72a5ad5fefd03d58b949e`
|
||||
- approval plan: `sha256:c8199d638c2bb2e4896ca9b219ca52db79713bee007df05653ab348905d6cf28`
|
||||
|
||||
The executor requires a root-owned accepted authorization receipt that matches
|
||||
all five fields. It does not accept caller-supplied replacement evidence.
|
||||
|
||||
## Execution order
|
||||
|
||||
1. exact authorization + artifact + Git lineage preflight
|
||||
2. annotated V2R1C7 Tag publication and Authority/network/Gitea verification
|
||||
3. deploy exact artifact; health must report ACTIVE and exact target/digest
|
||||
4. Registry atomic finalization
|
||||
5. authoritative Production Baseline CAS finalization **last**
|
||||
6. read back Tag/runtime/Registry/Baseline and require exact consistency
|
||||
|
||||
## Tag implementation
|
||||
|
||||
`governed_tag_adapter.py` is a thin adapter over the repository's existing
|
||||
`gitea_tag_publication.TagPublisher`. It intentionally calls only the existing
|
||||
tag verification/publication subroutine and never calls legacy
|
||||
`TagPublisher.publish()` because that legacy operation finalizes Baseline before
|
||||
the product runtime is ACTIVE.
|
||||
|
||||
## Runtime finalization
|
||||
|
||||
`release_metadata_adapter.py` writes no Git refs. Registry and Baseline are
|
||||
runtime state only and are atomic/CAS guarded. Baseline apply independently
|
||||
requires exact ACTIVE health, exact formal tag, exact Registry, and the exact
|
||||
previous Baseline SHA-256.
|
||||
|
||||
For this release the previous **production** baseline is V2R1C5. V2R1C6 has a
|
||||
formal tag but was never deployed, so rollback must return to V2R1C5.
|
||||
|
||||
## Governance
|
||||
|
||||
- force push: forbidden
|
||||
- tag rewrite/delete: forbidden
|
||||
- direct main push: forbidden
|
||||
- V2R1C5/V2R1C6 mutation: forbidden
|
||||
- plaintext secrets in Git: forbidden
|
||||
- exact binding replacement: manual Owner review required
|
||||
- after a verified Tag, uncertain errors become reconciliation-required
|
||||
- global `AIOS_GITOPS_WRITES_ENABLED` stays false; this executor is a narrow,
|
||||
separately installed production capability.
|
||||
@@ -0,0 +1,30 @@
|
||||
# AIOS Autonomous Release Campaign v1
|
||||
|
||||
This control plane changes the Owner interaction model from per-transaction
|
||||
approval to one durable, revocable campaign policy delegation.
|
||||
|
||||
The single bootstrap confirmation creates:
|
||||
1. a narrow task-writer grant for the control-plane implementation branch; and
|
||||
2. a root-owned campaign authorization in PENDING_CONTROL_PLANE_MERGE state.
|
||||
|
||||
After the protected control-plane PR merges, machine activation may move that
|
||||
same receipt to ACTIVE only when:
|
||||
- the merged main is a descendant of the campaign lineage anchor;
|
||||
- the campaign modules and tests are present in merged main;
|
||||
- the campaign receipt digest is valid;
|
||||
- no revocation exists;
|
||||
- global AIOS_GITOPS_WRITES_ENABLED remains false.
|
||||
|
||||
Once ACTIVE, routine engineering, protected PRs, exact release binding
|
||||
acceptance, tag publication, deployment, Registry/Baseline finalization and
|
||||
recoverable failure handling no longer ask the Owner for another interaction.
|
||||
|
||||
Manual Owner review remains mandatory only for:
|
||||
- formal same-name tag conflict;
|
||||
- force-push requirement;
|
||||
- published history rewrite;
|
||||
- non-fast-forward main divergence;
|
||||
- private signing material exposure;
|
||||
- repository identity conflict;
|
||||
- campaign revocation;
|
||||
- operation outside campaign scope.
|
||||
@@ -0,0 +1,53 @@
|
||||
# AIOS Local Owner Authentication Ingress
|
||||
|
||||
This component closes the Owner authentication bootstrap gap without creating a
|
||||
long-lived Owner PAT.
|
||||
|
||||
## Trust model
|
||||
|
||||
The ingress is deliberately one-shot and local-only.
|
||||
|
||||
1. A root-run server creates a Unix socket.
|
||||
2. The peer must be the configured OS Owner account (`aiman`).
|
||||
3. The server verifies Unix `SO_PEERCRED`, all four process UIDs, Linux audit
|
||||
`loginuid`, and ownership of the controlling TTY.
|
||||
4. A `sudo -u aiman` / `runuser` process from another authenticated login is
|
||||
rejected because its audit `loginuid` does not equal the Owner UID.
|
||||
5. The human Owner sees the exact task/base/branch/capability request and types
|
||||
a random one-time challenge.
|
||||
6. The server constructs only:
|
||||
- identity: `aiman`
|
||||
- role: `owner`
|
||||
- authenticated scope: `commit:create`
|
||||
- token scopes: [`commit:create`]
|
||||
7. It calls the existing `OwnerAuthorizationBroker`; it does not call the
|
||||
provisioner directly.
|
||||
8. It exits and removes the socket after the single authorization attempt.
|
||||
|
||||
No reusable token is minted and no production/tag/baseline/main capability is
|
||||
granted.
|
||||
|
||||
## Current V2R1C7 bootstrap binding
|
||||
|
||||
The bootstrap request is expected to authorize only:
|
||||
|
||||
- task: `V2R1C7_PRODUCTION_EXECUTOR_BOOTSTRAP`
|
||||
- branch: `agent/v2r1c7-production-executor-bootstrap-20260807-r2`
|
||||
- base: exact live `refs/heads/main`
|
||||
- repository: `aios/aios`
|
||||
- scope: `commit:create`
|
||||
- TTL: <= 900 seconds
|
||||
- production/tag/baseline/main write: false
|
||||
|
||||
## Installation model
|
||||
|
||||
For the current trust-root bootstrap, the exact SHA-verified script may be
|
||||
staged under `/run/aios-owner-bootstrap/` and run by a transient root systemd
|
||||
service. This staging step has no Git/Tag/Registry/Baseline side effect.
|
||||
|
||||
The Owner must connect from a real interactive login session as the OS account
|
||||
`aiman`.
|
||||
|
||||
After the governed writer is issued, the same module and tests are committed
|
||||
through the resulting protected engineering PR so future bootstrap uses code
|
||||
from main rather than an out-of-band artifact.
|
||||
@@ -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.
|
||||
@@ -0,0 +1,37 @@
|
||||
# Durable GitOps Job Controller
|
||||
|
||||
The control-plane write boundary is a durable Job, not a synchronous checkout
|
||||
or commit request. The source of truth is SQLite WAL at
|
||||
`/var/lib/aios-gitops/jobs/state.sqlite3`, with `jobs`, `idempotency_keys`,
|
||||
`branch_leases`, `state_transitions`, and `receipts` tables. Per-job
|
||||
`request.json`, `state.json`, `receipts/`, and `logs-digest/` files are
|
||||
durable mirrors/evidence, not a second state machine.
|
||||
|
||||
The controller owns only binding validation, idempotency, state transitions,
|
||||
and recovery classification. A supervised worker owns an isolated detached
|
||||
worktree and all Git side effects. `/api/file` and `/api/commit-file` adapters
|
||||
must return `job_id` and `side_effect_state`; they must never perform
|
||||
checkout, file write, `git add`, or `git commit` inline.
|
||||
|
||||
`/var/lib/aios-gitops/payloads/sha256/<digest>` is the content-addressed
|
||||
payload store. SQLite records only `payload_digest`, `payload_ref`, and size;
|
||||
the worker verifies both digest and size before consumption. The durable
|
||||
execution gate is `AIOS_GITOPS_DURABLE_JOB_EXECUTION_ENABLED=true`, while
|
||||
`AIOS_GITOPS_WRITES_ENABLED=false` remains the independent legacy repository
|
||||
mutation gate.
|
||||
|
||||
The idempotency key is the SHA-256 of the canonical tuple:
|
||||
|
||||
`campaign_id`, `repository`, `operation`, `branch`, `expected_branch_oid`, and
|
||||
`payload_digest`.
|
||||
|
||||
The writer lease is `CONTINUE_EXISTING` for the recovered campaign. It binds
|
||||
to lineage anchor `69b45e81dcd2c3707c367003b08c55fd361faae8` and exact initial
|
||||
head `bba675fab3b7e0c5fba7066720252b3c91b9f20d`. Every ref update is delegated
|
||||
to a non-Owner machine broker using compare-and-swap; a stale head is a
|
||||
`BRANCH_LEASE_STALE` condition and never a force push.
|
||||
|
||||
Production remains outside this worker contract. The fixed
|
||||
`aios-gitops-writer.service` uses `Accept=no`, automatic restart, and durable
|
||||
queue/reconciliation execution; it cannot mutate Git while the legacy global
|
||||
write gate is false or while the campaign/machine capability is absent.
|
||||
@@ -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}
|
||||
@@ -0,0 +1,15 @@
|
||||
# AIOS Mission Source Convergence
|
||||
|
||||
This directory contains the exact verified governance runtime sources migrated from the live control plane under `AIOS_AUTONOMOUS_RELEASE_MISSION_COMPLETION_AUTHORIZATION_V1`.
|
||||
|
||||
The executables remain capability-separated:
|
||||
|
||||
- `aios-governance-executor`: protected branch-policy GET, semantic CAS, and approved Stage-1 delta only.
|
||||
- `aios-governance-drift-monitor`: read-only semantic/transport drift classification.
|
||||
- `aios-governance-preapproval-publisher`: governance/request publication only; no protection, main, tag, production, Registry, Baseline, or credential mutation.
|
||||
- `semantic_protection.py`: canonical `aios.semantic-protection.v1` model.
|
||||
- `ref_reconciler.py`: Gitea-main to Authority FF-CAS reconciler with idempotent readback.
|
||||
|
||||
All credential values remain systemd-projected and are never part of source, logs, arguments, or receipts.
|
||||
|
||||
The live source hashes used for this migration are recorded in the mission receipt, while this Git tree is the permanent source of truth for future installs.
|
||||
@@ -0,0 +1,7 @@
|
||||
# AIOS Mission Zero-Interaction Self-Test
|
||||
|
||||
This marker is intentionally harmless and exists only to exercise the
|
||||
machine-authored branch, exact-head status publication, campaign approval gate,
|
||||
protected fast-forward merge, and Authority main CAS recovery path.
|
||||
|
||||
Production, tags, Registry, Baseline, and V2R1C7 remain unchanged.
|
||||
@@ -0,0 +1,15 @@
|
||||
# Bridge v2.1 Durable Reconciliation Bootstrap
|
||||
|
||||
This is the planning-only bootstrap artifact for
|
||||
`TASK-P0-BRIDGE-V21-DURABLE-RECONCILIATION-20260809`.
|
||||
|
||||
The task is bound to the exact base commit
|
||||
`3f670c387692b8865a10d5b419468d8a0b64d7a4` and the development branch
|
||||
`agent/p0-bridge-v21-durable-reconciliation-20260809`.
|
||||
|
||||
The first governed commit records the design boundary and acceptance contract
|
||||
only. It does not implement the v2.1 body, mutate main, create tags, change
|
||||
Baseline, or touch production. Future work must preserve absent-ref CAS,
|
||||
observation-first recovery, durable JobStore state, exact Authority/Gitea/API
|
||||
reconciliation, and the existing secret-scanning and credential-projection
|
||||
boundaries.
|
||||
+150
@@ -0,0 +1,150 @@
|
||||
# ChatGPT Bridge v2 Control Plane Convergence Design
|
||||
|
||||
Date: 2026-08-09
|
||||
Task: `TASK-P0-CHATGPT-BRIDGE-V2-CONTROL-PLANE-CONVERGENCE-20260809`
|
||||
Repository: `aios/aios`
|
||||
Approved development base: `2e8fa22989808a448065d1d76b5b18948774a637`
|
||||
Target branch: `agent/p0-chatgpt-bridge-v2-control-plane-convergence-20260809`
|
||||
|
||||
## Scope and authority
|
||||
|
||||
This design converges the ChatGPT GitOps Bridge, the existing Authorized Task Writer, the continuation lease protocol, the Durable GitOps Job Controller, and immutable control-plane runtime identity.
|
||||
|
||||
The development base is exact and must never be replaced with a dynamically resolved `HEAD`, `latest`, or `main`. If Authority `refs/heads/main` differs from the approved OID before the first governed writer push, execution fails with `BASE_OID_CHANGED`.
|
||||
|
||||
This task does not authorize production changes. The following remain immutable:
|
||||
|
||||
- Production Baseline `V2R1C7`.
|
||||
- Production target OID `c7adece1cc8beffe7c718d53c775898b45d6c7f8`.
|
||||
- Production tag OID `82792347937c0df59a8e02d6162775252bcad2e2`.
|
||||
- Production Registry, services, deployment state, runtime, tags, and release evidence.
|
||||
- Authority `main`.
|
||||
|
||||
Force push, history rewrite, direct main push, production mutation, tag mutation, baseline mutation, and secret commits are always denied.
|
||||
|
||||
## Immutable control-plane runtime identity
|
||||
|
||||
A control-plane release is installed into a versioned immutable directory equivalent to:
|
||||
|
||||
```text
|
||||
/opt/aios/gitops-control-plane/<release-id-or-oid>/
|
||||
```
|
||||
|
||||
The release manifest binds:
|
||||
|
||||
```text
|
||||
control_plane_release_version
|
||||
control_plane_target_oid
|
||||
artifact_sha256
|
||||
runtime_path
|
||||
bridge_schema_version
|
||||
repository_identity
|
||||
authoritative_git_dir
|
||||
deployment_state
|
||||
```
|
||||
|
||||
The validator is independent from the mutable Git workspace. It rejects execution when the runtime path is a workspace/worktree, the observed runtime commit or artifact digest differs from the manifest, the repository identity is not `aios/aios`, the authoritative Git directory is absent or mismatched, or the Bridge schema version is absent or unsupported.
|
||||
|
||||
Every failure is fail closed with `CONTROL_PLANE_RUNTIME_DRIFT`. No fallback to a mutable workspace is permitted. The installer and systemd template are staging-safe; this task may install and restart only a staging/test service.
|
||||
|
||||
## Bridge v2 adapter
|
||||
|
||||
The Bridge schema identity is `aios.chatgpt-gitops-bridge.v2`. It is independent from the production baseline, control-plane release version, and runtime release commit.
|
||||
|
||||
The Bridge exposes:
|
||||
|
||||
- `getRuntimeStatus`
|
||||
- `getGovernedWriteCapabilities`
|
||||
- `createGovernedTask`
|
||||
- `acquireWriterLease`
|
||||
- `commitFileGoverned`
|
||||
- `commitFilesGoverned`
|
||||
- `getGitOpsJob`
|
||||
- `resumeGitOpsJob`
|
||||
- `reconcileGitOpsJob`
|
||||
|
||||
The adapter reuses existing mechanisms and does not create a second writer, lease protocol, branch state store, job database, or direct Git mutation path.
|
||||
|
||||
## Governed task and lease flow
|
||||
|
||||
`createGovernedTask` binds an exact task ID, repository, approved base OID, non-main branch, risk, and description. Existing branches are read back and bound to their observed full OID. Idempotent replay returns the original binding; conflicting replay fails closed.
|
||||
|
||||
`acquireWriterLease` uses `WriterLeaseRequestV2`. Continuation uses:
|
||||
|
||||
```text
|
||||
branch_mode=CONTINUE_EXISTING
|
||||
expected_branch_oid=<full OID>
|
||||
generation=<monotonic integer>
|
||||
create_target_branch=false
|
||||
worktree_mode=detached_at_expected_head
|
||||
cas_ref_update_required=true
|
||||
```
|
||||
|
||||
Credentials remain server-side and never appear in responses, Git, durable job metadata, or logs.
|
||||
|
||||
## Governed commit flow
|
||||
|
||||
`commitFileGoverned` and `commitFilesGoverned` submit work to the existing `GitOpsJobController` and `JobStore`. The server computes payload digests. Raw file content is stored only in the content-addressed payload domain. Secret scanning, protected-path policy, repository binding, lease validation, and branch authorization run before side effects.
|
||||
|
||||
`commitFilesGoverned` is one atomic Git commit and enforces bounded file count, per-file size, and total payload size. Any failure leaves no partial commit or ref update.
|
||||
|
||||
All ref updates use compare-and-swap. Stale `expected_branch_oid` returns conflict and never triggers automatic rebase. After push, Authority and remote Gitea refs are independently read and must equal the resulting commit OID.
|
||||
|
||||
## Durable job lifecycle
|
||||
|
||||
Job reads return only non-sensitive metadata: `job_id`, `state`, `revision`, `side_effect_state`, `retryable`, `binding`, and `receipt_summary`.
|
||||
|
||||
Terminal jobs cannot be changed by ordinary resume. Resume requires `job_id` and `expected_revision` and uses revision CAS. Reconciliation is observation-first: for `UNKNOWN`, `RECONCILIATION_REQUIRED`, or `REMOTE_PUSH_PENDING`, local, Authority, and remote refs are read before state changes. A blind repeated push is never reconciliation.
|
||||
|
||||
## Legacy behavior
|
||||
|
||||
Legacy writes remain disabled while governed writes are enabled:
|
||||
|
||||
```text
|
||||
legacy_writes_enabled=false
|
||||
governed_writes_enabled=true
|
||||
```
|
||||
|
||||
The global `AIOS_GITOPS_WRITES_ENABLED` flag is never enabled as the solution. Legacy operations remain temporarily visible for compatibility and diagnostics, but return:
|
||||
|
||||
```json
|
||||
{
|
||||
"error": "LEGACY_WRITE_DISABLED",
|
||||
"replacement": "createGovernedTask",
|
||||
"governed_writes_available": true
|
||||
}
|
||||
```
|
||||
|
||||
## OpenAPI convergence
|
||||
|
||||
`docs/gitops/openapi-chatgpt-actions.yaml` is the canonical Bridge v2 schema and integrates the Authorized Task Writer fragment. The canonical CAS field is `expected_branch_oid`. `expected_head_oid` may be accepted only at an explicit deprecated adapter boundary.
|
||||
|
||||
Contract tests require exact equality between OpenAPI required fields, runtime parser required fields, and generated GPT Action schema required fields.
|
||||
|
||||
## Acceptance
|
||||
|
||||
Automated coverage includes runtime path/OID/digest/repository drift, exact task/branch/OID/lease/CAS behavior, job DB failure, restart during remote verification, remote timeout, response loss after remote success, idempotency replay/conflict, concurrent branch movement, unauthorized or main branch writes, protected paths, secret-like payloads, disabled governed writer, missing/expired lease, wrong repository identity, and observation-first reconciliation.
|
||||
|
||||
The staging sequence is:
|
||||
|
||||
```text
|
||||
getRuntimeStatus
|
||||
getGovernedWriteCapabilities
|
||||
createGovernedTask
|
||||
acquireWriterLease
|
||||
commitFilesGoverned
|
||||
getGitOpsJob
|
||||
verify Authority branch ref
|
||||
verify remote Gitea branch ref
|
||||
restart staging control-plane service
|
||||
getRuntimeStatus
|
||||
acquire continuation lease
|
||||
second governed commit
|
||||
verify remote ref again
|
||||
```
|
||||
|
||||
Completion requires all tests, staging restart continuation, Authority/remote OID equality, unchanged production invariants, no force push, no tag rewrite, no direct main push, and no secret commit.
|
||||
|
||||
## Implementation boundaries
|
||||
|
||||
Implementation is limited to the Bridge, existing writer and job-controller adapters, runtime identity/installer, staging systemd template, OpenAPI documents, tests, and engineering evidence. Hermes Adapter integration and production cutover are outside this task and require separate approval.
|
||||
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"artifact_path": "artifacts/v2r1c7/aios-V2R1C7-92b4a937e56b.tar.zst",
|
||||
"artifact_sha256": "6c7c763dc9a63a8411be3fc458881fc9bca1f403355d2f3a1cb338edbf9f24da",
|
||||
"artifact_size": 9672,
|
||||
"deployable": true,
|
||||
"executable_payload": true,
|
||||
"production_deployed": false,
|
||||
"required_components": [
|
||||
"runtime",
|
||||
"systemd_unit",
|
||||
"configuration",
|
||||
"health_probe",
|
||||
"rollback_metadata",
|
||||
"verifier",
|
||||
"release_metadata"
|
||||
],
|
||||
"required_members": 11,
|
||||
"schema": "aios.v2r1c7.artifact-verification.v2",
|
||||
"source_head_oid": "7f09670511138f1f1f74f6b40329cd35d6eb2e55",
|
||||
"status": "PASS",
|
||||
"version": "V2R1C7"
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"artifact_path": "artifacts/v2r1c7/aios-V2R1C7-92b4a937e56b.tar.zst",
|
||||
"artifact_sha256": "6c7c763dc9a63a8411be3fc458881fc9bca1f403355d2f3a1cb338edbf9f24da",
|
||||
"artifact_size": 9672,
|
||||
"base_oid": "b356dec2ac5bd2a5f721aa73f8fd1902f9c65632",
|
||||
"baseline_changed": false,
|
||||
"deployable": true,
|
||||
"evidence_stage": "PREMERGE_CANDIDATE",
|
||||
"executable_payload": true,
|
||||
"production_deployed": false,
|
||||
"schema": "aios.v2r1c7.build-receipt.v2",
|
||||
"source_head_oid": "7f09670511138f1f1f74f6b40329cd35d6eb2e55",
|
||||
"status": "PASS",
|
||||
"tag_created": false,
|
||||
"target_oid": "7f09670511138f1f1f74f6b40329cd35d6eb2e55",
|
||||
"version": "V2R1C7"
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"artifact_path": "artifacts/v2r1c7/aios-V2R1C7-92b4a937e56b.tar.zst",
|
||||
"artifact_sha256": "6c7c763dc9a63a8411be3fc458881fc9bca1f403355d2f3a1cb338edbf9f24da",
|
||||
"base_oid": "b356dec2ac5bd2a5f721aa73f8fd1902f9c65632",
|
||||
"baseline_modified": false,
|
||||
"candidate_source_head_oid": "7f09670511138f1f1f74f6b40329cd35d6eb2e55",
|
||||
"contains": [
|
||||
"BUILD_RECEIPT",
|
||||
"ARTIFACT_VERIFICATION",
|
||||
"STAGING_ACCEPTANCE",
|
||||
"ROLLBACK_VERIFICATION"
|
||||
],
|
||||
"final_production_evidence": false,
|
||||
"generated_at": "2026-08-07T03:29:43.262776+00:00",
|
||||
"parent_merge_target_oid": "b356dec2ac5bd2a5f721aa73f8fd1902f9c65632",
|
||||
"production_deployed": false,
|
||||
"registry_modified": false,
|
||||
"repository": "aios/aios",
|
||||
"rollback_verification": "PASS",
|
||||
"schema": "aios.v2r1c7.evidence-closure.v2",
|
||||
"staging_acceptance": "PASS",
|
||||
"status": "PREMERGE_CANDIDATE_EVIDENCE",
|
||||
"tag_rewritten": false,
|
||||
"target_branch": "release/v2r1c7-production-health-contract-20260807",
|
||||
"version": "V2R1C7"
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"artifact_sha256": "6c7c763dc9a63a8411be3fc458881fc9bca1f403355d2f3a1cb338edbf9f24da",
|
||||
"generated_at": "2026-08-07T03:29:43.262776+00:00",
|
||||
"health_after_rollback": "PASS",
|
||||
"production_changed": false,
|
||||
"production_deployed": false,
|
||||
"rollback_state": "ROLLED_BACK",
|
||||
"schema": "aios.v2r1c7.rollback-verification.v2",
|
||||
"source_head_oid": "7f09670511138f1f1f74f6b40329cd35d6eb2e55",
|
||||
"status": "PASS",
|
||||
"version": "V2R1C7"
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"artifact_sha256": "6c7c763dc9a63a8411be3fc458881fc9bca1f403355d2f3a1cb338edbf9f24da",
|
||||
"evidence_stage": "PREMERGE_CANDIDATE",
|
||||
"final_deployment_state": "ROLLED_BACK",
|
||||
"generated_at": "2026-08-07T03:29:43.262776+00:00",
|
||||
"health_artifact_sha256": "6c7c763dc9a63a8411be3fc458881fc9bca1f403355d2f3a1cb338edbf9f24da",
|
||||
"health_commit_oid": "7f09670511138f1f1f74f6b40329cd35d6eb2e55",
|
||||
"health_deployment_state": "STAGED",
|
||||
"health_http_status": 200,
|
||||
"health_probe": "PASS",
|
||||
"health_production_deployed": false,
|
||||
"health_timestamp_valid": true,
|
||||
"health_version": "V2R1C7",
|
||||
"install": "PASS",
|
||||
"listener": "PASS",
|
||||
"production_deployed": false,
|
||||
"restart": "PASS",
|
||||
"rollback": "PASS",
|
||||
"schema": "aios.v2r1c7.staging-acceptance.v2",
|
||||
"source_head_oid": "7f09670511138f1f1f74f6b40329cd35d6eb2e55",
|
||||
"status": "PASS",
|
||||
"systemd_verify": "PASS",
|
||||
"version": "V2R1C7"
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
[pytest]
|
||||
testpaths = tests
|
||||
python_files = test_*.py
|
||||
addopts = -ra --import-mode=importlib
|
||||
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"schema": "aios.release-source.v2",
|
||||
"version": "V2R1C7",
|
||||
"repository": "aios/aios",
|
||||
"base_ref": "refs/heads/main",
|
||||
"base_oid": "b356dec2ac5bd2a5f721aa73f8fd1902f9c65632",
|
||||
"binding_mode": "external_exact_oid",
|
||||
"release_readiness": "ENGINEERING_READY",
|
||||
"production_deployed": false,
|
||||
"tag_created": false,
|
||||
"baseline_changed": false,
|
||||
"protected_previous_tag": "V2R1C6",
|
||||
"production_service": "aios-v2r1c7-production.service",
|
||||
"production_port": 18927,
|
||||
"rollback_metadata": "deploy/aios-v2/v2r1c7-rollback.json",
|
||||
"controller_schema": "aios.release-request.v2",
|
||||
"required_checks": ["source_manifest", "deterministic_artifact", "systemd_verify", "health_probe", "listener", "restart_recovery", "rollback"]
|
||||
}
|
||||
@@ -0,0 +1,243 @@
|
||||
{
|
||||
"authority_proof": {
|
||||
"branch_oid": "2c6293160ed567a2098715edbb296741577c5bff",
|
||||
"commit_visible": true,
|
||||
"main_oid": "2e8fa22989808a448065d1d76b5b18948774a637",
|
||||
"v2r2c1_tag_oid": "998d30304787e31e5ae607c0bd86b5edb2b0e5ad"
|
||||
},
|
||||
"baseline_authority": {
|
||||
"artifact_sha256": "492338b895b152a55015fd92ea1d9dba2314894ab00c0ba219a78f3cef544b8a",
|
||||
"authority_sha256": "d8e23bad49bdb3e4192ba708c3dfe7fc5b7cdc6a513c2f87acd6412a51867231",
|
||||
"current_baseline": "V2R2C1",
|
||||
"raw_byte_sidecar_verification": "PASS",
|
||||
"target_oid": "998d30304787e31e5ae607c0bd86b5edb2b0e5ad"
|
||||
},
|
||||
"control_plane_source_oid": "b781129bd5a8b53c05ab648361424d2800166ee4",
|
||||
"diff_stat": "active-runtime/app/aios_authority_git_executor.py | 24 +\n active-runtime/app/aios_chatgpt_actions_api.py | 48 +-\n active-runtime/app/aios_chatgpt_bridge_v2.py | 167 ++++\n .../app/aios_chatgpt_bridge_v2_server.py | 431 +++++++++\n active-runtime/app/aios_continuation_executor.py | 594 ++++++++++++\n active-runtime/app/aios_continuation_lease.py | 6 +\n active-runtime/app/aios_control_plane_snapshot.py | 26 +\n active-runtime/app/aios_credential_broker.py | 78 ++\n active-runtime/app/aios_gitea_publisher.py | 93 ++\n active-runtime/app/aios_gitops_job_controller.py | 16 +-\n .../app/aios_gitops_reconciliation_core.py | 80 ++\n active-runtime/app/aios_governed_policy.py | 59 ++\n active-runtime/app/aios_governed_task_bootstrap.py | 84 ++\n active-runtime/app/aios_model_api_staging.py | 1022 ++++++++++++++++++++\n .../app/authorized_task_writer_provisioning.py | 4 +-\n active-runtime/app/baseline_authority.py | 84 ++\n active-runtime/app/governed_production_release.py | 568 +++++++++++\n active-runtime/app/v2r2c1_production_adapter.py | 135 +++\n .../systemd/aios-chatgpt-bridge-v2-staging.service | 23 +\n .../aios-gitops-model-api-v2r2c1-staging.service | 51 +\n ...09-bridge-v2.1-durable-reconciliation-design.md | 15 +\n ...t-bridge-v2-control-plane-convergence-design.md | 150 +++\n pytest.ini | 4 +\n scripts/build_v2r2c1_model_api_staging.py | 145 +++\n .../aios_release_controller/manifest.py | 4 +-\n .../test_v2r1c6_release_consistency.py | 4 +-\n .../aios_v2_candidate/test_v2r1c6_release_ready.py | 16 +-\n tests/conftest.py | 65 ++\n tests/test_bridge_v2_governed_http_bootstrap.py | 90 ++\n tests/test_governed_local_writer.py | 6 +-\n tests/test_model_api_immutable_staging.py | 35 +\n 31 files changed, 4108 insertions(+), 19 deletions(-)",
|
||||
"evidence_closure_oid": "a8b906d1bc24088c19647e98de6a679268f6d0a2",
|
||||
"forbidden_operations": [
|
||||
"direct_main_push",
|
||||
"main_merge_without_owner_approval",
|
||||
"force_push",
|
||||
"tag_mutation",
|
||||
"baseline_mutation",
|
||||
"registry_mutation",
|
||||
"production_restart",
|
||||
"production_cutover",
|
||||
"live_model_api_cutover",
|
||||
"history_rewrite",
|
||||
"secret_commit"
|
||||
],
|
||||
"governance": {
|
||||
"baseline_modified": false,
|
||||
"direct_main_push": false,
|
||||
"force_push_used": false,
|
||||
"history_rewritten": false,
|
||||
"live_model_api_cutover": false,
|
||||
"main_merged": false,
|
||||
"owner_approval_respected": true,
|
||||
"production_changed": false,
|
||||
"production_restarted": false,
|
||||
"registry_modified": false,
|
||||
"secrets_committed": false,
|
||||
"tag_rewritten": false
|
||||
},
|
||||
"included_path_count": 31,
|
||||
"integration_branch": "agent/v2r2c1-post-release-mainline-convergence-20260809",
|
||||
"integration_head_oid": "2c6293160ed567a2098715edbb296741577c5bff",
|
||||
"logical_commits": [
|
||||
{
|
||||
"message": "feat(v2r2c1): converge durable control-plane core",
|
||||
"oid": "40b685d1c70c767c7a79a2b184348e58e3e319ae",
|
||||
"parent_oid": "2e8fa22989808a448065d1d76b5b18948774a637"
|
||||
},
|
||||
{
|
||||
"message": "docs(v2r2c1): converge durable runtime and design sources",
|
||||
"oid": "5f616311bab38b0dac296ed8b0e69660e031a20e",
|
||||
"parent_oid": "40b685d1c70c767c7a79a2b184348e58e3e319ae"
|
||||
},
|
||||
{
|
||||
"message": "feat(v2r2c1): integrate governed release overlay",
|
||||
"oid": "57780e316e320a5128cc2c9f12adbb19ae57aef5",
|
||||
"parent_oid": "5f616311bab38b0dac296ed8b0e69660e031a20e"
|
||||
},
|
||||
{
|
||||
"message": "feat(v2r2c1): wire governed release operations into durable bridge",
|
||||
"oid": "e41b8d323a3e7a40440f5d32221f524074bea5fe",
|
||||
"parent_oid": "57780e316e320a5128cc2c9f12adbb19ae57aef5"
|
||||
},
|
||||
{
|
||||
"message": "test: repair canonical pytest repository entrypoint",
|
||||
"oid": "b20cb8130e4c68267d5d85b02eed51cb8d4f41c0",
|
||||
"parent_oid": "e41b8d323a3e7a40440f5d32221f524074bea5fe"
|
||||
},
|
||||
{
|
||||
"message": "test: make whole-suite collection deterministic",
|
||||
"oid": "c6a1693efaede9dace9b929d884a09e162a02c89",
|
||||
"parent_oid": "b20cb8130e4c68267d5d85b02eed51cb8d4f41c0"
|
||||
},
|
||||
{
|
||||
"message": "test: preserve legacy suite imports under canonical collection",
|
||||
"oid": "91f2ea0591787d3dbf8c2e27ab0460629d86900e",
|
||||
"parent_oid": "c6a1693efaede9dace9b929d884a09e162a02c89"
|
||||
},
|
||||
{
|
||||
"message": "test: expose repository test helpers to canonical collection",
|
||||
"oid": "85897d3f6d708271dff16767265f9d14b2f29801",
|
||||
"parent_oid": "91f2ea0591787d3dbf8c2e27ab0460629d86900e"
|
||||
},
|
||||
{
|
||||
"message": "fix(v2r2c1): close slash-branch and suite contract regressions",
|
||||
"oid": "f1110d24f30d8441f2df04e0774ee230b95c2e02",
|
||||
"parent_oid": "85897d3f6d708271dff16767265f9d14b2f29801"
|
||||
},
|
||||
{
|
||||
"message": "test: align writer regression with durable idempotent replay",
|
||||
"oid": "c10d2a5dfcc394dde7a7022952d7b423725747a3",
|
||||
"parent_oid": "f1110d24f30d8441f2df04e0774ee230b95c2e02"
|
||||
},
|
||||
{
|
||||
"message": "feat(v2r2c1): source baseline authority for model-api staging",
|
||||
"oid": "f969aedaf61501a866435c712be01ff2cfe43fb1",
|
||||
"parent_oid": "c10d2a5dfcc394dde7a7022952d7b423725747a3"
|
||||
},
|
||||
{
|
||||
"message": "feat(v2r2c1): add immutable model api staging runtime",
|
||||
"oid": "4ab70bfdb4d34aa9d1dabc4230a3367eaa93cc9b",
|
||||
"parent_oid": "f969aedaf61501a866435c712be01ff2cfe43fb1"
|
||||
},
|
||||
{
|
||||
"message": "fix(v2r2c1): avoid builder secret-scan false positives",
|
||||
"oid": "7c4e86bfc1f39e643e672968d1fdedbd72ace56d",
|
||||
"parent_oid": "4ab70bfdb4d34aa9d1dabc4230a3367eaa93cc9b"
|
||||
},
|
||||
{
|
||||
"message": "fix(v2r2c1): isolate model api staging port",
|
||||
"oid": "7564363a30bfa7fa81ac5c7c545ec704984ec543",
|
||||
"parent_oid": "7c4e86bfc1f39e643e672968d1fdedbd72ace56d"
|
||||
},
|
||||
{
|
||||
"message": "fix(v2r2c1): bind baseline authority and openapi staging path",
|
||||
"oid": "2c6293160ed567a2098715edbb296741577c5bff",
|
||||
"parent_oid": "7564363a30bfa7fa81ac5c7c545ec704984ec543"
|
||||
}
|
||||
],
|
||||
"main_base_oid": "2e8fa22989808a448065d1d76b5b18948774a637",
|
||||
"mandatory_gates": {
|
||||
"baseline_registry_readback_staging_pass": true,
|
||||
"durable_source_path_manifest_complete": true,
|
||||
"existing_v2r2c1_suite_pass": true,
|
||||
"governed_release_suite_pass": true,
|
||||
"integration_authority_git_api_equal": true,
|
||||
"integration_branch_governed": true,
|
||||
"logical_slice_commits_remote_verified": true,
|
||||
"main_base_exact_before_branch_create": true,
|
||||
"model_api_immutable_runtime_staging_pass": true,
|
||||
"owner_merge_request_generated_execute_false": true,
|
||||
"production_unchanged": true,
|
||||
"pytest_collection_entrypoints_fixed": true,
|
||||
"pytest_tests_results_recorded": true
|
||||
},
|
||||
"model_api_staging": {
|
||||
"artifact_sha256": "492338b895b152a55015fd92ea1d9dba2314894ab00c0ba219a78f3cef544b8a",
|
||||
"baseline": {
|
||||
"authority_sha256": "d8e23bad49bdb3e4192ba708c3dfe7fc5b7cdc6a513c2f87acd6412a51867231",
|
||||
"current_baseline": "V2R2C1",
|
||||
"deployment_state": "ACTIVE",
|
||||
"evidence_closure_oid": "a8b906d1bc24088c19647e98de6a679268f6d0a2",
|
||||
"generation": 4,
|
||||
"previous_baseline": "V2R1C7",
|
||||
"production_deployed": true,
|
||||
"status": "PRODUCTION_DEPLOYED",
|
||||
"tag_oid": "07fc2746b688f5f243f29b39de39f32edc3dd9d2",
|
||||
"target_oid": "998d30304787e31e5ae607c0bd86b5edb2b0e5ad"
|
||||
},
|
||||
"baseline_status": 200,
|
||||
"deployment_state": "STAGING",
|
||||
"health": {
|
||||
"artifact_sha256": "492338b895b152a55015fd92ea1d9dba2314894ab00c0ba219a78f3cef544b8a",
|
||||
"commit_oid": "2c6293160ed567a2098715edbb296741577c5bff",
|
||||
"deployment_state": "STAGING",
|
||||
"governed_release_routing": "bridge-v2",
|
||||
"legacy_global_writes_enabled": false,
|
||||
"production_deployed": false,
|
||||
"runtime_mode": "IMMUTABLE_STAGING",
|
||||
"status": "ok",
|
||||
"version": "V2R2C1"
|
||||
},
|
||||
"health_status": 200,
|
||||
"legacy_write_probe": "423_BLOCKED",
|
||||
"port": 18893,
|
||||
"restart_acceptance": "PASS",
|
||||
"runtime_root": "/opt/aios/gitops-model-api/2c6293160ed567a2098715edbb296741577c5bff/runtime",
|
||||
"source_oid": "2c6293160ed567a2098715edbb296741577c5bff",
|
||||
"systemd": "MainPID=788782\nExecStart={ path=/usr/bin/python3 ; argv[]=/usr/bin/python3 /opt/aios/gitops-model-api/2c6293160ed567a2098715edbb296741577c5bff/runtime/app/aios_model_api_staging.py ; ignore_errors=no ; start_time=[Sun 2026-08-09 19:11:19 CST] ; stop_time=[n/a] ; pid=788782 ; code=(null) ; status=0/0 }\nActiveState=active\nSubState=running",
|
||||
"unit": "aios-gitops-model-api-v2r2c1-staging.service",
|
||||
"versions": {
|
||||
"authoritative_head_oid": "2e8fa22989808a448065d1d76b5b18948774a637",
|
||||
"baseline_generation": 4,
|
||||
"baseline_sha256": "d8e23bad49bdb3e4192ba708c3dfe7fc5b7cdc6a513c2f87acd6412a51867231",
|
||||
"baseline_version": "V2R2C1",
|
||||
"query_error": null
|
||||
},
|
||||
"versions_status": 200
|
||||
},
|
||||
"owner_merge_request": {
|
||||
"control_plane_source_oid": "b781129bd5a8b53c05ab648361424d2800166ee4",
|
||||
"execute": false,
|
||||
"integration_head_oid": "b2cf510e11b712bb99f20ded81459b6524257394",
|
||||
"main_expected_old_oid": "2e8fa22989808a448065d1d76b5b18948774a637",
|
||||
"release_target_oid": "998d30304787e31e5ae607c0bd86b5edb2b0e5ad",
|
||||
"report_commit_oid": "b2cf510e11b712bb99f20ded81459b6524257394",
|
||||
"status": "OWNER_APPROVAL_REQUIRED"
|
||||
},
|
||||
"placeholder_fields": [],
|
||||
"production": {
|
||||
"baseline_mutation": false,
|
||||
"health": {
|
||||
"artifact_sha256": "ab4ae257346c82863f05abf5e309381a1c86612957608c3a4f4ab5cc9233f052",
|
||||
"commit_oid": "998d30304787e31e5ae607c0bd86b5edb2b0e5ad",
|
||||
"deployment_state": "ACTIVE",
|
||||
"health_timestamp": "2026-08-09T11:21:16.872581+00:00",
|
||||
"production_deployed": true,
|
||||
"status": "ok",
|
||||
"version": "V2R2C1"
|
||||
},
|
||||
"health_status": 200,
|
||||
"live_model_api_show": "MainPID=2816107\nActiveState=active\nSubState=running",
|
||||
"live_model_api_unchanged": true,
|
||||
"production_cutover": false,
|
||||
"production_restart": false,
|
||||
"registry_mutation": false,
|
||||
"shadow_service_unchanged": true,
|
||||
"shadow_show": "MainPID=1505658\nActiveState=active\nSubState=running",
|
||||
"tag_mutation": false
|
||||
},
|
||||
"release_target_oid": "998d30304787e31e5ae607c0bd86b5edb2b0e5ad",
|
||||
"remote_proof": {
|
||||
"api_branch_oid": "2c6293160ed567a2098715edbb296741577c5bff",
|
||||
"api_verification": "paginated_exact_name",
|
||||
"commit_visible": true,
|
||||
"git_remote_oid": "2c6293160ed567a2098715edbb296741577c5bff",
|
||||
"main_api_oid": "2e8fa22989808a448065d1d76b5b18948774a637",
|
||||
"main_git_oid": "2e8fa22989808a448065d1d76b5b18948774a637"
|
||||
},
|
||||
"report_commit_parent_oid": "2c6293160ed567a2098715edbb296741577c5bff",
|
||||
"repository": "aios/aios",
|
||||
"schema": "aios.v2r2c1.post-release-mainline-convergence.acceptance.v1",
|
||||
"status": "V2R2C1_MAINLINE_CONVERGENCE_READY_FOR_OWNER_MERGE",
|
||||
"success_status": "V2R2C1_MAINLINE_CONVERGENCE_READY_FOR_OWNER_MERGE",
|
||||
"task_id": "TASK-P0-V2R2C1-POST-RELEASE-MAINLINE-CONVERGENCE-20260809",
|
||||
"tests": {
|
||||
"collection": "PASS",
|
||||
"command": "pytest tests",
|
||||
"failed": 0,
|
||||
"passed": 709,
|
||||
"skipped": 2,
|
||||
"skipped_tests": [
|
||||
"tests/test_gitea_tag_publication_live.py (live opt-in)",
|
||||
"tests/test_signer_contract_compatibility.py (live integration opt-in)"
|
||||
],
|
||||
"subtests": 154
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
# V2R2C1 post-release mainline convergence diff summary
|
||||
|
||||
- task: `TASK-P0-V2R2C1-POST-RELEASE-MAINLINE-CONVERGENCE-20260809`
|
||||
- repository: `aios/aios`
|
||||
- main base: `2e8fa22989808a448065d1d76b5b18948774a637`
|
||||
- release target: `998d30304787e31e5ae607c0bd86b5edb2b0e5ad`
|
||||
- control-plane source: `b781129bd5a8b53c05ab648361424d2800166ee4`
|
||||
- implementation integration head before this report commit: `2c6293160ed567a2098715edbb296741577c5bff`
|
||||
- branch: `agent/v2r2c1-post-release-mainline-convergence-20260809`
|
||||
|
||||
## Logical governed commits
|
||||
|
||||
| OID | Parent | Message |
|
||||
|---|---|---|
|
||||
| `40b685d1c70c767c7a79a2b184348e58e3e319ae` | `2e8fa22989808a448065d1d76b5b18948774a637` | feat(v2r2c1): converge durable control-plane core |
|
||||
| `5f616311bab38b0dac296ed8b0e69660e031a20e` | `40b685d1c70c767c7a79a2b184348e58e3e319ae` | docs(v2r2c1): converge durable runtime and design sources |
|
||||
| `57780e316e320a5128cc2c9f12adbb19ae57aef5` | `5f616311bab38b0dac296ed8b0e69660e031a20e` | feat(v2r2c1): integrate governed release overlay |
|
||||
| `e41b8d323a3e7a40440f5d32221f524074bea5fe` | `57780e316e320a5128cc2c9f12adbb19ae57aef5` | feat(v2r2c1): wire governed release operations into durable bridge |
|
||||
| `b20cb8130e4c68267d5d85b02eed51cb8d4f41c0` | `e41b8d323a3e7a40440f5d32221f524074bea5fe` | test: repair canonical pytest repository entrypoint |
|
||||
| `c6a1693efaede9dace9b929d884a09e162a02c89` | `b20cb8130e4c68267d5d85b02eed51cb8d4f41c0` | test: make whole-suite collection deterministic |
|
||||
| `91f2ea0591787d3dbf8c2e27ab0460629d86900e` | `c6a1693efaede9dace9b929d884a09e162a02c89` | test: preserve legacy suite imports under canonical collection |
|
||||
| `85897d3f6d708271dff16767265f9d14b2f29801` | `91f2ea0591787d3dbf8c2e27ab0460629d86900e` | test: expose repository test helpers to canonical collection |
|
||||
| `f1110d24f30d8441f2df04e0774ee230b95c2e02` | `85897d3f6d708271dff16767265f9d14b2f29801` | fix(v2r2c1): close slash-branch and suite contract regressions |
|
||||
| `c10d2a5dfcc394dde7a7022952d7b423725747a3` | `f1110d24f30d8441f2df04e0774ee230b95c2e02` | test: align writer regression with durable idempotent replay |
|
||||
| `f969aedaf61501a866435c712be01ff2cfe43fb1` | `c10d2a5dfcc394dde7a7022952d7b423725747a3` | feat(v2r2c1): source baseline authority for model-api staging |
|
||||
| `4ab70bfdb4d34aa9d1dabc4230a3367eaa93cc9b` | `f969aedaf61501a866435c712be01ff2cfe43fb1` | feat(v2r2c1): add immutable model api staging runtime |
|
||||
| `7c4e86bfc1f39e643e672968d1fdedbd72ace56d` | `4ab70bfdb4d34aa9d1dabc4230a3367eaa93cc9b` | fix(v2r2c1): avoid builder secret-scan false positives |
|
||||
| `7564363a30bfa7fa81ac5c7c545ec704984ec543` | `7c4e86bfc1f39e643e672968d1fdedbd72ace56d` | fix(v2r2c1): isolate model api staging port |
|
||||
| `2c6293160ed567a2098715edbb296741577c5bff` | `7564363a30bfa7fa81ac5c7c545ec704984ec543` | fix(v2r2c1): bind baseline authority and openapi staging path |
|
||||
|
||||
## Exact diff stat
|
||||
|
||||
```text
|
||||
active-runtime/app/aios_authority_git_executor.py | 24 +
|
||||
active-runtime/app/aios_chatgpt_actions_api.py | 48 +-
|
||||
active-runtime/app/aios_chatgpt_bridge_v2.py | 167 ++++
|
||||
.../app/aios_chatgpt_bridge_v2_server.py | 431 +++++++++
|
||||
active-runtime/app/aios_continuation_executor.py | 594 ++++++++++++
|
||||
active-runtime/app/aios_continuation_lease.py | 6 +
|
||||
active-runtime/app/aios_control_plane_snapshot.py | 26 +
|
||||
active-runtime/app/aios_credential_broker.py | 78 ++
|
||||
active-runtime/app/aios_gitea_publisher.py | 93 ++
|
||||
active-runtime/app/aios_gitops_job_controller.py | 16 +-
|
||||
.../app/aios_gitops_reconciliation_core.py | 80 ++
|
||||
active-runtime/app/aios_governed_policy.py | 59 ++
|
||||
active-runtime/app/aios_governed_task_bootstrap.py | 84 ++
|
||||
active-runtime/app/aios_model_api_staging.py | 1022 ++++++++++++++++++++
|
||||
.../app/authorized_task_writer_provisioning.py | 4 +-
|
||||
active-runtime/app/baseline_authority.py | 84 ++
|
||||
active-runtime/app/governed_production_release.py | 568 +++++++++++
|
||||
active-runtime/app/v2r2c1_production_adapter.py | 135 +++
|
||||
.../systemd/aios-chatgpt-bridge-v2-staging.service | 23 +
|
||||
.../aios-gitops-model-api-v2r2c1-staging.service | 51 +
|
||||
...09-bridge-v2.1-durable-reconciliation-design.md | 15 +
|
||||
...t-bridge-v2-control-plane-convergence-design.md | 150 +++
|
||||
pytest.ini | 4 +
|
||||
scripts/build_v2r2c1_model_api_staging.py | 145 +++
|
||||
.../aios_release_controller/manifest.py | 4 +-
|
||||
.../test_v2r1c6_release_consistency.py | 4 +-
|
||||
.../aios_v2_candidate/test_v2r1c6_release_ready.py | 16 +-
|
||||
tests/conftest.py | 65 ++
|
||||
tests/test_bridge_v2_governed_http_bootstrap.py | 90 ++
|
||||
tests/test_governed_local_writer.py | 6 +-
|
||||
tests/test_model_api_immutable_staging.py | 35 +
|
||||
31 files changed, 4108 insertions(+), 19 deletions(-)
|
||||
```
|
||||
|
||||
## Included durable source domains
|
||||
|
||||
- Bridge v2 adapter/server and governed HTTP contracts.
|
||||
- Durable reconciliation core, continuation executor/lease, Authority Git CAS adapter, job controller compatibility.
|
||||
- Gitea publisher/verifier, credential broker, governed policy, control-plane snapshot, release routing.
|
||||
- Canonical pytest entrypoint/conftest and regression coverage.
|
||||
- Source-controlled immutable model-api staging runtime, builder and systemd template.
|
||||
- Selected design specifications required for durable mainline context.
|
||||
|
||||
## Explicitly excluded
|
||||
|
||||
- `main` ref mutation or merge without Owner approval.
|
||||
- Production service, Registry, Baseline body/sidecar, production tags and mutable `/var/lib` state.
|
||||
- Installed `/opt` artifacts, temporary staging transition state and task-local receipts/credentials.
|
||||
- Production cutover/restart, force push, history rewrite and plaintext secrets.
|
||||
|
||||
## Verification
|
||||
|
||||
- `pytest tests`: **709 passed, 2 skipped, 154 subtests, 0 failed**.
|
||||
- Staging model-api: immutable OID/digest bound, port 18893, health/base/ver/OpenAPI readback PASS, restart recovery PASS.
|
||||
- Authority/Gitea Git/Gitea API commit equality and commit visibility PASS.
|
||||
- Production remains V2R2C1 and was not restarted or cut over.
|
||||
@@ -0,0 +1,254 @@
|
||||
{
|
||||
"control_plane_source_oid": "b781129bd5a8b53c05ab648361424d2800166ee4",
|
||||
"diff_stat": "active-runtime/app/aios_authority_git_executor.py | 24 +\n active-runtime/app/aios_chatgpt_actions_api.py | 48 +-\n active-runtime/app/aios_chatgpt_bridge_v2.py | 167 ++++\n .../app/aios_chatgpt_bridge_v2_server.py | 431 +++++++++\n active-runtime/app/aios_continuation_executor.py | 594 ++++++++++++\n active-runtime/app/aios_continuation_lease.py | 6 +\n active-runtime/app/aios_control_plane_snapshot.py | 26 +\n active-runtime/app/aios_credential_broker.py | 78 ++\n active-runtime/app/aios_gitea_publisher.py | 93 ++\n active-runtime/app/aios_gitops_job_controller.py | 16 +-\n .../app/aios_gitops_reconciliation_core.py | 80 ++\n active-runtime/app/aios_governed_policy.py | 59 ++\n active-runtime/app/aios_governed_task_bootstrap.py | 84 ++\n active-runtime/app/aios_model_api_staging.py | 1022 ++++++++++++++++++++\n .../app/authorized_task_writer_provisioning.py | 4 +-\n active-runtime/app/baseline_authority.py | 84 ++\n active-runtime/app/governed_production_release.py | 568 +++++++++++\n active-runtime/app/v2r2c1_production_adapter.py | 135 +++\n .../systemd/aios-chatgpt-bridge-v2-staging.service | 23 +\n .../aios-gitops-model-api-v2r2c1-staging.service | 51 +\n ...09-bridge-v2.1-durable-reconciliation-design.md | 15 +\n ...t-bridge-v2-control-plane-convergence-design.md | 150 +++\n pytest.ini | 4 +\n scripts/build_v2r2c1_model_api_staging.py | 145 +++\n .../aios_release_controller/manifest.py | 4 +-\n .../test_v2r1c6_release_consistency.py | 4 +-\n .../aios_v2_candidate/test_v2r1c6_release_ready.py | 16 +-\n tests/conftest.py | 65 ++\n tests/test_bridge_v2_governed_http_bootstrap.py | 90 ++\n tests/test_governed_local_writer.py | 6 +-\n tests/test_model_api_immutable_staging.py | 35 +\n 31 files changed, 4108 insertions(+), 19 deletions(-)",
|
||||
"evidence_closure_oid": "a8b906d1bc24088c19647e98de6a679268f6d0a2",
|
||||
"excluded_paths": [
|
||||
{
|
||||
"path": "refs/heads/main",
|
||||
"reason": "main merge requires separate Owner approval; unchanged"
|
||||
},
|
||||
{
|
||||
"path": "refs/tags/*",
|
||||
"reason": "formal tags are outside engineering convergence scope; unchanged"
|
||||
},
|
||||
{
|
||||
"path": "baseline/current.json and sidecar",
|
||||
"reason": "production mutable authority and sidecar; read-only verified"
|
||||
},
|
||||
{
|
||||
"path": "/var/lib/aios-gitops/**",
|
||||
"reason": "host runtime state and receipts excluded from repository source"
|
||||
},
|
||||
{
|
||||
"path": "/opt/aios/**",
|
||||
"reason": "installed immutable staging artifacts excluded; digest recorded in reports"
|
||||
},
|
||||
{
|
||||
"path": "credentials and secrets",
|
||||
"reason": "availability-only; no plaintext credential committed"
|
||||
},
|
||||
{
|
||||
"path": "production runtime and Registry",
|
||||
"reason": "production cutover/registry mutation forbidden"
|
||||
}
|
||||
],
|
||||
"included_paths": [
|
||||
{
|
||||
"path": "active-runtime/app/aios_authority_git_executor.py",
|
||||
"status": "M"
|
||||
},
|
||||
{
|
||||
"path": "active-runtime/app/aios_chatgpt_actions_api.py",
|
||||
"status": "M"
|
||||
},
|
||||
{
|
||||
"path": "active-runtime/app/aios_chatgpt_bridge_v2.py",
|
||||
"status": "A"
|
||||
},
|
||||
{
|
||||
"path": "active-runtime/app/aios_chatgpt_bridge_v2_server.py",
|
||||
"status": "A"
|
||||
},
|
||||
{
|
||||
"path": "active-runtime/app/aios_continuation_executor.py",
|
||||
"status": "A"
|
||||
},
|
||||
{
|
||||
"path": "active-runtime/app/aios_continuation_lease.py",
|
||||
"status": "M"
|
||||
},
|
||||
{
|
||||
"path": "active-runtime/app/aios_control_plane_snapshot.py",
|
||||
"status": "A"
|
||||
},
|
||||
{
|
||||
"path": "active-runtime/app/aios_credential_broker.py",
|
||||
"status": "A"
|
||||
},
|
||||
{
|
||||
"path": "active-runtime/app/aios_gitea_publisher.py",
|
||||
"status": "A"
|
||||
},
|
||||
{
|
||||
"path": "active-runtime/app/aios_gitops_job_controller.py",
|
||||
"status": "M"
|
||||
},
|
||||
{
|
||||
"path": "active-runtime/app/aios_gitops_reconciliation_core.py",
|
||||
"status": "A"
|
||||
},
|
||||
{
|
||||
"path": "active-runtime/app/aios_governed_policy.py",
|
||||
"status": "A"
|
||||
},
|
||||
{
|
||||
"path": "active-runtime/app/aios_governed_task_bootstrap.py",
|
||||
"status": "A"
|
||||
},
|
||||
{
|
||||
"path": "active-runtime/app/aios_model_api_staging.py",
|
||||
"status": "A"
|
||||
},
|
||||
{
|
||||
"path": "active-runtime/app/authorized_task_writer_provisioning.py",
|
||||
"status": "M"
|
||||
},
|
||||
{
|
||||
"path": "active-runtime/app/baseline_authority.py",
|
||||
"status": "A"
|
||||
},
|
||||
{
|
||||
"path": "active-runtime/app/governed_production_release.py",
|
||||
"status": "A"
|
||||
},
|
||||
{
|
||||
"path": "active-runtime/app/v2r2c1_production_adapter.py",
|
||||
"status": "A"
|
||||
},
|
||||
{
|
||||
"path": "deploy/systemd/aios-chatgpt-bridge-v2-staging.service",
|
||||
"status": "A"
|
||||
},
|
||||
{
|
||||
"path": "deploy/systemd/aios-gitops-model-api-v2r2c1-staging.service",
|
||||
"status": "A"
|
||||
},
|
||||
{
|
||||
"path": "docs/superpowers/specs/2026-08-09-bridge-v2.1-durable-reconciliation-design.md",
|
||||
"status": "A"
|
||||
},
|
||||
{
|
||||
"path": "docs/superpowers/specs/2026-08-09-chatgpt-bridge-v2-control-plane-convergence-design.md",
|
||||
"status": "A"
|
||||
},
|
||||
{
|
||||
"path": "pytest.ini",
|
||||
"status": "A"
|
||||
},
|
||||
{
|
||||
"path": "scripts/build_v2r2c1_model_api_staging.py",
|
||||
"status": "A"
|
||||
},
|
||||
{
|
||||
"path": "services/aios-release-controller/aios_release_controller/manifest.py",
|
||||
"status": "M"
|
||||
},
|
||||
{
|
||||
"path": "tests/aios_v2_candidate/test_v2r1c6_release_consistency.py",
|
||||
"status": "M"
|
||||
},
|
||||
{
|
||||
"path": "tests/aios_v2_candidate/test_v2r1c6_release_ready.py",
|
||||
"status": "M"
|
||||
},
|
||||
{
|
||||
"path": "tests/conftest.py",
|
||||
"status": "A"
|
||||
},
|
||||
{
|
||||
"path": "tests/test_bridge_v2_governed_http_bootstrap.py",
|
||||
"status": "A"
|
||||
},
|
||||
{
|
||||
"path": "tests/test_governed_local_writer.py",
|
||||
"status": "M"
|
||||
},
|
||||
{
|
||||
"path": "tests/test_model_api_immutable_staging.py",
|
||||
"status": "A"
|
||||
}
|
||||
],
|
||||
"integration_branch": "agent/v2r2c1-post-release-mainline-convergence-20260809",
|
||||
"integration_head_oid": "2c6293160ed567a2098715edbb296741577c5bff",
|
||||
"logical_commits": [
|
||||
{
|
||||
"message": "feat(v2r2c1): converge durable control-plane core",
|
||||
"oid": "40b685d1c70c767c7a79a2b184348e58e3e319ae",
|
||||
"parent_oid": "2e8fa22989808a448065d1d76b5b18948774a637"
|
||||
},
|
||||
{
|
||||
"message": "docs(v2r2c1): converge durable runtime and design sources",
|
||||
"oid": "5f616311bab38b0dac296ed8b0e69660e031a20e",
|
||||
"parent_oid": "40b685d1c70c767c7a79a2b184348e58e3e319ae"
|
||||
},
|
||||
{
|
||||
"message": "feat(v2r2c1): integrate governed release overlay",
|
||||
"oid": "57780e316e320a5128cc2c9f12adbb19ae57aef5",
|
||||
"parent_oid": "5f616311bab38b0dac296ed8b0e69660e031a20e"
|
||||
},
|
||||
{
|
||||
"message": "feat(v2r2c1): wire governed release operations into durable bridge",
|
||||
"oid": "e41b8d323a3e7a40440f5d32221f524074bea5fe",
|
||||
"parent_oid": "57780e316e320a5128cc2c9f12adbb19ae57aef5"
|
||||
},
|
||||
{
|
||||
"message": "test: repair canonical pytest repository entrypoint",
|
||||
"oid": "b20cb8130e4c68267d5d85b02eed51cb8d4f41c0",
|
||||
"parent_oid": "e41b8d323a3e7a40440f5d32221f524074bea5fe"
|
||||
},
|
||||
{
|
||||
"message": "test: make whole-suite collection deterministic",
|
||||
"oid": "c6a1693efaede9dace9b929d884a09e162a02c89",
|
||||
"parent_oid": "b20cb8130e4c68267d5d85b02eed51cb8d4f41c0"
|
||||
},
|
||||
{
|
||||
"message": "test: preserve legacy suite imports under canonical collection",
|
||||
"oid": "91f2ea0591787d3dbf8c2e27ab0460629d86900e",
|
||||
"parent_oid": "c6a1693efaede9dace9b929d884a09e162a02c89"
|
||||
},
|
||||
{
|
||||
"message": "test: expose repository test helpers to canonical collection",
|
||||
"oid": "85897d3f6d708271dff16767265f9d14b2f29801",
|
||||
"parent_oid": "91f2ea0591787d3dbf8c2e27ab0460629d86900e"
|
||||
},
|
||||
{
|
||||
"message": "fix(v2r2c1): close slash-branch and suite contract regressions",
|
||||
"oid": "f1110d24f30d8441f2df04e0774ee230b95c2e02",
|
||||
"parent_oid": "85897d3f6d708271dff16767265f9d14b2f29801"
|
||||
},
|
||||
{
|
||||
"message": "test: align writer regression with durable idempotent replay",
|
||||
"oid": "c10d2a5dfcc394dde7a7022952d7b423725747a3",
|
||||
"parent_oid": "f1110d24f30d8441f2df04e0774ee230b95c2e02"
|
||||
},
|
||||
{
|
||||
"message": "feat(v2r2c1): source baseline authority for model-api staging",
|
||||
"oid": "f969aedaf61501a866435c712be01ff2cfe43fb1",
|
||||
"parent_oid": "c10d2a5dfcc394dde7a7022952d7b423725747a3"
|
||||
},
|
||||
{
|
||||
"message": "feat(v2r2c1): add immutable model api staging runtime",
|
||||
"oid": "4ab70bfdb4d34aa9d1dabc4230a3367eaa93cc9b",
|
||||
"parent_oid": "f969aedaf61501a866435c712be01ff2cfe43fb1"
|
||||
},
|
||||
{
|
||||
"message": "fix(v2r2c1): avoid builder secret-scan false positives",
|
||||
"oid": "7c4e86bfc1f39e643e672968d1fdedbd72ace56d",
|
||||
"parent_oid": "4ab70bfdb4d34aa9d1dabc4230a3367eaa93cc9b"
|
||||
},
|
||||
{
|
||||
"message": "fix(v2r2c1): isolate model api staging port",
|
||||
"oid": "7564363a30bfa7fa81ac5c7c545ec704984ec543",
|
||||
"parent_oid": "7c4e86bfc1f39e643e672968d1fdedbd72ace56d"
|
||||
},
|
||||
{
|
||||
"message": "fix(v2r2c1): bind baseline authority and openapi staging path",
|
||||
"oid": "2c6293160ed567a2098715edbb296741577c5bff",
|
||||
"parent_oid": "7564363a30bfa7fa81ac5c7c545ec704984ec543"
|
||||
}
|
||||
],
|
||||
"main_base_oid": "2e8fa22989808a448065d1d76b5b18948774a637",
|
||||
"release_target_oid": "998d30304787e31e5ae607c0bd86b5edb2b0e5ad",
|
||||
"remote_proof": {
|
||||
"api_branch_oid": "2c6293160ed567a2098715edbb296741577c5bff",
|
||||
"api_verification": "paginated_exact_name",
|
||||
"commit_visible": true,
|
||||
"git_remote_oid": "2c6293160ed567a2098715edbb296741577c5bff",
|
||||
"main_api_oid": "2e8fa22989808a448065d1d76b5b18948774a637",
|
||||
"main_git_oid": "2e8fa22989808a448065d1d76b5b18948774a637"
|
||||
},
|
||||
"report_commit_parent_oid": "2c6293160ed567a2098715edbb296741577c5bff",
|
||||
"repository": "aios/aios",
|
||||
"schema": "aios.v2r2c1.post-release-mainline-convergence.path-manifest.v1",
|
||||
"task_id": "TASK-P0-V2R2C1-POST-RELEASE-MAINLINE-CONVERGENCE-20260809"
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
{
|
||||
"artifact_sha256": "492338b895b152a55015fd92ea1d9dba2314894ab00c0ba219a78f3cef544b8a",
|
||||
"baseline_authority_path": "/var/lib/aios-gitops-runtime-anchor/baseline/current.json",
|
||||
"checks": {
|
||||
"artifact_sha256": "PASS",
|
||||
"baseline_registry_readback": "PASS",
|
||||
"health_contract": "PASS",
|
||||
"legacy_write_rejection": "PASS",
|
||||
"live_service_unchanged": "PASS",
|
||||
"manifest_binding": "PASS",
|
||||
"openapi_readback": "PASS",
|
||||
"restart_recovery": "PASS"
|
||||
},
|
||||
"current_live_port": 18890,
|
||||
"current_live_service": "aios-gitops-model-api.service",
|
||||
"deployment_state": "STAGING",
|
||||
"execute": false,
|
||||
"governed_release_routing": "bridge-v2",
|
||||
"legacy_global_writes_enabled": false,
|
||||
"live_cutover": false,
|
||||
"owner_actions_required": [
|
||||
"review exact source OID and artifact SHA",
|
||||
"approve a separate future live model-api cutover",
|
||||
"approve any production service transition"
|
||||
],
|
||||
"production_restart": false,
|
||||
"repository_identity": "aios/aios",
|
||||
"runtime_mode": "IMMUTABLE_STAGING",
|
||||
"schema": "aios.v2r2c1.model-api-runtime-activation-plan.v1",
|
||||
"source_oid": "2c6293160ed567a2098715edbb296741577c5bff",
|
||||
"staging_port": 18893,
|
||||
"staging_runtime_root": "/opt/aios/gitops-model-api/2c6293160ed567a2098715edbb296741577c5bff/runtime",
|
||||
"staging_service": "aios-gitops-model-api-v2r2c1-staging.service",
|
||||
"status": "OWNER_APPROVAL_REQUIRED",
|
||||
"target_version": "V2R2C1",
|
||||
"task_id": "TASK-P0-V2R2C1-POST-RELEASE-MAINLINE-CONVERGENCE-20260809"
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user