Compare commits

..
90 changed files with 4961 additions and 11246 deletions
+7 -14
View File
@@ -13,7 +13,6 @@ from hashlib import sha256
from pathlib import Path
import json
from typing import Any
from message_handoff import stable_event_id
def utc_now() -> str:
@@ -56,19 +55,13 @@ class AgentEventBus:
payload: dict[str, Any],
correlation_id: str | None = None,
) -> dict[str, Any]:
event_id = stable_event_id(
source=source, target=target, message_type=kind, payload=payload,
tenant_id=(payload.get('tenant_id') if isinstance(payload, dict) else None),
user_id=(payload.get('user_id') if isinstance(payload, dict) else None),
project_id=(payload.get('project_id') if isinstance(payload, dict) else None),
conversation_id=(payload.get('conversation_id') if isinstance(payload, dict) else None),
task_id=(payload.get('task_id') if isinstance(payload, dict) else None),
run_id=(payload.get('run_id') if isinstance(payload, dict) else None),
correlation_id=(payload.get('correlation_id') if isinstance(payload, dict) else None),
causation_id=(payload.get('causation_id') if isinstance(payload, dict) else None),
sequence=(payload.get('sequence') if isinstance(payload, dict) else None),
idempotency_key=(payload.get('idempotency_key') if isinstance(payload, dict) else None),
)
event_id = "evt_" + sha256(
json.dumps(
{"kind": kind, "source": source, "summary": summary, "payload": payload},
sort_keys=True,
ensure_ascii=False,
).encode("utf-8")
).hexdigest()[:16]
event = AgentEvent(event_id, kind, source, target, summary, payload, correlation_id=correlation_id)
self._write("pending", event.to_dict())
self._audit("created", event.to_dict())
@@ -7,7 +7,7 @@ class AgentEventObservabilityBridge:
def __init__(self, hooks: AgentObservabilityHooks | None = None):
self.hooks = hooks or AgentObservabilityHooks()
def consume(self, event: dict, *, actor=None, trace=None):
def consume(self, event: dict):
kind = event.get("kind")
payload = event.get("payload", {})
if kind == "agent_started":
@@ -16,8 +16,6 @@ class AgentEventObservabilityBridge:
visible_scope=payload.get("scope", "self"),
entry_agent=payload.get("agent", "unknown"),
execution_id=event.get("id"),
actor=actor,
trace=trace,
)
if kind == "agent_delegated":
return self.hooks.delegate(event["execution_id"], payload.get("agent", "unknown"))
-12
View File
@@ -31,18 +31,6 @@ class AgentExecutionRecord:
status: str = "completed"
started_at: str = field(default_factory=utc_now)
ended_at: str = field(default_factory=utc_now)
trace_id: str | None = None
span_id: str | None = None
parent_span_id: str | None = None
correlation_id: str | None = None
causation_id: str | None = None
tenant_id: str | None = None
project_id: str | None = None
conversation_id: str | None = None
task_id: str | None = None
claim_id: str | None = None
run_id: str | None = None
message_id: str | None = None
def to_dict(self):
return asdict(self)
+6 -30
View File
@@ -9,49 +9,25 @@ try:
except Exception:
APIRouter = None
from .agent_observability import AgentExecutionRecord, AgentObservability
from .agent_observability import AgentExecutionRecord
from .agent_observability_dashboard import AgentObservabilityDashboard
from .agent_observability_store import AgentObservabilityStore
from .trace_tool_credential_governance import GovernanceActor, TraceContext, authorize_scope
store = AgentObservabilityStore()
dashboard = AgentObservabilityDashboard(store.load())
def record_agent_execution(payload: dict[str, Any], *, actor: GovernanceActor | dict[str, Any] | None = None, trace: TraceContext | None = None):
scoped = any(payload.get(name) is not None for name in (
"trace_id", "span_id", "tenant_id", "project_id", "conversation_id", "task_id", "run_id"
))
values = dict(payload)
if scoped:
if actor is None or trace is None:
raise RuntimeError("TRACE_ACTOR_CONTEXT_REQUIRED")
trusted = GovernanceActor.from_value(actor)
authorize_scope(trusted, trace.scope)
bound = {
"trace_id": trace.trace_id, "span_id": trace.span_id,
"parent_span_id": trace.parent_span_id, "correlation_id": trace.correlation_id,
"causation_id": trace.causation_id, "tenant_id": trace.scope.tenant_id,
"user_id": trace.scope.user_id, "project_id": trace.scope.project_id,
"conversation_id": trace.scope.conversation_id, "task_id": trace.task_id,
"claim_id": trace.claim_id, "run_id": trace.scope.run_id, "message_id": trace.message_id,
}
for name, authoritative in bound.items():
if values.get(name) not in (None, authoritative):
raise RuntimeError("TRACE_SCOPE_MISMATCH_DENY:" + name)
values[name] = authoritative
record = AgentExecutionRecord(**values)
def record_agent_execution(payload: dict[str, Any]):
record = AgentExecutionRecord(**payload)
return {"status": "ok", "record": store.append(record)}
def get_agent_observability(user_id: str | None = None, group_id: str | None = None, role: str = "user", *, actor: GovernanceActor | dict[str, Any] | None = None):
records = store.query(user_id, group_id, role, actor=actor)
visible = AgentObservability()
visible.records = [AgentExecutionRecord(**record) for record in records]
def get_agent_observability(user_id: str | None = None, group_id: str | None = None, role: str = "user"):
records = store.query(user_id, group_id, role)
return {
"status": "ok",
"records": records,
"summary": visible.summary(None),
"summary": store.load().summary(user_id if role != "owner" else None),
}
@@ -14,11 +14,6 @@ from uuid import uuid4
from .agent_observability import AgentExecutionRecord
from .agent_observability_store import AgentObservabilityStore
from .trace_tool_credential_governance import (
GovernanceActor,
TraceContext,
authorize_scope,
)
def utc_now() -> str:
@@ -41,7 +36,6 @@ class AgentRunContext:
input_tokens: int = 0
output_tokens: int = 0
status: str = "running"
trace: TraceContext | None = None
def delegate(self, agent_name: str) -> None:
if agent_name not in self.path:
@@ -65,19 +59,7 @@ class AgentObservabilityHooks:
visible_scope: str,
entry_agent: str,
execution_id: str | None = None,
*,
actor: GovernanceActor | dict[str, Any] | None = None,
trace: TraceContext | None = None,
) -> dict[str, Any]:
if execution_id is not None and execution_id in self.active:
raise RuntimeError("OBSERVABILITY_EXECUTION_CONFLICT")
if trace is not None:
if actor is None:
raise RuntimeError("TRACE_ACTOR_CONTEXT_REQUIRED")
trusted_actor = GovernanceActor.from_value(actor)
authorize_scope(trusted_actor, trace.scope)
if trace.scope.user_id is not None and trace.scope.user_id != user_id:
raise RuntimeError("TRACE_SCOPE_MISMATCH_DENY:user_id")
run = AgentRunContext(
execution_id=execution_id or f"exec_{uuid4().hex[:12]}",
user_id=user_id,
@@ -85,7 +67,6 @@ class AgentObservabilityHooks:
entry_agent=entry_agent,
path=[entry_agent],
used_hermes=entry_agent.lower() == "hermes",
trace=trace,
)
self.active[run.execution_id] = run
return {"status": "started", "execution_id": run.execution_id, "run": run.__dict__}
@@ -125,18 +106,6 @@ class AgentObservabilityHooks:
status=status,
started_at=run.started_at,
ended_at=ended_at,
trace_id=run.trace.trace_id if run.trace else None,
span_id=run.trace.span_id if run.trace else None,
parent_span_id=run.trace.parent_span_id if run.trace else None,
correlation_id=run.trace.correlation_id if run.trace else None,
causation_id=run.trace.causation_id if run.trace else None,
tenant_id=run.trace.scope.tenant_id if run.trace else None,
project_id=run.trace.scope.project_id if run.trace else None,
conversation_id=run.trace.scope.conversation_id if run.trace else None,
task_id=run.trace.task_id if run.trace else None,
claim_id=run.trace.claim_id if run.trace else None,
run_id=run.trace.scope.run_id if run.trace else None,
message_id=run.trace.message_id if run.trace else None,
)
return {"status": "stored", "record": self.store.append(record)}
@@ -12,12 +12,6 @@ from pathlib import Path
from typing import Any
from .agent_observability import AgentExecutionRecord, AgentObservability
from .trace_tool_credential_governance import (
GovernanceActor,
GovernanceError,
GovernanceScope,
authorize_scope,
)
class AgentObservabilityStore:
@@ -29,11 +23,6 @@ class AgentObservabilityStore:
def append(self, record: AgentExecutionRecord) -> dict[str, Any]:
path = self.records_dir / f"{record.execution_id}.json"
data = record.to_dict()
if path.exists():
existing = json.loads(path.read_text(encoding="utf-8"))
if existing == data:
return existing
raise RuntimeError("OBSERVABILITY_EXECUTION_CONFLICT")
path.write_text(json.dumps(data, ensure_ascii=False, indent=2), encoding="utf-8")
return data
@@ -44,36 +33,8 @@ class AgentObservabilityStore:
observability.records.append(AgentExecutionRecord(**raw))
return observability
def query(self, user_id: str | None = None, group_id: str | None = None, role: str = "user", *, actor: GovernanceActor | dict[str, Any] | None = None) -> list[dict[str, Any]]:
def query(self, user_id: str | None = None, group_id: str | None = None, role: str = "user") -> list[dict[str, Any]]:
records = [record.to_dict() for record in self.load().records]
scoped = [record for record in records if record.get("tenant_id") is not None]
legacy = [record for record in records if record.get("tenant_id") is None]
if actor is not None:
trusted = GovernanceActor.from_value(actor)
visible: list[dict[str, Any]] = []
for record in scoped:
try:
authorize_scope(
trusted,
GovernanceScope(
"tenant",
tenant_id=record.get("tenant_id"),
user_id=record.get("user_id"),
project_id=record.get("project_id"),
conversation_id=record.get("conversation_id"),
run_id=record.get("run_id"),
),
)
except GovernanceError:
continue
visible.append(record)
if trusted.privileged:
visible.extend(legacy)
else:
visible.extend(record for record in legacy if record.get("user_id") == trusted.scope.user_id)
return visible
# Caller-supplied role remains a legacy compatibility path only.
records = legacy
if role == "owner":
return records
if role == "group_admin" and group_id:
@@ -1,427 +0,0 @@
"""Owner-controlled, CAS-bound retry extension for exhausted Online Agent Tasks."""
from __future__ import annotations
from dataclasses import dataclass
from datetime import datetime, timezone
import hashlib
import hmac
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
import json
import os
from pathlib import Path
import re
import sqlite3
from typing import Any, Mapping
TASK_RE = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._:-]{2,179}$")
KEY_RE = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._:-]{7,159}$")
MAX_BODY = 64 * 1024
AUTHORIZE_EXTENSION_ID_ENV = "AIOS_AGENT_TASK_AUTHORIZATION_ID"
class RetryExtensionError(RuntimeError):
def __init__(self, code: str, status: int = 409) -> None:
super().__init__(code)
self.code = code
self.status = status
def _canonical(value: object) -> bytes:
return json.dumps(value, sort_keys=True, separators=(",", ":"), ensure_ascii=False).encode("utf-8")
def _sha(value: object) -> str:
return hashlib.sha256(_canonical(value)).hexdigest()
def _required_authorization_id() -> str:
value = os.environ.get(AUTHORIZE_EXTENSION_ID_ENV, "").strip()
if not value:
raise RuntimeError("TASK_RETRY_AUTHORIZATION_ID_MISSING")
if KEY_RE.fullmatch(value) is None:
raise RuntimeError("TASK_RETRY_AUTHORIZATION_ID_INVALID")
return value
def _now() -> str:
return datetime.now(timezone.utc).isoformat()
def _require_string(value: object, code: str, pattern: re.Pattern[str] | None = None) -> str:
if not isinstance(value, str) or not value.strip():
raise RetryExtensionError(code, 400)
value = value.strip()
if pattern is not None and pattern.fullmatch(value) is None:
raise RetryExtensionError(code, 400)
return value
@dataclass(frozen=True)
class RetryExtensionRequest:
expected_status: str
expected_revision: int
expected_attempt: int
expected_max_attempts: int
next_max_attempts: int
previous_verification_sha256: str
authorization_id: str
idempotency_key: str
@classmethod
def from_mapping(cls, value: Mapping[str, Any]) -> "RetryExtensionRequest":
allowed = {
"expected_status", "expected_revision", "expected_attempt", "expected_max_attempts",
"next_max_attempts", "previous_verification_sha256", "authorization_id", "idempotency_key",
}
if set(value) != allowed:
raise RetryExtensionError("RETRY_EXTENSION_FIELDS_INVALID", 400)
status = _require_string(value["expected_status"], "EXPECTED_STATUS_INVALID")
if status != "FAILED":
raise RetryExtensionError("EXPECTED_STATUS_INVALID", 400)
ints: dict[str, int] = {}
for field in ("expected_revision", "expected_attempt", "expected_max_attempts", "next_max_attempts"):
item = value[field]
if not isinstance(item, int) or isinstance(item, bool) or item < 0:
raise RetryExtensionError("RETRY_EXTENSION_INTEGER_INVALID", 400)
ints[field] = item
if ints["expected_attempt"] != ints["expected_max_attempts"]:
raise RetryExtensionError("RETRY_EXTENSION_NOT_EXHAUSTED", 409)
if ints["next_max_attempts"] != ints["expected_max_attempts"] + 1:
raise RetryExtensionError("RETRY_EXTENSION_MUST_BE_BOUNDED_ONE_STEP", 400)
if ints["next_max_attempts"] > 50:
raise RetryExtensionError("RETRY_EXTENSION_LIMIT_EXCEEDED", 400)
verification = _require_string(value["previous_verification_sha256"], "VERIFICATION_SHA256_INVALID")
if re.fullmatch(r"[0-9a-f]{64}", verification) is None:
raise RetryExtensionError("VERIFICATION_SHA256_INVALID", 400)
authorization = _require_string(value["authorization_id"], "AUTHORIZATION_ID_INVALID", KEY_RE)
key = _require_string(value["idempotency_key"], "IDEMPOTENCY_KEY_INVALID", KEY_RE)
return cls(
status,
ints["expected_revision"],
ints["expected_attempt"],
ints["expected_max_attempts"],
ints["next_max_attempts"],
verification,
authorization,
key,
)
def public(self) -> dict[str, Any]:
return {
"expected_status": self.expected_status,
"expected_revision": self.expected_revision,
"expected_attempt": self.expected_attempt,
"expected_max_attempts": self.expected_max_attempts,
"next_max_attempts": self.next_max_attempts,
"previous_verification_sha256": self.previous_verification_sha256,
"authorization_id": self.authorization_id,
"idempotency_key": self.idempotency_key,
}
class RetryExtensionStore:
"""Owns the official mutation. Direct/manual DB edits remain forbidden."""
def __init__(self, db_path: Path) -> None:
self.db_path = Path(db_path)
def _connect(self) -> sqlite3.Connection:
connection = sqlite3.connect(self.db_path, timeout=30.0)
connection.row_factory = sqlite3.Row
connection.execute("PRAGMA foreign_keys=ON")
connection.execute("PRAGMA busy_timeout=30000")
return connection
@staticmethod
def _columns(connection: sqlite3.Connection, table: str) -> dict[str, sqlite3.Row]:
return {str(row["name"]): row for row in connection.execute(f'PRAGMA table_info("{table}")')}
@staticmethod
def _event_table(connection: sqlite3.Connection) -> tuple[str, dict[str, sqlite3.Row]]:
tables = {
row["name"]
for row in connection.execute("SELECT name FROM sqlite_master WHERE type='table'")
}
for name in ("task_events", "events"):
if name not in tables:
continue
columns = RetryExtensionStore._columns(connection, name)
if "task_id" in columns and ("event_type" in columns or "type" in columns):
return name, columns
raise RetryExtensionError("TASK_EVENT_TABLE_UNSUPPORTED", 503)
@staticmethod
def _verification_hash(raw: object) -> str:
if raw in (None, ""):
value: object = {}
elif isinstance(raw, str):
try:
value = json.loads(raw)
except json.JSONDecodeError as exc:
raise RetryExtensionError("TASK_VERIFICATION_INVALID", 409) from exc
else:
value = raw
return _sha(value)
@staticmethod
def _ensure_extension_table(connection: sqlite3.Connection) -> None:
connection.execute(
"""
CREATE TABLE IF NOT EXISTS task_retry_extensions (
task_id TEXT NOT NULL,
idempotency_key TEXT NOT NULL,
request_sha256 TEXT NOT NULL,
authorization_id TEXT NOT NULL,
previous_verification_sha256 TEXT NOT NULL,
previous_max_attempts INTEGER NOT NULL,
next_max_attempts INTEGER NOT NULL,
expected_revision INTEGER NOT NULL,
applied_revision INTEGER NOT NULL,
created_at TEXT NOT NULL,
PRIMARY KEY(task_id, idempotency_key)
)
"""
)
@staticmethod
def _insert_event(
connection: sqlite3.Connection,
task_id: str,
payload: Mapping[str, Any],
) -> None:
table, columns = RetryExtensionStore._event_table(connection)
values: dict[str, Any] = {"task_id": task_id}
if "event_type" in columns:
values["event_type"] = "TASK_RETRY_EXTENSION_AUTHORIZED"
else:
values["type"] = "TASK_RETRY_EXTENSION_AUTHORIZED"
payload_text = json.dumps(payload, sort_keys=True, separators=(",", ":"))
if "payload_json" in columns:
values["payload_json"] = payload_text
elif "payload" in columns:
values["payload"] = payload_text
else:
raise RetryExtensionError("TASK_EVENT_PAYLOAD_UNSUPPORTED", 503)
if "created_at" in columns:
values["created_at"] = _now()
if "agent_id" in columns:
values["agent_id"] = "owner-control"
if "actor_id" in columns:
values["actor_id"] = "owner-control"
names = list(values)
sql = f'INSERT INTO "{table}" ({",".join(f"""\"{name}\"""" for name in names)}) VALUES ({",".join("?" for _ in names)})'
connection.execute(sql, tuple(values[name] for name in names))
def extend(self, task_id: str, request: RetryExtensionRequest) -> dict[str, Any]:
task_id = _require_string(task_id, "TASK_ID_INVALID", TASK_RE)
required_authorization_id = _required_authorization_id()
if request.authorization_id != required_authorization_id:
raise RetryExtensionError("RETRY_EXTENSION_AUTHORIZATION_ID_MISMATCH", 403)
request_payload = request.public()
request_sha = _sha(request_payload)
with self._connect() as connection:
connection.execute("BEGIN IMMEDIATE")
self._ensure_extension_table(connection)
replay = connection.execute(
"SELECT * FROM task_retry_extensions WHERE task_id=? AND idempotency_key=?",
(task_id, request.idempotency_key),
).fetchone()
if replay is not None:
if replay["request_sha256"] != request_sha:
raise RetryExtensionError("RETRY_EXTENSION_IDEMPOTENCY_CONFLICT", 409)
connection.commit()
return {
"task_id": task_id,
"status": "QUEUED",
"attempt": request.expected_attempt,
"max_attempts": int(replay["next_max_attempts"]),
"revision": int(replay["applied_revision"]),
"idempotent": True,
"authorization_id": replay["authorization_id"],
"previous_verification_sha256": replay["previous_verification_sha256"],
}
columns = self._columns(connection, "tasks")
required = {
"task_id", "status", "attempt", "max_attempts", "revision", "claimed_by",
"lease_token_hash", "lease_expires_at", "verification_json",
}
if not required.issubset(columns):
raise RetryExtensionError("TASK_SCHEMA_RETRY_EXTENSION_UNSUPPORTED", 503)
task = connection.execute("SELECT * FROM tasks WHERE task_id=?", (task_id,)).fetchone()
if task is None:
raise RetryExtensionError("TASK_NOT_FOUND", 404)
actual_verification_sha = self._verification_hash(task["verification_json"])
if actual_verification_sha != request.previous_verification_sha256:
raise RetryExtensionError("RETRY_EXTENSION_VERIFICATION_DRIFT", 409)
if (
task["status"] != request.expected_status
or int(task["revision"]) != request.expected_revision
or int(task["attempt"]) != request.expected_attempt
or int(task["max_attempts"]) != request.expected_max_attempts
):
raise RetryExtensionError("RETRY_EXTENSION_CAS_MISMATCH", 409)
for lease_field in ("lease_token_hash", "lease_expires_at"):
if lease_field in columns and task[lease_field] not in (None, ""):
raise RetryExtensionError("RETRY_EXTENSION_ACTIVE_LEASE_DENY", 409)
previous_claimed_by = task["claimed_by"]
applied_revision = request.expected_revision + 1
cursor = connection.execute(
"""
UPDATE tasks
SET status='QUEUED', max_attempts=?, revision=?, claimed_by=NULL,
lease_token_hash=NULL, lease_expires_at=NULL
WHERE task_id=? AND status=? AND revision=? AND attempt=? AND max_attempts=?
AND claimed_by IS ?
AND lease_token_hash IS NULL AND lease_expires_at IS NULL
""",
(
request.next_max_attempts,
applied_revision,
task_id,
request.expected_status,
request.expected_revision,
request.expected_attempt,
request.expected_max_attempts,
previous_claimed_by,
),
)
if cursor.rowcount != 1:
raise RetryExtensionError("RETRY_EXTENSION_CAS_MISMATCH", 409)
event_payload = {
"schema": "aios.task-retry-extension.event.v1",
"task_id": task_id,
"authorization_id": request.authorization_id,
"idempotency_key": request.idempotency_key,
"previous_verification_sha256": request.previous_verification_sha256,
"previous_max_attempts": request.expected_max_attempts,
"next_max_attempts": request.next_max_attempts,
"previous_revision": request.expected_revision,
"applied_revision": applied_revision,
"request_sha256": request_sha,
"previous_claimed_by": previous_claimed_by,
"stale_terminal_claim_normalized": previous_claimed_by not in (None, ""),
}
self._insert_event(connection, task_id, event_payload)
connection.execute(
"""
INSERT INTO task_retry_extensions(
task_id,idempotency_key,request_sha256,authorization_id,previous_verification_sha256,
previous_max_attempts,next_max_attempts,expected_revision,applied_revision,created_at
) VALUES (?,?,?,?,?,?,?,?,?,?)
""",
(
task_id,
request.idempotency_key,
request_sha,
request.authorization_id,
request.previous_verification_sha256,
request.expected_max_attempts,
request.next_max_attempts,
request.expected_revision,
applied_revision,
_now(),
),
)
connection.commit()
return {
"task_id": task_id,
"status": "QUEUED",
"attempt": request.expected_attempt,
"max_attempts": request.next_max_attempts,
"revision": applied_revision,
"idempotent": False,
"authorization_id": request.authorization_id,
"previous_verification_sha256": request.previous_verification_sha256,
}
class RetryExtensionHandler(BaseHTTPRequestHandler):
store: RetryExtensionStore | None = None
control_token: str = ""
def log_message(self, *_args: Any) -> None:
return
def _json(self, value: Mapping[str, Any], status: int) -> None:
data = json.dumps(dict(value), sort_keys=True, separators=(",", ":")).encode("utf-8")
self.send_response(status)
self.send_header("Content-Type", "application/json")
self.send_header("Content-Length", str(len(data)))
self.end_headers()
self.wfile.write(data)
def _authorized(self) -> bool:
header = self.headers.get("Authorization", "")
return (
header.startswith("Bearer ")
and bool(self.control_token)
and hmac.compare_digest(header[7:], self.control_token)
)
def do_GET(self) -> None:
if self.path == "/health":
self._json({"status": "ok", "service": "aios-agent-task-retry-extension"}, 200)
else:
self._json({"error": "NOT_FOUND"}, 404)
def do_POST(self) -> None:
match = re.fullmatch(r"/v1/tasks/([^/]+)/retry-extension", self.path)
if match is None:
self._json({"error": "NOT_FOUND"}, 404)
return
if not self._authorized():
self._json({"error": "UNAUTHORIZED"}, 401)
return
try:
length = int(self.headers.get("Content-Length", "0"))
except ValueError:
self._json({"error": "CONTENT_LENGTH_INVALID"}, 400)
return
if length < 1 or length > MAX_BODY:
self._json({"error": "REQUEST_SIZE_INVALID"}, 413)
return
try:
value = json.loads(self.rfile.read(length).decode("utf-8"))
if not isinstance(value, Mapping):
raise ValueError
request = RetryExtensionRequest.from_mapping(value)
if self.store is None:
raise RetryExtensionError("RUNTIME_UNAVAILABLE", 503)
result = self.store.extend(match.group(1), request)
self._json(result, 200 if result["idempotent"] else 201)
except RetryExtensionError as exc:
self._json({"error": exc.code}, exc.status)
except (UnicodeDecodeError, json.JSONDecodeError, ValueError):
self._json({"error": "REQUEST_INVALID"}, 400)
def load_control_token(path: Path) -> str:
if path.is_symlink() or not path.is_file():
raise RuntimeError("TASK_RETRY_CONTROL_CREDENTIAL_UNAVAILABLE")
value = path.read_text(encoding="utf-8").strip()
if len(value) < 32:
raise RuntimeError("TASK_RETRY_CONTROL_CREDENTIAL_INVALID")
return value
def serve() -> None:
db = Path(os.environ.get("AIOS_AGENT_TASK_DB", "/var/lib/aios-agent-task/state.sqlite3"))
_required_authorization_id()
token_path = Path(
os.environ.get(
"AIOS_AGENT_TASK_CONTROL_TOKEN_FILE",
str(Path(os.environ.get("CREDENTIALS_DIRECTORY", "/run/credentials")) / "agent_task_control_token"),
)
)
host = os.environ.get("AIOS_TASK_RETRY_HOST", "127.0.0.1")
port = int(os.environ.get("AIOS_TASK_RETRY_PORT", "18941"))
RetryExtensionHandler.store = RetryExtensionStore(db)
RetryExtensionHandler.control_token = load_control_token(token_path)
ThreadingHTTPServer((host, port), RetryExtensionHandler).serve_forever()
if __name__ == "__main__":
serve()
@@ -94,91 +94,6 @@ class AuthorityGitExecutor:
self._git(["worktree", "add", "--detach", str(path), base_oid])
return path
def recover_failed_detached_worktree_exact(
self,
*,
path: Path,
expected_oid: str,
stale_oid: str,
target_ref: str,
) -> str:
"""Recreate one exact clean, detached, single-attempt worktree at its durable head."""
_oid(expected_oid, "expected_oid")
_oid(stale_oid, "stale_oid")
if not TASK_BRANCH_RE.fullmatch(target_ref):
raise AuthorityGitError("TASK_BRANCH_SCOPE_DENIED")
if self.read_ref(target_ref) is not None:
raise AuthorityGitError("AUTHORITY_TARGET_BRANCH_ALREADY_EXISTS")
if (
not path.is_absolute()
or path.parent != self.worktree_root
or path.name in {"", ".", ".."}
or path.is_symlink()
or not path.is_dir()
):
raise AuthorityGitError("WORKTREE_RECOVERY_PATH_INVALID")
head = subprocess.run(
["git", "-C", str(path), "rev-parse", "HEAD"],
text=True, capture_output=True, check=False,
)
if head.returncode != 0 or head.stdout.strip() != stale_oid or stale_oid == expected_oid:
raise AuthorityGitError("WORKTREE_RECOVERY_HEAD_INVALID")
symbolic = subprocess.run(
["git", "-C", str(path), "symbolic-ref", "-q", "HEAD"],
text=True, capture_output=True, check=False,
)
if symbolic.returncode != 1:
raise AuthorityGitError("WORKTREE_RECOVERY_NOT_DETACHED")
status = subprocess.run(
["git", "-C", str(path), "status", "--porcelain=v1", "--untracked-files=all"],
text=True, capture_output=True, check=False,
)
if status.returncode != 0 or status.stdout:
raise AuthorityGitError("WORKTREE_RECOVERY_NOT_CLEAN")
parents = self._git(["rev-list", "--parents", "-n", "1", stale_oid]).split()
if parents != [stale_oid, expected_oid]:
raise AuthorityGitError("WORKTREE_RECOVERY_LINEAGE_INVALID")
registered = [
line.removeprefix("worktree ")
for line in self._git(["worktree", "list", "--porcelain"]).splitlines()
if line.startswith("worktree ")
]
if registered.count(str(path)) != 1:
raise AuthorityGitError("WORKTREE_RECOVERY_REGISTRATION_INVALID")
self._git(["worktree", "remove", "--force", str(path)])
if path.exists() or path.is_symlink():
raise AuthorityGitError("WORKTREE_RECOVERY_REMOVE_FAILED")
recreated = self.create_detached_worktree(expected_oid, path.name, path.parent)
readback = subprocess.run(
["git", "-C", str(recreated), "rev-parse", "HEAD"],
text=True, capture_output=True, check=False,
)
detached = subprocess.run(
["git", "-C", str(recreated), "symbolic-ref", "-q", "HEAD"],
text=True, capture_output=True, check=False,
)
if readback.returncode != 0 or readback.stdout.strip() != expected_oid or detached.returncode != 1:
raise AuthorityGitError("WORKTREE_RECOVERY_READBACK_MISMATCH")
return stale_oid
def create_task_branch_CAS(self, branch: str, expected_base_oid: str, new_oid: str) -> str:
if not TASK_BRANCH_RE.fullmatch(branch):
raise AuthorityGitError("TASK_BRANCH_SCOPE_DENIED")
_oid(expected_base_oid, "expected_base_oid")
_oid(new_oid, "new_oid")
current = self.read_ref(branch)
if current is not None:
if current == new_oid:
return new_oid
raise AuthorityGitError("AUTHORITY_TARGET_BRANCH_ALREADY_EXISTS")
if not self.ancestry_check(expected_base_oid, new_oid):
raise AuthorityGitError("NEW_OID_NOT_DESCENDANT")
self._git(["update-ref", branch, new_oid, "0" * 40])
readback = self.read_ref(branch)
if readback != new_oid:
raise AuthorityGitError("AUTHORITY_CREATE_READBACK_MISMATCH")
return readback
def advance_task_branch_CAS(self, branch: str, expected_oid: str, new_oid: str) -> str:
if not TASK_BRANCH_RE.fullmatch(branch):
raise AuthorityGitError("TASK_BRANCH_SCOPE_DENIED")
+22 -6
View File
@@ -3,6 +3,12 @@ from __future__ import annotations
import hashlib, hmac, json, os, re, subprocess, sys, time, urllib.parse, urllib.request, uuid
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
from pathlib import Path
from version_registry import (
configured_authoritative_git_dir,
configured_repository_identity,
list_registry_versions,
list_registry_versions_report,
)
from typing import Any
try:
@@ -72,8 +78,8 @@ HOST = os.getenv('AIOS_GITOPS_HOST', '127.0.0.1')
PORT = int(os.getenv('AIOS_GITOPS_PORT', '18890'))
OPENAPI = WS / 'docs' / 'gitops' / 'openapi-chatgpt-actions.yaml'
BARE_REPO = Path(os.getenv('AIOS_GITOPS_BARE_REPO', str(ROOT / 'runtime' / 'gitea-bin' / 'data' / 'repositories' / 'aios' / 'aios.git')))
AUTHORITATIVE_GIT_DIR = Path(os.getenv('AIOS_GITOPS_AUTHORITATIVE_GIT_DIR')) if os.getenv('AIOS_GITOPS_AUTHORITATIVE_GIT_DIR') else None
REPOSITORY_IDENTITY = os.getenv('AIOS_GITOPS_REPOSITORY_IDENTITY', '')
AUTHORITATIVE_GIT_DIR = configured_authoritative_git_dir()
REPOSITORY_IDENTITY = configured_repository_identity()
SILENT_RELEASE_STATE_ROOT = Path(os.getenv(
'AIOS_SILENT_RELEASE_STATE_ROOT',
'/var/lib/aios-gitops/silent-release',
@@ -466,6 +472,14 @@ class H(BaseHTTPRequestHandler):
))
return self.sendj(data)
if path in ('/api/ver','/api/versions','/ver'):
registry_report = list_registry_versions_report(
WS,
baseline_file=Path(os.getenv('AIOS_GITOPS_BASELINE_AUTHORITY', '/var/lib/aios-gitops-runtime-anchor/baseline/current.json')),
# Do not invent an authoritative repository when authority
# discovery is disabled/omitted. Preserve the unavailable
# authority path (503) instead of turning it into a 404.
repo_git_dir=AUTHORITATIVE_GIT_DIR,
)
snapshot = repository_snapshot(
git_dir=AUTHORITATIVE_GIT_DIR,
repository_identity=REPOSITORY_IDENTITY,
@@ -474,7 +488,8 @@ class H(BaseHTTPRequestHandler):
return self.sendj({
'repository_available': False,
'query_error': snapshot['query_error'],
'registry_versions': [p.name for p in (WS/'versions').glob('*')],
'registry_versions': registry_report['registry_versions'],
**{key: registry_report[key] for key in ('effective_registry_versions', 'runtime_registry_versions', 'attested_projection_versions', 'attested_projection_version', 'source_registry_converged', 'runtime_registry_converged', 'runtime_bundle_contains_current_version', 'runtime_bundle_contains_version', 'registry_consistency_status')},
'local_authoritative_tags': [],
}, 503)
try:
@@ -483,10 +498,11 @@ class H(BaseHTTPRequestHandler):
return self.sendj({
'repository_available': False,
'query_error': str(exc),
'registry_versions': [p.name for p in (WS/'versions').glob('*')],
'registry_versions': registry_report['registry_versions'],
**{key: registry_report[key] for key in ('effective_registry_versions', 'runtime_registry_versions', 'attested_projection_versions', 'attested_projection_version', 'source_registry_converged', 'runtime_registry_converged', 'runtime_bundle_contains_current_version', 'runtime_bundle_contains_version', 'registry_consistency_status')},
'local_authoritative_tags': [],
}, 503)
registry = [p.name for p in (WS/'versions').glob('*')]
registry = registry_report['registry_versions']
return self.sendj({
'registry_versions': registry,
'local_authoritative_tags': tags,
@@ -496,6 +512,7 @@ class H(BaseHTTPRequestHandler):
'versions_dir': registry,
'head': snapshot['authoritative_head_oid'],
'authoritative_head_oid': snapshot['authoritative_head_oid'],
**{key: registry_report[key] for key in ('effective_registry_versions', 'runtime_registry_versions', 'attested_projection_versions', 'attested_projection_version', 'source_registry_converged', 'runtime_registry_converged', 'runtime_bundle_contains_current_version', 'runtime_bundle_contains_version', 'registry_consistency_status')},
})
if path in ('/api/diff','/diff'):
frm=(q.get('from') or [''])[0]
@@ -950,4 +967,3 @@ def main():
print(f'AIOS ChatGPT GitOps adapter listening on {HOST}:{PORT}')
ThreadingHTTPServer((HOST,PORT), H).serve_forever()
if __name__ == '__main__': main()
+3 -57
View File
@@ -13,10 +13,9 @@ BRIDGE_OPERATIONS = (
"getGovernedWriteCapabilities",
"createGovernedTask",
"acquireWriterLease",
"bootstrapWriterLease",
"renewWriterLease",
"commitFileGoverned",
"commitFilesGoverned",
"publishExistingBranchGoverned",
"getGitOpsJob",
"resumeGitOpsJob",
"reconcileGitOpsJob",
@@ -33,10 +32,9 @@ BRIDGE_OPERATIONS = (
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"),
"bootstrapWriterLease": ("schema", "task_id", "repository", "branch_mode", "base_oid", "expected_branch_oid", "target_branch", "generation", "lease_seconds"),
"renewWriterLease": ("schema", "task_id", "repository", "branch_mode", "base_oid", "expected_branch_oid", "target_branch", "generation", "lease_seconds"),
"commitFileGoverned": ("task_id", "repository", "expected_branch_oid", "target_branch", "generation", "idempotency_key", "path", "content_base64", "message"),
"commitFilesGoverned": ("task_id", "repository", "expected_branch_oid", "target_branch", "generation", "idempotency_key", "files", "message"),
"publishExistingBranchGoverned": ("repository", "branch", "expected_branch_oid", "owner_approval_reference", "idempotency_key"),
"getGitOpsJob": ("job_id",),
"resumeGitOpsJob": ("job_id", "expected_revision"),
"reconcileGitOpsJob": ("job_id", "expected_revision"),
@@ -50,49 +48,6 @@ OPERATION_REQUIRED_FIELDS = {
"getControlPlaneSnapshot": (),
}
BRIDGE_OPERATION_CONTRACTS = {
"acquireWriterLease": {
"method": "POST",
"path": "/v2/writer-lease",
"required_fields": OPERATION_REQUIRED_FIELDS["acquireWriterLease"],
"response_fields": (
"task_id", "job_id", "generation", "lease_seconds", "lease_expires_at",
"create_target_branch", "worktree_mode", "cas_ref_update_required",
),
},
"bootstrapWriterLease": {
"method": "POST",
"path": "/v2/writer-lease/bootstrap",
"required_fields": OPERATION_REQUIRED_FIELDS["bootstrapWriterLease"],
"response_fields": (
"task_id", "job_id", "generation", "lease_seconds", "lease_expires_at",
"create_target_branch", "worktree_mode", "cas_ref_update_required",
"recovered", "bootstrapped", "runtime_rehydrated",
"durable_branch_lease_present", "idempotent",
),
},
"renewWriterLease": {
"method": "POST",
"path": "/v2/writer-lease/renew",
"required_fields": OPERATION_REQUIRED_FIELDS["renewWriterLease"],
"response_fields": (
"task_id", "job_id", "generation", "lease_seconds", "lease_expires_at",
"recovered", "bootstrapped", "runtime_rehydrated",
"durable_branch_lease_present", "idempotent",
),
},
"commitFileGoverned": {
"method": "POST", "path": "/v2/commit-file",
"required_fields": OPERATION_REQUIRED_FIELDS["commitFileGoverned"],
"response_fields": ("job_id", "commit_oid", "commit_visible", "branch_mode", "target_branch", "generation"),
},
"commitFilesGoverned": {
"method": "POST", "path": "/v2/commit-files",
"required_fields": OPERATION_REQUIRED_FIELDS["commitFilesGoverned"],
"response_fields": ("job_id", "commit_oid", "commit_visible", "branch_mode", "target_branch", "generation"),
},
}
class RuntimeDriftError(RuntimeError):
pass
@@ -169,14 +124,6 @@ class BridgeV2Adapter:
"legacy_writes_enabled": False,
"governed_writes_enabled": True,
"operations": list(BRIDGE_OPERATIONS),
"operation_contracts": {
operation: {
**contract,
"required_fields": list(contract["required_fields"]),
"response_fields": list(contract["response_fields"]),
}
for operation, contract in BRIDGE_OPERATION_CONTRACTS.items()
},
"force_push": False,
"main_write": False,
"tag_write": False,
@@ -206,10 +153,9 @@ class BridgeV2Adapter:
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 bootstrapWriterLease(self, request: Mapping[str, Any]) -> Any: return self._call("bootstrapWriterLease", request)
def renewWriterLease(self, request: Mapping[str, Any]) -> Any: return self._call("renewWriterLease", request)
def commitFileGoverned(self, request: Mapping[str, Any]) -> Any: return self._call("commitFileGoverned", request)
def commitFilesGoverned(self, request: Mapping[str, Any]) -> Any: return self._call("commitFilesGoverned", request)
def publishExistingBranchGoverned(self, request: Mapping[str, Any]) -> Any: return self._call("publishExistingBranchGoverned", request)
def getGitOpsJob(self, request: Mapping[str, Any]) -> Any: return self._call("getGitOpsJob", request)
def resumeGitOpsJob(self, request: Mapping[str, Any]) -> Any: return self._call("resumeGitOpsJob", request)
def reconcileGitOpsJob(self, request: Mapping[str, Any]) -> Any: return self._call("reconcileGitOpsJob", request)
File diff suppressed because it is too large Load Diff
+34 -132
View File
@@ -51,13 +51,11 @@ class AuthorityPort(Protocol):
def branch_exists(self, branch: str) -> bool: ...
def branch_head(self, branch: str) -> str: ...
def is_ancestor(self, older: str, newer: str) -> bool: ...
def create_task_branch_cas(self, branch: str, expected_base: str, new: str) -> str: ...
def compare_and_swap_branch(self, branch: str, expected: str, new: str) -> str: ...
class RemotePort(Protocol):
def repository_identity(self) -> str: ...
def push_new_exact(self, branch: str, new: str) -> None: ...
def push_exact(self, branch: str, expected: str, new: str) -> None: ...
def read_git_ref(self, branch: str) -> str | None: ...
def read_api_branch(self, branch: str) -> str | None: ...
@@ -105,9 +103,6 @@ class AuthorityGitAdapter:
def is_ancestor(self, older: str, newer: str) -> bool:
return self.executor.ancestry_check(older, newer)
def create_task_branch_cas(self, branch: str, expected_base: str, new: str) -> str:
return self.executor.create_task_branch_CAS(self._ref(branch), expected_base, new)
def compare_and_swap_branch(self, branch: str, expected: str, new: str) -> str:
return self.executor.advance_task_branch_CAS(self._ref(branch), expected, new)
@@ -263,16 +258,7 @@ class GiteaContinuationRemote:
raise ContinuationExecutionError("GITEA_GIT_READBACK_INVALID")
return oid
def push_new_exact(self, branch: str, new: str) -> None:
self._require_projected()
self.repository_identity()
if self.read_git_ref(branch) is not None:
raise ContinuationExecutionError("GITEA_TARGET_BRANCH_ALREADY_EXISTS")
_run(["git", "push", self.remote_name, f"{new}:refs/heads/{branch}"], cwd=self.workspace)
if self.read_git_ref(branch) != new:
raise ContinuationExecutionError("GITEA_CREATE_READBACK_MISMATCH")
def push_exact(self, branch: str, expected: str, new: str) -> None:
def push_exact(self, branch: str, expected: str | None, new: str) -> None:
self._require_projected()
self.repository_identity()
if self.read_git_ref(branch) != expected:
@@ -343,7 +329,6 @@ class ContinuationPermissions:
@dataclass(frozen=True, slots=True)
class ContinuationRuntimeBinding:
request: WriterLeaseRequestV2
job_id: str
workspace_path: Path
lease_not_before: datetime
lease_expires_at: datetime
@@ -404,20 +389,13 @@ class ContinuationExecutor:
def observe_generation(self, branch: str, generation: int) -> None:
self._observed_generations[branch] = max(generation, self._observed_generations.get(branch, 0))
def _max_generation(self, branch: str, *, exclude_job_id: str | None = None) -> int:
def _max_generation(self, branch: str) -> int:
observed = self._observed_generations.get(branch, 0)
with self.controller.store._connect() as connection:
if exclude_job_id is None:
row = connection.execute(
"SELECT MAX(generation) AS generation FROM branch_leases WHERE branch=?",
(branch,),
).fetchone()
else:
row = connection.execute(
"SELECT MAX(generation) AS generation FROM branch_leases "
"WHERE branch=? AND job_id!=?",
(branch, exclude_job_id),
).fetchone()
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)
@@ -426,7 +404,6 @@ class ContinuationExecutor:
return {
"schema": WriterLeaseRequestV2.schema,
"task_id": request.task_id,
"job_id": request.job_id,
"repository": request.repository,
"branch_mode": request.branch_mode,
"base_oid": request.base_oid,
@@ -436,21 +413,10 @@ class ContinuationExecutor:
"lease_seconds": request.lease_seconds,
}
def _validate_preflight(self, binding: ContinuationRuntimeBinding, *, job_id: str | None = None) -> None:
def _validate_preflight(self, binding: ContinuationRuntimeBinding) -> None:
request = binding.request
request_mapping = self._request_mapping(request)
normalized = validate_writer_lease_request_v2(request_mapping, self.authority)
expected_normalized = {
**request_mapping,
"create_target_branch": request.branch_mode == "CREATE_NEW",
"worktree_mode": (
"detached_at_base_oid"
if request.branch_mode == "CREATE_NEW"
else "detached_at_expected_head"
),
"cas_ref_update_required": True,
}
if normalized != expected_normalized:
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")
@@ -460,7 +426,7 @@ class ContinuationExecutor:
raise ContinuationExecutionError("LEASE_NOT_ACTIVE")
if now >= binding.lease_expires_at:
raise ContinuationExecutionError("LEASE_EXPIRED")
if request.generation <= self._max_generation(request.target_branch, exclude_job_id=job_id):
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")
@@ -486,102 +452,57 @@ class ContinuationExecutor:
"generation": request.generation,
}
def execute(
self,
binding: ContinuationRuntimeBinding,
commit: ContinuationCommit,
*,
before_complete: Callable[[str], None] | None = None,
) -> dict[str, object]:
commit.payload()
if not binding.job_id or binding.request.job_id != binding.job_id:
raise ContinuationExecutionError("RUNTIME_LINEAGE_MISSING_JOB_ID")
current = self.controller.store.read(binding.job_id)
current_state = JobState(current["state"])
if current_state == JobState.COMPLETED:
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()
if current_state == JobState.REMOTE_VERIFIED:
receipt = json.loads(str(current["receipt_json"] or "{}"))
commit_oid = str(receipt.get("authority_oid") or "")
if not OID_RE.fullmatch(commit_oid):
raise ContinuationExecutionError("GOVERNED_COMMIT_OID_INVALID")
if before_complete is not None:
before_complete(commit_oid)
return self.controller.advance(
binding.job_id,
JobState.COMPLETED,
side_effect_state="REMOTE_VERIFIED",
receipt=receipt,
).as_dict()
self._validate_preflight(binding, job_id=binding.job_id)
self._validate_preflight(binding)
request = binding.request
projected = False
try:
self.credential.project()
projected = True
row = self.controller.store.read(binding.job_id)
state = JobState(row["state"])
if state == JobState.REQUESTED:
row = self.controller.store.read(submitted.job_id)
if JobState(row["state"]) == JobState.REQUESTED:
self.controller.advance(
binding.job_id,
submitted.job_id,
JobState.AUTHORIZED,
side_effect_state="AUTHORIZED",
receipt={"task_id": request.task_id, "original_grant_consumed": True},
expected_revision=int(row["revision"]),
)
row = self.controller.store.read(binding.job_id)
state = JobState(row["state"])
if state == JobState.AUTHORIZED:
row = self.controller.store.read(submitted.job_id)
if JobState(row["state"]) == JobState.AUTHORIZED:
self.controller.lease_job(
binding.job_id,
lease_id=f"lease-{binding.job_id}",
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"]),
)
row = self.controller.store.read(binding.job_id)
state = JobState(row["state"])
if state == JobState.LEASED:
self.controller.advance(binding.job_id, JobState.WORKTREE_READY,
side_effect_state="WORKTREE_READY", expected_revision=int(row["revision"]))
row = self.controller.store.read(binding.job_id)
state = JobState(row["state"])
if state == JobState.WORKTREE_READY:
self.controller.advance(binding.job_id, JobState.EXECUTING,
side_effect_state="IN_PROGRESS", expected_revision=int(row["revision"]))
elif state != JobState.EXECUTING:
raise ContinuationExecutionError("JOB_EXECUTION_STATE_INVALID")
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")
if request.branch_mode == "CREATE_NEW":
authority_oid = self.authority.create_task_branch_cas(request.target_branch, request.base_oid, new_oid)
else:
authority_oid = self.authority.compare_and_swap_branch(request.target_branch, request.expected_branch_oid, new_oid)
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(
binding.job_id,
submitted.job_id,
JobState.LOCAL_VERIFIED,
side_effect_state="LOCAL_VERIFIED",
receipt={"authority_oid": authority_oid, "force_push": False},
)
try:
self.controller.advance(binding.job_id, JobState.REMOTE_SYNCING, side_effect_state="IN_PROGRESS")
if request.branch_mode == "CREATE_NEW":
self.remote.push_new_exact(request.target_branch, new_oid)
else:
self.remote.push_exact(request.target_branch, request.expected_branch_oid, new_oid)
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(binding.job_id, authority_oid, new_oid)
return self._finish_remote_verification(
binding.job_id,
authority_oid,
new_oid,
before_complete=before_complete,
)
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()
@@ -604,14 +525,7 @@ class ContinuationExecutor:
)
return result.as_dict()
def _finish_remote_verification(
self,
job_id: str,
authority_oid: str,
new_oid: str,
*,
before_complete: Callable[[str], None] | None = None,
) -> dict[str, object]:
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"]
@@ -628,20 +542,8 @@ class ContinuationExecutor:
"reconciliation_decision": decision.action.value,
"force_push": False,
}
self.controller.advance(
job_id,
JobState.REMOTE_VERIFIED,
side_effect_state="REMOTE_VERIFIED",
receipt=receipt,
)
if before_complete is not None:
before_complete(new_oid)
return self.controller.advance(
job_id,
JobState.COMPLETED,
side_effect_state="REMOTE_VERIFIED",
receipt=receipt,
).as_dict()
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)
@@ -49,7 +49,6 @@ class WriterLeaseRequestV2:
target_branch: str
generation: int
lease_seconds: int = 900
job_id: str = ""
schema = "aios.task-writer-lease.v2"
accepted_schemas = frozenset({"aios.task-writer-lease.v2", "aios.task-writer-lease-request.v2"})
@@ -57,8 +56,6 @@ class WriterLeaseRequestV2:
def validate(self, probe: BranchProbe) -> None:
if not self.task_id or self.repository != "aios/aios":
raise ContinuationError("request_identity_invalid")
if not self.job_id or not self.job_id.startswith("job-"):
raise ContinuationError("job_id_invalid")
if self.branch_mode not in {"CREATE_NEW", "CONTINUE_EXISTING"}:
raise ContinuationError("branch_mode_invalid")
_oid(self.base_oid, "base_oid")
+4 -50
View File
@@ -6,7 +6,6 @@ from datetime import datetime, timedelta, timezone
import os
from pathlib import Path
import secrets
import re
from typing import Any, Callable, Mapping
@@ -22,14 +21,9 @@ class CredentialLease:
scopes: tuple[str, ...]
provider: str
state: str = "ACTIVE"
scope_type: str = "system"
tenant_id: str | None = None
user_id: str | None = None
project_id: str | None = None
run_id: str | None = None
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, "scope_type": self.scope_type, "tenant_id": self.tenant_id, "user_id": self.user_id, "project_id": self.project_id, "run_id": self.run_id}
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:
@@ -49,19 +43,11 @@ class CredentialBroker:
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, scope_type: str = "system", tenant_id: str | None = None, user_id: str | None = None, project_id: str | None = None, run_id: str | None = None) -> dict[str, Any]:
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")
if scope_type == "tenant":
if not tenant_id:
raise ValueError("CREDENTIAL_SCOPE_DENY")
elif scope_type in {"system", "global"}:
if any((tenant_id, user_id, project_id, run_id)):
raise ValueError("CREDENTIAL_SCOPE_DENY")
else:
raise ValueError("CREDENTIAL_SCOPE_DENY")
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__, "ACTIVE", scope_type, tenant_id, user_id, project_id, run_id)
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()
@@ -78,39 +64,7 @@ class CredentialBroker:
lease = self._get(lease_id)
if lease.state != "ACTIVE":
raise RuntimeError("CREDENTIAL_LEASE_NOT_ACTIVE")
environment = self.provider.environment(lease)
for key, value in environment.items():
upper = str(key).upper()
if not isinstance(value, str):
raise RuntimeError("CREDENTIAL_SECRET_EXPOSURE_DENY")
sensitive = any(marker in upper for marker in ("CREDENTIAL", "TOKEN", "SECRET", "PASSWORD", "PRIVATE_KEY", "API_KEY"))
if re.search(r"(?i)bearer\s+\S+|(?:token|secret|password|api[_-]?key)\s*[:=]", value):
raise RuntimeError("CREDENTIAL_SECRET_EXPOSURE_DENY")
if sensitive and upper.endswith("_FILE"):
candidate = Path(value)
credential_root = Path("/run/credentials")
try:
resolved = candidate.resolve(strict=False)
except OSError as exc:
raise RuntimeError("CREDENTIAL_SECRET_EXPOSURE_DENY") from exc
if not candidate.is_absolute() or not (resolved == credential_root or credential_root in resolved.parents):
raise RuntimeError("CREDENTIAL_SECRET_EXPOSURE_DENY")
elif sensitive and upper.endswith("_REFERENCE"):
if re.fullmatch(r"(?:systemd-credential|vault|secret-manager):[A-Za-z0-9][A-Za-z0-9._:/#-]{0,255}", value) is None:
raise RuntimeError("CREDENTIAL_SECRET_EXPOSURE_DENY")
elif sensitive:
raise RuntimeError("CREDENTIAL_SECRET_EXPOSURE_DENY")
return environment
def authorize(self, lease_id: str, *, task_id: str, operation: str, scope_type: str = "system", tenant_id: str | None = None, user_id: str | None = None, project_id: str | None = None, run_id: str | None = None) -> Mapping[str, str]:
lease = self._get(lease_id)
if lease.state != "ACTIVE" or lease.task_id != task_id or operation not in lease.scopes:
raise RuntimeError("CREDENTIAL_SCOPE_DENY")
requested = (scope_type, tenant_id, user_id, project_id, run_id)
actual = (lease.scope_type, lease.tenant_id, lease.user_id, lease.project_id, lease.run_id)
if requested != actual:
raise RuntimeError("CREDENTIAL_SCOPE_DENY")
return self.environment(lease_id)
return self.provider.environment(lease)
def status(self, lease_id: str) -> dict[str, Any]:
return self._get(lease_id).as_dict()
@@ -0,0 +1,252 @@
"""Fail-closed governed publication of an existing Authority branch at an exact OID."""
from __future__ import annotations
from collections.abc import Mapping
from dataclasses import asdict, dataclass
from enum import Enum
import hashlib
import json
import os
import re
import tempfile
import time
from pathlib import Path
from typing import Any, Protocol
OID_RE = re.compile(r"^[0-9a-f]{40}$")
BRANCH_RE = re.compile(r"^(?!refs/)(?!tags/)[A-Za-z0-9][A-Za-z0-9._/-]{2,179}$")
IDEMPOTENCY_RE = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._:-]{7,127}$")
class ExistingBranchPublicationError(RuntimeError):
"""Stable, non-secret error for the governed existing-branch publisher."""
class ExistingBranchPublicationConflict(ExistingBranchPublicationError):
pass
class ExistingBranchPublicationState(str, Enum):
QUEUED = "QUEUED"
PUBLISHING = "PUBLISHING"
REMOTE_PUSH_PENDING = "REMOTE_PUSH_PENDING"
RECONCILIATION_REQUIRED = "RECONCILIATION_REQUIRED"
MANUAL_OWNER_REVIEW_REQUIRED = "MANUAL_OWNER_REVIEW_REQUIRED"
COMPLETED = "COMPLETED"
FAILED = "FAILED"
@dataclass(frozen=True, slots=True)
class ExistingBranchPublicationRequest:
repository: str
branch: str
expected_branch_oid: str
owner_approval_reference: str
idempotency_key: str
@classmethod
def from_mapping(cls, value: Mapping[str, Any]) -> "ExistingBranchPublicationRequest":
required = ("repository", "branch", "expected_branch_oid", "owner_approval_reference", "idempotency_key")
if not isinstance(value, Mapping):
raise ExistingBranchPublicationConflict("REQUEST_OBJECT_REQUIRED")
if any(not isinstance(value.get(field), str) or not value[field].strip() for field in required):
raise ExistingBranchPublicationConflict("REQUEST_FIELDS_MISSING")
repository = value["repository"].strip()
branch = value["branch"].strip()
expected = value["expected_branch_oid"].strip().lower()
approval = value["owner_approval_reference"].strip()
idempotency = value["idempotency_key"].strip()
if repository != "aios/aios":
raise ExistingBranchPublicationConflict("REPOSITORY_IDENTITY_MISMATCH")
if not BRANCH_RE.fullmatch(branch) or branch in {"main", "master"}:
raise ExistingBranchPublicationConflict("BRANCH_NAMESPACE_REJECTED")
if not OID_RE.fullmatch(expected):
raise ExistingBranchPublicationConflict("EXPECTED_BRANCH_OID_INVALID")
if len(approval) > 512:
raise ExistingBranchPublicationConflict("OWNER_APPROVAL_REFERENCE_INVALID")
if not IDEMPOTENCY_RE.fullmatch(idempotency):
raise ExistingBranchPublicationConflict("IDEMPOTENCY_KEY_INVALID")
return cls(repository, branch, expected, approval, idempotency)
@property
def publication_job_id(self) -> str:
return "publication-" + hashlib.sha256(self.idempotency_key.encode()).hexdigest()[:32]
@property
def digest(self) -> str:
raw = json.dumps(asdict(self), sort_keys=True, separators=(",", ":")).encode()
return hashlib.sha256(raw).hexdigest()
class ExistingBranchAuthority(Protocol):
def read_ref(self, ref: str) -> str | None: ...
class ExistingBranchRemote(Protocol):
def repository_identity(self) -> str: ...
def read_git_ref(self, branch: str) -> str | None: ...
def read_api_branch(self, branch: str) -> str | None: ...
def commit_visible(self, oid: str) -> bool: ...
def push_exact(self, branch: str, expected: str | None, new: str) -> None: ...
class ExistingBranchPublicationStore:
def __init__(self, root: Path):
self.root = Path(root)
self.root.mkdir(parents=True, exist_ok=True)
def _path(self, request: ExistingBranchPublicationRequest) -> Path:
return self.root / f"{request.publication_job_id}.json"
def load(self, request: ExistingBranchPublicationRequest) -> dict[str, Any]:
try:
value = json.loads(self._path(request).read_text(encoding="utf-8"))
except FileNotFoundError:
return {}
if not isinstance(value, dict):
raise ExistingBranchPublicationConflict("PUBLICATION_STATE_INVALID")
return value
def record(self, request: ExistingBranchPublicationRequest, state: ExistingBranchPublicationState, **evidence: Any) -> dict[str, Any]:
payload = {
"schema": "aios.existing-branch-publication.v1",
"publication_job_id": request.publication_job_id,
"repository": request.repository,
"branch": request.branch,
"expected_branch_oid": request.expected_branch_oid,
"owner_approval_reference": request.owner_approval_reference,
"idempotency_key": request.idempotency_key,
"request_digest": request.digest,
"publication_state": state.value,
"updated_at": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()),
**evidence,
}
path = self._path(request)
fd, temporary = tempfile.mkstemp(prefix=f".{path.name}.", dir=path.parent)
try:
with os.fdopen(fd, "w", encoding="utf-8") as handle:
json.dump(payload, handle, ensure_ascii=False, sort_keys=True)
handle.write("\n")
handle.flush()
os.fsync(handle.fileno())
os.replace(temporary, path)
finally:
if os.path.exists(temporary):
os.unlink(temporary)
return payload
class ExistingBranchPublisher:
"""Publish only the exact, already-authorized Authority branch tip."""
def __init__(self, authority: ExistingBranchAuthority, remote: ExistingBranchRemote, state_root: Path):
self.authority = authority
self.remote = remote
self.store = ExistingBranchPublicationStore(state_root)
@staticmethod
def _safe_error(exc: BaseException) -> str:
return type(exc).__name__
def _observe(self, request: ExistingBranchPublicationRequest) -> dict[str, Any]:
return {
"authority_branch_oid": self.authority.read_ref(f"refs/heads/{request.branch}"),
"gitea_git_branch_oid": self.remote.read_git_ref(request.branch),
"gitea_api_branch_oid": self.remote.read_api_branch(request.branch),
"commit_visible": bool(self.remote.commit_visible(request.expected_branch_oid)),
}
@staticmethod
def _has_remote_conflict(request: ExistingBranchPublicationRequest, observation: Mapping[str, Any]) -> bool:
return any(
observation.get(field) not in (None, request.expected_branch_oid)
for field in ("gitea_git_branch_oid", "gitea_api_branch_oid")
)
def _manual(self, request: ExistingBranchPublicationRequest, reason: str, observation: Mapping[str, Any]) -> dict[str, Any]:
return self.store.record(
request,
ExistingBranchPublicationState.MANUAL_OWNER_REVIEW_REQUIRED,
error_code=reason,
**dict(observation),
)
def _finish(self, request: ExistingBranchPublicationRequest) -> dict[str, Any]:
try:
observation = self._observe(request)
except Exception as exc:
return self.store.record(
request,
ExistingBranchPublicationState.RECONCILIATION_REQUIRED,
error_code=self._safe_error(exc),
)
if observation.get("authority_branch_oid") != request.expected_branch_oid:
return self._manual(request, "AUTHORITY_BRANCH_OID_CHANGED", observation)
if self._has_remote_conflict(request, observation):
return self._manual(request, "REMOTE_BRANCH_OID_CONFLICT", observation)
if (
observation.get("gitea_git_branch_oid") == request.expected_branch_oid
and observation.get("gitea_api_branch_oid") == request.expected_branch_oid
and observation.get("commit_visible") is True
):
return self.store.record(request, ExistingBranchPublicationState.COMPLETED, **dict(observation))
return self.store.record(
request,
ExistingBranchPublicationState.RECONCILIATION_REQUIRED,
error_code="REMOTE_READBACK_INCOMPLETE",
**dict(observation),
)
def publish(self, value: Mapping[str, Any]) -> dict[str, Any]:
request = ExistingBranchPublicationRequest.from_mapping(value)
previous = self.store.load(request)
if previous:
if previous.get("request_digest") != request.digest:
raise ExistingBranchPublicationConflict("IDEMPOTENCY_KEY_REUSED")
if previous.get("publication_state") == ExistingBranchPublicationState.COMPLETED.value:
return {**previous, "idempotent": True}
authority_oid = self.authority.read_ref(f"refs/heads/{request.branch}")
if authority_oid is None:
raise ExistingBranchPublicationConflict("AUTHORITY_BRANCH_MISSING")
if authority_oid != request.expected_branch_oid:
raise ExistingBranchPublicationConflict("AUTHORITY_BRANCH_OID_MISMATCH")
if self.remote.repository_identity() != request.repository:
raise ExistingBranchPublicationConflict("REMOTE_REPOSITORY_IDENTITY_MISMATCH")
self.store.record(request, ExistingBranchPublicationState.QUEUED)
try:
observation = self._observe(request)
except Exception as exc:
return self.store.record(
request,
ExistingBranchPublicationState.RECONCILIATION_REQUIRED,
error_code=self._safe_error(exc),
)
if observation.get("authority_branch_oid") != request.expected_branch_oid:
return self._manual(request, "AUTHORITY_BRANCH_OID_CHANGED", observation)
if self._has_remote_conflict(request, observation):
return self._manual(request, "REMOTE_BRANCH_OID_CONFLICT", observation)
if (
observation.get("gitea_git_branch_oid") == request.expected_branch_oid
and observation.get("gitea_api_branch_oid") == request.expected_branch_oid
and observation.get("commit_visible") is True
):
return self.store.record(request, ExistingBranchPublicationState.COMPLETED, **dict(observation))
if observation.get("gitea_git_branch_oid") is not None or observation.get("gitea_api_branch_oid") is not None:
return self.store.record(
request,
ExistingBranchPublicationState.RECONCILIATION_REQUIRED,
error_code="REMOTE_READBACK_INCOMPLETE",
**dict(observation),
)
self.store.record(request, ExistingBranchPublicationState.PUBLISHING, **dict(observation))
try:
self.remote.push_exact(request.branch, None, request.expected_branch_oid)
except Exception as exc:
return self.store.record(
request,
ExistingBranchPublicationState.REMOTE_PUSH_PENDING,
error_code=self._safe_error(exc),
desired_remote_oid=request.expected_branch_oid,
**dict(observation),
)
return self._finish(request)
@@ -27,19 +27,6 @@ class ServiceUnavailable(JobError):
pass
@dataclass(frozen=True, slots=True)
class LeaseRenewalResult:
"""Durable outcome of one writer-lease recovery attempt."""
renewed: bool
idempotent: bool
durable: bool
lease_expires_at: str | None
def __bool__(self) -> bool:
return self.renewed or self.idempotent
class BindingConflict(JobError):
pass
@@ -79,14 +66,6 @@ def utc_now() -> str:
return datetime.now(timezone.utc).isoformat().replace("+00:00", "Z")
def parse_utc(value: str) -> datetime:
normalized = value[:-1] + "+00:00" if value.endswith("Z") else value
parsed = datetime.fromisoformat(normalized)
if parsed.tzinfo is None:
raise ValueError("LEASE_EXPIRY_INVALID")
return parsed.astimezone(timezone.utc)
def canonical_json(value: Any) -> bytes:
return (
json.dumps(value, sort_keys=True, separators=(",", ":"), ensure_ascii=False) + "\n"
@@ -425,16 +404,13 @@ class GitOpsJobController:
self.store._mirror(result)
return self.store._receipt(result, duplicate=True)
def update_lease(self, lease_id: str, *, expected_revision: int, current_head_oid: str, generation: int, state: str = "ACTIVE", lease_expires_at: str | None = None) -> None:
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")
if lease_expires_at is None:
updated = connection.execute("UPDATE branch_leases SET current_head_oid=?,generation=?,state=?,revision=revision+1 WHERE lease_id=? AND revision=?", (current_head_oid, generation, state, lease_id, expected_revision))
else:
updated = connection.execute("UPDATE branch_leases SET current_head_oid=?,generation=?,lease_expires_at=?,state=?,revision=revision+1 WHERE lease_id=? AND revision=?", (current_head_oid, generation, lease_expires_at, state, lease_id, expected_revision))
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()
@@ -443,94 +419,6 @@ class GitOpsJobController:
except sqlite3.Error as exc:
raise ServiceUnavailable("JOB_DB_UNAVAILABLE") from exc
def renew_lease(
self,
*,
branch: str,
lineage_anchor_oid: str,
current_head_oid: str,
generation: int,
lease_expires_at: str,
) -> LeaseRenewalResult:
if (
not branch
or len(lineage_anchor_oid) != 40
or len(current_head_oid) != 40
or generation < 1
or not lease_expires_at.strip()
):
raise JobError("LEASE_BINDING_INVALID")
try:
with self.store._connect() as connection:
connection.execute("BEGIN IMMEDIATE")
rows = connection.execute(
"SELECT lease_id, revision, generation, state, lease_expires_at "
"FROM branch_leases "
"WHERE branch=? AND lineage_anchor_oid=? AND current_head_oid=? "
"AND generation=? AND state='ACTIVE' ORDER BY revision DESC",
(branch, lineage_anchor_oid, current_head_oid, generation),
).fetchall()
if not rows:
connection.commit()
return LeaseRenewalResult(False, False, False, None)
if len(rows) != 1:
raise JobError("LEASE_BINDING_CONFLICT")
row = rows[0]
try:
current_expires_at = parse_utc(str(row["lease_expires_at"]))
requested_expires_at = parse_utc(lease_expires_at)
except (TypeError, ValueError) as exc:
raise JobError("LEASE_EXPIRY_INVALID") from exc
now = datetime.now(timezone.utc)
if requested_expires_at <= now:
raise JobError("LEASE_EXPIRY_INVALID")
if current_expires_at > now:
connection.commit()
return LeaseRenewalResult(
False,
True,
True,
str(row["lease_expires_at"]),
)
updated = connection.execute(
"UPDATE branch_leases SET lease_expires_at=?,revision=revision+1 "
"WHERE lease_id=? AND revision=? AND generation=? "
"AND state='ACTIVE' AND lease_expires_at=?",
(
lease_expires_at,
row["lease_id"],
int(row["revision"]),
generation,
row["lease_expires_at"],
),
)
if updated.rowcount != 1:
current = connection.execute(
"SELECT lease_expires_at, generation, state FROM branch_leases "
"WHERE lease_id=?",
(row["lease_id"],),
).fetchone()
if (
current is not None
and int(current["generation"]) == generation
and current["state"] == "ACTIVE"
and parse_utc(str(current["lease_expires_at"])) > now
):
connection.commit()
return LeaseRenewalResult(
False,
True,
True,
str(current["lease_expires_at"]),
)
raise JobError("LEASE_REVISION_CAS_CONFLICT")
connection.commit()
return LeaseRenewalResult(True, False, True, lease_expires_at)
except JobError:
raise
except sqlite3.Error as exc:
raise ServiceUnavailable("JOB_DB_UNAVAILABLE") from exc
def 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,
@@ -1,306 +0,0 @@
"""Uncredentialed deterministic executor for governed Online Agent changes.
This process never receives Agent/Owner/Bridge credentials and never writes Git refs.
It operates only on an exchange bundle and returns hash-bound changed files + test evidence.
"""
from __future__ import annotations
import base64
from dataclasses import dataclass
import errno
import hashlib
import io
import json
import os
from pathlib import Path
import re
import shutil
import socket
import subprocess
import tarfile
import tempfile
import time
from typing import Any, Mapping, Sequence
ALLOWED_PROGRAMS = {"/usr/bin/python3", "/usr/bin/git", "/bin/true", "/usr/bin/true"}
MAX_ARCHIVE_BYTES = 64 * 1024 * 1024
MAX_FILE_BYTES = 4 * 1024 * 1024
SECRET_ENV_TOKENS = ("TOKEN", "SECRET", "PASSWORD", "CREDENTIAL", "AUTHORIZATION", "PRIVATE_KEY")
REQUEST_ID_RE = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._:-]{15,254}$")
SENSITIVE_VALUE_RE = re.compile(
r"(?i)(authorization\s*[:=]\s*bearer\s+\S+|authorization\s+bearer\s+\S+|"
r"(?:password|passwd|secret|token|api[_-]?key)\s*[:=]\s*\S+)"
)
class IsolatedExecutorError(RuntimeError):
pass
def sha256(data: bytes) -> str:
return hashlib.sha256(data).hexdigest()
def canonical(value: object) -> bytes:
return json.dumps(value, sort_keys=True, separators=(",", ":"), ensure_ascii=False).encode("utf-8")
def safe_relative(value: object) -> str:
if not isinstance(value, str) or not value or value.startswith("/"):
raise IsolatedExecutorError("PATH_INVALID")
path = Path(value)
if ".." in path.parts or "." in path.parts or any(part in {"", ".git"} for part in path.parts):
raise IsolatedExecutorError("PATH_INVALID")
return path.as_posix()
def resolve_under(root: Path, relative: str) -> Path:
root = root.resolve()
path = (root / safe_relative(relative)).resolve(strict=False)
if path == root or root not in path.parents:
raise IsolatedExecutorError("PATH_ESCAPE")
return path
def _credential_exposure(directory: Path) -> str | None:
"""Return an exposure code; EACCES is the intended credential isolation proof."""
try:
if not directory.exists():
return None
if directory.is_symlink():
return "CREDENTIAL_MOUNT_SYMLINK"
if os.access(directory, os.R_OK):
return "CREDENTIAL_MOUNT_ACCESSIBLE"
if directory.is_dir():
for child in directory.iterdir():
if child.is_file() and os.access(child, os.R_OK):
return "CREDENTIAL_FILE_ACCESSIBLE"
except PermissionError:
return None
return None
def assert_uncredentialed() -> None:
credentials = Path("/run/credentials")
candidates = {credentials}
credentials_directory = Path(os.environ.get("CREDENTIALS_DIRECTORY", "/run/credentials"))
candidates.add(credentials_directory)
leaked = sorted(
key for key, value in os.environ.items()
if value
and (any(token in key.upper() for token in SECRET_ENV_TOKENS) or SENSITIVE_VALUE_RE.search(str(value)))
)
if leaked:
raise IsolatedExecutorError("CREDENTIAL_ENV_EXPOSED:" + ",".join(leaked))
for directory in candidates:
exposure = _credential_exposure(directory)
if exposure is not None:
raise IsolatedExecutorError(exposure)
# In the intended PrivateNetwork namespace host loopback services are unreachable.
try:
probe = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
except OSError as exc:
if exc.errno in {errno.EACCES, errno.EPERM, errno.EAFNOSUPPORT}:
return
raise IsolatedExecutorError("NETWORK_ISOLATION_PROBE_FAILED") from exc
probe.settimeout(0.15)
try:
if probe.connect_ex(("127.0.0.1", 18892)) == 0:
raise IsolatedExecutorError("GOVERNED_BRIDGE_REACHABLE")
finally:
probe.close()
def extract_source(archive: Path, root: Path) -> None:
if archive.is_symlink() or not archive.is_file() or archive.stat().st_size > MAX_ARCHIVE_BYTES:
raise IsolatedExecutorError("SOURCE_ARCHIVE_INVALID")
with tarfile.open(archive, "r:gz") as tf:
for member in tf.getmembers():
path = Path(member.name)
if path.is_absolute() or ".." in path.parts or ".git" in path.parts:
raise IsolatedExecutorError("SOURCE_ARCHIVE_PATH_INVALID")
if member.issym() or member.islnk():
raise IsolatedExecutorError("SOURCE_ARCHIVE_LINK_DENY")
tf.extractall(root, filter="data")
def run_command(item: Mapping[str, Any], root: Path) -> dict[str, Any]:
argv = item.get("argv")
if not isinstance(argv, Sequence) or isinstance(argv, (str, bytes)) or not argv:
raise IsolatedExecutorError("COMMAND_INVALID")
argv = [str(part) for part in argv]
if argv[0] not in ALLOWED_PROGRAMS:
raise IsolatedExecutorError("COMMAND_PROGRAM_DENY")
if argv[0] == "/usr/bin/python3" and "-m" in argv:
index = argv.index("-m")
if index + 1 >= len(argv) or argv[index + 1] not in {"pytest", "py_compile"}:
raise IsolatedExecutorError("PYTHON_MODULE_DENY")
if argv[0] == "/usr/bin/git" and any(
token in {"push", "update-ref", "commit", "merge", "rebase", "reset", "clean", "branch"}
for token in argv[1:]
):
raise IsolatedExecutorError("GIT_MUTATION_DENY")
timeout = int(item.get("timeout_seconds") or 300)
if not 1 <= timeout <= 1800:
raise IsolatedExecutorError("COMMAND_TIMEOUT_INVALID")
cwd = root if item.get("cwd", ".") == "." else resolve_under(root, str(item.get("cwd")))
started = time.monotonic()
completed = subprocess.run(
argv,
cwd=cwd,
capture_output=True,
text=True,
check=False,
timeout=timeout,
env={"PATH": "/usr/bin:/bin", "LC_ALL": "C.UTF-8"},
)
result = {
"label": str(item.get("label") or "command"),
"exit_code": completed.returncode,
"stdout_sha256": sha256(completed.stdout.encode("utf-8", "replace")),
"stderr_sha256": sha256(completed.stderr.encode("utf-8", "replace")),
"duration_ms": int((time.monotonic() - started) * 1000),
}
if completed.returncode != 0:
raise IsolatedExecutorError(
"COMMAND_FAILED:" + result["label"] + ":" + str(completed.returncode)
)
return result
def apply_changes(request: Mapping[str, Any], root: Path) -> list[str]:
changes = request.get("change_set")
if not isinstance(changes, list) or not changes:
raise IsolatedExecutorError("CHANGE_SET_INVALID")
changed: list[str] = []
for change in changes:
if not isinstance(change, Mapping):
raise IsolatedExecutorError("CHANGE_INVALID")
relative = safe_relative(change.get("path"))
path = resolve_under(root, relative)
current = path.read_bytes() if path.is_file() else None
expected = change.get("expected_sha256")
if expected is not None and (current is None or sha256(current) != expected):
raise IsolatedExecutorError("SOURCE_DRIFT:" + relative)
operation = change.get("operation")
if operation == "write_file":
content = change.get("content")
if not isinstance(content, str):
raise IsolatedExecutorError("WRITE_CONTENT_INVALID")
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(content, encoding="utf-8")
elif operation == "replace_text":
if current is None:
raise IsolatedExecutorError("REPLACE_SOURCE_MISSING")
old = str(change.get("old"))
new = str(change.get("new"))
count = int(change.get("count", 1))
text = current.decode("utf-8")
if text.count(old) != count:
raise IsolatedExecutorError("REPLACE_COUNT_INVALID:" + relative)
path.write_text(text.replace(old, new), encoding="utf-8")
elif operation == "delete_file":
if path.exists():
path.unlink()
else:
raise IsolatedExecutorError("CHANGE_OPERATION_INVALID")
changed.append(relative)
return sorted(set(changed))
def changed_bundle(root: Path, changed: list[str]) -> list[dict[str, Any]]:
files = []
for relative in changed:
path = resolve_under(root, relative)
if path.exists():
data = path.read_bytes()
if len(data) > MAX_FILE_BYTES:
raise IsolatedExecutorError("CHANGED_FILE_TOO_LARGE:" + relative)
files.append({
"path": relative,
"deleted": False,
"sha256": sha256(data),
"content_b64": base64.b64encode(data).decode("ascii"),
})
else:
files.append({"path": relative, "deleted": True, "sha256": None, "content_b64": None})
return files
def execute_request(request_path: Path, result_path: Path) -> None:
assert_uncredentialed()
request = json.loads(request_path.read_text(encoding="utf-8"))
if request.get("schema") != "aios.isolated-execution.request.v1":
raise IsolatedExecutorError("REQUEST_SCHEMA_INVALID")
request_id = request.get("request_id")
if not isinstance(request_id, str) or not REQUEST_ID_RE.fullmatch(request_id):
raise IsolatedExecutorError("REQUEST_ID_INVALID")
source_archive = Path(str(request.get("source_archive") or ""))
if not source_archive.is_absolute():
raise IsolatedExecutorError("SOURCE_ARCHIVE_PATH_INVALID")
expected_archive_sha = str(request.get("source_archive_sha256") or "")
if sha256(source_archive.read_bytes()) != expected_archive_sha:
raise IsolatedExecutorError("SOURCE_ARCHIVE_SHA_MISMATCH")
with tempfile.TemporaryDirectory(prefix="aios-isolated-executor-") as temporary:
root = Path(temporary) / "workspace"
root.mkdir()
extract_source(source_archive, root)
changed = apply_changes(request, root)
commands = [
run_command(item, root)
for item in request.get("verification", [])
if isinstance(item, Mapping)
]
result = {
"schema": "aios.isolated-execution.result.v1",
"request_id": request_id,
"request_sha256": sha256(canonical(request)),
"changed_files": changed_bundle(root, changed),
"verification": commands,
"credential_isolation": {
"credential_env_absent": True,
"credential_mount_inaccessible": True,
"governed_bridge_unreachable": True,
},
}
result["result_sha256"] = sha256(canonical(result))
temporary_result = result_path.with_suffix(".tmp")
temporary_result.write_text(json.dumps(result, sort_keys=True, separators=(",", ":")), encoding="utf-8")
os.replace(temporary_result, result_path)
def serve(root: Path) -> None:
assert_uncredentialed()
requests = root / "requests"
results = root / "results"
failed = root / "failed"
for path in (requests, results, failed):
path.mkdir(parents=True, exist_ok=True)
while True:
for request in sorted(requests.glob("*.json")):
if request.is_symlink():
continue
result = results / request.name
try:
execute_request(request, result)
request.unlink()
except Exception as exc:
(failed / request.name).write_text(
json.dumps({"error": str(exc) or type(exc).__name__}, sort_keys=True),
encoding="utf-8",
)
request.unlink(missing_ok=True)
time.sleep(0.5)
if __name__ == "__main__":
import argparse
parser = argparse.ArgumentParser()
parser.add_argument("--root", default="/var/lib/aios-online-agent-executor")
parser.add_argument("--request")
parser.add_argument("--result")
args = parser.parse_args()
if args.request and args.result:
execute_request(Path(args.request), Path(args.result))
else:
serve(Path(args.root))
+40 -16
View File
@@ -3,6 +3,12 @@ from __future__ import annotations
import hashlib, hmac, json, os, re, subprocess, sys, time, urllib.parse, urllib.request, uuid
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
from pathlib import Path
from version_registry import (
configured_authoritative_git_dir,
configured_repository_identity,
list_registry_versions,
list_registry_versions_report,
)
from baseline_authority import BaselineAuthority, BaselineAuthorityError
from typing import Any
@@ -15,6 +21,9 @@ except ModuleNotFoundError:
from gitea_tag_publication import (
GiteaTagClient,
GitTagBackend,
GiteaConfigurationError,
InvalidReleaseVersion,
PublicationError,
PublicationConflict,
PublicationPending,
PublicationManualOwnerReviewRequired,
@@ -73,10 +82,13 @@ HOST = os.getenv('AIOS_GITOPS_HOST', '127.0.0.1')
PORT = int(os.getenv('AIOS_GITOPS_PORT', '18890'))
OPENAPI = WS / 'docs' / 'gitops' / 'openapi-chatgpt-actions.yaml'
BARE_REPO = Path(os.getenv('AIOS_GITOPS_BARE_REPO', str(ROOT / 'runtime' / 'gitea-bin' / 'data' / 'repositories' / 'aios' / 'aios.git')))
AUTHORITATIVE_GIT_DIR = Path(os.getenv('AIOS_GITOPS_AUTHORITATIVE_GIT_DIR')) if os.getenv('AIOS_GITOPS_AUTHORITATIVE_GIT_DIR') else None
REPOSITORY_IDENTITY = os.getenv('AIOS_GITOPS_REPOSITORY_IDENTITY', '')
AUTHORITATIVE_GIT_DIR = configured_authoritative_git_dir()
REPOSITORY_IDENTITY = configured_repository_identity()
BASELINE_AUTHORITY = BaselineAuthority()
GIT_DIR = Path(os.getenv('AIOS_GITOPS_AUTHORITATIVE_GIT_DIR') or os.getenv('AIOS_GITOPS_REPO', '/data/aios-gitops/repo.git'))
# Retain the generic Git directory only for legacy/staging compatibility
# commands. Source-registry convergence receives the separate authority
# below and never falls back to this path.
GIT_DIR = Path(os.getenv('AIOS_GITOPS_REPO', '/data/aios-gitops/repo.git'))
LEGACY_GIT_WRITE_COMMANDS = frozenset({'add','commit','checkout','branch','tag','push','fetch','reset','merge','rebase','update-ref','worktree'})
SILENT_RELEASE_STATE_ROOT = Path(os.getenv(
'AIOS_SILENT_RELEASE_STATE_ROOT',
@@ -358,12 +370,7 @@ def publication_publisher() -> TagPublisher:
return TagPublisher(
WS,
GitTagBackend(WS, authoritative_repo=authoritative_repo),
GiteaTagClient(
os.getenv('AIOS_GITOPS_GITEA_API_URL',''),
os.getenv('AIOS_GITOPS_GITEA_REPOSITORY',''),
Path(os.getenv('AIOS_GITOPS_GITEA_TOKEN_FILE','')),
token_line=int(os.getenv('AIOS_GITOPS_GITEA_TOKEN_LINE','1')),
),
GiteaTagClient.from_authoritative_environment(),
os.getenv('AIOS_GITOPS_GITEA_REMOTE','gitea'),
)
@@ -495,6 +502,11 @@ class H(BaseHTTPRequestHandler):
data['legacy_global_writes_enabled'] = False
return self.sendj(data)
if path in ('/api/ver','/api/versions','/ver'):
registry_report = list_registry_versions_report(
WS,
baseline_file=BASELINE_AUTHORITY.path,
repo_git_dir=AUTHORITATIVE_GIT_DIR,
)
try:
baseline = dict(BASELINE_AUTHORITY.read())
except BaselineAuthorityError as exc:
@@ -507,7 +519,8 @@ class H(BaseHTTPRequestHandler):
return self.sendj({
'repository_available': False,
'query_error': snapshot['query_error'],
'registry_versions': [p.name for p in (WS/'versions').glob('*')],
'registry_versions': registry_report['registry_versions'],
**{key: registry_report[key] for key in ('effective_registry_versions', 'runtime_registry_versions', 'attested_projection_versions', 'attested_projection_version', 'source_registry_converged', 'runtime_registry_converged', 'runtime_bundle_contains_current_version', 'runtime_bundle_contains_version', 'registry_consistency_status')},
'local_authoritative_tags': [],
'baseline_version': baseline.get('current_baseline'),
'baseline_generation': baseline.get('generation'),
@@ -519,13 +532,14 @@ class H(BaseHTTPRequestHandler):
return self.sendj({
'repository_available': False,
'query_error': str(exc),
'registry_versions': [p.name for p in (WS/'versions').glob('*')],
'registry_versions': registry_report['registry_versions'],
**{key: registry_report[key] for key in ('effective_registry_versions', 'runtime_registry_versions', 'attested_projection_versions', 'attested_projection_version', 'source_registry_converged', 'runtime_registry_converged', 'runtime_bundle_contains_current_version', 'runtime_bundle_contains_version', 'registry_consistency_status')},
'local_authoritative_tags': [],
'baseline_version': baseline.get('current_baseline'),
'baseline_generation': baseline.get('generation'),
'baseline_sha256': baseline.get('authority_sha256'),
}, 503)
registry = [p.name for p in (WS/'versions').glob('*')]
registry = registry_report['registry_versions']
return self.sendj({
'registry_versions': registry,
'local_authoritative_tags': tags,
@@ -539,6 +553,7 @@ class H(BaseHTTPRequestHandler):
'baseline_generation': baseline.get('generation'),
'baseline_sha256': baseline.get('authority_sha256'),
'baseline': baseline,
**{key: registry_report[key] for key in ('effective_registry_versions', 'runtime_registry_versions', 'attested_projection_versions', 'attested_projection_version', 'source_registry_converged', 'runtime_registry_converged', 'runtime_bundle_contains_current_version', 'runtime_bundle_contains_version', 'registry_consistency_status')},
})
if path in ('/api/diff','/diff'):
frm=(q.get('from') or [''])[0]
@@ -634,12 +649,22 @@ class H(BaseHTTPRequestHandler):
try:
version=validate_version(version)
status=publication_publisher().get_tag_publication_status(version)
except ValueError:
return self.sendj({'error':'invalid release version'},400)
except InvalidReleaseVersion as exc:
return self.sendj({'error': exc.code},400)
except GiteaConfigurationError as exc:
return self.sendj({'error': exc.code},503)
except PublicationError as exc:
return self.sendj({'error': getattr(exc, 'code', str(exc))},503)
return self.sendj(status)
if path == '/api/release-consistency':
request=publication_request_from_query(q)
return self.sendj(publication_publisher().get_release_consistency(request))
try:
result=publication_publisher().get_release_consistency(request)
except GiteaConfigurationError as exc:
return self.sendj({'error': exc.code},503)
except PublicationError as exc:
return self.sendj({'error': getattr(exc, 'code', str(exc))},503)
return self.sendj(result)
if path == '/api/release-acceptance':
request=publication_request_from_query(q)
return self.sendj(publication_publisher().run_release_acceptance(request))
@@ -1019,4 +1044,3 @@ def main():
print(f'AIOS ChatGPT GitOps adapter listening on {HOST}:{PORT}')
ThreadingHTTPServer((HOST,PORT), H).serve_forever()
if __name__ == '__main__': main()
@@ -1,301 +0,0 @@
"""Durable checkpoint journal for the generic Online Agent worker.
The JSON task state remains a fast cache. This SQLite journal is the durable,
append-only evidence used for restart/resume/replay decisions.
"""
from __future__ import annotations
from dataclasses import dataclass
from datetime import datetime, timezone
import hashlib
import json
from pathlib import Path
import sqlite3
from typing import Any, Mapping
PHASE_ORDER = {
"claimed": 10,
"workspace_prepared": 20,
"verification_passed": 30,
"source_capture_done": 40,
"commit_done": 40,
"reconciliation_required": 45,
"result_pending": 50,
"result_submitted": 60,
}
class CheckpointError(RuntimeError):
pass
def canonical(value: object) -> bytes:
return json.dumps(value, sort_keys=True, separators=(",", ":"), ensure_ascii=False, default=str).encode("utf-8")
def sha256_json(value: object) -> str:
return hashlib.sha256(canonical(value)).hexdigest()
def now() -> str:
return datetime.now(timezone.utc).isoformat()
@dataclass(frozen=True)
class CheckpointBinding:
task_id: str
attempt: int
execution_digest: str
job_id: str
repository: str
base_oid: str
expected_branch_oid: str
target_branch: str
generation: int
def material(self) -> dict[str, Any]:
return {
"task_id": self.task_id,
"attempt": self.attempt,
"execution_digest": self.execution_digest,
"job_id": self.job_id,
"repository": self.repository,
"base_oid": self.base_oid,
"expected_branch_oid": self.expected_branch_oid,
"target_branch": self.target_branch,
"generation": self.generation,
}
@property
def binding_sha256(self) -> str:
return sha256_json(self.material())
class CheckpointStore:
def __init__(self, path: Path) -> None:
self.path = Path(path)
self.path.parent.mkdir(parents=True, exist_ok=True)
with self._connect() as connection:
connection.executescript(
"""
CREATE TABLE IF NOT EXISTS checkpoints(
task_id TEXT NOT NULL,
attempt INTEGER NOT NULL,
sequence INTEGER NOT NULL,
phase TEXT NOT NULL,
phase_order INTEGER NOT NULL,
execution_digest TEXT NOT NULL,
job_id TEXT NOT NULL,
repository TEXT NOT NULL,
base_oid TEXT NOT NULL,
expected_branch_oid TEXT NOT NULL,
target_branch TEXT NOT NULL,
generation INTEGER NOT NULL,
binding_sha256 TEXT NOT NULL,
payload_json TEXT NOT NULL,
payload_sha256 TEXT NOT NULL,
created_at TEXT NOT NULL,
PRIMARY KEY(task_id, attempt, sequence)
);
CREATE INDEX IF NOT EXISTS idx_checkpoints_task_latest
ON checkpoints(task_id, attempt DESC, sequence DESC);
CREATE UNIQUE INDEX IF NOT EXISTS idx_checkpoints_exact_replay
ON checkpoints(task_id, attempt, phase, payload_sha256);
CREATE TABLE IF NOT EXISTS checkpoint_heads(
task_id TEXT PRIMARY KEY,
attempt INTEGER NOT NULL,
sequence INTEGER NOT NULL,
phase TEXT NOT NULL,
phase_order INTEGER NOT NULL,
execution_digest TEXT NOT NULL,
binding_sha256 TEXT NOT NULL,
payload_sha256 TEXT NOT NULL,
updated_at TEXT NOT NULL
);
"""
)
def _connect(self) -> sqlite3.Connection:
connection = sqlite3.connect(self.path, timeout=30.0)
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=30000")
return connection
@staticmethod
def _phase_order(phase: str) -> int:
try:
return PHASE_ORDER[phase]
except KeyError as exc:
raise CheckpointError("CHECKPOINT_PHASE_INVALID") from exc
def append(
self,
binding: CheckpointBinding,
phase: str,
payload: Mapping[str, Any],
) -> dict[str, Any]:
order = self._phase_order(phase)
payload_value = dict(payload)
payload_sha = sha256_json(payload_value)
with self._connect() as connection:
connection.execute("BEGIN IMMEDIATE")
replay = connection.execute(
"""
SELECT * FROM checkpoints
WHERE task_id=? AND attempt=? AND phase=? AND payload_sha256=?
""",
(binding.task_id, binding.attempt, phase, payload_sha),
).fetchone()
if replay is not None:
connection.commit()
return dict(replay)
head = connection.execute(
"SELECT * FROM checkpoint_heads WHERE task_id=?",
(binding.task_id,),
).fetchone()
if head is not None:
head_attempt = int(head["attempt"])
head_order = int(head["phase_order"])
if binding.attempt < head_attempt:
raise CheckpointError("CHECKPOINT_ATTEMPT_REGRESSION")
if binding.attempt == head_attempt:
if head["execution_digest"] != binding.execution_digest:
raise CheckpointError("CHECKPOINT_EXECUTION_DIGEST_CONFLICT")
if head["binding_sha256"] != binding.binding_sha256:
raise CheckpointError("CHECKPOINT_SCOPE_MISMATCH_DENY")
if order < head_order and phase != "reconciliation_required":
raise CheckpointError("CHECKPOINT_PHASE_REGRESSION")
sequence = int(head["sequence"]) + 1
else:
sequence = 1
else:
sequence = 1
created_at = now()
connection.execute(
"""
INSERT INTO checkpoints(
task_id,attempt,sequence,phase,phase_order,execution_digest,job_id,
repository,base_oid,expected_branch_oid,target_branch,generation,
binding_sha256,payload_json,payload_sha256,created_at
) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)
""",
(
binding.task_id,
binding.attempt,
sequence,
phase,
order,
binding.execution_digest,
binding.job_id,
binding.repository,
binding.base_oid,
binding.expected_branch_oid,
binding.target_branch,
binding.generation,
binding.binding_sha256,
canonical(payload_value).decode("utf-8"),
payload_sha,
created_at,
),
)
connection.execute(
"""
INSERT INTO checkpoint_heads(
task_id,attempt,sequence,phase,phase_order,execution_digest,
binding_sha256,payload_sha256,updated_at
) VALUES (?,?,?,?,?,?,?,?,?)
ON CONFLICT(task_id) DO UPDATE SET
attempt=excluded.attempt,
sequence=excluded.sequence,
phase=excluded.phase,
phase_order=excluded.phase_order,
execution_digest=excluded.execution_digest,
binding_sha256=excluded.binding_sha256,
payload_sha256=excluded.payload_sha256,
updated_at=excluded.updated_at
""",
(
binding.task_id,
binding.attempt,
sequence,
phase,
order,
binding.execution_digest,
binding.binding_sha256,
payload_sha,
created_at,
),
)
connection.commit()
row = connection.execute(
"SELECT * FROM checkpoints WHERE task_id=? AND attempt=? AND sequence=?",
(binding.task_id, binding.attempt, sequence),
).fetchone()
return dict(row)
def latest(self, task_id: str) -> dict[str, Any] | None:
with self._connect() as connection:
row = connection.execute(
"""
SELECT * FROM checkpoints
WHERE task_id=?
ORDER BY attempt DESC, sequence DESC
LIMIT 1
""",
(task_id,),
).fetchone()
if row is None:
return None
value = dict(row)
value["payload"] = json.loads(value.pop("payload_json"))
return value
def latest_for_attempt(self, task_id: str, attempt: int) -> dict[str, Any] | None:
with self._connect() as connection:
row = connection.execute(
"""
SELECT * FROM checkpoints
WHERE task_id=? AND attempt=?
ORDER BY sequence DESC
LIMIT 1
""",
(task_id, attempt),
).fetchone()
if row is None:
return None
value = dict(row)
value["payload"] = json.loads(value.pop("payload_json"))
return value
def verify_latest_binding(self, binding: CheckpointBinding) -> dict[str, Any] | None:
value = self.latest(binding.task_id)
if value is None:
return None
if int(value["attempt"]) > binding.attempt:
raise CheckpointError("CHECKPOINT_FUTURE_ATTEMPT")
# A previous attempt may be reused only when the immutable execution/scope binding matches.
if value["execution_digest"] != binding.execution_digest:
return None
expected = CheckpointBinding(
task_id=value["task_id"],
attempt=int(value["attempt"]),
execution_digest=value["execution_digest"],
job_id=value["job_id"],
repository=value["repository"],
base_oid=value["base_oid"],
expected_branch_oid=value["expected_branch_oid"],
target_branch=value["target_branch"],
generation=int(value["generation"]),
)
current_scope = binding.material()
historical_scope = expected.material()
current_scope.pop("attempt", None)
historical_scope.pop("attempt", None)
if sha256_json(current_scope) != sha256_json(historical_scope):
raise CheckpointError("CHECKPOINT_SCOPE_MISMATCH_DENY")
return value
@@ -1,384 +0,0 @@
"""Bounded read-only repository interface map for governed Online Agent analysis.
The compact map intentionally returns symbols and short source excerpts rather than a
source archive. It is designed for control-plane consumers that cannot safely ingest a
multi-megabyte repository snapshot.
"""
from __future__ import annotations
import ast
import hashlib
import json
from pathlib import Path
import re
from typing import Any, Iterable, Mapping, Sequence
SCHEMA = "aios.engineering.interface-map.v1"
MAX_RESULT_BYTES = 120 * 1024
TARGET_RESULT_BYTES = 108 * 1024
MAX_MODULES = 40
MAX_SYMBOLS = 80
MAX_TESTS = 40
MAX_EXCERPTS = 25
MAX_EXCERPT_CHARS = 1200
MAX_FILE_BYTES = 2 * 1024 * 1024
ROOTS = (
"active-runtime/app",
"runtime/agent-os",
"tests",
"db/migrations",
"migrations",
"deploy/systemd",
"docs/task-plane",
"docs/superpowers/specs",
)
SUFFIXES = {".py", ".sql", ".service", ".md", ".json", ".yaml", ".yml"}
DEFAULT_TERMS = {
"agent", "worker", "fleet", "scheduler", "scheduling", "task", "claim", "lease",
"heartbeat", "capacity", "concurrency", "priority", "fairness", "queue", "retry",
"backoff", "circuit", "health", "liveness", "scope", "tenant", "project", "trace",
"message", "checkpoint", "recovery", "idempotency", "dispatch", "assignment",
}
CONTRACT_TERMS: dict[str, tuple[str, ...]] = {
"FLEET_AGENT_REGISTRATION": ("agent", "register", "worker", "capability", "version"),
"FLEET_CAPACITY_SCHEDULING": ("capacity", "slot", "concurrency", "scheduler", "queue"),
"FLEET_FAIRNESS": ("fair", "priority", "queue", "tenant", "project", "starvation"),
"FLEET_FAULT_ISOLATION": ("failure", "health", "liveness", "timeout", "circuit", "poison"),
"FLEET_WORKER_REASSIGNMENT": ("reassign", "assignment", "claim", "lease", "retry", "worker"),
"FLEET_SCOPE_ISOLATION": ("tenant", "project", "scope", "conversation", "task", "run"),
"FLEET_IDEMPOTENT_DISPATCH": ("idempot", "dispatch", "claim", "lease", "result", "attempt"),
"FLEET_FAILURE_RECOVERY": ("checkpoint", "resume", "recover", "retry", "cleanup", "receipt"),
}
SENSITIVE_RE = re.compile(
r"(?i)(authorization\s*:\s*bearer\s+\S+|"
r"(?:password|passwd|secret|token|api[_-]?key)\s*[:=]\s*['\"]?[^'\"\s,;]+)"
)
SQL_TABLE_RE = re.compile(
r"(?is)\bcreate\s+table\s+(?:if\s+not\s+exists\s+)?[\"`[]?([A-Za-z_][A-Za-z0-9_.-]*)"
)
class CompactInterfaceMapError(RuntimeError):
pass
def _sha(data: bytes) -> str:
return hashlib.sha256(data).hexdigest()
def _bounded_excerpt(text: str, start_line: int, end_line: int) -> str:
lines = text.splitlines()
start = max(0, start_line - 2)
end = min(len(lines), max(end_line + 1, start + 1))
excerpt = "\n".join(lines[start:end])
excerpt = SENSITIVE_RE.sub("<redacted-sensitive-source-pattern>", excerpt)
if len(excerpt) > MAX_EXCERPT_CHARS:
excerpt = excerpt[: MAX_EXCERPT_CHARS - 1] + "…"
return excerpt
def _signal_terms(signals: Sequence[str]) -> set[str]:
terms = set(DEFAULT_TERMS)
for signal in signals:
if not isinstance(signal, str):
continue
terms.update(
token.lower()
for token in re.findall(r"[A-Za-z][A-Za-z0-9_.-]{2,63}", signal)
)
return set(sorted(terms)[:96])
def _candidate_files(root: Path, terms: set[str]) -> list[dict[str, Any]]:
root = root.resolve()
candidates: list[dict[str, Any]] = []
for relative_root in ROOTS:
directory = (root / relative_root).resolve(strict=False)
if directory == root or root not in directory.parents:
raise CompactInterfaceMapError("INTERFACE_MAP_SCOPE_INVALID")
if not directory.is_dir() or directory.is_symlink():
continue
for path in sorted(directory.rglob("*")):
if path.is_symlink() or not path.is_file() or path.suffix.lower() not in SUFFIXES:
continue
resolved = path.resolve(strict=True)
if root not in resolved.parents:
raise CompactInterfaceMapError("INTERFACE_MAP_PATH_ESCAPE")
data = path.read_bytes()
if len(data) > MAX_FILE_BYTES:
continue
relative = path.relative_to(root).as_posix()
text = data.decode("utf-8", "ignore")
searchable = (relative + "\n" + text[:384000]).lower()
matched = sorted(term for term in terms if term in searchable)
if not matched:
continue
path_bonus = sum(3 for term in terms if term in relative.lower())
test_bonus = 3 if relative.startswith("tests/") else 0
score = min(250, len(matched) + path_bonus + test_bonus)
candidates.append(
{
"path": relative,
"sha256": _sha(data),
"bytes": len(data),
"score": score,
"text": text,
"suffix": path.suffix.lower(),
}
)
candidates.sort(key=lambda item: (-item["score"], item["path"]))
return candidates
def _python_symbols(item: Mapping[str, Any], terms: set[str]) -> list[dict[str, Any]]:
try:
tree = ast.parse(str(item["text"]), filename=str(item["path"]))
except SyntaxError:
return []
result: list[dict[str, Any]] = []
class Visitor(ast.NodeVisitor):
def __init__(self) -> None:
self.stack: list[str] = []
def _record(self, node: ast.AST, kind: str, name: str) -> None:
qualified = ".".join([*self.stack, name]) if self.stack else name
haystack = f"{item['path']} {qualified}".lower()
score = sum(3 if term in haystack else 0 for term in terms)
if name.startswith("test_"):
score += 12
result.append(
{
"path": item["path"],
"symbol": qualified,
"kind": kind,
"line": int(getattr(node, "lineno", 1)),
"end_line": int(getattr(node, "end_lineno", getattr(node, "lineno", 1))),
"score": score + int(item["score"]),
}
)
def visit_ClassDef(self, node: ast.ClassDef) -> Any:
self._record(node, "class", node.name)
self.stack.append(node.name)
self.generic_visit(node)
self.stack.pop()
def visit_FunctionDef(self, node: ast.FunctionDef) -> Any:
self._record(node, "function", node.name)
self.stack.append(node.name)
self.generic_visit(node)
self.stack.pop()
def visit_AsyncFunctionDef(self, node: ast.AsyncFunctionDef) -> Any:
self._record(node, "async_function", node.name)
self.stack.append(node.name)
self.generic_visit(node)
self.stack.pop()
Visitor().visit(tree)
return result
def _surface_ref(symbol: Mapping[str, Any]) -> str:
return f"{symbol['path']}::{symbol['symbol']}"
def _contract_map(
symbols: Sequence[Mapping[str, Any]],
modules: Sequence[Mapping[str, Any]],
) -> dict[str, Any]:
result: dict[str, Any] = {}
module_paths = [str(item["path"]) for item in modules]
for contract, terms in CONTRACT_TERMS.items():
scored: list[tuple[int, str]] = []
for symbol in symbols:
haystack = _surface_ref(symbol).lower()
score = sum(1 for term in terms if term in haystack)
if score:
scored.append((score, _surface_ref(symbol)))
scored.sort(key=lambda item: (-item[0], item[1]))
surfaces = [value for _, value in scored[:6]]
if not surfaces:
for path in module_paths:
score = sum(1 for term in terms if term in path.lower())
if score:
surfaces.append(path)
if len(surfaces) == 6:
break
result[contract] = {
"status": "PARTIAL" if surfaces else "GAP",
"surfaces": surfaces,
}
return result
def _gap_evidence(symbols: Sequence[Mapping[str, Any]]) -> tuple[list[str], list[str], list[str]]:
haystack = "\n".join(_surface_ref(item).lower() for item in symbols)
invariants = [
"Online Task claim/heartbeat remains the authoritative worker ownership boundary.",
"Governed mutation remains separated from read-only analysis and requires WriterLease.",
"P1 attempt-aware result idempotency and durable checkpoint recovery are reusable fleet primitives.",
"Scope/trace/message lineage must remain attached when work is scheduled or reassigned.",
]
failure_windows: list[str] = []
if "scheduler" not in haystack:
failure_windows.append("No first-class durable fleet scheduler symbol detected; worker-local polling may still be the scheduling mechanism.")
if "capacity" not in haystack and "slot" not in haystack:
failure_windows.append("No explicit durable worker-capacity/slot primitive detected.")
if "fair" not in haystack and "starvation" not in haystack:
failure_windows.append("No explicit fairness/starvation policy symbol detected.")
if "circuit" not in haystack:
failure_windows.append("No explicit circuit-breaker primitive detected for repeated worker/task failures.")
if "reassign" not in haystack:
failure_windows.append("No explicit worker-reassignment primitive detected beyond lease expiry/retry semantics.")
reuse_points: list[str] = []
desired = (
("checkpoint", "P1 durable checkpoint/resume"),
("message", "P0-B MessageEnvelope/handoff"),
("trace", "P0-C trace correlation"),
("scope", "P0-A scope isolation"),
("lease", "claim/WriterLease ownership"),
("heartbeat", "worker liveness"),
("idempot", "retry-safe dispatch/result"),
)
for token, label in desired:
match = next((item for item in symbols if token in _surface_ref(item).lower()), None)
if match is not None:
reuse_points.append(f"{label}: {_surface_ref(match)}")
return invariants, failure_windows, reuse_points
def _trim_to_budget(value: dict[str, Any]) -> dict[str, Any]:
def size() -> int:
return len(json.dumps(value, ensure_ascii=False, sort_keys=True, separators=(",", ":")).encode("utf-8"))
while size() > TARGET_RESULT_BYTES and value["bounded_excerpts"]:
value["bounded_excerpts"].pop()
while size() > TARGET_RESULT_BYTES and len(value["implementation_map"]["functions"]) > 20:
value["implementation_map"]["functions"].pop()
while size() > TARGET_RESULT_BYTES and len(value["implementation_map"]["classes"]) > 15:
value["implementation_map"]["classes"].pop()
while size() > TARGET_RESULT_BYTES and len(value["implementation_map"]["tests"]) > 15:
value["implementation_map"]["tests"].pop()
while size() > TARGET_RESULT_BYTES and len(value["implementation_map"]["modules"]) > 20:
value["implementation_map"]["modules"].pop()
final_size = size()
if final_size > MAX_RESULT_BYTES:
raise CompactInterfaceMapError("INTERFACE_MAP_RESULT_TOO_LARGE")
value["result_bytes"] = final_size
return value
def build_compact_interface_map(
worktree: Path,
source_signals: Sequence[str],
*,
source_commit: str,
) -> dict[str, Any]:
root = Path(worktree)
if not root.is_dir() or root.is_symlink():
raise CompactInterfaceMapError("INTERFACE_MAP_WORKTREE_INVALID")
if re.fullmatch(r"[0-9a-f]{40}", source_commit) is None:
raise CompactInterfaceMapError("INTERFACE_MAP_SOURCE_COMMIT_INVALID")
terms = _signal_terms(source_signals)
candidates = _candidate_files(root, terms)
if not candidates:
raise CompactInterfaceMapError("INTERFACE_MAP_EMPTY")
module_candidates = candidates[:MAX_MODULES]
modules = [
{
"path": item["path"],
"sha256": item["sha256"],
"bytes": item["bytes"],
"score": item["score"],
}
for item in module_candidates
]
symbols: list[dict[str, Any]] = []
tables: list[dict[str, Any]] = []
systemd_units: list[dict[str, Any]] = []
for item in candidates:
if item["suffix"] == ".py":
symbols.extend(_python_symbols(item, terms))
elif item["suffix"] == ".sql":
for table in SQL_TABLE_RE.findall(str(item["text"])):
tables.append({"path": item["path"], "table": table, "sha256": item["sha256"]})
elif item["suffix"] == ".service":
systemd_units.append({"path": item["path"], "unit": Path(str(item["path"])).name, "sha256": item["sha256"]})
symbols.sort(key=lambda item: (-int(item["score"]), str(item["path"]), str(item["symbol"])))
all_symbols = list(symbols)
test_symbols = [
item for item in all_symbols
if str(item["path"]).startswith("tests/") or str(item["symbol"]).split(".")[-1].startswith("test_")
][: min(MAX_TESTS, 24)]
non_test_symbols = [
item for item in all_symbols
if item not in test_symbols
][: max(0, MAX_SYMBOLS - len(test_symbols))]
symbols = [*test_symbols, *non_test_symbols]
symbols.sort(key=lambda item: (-int(item["score"]), str(item["path"]), str(item["symbol"])))
classes = [
{k: item[k] for k in ("path", "symbol", "line", "end_line")}
for item in symbols
if item["kind"] == "class"
]
functions = [
{k: item[k] for k in ("path", "symbol", "line", "end_line")}
for item in symbols
if item["kind"] in {"function", "async_function"} and not str(item["symbol"]).split(".")[-1].startswith("test_")
]
tests = [
{k: item[k] for k in ("path", "symbol", "line", "end_line")}
for item in symbols
if str(item["path"]).startswith("tests/") or str(item["symbol"]).split(".")[-1].startswith("test_")
][:MAX_TESTS]
excerpts: list[dict[str, Any]] = []
by_path = {str(item["path"]): item for item in candidates}
for symbol in symbols:
if len(excerpts) == MAX_EXCERPTS:
break
item = by_path.get(str(symbol["path"]))
if item is None:
continue
excerpt = _bounded_excerpt(str(item["text"]), int(symbol["line"]), int(symbol["end_line"]))
if not excerpt.strip():
continue
excerpts.append(
{
"path": symbol["path"],
"symbol": symbol["symbol"],
"sha256": item["sha256"],
"excerpt": excerpt,
}
)
invariants, failure_windows, reuse_points = _gap_evidence(all_symbols)
result = {
"schema": SCHEMA,
"source_commit": source_commit,
"implementation_map": {
"modules": modules,
"classes": classes[:MAX_SYMBOLS],
"functions": functions[:MAX_SYMBOLS],
"tables": tables[:40],
"systemd_units": systemd_units[:20],
"tests": tests,
"current_invariants": invariants,
"failure_windows": failure_windows,
"reuse_points": reuse_points,
},
"bounded_excerpts": excerpts,
"contracts": _contract_map(all_symbols, modules),
}
return _trim_to_budget(result)
@@ -1,397 +0,0 @@
"""Canonical governed execution projection for Online Agent Tasks.
The durable governed binding is authoritative. Natural-language task fields and
worker configuration are never allowed to invent repository, ref, generation, or
idempotency coordinates.
"""
from __future__ import annotations
from dataclasses import asdict, dataclass
import hashlib
import json
import re
from pathlib import Path
from typing import Any, Mapping, Sequence
import urllib.error
import urllib.request
EXECUTION_SCHEMA = "aios.online-agent.execution.v2"
PROJECTION_SCHEMA = "aios.online-agent.task-projection.v1"
OID_RE = re.compile(r"^[0-9a-f]{40}$")
TASK_RE = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._:-]{2,179}$")
BRANCH_RE = re.compile(r"^agent/[A-Za-z0-9][A-Za-z0-9._/-]{2,179}$")
KEY_RE = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._:-]{7,159}$")
PROTECTED_PREFIXES = ("main", "master", "refs/", "tags/", "tag/", "release/", "baseline/", "production/")
MUTATION_ACTIONS = frozenset({"deterministic"})
SUPPORTED_ACTIONS = frozenset({"deterministic", "source_capture"})
class ExecutionProjectionError(RuntimeError):
"""A canonical fail-closed projection or validation error."""
class OnlineTaskProjectionClient:
"""Official HTTP projection client using only a control credential reference."""
def __init__(self, api_base: str, control_token_path: Path) -> None:
self.api_base = api_base.rstrip("/")
self.control_token_path = Path(control_token_path)
def _token(self) -> str:
path = self.control_token_path
if path.is_symlink() or not path.is_file():
_error("ONLINE_TASK_PROJECTION_CONTROL_UNAVAILABLE")
value = path.read_text(encoding="utf-8").strip()
if len(value) < 32:
_error("ONLINE_TASK_PROJECTION_CONTROL_INVALID")
return value
def create(
self,
projected_task: Mapping[str, Any],
*,
binding: "GovernedExecutionBinding",
) -> dict[str, Any]:
body = dict(projected_task)
payload = _canonical(body).encode("utf-8")
request = urllib.request.Request(
self.api_base + "/v1/tasks",
method="POST",
data=payload,
headers={
"Authorization": "Bearer " + self._token(),
"Content-Type": "application/json",
"Accept": "application/json",
},
)
try:
with urllib.request.urlopen(request, timeout=15) as response:
result = json.loads(response.read().decode("utf-8"))
status = response.status
except urllib.error.HTTPError as exc:
raw = exc.read(8192).decode("utf-8", "replace")
try:
error = json.loads(raw).get("error")
except (AttributeError, json.JSONDecodeError):
error = "HTTP_ERROR"
raise ExecutionProjectionError(f"ONLINE_TASK_PROJECTION_FAILED:{exc.code}:{error}") from exc
except (OSError, urllib.error.URLError, TimeoutError) as exc:
raise ExecutionProjectionError("ONLINE_TASK_PROJECTION_UNAVAILABLE") from exc
if status not in {200, 201} or not isinstance(result, dict):
_error("ONLINE_TASK_PROJECTION_RESPONSE_INVALID")
if result.get("task_id") != binding.task_id or result.get("status") not in {"QUEUED", "VERIFYING", "SUCCEEDED"}:
_error("ONLINE_TASK_PROJECTION_RESPONSE_INVALID")
metadata = result.get("metadata")
if not isinstance(metadata, Mapping):
_error("ONLINE_TASK_PROJECTION_RESPONSE_INVALID")
returned_execution = validate_execution_envelope(
_object(metadata.get("execution"), "ONLINE_TASK_PROJECTION_RESPONSE_INVALID"),
binding=binding,
)
submitted_metadata = _object(body.get("metadata"), "ONLINE_TASK_PROJECTION_REQUEST_INVALID")
submitted_execution = validate_execution_envelope(
_object(submitted_metadata.get("execution"), "ONLINE_TASK_PROJECTION_REQUEST_INVALID"),
binding=binding,
)
if _canonical(returned_execution) != _canonical(submitted_execution):
_error("ONLINE_TASK_PROJECTION_RESPONSE_BINDING_INVALID")
return result
def _error(code: str) -> None:
raise ExecutionProjectionError(code)
def _object(value: object, code: str) -> dict[str, Any]:
if not isinstance(value, Mapping):
_error(code)
return dict(value)
def _string(value: object, code: str) -> str:
if not isinstance(value, str) or not value.strip():
_error(code)
return value.strip()
def _task_id(value: object) -> str:
value = _string(value, "TASK_ID_INVALID")
if TASK_RE.fullmatch(value) is None:
_error("TASK_ID_INVALID")
return value
def _oid(value: object, code: str) -> str:
value = _string(value, code)
if OID_RE.fullmatch(value) is None:
_error(code)
return value
def _branch(value: object) -> str:
value = _string(value, "TARGET_BRANCH_INVALID")
if BRANCH_RE.fullmatch(value) is None or value.startswith(PROTECTED_PREFIXES):
_error("TARGET_BRANCH_INVALID")
return value
def _generation(value: object) -> int:
if not isinstance(value, int) or isinstance(value, bool) or value < 1:
_error("GENERATION_INVALID")
return value
def _canonical(value: object) -> str:
return json.dumps(value, ensure_ascii=False, sort_keys=True, separators=(",", ":"))
def _digest(value: object) -> str:
return hashlib.sha256(_canonical(value).encode("utf-8")).hexdigest()
def _source_signals(value: object) -> tuple[str, ...]:
"""Normalize untrusted natural-language hints before they enter a signed envelope."""
if value is None:
return ()
if not isinstance(value, Sequence) or isinstance(value, (str, bytes)):
_error("SOURCE_CAPTURE_SIGNALS_INVALID")
result: list[str] = []
for item in value:
if not isinstance(item, str):
_error("SOURCE_CAPTURE_SIGNALS_INVALID")
text = " ".join(item.split()).strip()
if not text:
continue
# Projection may accept long task prose, but execution envelopes stay bounded.
text = text[:512].rstrip()
if text not in result:
result.append(text)
if len(result) == 16:
break
return tuple(result)
def _capabilities(value: object) -> tuple[str, ...]:
if value is None:
return ()
if not isinstance(value, Sequence) or isinstance(value, (str, bytes)):
_error("CAPABILITY_SCOPE_INVALID")
result = tuple(sorted({_string(item, "CAPABILITY_SCOPE_INVALID") for item in value}))
if any(len(item) > 120 for item in result):
_error("CAPABILITY_SCOPE_INVALID")
return result
@dataclass(frozen=True, slots=True)
class GovernedExecutionBinding:
task_id: str
job_id: str
repository: str
base_oid: str
expected_branch_oid: str
target_branch: str
branch_mode: str
generation: int
idempotency_key: str
@classmethod
def from_mapping(cls, value: Mapping[str, Any]) -> "GovernedExecutionBinding":
data = dict(value)
task_id = _task_id(data.get("task_id"))
job_id = _string(data.get("job_id"), "JOB_ID_REQUIRED")
if TASK_RE.fullmatch(job_id) is None:
_error("JOB_ID_INVALID")
repository = _string(data.get("repository"), "REPOSITORY_INVALID")
if repository != "aios/aios":
_error("WRONG_REPOSITORY_ENVELOPE_DENY")
base_oid = _oid(data.get("base_oid"), "BASE_OID_INVALID")
expected = _oid(data.get("expected_branch_oid"), "EXPECTED_BRANCH_OID_INVALID")
target = _branch(data.get("target_branch"))
mode = _string(data.get("branch_mode"), "BRANCH_MODE_MISSING")
if mode not in {"CREATE_NEW", "CONTINUE_EXISTING"}:
_error("BRANCH_MODE_INVALID")
generation = _generation(data.get("generation"))
if mode == "CREATE_NEW" and expected != base_oid:
_error("EXPECTED_BASE_CONFLICT")
key = _string(data.get("idempotency_key"), "IDEMPOTENCY_KEY_INVALID")
if KEY_RE.fullmatch(key) is None:
_error("IDEMPOTENCY_KEY_INVALID")
return cls(task_id, job_id, repository, base_oid, expected, target, mode, generation, key)
def public(self) -> dict[str, Any]:
return asdict(self)
def binding_from_bootstrap(value: Mapping[str, Any]) -> GovernedExecutionBinding:
return GovernedExecutionBinding.from_mapping(value)
def validate_execution_envelope(
value: Mapping[str, Any],
*,
binding: GovernedExecutionBinding | None = None,
worker_capabilities: Sequence[str] | None = None,
) -> dict[str, Any]:
envelope = dict(value)
if envelope.get("schema") != EXECUTION_SCHEMA:
_error("EXECUTION_ENVELOPE_SCHEMA_VALIDATION")
candidate = GovernedExecutionBinding.from_mapping(envelope)
if binding is not None and candidate != binding:
comparisons = (
(candidate.task_id, binding.task_id, "TASK_BINDING_CONFLICT"),
(candidate.job_id, binding.job_id, "JOB_BINDING_CONFLICT"),
(candidate.repository, binding.repository, "WRONG_REPOSITORY_ENVELOPE_DENY"),
(candidate.base_oid, binding.base_oid, "WRONG_BASE_ENVELOPE_DENY"),
(candidate.expected_branch_oid, binding.expected_branch_oid, "WRONG_EXPECTED_HEAD_ENVELOPE_DENY"),
(candidate.target_branch, binding.target_branch, "WRONG_BRANCH_ENVELOPE_DENY"),
(candidate.generation, binding.generation, "WRONG_GENERATION_ENVELOPE_DENY"),
(candidate.branch_mode, binding.branch_mode, "BRANCH_MODE_BINDING_CONFLICT"),
(candidate.idempotency_key, binding.idempotency_key, "IDEMPOTENCY_BINDING_CONFLICT"),
)
for actual, expected, code in comparisons:
if actual != expected:
_error(code)
action = _string(envelope.get("action"), "EXECUTION_ACTION_MISSING")
if action not in SUPPORTED_ACTIONS:
_error("EXECUTION_ACTION_UNSUPPORTED")
capability_scope = _capabilities(envelope.get("capability_scope"))
if worker_capabilities is not None and not set(capability_scope).issubset(set(worker_capabilities)):
_error("CAPABILITY_MISMATCH_DENY")
workspace = _object(envelope.get("workspace"), "WORKSPACE_POLICY_REQUIRED")
if workspace.get("authority_git_dir") != "/data/aios-gitops/repo.git" or workspace.get("read_only_repository") is not True:
_error("WORKSPACE_POLICY_DENY")
governance = _object(envelope.get("governance"), "GOVERNANCE_POLICY_REQUIRED")
governed_coordinates = (
(governance.get("task_id"), candidate.task_id, "TASK_BINDING_CONFLICT"),
(governance.get("job_id"), candidate.job_id, "JOB_BINDING_CONFLICT"),
(governance.get("idempotency_key"), candidate.idempotency_key, "IDEMPOTENCY_BINDING_CONFLICT"),
)
for actual_coordinate, expected_coordinate, code in governed_coordinates:
if actual_coordinate != expected_coordinate:
_error(code)
commit_key = _string(governance.get("commit_idempotency_key"), "COMMIT_IDEMPOTENCY_KEY_INVALID")
if KEY_RE.fullmatch(commit_key) is None:
_error("COMMIT_IDEMPOTENCY_KEY_INVALID")
if governance.get("writer_lease") is not (action in MUTATION_ACTIONS):
_error("WRITER_LEASE_POLICY_DENY")
for field in ("main_write", "production_write", "tag_write", "baseline_write", "force_push", "protected_ref_bypass"):
if governance.get(field, False) is not False:
_error("POLICY_DENY")
changes = envelope.get("change_set")
verification = envelope.get("verification")
if not isinstance(changes, list) or not isinstance(verification, list):
_error("EXECUTION_OPERATIONS_INVALID")
if action == "source_capture":
if changes or verification:
_error("SOURCE_CAPTURE_MUTATION_DENY")
signals = list(_source_signals(envelope.get("source_signals") or []))
if envelope.get("source_signals") != signals:
_error("SOURCE_CAPTURE_SIGNALS_INVALID")
elif not changes:
_error("CHANGE_SET_INVALID")
declared = str(envelope.get("execution_digest") or "")
material = {key: item for key, item in envelope.items() if key != "execution_digest"}
actual = _digest(material)
if declared != actual:
_error("EXECUTION_DIGEST_INVALID")
return {**material, "execution_digest": actual}
def project_execution_envelope(
binding: GovernedExecutionBinding,
execution_request: Mapping[str, Any] | None = None,
*,
required_capabilities: Sequence[str] = (),
) -> dict[str, Any]:
request = dict(execution_request or {})
action = str(request.get("action") or "source_capture")
if action not in SUPPORTED_ACTIONS:
_error("EXECUTION_ACTION_UNSUPPORTED")
capabilities = _capabilities(request.get("capability_scope", required_capabilities))
governance = {
"task_id": binding.task_id,
"job_id": binding.job_id,
"idempotency_key": binding.idempotency_key,
"commit_idempotency_key": str(request.get("commit_idempotency_key") or f"{binding.task_id}-commit-g{binding.generation}"),
"writer_lease": action in MUTATION_ACTIONS,
"main_write": False,
"production_write": False,
"tag_write": False,
"baseline_write": False,
"force_push": False,
"protected_ref_bypass": False,
}
supplied_governance = request.get("governance")
if supplied_governance is not None:
governance.update(_object(supplied_governance, "GOVERNANCE_POLICY_REQUIRED"))
material: dict[str, Any] = {
"schema": EXECUTION_SCHEMA,
**binding.public(),
"action": action,
"adapter": str(request.get("adapter") or ("deterministic" if action == "deterministic" else "source_capture")),
"capability_scope": list(capabilities),
"change_set": list(request.get("change_set") or []),
"verification": list(request.get("verification") or []),
"source_signals": list(_source_signals(request.get("source_signals") or [])) if action == "source_capture" else [],
"governance": governance,
"workspace": {
"authority_git_dir": "/data/aios-gitops/repo.git",
"read_only_repository": True,
"cleanup_policy": "always",
},
}
envelope = {**material, "execution_digest": _digest(material)}
return validate_execution_envelope(envelope, binding=binding)
def project_online_task(
binding: GovernedExecutionBinding,
task_request: Mapping[str, Any],
execution_request: Mapping[str, Any] | None = None,
) -> dict[str, Any]:
request = dict(task_request)
required = _capabilities(request.get("required_capabilities"))
execution = project_execution_envelope(binding, execution_request, required_capabilities=required)
metadata = dict(request.get("metadata") or {})
if "execution" in metadata:
validate_execution_envelope(_object(metadata["execution"], "EXECUTION_ENVELOPE_INVALID"), binding=binding)
if metadata["execution"] != execution:
_error("EXECUTION_PROJECTION_CONFLICT")
metadata["execution"] = execution
metadata["execution_projection"] = {
"schema": PROJECTION_SCHEMA,
"task_id": binding.task_id,
"job_id": binding.job_id,
"execution_digest": execution["execution_digest"],
}
return {
**request,
"task_id": binding.task_id,
"metadata": metadata,
"required_capabilities": list(required),
"idempotency_key": str(request.get("idempotency_key") or f"online-task:{binding.idempotency_key}"),
}
def resolve_task_execution(
task: Mapping[str, Any],
binding: GovernedExecutionBinding,
*,
worker_capabilities: Sequence[str] | None = None,
) -> tuple[dict[str, Any], str]:
metadata = task.get("metadata")
if isinstance(metadata, Mapping) and "execution" in metadata:
value = validate_execution_envelope(
_object(metadata["execution"], "EXECUTION_ENVELOPE_INVALID"),
binding=binding,
worker_capabilities=worker_capabilities,
)
return value, "persisted"
projected = project_execution_envelope(
binding,
{"action": "source_capture", "capability_scope": task.get("required_capabilities") or []},
required_capabilities=task.get("required_capabilities") or [],
)
validate_execution_envelope(projected, binding=binding, worker_capabilities=worker_capabilities)
return projected, "recovered_projection"
@@ -1,187 +0,0 @@
"""Durable, authority-bound deterministic execution plans for Online Agent Tasks."""
from __future__ import annotations
from contextlib import contextmanager
from dataclasses import dataclass
import fcntl
import hashlib
import json
from pathlib import Path
from typing import Any, Mapping
from aios_online_agent_execution import (
GovernedExecutionBinding,
project_execution_envelope,
)
class ExecutionPlanError(RuntimeError):
pass
def _canonical(value: object) -> bytes:
return json.dumps(value, sort_keys=True, separators=(",", ":"), ensure_ascii=False).encode("utf-8")
def _digest(value: object) -> str:
return hashlib.sha256(_canonical(value)).hexdigest()
def _task_key(task_id: str) -> str:
readable = "".join(ch if ch.isalnum() or ch in "._-" else "-" for ch in task_id).strip("-")[:72] or "task"
return readable + "-" + hashlib.sha256(task_id.encode("utf-8")).hexdigest()[:24]
@dataclass(frozen=True)
class ExecutionPlanRecord:
task_id: str
job_id: str
source_head_oid: str
generation: int
idempotency_key: str
plan_digest: str
execution: dict[str, Any]
def public(self) -> dict[str, Any]:
return {
"schema": "aios.online-agent.plan-record.v1",
"task_id": self.task_id,
"job_id": self.job_id,
"source_head_oid": self.source_head_oid,
"generation": self.generation,
"idempotency_key": self.idempotency_key,
"plan_digest": self.plan_digest,
"execution": dict(self.execution),
}
class ExecutionPlanStore:
def __init__(self, root: Path) -> None:
self.root = Path(root)
self.root.mkdir(parents=True, exist_ok=True)
def _path(self, task_id: str) -> Path:
return self.root / (_task_key(task_id) + ".json")
@contextmanager
def _lock(self, task_id: str):
path = self.root / (_task_key(task_id) + ".lock")
with path.open("a+b") as handle:
fcntl.flock(handle.fileno(), fcntl.LOCK_EX)
try:
yield
finally:
fcntl.flock(handle.fileno(), fcntl.LOCK_UN)
@staticmethod
def _source_head(binding: GovernedExecutionBinding) -> str:
return binding.expected_branch_oid if binding.branch_mode == "CONTINUE_EXISTING" else binding.base_oid
def _read(self, task_id: str) -> ExecutionPlanRecord | None:
path = self._path(task_id)
if not path.is_file() or path.is_symlink():
return None
value = json.loads(path.read_text(encoding="utf-8"))
if not isinstance(value, Mapping) or value.get("schema") != "aios.online-agent.plan-record.v1":
raise ExecutionPlanError("EXECUTION_PLAN_RECORD_INVALID")
execution = value.get("execution")
if not isinstance(execution, Mapping):
raise ExecutionPlanError("EXECUTION_PLAN_RECORD_INVALID")
return ExecutionPlanRecord(
task_id=str(value.get("task_id") or ""),
job_id=str(value.get("job_id") or ""),
source_head_oid=str(value.get("source_head_oid") or ""),
generation=int(value.get("generation") or 0),
idempotency_key=str(value.get("idempotency_key") or ""),
plan_digest=str(value.get("plan_digest") or ""),
execution=dict(execution),
)
def register(
self,
binding: GovernedExecutionBinding,
request: Mapping[str, Any],
) -> dict[str, Any]:
with self._lock(binding.task_id):
return self._register_locked(binding, request)
def _register_locked(
self,
binding: GovernedExecutionBinding,
request: Mapping[str, Any],
) -> dict[str, Any]:
if request.get("schema") != "aios.online-agent.plan.v1":
raise ExecutionPlanError("EXECUTION_PLAN_SCHEMA_INVALID")
if request.get("task_id") != binding.task_id or request.get("job_id") != binding.job_id:
raise ExecutionPlanError("EXECUTION_PLAN_BINDING_CONFLICT")
if int(request.get("generation") or 0) != binding.generation:
raise ExecutionPlanError("EXECUTION_PLAN_GENERATION_CONFLICT")
source_head = str(request.get("source_head_oid") or "")
if source_head != self._source_head(binding):
raise ExecutionPlanError("EXECUTION_PLAN_SOURCE_HEAD_CONFLICT")
key = str(request.get("idempotency_key") or "")
if len(key) < 8:
raise ExecutionPlanError("EXECUTION_PLAN_IDEMPOTENCY_INVALID")
execution_request = request.get("execution")
if not isinstance(execution_request, Mapping) or execution_request.get("action") != "deterministic":
raise ExecutionPlanError("EXECUTION_PLAN_DETERMINISTIC_REQUIRED")
envelope = project_execution_envelope(binding, execution_request)
material = {
"task_id": binding.task_id,
"job_id": binding.job_id,
"source_head_oid": source_head,
"generation": binding.generation,
"idempotency_key": key,
"execution": dict(execution_request),
}
plan_digest = _digest(material)
existing = self._read(binding.task_id)
expected_previous = request.get("expected_previous_plan_digest")
if existing is not None:
if existing.idempotency_key == key:
if existing.plan_digest != plan_digest:
raise ExecutionPlanError("EXECUTION_PLAN_IDEMPOTENCY_CONFLICT")
return {**existing.public(), "idempotent": True, "execution_digest": envelope["execution_digest"]}
if expected_previous != existing.plan_digest:
raise ExecutionPlanError("EXECUTION_PLAN_REPLACEMENT_CAS_MISMATCH")
if existing.source_head_oid != source_head:
raise ExecutionPlanError("EXECUTION_PLAN_STALE_REPLACEMENT_DENY")
elif expected_previous not in (None, ""):
raise ExecutionPlanError("EXECUTION_PLAN_REPLACEMENT_CAS_MISMATCH")
record = ExecutionPlanRecord(
task_id=binding.task_id,
job_id=binding.job_id,
source_head_oid=source_head,
generation=binding.generation,
idempotency_key=key,
plan_digest=plan_digest,
execution=dict(execution_request),
)
path = self._path(binding.task_id)
temporary = path.with_suffix(".tmp")
temporary.write_text(
json.dumps(record.public(), sort_keys=True, separators=(",", ":")),
encoding="utf-8",
)
temporary.replace(path)
return {**record.public(), "idempotent": False, "execution_digest": envelope["execution_digest"]}
def resolve(self, binding: GovernedExecutionBinding) -> dict[str, Any] | None:
record = self._read(binding.task_id)
if record is None:
return None
if (
record.task_id != binding.task_id
or record.job_id != binding.job_id
or record.generation != binding.generation
):
raise ExecutionPlanError("EXECUTION_PLAN_BINDING_CONFLICT")
if record.source_head_oid != self._source_head(binding):
raise ExecutionPlanError("EXECUTION_PLAN_SOURCE_HEAD_DRIFT")
envelope = project_execution_envelope(binding, record.execution)
return {
"execution": dict(record.execution),
"plan_digest": record.plan_digest,
"execution_digest": envelope["execution_digest"],
}
File diff suppressed because it is too large Load Diff
+86
View File
@@ -0,0 +1,86 @@
"""Narrow, non-root client contract for the privileged signer broker."""
from __future__ import annotations
import base64
import hashlib
import json
from pathlib import Path
import re
import socket
from typing import Any, Protocol
DEFAULT_SIGNER_BROKER_SOCKET = Path("/run/aios/signer-broker.sock")
MAX_SIGNING_PAYLOAD = 1024 * 1024
CODE_RE = re.compile(r"^[A-Z0-9_.:-]{3,96}$")
class SignerBrokerError(RuntimeError):
def __init__(self, code: str):
self.code = code
super().__init__(code)
class SignerBroker(Protocol):
def self_test(self) -> None: ...
def sign(self, payload_kind: str, payload: bytes) -> tuple[bytes, str]: ...
class UnixSignerBroker:
"""AF_UNIX request client; it never accepts commands, paths, or shell text."""
def __init__(self, socket_path: Path = DEFAULT_SIGNER_BROKER_SOCKET, timeout: float = 15.0):
path = Path(socket_path)
if path != DEFAULT_SIGNER_BROKER_SOCKET or not path.is_absolute():
raise SignerBrokerError("SIGNER_BROKER_SOCKET_INVALID")
self.socket_path = path
self.timeout = timeout
def _request(self, operation: str, **payload: Any) -> dict[str, Any]:
if operation not in {"self_test", "sign"}:
raise SignerBrokerError("SIGNER_OPERATION_REJECTED")
request = {
"schema": "aios.signer-broker.request.v1",
"operation": operation,
**payload,
}
try:
with socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) as channel:
channel.settimeout(self.timeout)
channel.connect(str(self.socket_path))
channel.sendall((json.dumps(request, sort_keys=True, separators=(",", ":")) + "\n").encode())
raw = channel.makefile("rb").readline()
except (OSError, TimeoutError) as exc:
raise SignerBrokerError("SIGNER_BROKER_UNAVAILABLE") from exc
try:
response = json.loads(raw.decode())
except (UnicodeDecodeError, json.JSONDecodeError) as exc:
raise SignerBrokerError("SIGNER_BROKER_RESPONSE_INVALID") from exc
if not isinstance(response, dict) or response.get("schema") != "aios.signer-broker.response.v1":
raise SignerBrokerError("SIGNER_BROKER_RESPONSE_INVALID")
if response.get("status") != "PASS":
code = str(response.get("error_code") or "SIGNER_BROKER_REJECTED")
raise SignerBrokerError(code if CODE_RE.fullmatch(code) else "SIGNER_BROKER_REJECTED")
return response
def self_test(self) -> None:
self._request("self_test")
def sign(self, payload_kind: str, payload: bytes) -> tuple[bytes, str]:
if payload_kind not in {"approval_plan", "grant_payload"}:
raise SignerBrokerError("SIGNER_PAYLOAD_KIND_REJECTED")
if not isinstance(payload, bytes) or not payload or len(payload) > MAX_SIGNING_PAYLOAD:
raise SignerBrokerError("SIGNER_PAYLOAD_INVALID")
response = self._request(
"sign",
payload_kind=payload_kind,
payload_sha256=hashlib.sha256(payload).hexdigest(),
payload_base64=base64.b64encode(payload).decode("ascii"),
)
try:
signature = base64.b64decode(str(response["signature_base64"]), validate=True)
key_id = str(response["key_id"]).strip()
except (KeyError, ValueError) as exc:
raise SignerBrokerError("SIGNER_BROKER_RESPONSE_INVALID") from exc
if not signature or not key_id or len(key_id) > 256:
raise SignerBrokerError("SIGNER_BROKER_RESPONSE_INVALID")
return signature, key_id
@@ -0,0 +1,289 @@
"""Privileged, fixed-function signer broker service."""
from __future__ import annotations
import argparse
import base64
import binascii
from dataclasses import dataclass
from datetime import datetime, timezone
import hashlib
import json
import os
from pathlib import Path
import re
import socket
import socketserver
import stat
import struct
import subprocess
import tempfile
import threading
from typing import Any, Callable, Mapping, Protocol, Sequence
REQUEST_SCHEMA = "aios.signer-broker.request.v1"
RESPONSE_SCHEMA = "aios.signer-broker.response.v1"
AUDIT_SCHEMA = "aios.signer-broker.audit.v1"
DEFAULT_SOCKET_PATH = Path("/run/aios/signer-broker.sock")
DEFAULT_SIGNER_PATH = Path("/usr/local/libexec/aios-grant-signer-adapter")
DEFAULT_AUDIT_PATH = Path("/var/lib/aios-gitops/audit/signer-broker.jsonl")
DEFAULT_STATE_ROOT = Path("/var/lib/aios-gitops/signer-broker")
ALLOWED_OPERATIONS = frozenset({"self_test", "sign"})
ALLOWED_PAYLOAD_KINDS = frozenset({"approval_plan", "grant_payload"})
MAX_SIGNING_PAYLOAD = 1024 * 1024
MAX_REQUEST_BYTES = 2 * MAX_SIGNING_PAYLOAD + 8192
CODE_RE = re.compile(r"^[A-Z0-9_.:-]{3,96}$")
SHA256_RE = re.compile(r"^[0-9a-f]{64}$")
class SignerBrokerServiceError(RuntimeError):
def __init__(self, code: str):
self.code = code if CODE_RE.fullmatch(code) else "SIGNER_INTERNAL_ERROR"
super().__init__(self.code)
class SignerRunner(Protocol):
def self_test(self) -> None: ...
def sign(self, payload_kind: str, payload: bytes) -> tuple[bytes, str]: ...
@dataclass(frozen=True, slots=True)
class SignerBrokerPolicy:
socket_path: Path = DEFAULT_SOCKET_PATH
signer_path: Path = DEFAULT_SIGNER_PATH
audit_path: Path = DEFAULT_AUDIT_PATH
state_root: Path = DEFAULT_STATE_ROOT
allowed_uids: frozenset[int] = frozenset()
signer_timeout: float = 60.0
max_payload_bytes: int = MAX_SIGNING_PAYLOAD
def __post_init__(self) -> None:
for name in ("socket_path", "signer_path", "audit_path", "state_root"):
if not Path(getattr(self, name)).is_absolute():
raise SignerBrokerServiceError("SIGNER_POLICY_PATH_INVALID")
if not self.allowed_uids or any(not isinstance(uid, int) or uid < 0 for uid in self.allowed_uids):
raise SignerBrokerServiceError("SIGNER_POLICY_CALLER_INVALID")
if not 0 < self.signer_timeout <= 300:
raise SignerBrokerServiceError("SIGNER_POLICY_TIMEOUT_INVALID")
if not 0 < self.max_payload_bytes <= MAX_SIGNING_PAYLOAD:
raise SignerBrokerServiceError("SIGNER_POLICY_PAYLOAD_LIMIT_INVALID")
class FixedSignerRunner:
"""Use one configured signer executable and broker-owned temporary paths."""
def __init__(self, policy: SignerBrokerPolicy):
self.policy = policy
def _run(self, argv: Sequence[str]) -> None:
try:
result = subprocess.run(list(argv), capture_output=True, text=True, check=False, timeout=self.policy.signer_timeout)
except FileNotFoundError as exc:
raise SignerBrokerServiceError("SIGNER_UNAVAILABLE") from exc
except subprocess.TimeoutExpired as exc:
raise SignerBrokerServiceError("SIGNER_TIMEOUT") from exc
except OSError as exc:
raise SignerBrokerServiceError("SIGNER_UNAVAILABLE") from exc
if result.returncode != 0:
raise SignerBrokerServiceError("SIGNER_FAILED")
def self_test(self) -> None:
self._run((str(self.policy.signer_path), "self-test"))
def sign(self, payload_kind: str, payload: bytes) -> tuple[bytes, str]:
if payload_kind not in ALLOWED_PAYLOAD_KINDS:
raise SignerBrokerServiceError("SIGNER_PAYLOAD_KIND_REJECTED")
try:
self.policy.state_root.mkdir(parents=True, exist_ok=True)
os.chmod(self.policy.state_root, 0o700)
with tempfile.TemporaryDirectory(prefix="request-", dir=self.policy.state_root) as directory:
root = Path(directory)
input_path = root / "payload.canonical"
signature_path = root / "payload.sig"
key_id_path = root / "payload.key-id"
input_path.write_bytes(payload)
os.chmod(input_path, 0o600)
self._run((str(self.policy.signer_path), "sign", "--namespace", payload_kind, "--input", str(input_path), "--signature-out", str(signature_path), "--key-id-out", str(key_id_path)))
signature = signature_path.read_bytes()
key_id = key_id_path.read_text(encoding="utf-8").strip()
except SignerBrokerServiceError:
raise
except (OSError, UnicodeError) as exc:
raise SignerBrokerServiceError("SIGNER_OUTPUT_INVALID") from exc
if not signature or not key_id or len(key_id) > 256 or "\n" in key_id or "\r" in key_id:
raise SignerBrokerServiceError("SIGNER_OUTPUT_INVALID")
return signature, key_id
class SignerBrokerService:
def __init__(self, policy: SignerBrokerPolicy, runner: SignerRunner | None = None, *, now: Callable[[], datetime] | None = None) -> None:
self.policy = policy
self.runner = runner or FixedSignerRunner(policy)
self.now = now or (lambda: datetime.now(timezone.utc))
self._audit_lock = threading.Lock()
@staticmethod
def _operation(value: Any) -> str:
return value if isinstance(value, str) and re.fullmatch(r"[A-Za-z0-9_.:-]{1,64}", value) else "INVALID"
def _audit(self, *, peer_uid: int, operation: str, status: str, error_code: str | None = None, payload_sha256: str | None = None) -> None:
event: dict[str, Any] = {"schema": AUDIT_SCHEMA, "timestamp": self.now().astimezone(timezone.utc).isoformat(), "peer_uid": peer_uid, "operation": self._operation(operation), "status": status}
if isinstance(payload_sha256, str) and SHA256_RE.fullmatch(payload_sha256):
event["payload_sha256"] = payload_sha256
if error_code:
event["error_code"] = error_code if CODE_RE.fullmatch(error_code) else "SIGNER_INTERNAL_ERROR"
try:
with self._audit_lock:
self.policy.audit_path.parent.mkdir(parents=True, exist_ok=True)
with self.policy.audit_path.open("a", encoding="utf-8") as handle:
handle.write(json.dumps(event, sort_keys=True, separators=(",", ":")) + "\n")
handle.flush()
os.fsync(handle.fileno())
os.chmod(self.policy.audit_path, 0o600)
except OSError as exc:
raise SignerBrokerServiceError("SIGNER_AUDIT_UNAVAILABLE") from exc
@staticmethod
def _failure(code: str) -> dict[str, Any]:
return {"schema": RESPONSE_SCHEMA, "status": "FAIL", "error_code": code if CODE_RE.fullmatch(code) else "SIGNER_INTERNAL_ERROR"}
def _dispatch(self, request: Mapping[str, Any], peer_uid: int) -> tuple[dict[str, Any], str, str | None]:
operation = self._operation(request.get("operation"))
if peer_uid not in self.policy.allowed_uids:
raise SignerBrokerServiceError("CALLER_UNAUTHORIZED")
if request.get("schema") != REQUEST_SCHEMA:
raise SignerBrokerServiceError("REQUEST_SCHEMA_INVALID")
if operation not in ALLOWED_OPERATIONS:
raise SignerBrokerServiceError("SIGNER_OPERATION_REJECTED")
if operation == "self_test":
if set(request) != {"schema", "operation"}:
raise SignerBrokerServiceError("REQUEST_FIELDS_REJECTED")
self.runner.self_test()
return {"schema": RESPONSE_SCHEMA, "status": "PASS", "operation": operation}, operation, None
if set(request) != {"schema", "operation", "payload_kind", "payload_sha256", "payload_base64"}:
raise SignerBrokerServiceError("REQUEST_FIELDS_REJECTED")
payload_kind = request.get("payload_kind")
encoded = request.get("payload_base64")
claimed_hash = request.get("payload_sha256")
if payload_kind not in ALLOWED_PAYLOAD_KINDS or not isinstance(encoded, str) or not isinstance(claimed_hash, str) or not SHA256_RE.fullmatch(claimed_hash):
raise SignerBrokerServiceError("SIGNER_PAYLOAD_INVALID")
if len(encoded) > self.policy.max_payload_bytes * 2 + 8:
raise SignerBrokerServiceError("SIGNER_PAYLOAD_INVALID")
try:
payload = base64.b64decode(encoded.encode("ascii"), validate=True)
except (ValueError, UnicodeError, binascii.Error) as exc:
raise SignerBrokerServiceError("SIGNER_PAYLOAD_INVALID") from exc
if not payload or len(payload) > self.policy.max_payload_bytes:
raise SignerBrokerServiceError("SIGNER_PAYLOAD_INVALID")
actual_hash = hashlib.sha256(payload).hexdigest()
if actual_hash != claimed_hash:
raise SignerBrokerServiceError("SIGNER_PAYLOAD_HASH_MISMATCH")
signature, key_id = self.runner.sign(payload_kind, payload)
if not isinstance(signature, bytes) or not signature or not isinstance(key_id, str) or not key_id.strip() or len(key_id) > 256:
raise SignerBrokerServiceError("SIGNER_OUTPUT_INVALID")
return {"schema": RESPONSE_SCHEMA, "status": "PASS", "operation": operation, "signature_base64": base64.b64encode(signature).decode("ascii"), "key_id": key_id.strip()}, operation, actual_hash
def handle_request(self, request: Mapping[str, Any], peer_uid: int) -> dict[str, Any]:
operation = self._operation(request.get("operation")) if isinstance(request, Mapping) else "INVALID"
payload_hash = request.get("payload_sha256") if isinstance(request, Mapping) else None
status = "FAIL"
error_code: str | None = None
try:
if not isinstance(request, Mapping):
raise SignerBrokerServiceError("REQUEST_OBJECT_REQUIRED")
response, operation, payload_hash = self._dispatch(request, peer_uid)
status = "PASS"
except SignerBrokerServiceError as exc:
error_code = exc.code
response = self._failure(exc.code)
except Exception:
error_code = "SIGNER_INTERNAL_ERROR"
response = self._failure(error_code)
try:
self._audit(peer_uid=peer_uid, operation=operation, status=status, error_code=error_code, payload_sha256=payload_hash if isinstance(payload_hash, str) else None)
except SignerBrokerServiceError as exc:
response = self._failure(exc.code)
return response
def handle_json_line(self, raw: bytes, peer_uid: int) -> dict[str, Any]:
if len(raw) > MAX_REQUEST_BYTES:
return self.handle_request({"operation": "invalid"}, peer_uid) | {"error_code": "REQUEST_TOO_LARGE", "status": "FAIL"}
try:
request = json.loads(raw.decode("utf-8"))
except (UnicodeDecodeError, json.JSONDecodeError):
return self.handle_request({"operation": "invalid"}, peer_uid) | {"error_code": "REQUEST_JSON_INVALID", "status": "FAIL"}
return self.handle_request(request, peer_uid)
class _UnixRequestHandler(socketserver.StreamRequestHandler):
def handle(self) -> None:
service: SignerBrokerService = self.server.service # type: ignore[attr-defined]
response = service.handle_json_line(self.rfile.readline(MAX_REQUEST_BYTES + 1), _peer_uid(self.request))
self.wfile.write((json.dumps(response, sort_keys=True, separators=(",", ":")) + "\n").encode("utf-8"))
self.wfile.flush()
class SignerBrokerUnixServer(socketserver.ThreadingMixIn, socketserver.UnixStreamServer):
daemon_threads = True
allow_reuse_address = True
def __init__(self, socket_path: Path, service: SignerBrokerService):
self.socket_path = Path(socket_path)
self.service = service
super().__init__(str(self.socket_path), _UnixRequestHandler)
def server_close(self) -> None:
super().server_close()
try:
if self.socket_path.exists() and stat.S_ISSOCK(self.socket_path.stat().st_mode):
self.socket_path.unlink()
except OSError:
pass
def _peer_uid(channel: socket.socket) -> int:
if not hasattr(socket, "SO_PEERCRED"):
return -1
try:
_pid, uid, _gid = struct.unpack("3i", channel.getsockopt(socket.SOL_SOCKET, socket.SO_PEERCRED, struct.calcsize("3i")))
return int(uid)
except (OSError, struct.error):
return -1
def build_server(policy: SignerBrokerPolicy, runner: SignerRunner | None = None) -> SignerBrokerUnixServer:
socket_path = policy.socket_path
socket_path.parent.mkdir(parents=True, exist_ok=True)
if socket_path.exists():
if not stat.S_ISSOCK(socket_path.stat().st_mode):
raise SignerBrokerServiceError("SIGNER_BROKER_SOCKET_CONFLICT")
socket_path.unlink()
server = SignerBrokerUnixServer(socket_path, SignerBrokerService(policy, runner))
os.chmod(socket_path, 0o660)
return server
def _parser() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser(description="AIOS fixed-function signer broker")
parser.add_argument("command", choices=("serve",))
parser.add_argument("--socket", default=str(DEFAULT_SOCKET_PATH))
parser.add_argument("--signer", default=str(DEFAULT_SIGNER_PATH))
parser.add_argument("--audit", default=str(DEFAULT_AUDIT_PATH))
parser.add_argument("--state-root", default=str(DEFAULT_STATE_ROOT))
parser.add_argument("--allowed-uid", action="append", type=int, required=True)
parser.add_argument("--timeout", type=float, default=60.0)
return parser
def main(argv: Sequence[str] | None = None) -> int:
args = _parser().parse_args(argv)
policy = SignerBrokerPolicy(socket_path=Path(args.socket), signer_path=Path(args.signer), audit_path=Path(args.audit), state_root=Path(args.state_root), allowed_uids=frozenset(args.allowed_uid), signer_timeout=args.timeout)
server = build_server(policy)
try:
server.serve_forever()
finally:
server.server_close()
return 0
if __name__ == "__main__":
raise SystemExit(main())
+445 -39
View File
@@ -31,6 +31,8 @@ import time
from pathlib import Path
from typing import Any, Mapping, Sequence
from version_registry import configured_authoritative_git_dir, verify_authoritative_repository
OID_RE = re.compile(r"^[0-9a-f]{40}$")
SHA_RE = re.compile(r"^[0-9a-f]{64}$")
PLAN_RE = re.compile(r"^sha256:[0-9a-f]{64}$")
@@ -49,6 +51,11 @@ REQUIRED_V2R1C7_MEMBERS = {
"payload/ARTIFACT-MANIFEST.json",
"payload/ROLLBACK-METADATA.json",
}
REQUIRED_V2R2C1_MEMBERS = {
"payload/versions/V2R2C1.json",
}
V2R2C1_TAG_OID = "07fc2746b688f5f243f29b39de39f32edc3dd9d2"
DEFAULT_AUTHORITATIVE_GIT_DIR = Path("/data/aios-gitops/repo.git")
class ReleaseBlocked(RuntimeError):
pass
@@ -72,6 +79,13 @@ class Binding:
production_service: str
production_port: int
repository_identity: str = "aios/aios"
# Legacy V2R2C1 receipts intentionally omit tag_oid. Future release
# bindings must provide it explicitly and may use a versioned schema.
tag_oid: str | None = None
binding_schema: str | None = None
source_registry_main_oid: str | None = None
source_registry_path: str | None = None
source_registry_sha256: str | None = None
def __post_init__(self) -> None:
if not self.version or not OID_RE.fullmatch(self.target_oid):
@@ -90,6 +104,32 @@ class Binding:
raise ValueError("invalid production port")
if self.repository_identity != "aios/aios":
raise ValueError("unexpected repository identity")
if self.tag_oid is not None and not OID_RE.fullmatch(self.tag_oid):
raise ValueError("invalid annotated tag object OID")
if self.binding_schema is not None and not self.binding_schema.startswith(
"aios.release-binding."
):
raise ValueError("invalid release binding schema")
provenance = (
self.source_registry_main_oid,
self.source_registry_sha256,
)
if any(item is not None for item in provenance):
if not all(item is not None for item in provenance):
raise ValueError("source registry provenance must be complete")
if not OID_RE.fullmatch(str(self.source_registry_main_oid)):
raise ValueError("invalid source registry main OID")
if not SHA_RE.fullmatch(str(self.source_registry_sha256)):
raise ValueError("invalid source registry SHA-256")
path = self.source_registry_path or f"versions/{self.version}.json"
candidate = Path(path)
if (
candidate.is_absolute()
or ".." in candidate.parts
or "\\" in path
or not path.endswith(".json")
):
raise ValueError("invalid source registry path")
@property
def tag(self) -> str:
@@ -108,10 +148,22 @@ class Binding:
"production_port": self.production_port,
"repository_identity": self.repository_identity,
}
if self.tag_oid is not None:
payload["tag_oid"] = self.tag_oid
payload["binding_schema"] = self.binding_schema or "aios.release-binding.v2"
if self.source_registry_main_oid is not None:
payload.update(
{
"source_registry_main_oid": self.source_registry_main_oid,
"source_registry_path": self.source_registry_path
or f"versions/{self.version}.json",
"source_registry_sha256": self.source_registry_sha256,
}
)
return hashlib.sha256(canonical_json(payload)).hexdigest()
def request(self, operation: str) -> dict[str, Any]:
return {
request = {
"schema": "aios.production-executor-request.v1",
"operation": operation,
"binding_fingerprint": self.fingerprint,
@@ -127,6 +179,19 @@ class Binding:
"production_port": self.production_port,
"repository_identity": self.repository_identity,
}
if self.tag_oid is not None:
request["tag_oid"] = self.tag_oid
request["binding_schema"] = self.binding_schema or "aios.release-binding.v2"
if self.source_registry_main_oid is not None:
request.update(
{
"source_registry_main_oid": self.source_registry_main_oid,
"source_registry_path": self.source_registry_path
or f"versions/{self.version}.json",
"source_registry_sha256": self.source_registry_sha256,
}
)
return request
def canonical_json(value: object) -> bytes:
@@ -168,6 +233,23 @@ def run_git(git_dir: Path, *args: str, check: bool = True) -> subprocess.Complet
)
def run_git_bytes(
git_dir: Path,
*args: str,
check: bool = True,
) -> subprocess.CompletedProcess[bytes]:
completed = subprocess.run(
["git", f"--git-dir={git_dir}", *args],
check=False,
capture_output=True,
timeout=30,
env={"PATH": os.environ.get("PATH", "")},
)
if check and completed.returncode != 0:
raise ReleaseBlocked(completed.stderr.decode("utf-8", "replace").strip())
return completed
def verify_git_lineage(git_dir: Path, binding: Binding) -> dict[str, Any]:
authority_main = run_git(git_dir, "rev-parse", "refs/heads/main").stdout.strip()
if not OID_RE.fullmatch(authority_main):
@@ -209,7 +291,45 @@ def verify_git_lineage(git_dir: Path, binding: Binding) -> dict[str, Any]:
if not target_in_evidence:
raise ReleaseBlocked("approved target is not an ancestor of evidence closure")
return {
source_main = None
if binding.source_registry_main_oid is not None:
source_main = binding.source_registry_main_oid
source_type = run_git(git_dir, "cat-file", "-t", source_main).stdout.strip()
if source_type != "commit":
raise ReleaseBlocked("source_registry_main_oid is not a commit")
target_in_source_main = run_git(
git_dir,
"merge-base",
"--is-ancestor",
binding.target_oid,
source_main,
check=False,
).returncode == 0
if not target_in_source_main:
raise ManualOwnerReviewRequired(
"approved target is not an ancestor of source registry main"
)
expected_tag_oid = binding.tag_oid
if expected_tag_oid is None and binding.version == "V2R2C1":
expected_tag_oid = V2R2C1_TAG_OID
if expected_tag_oid is not None:
local_tag = run_git(
git_dir,
"rev-parse",
"--verify",
f"refs/tags/{binding.version}",
).stdout.strip()
if local_tag != expected_tag_oid:
raise ManualOwnerReviewRequired(
"future release binding tag object differs from local tag"
)
if run_git(git_dir, "cat-file", "-t", local_tag).stdout.strip() != "tag":
raise ReleaseBlocked("release tag is not annotated")
peeled = run_git(git_dir, "rev-parse", "--verify", f"{local_tag}^{{commit}}").stdout.strip()
if peeled != binding.target_oid:
raise ManualOwnerReviewRequired("release tag peeled target mismatch")
result = {
"authority_main_oid": authority_main,
"target_oid": binding.target_oid,
"target_is_ancestor_of_main": True,
@@ -217,9 +337,231 @@ def verify_git_lineage(git_dir: Path, binding: Binding) -> dict[str, Any]:
"evidence_object_type": evidence_type,
"target_is_ancestor_of_evidence": True,
}
if source_main is not None:
result["source_registry_main_oid"] = source_main
result["target_is_ancestor_of_source_registry_main"] = True
return result
def verify_artifact(binding: Binding) -> dict[str, Any]:
def _source_registry_git_provenance(
git_dir: Path,
binding: Binding,
) -> dict[str, Any] | None:
"""Read M:path and C:path independently of the artifact metadata."""
if binding.source_registry_main_oid is None:
return None
verify_authoritative_repository(git_dir)
source_main = binding.source_registry_main_oid
source_path = binding.source_registry_path or f"versions/{binding.version}.json"
authority_main = run_git(git_dir, "rev-parse", "--verify", "refs/heads/main").stdout.strip()
if run_git(git_dir, "cat-file", "-t", source_main).stdout.strip() != "commit":
raise ReleaseBlocked("source_registry_main_oid is not a commit")
if run_git(git_dir, "cat-file", "-t", authority_main).stdout.strip() != "commit":
raise ReleaseBlocked("Authority main OID is not a commit")
if run_git(
git_dir,
"merge-base",
"--is-ancestor",
source_main,
authority_main,
check=False,
).returncode != 0:
raise ManualOwnerReviewRequired(
"source registry main is not an ancestor of current Authority main"
)
source_blob = run_git_bytes(git_dir, "show", f"{source_main}:{source_path}").stdout
current_blob = run_git_bytes(git_dir, "show", f"{authority_main}:{source_path}").stdout
if source_blob != current_blob:
raise ManualOwnerReviewRequired(
"current Authority main source registry entry differs from pinned M"
)
try:
source = json.loads(source_blob.decode("utf-8"))
except (UnicodeDecodeError, json.JSONDecodeError) as exc:
raise ReleaseBlocked("Git source registry entry is invalid") from exc
expected_tag_oid = binding.tag_oid
if expected_tag_oid is None and binding.version == "V2R2C1":
expected_tag_oid = V2R2C1_TAG_OID
expected = {
"version": binding.version,
"repository": "aios/aios",
"tag_oid": expected_tag_oid,
"target_oid": binding.target_oid,
"peeled_target_oid": binding.target_oid,
"evidence_closure_oid": binding.evidence_closure_oid,
"approval_plan_id": binding.approval_plan_id,
}
if not isinstance(source, dict) or source.get("schema") not in {
"aios.source-registry.v1",
"aios.production-registry.v2",
}:
raise ReleaseBlocked("Git source registry schema is invalid")
if expected_tag_oid is None or any(source.get(key) != value for key, value in expected.items()):
raise ReleaseBlocked("Git source registry identity mismatch")
if source.get("tag_object") != expected_tag_oid:
raise ReleaseBlocked("Git source registry tag object mismatch")
digest = hashlib.sha256(source_blob).hexdigest()
if digest != binding.source_registry_sha256:
raise ReleaseBlocked("Git source registry SHA-256 mismatch")
return {
"source_registry_main_oid": source_main,
"current_main_oid": authority_main,
"source_registry_path": source_path,
"source_registry_bytes": source_blob,
"source_registry_sha256": digest,
"source_registry_identity_verified": True,
"source_registry_main_converged": True,
}
def _verify_artifact_members(
path: Path,
required: set[str],
binding: Binding | None = None,
git_provenance: Mapping[str, Any] | None = None,
) -> dict[str, Any]:
"""Verify archive safety and independently verify packaged release data."""
# Python tarfile may not have zstd support on the target host, so
# decompress into a private temporary tar and inspect it there.
with tempfile.TemporaryDirectory(prefix="aios-artifact-verify-") as temp_dir:
tar_path = Path(temp_dir) / "release.tar"
with tar_path.open("wb") as output:
completed = subprocess.run(
["zstd", "-dc", str(path)],
check=False,
stdout=output,
stderr=subprocess.PIPE,
timeout=60,
env={"PATH": os.environ.get("PATH", "")},
)
if completed.returncode != 0:
raise ReleaseBlocked("artifact zstd decompression failed")
import tarfile
with tarfile.open(tar_path, "r:") as archive:
members = archive.getmembers()
names: set[str] = set()
for member in members:
candidate = Path(member.name)
if (
candidate.is_absolute()
or ".." in candidate.parts
or member.issym()
or member.islnk()
or not (member.isfile() or member.isdir())
):
raise ReleaseBlocked(f"unsafe artifact member: {member.name}")
if member.name in names:
raise ReleaseBlocked(f"duplicate artifact member: {member.name}")
names.add(member.name)
source_path = None
if binding is not None and binding.version != "V2R1C7":
source_path = binding.source_registry_path or f"versions/{binding.version}.json"
required_members = set(required)
if binding is not None and binding.source_registry_main_oid is not None:
required_members.add("payload/BUILD-METADATA.json")
if source_path is not None and binding is not None:
required_members.add(f"payload/{source_path}")
missing = sorted(required_members - names)
if missing:
raise ReleaseBlocked(
"artifact missing required members: " + ",".join(missing)
)
verification: dict[str, Any] = {}
if binding is not None and source_path is not None:
member_name = f"payload/{source_path}"
member = archive.getmember(member_name)
stream = archive.extractfile(member)
if stream is None:
raise ReleaseBlocked("source registry member is unreadable")
raw = stream.read()
try:
source = json.loads(raw.decode("utf-8"))
except (UnicodeDecodeError, json.JSONDecodeError) as exc:
raise ReleaseBlocked("source registry member is invalid") from exc
canonical_source = (
json.dumps(source, ensure_ascii=False, sort_keys=True, indent=2) + "\n"
).encode("utf-8")
if raw != canonical_source:
raise ReleaseBlocked("source registry member is not canonical JSON")
expected_tag_oid = binding.tag_oid
if expected_tag_oid is None and binding.version == "V2R2C1":
expected_tag_oid = V2R2C1_TAG_OID
expected = {
"version": binding.version,
"repository": "aios/aios",
"tag_oid": expected_tag_oid,
"target_oid": binding.target_oid,
"peeled_target_oid": binding.target_oid,
"evidence_closure_oid": binding.evidence_closure_oid,
"approval_plan_id": binding.approval_plan_id,
}
if expected_tag_oid is None:
raise ReleaseBlocked("future release binding tag object is required")
if not isinstance(source, dict) or any(
source.get(key) != value for key, value in expected.items()
):
raise ReleaseBlocked("packaged source registry identity mismatch")
if source.get("tag_object") != expected_tag_oid:
raise ReleaseBlocked("packaged source registry tag object mismatch")
source_digest = hashlib.sha256(raw).hexdigest()
if git_provenance is not None:
if raw != git_provenance["source_registry_bytes"]:
raise ReleaseBlocked(
"packaged source registry differs from Git M:path"
)
if source_digest != git_provenance["source_registry_sha256"]:
raise ReleaseBlocked(
"packaged source registry SHA-256 differs from Git M:path"
)
verification.update(
{
"source_registry_path": source_path,
"source_registry_sha256": source_digest,
"source_registry_identity_verified": True,
}
)
if binding.source_registry_sha256 is not None:
if source_digest != binding.source_registry_sha256:
raise ReleaseBlocked("packaged source registry SHA-256 mismatch")
metadata_member = archive.getmember("payload/BUILD-METADATA.json")
metadata_stream = archive.extractfile(metadata_member)
if metadata_stream is None:
raise ReleaseBlocked("BUILD-METADATA.json is unreadable")
try:
metadata = json.loads(metadata_stream.read().decode("utf-8"))
except (UnicodeDecodeError, json.JSONDecodeError) as exc:
raise ReleaseBlocked("BUILD-METADATA.json is invalid") from exc
metadata_expected = {
"release_target_oid": binding.target_oid,
"source_registry_main_oid": binding.source_registry_main_oid,
"source_registry_path": source_path,
"source_registry_sha256": binding.source_registry_sha256,
}
if not isinstance(metadata, dict) or any(
metadata.get(key) != value
for key, value in metadata_expected.items()
):
raise ReleaseBlocked("BUILD-METADATA source provenance mismatch")
verification["build_metadata_provenance_verified"] = True
if git_provenance is not None:
verification.update(
{
"git_source_registry_member_verified": True,
"git_source_registry_main_converged": True,
"git_source_registry_sha256": git_provenance[
"source_registry_sha256"
],
}
)
return verification
def verify_artifact(
binding: Binding,
*,
repository: Path | str | None = None,
) -> dict[str, Any]:
path = Path(binding.artifact_path)
if path.stat().st_size != binding.artifact_size:
raise ReleaseBlocked("artifact size mismatch")
@@ -227,46 +569,32 @@ def verify_artifact(binding: Binding) -> dict[str, Any]:
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)
)
required = {
"V2R1C7": REQUIRED_V2R1C7_MEMBERS,
"V2R2C1": REQUIRED_V2R2C1_MEMBERS,
}.get(binding.version)
member_verification: dict[str, Any] = {}
git_provenance = None
if binding.source_registry_main_oid is not None:
git_provenance = _source_registry_git_provenance(
Path(repository) if repository is not None else Path(
configured_authoritative_git_dir()
),
binding,
)
if required or binding.source_registry_main_oid is not None:
member_verification = _verify_artifact_members(
path,
required or set(),
binding,
git_provenance,
)
return {
"artifact_path": str(path),
"artifact_sha256": actual,
"artifact_size": binding.artifact_size,
"verified": True,
**member_verification,
}
@@ -282,6 +610,18 @@ def verify_authorization(receipt: Mapping[str, Any], binding: Binding) -> dict[s
"artifact_sha256": binding.artifact_sha256,
"approval_plan_id": binding.approval_plan_id,
}
if binding.tag_oid is not None:
expected["tag_oid"] = binding.tag_oid
expected["binding_schema"] = binding.binding_schema or "aios.release-binding.v2"
if binding.source_registry_main_oid is not None:
expected.update(
{
"source_registry_main_oid": binding.source_registry_main_oid,
"source_registry_path": binding.source_registry_path
or f"versions/{binding.version}.json",
"source_registry_sha256": binding.source_registry_sha256,
}
)
for key, value in expected.items():
if receipt.get(key) != value:
raise ManualOwnerReviewRequired(f"Owner authorization mismatch: {key}")
@@ -433,13 +773,29 @@ def tag_step(command: Sequence[str], binding: Binding) -> dict[str, Any]:
tag_oid = probe.get("tag_oid")
if not isinstance(tag_oid, str) or not OID_RE.fullmatch(tag_oid):
raise ReleaseBlocked("verified annotated tag object OID missing")
if binding.tag_oid is not None and tag_oid != binding.tag_oid:
raise ManualOwnerReviewRequired(
"same-name tag object differs from the future release binding"
)
return dict(probe)
def deploy_step(command: Sequence[str], binding: Binding) -> dict[str, Any]:
def require_runtime_registry(result: Mapping[str, Any]) -> None:
if binding.source_registry_main_oid is None:
return
if (
result.get("runtime_bundle_contains_current_version") is not True
or result.get("runtime_registry_converged") is not True
):
raise ReconciliationRequired(
"runtime bundle/runtime registry convergence was not independently verified"
)
probe = run_adapter(command, binding.request("probe"))
require_exact_response(probe, binding)
if probe.get("status") == "ACTIVE_VERIFIED":
require_runtime_registry(probe)
return dict(probe)
if probe.get("status") == "CONFLICT":
raise ManualOwnerReviewRequired("production runtime has conflicting binding")
@@ -458,9 +814,30 @@ def deploy_step(command: Sequence[str], binding: Binding) -> dict[str, Any]:
or probe.get("production_deployed") is not True
):
raise ReconciliationRequired("production health contract is not ACTIVE")
require_runtime_registry(probe)
return dict(probe)
def verify_post_deploy_source_registry(
repository: Path,
binding: Binding,
) -> dict[str, Any]:
"""Re-read authoritative Git after runtime deployment and before closure."""
if binding.source_registry_main_oid is None:
return {
"source_registry_converged": "not_required",
"source_registry_check": "not_required_for_legacy_binding",
}
provenance = _source_registry_git_provenance(repository, binding)
if provenance is None:
raise ReconciliationRequired("authoritative source registry was not checked")
return {
**provenance,
"source_registry_converged": True,
"source_registry_check": "post_deploy_authoritative_git_read",
}
def finalize_step(
command: Sequence[str],
binding: Binding,
@@ -501,7 +878,7 @@ def execute(
) -> dict[str, Any]:
authorization = verify_authorization(secure_json(authorization_path), binding)
lineage = verify_git_lineage(repository, binding)
artifact = verify_artifact(binding)
artifact = verify_artifact(binding, repository=repository)
state = DurableState(state_root, binding)
with FileLock(state_root / "executor.lock"):
state.record(
@@ -518,6 +895,13 @@ def execute(
runtime = deploy_step(deploy_command, binding)
state.record("RUNTIME_VERIFIED", "runtime", runtime)
# The runtime adapter proves only runtime facts. Re-read M:path
# and C:path from authoritative Git immediately before Registry
# and Baseline closure so a post-preflight main change cannot be
# mistaken for source convergence.
source_registry = verify_post_deploy_source_registry(repository, binding)
state.record("SOURCE_REGISTRY_VERIFIED", "source_registry", source_registry)
registry = finalize_step(
registry_command,
binding,
@@ -547,10 +931,21 @@ def execute(
require_exact_response(value, binding)
if final_runtime.get("status") != "ACTIVE_VERIFIED":
raise ReconciliationRequired("final runtime readback failed")
if binding.source_registry_main_oid is not None and not all(
final_runtime.get(name) is True
for name in (
"runtime_bundle_contains_current_version",
"runtime_registry_converged",
)
):
raise ReconciliationRequired(
"final runtime registry convergence readback failed"
)
if final_registry.get("status") != "VERIFIED":
raise ReconciliationRequired("final Registry readback failed")
if final_baseline.get("status") != "VERIFIED":
raise ReconciliationRequired("final Baseline readback failed")
final_source_registry = verify_post_deploy_source_registry(repository, binding)
final = {
"status": "PRODUCTION_ACCEPTED",
"version": binding.version,
@@ -561,6 +956,7 @@ def execute(
"tag_oid": tag["tag_oid"],
"tag": final_tag,
"runtime": final_runtime,
"source_registry": final_source_registry,
"registry": final_registry,
"baseline": final_baseline,
"force_push_used": False,
@@ -600,6 +996,11 @@ def parser() -> argparse.ArgumentParser:
p.add_argument("--evidence-closure-oid", required=True)
p.add_argument("--artifact-sha256", required=True)
p.add_argument("--approval-plan-id", required=True)
p.add_argument("--tag-oid")
p.add_argument("--binding-schema")
p.add_argument("--source-registry-main-oid")
p.add_argument("--source-registry-path")
p.add_argument("--source-registry-sha256")
p.add_argument("--production-service", required=True)
p.add_argument("--production-port", required=True, type=int)
p.add_argument("--repository-identity", default="aios/aios")
@@ -623,6 +1024,11 @@ def main() -> int:
production_service=args.production_service,
production_port=args.production_port,
repository_identity=args.repository_identity,
tag_oid=args.tag_oid,
binding_schema=args.binding_schema,
source_registry_main_oid=args.source_registry_main_oid,
source_registry_path=args.source_registry_path,
source_registry_sha256=args.source_registry_sha256,
)
try:
result = execute(
@@ -964,7 +964,6 @@ def validate_writer_lease_request_v2(raw: Mapping[str, Any], probe: Any) -> dict
required = {
"schema",
"task_id",
"job_id",
"repository",
"branch_mode",
"base_oid",
@@ -977,7 +976,6 @@ def validate_writer_lease_request_v2(raw: Mapping[str, Any], probe: Any) -> dict
raise ProvisioningError("LEASE_REQUEST_V2_FIELDS_REJECTED", 409)
request = WriterLeaseRequestV2(
task_id=str(raw["task_id"]),
job_id=str(raw["job_id"]),
repository=str(raw["repository"]),
branch_mode=str(raw["branch_mode"]),
base_oid=str(raw["base_oid"]),
@@ -990,7 +988,6 @@ def validate_writer_lease_request_v2(raw: Mapping[str, Any], probe: Any) -> dict
return {
"schema": WriterLeaseRequestV2.schema,
"task_id": request.task_id,
"job_id": request.job_id,
"repository": request.repository,
"branch_mode": request.branch_mode,
"base_oid": request.base_oid,
+115 -14
View File
@@ -64,6 +64,40 @@ class PublicationError(RuntimeError):
pass
class PublicationValueError(PublicationError, ValueError):
def __init__(self, code: str, message: str):
self.code = code
super().__init__(message)
class InvalidReleaseVersion(PublicationValueError):
def __init__(self):
super().__init__("INVALID_RELEASE_VERSION", "invalid release version")
class GiteaConfigurationError(PublicationValueError):
def __init__(self, code: str):
super().__init__(code, code)
class GiteaUnavailableError(PublicationError):
def __init__(self, code: str = "GITEA_UNAVAILABLE"):
self.code = code
super().__init__(code)
class GiteaTagNotFound(PublicationError):
def __init__(self):
self.code = "TAG_NOT_FOUND"
super().__init__(self.code)
class TagTargetMismatch(PublicationError):
def __init__(self):
self.code = "TAG_TARGET_MISMATCH"
super().__init__(self.code)
class PublicationConflict(PublicationError):
pass
@@ -85,7 +119,7 @@ class PublicationManualOwnerReviewRequired(
def validate_version(version: str) -> str:
value = str(version or "")
if not VERSION_RE.fullmatch(value):
raise ValueError("invalid release version")
raise InvalidReleaseVersion()
return value
@@ -402,6 +436,40 @@ class GitTagBackend:
)
@dataclass(frozen=True)
class GiteaClientConfig:
api_url: str
repository: str
token_file: Path
remote: str = "gitea"
token_line: int = 1
@classmethod
def from_authoritative_environment(cls, environ: dict[str, str] | None = None) -> "GiteaClientConfig":
values = os.environ if environ is None else environ
api_url = str(values.get("AIOS_GITOPS_GITEA_API_URL", "")).strip()
repository = str(values.get("AIOS_GITOPS_GITEA_REPOSITORY", "")).strip()
token_ref = str(values.get("AIOS_GITOPS_GITEA_TOKEN_FILE", "")).strip()
if not api_url:
raise GiteaConfigurationError("GITEA_API_URL_MISSING")
if not api_url.startswith(("https://", "http://")):
raise GiteaConfigurationError("GITEA_API_URL_INVALID")
if not repository:
raise GiteaConfigurationError("GITEA_REPOSITORY_MISSING")
if repository.count("/") != 1 or repository.startswith("/"):
raise GiteaConfigurationError("GITEA_REPOSITORY_INVALID")
if not token_ref:
raise GiteaConfigurationError("GITEA_TOKEN_REFERENCE_MISSING")
token_file = Path(token_ref)
if not token_file.is_absolute():
raise GiteaConfigurationError("GITEA_TOKEN_REFERENCE_INVALID")
remote = str(values.get("AIOS_GITOPS_GITEA_REMOTE", "gitea")).strip() or "gitea"
token_line = int(str(values.get("AIOS_GITOPS_GITEA_TOKEN_LINE", "1")))
if token_line < 1:
raise GiteaConfigurationError("GITEA_TOKEN_REFERENCE_INVALID")
return cls(api_url, repository, token_file, remote, token_line)
class GiteaTagClient:
def __init__(
self,
@@ -411,23 +479,37 @@ class GiteaTagClient:
timeout: float = 15,
token_line: int = 1,
):
self.api_url = api_url.rstrip("/")
self.repository = repository.strip("/")
self.token_file = token_file
self.api_url = str(api_url or "").rstrip("/")
self.repository = str(repository or "").strip("/")
self.token_file = Path(token_file)
self.timeout = timeout
self.token_line = token_line
if not self.api_url:
raise GiteaConfigurationError("GITEA_API_URL_MISSING")
if not self.api_url.startswith(("https://", "http://")):
raise ValueError("Gitea API URL must be HTTP(S)")
raise GiteaConfigurationError("GITEA_API_URL_INVALID")
if not self.repository:
raise GiteaConfigurationError("GITEA_REPOSITORY_MISSING")
if self.repository.count("/") != 1:
raise ValueError("Gitea repository must be owner/name")
raise GiteaConfigurationError("GITEA_REPOSITORY_INVALID")
if not str(token_file) or not self.token_file.is_absolute():
raise GiteaConfigurationError("GITEA_TOKEN_REFERENCE_MISSING")
@classmethod
def from_authoritative_environment(cls, environ: dict[str, str] | None = None) -> "GiteaTagClient":
config = GiteaClientConfig.from_authoritative_environment(environ)
return cls(config.api_url, config.repository, config.token_file, token_line=config.token_line)
def _get(self, path: str) -> Any:
lines = self.token_file.read_text(encoding="utf-8").splitlines()
try:
lines = self.token_file.read_text(encoding="utf-8").splitlines()
except (OSError, UnicodeError) as exc:
raise GiteaConfigurationError("GITEA_TOKEN_REFERENCE_UNAVAILABLE") from exc
if self.token_line < 1 or self.token_line > len(lines):
raise RuntimeError("Gitea credential reference has no configured token line")
raise GiteaConfigurationError("GITEA_TOKEN_REFERENCE_INVALID")
token = lines[self.token_line - 1].strip()
if not token:
raise RuntimeError("Gitea token file is empty")
raise GiteaConfigurationError("GITEA_TOKEN_REFERENCE_INVALID")
request = urllib.request.Request(
f"{self.api_url}{path}",
headers={
@@ -439,10 +521,12 @@ class GiteaTagClient:
try:
with urllib.request.urlopen(request, timeout=self.timeout) as response:
return json.loads(response.read().decode("utf-8"))
except (urllib.error.URLError, urllib.error.HTTPError, TimeoutError) as exc:
status = getattr(exc, "code", None)
detail = f" HTTP {status}" if status else ""
raise RuntimeError(f"Gitea API unavailable{detail}") from exc
except urllib.error.HTTPError as exc:
if exc.code == 404:
raise GiteaTagNotFound() from exc
raise GiteaUnavailableError() from exc
except (urllib.error.URLError, TimeoutError, OSError) as exc:
raise GiteaUnavailableError() from exc
def repository_identity(self) -> str:
repository = self._get(f"/repos/{self.repository}")
@@ -950,6 +1034,12 @@ class TagPublisher:
listing = state.get("gitea_listing_visible") is True
expected_tag_oid = state.get("tag_oid")
registry_tag_oid = registry.get("tag_oid") or registry.get("tag_object")
target_mismatch = any(
isinstance(value, tuple)
and len(value) > 1
and value[1] not in (None, request.target_oid)
for value in (remote, api)
)
checks = {
"fingerprint": all(
baseline.get(field) == getattr(request, field)
@@ -974,7 +1064,7 @@ class TagPublisher:
and registry.get("enabled") is True,
}
status = "PASS" if all(checks.values()) else "FAIL"
return {
result = {
"version": request.version,
"status": status,
"checks": checks,
@@ -986,8 +1076,19 @@ class TagPublisher:
"gitea_listing_visible": listing,
"repository_identity": repository_identity,
}
if target_mismatch:
result["fail_closed"] = True
result["error"] = TagTargetMismatch().code
return result
except PublicationManualOwnerReviewRequired:
raise
except PublicationError as exc:
return {
"version": request.version,
"status": "UNKNOWN",
"fail_closed": True,
"error": str(getattr(exc, "code", exc)),
}
except Exception as exc:
return {
"version": request.version,
+157 -15
View File
@@ -29,9 +29,18 @@ from gitea_tag_publication import (
PublicationRequest,
TagPublisher,
)
from version_registry import (
RegistryConvergenceError,
assert_source_registry_convergence,
resolve_source_registry_main_oid,
source_registry_snapshot,
verify_authoritative_repository,
verify_source_registry_git_identity,
)
VERSION = "V2R2C1"
TAG_OID = "07fc2746b688f5f243f29b39de39f32edc3dd9d2"
TARGET_OID = "998d30304787e31e5ae607c0bd86b5edb2b0e5ad"
EVIDENCE_CLOSURE_OID = "a8b906d1bc24088c19647e98de6a679268f6d0a2"
APPROVAL_PLAN_ID = "sha256:c0df52008ae7f64cec5ee2d28947aa7b892066c670ab8295e4549140b1a7500e"
@@ -79,6 +88,7 @@ def atomic_json(path: Path, value: Mapping[str, Any], mode: int = 0o600) -> None
@dataclass(frozen=True)
class ReleaseBinding:
version: str = VERSION
tag_oid: str = TAG_OID
target_oid: str = TARGET_OID
evidence_closure_oid: str = EVIDENCE_CLOSURE_OID
approval_plan_id: str = APPROVAL_PLAN_ID
@@ -89,6 +99,8 @@ class ReleaseBinding:
def validate(self) -> None:
if self.version != VERSION or self.repository != REPOSITORY:
raise ManualOwnerReviewRequired("release binding is outside V2R2C1 scope")
if not OID_RE.fullmatch(self.tag_oid) or self.tag_oid != TAG_OID:
raise ManualOwnerReviewRequired("V2R2C1 tag_oid mismatch")
if not OID_RE.fullmatch(self.target_oid) or self.target_oid != TARGET_OID:
raise ManualOwnerReviewRequired("V2R2C1 target_oid mismatch")
if not OID_RE.fullmatch(self.evidence_closure_oid) or self.evidence_closure_oid != EVIDENCE_CLOSURE_OID:
@@ -192,19 +204,48 @@ def _ancestor(git_dir: Path, ancestor: str, descendant: str) -> bool:
).returncode == 0
def verify_lineage(git_dir: Path, binding: ReleaseBinding) -> dict[str, Any]:
def verify_lineage(
git_dir: Path,
binding: ReleaseBinding,
*,
authority_main_oid: str | None = None,
) -> dict[str, Any]:
binding.validate()
main_oid = authority_main_oid or _git(git_dir, "rev-parse", "refs/heads/main")
if not OID_RE.fullmatch(main_oid) or _git(git_dir, "cat-file", "-t", main_oid) != "commit":
raise ReleaseBlocked("authoritative main OID is not a commit")
if _git(git_dir, "cat-file", "-t", binding.target_oid) != "commit":
raise ReleaseBlocked("approved target_oid is not a commit")
if _git(git_dir, "cat-file", "-t", binding.evidence_closure_oid) != "commit":
raise ReleaseBlocked("evidence closure is not a commit")
if not _ancestor(git_dir, binding.target_oid, binding.evidence_closure_oid):
raise ManualOwnerReviewRequired("approved target is not an ancestor of evidence closure")
if not _ancestor(git_dir, binding.target_oid, main_oid):
raise ManualOwnerReviewRequired("approved target is not an ancestor of source registry main")
return {
"target_oid": binding.target_oid,
"evidence_closure_oid": binding.evidence_closure_oid,
"target_is_ancestor_of_evidence": True,
"main_oid": _git(git_dir, "rev-parse", "refs/heads/main"),
"main_oid": main_oid,
"target_is_ancestor_of_main": True,
}
def verify_local_annotated_tag(git_dir: Path, binding: ReleaseBinding) -> dict[str, Any]:
"""Verify the local authoritative tag object without creating or rewriting it."""
tag_oid = _git(git_dir, "rev-parse", "--verify", f"refs/tags/{binding.version}")
if tag_oid != binding.tag_oid:
raise ManualOwnerReviewRequired("local annotated tag object mismatch")
if _git(git_dir, "cat-file", "-t", tag_oid) != "tag":
raise ReleaseBlocked("release tag is not annotated")
peeled = _git(git_dir, "rev-parse", "--verify", f"{tag_oid}^{{commit}}")
if peeled != binding.target_oid:
raise ManualOwnerReviewRequired("local annotated tag target mismatch")
return {
"tag_oid": tag_oid,
"tag_object_type": "tag",
"peeled_target_oid": peeled,
"target_oid": binding.target_oid,
}
@@ -248,9 +289,41 @@ def build_production_artifact(
binding: ReleaseBinding,
output_root: Path,
) -> dict[str, Any]:
"""Build a deterministic executable V2R2C1 health artifact from target_oid."""
"""Build a deterministic artifact from composite immutable provenance.
Release payload files are read from target ``T``. The source registry is
read once from the pinned main/convergence commit ``M`` and its exact blob
bytes are reused for packaging and all recorded digests.
"""
binding.validate()
verify_lineage(git_dir, binding)
verify_authoritative_repository(git_dir)
source_registry_main_oid = resolve_source_registry_main_oid(
git_dir,
main_ref="refs/heads/main",
)
verify_lineage(git_dir, binding, authority_main_oid=source_registry_main_oid)
tag_lineage = verify_local_annotated_tag(git_dir, binding)
try:
source_snapshot = source_registry_snapshot(
git_dir,
VERSION,
source_registry_main_oid=source_registry_main_oid,
expected_tag_oid=binding.tag_oid,
expected_target_oid=binding.target_oid,
expected_evidence_closure_oid=binding.evidence_closure_oid,
expected_approval_plan_id=binding.approval_plan_id,
)
source_registry = source_snapshot["record"]
source_registry_bytes = source_snapshot["bytes"]
verify_source_registry_git_identity(git_dir, source_registry)
except RegistryConvergenceError as exc:
raise ReleaseBlocked(f"SOURCE_REGISTRY_NOT_CONVERGED:{exc}") from exc
if source_snapshot["source_registry_main_oid"] != source_registry_main_oid:
raise ReconciliationRequired("source registry main OID changed during build")
if not _ancestor(git_dir, binding.target_oid, source_registry_main_oid):
raise ManualOwnerReviewRequired("release target is not an ancestor of source registry main")
source_registry_path = source_snapshot["source_registry_path"]
source_registry_sha256 = source_snapshot["source_registry_sha256"]
epoch = _commit_epoch(git_dir, binding.target_oid)
output_root.mkdir(parents=True, exist_ok=True)
stem = f"aios-{VERSION}-{binding.target_oid[:12]}"
@@ -261,11 +334,15 @@ def build_production_artifact(
source_paths = {
"active-runtime/app/deployment_state.py": "active-runtime/app/deployment_state.py",
"active-runtime/app/v2r1c6_privacy_tools.py": "active-runtime/app/v2r1c6_privacy_tools.py",
"versions/V2R2C1.json": "versions/V2R2C1.json",
}
for destination, source in source_paths.items():
target = root / destination
target.parent.mkdir(parents=True, exist_ok=True)
target.write_bytes(_payload_file(git_dir, binding.target_oid, source))
if destination == source_registry_path:
target.write_bytes(source_registry_bytes)
else:
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": (
@@ -307,16 +384,6 @@ def build_production_artifact(
"with urllib.request.urlopen(url, timeout=5) as r: print(json.dumps(json.load(r), sort_keys=True))\n"
),
"scripts/rollback_v2r2c1_release.sh": "#!/bin/sh\nset -eu\nprintf '%s\\n' 'rollback requires governed owner-approved executor'\nexit 2\n",
"release-manifests/V2R2C1.json": json.dumps({
"schema": "aios.release-source.v2",
"version": VERSION,
"repository": REPOSITORY,
"target_oid": binding.target_oid,
"evidence_closure_oid": binding.evidence_closure_oid,
"production_deployed": False,
"deployment_state": "STAGING",
"artifact_kind": "executable-production-health",
}, sort_keys=True, separators=(",", ":")) + "\n",
}
for rel, text in generated.items():
target = root / rel
@@ -328,6 +395,10 @@ def build_production_artifact(
"schema": "aios.release-artifact-build.v3",
"version": VERSION,
"repository": REPOSITORY,
"release_target_oid": binding.target_oid,
"source_registry_main_oid": source_registry_main_oid,
"source_registry_path": source_registry_path,
"source_registry_sha256": source_registry_sha256,
"source_head_oid": binding.target_oid,
"evidence_closure_oid": binding.evidence_closure_oid,
"source_date_epoch": epoch,
@@ -337,6 +408,31 @@ def build_production_artifact(
"tag_created": False,
"baseline_changed": False,
"secrets_included": False,
"source_registry": {
"path": source_registry_path,
"version": source_registry["version"],
"tag_oid": source_registry["tag_oid"],
"target_oid": source_registry["target_oid"],
"peeled_target_oid": source_registry["peeled_target_oid"],
"evidence_closure_oid": source_registry["evidence_closure_oid"],
"approval_plan_id": source_registry["approval_plan_id"],
"source_registry_main_oid": source_registry_main_oid,
"source_registry_sha256": source_registry_sha256,
},
"provenance": {
"release_payload_source": {
"commit_oid": binding.target_oid,
"role": "release_target",
},
"source_registry_source": {
"commit_oid": source_registry_main_oid,
"path": source_registry_path,
"sha256": source_registry_sha256,
"role": "source_registry_convergence",
},
"release_target_is_ancestor_of_source_registry_main": True,
"local_annotated_tag": tag_lineage,
},
}
(root / "BUILD-METADATA.json").write_bytes(canonical(metadata))
(root / "ROLLBACK-METADATA.json").write_bytes(canonical(json.loads((root / "deploy/aios-v2/v2r2c1-rollback.json").read_text(encoding="utf-8"))))
@@ -344,6 +440,10 @@ def build_production_artifact(
(root / "ARTIFACT-MANIFEST.json").write_bytes(canonical({
"schema": "aios.release-artifact-manifest.v3",
"version": VERSION,
"release_target_oid": binding.target_oid,
"source_registry_main_oid": source_registry_main_oid,
"source_registry_path": source_registry_path,
"source_registry_sha256": source_registry_sha256,
"source_head_oid": binding.target_oid,
"evidence_closure_oid": binding.evidence_closure_oid,
"repository": REPOSITORY,
@@ -375,6 +475,10 @@ def build_production_artifact(
"schema": "aios.release-artifact-receipt.v3",
"version": VERSION,
"repository": REPOSITORY,
"release_target_oid": binding.target_oid,
"source_registry_main_oid": source_registry_main_oid,
"source_registry_path": source_registry_path,
"source_registry_sha256": source_registry_sha256,
"source_head_oid": binding.target_oid,
"evidence_closure_oid": binding.evidence_closure_oid,
"artifact": str(output),
@@ -385,6 +489,14 @@ def build_production_artifact(
"production_deployed": False,
"tag_created": False,
"baseline_changed": False,
"source_registry_path": source_registry_path,
"source_registry_identity": source_registry,
"provenance": {
"release_payload_source": binding.target_oid,
"source_registry_source": source_registry_main_oid,
"release_target_is_ancestor_of_source_registry_main": True,
"local_annotated_tag": tag_lineage,
},
}
atomic_json(output.with_suffix(output.suffix + ".receipt.json"), receipt, mode=0o640)
return receipt
@@ -458,6 +570,12 @@ class GovernedProductionReleaseExecutor:
self.state.fail("RECONCILIATION_REQUIRED", str(exc))
raise ReconciliationRequired(str(exc)) from exc
result = dict(verified)
if result.get("tag_oid") != self.binding.tag_oid:
self.state.fail(
"MANUAL_OWNER_REVIEW_REQUIRED",
"same-name tag object differs from the approved V2R2C1 tag",
)
raise ManualOwnerReviewRequired("approved tag object mismatch")
self.state.record("TAG_VERIFIED", "tag", result)
return {"status": "REMOTE_VERIFIED", **result}
@@ -478,6 +596,8 @@ class GovernedProductionReleaseExecutor:
tag_state = self.state.value.get("completed", {}).get("tag")
if not isinstance(tag_state, Mapping) or tag_state.get("state") not in {"GITEA_API_CONFIRMED", "REMOTE_VERIFIED"}:
raise ReleaseBlocked("REMOTE_TAG_PROOF_REQUIRED")
if tag_state.get("tag_oid") != self.binding.tag_oid:
raise ManualOwnerReviewRequired("approved tag object mismatch")
artifact = self.state.value.get("completed", {}).get("production_artifact")
if not isinstance(artifact, Mapping):
artifact = self.build_artifact(request)
@@ -488,6 +608,13 @@ class GovernedProductionReleaseExecutor:
"artifact_size": int(artifact["artifact_size"]),
"artifact_sha256": artifact["artifact_sha256"],
}
for name in (
"source_registry_main_oid",
"source_registry_path",
"source_registry_sha256",
):
if name in artifact:
value[name] = artifact[name]
# The production adapter is privileged and is run through the root
# approved-release service boundary by the caller. This handler is
# still useful for a root-owned staging invocation and remains
@@ -507,6 +634,21 @@ class GovernedProductionReleaseExecutor:
result["production_health"] = _health(self.config.health_url, self.binding, self.state.value.get("completed", {}).get("production_artifact", {}))
result["registry"] = _read_json(Path(self.config.registry_root) / f"{VERSION}.json")
result["baseline"] = _read_json(Path(self.config.baseline_path))
try:
assert_source = Path(self.config.registry_root) / f"{VERSION}.json"
if not assert_source.is_file():
raise RegistryConvergenceError("runtime_registry_entry_missing")
assert_source_registry_convergence(
assert_source,
repo_git_dir=self.config.authoritative_git_dir,
)
source_check = {"status": "PASS", "path": str(assert_source)}
except RegistryConvergenceError as exc:
source_check = {"status": "FAIL", "error": str(exc)}
result["source_registry"] = source_check
result.setdefault("checks", {})["source_registry"] = source_check["status"] == "PASS"
if result.get("status") == "PASS" and source_check["status"] != "PASS":
result["status"] = "FAIL"
return result
def acceptance(self, request: Mapping[str, Any]) -> dict[str, Any]:
-456
View File
@@ -1,456 +0,0 @@
from __future__ import annotations
import dataclasses
import datetime as dt
import hashlib
import json
import sqlite3
import re
from pathlib import Path
from typing import Any, Iterable, Mapping
UTC = dt.timezone.utc
_ALLOWED_CONFIDENTIALITY = frozenset({"public", "tenant", "project", "private", "run"})
_PRIVILEGED = frozenset({"*", "admin", "system.admin", "message.admin", "task.admin"})
class HandoffError(RuntimeError):
code = "HANDOFF_ERROR"
class MessageScopeMismatch(HandoffError):
code = "MESSAGE_SCOPE_MISMATCH_DENY"
class ConfidentialityScopeDenied(HandoffError):
code = "MESSAGE_CONFIDENTIALITY_SCOPE_ENFORCE"
class MessageExpired(HandoffError):
code = "MESSAGE_EXPIRED"
class SequenceGap(HandoffError):
code = "MESSAGE_SEQUENCE_GAP_DETECT"
class SequenceConflict(HandoffError):
code = "MESSAGE_SEQUENCE_CONFLICT"
class IdempotencyConflict(HandoffError):
code = "MESSAGE_IDEMPOTENCY_CONFLICT"
def _utcnow() -> dt.datetime:
return dt.datetime.now(UTC)
def _iso(value: dt.datetime | str | None) -> str | None:
if value is None:
return None
parsed = dt.datetime.fromisoformat(value.replace("Z", "+00:00")) if isinstance(value, str) else value
if parsed.tzinfo is None:
parsed = parsed.replace(tzinfo=UTC)
return parsed.astimezone(UTC).isoformat()
def _canonical(value: Any) -> str:
return json.dumps(value, sort_keys=True, separators=(",", ":"), ensure_ascii=False, default=str)
def _sha(value: Any) -> str:
return hashlib.sha256(_canonical(value).encode("utf-8")).hexdigest()
def _clean(value: Any) -> str | None:
if value is None:
return None
text = str(value).strip()
return text or None
def _tuple_scope(value: Iterable[str] | str | None) -> tuple[str, ...]:
if value is None:
return ()
if isinstance(value, str):
value = value.split(",")
return tuple(sorted({str(item).strip() for item in value if str(item).strip()}))
_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._~+/-]{8,}|(?:token|secret|password|api[_-]?key)\s*[:=]\s*\S+")
_SAFE_REFERENCE = re.compile(r"^(?:systemd-credential|vault|secret-manager):[A-Za-z0-9][A-Za-z0-9._:/#-]{0,255}$")
def assert_payload_secret_safe(value: Any, *, key: str = "") -> None:
if isinstance(value, Mapping):
for child_key, child in value.items():
assert_payload_secret_safe(child, key=str(child_key))
return
if isinstance(value, (list, tuple)):
for child in value:
assert_payload_secret_safe(child, key=key)
return
if isinstance(value, (bytes, bytearray)):
raise ValueError("CREDENTIAL_SECRET_EXPOSURE_DENY")
if isinstance(value, str):
if _SECRET_VALUE.search(value):
raise ValueError("CREDENTIAL_SECRET_EXPOSURE_DENY")
if _SECRET_KEY.search(key) and value and _SAFE_REFERENCE.fullmatch(value) is None:
raise ValueError("CREDENTIAL_SECRET_EXPOSURE_DENY")
@dataclasses.dataclass(frozen=True)
class DeliveryContext:
tenant_id: str | None = None
user_id: str | None = None
project_id: str | None = None
conversation_id: str | None = None
run_id: str | None = None
capabilities: tuple[str, ...] = ()
@classmethod
def from_actor(cls, actor: Any) -> "DeliveryContext":
def get(name: str) -> Any:
if isinstance(actor, Mapping):
return actor.get(name)
return getattr(actor, name, None)
return cls(
tenant_id=_clean(get("tenant_id")),
user_id=_clean(get("user_id")),
project_id=_clean(get("project_id")),
conversation_id=_clean(get("conversation_id")),
run_id=_clean(get("run_id")),
capabilities=_tuple_scope(get("capabilities") or get("capability_scope")),
)
@property
def privileged(self) -> bool:
return bool(set(self.capabilities) & _PRIVILEGED)
@dataclasses.dataclass(frozen=True)
class MessageEnvelope:
source: str
target: str
message_type: str
payload: Mapping[str, Any]
sequence: int
idempotency_key: str
tenant_id: str | None = None
user_id: str | None = None
project_id: str | None = None
conversation_id: str | None = None
task_id: str | None = None
run_id: str | None = None
correlation_id: str | None = None
causation_id: str | None = None
capability_scope: tuple[str, ...] = ()
confidentiality_scope: str = "private"
expires_at: str | None = None
created_at: str = dataclasses.field(default_factory=lambda: _iso(_utcnow()) or "")
envelope_id: str = ""
def __post_init__(self) -> None:
assert_payload_secret_safe(self.payload)
for field in ("source", "target", "message_type", "idempotency_key", "tenant_id", "user_id",
"project_id", "conversation_id", "task_id", "run_id", "correlation_id", "causation_id"):
object.__setattr__(self, field, _clean(getattr(self, field)) if field not in {
"source", "target", "message_type", "idempotency_key"
} else (_clean(getattr(self, field)) or ""))
object.__setattr__(self, "capability_scope", _tuple_scope(self.capability_scope))
object.__setattr__(self, "confidentiality_scope", (_clean(self.confidentiality_scope) or "private").lower())
object.__setattr__(self, "expires_at", _iso(self.expires_at))
object.__setattr__(self, "created_at", _iso(self.created_at) or "")
if not self.source or not self.target or not self.message_type or not self.idempotency_key:
raise ValueError("MESSAGE_ENVELOPE_IDENTITY_REQUIRED")
if not isinstance(self.sequence, int) or self.sequence < 1:
raise ValueError("MESSAGE_SEQUENCE_MUST_BE_POSITIVE")
if self.confidentiality_scope not in _ALLOWED_CONFIDENTIALITY:
raise ValueError("MESSAGE_CONFIDENTIALITY_SCOPE_INVALID")
if self.confidentiality_scope != "public" and self.tenant_id is None:
raise ValueError("MESSAGE_TENANT_REQUIRED")
if self.confidentiality_scope in {"project", "private", "run"} and self.project_id is None:
raise ValueError("MESSAGE_PROJECT_REQUIRED")
if self.confidentiality_scope in {"private", "run"} and self.user_id is None:
raise ValueError("MESSAGE_USER_REQUIRED")
if self.confidentiality_scope == "run" and self.run_id is None:
raise ValueError("MESSAGE_RUN_REQUIRED")
expected = stable_event_id(
source=self.source, target=self.target, message_type=self.message_type, payload=self.payload,
tenant_id=self.tenant_id, user_id=self.user_id, project_id=self.project_id,
conversation_id=self.conversation_id, task_id=self.task_id, run_id=self.run_id,
correlation_id=self.correlation_id, causation_id=self.causation_id,
sequence=self.sequence, idempotency_key=self.idempotency_key,
)
if self.envelope_id and self.envelope_id != expected:
raise ValueError("MESSAGE_ENVELOPE_ID_MISMATCH")
object.__setattr__(self, "envelope_id", expected)
@property
def payload_sha256(self) -> str:
return _sha(self.payload)
@property
def stream_key(self) -> str:
return _sha({
"tenant_id": self.tenant_id, "source": self.source, "target": self.target,
"conversation_id": self.conversation_id, "task_id": self.task_id, "run_id": self.run_id,
})
def is_expired(self, *, now: dt.datetime | None = None) -> bool:
if self.expires_at is None:
return False
return dt.datetime.fromisoformat(self.expires_at) <= (now or _utcnow()).astimezone(UTC)
def as_record(self) -> dict[str, Any]:
return {
"envelope_id": self.envelope_id, "stream_key": self.stream_key, "source": self.source,
"target": self.target, "message_type": self.message_type, "payload_json": _canonical(self.payload),
"payload_sha256": self.payload_sha256, "tenant_id": self.tenant_id, "user_id": self.user_id,
"project_id": self.project_id, "conversation_id": self.conversation_id, "task_id": self.task_id,
"run_id": self.run_id, "correlation_id": self.correlation_id,
"causation_id": self.causation_id, "sequence": self.sequence,
"capability_scope_json": _canonical(self.capability_scope),
"confidentiality_scope": self.confidentiality_scope, "idempotency_key": self.idempotency_key,
"expires_at": self.expires_at, "created_at": self.created_at,
}
def stable_event_id(*, source: Any, target: Any, message_type: Any, payload: Any,
tenant_id: Any = None, user_id: Any = None, project_id: Any = None,
conversation_id: Any = None, task_id: Any = None, run_id: Any = None,
correlation_id: Any = None, causation_id: Any = None,
sequence: Any = None, idempotency_key: Any = None) -> str:
assert_payload_secret_safe(payload)
material = {
"source": _clean(source), "target": _clean(target), "message_type": _clean(message_type),
"payload_sha256": _sha(payload), "tenant_id": _clean(tenant_id), "user_id": _clean(user_id),
"project_id": _clean(project_id), "conversation_id": _clean(conversation_id),
"task_id": _clean(task_id), "run_id": _clean(run_id), "causation_id": _clean(causation_id),
"sequence": sequence, "idempotency_key": _clean(idempotency_key),
}
# Preserve historical envelope IDs for legacy rows that predate correlation_id.
if _clean(correlation_id) is not None:
material["correlation_id"] = _clean(correlation_id)
return _sha(material)
def bind_envelope_scope(envelope: MessageEnvelope, actor: Any) -> MessageEnvelope:
ctx = DeliveryContext.from_actor(actor)
if ctx.privileged:
return envelope
if ctx.tenant_id is None:
if envelope.tenant_id is not None:
raise MessageScopeMismatch(MessageScopeMismatch.code)
return envelope
updates: dict[str, Any] = {}
for field in ("tenant_id", "user_id", "project_id", "conversation_id", "run_id"):
trusted = getattr(ctx, field)
requested = getattr(envelope, field)
if requested is not None and requested != trusted:
raise MessageScopeMismatch(f"{MessageScopeMismatch.code}:{field}")
if requested is None and trusted is not None:
updates[field] = trusted
if not updates:
return envelope
data = dataclasses.asdict(envelope)
data.update(updates)
data["envelope_id"] = ""
return MessageEnvelope(**data)
def authorize_delivery(actor: Any, envelope: MessageEnvelope) -> None:
ctx = DeliveryContext.from_actor(actor)
if ctx.privileged:
return
if not set(envelope.capability_scope).issubset(set(ctx.capabilities)):
raise ConfidentialityScopeDenied(f"{ConfidentialityScopeDenied.code}:capability")
if envelope.confidentiality_scope == "public":
return
if ctx.tenant_id != envelope.tenant_id:
raise MessageScopeMismatch(f"{MessageScopeMismatch.code}:tenant_id")
if envelope.confidentiality_scope in {"project", "private", "run"} and ctx.project_id != envelope.project_id:
raise ConfidentialityScopeDenied(f"{ConfidentialityScopeDenied.code}:project_id")
if envelope.confidentiality_scope in {"private", "run"} and ctx.user_id != envelope.user_id:
raise ConfidentialityScopeDenied(f"{ConfidentialityScopeDenied.code}:user_id")
if envelope.confidentiality_scope == "run" and ctx.run_id != envelope.run_id:
raise ConfidentialityScopeDenied(f"{ConfidentialityScopeDenied.code}:run_id")
_SCHEMA = """
CREATE TABLE IF NOT EXISTS message_handoff (
envelope_id TEXT PRIMARY KEY,
stream_key TEXT NOT NULL,
source TEXT NOT NULL,
target TEXT NOT NULL,
message_type TEXT NOT NULL,
payload_json TEXT NOT NULL,
payload_sha256 TEXT NOT NULL,
tenant_id TEXT,
user_id TEXT,
project_id TEXT,
conversation_id TEXT,
task_id TEXT,
run_id TEXT,
correlation_id TEXT,
causation_id TEXT,
sequence INTEGER NOT NULL CHECK(sequence >= 1),
capability_scope_json TEXT NOT NULL,
confidentiality_scope TEXT NOT NULL,
idempotency_key TEXT NOT NULL,
expires_at TEXT,
created_at TEXT NOT NULL,
status TEXT NOT NULL DEFAULT 'PENDING',
delivered_at TEXT,
acked_at TEXT,
UNIQUE(stream_key, idempotency_key),
UNIQUE(stream_key, sequence)
);
CREATE INDEX IF NOT EXISTS idx_message_handoff_target_status
ON message_handoff(target, status, created_at);
CREATE INDEX IF NOT EXISTS idx_message_handoff_scope
ON message_handoff(tenant_id, project_id, user_id, conversation_id, run_id);
"""
class DurableHandoffStore:
def __init__(self, path: str | Path) -> None:
self.path = Path(path)
self.path.parent.mkdir(parents=True, exist_ok=True)
with self._connect() as conn:
conn.executescript(_SCHEMA)
columns = {str(row[1]) for row in conn.execute("PRAGMA table_info(message_handoff)")}
if "correlation_id" not in columns:
conn.execute("ALTER TABLE message_handoff ADD COLUMN correlation_id TEXT")
conn.execute(
"CREATE INDEX IF NOT EXISTS idx_message_handoff_lineage "
"ON message_handoff(correlation_id, causation_id)"
)
def _connect(self) -> sqlite3.Connection:
conn = sqlite3.connect(self.path, timeout=30.0)
conn.row_factory = sqlite3.Row
conn.execute("PRAGMA foreign_keys=ON")
conn.execute("PRAGMA busy_timeout=30000")
return conn
@staticmethod
def _from_row(row: sqlite3.Row) -> MessageEnvelope:
return MessageEnvelope(
source=row["source"], target=row["target"], message_type=row["message_type"],
payload=json.loads(row["payload_json"]), tenant_id=row["tenant_id"], user_id=row["user_id"],
project_id=row["project_id"], conversation_id=row["conversation_id"], task_id=row["task_id"],
run_id=row["run_id"], correlation_id=row["correlation_id"],
causation_id=row["causation_id"], sequence=int(row["sequence"]),
capability_scope=tuple(json.loads(row["capability_scope_json"])),
confidentiality_scope=row["confidentiality_scope"], idempotency_key=row["idempotency_key"],
expires_at=row["expires_at"], created_at=row["created_at"], envelope_id=row["envelope_id"],
)
def put(self, envelope: MessageEnvelope) -> tuple[str, MessageEnvelope]:
if envelope.is_expired():
raise MessageExpired(MessageExpired.code)
record = envelope.as_record()
with self._connect() as conn:
conn.execute("BEGIN IMMEDIATE")
replay = conn.execute(
"SELECT * FROM message_handoff WHERE stream_key=? AND idempotency_key=?",
(envelope.stream_key, envelope.idempotency_key),
).fetchone()
if replay is not None:
existing = self._from_row(replay)
if existing.envelope_id != envelope.envelope_id or existing.payload_sha256 != envelope.payload_sha256:
raise IdempotencyConflict(IdempotencyConflict.code)
conn.commit()
return "REPLAY", existing
seq = conn.execute(
"SELECT * FROM message_handoff WHERE stream_key=? AND sequence=?",
(envelope.stream_key, envelope.sequence),
).fetchone()
if seq is not None:
existing = self._from_row(seq)
if existing.envelope_id == envelope.envelope_id:
conn.commit()
return "REPLAY", existing
raise SequenceConflict(SequenceConflict.code)
maximum = int(conn.execute(
"SELECT COALESCE(MAX(sequence),0) FROM message_handoff WHERE stream_key=?",
(envelope.stream_key,),
).fetchone()[0])
expected = maximum + 1
if envelope.sequence != expected:
raise SequenceGap(f"{SequenceGap.code}:expected={expected}:actual={envelope.sequence}")
columns = tuple(record)
placeholders = ",".join("?" for _ in columns)
conn.execute(
f"INSERT INTO message_handoff ({','.join(columns)}) VALUES ({placeholders})",
tuple(record[column] for column in columns),
)
conn.commit()
return "CREATED", envelope
def get(self, envelope_id: str, *, actor: Any) -> MessageEnvelope:
with self._connect() as conn:
row = conn.execute("SELECT * FROM message_handoff WHERE envelope_id=?", (envelope_id,)).fetchone()
if row is None:
raise KeyError(envelope_id)
envelope = self._from_row(row)
if envelope.is_expired():
conn.execute("UPDATE message_handoff SET status='EXPIRED' WHERE envelope_id=?", (envelope_id,))
conn.commit()
raise MessageExpired(MessageExpired.code)
authorize_delivery(actor, envelope)
return envelope
def pending(self, *, target: str, actor: Any, limit: int = 100) -> list[MessageEnvelope]:
if not 1 <= limit <= 1000:
raise ValueError("MESSAGE_PENDING_LIMIT_INVALID")
visible: list[MessageEnvelope] = []
with self._connect() as conn:
rows = conn.execute(
"SELECT * FROM message_handoff WHERE target=? AND status='PENDING' ORDER BY created_at, sequence LIMIT ?",
(target, limit),
).fetchall()
for row in rows:
envelope = self._from_row(row)
if envelope.is_expired():
conn.execute("UPDATE message_handoff SET status='EXPIRED' WHERE envelope_id=?", (envelope.envelope_id,))
continue
try:
authorize_delivery(actor, envelope)
except HandoffError:
continue
visible.append(envelope)
conn.commit()
return visible
def mark_delivered(self, envelope_id: str, *, actor: Any) -> MessageEnvelope:
envelope = self.get(envelope_id, actor=actor)
with self._connect() as conn:
conn.execute(
"UPDATE message_handoff SET status='DELIVERED', delivered_at=? "
"WHERE envelope_id=? AND status='PENDING'",
(_iso(_utcnow()), envelope_id),
)
conn.commit()
return envelope
def acknowledge(self, envelope_id: str, *, actor: Any) -> MessageEnvelope:
envelope = self.get(envelope_id, actor=actor)
with self._connect() as conn:
conn.execute(
"UPDATE message_handoff SET status='ACKED', acked_at=? WHERE envelope_id=?",
(_iso(_utcnow()), envelope_id),
)
conn.commit()
return envelope
class HandoffFabric:
def __init__(self, store: DurableHandoffStore) -> None:
self.store = store
def publish(self, envelope: MessageEnvelope, *, actor: Any | None = None) -> tuple[str, MessageEnvelope]:
return self.store.put(bind_envelope_scope(envelope, actor) if actor is not None else envelope)
@@ -1,68 +0,0 @@
from __future__ import annotations
from typing import Any, Mapping
from message_handoff import MessageEnvelope
def _get(value: Any, *names: str, default: Any = None) -> Any:
for name in names:
if isinstance(value, Mapping) and name in value:
return value[name]
if hasattr(value, name):
return getattr(value, name)
return default
def agent_event_to_envelope(event: Any, *, target: str | None = None, sequence: int = 1,
idempotency_key: str | None = None, **scope: Any) -> MessageEnvelope:
payload = _get(event, "payload", "data", default={}) or {}
target = target or _get(event, "target", "recipient")
if not target:
raise ValueError("MESSAGE_TARGET_REQUIRED")
return MessageEnvelope(
source=str(_get(event, "source", "producer", "agent_id", default="agent-event")),
target=str(target),
message_type=str(_get(event, "event_type", "type", "message_type", default="agent.event")),
payload=dict(payload),
sequence=sequence,
idempotency_key=idempotency_key or str(_get(event, "id", "event_id", default="")) or f"agent-event:{sequence}",
causation_id=_get(event, "causation_id"),
**scope,
)
def taskplane_event_to_envelope(event: Any, *, target: str, sequence: int = 1,
idempotency_key: str | None = None, **scope: Any) -> MessageEnvelope:
data = _get(event, "data", "payload", default={}) or {}
event_id = str(_get(event, "id", "event_id", default=""))
return MessageEnvelope(
source=str(_get(event, "source", default="task-plane")),
target=target,
message_type=str(_get(event, "type", "event_type", default="task.event")),
payload=dict(data),
sequence=sequence,
idempotency_key=idempotency_key or event_id or f"task-event:{sequence}",
causation_id=event_id or None,
**scope,
)
def legacy_message_to_envelope(message: Any, *, target: str | None = None, sequence: int = 1,
idempotency_key: str | None = None, **scope: Any) -> MessageEnvelope:
payload = _get(message, "payload", "data", "body", "content", default={})
if not isinstance(payload, Mapping):
payload = {"value": payload}
target = target or _get(message, "target", "recipient", "receiver")
if not target:
raise ValueError("MESSAGE_TARGET_REQUIRED")
source = _get(message, "source", "sender", "agent_id", default="legacy-message-bus")
kind = _get(message, "message_type", "type", "kind", default="legacy.message")
identifier = _get(message, "id", "message_id", "event_id")
return MessageEnvelope(
source=str(source), target=str(target), message_type=str(kind), payload=dict(payload),
sequence=sequence,
idempotency_key=idempotency_key or (str(identifier) if identifier else f"legacy:{sequence}"),
causation_id=str(identifier) if identifier else None,
**scope,
)
+73 -5
View File
@@ -7,7 +7,7 @@ Registry and authoritative runtime-anchor Baseline only.
Registry is finalized before Baseline. Baseline apply independently verifies:
- exact Authority annotated tag and peeled target
- exact ACTIVE production health
- exact V2R1C7 Registry record
- exact runtime Registry record and source-registry convergence
- exact previous Baseline file SHA-256 (CAS)
"""
from __future__ import annotations
@@ -25,6 +25,7 @@ import time
import urllib.error
import urllib.request
from pathlib import Path
from version_registry import assert_source_registry_convergence
from typing import Any, Mapping
OID_RE = re.compile(r"^[0-9a-f]{40}$")
@@ -97,11 +98,40 @@ def load_request() -> dict[str, Any]:
raise ValueError("approval_plan_id_invalid")
if "tag_oid" in value and not OID_RE.fullmatch(str(value["tag_oid"])):
raise ValueError("tag_oid_invalid")
if "source_registry_main_oid" in value and not OID_RE.fullmatch(
str(value["source_registry_main_oid"])
):
raise ValueError("source_registry_main_oid_invalid")
if "source_registry_sha256" in value and not SHA_RE.fullmatch(
str(value["source_registry_sha256"])
):
raise ValueError("source_registry_sha256_invalid")
if "source_registry_path" in value:
source_path = str(value["source_registry_path"])
if (
not source_path.startswith("versions/")
or ".." in Path(source_path).parts
or "\\" in source_path
or not source_path.endswith(".json")
):
raise ValueError("source_registry_path_invalid")
provenance = tuple(
value.get(name)
for name in (
"source_registry_main_oid",
"source_registry_path",
"source_registry_sha256",
)
)
if any(item is not None for item in provenance) and not all(
item is not None for item in provenance
):
raise ValueError("source_registry_provenance_incomplete")
return value
def exact_response(value: Mapping[str, Any], status: str, **extra: Any) -> dict[str, Any]:
return {
result = {
"status": status,
"version": value["version"],
"target_oid": value["target_oid"],
@@ -110,6 +140,14 @@ def exact_response(value: Mapping[str, Any], status: str, **extra: Any) -> dict[
"approval_plan_id": value["approval_plan_id"],
**extra,
}
for name in (
"source_registry_main_oid",
"source_registry_path",
"source_registry_sha256",
):
if name in value:
result[name] = value[name]
return result
def git(repository: Path, *args: str, check: bool = True) -> subprocess.CompletedProcess[str]:
@@ -154,7 +192,7 @@ def health(url: str, value: Mapping[str, Any]) -> dict[str, Any]:
def registry_record(value: Mapping[str, Any], tag_oid: str, production_health: Mapping[str, Any]) -> dict[str, Any]:
return {
result = {
"schema": "aios.production-registry.v2",
"version": value["version"],
"repository": "aios/aios",
@@ -175,10 +213,18 @@ def registry_record(value: Mapping[str, Any], tag_oid: str, production_health: M
"deployment_state": production_health["deployment_state"],
"updated_at": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()),
}
for name in (
"source_registry_main_oid",
"source_registry_path",
"source_registry_sha256",
):
if name in value:
result[name] = value[name]
return result
def registry_matches(record: Mapping[str, Any], value: Mapping[str, Any], tag_oid: str) -> bool:
return all(
matches = all(
record.get(key) == expected
for key, expected in {
"version": value["version"],
@@ -192,11 +238,16 @@ def registry_matches(record: Mapping[str, Any], value: Mapping[str, Any], tag_oi
"production_deployed": True,
}.items()
) and record.get("registered") is True and record.get("enabled") is True
return matches and all(
record.get(name) == value.get(name)
for name in ("source_registry_main_oid", "source_registry_path", "source_registry_sha256")
if name in value
)
def baseline_matches(record: Mapping[str, Any], value: Mapping[str, Any], tag_oid: str) -> bool:
current = record.get("current_baseline") or record.get("version")
return current == value["version"] and all(
matches = current == value["version"] and all(
record.get(key) == expected
for key, expected in {
"target_oid": value["target_oid"],
@@ -209,6 +260,11 @@ def baseline_matches(record: Mapping[str, Any], value: Mapping[str, Any], tag_oi
"status": "PRODUCTION_DEPLOYED",
}.items()
)
return matches and all(
record.get(name) == value.get(name)
for name in ("source_registry_main_oid", "source_registry_path", "source_registry_sha256")
if name in value
)
def registry_probe(args: argparse.Namespace, value: Mapping[str, Any]) -> dict[str, Any]:
@@ -305,6 +361,11 @@ def baseline_apply(args: argparse.Namespace, value: Mapping[str, Any]) -> dict[s
int(previous.get("generation", 0) or 0),
int(previous.get("authority_generation", 0) or 0),
) + 1
assert_source_registry_convergence(
registry_path,
repo_git_dir=args.repository,
source_registry_main_oid=value.get("source_registry_main_oid"),
)
record = {
"schema": "aios.production-baseline.v2",
"source_identity": "aios-gitops-runtime-anchor-baseline-authority-v1",
@@ -336,6 +397,13 @@ def baseline_apply(args: argparse.Namespace, value: Mapping[str, Any]) -> dict[s
"promoted_at": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()),
"promoted_by": "aios-approved-production-release-executor",
}
for name in (
"source_registry_main_oid",
"source_registry_path",
"source_registry_sha256",
):
if name in value:
record[name] = value[name]
atomic_json(args.baseline_path, record)
return baseline_probe(args, value)
+1 -9
View File
@@ -11,7 +11,6 @@ from dataclasses import asdict, dataclass, field
from typing import Any
from .search_gateway_orchestrator import SearchGatewayOrchestrator
from .trace_tool_credential_governance import GovernanceActor, TraceContext
@dataclass(frozen=True)
@@ -44,9 +43,6 @@ class SearchGatewayAgentAdapter:
domain_profiles: list[str] | None = None,
user_scope: str = "public",
correlation_id: str | None = None,
*,
actor: GovernanceActor | dict[str, Any] | None = None,
trace: TraceContext | None = None,
) -> dict[str, Any]:
payload = {
"query": query,
@@ -63,11 +59,7 @@ class SearchGatewayAgentAdapter:
"session_profile": context.session_profile,
"correlation_id": correlation_id,
}
if actor is None and trace is None:
# Preserve adapter compatibility; the governed orchestrator itself
# still fails closed when trusted context is absent.
return self.orchestrator.execute(payload)
return self.orchestrator.execute(payload, actor=actor, trace=trace)
return self.orchestrator.execute(payload)
class OpenClawSearchClient(SearchGatewayAgentAdapter):
+4 -70
View File
@@ -13,18 +13,10 @@ from __future__ import annotations
from dataclasses import asdict, dataclass, field
from datetime import datetime, timezone
import hashlib
import json
from typing import Any, Protocol
from uuid import uuid4
from .search_profile_resolver import DomainQueryProfileRegistry, SearchProfile, SearchProfileResolver
from .trace_tool_credential_governance import (
GovernanceActor,
GovernanceError,
GovernanceStore,
TraceContext,
)
def utc_now() -> str:
@@ -164,25 +156,13 @@ class SearchPolicyGuard:
class SearchGateway:
def __init__(self, providers: list[SearchProvider] | None = None, *, governance_store: GovernanceStore | None = None):
def __init__(self, providers: list[SearchProvider] | None = None):
self.providers = providers or []
self.governance_store = governance_store
self.profile_resolver = SearchProfileResolver()
self.planner = QueryPlanner()
self.guard = SearchPolicyGuard()
def search(self, request: SearchRequest, *, actor: GovernanceActor | dict[str, Any] | None = None, trace: TraceContext | None = None) -> SearchResponse:
trusted_actor: GovernanceActor | None = None
if self.governance_store is not None:
if actor is None or trace is None:
raise GovernanceError("TOOL_MCP_AUTHZ_DENY")
trusted_actor = GovernanceActor.from_value(actor)
if request.requester != trusted_actor.identity:
raise GovernanceError("TOOL_MCP_AUTHZ_DENY", field="requester")
if request.correlation_id is not None and request.correlation_id not in {
trace.trace_id, trace.correlation_id
}:
raise GovernanceError("TOOL_SCOPE_PROPAGATION", field="correlation_id")
def search(self, request: SearchRequest) -> SearchResponse:
request_id = request.correlation_id or f"search_{uuid4().hex[:12]}"
profile = self.profile_resolver.resolve(request.group_profile, request.user_profile, request.session_profile)
expanded_queries = self.planner.expand(request, profile)
@@ -204,60 +184,14 @@ class SearchGateway:
attempts: list[SearchProviderAttempt] = []
providers = self._ordered_providers(profile)
for provider in providers:
if request.authorized_sources and provider.name not in request.authorized_sources:
attempts.append(SearchProviderAttempt(provider=provider.name, status="denied", reason="SOURCE_AUTHZ_DENY"))
continue
if request.user_scope != "public" and provider.name not in request.authorized_sources:
attempts.append(SearchProviderAttempt(provider=provider.name, status="denied", reason="SOURCE_AUTHZ_REQUIRED"))
continue
try:
if self.governance_store is not None:
assert trusted_actor is not None and trace is not None
input_digest = hashlib.sha256(
json.dumps(
{"query": request.query, "intent": request.intent, "provider": provider.name},
sort_keys=True, separators=(",", ":"), ensure_ascii=False,
).encode("utf-8")
).hexdigest()
invocation_key = hashlib.sha256(
f"{trace.trace_id}:{trace.span_id}:{provider.name}:{input_digest}".encode("utf-8")
).hexdigest()
authorization = self.governance_store.authorize_tool(
actor=trusted_actor,
tool_name=f"search.{provider.name}",
trace=trace,
requested_scope=trace.scope,
input_digest=input_digest,
invocation_key=invocation_key,
)
if authorization.get("idempotent"):
raise GovernanceError("TOOL_INVOCATION_REPLAY")
provider_evidence, attempt = provider.search(request, expanded_queries)
if self.governance_store is not None:
result_digest = hashlib.sha256(
json.dumps(
{
"evidence": [item.to_dict() for item in provider_evidence],
"attempt": attempt.to_dict(),
},
sort_keys=True, separators=(",", ":"), ensure_ascii=False,
).encode("utf-8")
).hexdigest()
self.governance_store.record_tool_receipt(
invocation_key=invocation_key,
result_digest=result_digest,
actor=trusted_actor,
trace=trace,
)
evidence.extend(provider_evidence)
attempts.append(attempt)
if provider_evidence:
break
except GovernanceError:
raise
except Exception:
# Provider exceptions may contain URLs, tokens, or local paths.
attempts.append(SearchProviderAttempt(provider=getattr(provider, "name", "unknown"), status="failed", reason="PROVIDER_FAILURE"))
except Exception as exc:
attempts.append(SearchProviderAttempt(provider=getattr(provider, "name", "unknown"), status="failed", reason=str(exc)))
if evidence:
return SearchResponse(
+6 -8
View File
@@ -7,29 +7,27 @@ try:
except Exception:
APIRouter = None
from .search_gateway import SearchRequest
from .search_gateway_orchestrator import SearchGatewayOrchestrator
from .trace_tool_credential_governance import GovernanceActor, TraceContext
from .search_gateway import SearchGateway, SearchRequest
from .search_gateway_dashboard import SearchGatewayDashboard
orchestrator = SearchGatewayOrchestrator()
gateway = SearchGateway()
dashboard = SearchGatewayDashboard()
def search_gateway_request(payload: dict[str, Any], *, actor: GovernanceActor | dict[str, Any] | None = None, trace: TraceContext | None = None) -> dict[str, Any]:
return orchestrator.execute(payload, actor=actor, trace=trace)
def search_gateway_request(payload: dict[str, Any]) -> dict[str, Any]:
request = SearchRequest(**payload)
return gateway.search(request).to_dict()
def get_search_gateway_status() -> dict[str, Any]:
return {
"module": "search-gateway",
"status": "ready",
"providers_configured": len(orchestrator.gateway.providers),
"providers_configured": len(gateway.providers),
"policy_guard": "enabled",
"profile_customization": "enabled",
"fallback_required_when_provider_unavailable": True,
"governance": "required",
}
@@ -3,49 +3,24 @@
from __future__ import annotations
from typing import Any
import os
from .search_audit_store import SearchAuditStore
from .search_event_bridge import SearchEventBridge
from .search_gateway import SearchGateway, SearchRequest
from .search_provider_registry import SearchProviderRegistry
from .trace_tool_credential_governance import (
GovernanceActor,
GovernanceScope,
GovernanceStore,
ToolDescriptor,
TraceContext,
)
class SearchGatewayOrchestrator:
def __init__(self, *, governance_store: GovernanceStore | None = None):
def __init__(self):
self.registry = SearchProviderRegistry()
self.audit = SearchAuditStore()
self.events = SearchEventBridge()
self.governance = governance_store or GovernanceStore(
os.environ.get("AIOS_SEARCH_GOVERNANCE_DB", "runtime/search-governance.sqlite3")
)
registrar = GovernanceActor(
"search-runtime-bootstrap", GovernanceScope("system"), ("system.admin",)
)
for provider in self.registry.ordered():
self.governance.register_tool(
ToolDescriptor(
f"search.{provider.name}", "tool", GovernanceScope("global"), ("tool.search",)
),
registrar,
)
self.gateway = SearchGateway(self.registry.ordered(), governance_store=self.governance)
self.gateway = SearchGateway(self.registry.ordered())
def execute(self, payload: dict[str, Any], *, actor: GovernanceActor | dict[str, Any] | None = None, trace: TraceContext | None = None) -> dict[str, Any]:
if actor is None or trace is None:
from .trace_tool_credential_governance import GovernanceError
raise GovernanceError("TOOL_MCP_AUTHZ_DENY")
self.governance.record_trace(trace, actor)
def execute(self, payload: dict[str, Any]) -> dict[str, Any]:
request = SearchRequest(**payload)
self.events.search_started(request.to_dict())
response = self.gateway.search(request, actor=actor, trace=trace)
response = self.gateway.search(request)
result = response.to_dict()
for attempt in result.get("attempts", []):
self.events.provider_attempted(result["request_id"], attempt)
@@ -60,7 +35,6 @@ class SearchGatewayOrchestrator:
"registry": self.registry.status(),
"audit": "enabled",
"events": "enabled",
"governance": "required",
}
@@ -15,6 +15,8 @@ import subprocess
from pathlib import Path
from typing import Any, Mapping, Sequence
from aios_signer_broker import SignerBroker, SignerBrokerError, UnixSignerBroker
_SIGNER_FLAGS = (
"--namespace",
@@ -155,6 +157,36 @@ def _sign(
raise SignerContractError("signer_output_empty")
def run_signer_self_test(broker: SignerBroker | None = None) -> dict[str, str]:
active = broker or UnixSignerBroker()
try:
active.self_test()
except SignerBrokerError as exc:
raise SignerContractError(exc.code) from exc
return {"status": "PASS", "contract": "aios.signer-broker.v1"}
def _sign_with_broker(
broker: SignerBroker,
payload_kind: str,
input_path: Path,
signature_path: Path,
key_id_path: Path,
) -> None:
if signature_path.exists() or key_id_path.exists():
raise SignerContractError("contract_output_exists")
try:
signature, key_id = broker.sign(payload_kind, input_path.read_bytes())
except SignerBrokerError as exc:
raise SignerContractError(exc.code) from exc
if not signature or not key_id.strip():
raise SignerContractError("signer_output_empty")
signature_path.write_bytes(signature)
key_id_path.write_text(key_id.strip() + "\n", encoding="utf-8")
os.chmod(signature_path, 0o600)
os.chmod(key_id_path, 0o600)
def _manifest(grant_dir: Path, names: Sequence[str]) -> None:
rows = []
for name in names:
@@ -284,6 +316,8 @@ def run_compatible_signer(
signer_command: Sequence[str],
grant_dir: Path,
result_path: Path,
*,
broker: SignerBroker | None = None,
) -> dict[str, str]:
try:
if not _supports_deployed_signer(signer_command):
@@ -318,22 +352,21 @@ def run_compatible_signer(
"signing_key_identity": signer,
}
prepared = _prepare(grant_dir)
config_path = Path(
os.environ.get("AIOS_GRANT_ROOT_CONFIG", "/etc/aios/grant-root/config.json")
)
config = _read(config_path)
grant_namespace = str(config["grant_namespace"])
approval_namespace = str(config["approval_namespace"])
_sign(
signer_command,
approval_namespace,
active_broker = broker or UnixSignerBroker()
try:
active_broker.self_test()
except SignerBrokerError as exc:
raise SignerContractError(exc.code) from exc
_sign_with_broker(
active_broker,
"approval_plan",
grant_dir / "approval-plan.canonical.json",
grant_dir / "approval-plan.sig",
grant_dir / "approval-plan.key-id",
)
_sign(
signer_command,
grant_namespace,
_sign_with_broker(
active_broker,
"grant_payload",
grant_dir / "grant-payload.canonical.json",
grant_dir / "signed-grant.sig",
grant_dir / "signed-grant.key-id",
+26 -56
View File
@@ -34,7 +34,6 @@ from .models import (
)
from .policy import PolicyEngine
from .store import TaskPlaneStore
from .scope import bind_task_scope
from .worker_registry import WorkerRegistry
@@ -69,54 +68,28 @@ class TaskPlaneService:
self.delegation = DelegationEngine(store)
self.store.probe_attestation_verifier = self.policy.probe_attestation_verifier
def _authorized_task(self, task_id: str, actor: str, action: str) -> TaskRecord:
task = self.store.get_task(task_id, self.policy.actor_context(actor))
self.policy.assert_allowed(action, actor, task=task)
return task
def create_task(
self,
body: dict[str, Any],
idempotency_key: str | None = None,
*,
actor_identity: str | None = None,
) -> dict[str, Any]:
def create_task(self, body: dict[str, Any], idempotency_key: str | None = None) -> dict[str, Any]:
if body.get("parent_task_id"):
raise ForbiddenError("child tasks must use the delegate endpoint")
issuer = body.get("issuer", {})
declared_actor = issuer.get("id", "anon") if isinstance(issuer, dict) else "anon"
actor = actor_identity or declared_actor
actor = issuer.get("id", "anon") if isinstance(issuer, dict) else "anon"
self.policy.assert_allowed("create", actor)
try:
plan = TaskPlan.from_dict(body)
except (TypeError, ValueError) as exc:
raise ValidationError("invalid task plan") from exc
if actor_identity is not None:
plan = bind_task_scope(
plan, self.policy.actor_context(actor_identity), public_create=True
)
task = self.store.create_task_with_event(plan, idempotency_key=idempotency_key)
return task.to_dict()
def list_tasks(self, actor: str | None = None) -> list[dict[str, Any]]:
context = None
if actor is not None:
self.policy.assert_allowed("read", actor)
context = self.policy.actor_context(actor)
return [
task.to_dict(include_task=False)
for task in self.store.list_tasks(context)
]
return [task.to_dict(include_task=False) for task in self.store.list_tasks()]
def get_task(self, task_id: str, actor: str | None = None) -> dict[str, Any]:
context = None
if actor is not None:
self.policy.assert_allowed("read", actor)
context = self.policy.actor_context(actor)
task = self.store.get_task(task_id, context)
if actor is not None:
self.policy.assert_allowed("read", actor, task=task)
return task.to_dict()
self.policy.assert_allowed("read", actor, task=self.store.get_task(task_id))
return self.store.get_task(task_id).to_dict()
def get_worker(self, worker_id: str, actor: str | None = None) -> dict[str, Any]:
if actor is not None:
@@ -190,8 +163,9 @@ class TaskPlaneService:
if not worker_id:
raise ValidationError("worker_id required")
task = self._authorized_task(task_id, actor, "claim")
task = self.store.get_task(task_id)
self.policy.assert_readonly_worker_binding(actor, str(worker_id))
self.policy.assert_allowed("claim", actor, task=task)
lease_seconds = _bounded_int(
body.get("lease_seconds", 30), "lease_seconds", 1, 3600
)
@@ -210,9 +184,10 @@ class TaskPlaneService:
if not claim_id:
raise ValidationError("claim_id required")
actor = body.get("actor_identity") or body.get("worker_identity") or "worker"
task = self._authorized_task(task_id, actor, "heartbeat")
task = self.store.get_task(task_id)
claim = self.store.get_claim(claim_id)
self.policy.assert_readonly_worker_binding(actor, claim.worker_id)
self.policy.assert_allowed("heartbeat", actor, task=task)
if claim.worker_identity != actor:
raise ForbiddenError("claim identity mismatch")
lease_seconds = _bounded_int(
@@ -231,9 +206,10 @@ class TaskPlaneService:
message = body.get("message")
if progress is None:
raise ValidationError("progress required")
task = self._authorized_task(task_id, actor, "progress")
task = self.store.get_task(task_id)
claim = self.store.get_claim(claim_id)
self.policy.assert_readonly_worker_binding(actor, claim.worker_id)
self.policy.assert_allowed("progress", actor, task=task)
if task.task_plan.data_classification == "credential-secret":
if message not in {None, ""} or contains_secret_text(stage):
raise ValidationError(
@@ -258,7 +234,7 @@ class TaskPlaneService:
if not claim_id:
raise ValidationError("claim_id required")
actor = body.get("actor_identity") or body.get("worker_identity") or "worker"
task = self._authorized_task(task_id, actor, "result")
task = self.store.get_task(task_id)
status = body.get("status", "COMPLETED")
outputs = body.get("outputs", [])
@@ -271,6 +247,7 @@ class TaskPlaneService:
raise ValidationError("evidence items must be objects")
claim = self.store.get_claim(claim_id)
self.policy.assert_readonly_worker_binding(actor, claim.worker_id)
self.policy.assert_allowed("result", actor, task=task)
if claim.worker_identity != actor:
raise ForbiddenError("claim identity mismatch")
if task.task_plan.data_classification == "credential-secret":
@@ -349,7 +326,8 @@ class TaskPlaneService:
reason = body.get("reason", "failed")
actor = body.get("actor_identity") or body.get("worker_identity") or "worker"
self.policy.assert_allowed("fail", actor)
task = self._authorized_task(task_id, actor, "fail")
task = self.store.get_task(task_id)
self.policy.assert_allowed("fail", actor, task=task)
claim = self.store.get_claim(claim_id)
if claim.worker_identity != actor:
raise ForbiddenError("claim identity mismatch")
@@ -361,7 +339,8 @@ class TaskPlaneService:
actor = body.get("actor_identity") or body.get("worker_identity") or "owner"
self.policy.assert_allowed("cancel", actor)
reason = body.get("reason", "cancelled")
task = self._authorized_task(task_id, actor, "cancel")
task = self.store.get_task(task_id)
self.policy.assert_allowed("cancel", actor, task=task)
updated = self.store.cancel_task_with_event(
task_id, task.revision, reason=reason
)
@@ -370,7 +349,8 @@ class TaskPlaneService:
def delegate(self, task_id: str, body: dict[str, Any]) -> dict[str, Any]:
actor = body.get("actor_identity") or "owner"
self.policy.assert_allowed("delegate", actor)
parent = self._authorized_task(task_id, actor, "delegate")
parent = self.store.get_task(task_id)
self.policy.assert_allowed("delegate", actor, task=parent)
if "task" in body:
try:
child_plan = TaskPlan.from_dict(body["task"])
@@ -384,31 +364,25 @@ class TaskPlaneService:
except (TypeError, ValueError) as exc:
raise ValidationError("invalid child task plan") from exc
child_plan.parent_task_id = task_id
child_plan = bind_task_scope(
child_plan, self.policy.actor_context(actor), public_create=False
)
child = self.delegation.delegate(task_id, child_plan, caller=actor)
return child.to_dict()
def get_events(self, task_id: str, actor: str | None = None) -> list[dict[str, Any]]:
if actor is not None:
self._authorized_task(task_id, actor, "audit")
else:
self.store.get_task(task_id)
self.policy.assert_allowed("audit", actor, task=self.store.get_task(task_id))
self.store.get_task(task_id)
return self.store.list_events(task_id)
def get_evidence(self, task_id: str, actor: str | None = None) -> list[dict[str, Any]]:
if actor is not None:
self._authorized_task(task_id, actor, "audit")
else:
self.store.get_task(task_id)
self.policy.assert_allowed("audit", actor, task=self.store.get_task(task_id))
self.store.get_task(task_id)
return self.evidence.list(task_id)
def get_children(self, task_id: str, actor: str | None = None) -> list[str]:
if actor is not None:
self._authorized_task(task_id, actor, "read")
else:
self.store.get_task(task_id)
self.policy.assert_allowed("read", actor, task=self.store.get_task(task_id))
self.store.get_task(task_id)
return self.store.list_children(task_id)
@@ -532,11 +506,7 @@ class TaskPlaneRequestHandler(BaseHTTPRequestHandler):
):
raise ForbiddenError("issuer identity mismatch")
body["issuer"] = {"type": "authenticated", "id": identity}
return 201, self.service.create_task(
body or {},
headers.get("idempotency-key"),
actor_identity=identity,
)
return 201, self.service.create_task(body or {}, headers.get("idempotency-key"))
if body.get("actor_identity") not in {None, identity}:
raise ForbiddenError("actor identity mismatch")
body["actor_identity"] = identity
-5
View File
@@ -106,8 +106,3 @@ class DurableOutboxDispatcher:
receipt=str(receipt) if receipt else None,
error=None if receipt else f"{adapter.name} receipt unavailable",
)
# R4.2 P0-B compatibility adapter.
def to_handoff_envelope(event, *, target, sequence=1, idempotency_key=None, **scope):
from message_handoff_adapters import taskplane_event_to_envelope
return taskplane_event_to_envelope(event, target=target, sequence=sequence, idempotency_key=idempotency_key, **scope)
-27
View File
@@ -13,8 +13,6 @@ from dataclasses import dataclass, field
from datetime import datetime, timezone
from typing import Any
from .scope import normalize_task_scope
SENSITIVE_KEYS = {
"secret",
@@ -68,12 +66,6 @@ TASK_PLAN_KEYS = {
"allowed_input_paths",
"allowed_output_paths",
"output_policy",
"scope_type",
"tenant_id",
"user_id",
"project_id",
"conversation_id",
"run_id",
}
@@ -245,12 +237,6 @@ class TaskPlan:
schema_version: int = 1
task_id: str | None = None
task_type: str = "code.change"
scope_type: str = "legacy"
tenant_id: str | None = None
user_id: str | None = None
project_id: str | None = None
conversation_id: str | None = None
run_id: str | None = None
issuer: dict[str, Any] = field(default_factory=dict)
intent: str | None = None
required_capabilities: list[str] = field(default_factory=list)
@@ -325,17 +311,10 @@ class TaskPlan:
raise ValueError("credential-secret requires metadata-only logging")
if output_policy != "hash-count-status-only":
raise ValueError("credential-secret requires restricted output")
scope = normalize_task_scope(data)
return cls(
schema_version=schema_version,
task_id=validate_identifier(data["task_id"], "task_id"),
task_type=validate_type(data.get("task_type", "code.change"), "task_type"),
scope_type=scope.scope_type,
tenant_id=scope.tenant_id,
user_id=scope.user_id,
project_id=scope.project_id,
conversation_id=scope.conversation_id,
run_id=scope.run_id,
issuer=issuer,
intent=data.get("intent"),
required_capabilities=_normalize_list(data.get("required_capabilities", [])),
@@ -369,12 +348,6 @@ class TaskPlan:
"schema_version": self.schema_version,
"task_id": self.task_id,
"task_type": self.task_type,
"scope_type": self.scope_type,
"tenant_id": self.tenant_id,
"user_id": self.user_id,
"project_id": self.project_id,
"conversation_id": self.conversation_id,
"run_id": self.run_id,
"issuer": self.issuer,
"intent": self.intent,
"required_capabilities": list(self.required_capabilities),
+1 -20
View File
@@ -3,11 +3,10 @@
from __future__ import annotations
from dataclasses import dataclass
from typing import Any, Callable, Mapping
from typing import Callable
from .errors import ForbiddenError, LockedError
from .models import ProbeEvidence, ResultEnvelope, TaskRecord
from .scope import TaskActorContext, actor_context as build_actor_context
RISK_RANK = {"low": 1, "medium": 2, "high": 3}
@@ -94,10 +93,6 @@ class PolicyEngine:
result_signature_verifier: Callable[[ResultEnvelope], bool] | None = None,
probe_attestation_verifier: Callable[[ProbeEvidence], bool] | None = None,
identity_capabilities: dict[str, set[str]] | None = None,
identity_actor_contexts: Mapping[
str, TaskActorContext | Mapping[str, Any]
]
| None = None,
admin_identities: set[str] | None = None,
readonly_worker_execution_enabled: bool = False,
readonly_worker_profiles: list[ReadonlyWorkerProfile] | None = None,
@@ -114,26 +109,12 @@ class PolicyEngine:
identity: set(capabilities)
for identity, capabilities in (identity_capabilities or {}).items()
}
self.identity_actor_contexts = dict(identity_actor_contexts or {})
self.admin_identities = set(admin_identities or ())
self.readonly_worker_execution_enabled = readonly_worker_execution_enabled
self.readonly_worker_profiles = {
profile.identity: profile for profile in (readonly_worker_profiles or [])
}
def actor_context(self, identity: str) -> TaskActorContext:
configured = self.identity_actor_contexts.get(
identity, self.identity_actor_contexts.get("*")
)
capabilities = set(
self.identity_capabilities.get(
identity, self.identity_capabilities.get("*", set())
)
)
if identity in self.admin_identities:
capabilities.add("task.admin")
return build_actor_context(identity, configured, capabilities)
def _readonly_worker_allows(
self, action: str, actor: str, task: TaskRecord | None
) -> bool:
-181
View File
@@ -1,181 +0,0 @@
"""Trusted TaskPlane scope model and SQL authorization helpers."""
from __future__ import annotations
from dataclasses import dataclass, replace
from typing import Any, Mapping
from .errors import ForbiddenError, ValidationError
SCOPE_FIELDS = ("tenant_id", "user_id", "project_id", "conversation_id", "run_id")
PRIVILEGED_CAPABILITIES = frozenset(
{"*", "admin", "system.admin", "task.admin", "task.read.all"}
)
def _value(value: Any, field: str) -> Any:
if hasattr(value, "task_plan"):
value = value.task_plan
if isinstance(value, Mapping):
return value.get(field)
return getattr(value, field, None)
def _text(value: Any) -> str | None:
if value is None:
return None
result = str(value).strip()
return result or None
@dataclass(frozen=True)
class TaskScope:
scope_type: str
tenant_id: str | None = None
user_id: str | None = None
project_id: str | None = None
conversation_id: str | None = None
run_id: str | None = None
@dataclass(frozen=True)
class TaskActorContext:
identity: str
scope_type: str = "legacy"
tenant_id: str | None = None
user_id: str | None = None
project_id: str | None = None
conversation_id: str | None = None
run_id: str | None = None
capabilities: frozenset[str] = frozenset()
def normalize_task_scope(value: Any) -> TaskScope:
fields = {field: _text(_value(value, field)) for field in SCOPE_FIELDS}
declared = _text(_value(value, "scope_type"))
if declared is None:
declared = "tenant" if fields["tenant_id"] else "legacy"
if declared == "tenant":
if fields["tenant_id"] is None:
raise ValidationError(
"tenant scope requires tenant_id", code="TASK_SCOPE_TENANT_ID_REQUIRED"
)
elif declared == "legacy":
if any(fields.values()):
raise ValidationError(
"legacy scope cannot contain scoped ownership",
code="TASK_SCOPE_LEGACY_WITH_SCOPED_FIELDS",
)
elif declared == "system":
if any(fields[field] for field in ("tenant_id", "user_id", "project_id", "conversation_id")):
raise ValidationError(
"system scope cannot contain tenant ownership",
code="TASK_SCOPE_SYSTEM_WITH_TENANT_FIELDS",
)
else:
raise ValidationError("invalid task scope type", code="TASK_SCOPE_TYPE_INVALID")
return TaskScope(declared, **fields)
def actor_is_privileged(actor: TaskActorContext) -> bool:
return bool(actor.capabilities & PRIVILEGED_CAPABILITIES)
def actor_context(
identity: str,
configured: TaskActorContext | Mapping[str, Any] | None,
capabilities: set[str] | frozenset[str],
) -> TaskActorContext:
if isinstance(configured, TaskActorContext):
context = configured
else:
data = dict(configured or {})
data.pop("identity", None)
data.pop("capabilities", None)
context = TaskActorContext(identity=str(identity), **data)
scope = normalize_task_scope(context)
return replace(
context,
identity=str(identity),
scope_type=scope.scope_type,
tenant_id=scope.tenant_id,
user_id=scope.user_id,
project_id=scope.project_id,
conversation_id=scope.conversation_id,
run_id=scope.run_id,
capabilities=frozenset(str(item) for item in capabilities),
)
def bind_task_scope(plan: Any, actor: TaskActorContext, *, public_create: bool) -> Any:
requested = normalize_task_scope(plan)
if actor_is_privileged(actor):
trusted = normalize_task_scope(actor)
if requested.scope_type == "legacy" and trusted.scope_type != "legacy":
plan.scope_type = trusted.scope_type
for field in SCOPE_FIELDS:
setattr(plan, field, getattr(actor, field))
return plan
trusted = normalize_task_scope(actor)
if public_create and trusted.scope_type == "legacy":
raise ForbiddenError(
"authenticated identity has no tenant scope",
code="TASK_ACTOR_SCOPE_UNAVAILABLE",
)
if trusted.scope_type != "tenant":
if requested.scope_type == "legacy":
plan.scope_type = trusted.scope_type
for field in SCOPE_FIELDS:
setattr(plan, field, getattr(actor, field))
return plan
if requested.scope_type != trusted.scope_type:
raise ForbiddenError("task scope mismatch", code="TASK_SCOPE_MISMATCH_DENY")
return plan
if requested.scope_type not in {"legacy", "tenant"}:
raise ForbiddenError("task scope mismatch", code="TASK_SCOPE_MISMATCH_DENY")
updates: dict[str, Any] = {"scope_type": "tenant"}
for field in SCOPE_FIELDS:
declared = getattr(requested, field)
authoritative = getattr(actor, field)
if declared is not None and declared != authoritative:
raise ForbiddenError(
"task scope mismatch",
code="TASK_SCOPE_MISMATCH_DENY",
details={"field": field},
)
updates[field] = authoritative
for field, item in updates.items():
setattr(plan, field, item)
return plan
def actor_scope_sql(actor: TaskActorContext | None) -> tuple[str, tuple[Any, ...]]:
if actor is None or actor_is_privileged(actor):
return "", ()
scope = normalize_task_scope(actor)
if scope.scope_type == "legacy":
return "scope_type = 'legacy'", ()
if scope.scope_type == "system":
return "scope_type = 'system'", ()
clauses = ["scope_type = 'tenant'", "tenant_id = ?"]
params: list[Any] = [scope.tenant_id]
for field in ("user_id", "project_id", "conversation_id", "run_id"):
value = getattr(scope, field)
if value is None:
clauses.append(f"{field} IS NULL")
else:
clauses.append(f"({field} IS NULL OR {field} = ?)")
params.append(value)
return " AND ".join(clauses), tuple(params)
def task_scope_values(value: Any) -> tuple[str | None, ...]:
scope = normalize_task_scope(value)
return (
scope.scope_type,
scope.tenant_id,
scope.user_id,
scope.project_id,
scope.conversation_id,
scope.run_id,
)
+11 -95
View File
@@ -32,7 +32,6 @@ from .models import (
redact_value,
validate_identifier,
)
from .scope import TaskActorContext, actor_scope_sql, task_scope_values
EventEnvelope = tuple[str, str, str, str, str, float]
@@ -135,12 +134,6 @@ class TaskPlaneStore:
status TEXT NOT NULL,
task_json TEXT NOT NULL,
task_type TEXT NOT NULL,
scope_type TEXT NOT NULL DEFAULT 'legacy',
tenant_id TEXT,
user_id TEXT,
project_id TEXT,
conversation_id TEXT,
run_id TEXT,
root_task_id TEXT NOT NULL,
parent_task_id TEXT,
correlation_id TEXT,
@@ -279,21 +272,6 @@ class TaskPlaneStore:
)
"""
)
self._ensure_columns(
"tasks",
{
"scope_type": "TEXT NOT NULL DEFAULT 'legacy'",
"tenant_id": "TEXT",
"user_id": "TEXT",
"project_id": "TEXT",
"conversation_id": "TEXT",
"run_id": "TEXT",
},
)
self.conn.execute(
"UPDATE tasks SET scope_type='legacy' "
"WHERE scope_type IS NULL OR TRIM(scope_type)=''"
)
self._ensure_columns(
"workers",
{
@@ -321,14 +299,6 @@ class TaskPlaneStore:
)
self.conn.execute("CREATE INDEX IF NOT EXISTS ix_tasks_status ON tasks(status)")
self.conn.execute("CREATE INDEX IF NOT EXISTS ix_tasks_parent ON tasks(parent_task_id)")
self.conn.execute(
"CREATE INDEX IF NOT EXISTS ix_tasks_scope_tenant_project "
"ON tasks(scope_type, tenant_id, project_id)"
)
self.conn.execute(
"CREATE INDEX IF NOT EXISTS ix_tasks_scope_owner "
"ON tasks(scope_type, tenant_id, user_id, conversation_id, run_id)"
)
self.conn.execute("CREATE INDEX IF NOT EXISTS ix_workers_status ON workers(status)")
self.conn.execute("CREATE INDEX IF NOT EXISTS ix_claims_task ON claims(task_id)")
self.conn.execute(
@@ -377,42 +347,6 @@ class TaskPlaneStore:
raise NotFoundError("task")
task_json = json.loads(row["task_json"])
task_plan = TaskPlan.from_dict(task_json)
stored_scope = tuple(
row[field]
for field in (
"scope_type",
"tenant_id",
"user_id",
"project_id",
"conversation_id",
"run_id",
)
)
json_has_scope = any(
field in task_json
for field in (
"scope_type",
"tenant_id",
"user_id",
"project_id",
"conversation_id",
"run_id",
)
)
if json_has_scope and stored_scope != task_scope_values(task_plan):
raise ConflictError(
"task scope persistence mismatch",
code="TASK_SCOPE_PERSISTENCE_MISMATCH",
)
if not json_has_scope:
(
task_plan.scope_type,
task_plan.tenant_id,
task_plan.user_id,
task_plan.project_id,
task_plan.conversation_id,
task_plan.run_id,
) = stored_scope
task_plan.parent_task_id = row["parent_task_id"]
task_plan.correlation_id = row["correlation_id"]
return TaskRecord(
@@ -455,7 +389,6 @@ class TaskPlaneStore:
task_id = task_plan.task_id or f"task-{uuid.uuid4()}"
task_plan.task_id = task_id
task_json = redact_value(task_plan.to_dict(redact=False))
scope_values = task_scope_values(task_plan)
now = _now()
with self._transaction():
if idempotency_key:
@@ -464,11 +397,7 @@ class TaskPlaneStore:
(idempotency_key,),
).fetchone()
if existing:
existing_plan = TaskPlan.from_dict(
json.loads(existing["task_json"])
)
existing_json = redact_value(existing_plan.to_dict(redact=False))
if _canonical(existing_json) != _canonical(task_json):
if _canonical(json.loads(existing["task_json"])) != _canonical(task_json):
raise ConflictError("idempotency key payload mismatch")
return self.get_task(existing["task_id"])
root_task_id = task_id
@@ -483,18 +412,14 @@ class TaskPlaneStore:
self.conn.execute(
"""
INSERT INTO tasks
(task_id, revision, status, task_json, task_type,
scope_type, tenant_id, user_id, project_id, conversation_id, run_id,
root_task_id, parent_task_id, correlation_id, idempotency_key,
created_at, updated_at, failure_reason)
VALUES (?, 1, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, NULL)
(task_id, revision, status, task_json, task_type, root_task_id, parent_task_id, correlation_id, idempotency_key, created_at, updated_at, failure_reason)
VALUES (?, 1, ?, ?, ?, ?, ?, ?, ?, ?, ?, NULL)
""",
(
task_id,
"CREATED",
json.dumps(task_json, ensure_ascii=False),
task_plan.task_type,
*scope_values,
root_task_id,
task_plan.parent_task_id,
task_plan.correlation_id,
@@ -526,30 +451,21 @@ class TaskPlaneStore:
)
return self.get_task(task.task_id)
def get_task(
self, task_id: str, actor_context: TaskActorContext | None = None
) -> TaskRecord:
def get_task(self, task_id: str) -> TaskRecord:
task_id = _validated_id(task_id, "task_id")
predicate, scope_params = actor_scope_sql(actor_context)
sql = "SELECT * FROM tasks WHERE task_id=?"
if predicate:
sql += f" AND ({predicate})"
with self._lock:
row = self.conn.execute(sql, (task_id, *scope_params)).fetchone()
row = self.conn.execute(
"SELECT * FROM tasks WHERE task_id=?", (task_id,)
).fetchone()
if row is None:
raise NotFoundError(f"task {task_id} not found")
return self._row_to_task(row)
def list_tasks(
self, actor_context: TaskActorContext | None = None
) -> list[TaskRecord]:
predicate, scope_params = actor_scope_sql(actor_context)
sql = "SELECT * FROM tasks"
if predicate:
sql += f" WHERE ({predicate})"
sql += " ORDER BY created_at DESC"
def list_tasks(self) -> list[TaskRecord]:
with self._lock:
rows = self.conn.execute(sql, scope_params).fetchall()
rows = self.conn.execute(
"SELECT * FROM tasks ORDER BY created_at DESC"
).fetchall()
return [self._row_to_task(row) for row in rows]
def get_task_budget(self, task_id: str) -> Budget:
@@ -1,759 +0,0 @@
"""Scope-bound governance for traces, Tool/MCP calls, and credentials.
This module deliberately stores references and audit metadata only. Secret
material remains in the configured credential provider boundary.
"""
from __future__ import annotations
from dataclasses import asdict, dataclass, field
from datetime import datetime, timezone
from contextlib import contextmanager
import hashlib
import json
import re
import sqlite3
from pathlib import Path
from typing import Any, Iterable, Mapping
SCOPE_FIELDS = ("tenant_id", "user_id", "project_id", "conversation_id", "run_id")
PRIVILEGED_CAPABILITIES = frozenset({"*", "admin", "system.admin", "governance.admin"})
REFERENCE_RE = re.compile(r"^(systemd-credential|vault|secret-manager):[A-Za-z0-9][A-Za-z0-9._:/#-]{0,255}$")
IDENTIFIER_RE = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._:-]{0,179}$")
class GovernanceError(RuntimeError):
def __init__(self, code: str, *, field: str | None = None) -> None:
super().__init__(code if field is None else f"{code}:{field}")
self.code = code
self.field = field
def _clean(value: Any) -> str | None:
if value is None:
return None
text = str(value).strip()
return text or None
def _scope_tuple(value: Iterable[str] | str | None) -> tuple[str, ...]:
if value is None:
return ()
if isinstance(value, str):
value = value.split(",")
return tuple(sorted({_clean(item) for item in value if _clean(item)}))
def _canonical(value: Any) -> str:
return json.dumps(value, sort_keys=True, separators=(",", ":"), ensure_ascii=False, default=str)
def _sha(value: Any) -> str:
return hashlib.sha256(_canonical(value).encode("utf-8")).hexdigest()
def _now() -> str:
return datetime.now(timezone.utc).isoformat()
def _identifier(value: Any, code: str) -> str:
text = _clean(value)
if text is None or IDENTIFIER_RE.fullmatch(text) is None:
raise GovernanceError(code)
return text
@dataclass(frozen=True)
class GovernanceScope:
"""Explicit resource scope; system/global resources never get fake tenants."""
scope_type: str
tenant_id: str | None = None
user_id: str | None = None
project_id: str | None = None
conversation_id: str | None = None
run_id: str | None = None
def __post_init__(self) -> None:
object.__setattr__(self, "scope_type", (_clean(self.scope_type) or "").lower())
for name in SCOPE_FIELDS:
object.__setattr__(self, name, _clean(getattr(self, name)))
if self.scope_type == "tenant":
if self.tenant_id is None:
raise GovernanceError("GOVERNANCE_TENANT_ID_REQUIRED")
elif self.scope_type in {"system", "global"}:
if any(getattr(self, name) is not None for name in SCOPE_FIELDS):
raise GovernanceError("GOVERNANCE_NON_TENANT_SCOPE_INVALID")
elif self.scope_type == "legacy":
if any(getattr(self, name) is not None for name in SCOPE_FIELDS):
raise GovernanceError("GOVERNANCE_LEGACY_SCOPE_INVALID")
else:
raise GovernanceError("GOVERNANCE_SCOPE_TYPE_INVALID")
@classmethod
def from_value(cls, value: Any) -> "GovernanceScope":
def get(name: str) -> Any:
return value.get(name) if isinstance(value, Mapping) else getattr(value, name, None)
declared = _clean(get("scope_type"))
tenant_id = _clean(get("tenant_id"))
return cls(
scope_type=declared or ("tenant" if tenant_id else "legacy"),
tenant_id=tenant_id,
user_id=get("user_id"),
project_id=get("project_id"),
conversation_id=get("conversation_id"),
run_id=get("run_id"),
)
def as_dict(self) -> dict[str, Any]:
return asdict(self)
@dataclass(frozen=True)
class GovernanceActor:
identity: str
scope: GovernanceScope
capabilities: tuple[str, ...] = ()
def __post_init__(self) -> None:
object.__setattr__(self, "identity", _identifier(self.identity, "GOVERNANCE_ACTOR_INVALID"))
object.__setattr__(self, "capabilities", _scope_tuple(self.capabilities))
@classmethod
def from_value(cls, value: Any) -> "GovernanceActor":
if isinstance(value, cls):
return value
if not isinstance(value, Mapping):
raise GovernanceError("GOVERNANCE_ACTOR_INVALID")
return cls(
identity=value.get("identity") or value.get("actor_id") or value.get("user_id"),
scope=GovernanceScope.from_value(value),
capabilities=_scope_tuple(value.get("capabilities") or value.get("capability_scope")),
)
@property
def privileged(self) -> bool:
return bool(set(self.capabilities) & PRIVILEGED_CAPABILITIES)
def authorize_scope(actor: GovernanceActor | Mapping[str, Any], resource: GovernanceScope | Mapping[str, Any]) -> None:
actor = GovernanceActor.from_value(actor)
resource = resource if isinstance(resource, GovernanceScope) else GovernanceScope.from_value(resource)
if actor.privileged:
return
if resource.scope_type in {"system", "global"}:
raise GovernanceError("GOVERNANCE_SYSTEM_SCOPE_AUTHZ_DENY")
if resource.scope_type == "legacy":
if actor.scope.scope_type != "legacy":
raise GovernanceError("GOVERNANCE_LEGACY_SCOPE_AUTHZ_DENY")
return
if actor.scope.scope_type != "tenant":
raise GovernanceError("GOVERNANCE_SCOPE_MISMATCH_DENY", field="scope_type")
for name in SCOPE_FIELDS:
expected = getattr(resource, name)
actual = getattr(actor.scope, name)
if expected is not None and expected != actual:
raise GovernanceError("GOVERNANCE_SCOPE_MISMATCH_DENY", field=name)
@dataclass(frozen=True)
class TraceContext:
trace_id: str
span_id: str
scope: GovernanceScope
parent_span_id: str | None = None
correlation_id: str | None = None
causation_id: str | None = None
task_id: str | None = None
claim_id: str | None = None
message_id: str | None = None
def __post_init__(self) -> None:
object.__setattr__(self, "trace_id", _identifier(self.trace_id, "TRACE_ID_INVALID"))
object.__setattr__(self, "span_id", _identifier(self.span_id, "TRACE_SPAN_ID_INVALID"))
for name in ("parent_span_id", "correlation_id", "causation_id", "task_id", "claim_id", "message_id"):
value = _clean(getattr(self, name))
if value is not None and IDENTIFIER_RE.fullmatch(value) is None:
raise GovernanceError("TRACE_LINEAGE_IDENTIFIER_INVALID", field=name)
object.__setattr__(self, name, value)
def child(self, *, span_id: str, actor: GovernanceActor | Mapping[str, Any], **lineage: Any) -> "TraceContext":
authorize_scope(actor, self.scope)
for name in ("trace_id", "task_id", "claim_id", "message_id", "correlation_id"):
supplied = _clean(lineage.get(name))
expected = getattr(self, name)
if supplied is not None and expected is not None and supplied != expected:
raise GovernanceError("TRACE_LINEAGE_CONTINUITY", field=name)
requested_scope = lineage.pop("scope", self.scope)
requested_scope = requested_scope if isinstance(requested_scope, GovernanceScope) else GovernanceScope.from_value(requested_scope)
if requested_scope != self.scope:
raise GovernanceError("TRACE_SCOPE_MISMATCH_DENY")
return TraceContext(
trace_id=self.trace_id,
span_id=span_id,
parent_span_id=self.span_id,
correlation_id=self.correlation_id,
causation_id=_clean(lineage.get("causation_id")) or self.span_id,
task_id=self.task_id,
claim_id=self.claim_id,
message_id=self.message_id,
scope=self.scope,
)
def as_dict(self) -> dict[str, Any]:
return {
"trace_id": self.trace_id,
"span_id": self.span_id,
"parent_span_id": self.parent_span_id,
"correlation_id": self.correlation_id,
"causation_id": self.causation_id,
"task_id": self.task_id,
"claim_id": self.claim_id,
"message_id": self.message_id,
**self.scope.as_dict(),
}
def bind_trace_lineage(
*,
actor: GovernanceActor | Mapping[str, Any],
task: Any,
message: Any,
trace_id: str,
span_id: str,
parent: TraceContext | None = None,
) -> TraceContext:
actor = GovernanceActor.from_value(actor)
task_scope = GovernanceScope.from_value(task)
message_scope = GovernanceScope.from_value(message)
if task_scope != message_scope:
raise GovernanceError("TRACE_SCOPE_MISMATCH_DENY")
authorize_scope(actor, task_scope)
def get(value: Any, name: str) -> Any:
return value.get(name) if isinstance(value, Mapping) else getattr(value, name, None)
task_id = _clean(get(task, "task_id"))
message_task = _clean(get(message, "task_id"))
if task_id and message_task and task_id != message_task:
raise GovernanceError("TRACE_LINEAGE_CONTINUITY", field="task_id")
context = TraceContext(
trace_id=trace_id,
span_id=span_id,
parent_span_id=parent.span_id if parent else None,
correlation_id=_clean(get(task, "correlation_id")) or _clean(get(message, "correlation_id")),
causation_id=_clean(get(message, "causation_id")) or (parent.span_id if parent else None),
task_id=task_id or message_task,
claim_id=_clean(get(task, "claim_id")) or _clean(get(message, "claim_id")),
message_id=_clean(get(message, "envelope_id")) or _clean(get(message, "id")),
scope=task_scope,
)
if parent is not None:
if parent.trace_id != context.trace_id or parent.scope != context.scope:
raise GovernanceError("TRACE_LINEAGE_CONTINUITY")
if parent.task_id and context.task_id and parent.task_id != context.task_id:
raise GovernanceError("TRACE_LINEAGE_CONTINUITY", field="task_id")
return context
@dataclass(frozen=True)
class ToolDescriptor:
name: str
kind: str
scope: GovernanceScope
required_capabilities: tuple[str, ...] = ()
credential_ref: str | None = None
def __post_init__(self) -> None:
object.__setattr__(self, "name", _identifier(self.name, "TOOL_NAME_INVALID"))
object.__setattr__(self, "kind", (_clean(self.kind) or "").lower())
if self.kind not in {"tool", "mcp"}:
raise GovernanceError("TOOL_KIND_INVALID")
object.__setattr__(self, "required_capabilities", _scope_tuple(self.required_capabilities))
if self.credential_ref is not None and REFERENCE_RE.fullmatch(self.credential_ref) is None:
raise GovernanceError("CREDENTIAL_SECRET_EXPOSURE_DENY")
@dataclass(frozen=True)
class CredentialGrant:
grant_id: str
credential_ref: str
scope: GovernanceScope
operations: tuple[str, ...]
task_id: str | None = None
tool_name: str | None = None
expires_at: str | None = None
def __post_init__(self) -> None:
object.__setattr__(self, "grant_id", _identifier(self.grant_id, "CREDENTIAL_GRANT_ID_INVALID"))
if REFERENCE_RE.fullmatch(self.credential_ref or "") is None:
raise GovernanceError("CREDENTIAL_SECRET_EXPOSURE_DENY")
object.__setattr__(self, "operations", _scope_tuple(self.operations))
if not self.operations:
raise GovernanceError("CREDENTIAL_OPERATIONS_REQUIRED")
if self.expires_at is not None:
parsed = datetime.fromisoformat(self.expires_at.replace("Z", "+00:00"))
if parsed.tzinfo is None:
raise GovernanceError("CREDENTIAL_EXPIRY_INVALID")
def authorize(
self,
actor: GovernanceActor | Mapping[str, Any],
*,
operation: str,
task_id: str | None = None,
tool_name: str | None = None,
now: datetime | None = None,
) -> str:
try:
authorize_scope(actor, self.scope)
except GovernanceError as exc:
raise GovernanceError("CREDENTIAL_SCOPE_DENY", field=exc.field) from exc
if operation not in self.operations:
raise GovernanceError("CREDENTIAL_SCOPE_DENY", field="operation")
if self.task_id is not None and self.task_id != _clean(task_id):
raise GovernanceError("CREDENTIAL_SCOPE_DENY", field="task_id")
if self.tool_name is not None and self.tool_name != _clean(tool_name):
raise GovernanceError("CREDENTIAL_SCOPE_DENY", field="tool_name")
if self.expires_at is not None:
current = (now or datetime.now(timezone.utc)).astimezone(timezone.utc)
expiry = datetime.fromisoformat(self.expires_at.replace("Z", "+00:00")).astimezone(timezone.utc)
if current >= expiry:
raise GovernanceError("CREDENTIAL_SCOPE_DENY", field="expires_at")
return self.credential_ref
def public(self) -> dict[str, Any]:
return {
"grant_id": self.grant_id,
"credential_ref": self.credential_ref,
"operations": list(self.operations),
"task_id": self.task_id,
"tool_name": self.tool_name,
"expires_at": self.expires_at,
**self.scope.as_dict(),
}
_SCHEMA = """
CREATE TABLE IF NOT EXISTS governance_schema_metadata (
singleton INTEGER PRIMARY KEY CHECK(singleton=1),
schema_version INTEGER NOT NULL
);
CREATE TABLE IF NOT EXISTS governed_tools (
name TEXT PRIMARY KEY,
kind TEXT NOT NULL,
scope_json TEXT NOT NULL,
required_capabilities_json TEXT NOT NULL,
credential_ref TEXT,
descriptor_sha256 TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS governed_trace_spans (
trace_id TEXT NOT NULL,
span_id TEXT NOT NULL,
parent_span_id TEXT,
context_json TEXT NOT NULL,
context_sha256 TEXT NOT NULL,
created_at TEXT NOT NULL,
PRIMARY KEY(trace_id, span_id)
);
CREATE TABLE IF NOT EXISTS governed_credential_grants (
grant_id TEXT PRIMARY KEY,
grant_json TEXT NOT NULL,
grant_sha256 TEXT NOT NULL,
created_at TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS governed_tool_invocations (
invocation_key TEXT PRIMARY KEY,
request_sha256 TEXT NOT NULL,
tool_name TEXT NOT NULL,
task_id TEXT NOT NULL,
claim_id TEXT NOT NULL,
trace_id TEXT NOT NULL,
span_id TEXT NOT NULL,
status TEXT NOT NULL,
receipt_sha256 TEXT,
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS governance_audit_evidence (
sequence INTEGER PRIMARY KEY AUTOINCREMENT,
evidence_id TEXT NOT NULL UNIQUE,
action TEXT NOT NULL,
actor_identity TEXT NOT NULL,
trace_id TEXT,
span_id TEXT,
resource_id TEXT NOT NULL,
outcome TEXT NOT NULL,
details_json TEXT NOT NULL,
previous_sha256 TEXT NOT NULL,
evidence_sha256 TEXT NOT NULL,
created_at TEXT NOT NULL
);
"""
class GovernanceStore:
def __init__(self, path: str | Path) -> None:
self.path = Path(path)
self.path.parent.mkdir(parents=True, exist_ok=True)
with self._connection() as connection:
has_metadata = connection.execute(
"SELECT 1 FROM sqlite_master WHERE type='table' AND name='governance_schema_metadata'"
).fetchone()
if has_metadata:
row = connection.execute(
"SELECT schema_version FROM governance_schema_metadata WHERE singleton=1"
).fetchone()
if row is not None and int(row[0]) != 1:
raise GovernanceError("GOVERNANCE_SCHEMA_VERSION_UNSUPPORTED")
connection.executescript(_SCHEMA)
row = connection.execute(
"SELECT schema_version FROM governance_schema_metadata WHERE singleton=1"
).fetchone()
if row is None:
connection.execute(
"INSERT INTO governance_schema_metadata(singleton,schema_version) VALUES(1,1)"
)
connection.commit()
def _connect(self) -> sqlite3.Connection:
connection = sqlite3.connect(self.path, timeout=30.0)
connection.row_factory = sqlite3.Row
connection.execute("PRAGMA foreign_keys=ON")
connection.execute("PRAGMA busy_timeout=30000")
return connection
@contextmanager
def _connection(self):
connection = self._connect()
try:
yield connection
connection.commit()
except Exception:
connection.rollback()
raise
finally:
connection.close()
def _audit(
self,
connection: sqlite3.Connection,
*,
action: str,
actor: GovernanceActor,
resource_id: str,
outcome: str,
trace: TraceContext | None,
details: Mapping[str, Any],
) -> dict[str, Any]:
previous = connection.execute(
"SELECT evidence_sha256 FROM governance_audit_evidence ORDER BY sequence DESC LIMIT 1"
).fetchone()
previous_sha = str(previous[0]) if previous else ""
created_at = _now()
material = {
"action": action,
"actor_identity": actor.identity,
"trace_id": trace.trace_id if trace else None,
"span_id": trace.span_id if trace else None,
"resource_id": resource_id,
"outcome": outcome,
"details": dict(details),
"previous_sha256": previous_sha,
"created_at": created_at,
}
digest = _sha(material)
evidence_id = "gev-" + digest[:32]
connection.execute(
"""INSERT INTO governance_audit_evidence
(evidence_id,action,actor_identity,trace_id,span_id,resource_id,outcome,
details_json,previous_sha256,evidence_sha256,created_at)
VALUES (?,?,?,?,?,?,?,?,?,?,?)""",
(
evidence_id, action, actor.identity, material["trace_id"], material["span_id"],
resource_id, outcome, _canonical(material["details"]), previous_sha, digest, created_at,
),
)
return {"evidence_id": evidence_id, "evidence_sha256": digest, **material}
def record_trace(self, trace: TraceContext, actor: GovernanceActor | Mapping[str, Any]) -> dict[str, Any]:
actor = GovernanceActor.from_value(actor)
authorize_scope(actor, trace.scope)
context = trace.as_dict()
digest = _sha(context)
with self._connection() as connection:
existing = connection.execute(
"SELECT context_sha256 FROM governed_trace_spans WHERE trace_id=? AND span_id=?",
(trace.trace_id, trace.span_id),
).fetchone()
if existing is not None and existing[0] != digest:
raise GovernanceError("TRACE_LINEAGE_CONFLICT")
if existing is not None:
return {"context": context, "context_sha256": digest, "idempotent": True}
if trace.parent_span_id:
parent = connection.execute(
"SELECT context_json FROM governed_trace_spans WHERE trace_id=? AND span_id=?",
(trace.trace_id, trace.parent_span_id),
).fetchone()
if parent is None:
raise GovernanceError("TRACE_LINEAGE_CONTINUITY", field="parent_span_id")
parent_context = json.loads(parent[0])
if GovernanceScope.from_value(parent_context) != trace.scope:
raise GovernanceError("TRACE_SCOPE_MISMATCH_DENY")
connection.execute(
"INSERT OR IGNORE INTO governed_trace_spans VALUES (?,?,?,?,?,?)",
(trace.trace_id, trace.span_id, trace.parent_span_id, _canonical(context), digest, _now()),
)
evidence = self._audit(
connection, action="TRACE_RECORDED", actor=actor,
resource_id=f"{trace.trace_id}:{trace.span_id}", outcome="ALLOW", trace=trace,
details={"context_sha256": digest},
)
return {"context": context, "context_sha256": digest, "evidence": evidence}
def register_tool(self, descriptor: ToolDescriptor, actor: GovernanceActor | Mapping[str, Any]) -> dict[str, Any]:
actor = GovernanceActor.from_value(actor)
if not actor.privileged:
raise GovernanceError("TOOL_MCP_AUTHZ_DENY")
public = {
"name": descriptor.name,
"kind": descriptor.kind,
"required_capabilities": list(descriptor.required_capabilities),
"credential_ref": descriptor.credential_ref,
**descriptor.scope.as_dict(),
}
digest = _sha(public)
with self._connection() as connection:
existing = connection.execute(
"SELECT descriptor_sha256 FROM governed_tools WHERE name=?", (descriptor.name,)
).fetchone()
if existing is not None and existing[0] != digest:
raise GovernanceError("TOOL_DESCRIPTOR_CONFLICT")
if existing is not None:
return {"descriptor": public, "descriptor_sha256": digest, "idempotent": True}
connection.execute(
"INSERT OR IGNORE INTO governed_tools VALUES (?,?,?,?,?,?)",
(descriptor.name, descriptor.kind, _canonical(descriptor.scope.as_dict()),
_canonical(descriptor.required_capabilities), descriptor.credential_ref, digest),
)
evidence = self._audit(
connection, action="TOOL_REGISTERED", actor=actor, resource_id=descriptor.name,
outcome="ALLOW", trace=None, details={"descriptor_sha256": digest},
)
return {"descriptor": public, "descriptor_sha256": digest, "evidence": evidence}
def authorize_tool(
self,
*,
actor: GovernanceActor | Mapping[str, Any],
tool_name: str,
trace: TraceContext,
requested_scope: GovernanceScope | Mapping[str, Any],
input_digest: str,
invocation_key: str,
) -> dict[str, Any]:
actor = GovernanceActor.from_value(actor)
invocation_key = _identifier(invocation_key, "TOOL_INVOCATION_KEY_INVALID")
if not re.fullmatch(r"[0-9a-f]{64}", input_digest):
raise GovernanceError("TOOL_INPUT_DIGEST_INVALID")
if trace.task_id is None or trace.claim_id is None:
raise GovernanceError("TOOL_MCP_AUTHZ_DENY", field="active_claim")
requested_scope = requested_scope if isinstance(requested_scope, GovernanceScope) else GovernanceScope.from_value(requested_scope)
if requested_scope != trace.scope:
raise GovernanceError("TOOL_SCOPE_PROPAGATION")
authorize_scope(actor, requested_scope)
with self._connection() as connection:
persisted_trace = connection.execute(
"SELECT context_sha256 FROM governed_trace_spans WHERE trace_id=? AND span_id=?",
(trace.trace_id, trace.span_id),
).fetchone()
if persisted_trace is None or persisted_trace[0] != _sha(trace.as_dict()):
raise GovernanceError("TOOL_MCP_AUTHZ_DENY", field="trace")
row = connection.execute("SELECT * FROM governed_tools WHERE name=?", (tool_name,)).fetchone()
if row is None:
raise GovernanceError("TOOL_MCP_AUTHZ_DENY")
descriptor_scope = GovernanceScope.from_value(json.loads(row["scope_json"]))
if descriptor_scope.scope_type == "tenant" and descriptor_scope != requested_scope:
raise GovernanceError("TOOL_MCP_AUTHZ_DENY")
required = set(json.loads(row["required_capabilities_json"]))
if not required.issubset(set(actor.capabilities)):
raise GovernanceError("TOOL_MCP_AUTHZ_DENY")
request_sha = _sha({
"tool_name": tool_name, "input_digest": input_digest,
"task_id": trace.task_id, "claim_id": trace.claim_id,
"trace_id": trace.trace_id, "span_id": trace.span_id,
"scope": requested_scope.as_dict(),
})
replay = connection.execute(
"SELECT * FROM governed_tool_invocations WHERE invocation_key=?",
(invocation_key,),
).fetchone()
if replay is not None:
if replay["request_sha256"] != request_sha:
raise GovernanceError("TOOL_INVOCATION_IDEMPOTENCY_CONFLICT")
if replay["status"] != "VERIFIED":
raise GovernanceError("TOOL_INVOCATION_RECONCILIATION_REQUIRED")
return {
"tool_name": tool_name, "trace_id": trace.trace_id,
"span_id": trace.span_id, "invocation_key": invocation_key,
"status": "VERIFIED", "receipt_sha256": replay["receipt_sha256"],
"idempotent": True,
}
created_at = _now()
connection.execute(
"""INSERT INTO governed_tool_invocations
(invocation_key,request_sha256,tool_name,task_id,claim_id,trace_id,span_id,
status,receipt_sha256,created_at,updated_at)
VALUES(?,?,?,?,?,?,?,?,?,?,?)""",
(invocation_key, request_sha, tool_name, trace.task_id, trace.claim_id,
trace.trace_id, trace.span_id, "AUTHORIZED", None, created_at, created_at),
)
evidence = self._audit(
connection, action="TOOL_AUTHORIZED", actor=actor, resource_id=tool_name,
outcome="ALLOW", trace=trace,
details={"input_sha256": input_digest, "scope_sha256": _sha(requested_scope.as_dict()),
"invocation_key": invocation_key},
)
return {
"tool_name": tool_name,
"trace_id": trace.trace_id,
"span_id": trace.span_id,
"invocation_key": invocation_key,
"status": "AUTHORIZED",
"credential_required": row["credential_ref"] is not None,
"evidence": evidence,
}
def record_tool_receipt(
self,
*,
invocation_key: str,
result_digest: str,
actor: GovernanceActor | Mapping[str, Any],
trace: TraceContext,
) -> dict[str, Any]:
actor = GovernanceActor.from_value(actor)
invocation_key = _identifier(invocation_key, "TOOL_INVOCATION_KEY_INVALID")
if not re.fullmatch(r"[0-9a-f]{64}", result_digest):
raise GovernanceError("TOOL_RECEIPT_DIGEST_INVALID")
authorize_scope(actor, trace.scope)
with self._connection() as connection:
row = connection.execute(
"SELECT * FROM governed_tool_invocations WHERE invocation_key=?",
(invocation_key,),
).fetchone()
if row is None or row["trace_id"] != trace.trace_id or row["span_id"] != trace.span_id:
raise GovernanceError("TOOL_RECEIPT_BINDING_INVALID")
if row["status"] == "VERIFIED":
if row["receipt_sha256"] != result_digest:
raise GovernanceError("TOOL_RECEIPT_CONFLICT")
return {"invocation_key": invocation_key, "status": "VERIFIED", "idempotent": True}
if row["status"] != "AUTHORIZED":
raise GovernanceError("TOOL_INVOCATION_RECONCILIATION_REQUIRED")
connection.execute(
"UPDATE governed_tool_invocations SET status='VERIFIED',receipt_sha256=?,updated_at=? "
"WHERE invocation_key=? AND status='AUTHORIZED'",
(result_digest, _now(), invocation_key),
)
evidence = self._audit(
connection, action="TOOL_RESULT_VERIFIED", actor=actor,
resource_id=row["tool_name"], outcome="ALLOW", trace=trace,
details={"invocation_key": invocation_key, "result_sha256": result_digest},
)
return {"invocation_key": invocation_key, "status": "VERIFIED", "evidence": evidence}
def store_grant(self, grant: CredentialGrant, actor: GovernanceActor | Mapping[str, Any]) -> dict[str, Any]:
actor = GovernanceActor.from_value(actor)
if not actor.privileged:
raise GovernanceError("CREDENTIAL_SCOPE_DENY")
public = grant.public()
digest = _sha(public)
with self._connection() as connection:
existing = connection.execute(
"SELECT grant_sha256 FROM governed_credential_grants WHERE grant_id=?", (grant.grant_id,)
).fetchone()
if existing is not None and existing[0] != digest:
raise GovernanceError("CREDENTIAL_GRANT_CONFLICT")
connection.execute(
"INSERT OR IGNORE INTO governed_credential_grants VALUES (?,?,?,?)",
(grant.grant_id, _canonical(public), digest, _now()),
)
evidence = self._audit(
connection, action="CREDENTIAL_GRANT_STORED", actor=actor,
resource_id=grant.grant_id, outcome="ALLOW", trace=None,
details={"grant_sha256": digest},
)
return {"grant": public, "grant_sha256": digest, "evidence": evidence}
def authorize_credential(
self,
*,
grant_id: str,
actor: GovernanceActor | Mapping[str, Any],
trace: TraceContext,
operation: str,
task_id: str | None,
tool_name: str | None,
now: datetime | None = None,
) -> dict[str, Any]:
actor = GovernanceActor.from_value(actor)
with self._connection() as connection:
row = connection.execute(
"SELECT grant_json FROM governed_credential_grants WHERE grant_id=?", (grant_id,)
).fetchone()
if row is None:
raise GovernanceError("CREDENTIAL_SCOPE_DENY")
raw = json.loads(row[0])
grant = CredentialGrant(
grant_id=raw["grant_id"], credential_ref=raw["credential_ref"],
scope=GovernanceScope.from_value(raw), operations=tuple(raw["operations"]),
task_id=raw.get("task_id"), tool_name=raw.get("tool_name"),
expires_at=raw.get("expires_at"),
)
if trace.scope != grant.scope:
raise GovernanceError("CREDENTIAL_SCOPE_DENY", field="trace_scope")
reference = grant.authorize(
actor, operation=operation, task_id=task_id, tool_name=tool_name, now=now
)
evidence = self._audit(
connection, action="CREDENTIAL_AUTHORIZED", actor=actor,
resource_id=grant_id, outcome="ALLOW", trace=trace,
details={"operation": operation, "task_id": task_id, "tool_name": tool_name,
"credential_ref_sha256": _sha(reference)},
)
return {"credential_ref": reference, "evidence": evidence}
def verify_audit_chain(self, required_actions: Iterable[str] = ()) -> bool:
ordered_actions: list[str] = []
previous = ""
with self._connection() as connection:
rows = connection.execute("SELECT * FROM governance_audit_evidence ORDER BY sequence").fetchall()
for row in rows:
details = json.loads(row["details_json"])
material = {
"action": row["action"], "actor_identity": row["actor_identity"],
"trace_id": row["trace_id"], "span_id": row["span_id"],
"resource_id": row["resource_id"], "outcome": row["outcome"],
"details": details, "previous_sha256": row["previous_sha256"],
"created_at": row["created_at"],
}
if row["previous_sha256"] != previous or row["evidence_sha256"] != _sha(material):
return False
previous = row["evidence_sha256"]
ordered_actions.append(row["action"])
position = 0
for required in required_actions:
try:
position = ordered_actions.index(required, position) + 1
except ValueError:
return False
return True
__all__ = [
"CredentialGrant", "GovernanceActor", "GovernanceError", "GovernanceScope",
"GovernanceStore", "ToolDescriptor", "TraceContext", "authorize_scope",
"bind_trace_lineage",
]
@@ -7,6 +7,7 @@ previous V2R1C7 release available for an explicit, owner-approved rollback.
from __future__ import annotations
import json
import hashlib
import os
import shutil
import sys
@@ -29,6 +30,7 @@ ROLLBACK_ROOT = Path("/var/lib/aios/v2r2c1/rollback")
REQUIRED_MEMBERS = {
"payload/VERSION",
"payload/versions/V2R2C1.json",
"payload/active-runtime/app/deployment_state.py",
"payload/active-runtime/app/v2r1c6_privacy_tools.py",
"payload/active-runtime/app/v2r2c1_health_service.py",
@@ -87,12 +89,65 @@ def execute_request(value: Mapping[str, Any]) -> dict[str, Any]:
_backup(Path("/var/lib/aios/v2r1c7/deployment-state.json"), "v2r1c7-deployment-state.previous")
return base.install_release(_args(), value)
if value.get("operation") == "probe":
return base.probe(_args(), value)
result = base.probe(_args(), value)
if result.get("status") == "ACTIVE_VERIFIED":
result.update(_runtime_registry_gate(value))
return result
if value.get("operation") == "rollback":
return rollback(value)
raise ValueError("unsupported_operation")
def _runtime_registry_gate(value: Mapping[str, Any]) -> dict[str, Any]:
"""Prove the deployed immutable runtime contains the exact M:path blob."""
source_digest = value.get("source_registry_sha256")
source_main = value.get("source_registry_main_oid")
if not source_digest or not source_main:
return {}
source_path = str(value.get("source_registry_path") or f"versions/{VERSION}.json")
member = CURRENT_LINK / source_path
if member.is_symlink() or not member.is_file():
raise RuntimeError("RUNTIME_SOURCE_REGISTRY_MEMBER_MISSING")
raw = member.read_bytes()
digest = hashlib.sha256(raw).hexdigest()
if digest != source_digest:
raise RuntimeError("RUNTIME_SOURCE_REGISTRY_DIGEST_MISMATCH")
try:
source = json.loads(raw.decode("utf-8"))
except (UnicodeDecodeError, json.JSONDecodeError) as exc:
raise RuntimeError("RUNTIME_SOURCE_REGISTRY_INVALID") from exc
expected = {
"version": VERSION,
"repository": "aios/aios",
"tag_oid": value.get("tag_oid"),
"target_oid": value.get("target_oid"),
"peeled_target_oid": value.get("target_oid"),
"evidence_closure_oid": value.get("evidence_closure_oid"),
"approval_plan_id": value.get("approval_plan_id"),
}
if not isinstance(source, dict) or any(source.get(key) != expected_value for key, expected_value in expected.items()):
raise RuntimeError("RUNTIME_SOURCE_REGISTRY_IDENTITY_MISMATCH")
if source.get("tag_object") != expected["tag_oid"]:
raise RuntimeError("RUNTIME_SOURCE_REGISTRY_TAG_OBJECT_MISMATCH")
version_name = f"{VERSION}.json"
return {
"runtime_registry_versions": [version_name],
"attested_projection_versions": [version_name],
"effective_registry_versions": [version_name],
# The adapter can only inspect the deployed immutable runtime. It
# must not claim convergence with mutable authoritative Git; the
# executor performs that independent M/C read after deployment.
"source_registry_converged": "not_checked",
"source_registry_convergence_error": "executor_must_recheck_authoritative_git",
"runtime_registry_converged": True,
"runtime_bundle_contains_current_version": True,
"runtime_bundle_contains_version": True,
"registry_consistency_status": "RUNTIME_ONLY_VERIFIED",
"runtime_source_registry_main_oid": source_main,
"runtime_source_registry_sha256": digest,
}
def rollback(value: Mapping[str, Any]) -> dict[str, Any]:
"""Explicit rollback only; this adapter never invokes it automatically."""
import subprocess
+982
View File
@@ -0,0 +1,982 @@
"""Release registry visibility and source-convergence gates.
Two invariants are enforced:
1. listVersions may overlay exactly the current Production Baseline-attested
projection onto the immutable source bundle. Arbitrary projection files
are not surfaced.
2. release_metadata_adapter.baseline_apply may not finalize a production
baseline unless the governed repository/main already contains a source
versions/<version>.json entry matching the release identity.
The source convergence gate intentionally checks release identity, not
post-deployment runtime fields. This allows the source entry to be committed
before deployment while still preventing PRODUCTION_DEPLOYED from being
written for an uncommitted release identity.
"""
from __future__ import annotations
import hashlib
import json
import os
import re
import stat
import subprocess
from datetime import datetime, timezone
from pathlib import Path
from typing import Any
_VERSION_RE = re.compile(r"^V[0-9]+R[0-9]+C[0-9]+(?:-[A-Za-z0-9_.-]+)?$")
_OID_RE = re.compile(r"^[0-9a-f]{40}$")
_PLAN_RE = re.compile(r"^sha256:[0-9a-f]{64}$")
VERSION_RE = _VERSION_RE
OID_RE = _OID_RE
SOURCE_REGISTRY_SCHEMAS = frozenset(
{"aios.source-registry.v1", "aios.production-registry.v2"}
)
SOURCE_REGISTRY_FIELDS = (
"version",
"tag_oid",
"target_oid",
"peeled_target_oid",
"evidence_closure_oid",
"approval_plan_id",
)
REPOSITORY_IDENTITY = "aios/aios"
DEFAULT_PROJECTION_ROOT = Path(
os.getenv(
"AIOS_GITOPS_REGISTRY_PROJECTION_ROOT",
"/var/lib/aios-gitops/runtime/registry-projection",
)
)
DEFAULT_BASELINE_FILE = Path(
os.getenv(
"AIOS_GITOPS_BASELINE_FILE",
"/var/lib/aios-gitops-runtime-anchor/baseline/current.json",
)
)
DEFAULT_MAIN_REF = os.getenv("AIOS_GITOPS_MAIN_REF", "refs/heads/main")
DEFAULT_AUTHORITATIVE_GIT_DIR = Path("/data/aios-gitops/repo.git")
DEFAULT_APPROVAL_AUTHORITY_ROOT = Path("/var/lib/aios-gitops/approval-authority")
TEST_MODE_ENV = "AIOS_TEST_MODE"
AUTHORITATIVE_GIT_ENV = "AIOS_GITOPS_AUTHORITATIVE_GIT_DIR"
APPROVAL_AUTHORITY_ROOT_ENV = "AIOS_GITOPS_APPROVAL_AUTHORITY_ROOT"
REPOSITORY_IDENTITY_ENV = "AIOS_GITOPS_REPOSITORY_IDENTITY"
class RegistryConvergenceError(RuntimeError):
"""Release registry/source convergence is not proven."""
def _load_object(path: Path) -> dict[str, Any]:
obj = json.loads(path.read_text(encoding="utf-8"))
if not isinstance(obj, dict):
raise RegistryConvergenceError(f"json_not_object:{path}")
return obj
def _sha256(path: Path) -> str:
return hashlib.sha256(path.read_bytes()).hexdigest()
def _git_text(repo_git_dir: Path | str, *args: str) -> str:
completed = subprocess.run(
["git", f"--git-dir={Path(repo_git_dir)}", *args],
capture_output=True,
text=True,
check=False,
timeout=45,
env={"PATH": os.environ.get("PATH", "")},
)
if completed.returncode != 0:
raise RegistryConvergenceError(
completed.stderr.strip() or "authoritative_git_query_failed"
)
return completed.stdout.strip()
def _git_bytes(repo_git_dir: Path | str, *args: str) -> bytes:
completed = subprocess.run(
["git", f"--git-dir={Path(repo_git_dir)}", *args],
capture_output=True,
check=False,
timeout=45,
env={"PATH": os.environ.get("PATH", "")},
)
if completed.returncode != 0:
raise RegistryConvergenceError(
completed.stderr.decode("utf-8", "replace").strip()
or "authoritative_git_query_failed"
)
return completed.stdout
def test_mode_enabled() -> bool:
"""Return whether explicit dependency injection is allowed for tests."""
return os.environ.get(TEST_MODE_ENV) == "1"
def _configured_override(env_name: str, default: str, label: str) -> Path:
if env_name in os.environ:
if not test_mode_enabled():
raise RegistryConvergenceError(f"{label}_env_override_forbidden")
value = os.environ.get(env_name, "")
if not value:
raise RegistryConvergenceError(f"{label}_env_override_invalid")
return Path(value)
return Path(default)
def configured_authoritative_git_dir() -> Path:
return _configured_override(
AUTHORITATIVE_GIT_ENV,
str(DEFAULT_AUTHORITATIVE_GIT_DIR),
"authoritative_repository",
)
def configured_repository_identity() -> str:
if REPOSITORY_IDENTITY_ENV in os.environ:
if not test_mode_enabled():
raise RegistryConvergenceError("repository_identity_env_override_forbidden")
value = os.environ.get(REPOSITORY_IDENTITY_ENV, "")
if not value:
raise RegistryConvergenceError("repository_identity_env_override_invalid")
return value
return REPOSITORY_IDENTITY
def verify_authoritative_repository(
repo_git_dir: Path | str,
*,
repository_identity: str = REPOSITORY_IDENTITY,
expected_path: Path | str | None = None,
) -> dict[str, str]:
"""Prove that a Git directory is the configured AIOS authority.
A repository that merely contains internally consistent objects is not
sufficient. The resolved path must be the configured authority and the
bare repository must advertise the expected Gitea repository identity.
"""
if repository_identity != REPOSITORY_IDENTITY:
raise RegistryConvergenceError("authoritative_repository_identity_invalid")
candidate = Path(repo_git_dir)
configured = Path(expected_path) if expected_path is not None else configured_authoritative_git_dir()
if test_mode_enabled() and expected_path is None and AUTHORITATIVE_GIT_ENV not in os.environ:
# Explicit test mode may inject a temporary bare repository directly.
# Production mode always compares against the fixed authority path.
configured = candidate
try:
candidate_real = candidate.resolve(strict=True)
configured_real = configured.resolve(strict=True)
except OSError as exc:
raise RegistryConvergenceError("authoritative_repository_path_unavailable") from exc
if candidate_real != configured_real:
raise RegistryConvergenceError("authoritative_repository_path_mismatch")
if not candidate_real.is_dir():
raise RegistryConvergenceError("authoritative_repository_not_directory")
if _git_text(candidate_real, "rev-parse", "--is-bare-repository") != "true":
raise RegistryConvergenceError("authoritative_repository_not_bare")
remotes = subprocess.run(
[
"git",
f"--git-dir={candidate_real}",
"config",
"--get-regexp",
r"^remote\..*\.url$",
],
capture_output=True,
text=True,
check=False,
timeout=45,
env={"PATH": os.environ.get("PATH", "")},
)
urls = [line.split(None, 1)[1] for line in remotes.stdout.splitlines() if " " in line]
identity_suffix = repository_identity.removesuffix(".git")
identity_verified = any(
url.rstrip("/").removesuffix(".git").replace("\\", "/").split(":")[-1]
in {identity_suffix, "/" + identity_suffix}
or url.rstrip("/").removesuffix(".git").replace("\\", "/").split(":")[-1].endswith(
"/" + identity_suffix
)
for url in urls
)
if not test_mode_enabled() and not identity_verified:
raise RegistryConvergenceError("authoritative_repository_identity_unverified")
return {
"repository_identity": repository_identity,
"configured_path": str(configured_real),
"resolved_path": str(candidate_real),
}
def resolve_source_registry_main_oid(
repo_git_dir: Path | str,
*,
main_ref: str = DEFAULT_MAIN_REF,
) -> str:
"""Resolve the authoritative main ref to one immutable commit OID."""
oid = _git_text(repo_git_dir, "rev-parse", "--verify", f"{main_ref}^{{commit}}")
if not _OID_RE.fullmatch(oid):
raise RegistryConvergenceError("source_registry_main_oid_invalid")
if _git_text(repo_git_dir, "cat-file", "-t", oid) != "commit":
raise RegistryConvergenceError("source_registry_main_oid_not_commit")
return oid
def source_registry_snapshot(
repo_git_dir: Path | str,
version: str,
*,
main_ref: str = DEFAULT_MAIN_REF,
source_registry_main_oid: str | None = None,
expected_tag_oid: str | None = None,
expected_target_oid: str | None = None,
expected_evidence_closure_oid: str | None = None,
expected_approval_plan_id: str | None = None,
) -> dict[str, Any]:
"""Read one source entry and its exact bytes from one immutable commit.
Callers building an artifact should resolve ``source_registry_main_oid``
once and pass it back here. The function then never consults the mutable
ref again, so validation and packaging use the same Git blob.
"""
if not isinstance(version, str) or not _VERSION_RE.fullmatch(version):
raise RegistryConvergenceError("source_registry_version_invalid")
main_oid = source_registry_main_oid or resolve_source_registry_main_oid(
repo_git_dir,
main_ref=main_ref,
)
if not _OID_RE.fullmatch(main_oid):
raise RegistryConvergenceError("source_registry_main_oid_invalid")
if _git_text(repo_git_dir, "cat-file", "-t", main_oid) != "commit":
raise RegistryConvergenceError("source_registry_main_oid_not_commit")
path = f"versions/{version}.json"
try:
raw = _git_bytes(repo_git_dir, "show", f"{main_oid}:{path}")
except RegistryConvergenceError as exc:
if "path" in str(exc).lower() or "does not exist" in str(exc).lower():
raise RegistryConvergenceError(
f"source_registry_entry_missing:{main_oid}:{path}"
) from exc
raise
try:
source = json.loads(raw.decode("utf-8"))
except (UnicodeDecodeError, json.JSONDecodeError) as exc:
raise RegistryConvergenceError("source_registry_json_invalid") from exc
record = validate_source_registry_record(
source,
expected_version=version,
expected_tag_oid=expected_tag_oid,
expected_target_oid=expected_target_oid,
expected_evidence_closure_oid=expected_evidence_closure_oid,
expected_approval_plan_id=expected_approval_plan_id,
)
return {
"record": record,
"bytes": raw,
"source_registry_main_oid": main_oid,
"source_registry_path": path,
"source_registry_sha256": hashlib.sha256(raw).hexdigest(),
}
def validate_source_registry_record(
value: Any,
*,
expected_version: str | None = None,
expected_tag_oid: str | None = None,
expected_target_oid: str | None = None,
expected_evidence_closure_oid: str | None = None,
expected_approval_plan_id: str | None = None,
) -> dict[str, Any]:
"""Validate the immutable identity carried by versions/<version>.json."""
if not isinstance(value, dict):
raise RegistryConvergenceError("source_registry_not_object")
if value.get("schema") not in SOURCE_REGISTRY_SCHEMAS:
raise RegistryConvergenceError("source_registry_schema_invalid")
version = value.get("version")
if not isinstance(version, str) or not _VERSION_RE.fullmatch(version):
raise RegistryConvergenceError("source_registry_version_invalid")
if expected_version is not None and version != expected_version:
raise RegistryConvergenceError("source_registry_mismatch:version")
if value.get("repository") != REPOSITORY_IDENTITY:
raise RegistryConvergenceError("source_registry_repository_invalid")
for field in ("tag_oid", "target_oid", "peeled_target_oid", "evidence_closure_oid"):
item = value.get(field)
if not isinstance(item, str) or not _OID_RE.fullmatch(item):
raise RegistryConvergenceError(f"source_registry_{field}_invalid")
if value["peeled_target_oid"] != value["target_oid"]:
raise RegistryConvergenceError("source_registry_peeled_target_mismatch")
approval = value.get("approval_plan_id")
if not isinstance(approval, str) or not _PLAN_RE.fullmatch(approval):
raise RegistryConvergenceError("source_registry_approval_plan_invalid")
if value.get("tag_object", value["tag_oid"]) != value["tag_oid"]:
raise RegistryConvergenceError("source_registry_tag_object_mismatch")
expected = {
"tag_oid": expected_tag_oid,
"target_oid": expected_target_oid,
"evidence_closure_oid": expected_evidence_closure_oid,
"approval_plan_id": expected_approval_plan_id,
}
for field, wanted in expected.items():
if wanted is not None and value.get(field) != wanted:
raise RegistryConvergenceError(f"source_registry_mismatch:{field}")
return dict(value)
def build_source_registry_entry(
*,
version: str,
tag_oid: str,
target_oid: str,
peeled_target_oid: str,
evidence_closure_oid: str,
approval_plan_id: str,
repository: str = REPOSITORY_IDENTITY,
) -> dict[str, Any]:
"""Build the pre-promotion source entry; it performs no Git or runtime write."""
value = {
"schema": "aios.source-registry.v1",
"version": version,
"repository": repository,
"tag": version,
"tag_oid": tag_oid,
"tag_object": tag_oid,
"target_oid": target_oid,
"peeled_target_oid": peeled_target_oid,
"evidence_closure_oid": evidence_closure_oid,
"approval_plan_id": approval_plan_id,
}
return validate_source_registry_record(
value,
expected_version=version,
expected_tag_oid=tag_oid,
expected_target_oid=target_oid,
expected_evidence_closure_oid=evidence_closure_oid,
expected_approval_plan_id=approval_plan_id,
)
def verify_source_registry_git_identity(
repo_git_dir: Path | str,
record: dict[str, Any],
) -> dict[str, Any]:
"""Prove that every Git identity in a source entry exists and is related."""
record = validate_source_registry_record(record)
version = record["version"]
tag_oid = record["tag_oid"]
target_oid = record["target_oid"]
evidence_oid = record["evidence_closure_oid"]
if _git_text(repo_git_dir, "cat-file", "-t", tag_oid) != "tag":
raise RegistryConvergenceError("source_registry_tag_not_annotated")
named_tag = _git_text(
repo_git_dir,
"rev-parse",
"--verify",
f"refs/tags/{version}",
)
if named_tag != tag_oid:
raise RegistryConvergenceError("source_registry_named_tag_mismatch")
if _git_text(repo_git_dir, "cat-file", "-t", target_oid) != "commit":
raise RegistryConvergenceError("source_registry_target_not_commit")
peeled = _git_text(
repo_git_dir,
"rev-parse",
"--verify",
f"{tag_oid}^{{commit}}",
)
if peeled != target_oid or record["peeled_target_oid"] != target_oid:
raise RegistryConvergenceError("source_registry_tag_target_mismatch")
if _git_text(repo_git_dir, "cat-file", "-t", evidence_oid) != "commit":
raise RegistryConvergenceError("source_registry_evidence_not_commit")
ancestor = subprocess.run(
[
"git",
f"--git-dir={Path(repo_git_dir)}",
"merge-base",
"--is-ancestor",
target_oid,
evidence_oid,
],
check=False,
capture_output=True,
timeout=45,
env={"PATH": os.environ.get("PATH", "")},
)
if ancestor.returncode != 0:
raise RegistryConvergenceError("source_registry_evidence_lineage_invalid")
return {
"repository": REPOSITORY_IDENTITY,
"version": version,
"tag_oid": tag_oid,
"tag_object_type": "tag",
"target_oid": target_oid,
"peeled_target_oid": peeled,
"evidence_closure_oid": evidence_oid,
"target_is_ancestor_of_evidence": True,
}
def verify_source_registry_main_convergence(
repo_git_dir: Path | str,
version: str,
*,
main_ref: str = DEFAULT_MAIN_REF,
source_registry_main_oid: str | None = None,
expected_tag_oid: str | None = None,
expected_target_oid: str | None = None,
expected_evidence_closure_oid: str | None = None,
expected_approval_plan_id: str | None = None,
) -> dict[str, Any]:
"""Prove that pinned M is still converged with current authoritative main C."""
current_main_oid = resolve_source_registry_main_oid(repo_git_dir, main_ref=main_ref)
source_main_oid = source_registry_main_oid or current_main_oid
if not _OID_RE.fullmatch(source_main_oid):
raise RegistryConvergenceError("source_registry_main_oid_invalid")
ancestor = subprocess.run(
[
"git",
f"--git-dir={Path(repo_git_dir)}",
"merge-base",
"--is-ancestor",
source_main_oid,
current_main_oid,
],
check=False,
capture_output=True,
timeout=45,
env={"PATH": os.environ.get("PATH", "")},
)
if ancestor.returncode != 0:
raise RegistryConvergenceError("source_registry_main_not_ancestor_of_current_main")
pinned = source_registry_snapshot(
repo_git_dir,
version,
source_registry_main_oid=source_main_oid,
expected_tag_oid=expected_tag_oid,
expected_target_oid=expected_target_oid,
expected_evidence_closure_oid=expected_evidence_closure_oid,
expected_approval_plan_id=expected_approval_plan_id,
)
current = source_registry_snapshot(
repo_git_dir,
version,
source_registry_main_oid=current_main_oid,
expected_tag_oid=expected_tag_oid,
expected_target_oid=expected_target_oid,
expected_evidence_closure_oid=expected_evidence_closure_oid,
expected_approval_plan_id=expected_approval_plan_id,
)
if pinned["bytes"] != current["bytes"]:
raise RegistryConvergenceError("source_registry_main_entry_changed_after_pin")
return {
"source_registry_main_oid": source_main_oid,
"current_main_oid": current_main_oid,
"source_registry_path": pinned["source_registry_path"],
"source_registry_sha256": pinned["source_registry_sha256"],
"current_main_source_registry_sha256": current["source_registry_sha256"],
"source_registry_main_is_ancestor_of_current_main": True,
"source_registry_entry_matches_current_main": True,
}
_APPROVAL_AUTHORITY_STATES = frozenset(
{
"APPROVED",
"GITEA_API_CONFIRMED",
"REMOTE_VERIFIED",
"PRODUCTION_DEPLOYED",
}
)
_APPROVAL_AUTHORITY_SCHEMAS = frozenset(
{
"aios.v2r2c1.tag-approval-request.v1",
"aios.owner-release-approval.v1",
"aios.release-binding-approval.v2",
}
)
def configured_approval_authority_root() -> Path:
return _configured_override(
APPROVAL_AUTHORITY_ROOT_ENV,
str(DEFAULT_APPROVAL_AUTHORITY_ROOT),
"approval_authority_root",
)
def _secure_approval_authority_path(
authority_path: Path | str,
version: str,
) -> tuple[Path, Path]:
root = configured_approval_authority_root()
path = Path(authority_path)
if path.is_dir():
path = path / f"{version}.json"
try:
root_real = root.resolve(strict=True)
path_real = path.resolve(strict=True)
except OSError as exc:
raise RegistryConvergenceError("approval_authority_trusted_root_missing") from exc
try:
path_real.relative_to(root_real)
except ValueError as exc:
raise RegistryConvergenceError("approval_authority_untrusted_path") from exc
metadata = path.lstat()
if not stat.S_ISREG(metadata.st_mode) or path.is_symlink():
raise RegistryConvergenceError("approval_authority_not_regular")
if metadata.st_mode & 0o022:
raise RegistryConvergenceError("approval_authority_permissions_invalid")
if APPROVAL_AUTHORITY_ROOT_ENV not in os.environ and metadata.st_uid != 0:
raise RegistryConvergenceError("approval_authority_owner_invalid")
return path, root_real
def _canonical_json(value: object) -> bytes:
return (json.dumps(value, sort_keys=True, separators=(",", ":")) + "\n").encode(
"utf-8"
)
def _approval_is_revoked(root: Path, approval_plan_id: str, authority: dict[str, Any]) -> bool:
if any(
authority.get(name) is True
for name in ("revoked", "invalidated", "revocation")
):
return True
state = authority.get("state") or authority.get("status")
if state in {"REVOKED", "INVALIDATED", "EXPIRED", "CANCELLED"}:
return True
for marker in (
root / "revocations" / approval_plan_id,
root / "revocations" / approval_plan_id.replace(":", "_"),
):
if marker.exists():
return True
expires_at = authority.get("expires_at")
if isinstance(expires_at, str) and expires_at:
try:
instant = datetime.fromisoformat(expires_at.replace("Z", "+00:00"))
except ValueError as exc:
raise RegistryConvergenceError("approval_expiry_invalid") from exc
if instant <= datetime.now(timezone.utc):
return True
return False
def verify_approval_authority(
authority_path: Path | str,
record: dict[str, Any],
) -> dict[str, Any]:
"""Verify the existing release approval/publication receipt association.
The authority document is intentionally supplied by the governed caller;
this helper never creates or rewrites an approval. The path must be under
the configured canonical Owner-authority root and the document must use a
known release-approval schema. A caller-supplied JSON file elsewhere is
never an authority, even when its fields are internally consistent.
"""
path, trusted_root = _secure_approval_authority_path(authority_path, record["version"])
authority = _load_object(path)
schema = authority.get("schema")
if schema not in _APPROVAL_AUTHORITY_SCHEMAS:
raise RegistryConvergenceError("approval_authority_schema_invalid")
if _approval_is_revoked(trusted_root, record["approval_plan_id"], authority):
raise RegistryConvergenceError("approval_plan_revoked_or_invalidated")
nested = authority.get("release_binding") or authority.get("release_request")
if not isinstance(nested, dict):
nested = authority.get("binding")
if not isinstance(nested, dict):
raise RegistryConvergenceError("approval_authority_binding_missing")
expected = {
"version": record["version"],
"repository": REPOSITORY_IDENTITY,
"tag_oid": record["tag_oid"],
"target_oid": record["target_oid"],
"peeled_target_oid": record["peeled_target_oid"],
"evidence_closure_oid": record["evidence_closure_oid"],
"approval_plan_id": record["approval_plan_id"],
}
if authority.get("approval_plan_id") != expected["approval_plan_id"]:
raise RegistryConvergenceError("approval_plan_not_found_or_mismatched")
state = authority.get("state") or authority.get("status")
if state not in _APPROVAL_AUTHORITY_STATES and authority.get("approved") is not True:
raise RegistryConvergenceError("approval_plan_not_approved")
owner_approval = authority.get("owner_approval")
if schema == "aios.v2r2c1.tag-approval-request.v1":
if not isinstance(owner_approval, dict):
raise RegistryConvergenceError("canonical_owner_approval_missing")
if (
owner_approval.get("approved") is not True
or owner_approval.get("status") != "APPROVED"
or owner_approval.get("approved_by") != "aiman"
or owner_approval.get("approval_source") != "explicit_owner_approval_in_chat"
):
raise RegistryConvergenceError("canonical_owner_approval_invalid")
expected_digest = hashlib.sha256(_canonical_json(owner_approval)).hexdigest()
if authority.get("owner_approval_digest_sha256") != expected_digest:
raise RegistryConvergenceError("canonical_owner_approval_digest_invalid")
for field, wanted in expected.items():
observed = nested.get(field)
if field == "repository" and observed is None:
observed = nested.get("repository_identity") or authority.get(
"repository_identity"
)
if field == "peeled_target_oid" and observed is None:
observed = (
nested.get("gitea_peeled_target_oid")
or authority.get("gitea_peeled_target_oid")
or nested.get("remote_peeled_target_oid")
or authority.get("remote_peeled_target_oid")
)
if observed != wanted:
raise RegistryConvergenceError(f"approval_plan_release_binding_mismatch:{field}")
if state == "GITEA_API_CONFIRMED":
gitea_tag = authority.get("gitea_tag_oid")
gitea_peeled = authority.get("gitea_peeled_target_oid")
if gitea_tag != expected["tag_oid"] or gitea_peeled != expected["target_oid"]:
raise RegistryConvergenceError("approval_plan_remote_tag_binding_mismatch")
return {
"path": str(path),
"schema": schema,
"state": state or "APPROVED",
"approval_plan_id": expected["approval_plan_id"],
"owner_approval_verified": schema == "aios.v2r2c1.tag-approval-request.v1",
"release_binding_verified": True,
}
def source_registry_json(value: dict[str, Any]) -> bytes:
"""Return the canonical bytes used by the governed file writer."""
return (json.dumps(value, ensure_ascii=False, sort_keys=True, indent=2) + "\n").encode(
"utf-8"
)
def _validated_current_projection(
projection_root: Path,
baseline_file: Path,
) -> Path | None:
"""Return only the exact projection attested by the current Baseline."""
if not baseline_file.is_file():
return None
try:
baseline = _load_object(baseline_file)
except Exception:
return None
version = baseline.get("version")
if not isinstance(version, str) or not _VERSION_RE.fullmatch(version):
return None
if baseline.get("status") != "PRODUCTION_DEPLOYED":
return None
if baseline.get("production_deployed") is not True:
return None
source = baseline.get("registry_source")
digest = baseline.get("registry_digest")
if not isinstance(source, str) or not isinstance(digest, str):
return None
candidate = projection_root / f"{version}.json"
try:
if candidate.resolve() != Path(source).resolve():
return None
except Exception:
return None
if not candidate.is_file() or _sha256(candidate) != digest:
return None
try:
record = _load_object(candidate)
except Exception:
return None
try:
validate_source_registry_record(
record,
expected_version=version,
expected_tag_oid=baseline.get("tag_oid"),
expected_target_oid=baseline.get("target_oid"),
expected_evidence_closure_oid=baseline.get("evidence_closure_oid"),
expected_approval_plan_id=baseline.get("approval_plan_id"),
)
except RegistryConvergenceError:
return None
if record.get("schema") != "aios.production-registry.v2":
return None
if record.get("status") != "PRODUCTION_DEPLOYED":
return None
if record.get("version") != version:
return None
if record.get("repository") != REPOSITORY_IDENTITY:
return None
if record.get("production_deployed") is not True:
return None
tag_oid = record.get("tag_oid")
target_oid = record.get("target_oid")
if not isinstance(tag_oid, str) or not _OID_RE.fullmatch(tag_oid):
return None
if not isinstance(target_oid, str) or not _OID_RE.fullmatch(target_oid):
return None
if record.get("peeled_target_oid") != target_oid:
return None
return candidate
def list_registry_versions(
workspace: Path | str,
*,
projection_root: Path = DEFAULT_PROJECTION_ROOT,
baseline_file: Path = DEFAULT_BASELINE_FILE,
) -> list[str]:
"""Return the legacy display list; use the report for provenance."""
return list_registry_versions_report(
workspace,
projection_root=projection_root,
baseline_file=baseline_file,
)["registry_versions"]
def list_registry_versions_report(
workspace: Path | str,
*,
projection_root: Path = DEFAULT_PROJECTION_ROOT,
baseline_file: Path = DEFAULT_BASELINE_FILE,
repo_git_dir: Path | str | None = None,
main_ref: str = DEFAULT_MAIN_REF,
) -> dict[str, Any]:
"""Return version visibility with runtime/projection provenance.
The display list intentionally retains the historical projection overlay
for API compatibility. The explicit booleans make that overlay
non-authoritative: a projection-only version has
``runtime_bundle_contains_version == false`` and can never be reported as
converged.
"""
ws = Path(workspace)
versions_root = ws / "versions"
runtime_names = {
path.name
for path in versions_root.iterdir()
if path.is_file()
and not path.is_symlink()
and path.suffix == ".json"
and _VERSION_RE.fullmatch(path.stem)
} if versions_root.is_dir() else set()
current = _validated_current_projection(projection_root, baseline_file)
attested_version = current.stem if current is not None else None
runtime_bundle_contains = bool(
attested_version is not None
and f"{attested_version}.json" in runtime_names
)
consistency_status = "NO_ATTESTED_PROJECTION"
source_converged: bool | str = "unknown"
source_error: str | None = None
runtime_converged = False
projection_record: dict[str, Any] | None = None
if current is not None:
try:
projection_record = _load_object(current)
except (OSError, RegistryConvergenceError, json.JSONDecodeError) as exc:
source_error = f"projection_invalid:{type(exc).__name__}"
if repo_git_dir is None:
source_converged = "unknown"
source_error = "authoritative_repository_unavailable"
elif projection_record is not None:
try:
verify_authoritative_repository(repo_git_dir)
verify_source_registry_main_convergence(
repo_git_dir,
attested_version or "",
main_ref=main_ref,
expected_tag_oid=projection_record.get("tag_oid"),
expected_target_oid=projection_record.get("target_oid"),
expected_evidence_closure_oid=projection_record.get(
"evidence_closure_oid"
),
expected_approval_plan_id=projection_record.get("approval_plan_id"),
)
source_converged = True
except RegistryConvergenceError as exc:
detail = str(exc)
if detail.startswith("authoritative_repository_"):
source_converged = "unknown"
source_error = (
"authoritative_repository_unavailable"
if detail.endswith("path_unavailable")
else detail
)
else:
source_converged = False
source_error = detail
if current is not None and not runtime_bundle_contains:
consistency_status = "RUNTIME_ARTIFACT_RECONCILIATION_REQUIRED"
elif current is not None and runtime_bundle_contains:
runtime_path = versions_root / current.name
try:
projection_record = projection_record or _load_object(current)
runtime_record = _load_object(runtime_path)
identity_fields = (
"version",
"repository",
"tag_oid",
"target_oid",
"peeled_target_oid",
"evidence_closure_oid",
"approval_plan_id",
)
runtime_converged = all(
runtime_record.get(field) == projection_record.get(field)
for field in identity_fields
)
if not runtime_converged:
consistency_status = "RUNTIME_IDENTITY_MISMATCH"
except (OSError, RegistryConvergenceError, json.JSONDecodeError):
consistency_status = "RUNTIME_ENTRY_INVALID"
runtime_converged = False
if current is not None and runtime_bundle_contains and runtime_converged:
if source_converged is True:
consistency_status = "FULLY_CONSISTENT"
elif source_converged == "unknown":
consistency_status = "SOURCE_GIT_NOT_CHECKED"
else:
consistency_status = "SOURCE_REGISTRY_NOT_CONVERGED"
elif current is not None and not runtime_bundle_contains:
if source_converged is False:
consistency_status = "SOURCE_REGISTRY_NOT_CONVERGED"
else:
consistency_status = "RUNTIME_ARTIFACT_RECONCILIATION_REQUIRED"
names = set(runtime_names)
if current is not None:
names.add(current.name)
effective = sorted(names)
projection_versions = [attested_version + ".json"] if attested_version else []
return {
"registry_versions": effective,
"effective_registry_versions": effective,
"runtime_registry_versions": sorted(runtime_names),
"attested_projection_versions": projection_versions,
"attested_projection_version": attested_version,
"source_registry_converged": source_converged,
"source_registry_convergence_error": source_error,
"runtime_registry_converged": runtime_converged,
"runtime_bundle_contains_current_version": runtime_bundle_contains,
"runtime_bundle_contains_version": runtime_bundle_contains,
"registry_consistency_status": consistency_status,
}
def assert_runtime_registry_convergence(
workspace: Path | str,
version: str,
*,
projection_root: Path = DEFAULT_PROJECTION_ROOT,
baseline_file: Path = DEFAULT_BASELINE_FILE,
) -> dict[str, Any]:
"""Fail closed when a projection-only version is visible but unpackaged."""
report = list_registry_versions_report(
workspace,
projection_root=projection_root,
baseline_file=baseline_file,
)
expected_name = f"{version}.json"
if (
report.get("attested_projection_version") == version
and report.get("runtime_bundle_contains_version") is not True
):
raise RegistryConvergenceError("runtime_bundle_missing_source_registry")
if expected_name not in report.get("runtime_registry_versions", []):
raise RegistryConvergenceError("runtime_bundle_missing_source_registry")
if report.get("source_registry_converged") is not True:
raise RegistryConvergenceError("source_registry_not_converged_or_not_checked")
if report.get("registry_consistency_status") not in {"FULLY_CONSISTENT", "PASS"}:
raise RegistryConvergenceError(
f"runtime_registry_consistency:{report.get('registry_consistency_status')}"
)
return report
def source_registry_from_main(
repo_git_dir: Path | str,
version: str,
*,
main_ref: str = DEFAULT_MAIN_REF,
source_registry_main_oid: str | None = None,
expected_tag_oid: str | None = None,
expected_target_oid: str | None = None,
expected_evidence_closure_oid: str | None = None,
expected_approval_plan_id: str | None = None,
) -> dict[str, Any]:
"""Read and validate the canonical source entry from the supplied main ref."""
try:
return source_registry_snapshot(
repo_git_dir,
version,
main_ref=main_ref,
source_registry_main_oid=source_registry_main_oid,
expected_tag_oid=expected_tag_oid,
expected_target_oid=expected_target_oid,
expected_evidence_closure_oid=expected_evidence_closure_oid,
expected_approval_plan_id=expected_approval_plan_id,
)["record"]
except RegistryConvergenceError as exc:
if str(exc).startswith("fatal:") or "path" in str(exc).lower():
raise RegistryConvergenceError(
f"source_registry_entry_missing:{source_registry_main_oid or main_ref}:versions/{version}.json"
) from exc
raise
def assert_source_registry_convergence(
registry_path: Path | str,
*,
repo_git_dir: Path | str,
main_ref: str = DEFAULT_MAIN_REF,
source_registry_main_oid: str | None = None,
) -> dict[str, Any]:
"""Require governed main to contain a source entry for the release identity.
The source entry is intentionally required to match only fields that are
knowable and governable before production deployment. Post-deployment
fields such as production_service/port/status timestamps are not required.
"""
registry_path = Path(registry_path)
repo_git_dir = Path(repo_git_dir)
verify_authoritative_repository(repo_git_dir)
record = _load_object(registry_path)
version = record.get("version")
tag_oid = record.get("tag_oid")
target_oid = record.get("target_oid")
try:
validate_source_registry_record(
record,
expected_version=version,
expected_tag_oid=tag_oid,
expected_target_oid=target_oid,
expected_evidence_closure_oid=record.get("evidence_closure_oid"),
expected_approval_plan_id=record.get("approval_plan_id"),
)
except RegistryConvergenceError as exc:
raise RegistryConvergenceError(f"registry_identity_invalid:{exc}") from exc
convergence = verify_source_registry_main_convergence(
repo_git_dir,
version,
main_ref=main_ref,
source_registry_main_oid=source_registry_main_oid,
expected_tag_oid=tag_oid,
expected_target_oid=target_oid,
expected_evidence_closure_oid=record["evidence_closure_oid"],
expected_approval_plan_id=record["approval_plan_id"],
)
return {
**convergence,
}
@@ -1,36 +0,0 @@
-- R4.2 P0-B durable Message/Handoff Fabric.
-- Forward-only. The existing 20260707 message_outbox remains a compatibility source.
CREATE TABLE IF NOT EXISTS message_handoff (
envelope_id TEXT PRIMARY KEY,
stream_key TEXT NOT NULL,
source TEXT NOT NULL,
target TEXT NOT NULL,
message_type TEXT NOT NULL,
payload_json TEXT NOT NULL,
payload_sha256 TEXT NOT NULL,
tenant_id TEXT,
user_id TEXT,
project_id TEXT,
conversation_id TEXT,
task_id TEXT,
run_id TEXT,
causation_id TEXT,
sequence INTEGER NOT NULL CHECK(sequence >= 1),
capability_scope_json TEXT NOT NULL,
confidentiality_scope TEXT NOT NULL,
idempotency_key TEXT NOT NULL,
expires_at TEXT,
created_at TEXT NOT NULL,
status TEXT NOT NULL DEFAULT 'PENDING',
delivered_at TEXT,
acked_at TEXT,
UNIQUE(stream_key, idempotency_key),
UNIQUE(stream_key, sequence)
);
CREATE INDEX IF NOT EXISTS idx_message_handoff_target_status
ON message_handoff(target, status, created_at);
CREATE INDEX IF NOT EXISTS idx_message_handoff_scope
ON message_handoff(tenant_id, project_id, user_id, conversation_id, run_id);
@@ -1,36 +0,0 @@
[Unit]
Description=AIOS owner-controlled Task retry-extension API
After=aios-agent-task-api.service
Wants=aios-agent-task-api.service
[Service]
Type=simple
User=aios-agent-task
Group=aios-agent-task
LoadCredential=agent_task_control_token:/etc/aios-gitops/agent-task-control.token
EnvironmentFile=/etc/aios-gitops/agent-task-retry-extension.env
Environment=AIOS_AGENT_TASK_DB=/var/lib/aios-agent-task/state.sqlite3
Environment=AIOS_AGENT_TASK_CONTROL_TOKEN_FILE=%d/agent_task_control_token
Environment=AIOS_TASK_RETRY_HOST=127.0.0.1
Environment=AIOS_TASK_RETRY_PORT=18941
ExecStart=/usr/bin/python3 -B /opt/aios/current/active-runtime/app/aios_agent_task_retry_extension.py
Restart=on-failure
RestartSec=2
NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=strict
ProtectHome=true
ProtectKernelTunables=true
ProtectKernelModules=true
ProtectControlGroups=true
LockPersonality=true
RestrictSUIDSGID=true
RestrictRealtime=true
RestrictNamespaces=true
MemoryDenyWriteExecute=true
RestrictAddressFamilies=AF_UNIX AF_INET
ReadWritePaths=/var/lib/aios-agent-task
UMask=0077
[Install]
WantedBy=multi-user.target
@@ -1,33 +0,0 @@
[Unit]
Description=AIOS uncredentialed deterministic Planner/Executor
After=local-fs.target
[Service]
Type=simple
User=aios-engineering-executor
Group=aios-executor-exchange
ExecStart=/usr/bin/python3 -B /opt/aios/current/active-runtime/app/aios_isolated_executor.py --root /var/lib/aios-online-agent-executor
Restart=always
RestartSec=2
NoNewPrivileges=true
PrivateTmp=true
PrivateNetwork=true
ProtectSystem=strict
ProtectHome=true
ProtectKernelTunables=true
ProtectKernelModules=true
ProtectControlGroups=true
LockPersonality=true
RestrictSUIDSGID=true
RestrictRealtime=true
RestrictNamespaces=true
MemoryDenyWriteExecute=true
RestrictAddressFamilies=AF_UNIX AF_INET
IPAddressDeny=any
IPAddressAllow=localhost
InaccessiblePaths=/run/credentials
ReadWritePaths=/var/lib/aios-online-agent-executor
UMask=0027
[Install]
WantedBy=multi-user.target
@@ -1,46 +0,0 @@
[Unit]
Description=AIOS governed Online Agent Worker
After=aios-agent-task-api.service aios-chatgpt-bridge-v2-staging.service
Wants=aios-agent-task-api.service aios-chatgpt-bridge-v2-staging.service
[Service]
Type=simple
User=aios-engineering-worker
Group=aios-engineering-worker
SupplementaryGroups=gitops aios-executor-exchange
LoadCredentialEncrypted=agent_token:/etc/credstore.encrypted/aios-online-agent-worker-20260815/agent_token
Environment=PYTHONUNBUFFERED=1
Environment=AIOS_AGENT_TASK_API_URL=http://127.0.0.1:18940
Environment=AIOS_GOVERNED_WRITE_API_URL=http://127.0.0.1:18892
Environment=AIOS_AGENT_ID=online-engineering-worker
Environment=AIOS_AGENT_TYPE=trusted-online-engineering-worker
Environment=AIOS_AGENT_VERSION=2026-08-17.2-durable
Environment=AIOS_WORKER_CAPABILITIES=git.read,git.test,governed.write.branch,governed.write.file
Environment=AIOS_AUTHORITY_GIT_DIR=/data/aios-gitops/repo.git
Environment=AIOS_ISOLATED_EXECUTOR_ROOT=/var/lib/aios-online-agent-executor
Environment=AIOS_WORKER_STATE_ROOT=/var/lib/aios-online-agent-worker
Environment=AIOS_WORKER_CHECKPOINT_DB=/var/lib/aios-online-agent-worker/checkpoints.sqlite3
Environment=AIOS_WORKER_WORKSPACE_ROOT=/var/lib/aios-online-agent-worker/workspaces
ExecStart=/usr/bin/python3 -B /opt/aios/current/active-runtime/app/aios_online_agent_worker.py
Restart=always
RestartSec=5
TimeoutStopSec=20
NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=strict
ProtectHome=true
ProtectKernelTunables=true
ProtectKernelModules=true
ProtectControlGroups=true
LockPersonality=true
RestrictSUIDSGID=true
RestrictRealtime=true
RestrictNamespaces=true
MemoryDenyWriteExecute=true
RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6
ReadOnlyPaths=/data/aios-gitops/repo.git
ReadWritePaths=/var/lib/aios-online-agent-worker
UMask=0027
[Install]
WantedBy=multi-user.target
@@ -1,4 +0,0 @@
g aios-executor-exchange -
u aios-engineering-executor - "AIOS isolated engineering executor" /var/lib/aios-online-agent-executor /usr/sbin/nologin
m aios-engineering-executor aios-executor-exchange
m aios-engineering-worker aios-executor-exchange
@@ -1,5 +0,0 @@
d /var/lib/aios-online-agent-executor 2770 aios-engineering-executor aios-executor-exchange -
d /var/lib/aios-online-agent-executor/requests 2770 aios-engineering-worker aios-executor-exchange -
d /var/lib/aios-online-agent-executor/results 2770 aios-engineering-executor aios-executor-exchange -
d /var/lib/aios-online-agent-executor/failed 2770 aios-engineering-executor aios-executor-exchange -
d /var/lib/aios-online-agent-executor/sources 2770 aios-engineering-worker aios-executor-exchange -
-223
View File
@@ -240,114 +240,6 @@ components:
additionalProperties: false
properties:
candidate_id: {type: string}
BridgeWriterLeaseRecoveryRequest:
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
enum: [aios.task-writer-lease.v2, aios.task-writer-lease-request.v2]
task_id: {type: string, minLength: 1, maxLength: 256}
job_id: {type: string, pattern: '^job-[A-Za-z0-9][A-Za-z0-9._-]{7,255}$'}
repository: {type: string, const: aios/aios}
branch_mode: {type: string, enum: [CREATE_NEW, CONTINUE_EXISTING]}
base_oid: {type: string, pattern: '^[0-9a-f]{40}$'}
expected_branch_oid: {type: string, pattern: '^[0-9a-f]{40}$'}
target_branch: {type: string, pattern: '^agent/[A-Za-z0-9][A-Za-z0-9._/-]{2,179}$'}
generation: {type: integer, minimum: 1}
lease_seconds: {type: integer, minimum: 1, maximum: 86400}
BridgeWriterLeaseResponse:
type: object
additionalProperties: false
required: [schema, task_id, job_id, repository, branch_mode, base_oid, expected_branch_oid, target_branch, generation, lease_seconds, create_target_branch, worktree_mode, cas_ref_update_required]
properties:
schema: {type: string, enum: [aios.task-writer-lease.v2]}
task_id: {type: string}
job_id: {type: string, pattern: '^job-[A-Za-z0-9][A-Za-z0-9._-]{7,255}$'}
repository: {type: string, const: aios/aios}
branch_mode: {type: string, enum: [CREATE_NEW, CONTINUE_EXISTING]}
base_oid: {type: string, pattern: '^[0-9a-f]{40}$'}
expected_branch_oid: {type: string, pattern: '^[0-9a-f]{40}$'}
target_branch: {type: string, pattern: '^agent/[A-Za-z0-9][A-Za-z0-9._/-]{2,179}$'}
generation: {type: integer, minimum: 1}
lease_seconds: {type: integer, minimum: 1, maximum: 86400}
create_target_branch: {type: boolean}
worktree_mode: {type: string, enum: [detached_at_base_oid, detached_at_expected_head]}
cas_ref_update_required: {type: boolean, const: true}
lease_expires_at: {type: string, format: date-time}
BridgeWriterLeaseRecoveryResponse:
type: object
additionalProperties: false
required: [schema, task_id, job_id, repository, branch_mode, base_oid, expected_branch_oid, target_branch, generation, lease_seconds, lease_expires_at, create_target_branch, worktree_mode, cas_ref_update_required, recovered, bootstrapped, idempotent, runtime_rehydrated, durable_branch_lease_present]
properties:
schema: {type: string, enum: [aios.task-writer-lease.v2]}
task_id: {type: string}
job_id: {type: string, pattern: '^job-[A-Za-z0-9][A-Za-z0-9._-]{7,255}$'}
repository: {type: string, const: aios/aios}
branch_mode: {type: string, enum: [CREATE_NEW, CONTINUE_EXISTING]}
base_oid: {type: string, pattern: '^[0-9a-f]{40}$'}
expected_branch_oid: {type: string, pattern: '^[0-9a-f]{40}$'}
target_branch: {type: string, pattern: '^agent/[A-Za-z0-9][A-Za-z0-9._/-]{2,179}$'}
generation: {type: integer, minimum: 1}
lease_seconds: {type: integer, minimum: 1, maximum: 86400}
lease_expires_at: {type: string, format: date-time}
create_target_branch: {type: boolean, const: false}
worktree_mode: {type: string, enum: [detached_at_base_oid, detached_at_expected_head]}
cas_ref_update_required: {type: boolean, const: true}
recovered: {type: boolean}
bootstrapped: {type: boolean}
idempotent: {type: boolean}
runtime_rehydrated: {type: boolean}
durable_branch_lease_present: {type: boolean}
BridgeCommitFileRequest:
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, minLength: 1}
job_id: {type: string, pattern: '^job-[A-Za-z0-9][A-Za-z0-9._-]{7,255}$'}
repository: {type: string, const: aios/aios}
expected_branch_oid: {type: string, pattern: '^[0-9a-f]{40}$'}
target_branch: {type: string, pattern: '^agent/[A-Za-z0-9][A-Za-z0-9._/-]{2,179}$'}
generation: {type: integer, minimum: 1}
idempotency_key: {type: string, minLength: 1, maxLength: 256}
path: {type: string, minLength: 1, maxLength: 512}
content_base64: {type: string, format: byte}
message: {type: string, minLength: 1, maxLength: 200}
BridgeCommitFilesRequest:
type: object
additionalProperties: false
required: [task_id, repository, expected_branch_oid, target_branch, generation, idempotency_key, files, message]
properties:
task_id: {type: string, minLength: 1}
job_id: {type: string, pattern: '^job-[A-Za-z0-9][A-Za-z0-9._-]{7,255}$'}
repository: {type: string, const: aios/aios}
expected_branch_oid: {type: string, pattern: '^[0-9a-f]{40}$'}
target_branch: {type: string, pattern: '^agent/[A-Za-z0-9][A-Za-z0-9._/-]{2,179}$'}
generation: {type: integer, minimum: 1}
idempotency_key: {type: string, minLength: 1, maxLength: 256}
files: {type: object, additionalProperties: {type: string, format: byte}, minProperties: 1}
message: {type: string, minLength: 1, maxLength: 200}
BridgeCommitResponse:
type: object
additionalProperties: true
required: [job_id, commit_oid, commit_visible, branch_mode, target_branch, generation]
properties:
job_id: {type: string, pattern: '^job-[A-Za-z0-9][A-Za-z0-9._-]{7,255}$'}
commit_oid: {type: string, pattern: '^[0-9a-f]{40}$'}
commit_visible: {type: boolean}
branch_mode: {type: string, enum: [CONTINUE_EXISTING]}
target_branch: {type: string, pattern: '^agent/[A-Za-z0-9][A-Za-z0-9._/-]{2,179}$'}
generation: {type: integer, minimum: 1}
BridgeGovernedCapabilities:
type: object
required: [bridge_schema_version, governed_writes_enabled, operations, operation_contracts]
properties:
bridge_schema_version: {type: string, const: aios.chatgpt-gitops-bridge.v2}
governed_writes_enabled: {type: boolean, const: true}
operations: {type: array, items: {type: string}}
operation_contracts: {type: object, additionalProperties: true}
paths:
/createReleaseIntent:
post:
@@ -675,118 +567,3 @@ paths:
'200': {description: Safe GitOps plan}
'401': {description: Unauthorized}
x-openai-isConsequential: true
/v2/capabilities:
get:
operationId: getGovernedWriteCapabilities
summary: Read the governed Bridge v2 capability contract
responses:
'200':
description: Governed operations and their request/response contracts
content:
application/json:
schema: {$ref: '#/components/schemas/BridgeGovernedCapabilities'}
/v2/writer-lease:
post:
operationId: acquireWriterLease
summary: Acquire the exact governed writer lease for a task generation
security: [{bearerAuth: []}]
requestBody:
required: true
content:
application/json:
schema: {$ref: '#/components/schemas/BridgeWriterLeaseRecoveryRequest'}
responses:
'200':
description: Exact detached worktree lease binding
content:
application/json:
schema: {$ref: '#/components/schemas/BridgeWriterLeaseResponse'}
'400': {description: Invalid lease binding}
'409': {description: Branch, base, or generation conflict}
x-openai-isConsequential: true
/v2/writer-lease/bootstrap:
post:
operationId: bootstrapWriterLease
summary: Bootstrap a governed writer runtime without creating a durable lease
description: >
Staging-only runtime recovery. It validates the exact task, repository,
base OID, expected branch OID, target branch, and generation. It must
not create a branch, insert a branch lease, or consume a generation.
security: [{bearerAuth: []}]
requestBody:
required: true
content:
application/json:
schema: {$ref: '#/components/schemas/BridgeWriterLeaseRecoveryRequest'}
responses:
'200':
description: Runtime binding bootstrapped or returned idempotently
content:
application/json:
schema: {$ref: '#/components/schemas/BridgeWriterLeaseRecoveryResponse'}
'400': {description: Invalid lease binding}
'409': {description: A matching durable lease already exists or binding conflicts}
'503': {description: Runtime or durable state unavailable}
x-openai-isConsequential: true
/v2/writer-lease/renew:
post:
operationId: renewWriterLease
summary: Recover an expired governed writer lease without changing its binding
description: >
Renews only the current task generation and exact repository, base OID,
expected branch OID, and target branch. Durable renewal is revision-CAS
protected; runtime rehydration is reported separately.
security: [{bearerAuth: []}]
requestBody:
required: true
content:
application/json:
schema: {$ref: '#/components/schemas/BridgeWriterLeaseRecoveryRequest'}
responses:
'200':
description: Lease recovered or idempotently observed as already active
content:
application/json:
schema: {$ref: '#/components/schemas/BridgeWriterLeaseRecoveryResponse'}
'400': {description: Invalid schema, generation, or lease duration}
'409': {description: Task, base, repository, branch, generation, or durable lease conflict}
'503': {description: Durable job store or runtime unavailable}
x-openai-isConsequential: true
/v2/commit-file:
post:
operationId: commitFileGoverned
summary: Commit one file through the governed writer lease
security: [{bearerAuth: []}]
requestBody:
required: true
content:
application/json:
schema: {$ref: '#/components/schemas/BridgeCommitFileRequest'}
responses:
'200':
description: Authority and remote visibility receipt
content:
application/json:
schema: {$ref: '#/components/schemas/BridgeCommitResponse'}
'409': {description: Lease, expected-head, idempotency, or repository conflict}
'503': {description: Reconciliation required}
x-openai-isConsequential: true
/v2/commit-files:
post:
operationId: commitFilesGoverned
summary: Atomically commit a bounded set of files through the governed writer lease
security: [{bearerAuth: []}]
requestBody:
required: true
content:
application/json:
schema: {$ref: '#/components/schemas/BridgeCommitFilesRequest'}
responses:
'200':
description: Authority and remote visibility receipt
content:
application/json:
schema: {$ref: '#/components/schemas/BridgeCommitResponse'}
'409': {description: Lease, expected-head, idempotency, or repository conflict}
'503': {description: Reconciliation required}
x-openai-isConsequential: true
@@ -1,54 +0,0 @@
# R4.2 P0-A TaskPlane Scope Isolation Design
Status: approved candidate design. This document is not evidence of a protected-main merge or production deployment.
## Boundary
- Task: `r42-p0a-taskplane-scope-isolation-20260816`
- Governed Job: `job-3fd743b51971c6007923699c1233f84c`
- Repository: `aios/aios`
- Base: `0eb6e1a864edfdaeef37b438991707a110e21558`
- Target branch: `agent/r42-p0a-taskplane-scope-isolation-20260816`
- Generation: `1`
The implementation reuses this exact boundary. It does not create a replacement Task, Job, branch, generation, or idempotency key.
## Model and invariants
`TaskPlan` gains first-class `scope_type`, `tenant_id`, `user_id`, `project_id`, `conversation_id`, and `run_id` fields. The fields participate in accepted-key validation, parsing, serialization, persistence, and authorization.
Historical rows without scope data normalize to `scope_type=legacy`; no tenant is inferred. Tenant-scoped tasks require a non-empty `tenant_id`. System/global resources use an explicit `scope_type=system` invariant and do not receive a fabricated tenant. A task may add user, project, conversation, and run ownership within its tenant.
## Trusted ActorContext
TaskPlane defines a small immutable `TaskActorContext` used only for authorization. The HTTP authenticator returns an identity. `PolicyEngine` resolves that identity through server-owned policy configuration into the effective context and capabilities. Request fields such as `tenant_id`, `user_id`, `group_id`, and `role` are declarations only and never expand authority.
Public create, list, and get paths always use the resolved context. Internal compatibility calls may omit a context and retain explicit legacy behavior, but the HTTP adapter never uses that path.
## Persistence
SQLite remains the current runtime. `_create_schema()` performs an idempotent forward migration that adds the six scope columns and indexes, then backfills only missing `scope_type` values to `legacy`.
New writes persist scope in both the canonical `task_json` representation and first-class columns. Reads accept historical JSON without scope, using legacy columns, but reject a new-row mismatch between JSON and columns. SQL list/get predicates are parameterized and applied before row hydration.
PostgreSQL remains a separate migration target and is not claimed by this change.
## Authorization flow
1. HTTP bearer authentication yields an identity.
2. `PolicyEngine` verifies capability and resolves the identity to `TaskActorContext`.
3. Create binds the requested scope to the trusted context and rejects mismatches.
4. List/get pass the trusted context to `TaskPlaneStore`.
5. Store applies tenant and ownership predicates in SQL; privileged capabilities may explicitly cross scope.
Tenant actors cannot read legacy tasks, another tenant, or mismatched owned resources. Legacy actors can only read legacy tasks. System actors require explicit privileged capability for cross-scope access.
## Failure behavior
Missing tenant scope, forged scope, unknown scope types, JSON/column divergence, and unauthorized list/get operations fail closed with canonical TaskPlane errors. Public get does not reveal whether an out-of-scope task exists.
## Verification
Tests exercise real model round trips, SQLite migration/restart, Service/Policy/Store wiring, tenant A versus tenant B list/get denial, ownership filtering, forged create payload rejection, legacy isolation, privileged access, and SQL parameterization. Existing TaskPlane API, RBAC, recovery/security, and security-contract suites remain mandatory.
The candidate is complete only after real `TASK_CLAIMED`, `TASK_HEARTBEAT`, exact WriterLease, governed commit, OID convergence, result submission, and `TASK_VERIFIED`. Protected main and production remain unchanged.
@@ -1,90 +0,0 @@
# R4.2 P0-C Trace / Tool-MCP / Credential Governance
## Status boundary
This change is a candidate implementation on the governed P0-C branch. It does
not promote protected `main`, deploy Production, or assert that all R4.2 target
architecture is complete.
## Scope model
Governance resources use an explicit `scope_type`:
- `tenant` requires `tenant_id` and may narrow to user, project,
conversation, and run;
- `system` and `global` prohibit tenant ownership fields;
- `legacy` preserves unscoped historical records without inventing ownership.
System and global tools therefore never receive a synthetic tenant merely to
satisfy a tenant-oriented schema.
## Trace lineage
`TaskPlan` and `MessageEnvelope` scope is bound to a `TraceContext`. The trace
records `trace_id`, `span_id`, parent span, correlation, causation, task,
message, and the canonical P0-A scope. Child spans must keep the parent's trace,
task, and scope. Privileged visibility does not permit cross-scope trace
splicing.
`MessageEnvelope.correlation_id` is persisted and identity-bound. Existing
handoff databases are upgraded in place with an idempotent column migration;
legacy rows retain their prior event IDs because a null correlation field is
not added to the historical identity material.
Observability keeps its legacy presentation fields, but new governed records
also carry immutable trace and scope fields. Scoped records require a trusted
server actor for queries. A repeated execution ID is an idempotent replay only
when the complete record matches; conflicting overwrites are denied.
## Tool and MCP governance
`ToolDescriptor` binds a tool or MCP server to:
- an explicit resource scope;
- required actor capabilities;
- an optional credential reference.
Invocation authorization requires exact propagation of the trace scope and a
capability superset. Unknown tools, cross-tenant calls, scope substitution, and
non-privileged access to system/global resources fail closed. Every successful
registration and invocation appends hash-chained audit evidence.
## Credential governance
Credentials are represented only by references whose schemes are
`systemd-credential`, `vault`, or `secret-manager`. Raw bearer tokens, passwords,
and arbitrary values are rejected. Grants are bound to scope, operation, task,
tool, and expiry. Public/audit records contain a reference hash where disclosure
is unnecessary; secret material is never persisted by this module.
The existing runtime credential broker remains backward compatible for system
leases, adds explicit tenant/system/global invariants, and rejects providers
that attempt to return raw token/secret/password environment values instead of
file/reference indirection.
## Durable evidence
The SQLite governance store is restart-safe and idempotently initializes four
tables: governed tools, trace spans, credential grants, and audit evidence.
Audit entries form a SHA-256 chain. Acceptance requires the chain to verify and
contain the required actions; missing evidence is not treated as success.
## Acceptance tests
The candidate must pass:
- `TRACE_LINEAGE_CONTINUITY`;
- `TRACE_SCOPE_MISMATCH_DENY`;
- `TOOL_MCP_AUTHZ_DENY`;
- `TOOL_SCOPE_PROPAGATION`;
- `CREDENTIAL_SCOPE_DENY`;
- `CREDENTIAL_SECRET_EXPOSURE_DENY`;
- `AUDIT_EVIDENCE_COMPLETENESS`;
- P0-A scope and P0-B handoff regression suites;
- Python compilation, `git diff --check`, and a diff-scoped secret scan.
The repository-wide baseline currently has seven unrelated failures in the
isolated host environment (five stale continuation-contract fixtures and two
host-permission fixtures). The same seven failures reproduce at the untouched
`a642b045775b9154a00ae78543037fbef4fb447b` base; they are not suppressed or
modified by P0-C.
@@ -1,75 +0,0 @@
# R4.2 P1 — Durable Online Agent Execution
Status: candidate implementation. This document does not imply protected-main promotion or Production deployment.
## State model
The generic worker uses two local durable layers:
1. `tasks/<task_id>.json`: atomic fast-state cache for compatibility and quick restart.
2. `checkpoints.sqlite3`: append-only attempt/phase journal with `synchronous=FULL`.
Checkpoint identity is bound to:
- task_id
- attempt
- execution_digest
- job_id
- repository
- base_oid
- expected_branch_oid
- target_branch
- generation
A checkpoint from another repository/branch/generation is never reused.
Phases are monotonic within an attempt:
`claimed → workspace_prepared → verification_passed → source_capture_done|commit_done → result_pending → result_submitted`
`reconciliation_required` is an explicit recovery phase.
## Attempt-aware result idempotency
Result submission idempotency keys include the official Task attempt and the execution digest. The same
attempt reuses the same key after a timeout/restart; a later official attempt cannot collide with an
earlier result.
Before submitting a result the worker durably writes `result_pending`, including the result hash and
idempotency key. After a successful or exact-idempotency-reconciled API response it writes
`result_submitted`.
A 409 is accepted only if the Task API already contains the exact same result body and the Task is
VERIFYING/SUCCEEDED. A conflicting result remains a hard failure.
## Resume rules
- `source_capture_done` and `result_pending` may be reused across later official attempts only when the
execution/scope binding is identical. This avoids repeating read-only source capture.
- A deterministic task that crashed before governed commit replays from a new clean detached worktree.
This is safe because there is no external side effect yet.
- `commit_done` is never recommitted. Recovery requires exact Job receipt OID convergence plus exactly
one ACTIVE WriterLease whose `current_head_oid` equals the governed commit.
- delayed Git visibility remains `RECONCILIATION_REQUIRED`, preserving an explicit recovery entry.
## Crash cleanup
On worker startup, stale `task-*` workspaces under the configured workspace root are removed before
polling begins. The path boundary is verified before deletion.
## Contracts
P1 gates include:
- CHECKPOINT_DURABLE_WRITE
- RESUME_FROM_CHECKPOINT
- RETRY_IDEMPOTENT_NO_DUPLICATE_SIDE_EFFECT
- CRASH_RECOVERY_CONVERGENCE
- WORKER_RESTART_RESUME
- CHECKPOINT_SCOPE_ISOLATION
- RESULT_SUBMISSION_REPLAY_SAFE
- WORKSPACE_CLEANUP_RECOVERY
- LEASE_RENEWAL_RECOVERY
WriterLease and governed Job authority remain in the existing Bridge/GitOps durable stores; the worker
checkpoint DB is not a competing authority for Git state.
@@ -1,79 +0,0 @@
# R4.2 P2 Compact Interface Map Runtime
## Purpose
The persistent Online Agent already supports the canonical read-only `source_capture` action, but
its `SourceCaptureExecutor` always returns an `aios.engineering.source-snapshot.v2` tar.gz+base64
payload. That is correct for implementation source transfer, but too large for control-plane
consumers that only need a bounded implementation map.
P2 adds a new **adapter**, not a new execution action:
```text
action=source_capture
adapter=compact_interface_map
```
This preserves the existing execution-v2 schema and Bridge policy:
- read-only repository;
- no change set;
- no verification commands;
- `writer_lease=false`;
- no branch or commit;
- existing capability and execution-digest validation.
## Compact map contract
`aios_online_agent_compact_map.py` scans only approved repository roots and source suffixes. It
returns:
- at most 40 relevant modules;
- at most 80 selected Python symbols in total;
- at most 40 test symbols;
- at most 25 excerpts;
- at most 1200 characters per excerpt;
- SQL table names;
- systemd unit names;
- current invariants, failure windows and reuse points;
- P2 fleet contract-to-surface mappings.
The result is hard-bounded to 120 KiB and contains no source archive or `payload_b64`.
Source excerpts apply a conservative sensitive-pattern redaction before result construction.
## P2 contracts
The map supplies evidence surfaces for:
- `FLEET_AGENT_REGISTRATION`
- `FLEET_CAPACITY_SCHEDULING`
- `FLEET_FAIRNESS`
- `FLEET_FAULT_ISOLATION`
- `FLEET_WORKER_REASSIGNMENT`
- `FLEET_SCOPE_ISOLATION`
- `FLEET_IDEMPOTENT_DISPATCH`
- `FLEET_FAILURE_RECOVERY`
Missing first-class scheduling/fleet primitives are reported as gaps rather than invented.
## Result
When the adapter is selected, the worker returns:
```text
status=SUCCEEDED
classification=P2_INTERFACE_MAP_CAPTURED
execution_mode=compact_interface_map
writer_lease=NONE
```
The existing `source_capture_done` / P1 checkpoint/retry path is reused, so the compact analysis
inherits attempt-aware result idempotency and restart-safe result submission.
## Governance
This capability is runtime infrastructure only. The implementation candidate may use a governed
branch/WriterLease for its own code change, but any task executed through `compact_interface_map`
is read-only and must not create a WriterLease, branch, commit, protected-ref write, or Production
side effect.
@@ -1,9 +0,0 @@
# R4.2 P0-A TaskPlane Scope Isolation
Status: candidate implementation. This is not proof of protected-main promotion or production deployment.
Task scope is now represented by `scope_type`, `tenant_id`, `user_id`, `project_id`, `conversation_id`, and `run_id` in the model and SQLite persistence. Historical rows become `legacy`; their tenant is never guessed. Tenant tasks require `tenant_id`. System resources use `scope_type=system` without a fabricated tenant.
The HTTP adapter uses the authenticated identity. `PolicyEngine` resolves that identity through server-owned `identity_actor_contexts`; request payload fields do not grant scope. Public create binds the trusted scope, and public list/get apply parameterized scope predicates in `TaskPlaneStore`.
SQLite is the current runtime database. PostgreSQL remains a separate migration and runtime-acceptance gate.
@@ -1,38 +0,0 @@
# R4.2 P0-B — Durable Message / Handoff Fabric
Status: candidate implementation; not evidence of protected-main merge or Production deployment.
`MessageEnvelope` is the single durable handoff identity and carries source/target/type, tenant/user/
project/conversation/task/run scope, causation, sequence, capability scope, confidentiality scope,
idempotency key, expiry and delivery state.
Identity includes **target + scope + sequence + idempotency key + payload hash**, fixing the existing
AgentEventBus target collision. Server ActorContext is authoritative; caller scope may narrow but may
not widen/forge trusted tenant/user/project/conversation/run fields.
`DurableHandoffStore` provides SQLite durability in the current runtime. Each logical stream has
unique `(stream_key, idempotency_key)` and `(stream_key, sequence)`. Exact retry returns `REPLAY`;
changed payload under the same key is rejected; sequence gaps are rejected; expired messages are not
delivered.
Confidentiality scopes:
- public: no tenant requirement
- tenant: same tenant
- project: same tenant + project
- private: same tenant + project + user
- run: same tenant + project + user + run
Required `capability_scope` must be present on the authenticated delivery actor unless the actor is
explicitly administrative.
Compatibility adapters convert AgentEventBus events, TaskPlane CloudEvents and legacy Agent OS
MessageBus messages into the durable envelope. Existing message_outbox remains intact as a migration
source; P0-B does not delete or silently rewrite it.
Required gates:
`MESSAGE_SCOPE_MISMATCH_DENY`, `MESSAGE_TARGET_COLLISION_DENY`, `MESSAGE_IDEMPOTENCY_REPLAY`,
`MESSAGE_SEQUENCE_GAP_DETECT`, `MESSAGE_CONFIDENTIALITY_SCOPE_ENFORCE`,
`HANDOFF_CROSS_TENANT_DENY`, plus existing P0-A TaskPlane scope/security regressions.
SQLite remains current runtime persistence. PostgreSQL remains a separate migration gate.
@@ -0,0 +1,24 @@
{
"schema": "aios.source-registry-reconciliation.v1",
"version": "V2R2C1",
"root_cause": "PROMOTION_STATE_MACHINE_FINALIZED_BEFORE_SOURCE_REGISTRY_CONVERGENCE",
"main_before": "3244f0f17f8845592ac94d585f5ebd65693c9f29",
"tag_oid": "07fc2746b688f5f243f29b39de39f32edc3dd9d2",
"target_oid": "998d30304787e31e5ae607c0bd86b5edb2b0e5ad",
"projection_sha256": "924dfc0ec58afc4e31638f6538e551c5bc8858f804cdbee5af7dce6eba7c90fe",
"evidence_closure_oid": "a8b906d1bc24088c19647e98de6a679268f6d0a2",
"approval_plan_id": "sha256:c0df52008ae7f64cec5ee2d28947aa7b892066c670ab8295e4549140b1a7500e",
"remote_tag_verified": true,
"remote_target_verified": true,
"gitea_api_verified_by_owner_acceptance": true,
"source_entry_before": "missing",
"source_entry_added": "versions/V2R2C1.json",
"source_entry_is_exact_attested_projection": true,
"release_manifest_added": false,
"release_manifest_reason": "Historical release-manifest schema differs; no canonical V2R2C1 source-manifest generator was proven. Do not synthesize one by copying old release values.",
"runtime_backing": "immutable WS/versions bundle",
"runtime_mutated": false,
"tag_rewritten": false,
"baseline_modified": false,
"production_modified": false
}
-5
View File
@@ -48,8 +48,3 @@ class MessageBus:
messages = list(self.queue)
self.queue.clear()
return messages
# R4.2 P0-B compatibility adapter.
def to_handoff_envelope(message, *, target=None, sequence=1, idempotency_key=None, **scope):
from message_handoff_adapters import legacy_message_to_envelope
return legacy_message_to_envelope(message, target=target, sequence=sequence, idempotency_key=idempotency_key, **scope)
+19 -1
View File
@@ -32,18 +32,36 @@ def build_parser() -> argparse.ArgumentParser:
parser.add_argument("--tree-oid", required=True)
parser.add_argument("--output-directory", required=True, type=Path)
parser.add_argument("--name", default="aios-autonomous-release-candidate")
parser.add_argument("--release-version")
parser.add_argument("--release-binding-json", type=Path)
parser.add_argument("--artifact-mode", choices=("release", "general"))
return parser
def main(argv: list[str] | None = None) -> int:
args = build_parser().parse_args(argv)
release_binding = (
json.loads(args.release_binding_json.read_text(encoding="utf-8"))
if args.release_binding_json is not None
else None
)
if release_binding is not None and not isinstance(release_binding, dict):
raise ValueError("release binding JSON must be an object")
output = args.output_directory
output.mkdir(parents=True, exist_ok=True, mode=0o750)
if output.is_symlink():
raise RuntimeError("artifact output directory symlink rejected")
os.chmod(output, output.stat().st_mode & 0o750)
builder = ArtifactBuilder()
built = builder.build_from_directory(args.source_oid, args.tree_oid, args.source)
built = builder.build_from_directory(
args.source_oid,
args.tree_oid,
args.source,
release_version=args.release_version,
release_binding=release_binding,
release_mode=args.release_version is not None,
artifact_mode=args.artifact_mode,
)
builder.verify(built)
artifact_path = output / f"{args.name}.tar"
manifest_path = output / f"{args.name}.manifest.json"
+124
View File
@@ -0,0 +1,124 @@
#!/usr/bin/env python3
"""Generate one canonical source entry for a governed non-main change.
This utility only creates the file in the caller's worktree. It never commits,
pushes, changes main, rewrites a tag, or updates a production projection. The
caller must submit the resulting file through the existing governed writer and
approval flow.
"""
from __future__ import annotations
import argparse
import json
import os
import sys
from pathlib import Path
APP = Path(__file__).resolve().parents[1] / "active-runtime" / "app"
sys.path.insert(0, str(APP))
from version_registry import ( # noqa: E402
build_source_registry_entry,
configured_authoritative_git_dir,
configured_repository_identity,
source_registry_json,
validate_source_registry_record,
verify_approval_authority,
verify_authoritative_repository,
verify_source_registry_git_identity,
)
def parser() -> argparse.ArgumentParser:
value = argparse.ArgumentParser()
value.add_argument("--output", required=True, type=Path)
value.add_argument("--version", required=True)
value.add_argument("--tag-oid", required=True)
value.add_argument("--target-oid", required=True)
value.add_argument("--peeled-target-oid", required=True)
value.add_argument("--evidence-closure-oid", required=True)
value.add_argument("--approval-plan-id", required=True)
value.add_argument(
"--repository",
type=Path,
default=configured_authoritative_git_dir(),
help="authoritative bare repository; required for release generation",
)
value.add_argument(
"--approval-authority",
type=Path,
default=(
Path(os.environ["AIOS_GITOPS_APPROVAL_AUTHORITY"])
if os.environ.get("AIOS_GITOPS_APPROVAL_AUTHORITY")
else None
),
help="canonical approval/publication receipt for this release request",
)
value.add_argument(
"--repository-identity",
default=configured_repository_identity(),
)
return value
def main() -> int:
args = parser().parse_args()
if args.approval_authority is None:
raise ValueError("canonical approval authority is required")
verify_authoritative_repository(
args.repository,
repository_identity=args.repository_identity,
)
tag_oid = args.tag_oid.lower()
target_oid = args.target_oid.lower()
peeled_target_oid = args.peeled_target_oid.lower()
evidence_closure_oid = args.evidence_closure_oid.lower()
expected = build_source_registry_entry(
version=args.version,
tag_oid=tag_oid,
target_oid=target_oid,
peeled_target_oid=peeled_target_oid,
evidence_closure_oid=evidence_closure_oid,
approval_plan_id=args.approval_plan_id,
repository=args.repository_identity,
)
verify_source_registry_git_identity(args.repository, expected)
verify_approval_authority(args.approval_authority, expected)
destination = args.output.resolve()
if destination.name != f"{args.version}.json" or destination.parent.name != "versions":
raise ValueError("output must be versions/<version>.json")
payload = source_registry_json(expected)
destination.parent.mkdir(parents=True, exist_ok=True)
if destination.is_symlink():
raise ValueError("existing source registry path must not be a symlink")
if destination.exists() and not destination.is_file():
raise ValueError("existing source registry path must be a regular file")
if destination.exists():
current = json.loads(destination.read_text(encoding="utf-8"))
validate_source_registry_record(
current,
expected_version=args.version,
expected_tag_oid=tag_oid,
expected_target_oid=target_oid,
expected_evidence_closure_oid=evidence_closure_oid,
expected_approval_plan_id=args.approval_plan_id,
)
print(json.dumps({"status": "EXISTING_IDENTITY_VERIFIED", "path": str(destination)}))
return 0
descriptor = os.open(
destination,
os.O_WRONLY | os.O_CREAT | os.O_EXCL | getattr(os, "O_NOFOLLOW", 0),
0o640,
)
try:
os.write(descriptor, payload)
os.fsync(descriptor)
finally:
os.close(descriptor)
print(json.dumps({"status": "GENERATED", "path": str(destination)}))
return 0
if __name__ == "__main__":
raise SystemExit(main())
@@ -8,6 +8,7 @@ import io
import json
import os
from pathlib import Path, PurePosixPath
import re
import stat
import tarfile
from typing import Mapping
@@ -75,6 +76,30 @@ class BuiltArtifact:
manifest: ArtifactManifest
@dataclass(frozen=True, slots=True)
class ReleaseIdentityBinding:
"""Immutable release identity required for a release artifact build."""
version: str
repository: str
tag_oid: str
target_oid: str
evidence_closure_oid: str
approval_plan_id: str
peeled_target_oid: str | None = None
def as_dict(self) -> dict[str, str]:
return {
"version": self.version,
"repository": self.repository,
"tag_oid": self.tag_oid,
"target_oid": self.target_oid,
"peeled_target_oid": self.peeled_target_oid or self.target_oid,
"evidence_closure_oid": self.evidence_closure_oid,
"approval_plan_id": self.approval_plan_id,
}
class ArtifactBuilder:
MAX_FILE_BYTES = 8 * 1024 * 1024
MAX_ARTIFACT_BYTES = 64 * 1024 * 1024
@@ -92,14 +117,188 @@ class ArtifactBuilder:
if not is_git_oid(value):
raise ValueError(f"{name} must be a full lowercase Git OID")
@staticmethod
def _binding_dict(
binding: ReleaseIdentityBinding | Mapping[str, str],
) -> dict[str, str]:
if isinstance(binding, ReleaseIdentityBinding):
return binding.as_dict()
values = {str(key): str(value) for key, value in binding.items()}
values.setdefault("peeled_target_oid", values.get("target_oid", ""))
return values
@classmethod
def _validate_source_registry(
cls,
root: Path,
release_version: str,
release_binding: ReleaseIdentityBinding | Mapping[str, str] | None = None,
*,
release_mode: bool = False,
) -> dict[str, str]:
if not re.fullmatch(r"V\d+R\d+C\d+(?:-[A-Za-z0-9_.-]+)?", release_version):
raise ReleaseBlocked(
"ARTIFACT_SOURCE_REGISTRY_INVALID", "release version is not canonical"
)
path = root / "versions" / f"{release_version}.json"
if path.is_symlink() or not path.is_file():
raise ReleaseBlocked(
"ARTIFACT_SOURCE_REGISTRY_MISSING",
f"artifact source is missing versions/{release_version}.json",
)
try:
value = json.loads(path.read_text(encoding="utf-8"))
except (OSError, UnicodeDecodeError, json.JSONDecodeError) as exc:
raise ReleaseBlocked(
"ARTIFACT_SOURCE_REGISTRY_INVALID",
f"artifact source registry cannot be read: {path}",
) from exc
if not isinstance(value, dict):
raise ReleaseBlocked(
"ARTIFACT_SOURCE_REGISTRY_INVALID", "source registry is not an object"
)
if value.get("version") != release_version or value.get("repository") != "aios/aios":
raise ReleaseBlocked(
"ARTIFACT_SOURCE_REGISTRY_IDENTITY_MISMATCH",
"source registry version or repository does not match the artifact",
)
if value.get("schema") not in {
"aios.source-registry.v1",
"aios.production-registry.v2",
}:
raise ReleaseBlocked(
"ARTIFACT_SOURCE_REGISTRY_INVALID", "source registry schema is invalid"
)
for field in ("tag_oid", "target_oid", "peeled_target_oid", "evidence_closure_oid"):
item = value.get(field)
if not isinstance(item, str) or not re.fullmatch(r"[0-9a-f]{40}", item):
raise ReleaseBlocked(
"ARTIFACT_SOURCE_REGISTRY_INVALID",
f"source registry field is invalid: {field}",
)
if value["peeled_target_oid"] != value["target_oid"]:
raise ReleaseBlocked(
"ARTIFACT_SOURCE_REGISTRY_IDENTITY_MISMATCH",
"source registry peeled target differs from target",
)
if not isinstance(value.get("approval_plan_id"), str) or not re.fullmatch(
r"sha256:[0-9a-f]{64}", value["approval_plan_id"]
):
raise ReleaseBlocked(
"ARTIFACT_SOURCE_REGISTRY_INVALID", "source registry approval plan is invalid"
)
if release_mode and release_binding is None:
raise ReleaseBlocked(
"ARTIFACT_RELEASE_BINDING_REQUIRED",
"release artifact builds require an expected release identity",
)
if release_binding is not None:
expected = cls._binding_dict(release_binding)
required = (
"version",
"repository",
"tag_oid",
"target_oid",
"peeled_target_oid",
"evidence_closure_oid",
"approval_plan_id",
)
if any(not expected.get(name) for name in required):
raise ReleaseBlocked(
"ARTIFACT_RELEASE_BINDING_INVALID",
"expected release identity is incomplete",
)
for name in ("tag_oid", "target_oid", "peeled_target_oid", "evidence_closure_oid"):
if not re.fullmatch(r"[0-9a-f]{40}", expected[name]):
raise ReleaseBlocked(
"ARTIFACT_RELEASE_BINDING_INVALID",
f"expected release identity field is invalid: {name}",
)
if not re.fullmatch(r"sha256:[0-9a-f]{64}", expected["approval_plan_id"]):
raise ReleaseBlocked(
"ARTIFACT_RELEASE_BINDING_INVALID",
"expected release identity approval plan is invalid",
)
for name in required:
if value.get(name) != expected[name]:
raise ReleaseBlocked(
"ARTIFACT_SOURCE_REGISTRY_IDENTITY_MISMATCH",
f"source registry does not match expected release identity: {name}",
)
return {
"version": str(value["version"]),
"repository": str(value["repository"]),
"tag_oid": str(value["tag_oid"]),
"target_oid": str(value["target_oid"]),
"peeled_target_oid": str(value["peeled_target_oid"]),
"evidence_closure_oid": str(value["evidence_closure_oid"]),
"approval_plan_id": str(value["approval_plan_id"]),
}
def build_from_directory(
self, source_oid: str, tree_oid: str, source: str | Path
self,
source_oid: str,
tree_oid: str,
source: str | Path,
release_version: str | None = None,
release_binding: ReleaseIdentityBinding | Mapping[str, str] | None = None,
release_mode: bool | None = None,
artifact_mode: str | None = None,
) -> BuiltArtifact:
root = Path(source)
if root.is_symlink() or not root.is_dir():
raise ReleaseBlocked(
"ARTIFACT_SOURCE_LINK_REJECTED", "artifact source must be a real directory"
)
if artifact_mode not in {None, "release", "general"}:
raise ValueError("artifact_mode must be release, general, or omitted")
if artifact_mode == "release" and release_mode is False:
raise ValueError("artifact_mode=release conflicts with release_mode=False")
if artifact_mode == "general" and release_mode is True:
raise ValueError("artifact_mode=general conflicts with release_mode=True")
explicit_release_version = release_version is not None
detected_canonical_version = False
if release_version is None:
version_file = root / "VERSION"
if version_file.is_file() and not version_file.is_symlink():
release_version = version_file.read_text(encoding="utf-8").strip()
detected_canonical_version = bool(
re.fullmatch(r"V\d+R\d+C\d+(?:-[A-Za-z0-9_.-]+)?", release_version)
)
elif re.fullmatch(r"V\d+R\d+C\d+(?:-[A-Za-z0-9_.-]+)?", release_version):
detected_canonical_version = True
binding_expected: dict[str, str] | None = None
if release_binding is not None:
binding_expected = self._binding_dict(release_binding)
if not binding_expected.get("version"):
raise ReleaseBlocked(
"ARTIFACT_RELEASE_BINDING_INVALID",
"release identity binding must include version",
)
release_version = release_version or binding_expected["version"]
release_requested = bool(
explicit_release_version
or detected_canonical_version
or release_binding is not None
or release_mode is True
or artifact_mode == "release"
)
if artifact_mode == "general" and release_requested:
raise ReleaseBlocked(
"ARTIFACT_GENERAL_MODE_RELEASE_VERSION",
"general artifacts cannot contain canonical release metadata",
)
if release_requested:
release_mode = True
self._validate_source_registry(
root,
release_version or "",
release_binding,
release_mode=True,
)
files: list[ArtifactFile] = []
for directory, dirnames, filenames in os.walk(root, followlinks=False):
current = Path(directory)
@@ -61,6 +61,9 @@ def build_parser() -> argparse.ArgumentParser:
artifact_build.add_argument("--source-oid", required=True)
artifact_build.add_argument("--tree-oid", required=True)
artifact_build.add_argument("--output-directory", required=True, type=Path)
artifact_build.add_argument("--release-version")
artifact_build.add_argument("--release-binding-json", type=Path)
artifact_build.add_argument("--artifact-mode", choices=("release", "general"))
artifact_verify = artifact_sub.add_parser("verify")
artifact_verify.add_argument("--artifact", required=True, type=Path)
artifact_verify.add_argument("--manifest", required=True, type=Path)
@@ -145,7 +148,20 @@ def main(argv: list[str] | None = None) -> int:
if args.command == "artifact":
builder = ArtifactBuilder()
if args.artifact_command == "build":
built = builder.build_from_directory(args.source_oid, args.tree_oid, args.source)
release_binding = (
load_json(args.release_binding_json)
if args.release_binding_json is not None
else None
)
built = builder.build_from_directory(
args.source_oid,
args.tree_oid,
args.source,
release_version=args.release_version,
release_binding=release_binding,
release_mode=args.release_version is not None,
artifact_mode=args.artifact_mode,
)
builder.verify(built)
artifact_path = args.output_directory / "candidate.tar"
manifest_path = args.output_directory / "manifest.json"
@@ -1,11 +1,35 @@
import json
from pathlib import Path
import pytest
from aios_autonomous_release_candidate.artifact import ArtifactBuilder, ArtifactFile, BuiltArtifact
from aios_autonomous_release_candidate.artifact import (
ArtifactBuilder,
ArtifactFile,
BuiltArtifact,
ReleaseIdentityBinding,
)
from aios_autonomous_release_candidate.errors import EvidenceIncomplete, ReleaseBlocked
def _source_registry(root: Path) -> None:
target = "2" * 40
value = {
"schema": "aios.source-registry.v1",
"version": "V2R2C1",
"repository": "aios/aios",
"tag_oid": "1" * 40,
"tag_object": "1" * 40,
"target_oid": target,
"peeled_target_oid": target,
"evidence_closure_oid": "3" * 40,
"approval_plan_id": "sha256:" + "4" * 64,
}
path = root / "versions" / "V2R2C1.json"
path.parent.mkdir(parents=True)
path.write_text(json.dumps(value) + "\n", encoding="utf-8")
def test_artifact_is_deterministic_and_reverified() -> None:
builder = ArtifactBuilder()
files = (
@@ -32,6 +56,143 @@ def test_source_tree_rejects_symlink_and_hardlink(tmp_path: Path) -> None:
ArtifactBuilder().build_from_directory("a" * 40, "b" * 40, root)
def test_release_source_tree_requires_source_registry_entry(tmp_path: Path) -> None:
root = tmp_path / "source"
root.mkdir()
(root / "VERSION").write_text("V2R2C1\n", encoding="utf-8")
with pytest.raises(ReleaseBlocked, match="ARTIFACT_SOURCE_REGISTRY_MISSING"):
ArtifactBuilder().build_from_directory("a" * 40, "b" * 40, root)
def test_canonical_version_without_binding_fails_closed(tmp_path: Path) -> None:
root = tmp_path / "source"
root.mkdir()
(root / "VERSION").write_text("V2R2C1\n", encoding="utf-8")
_source_registry(root)
with pytest.raises(ReleaseBlocked, match="ARTIFACT_RELEASE_BINDING_REQUIRED"):
ArtifactBuilder().build_from_directory("a" * 40, "b" * 40, root)
def test_release_source_tree_requires_expected_release_identity(tmp_path: Path) -> None:
root = tmp_path / "source"
root.mkdir()
(root / "VERSION").write_text("V2R2C1\n", encoding="utf-8")
_source_registry(root)
binding = ReleaseIdentityBinding(
version="V2R2C1",
repository="aios/aios",
tag_oid="1" * 40,
target_oid="2" * 40,
evidence_closure_oid="3" * 40,
approval_plan_id="sha256:" + "4" * 64,
)
built = ArtifactBuilder().build_from_directory(
"a" * 40,
"b" * 40,
root,
release_binding=binding,
)
ArtifactBuilder().verify(built)
def test_canonical_version_with_malformed_binding_fails_closed(tmp_path: Path) -> None:
root = tmp_path / "source"
root.mkdir()
(root / "VERSION").write_text("V2R2C1\n", encoding="utf-8")
_source_registry(root)
with pytest.raises(ReleaseBlocked, match="ARTIFACT_RELEASE_BINDING_INVALID"):
ArtifactBuilder().build_from_directory(
"a" * 40,
"b" * 40,
root,
release_binding={
"version": "V2R2C1",
"repository": "aios/aios",
"tag_oid": "not-an-oid",
},
)
def test_explicit_general_artifact_without_release_version_passes(tmp_path: Path) -> None:
root = tmp_path / "source"
root.mkdir()
(root / "README.txt").write_text("general candidate\n", encoding="utf-8")
built = ArtifactBuilder().build_from_directory(
"a" * 40,
"b" * 40,
root,
artifact_mode="general",
)
assert "README.txt" in built.manifest.files
def test_explicit_general_mode_rejects_canonical_version(tmp_path: Path) -> None:
root = tmp_path / "source"
root.mkdir()
(root / "VERSION").write_text("V2R2C1\n", encoding="utf-8")
_source_registry(root)
with pytest.raises(ReleaseBlocked, match="ARTIFACT_GENERAL_MODE_RELEASE_VERSION"):
ArtifactBuilder().build_from_directory(
"a" * 40,
"b" * 40,
root,
artifact_mode="general",
)
@pytest.mark.parametrize(
"field,wrong",
[
("repository", "other/repository"),
("version", "V2R2C2"),
("tag_oid", "9" * 40),
("target_oid", "8" * 40),
("peeled_target_oid", "7" * 40),
("evidence_closure_oid", "6" * 40),
("approval_plan_id", "sha256:" + "5" * 64),
],
)
def test_release_source_tree_rejects_every_identity_mismatch(
tmp_path: Path, field: str, wrong: str
) -> None:
root = tmp_path / "source"
root.mkdir()
(root / "VERSION").write_text("V2R2C1\n", encoding="utf-8")
_source_registry(root)
binding = {
"version": "V2R2C1",
"repository": "aios/aios",
"tag_oid": "1" * 40,
"target_oid": "2" * 40,
"peeled_target_oid": "2" * 40,
"evidence_closure_oid": "3" * 40,
"approval_plan_id": "sha256:" + "4" * 64,
}
binding[field] = wrong
with pytest.raises(ReleaseBlocked, match="ARTIFACT_SOURCE_REGISTRY"):
ArtifactBuilder().build_from_directory(
"a" * 40,
"b" * 40,
root,
release_binding=binding,
)
def test_release_mode_without_binding_fails_closed(tmp_path: Path) -> None:
root = tmp_path / "source"
root.mkdir()
(root / "VERSION").write_text("V2R2C1\n", encoding="utf-8")
_source_registry(root)
with pytest.raises(ReleaseBlocked, match="ARTIFACT_RELEASE_BINDING_REQUIRED"):
ArtifactBuilder().build_from_directory(
"a" * 40,
"b" * 40,
root,
release_version="V2R2C1",
release_mode=True,
)
def test_artifact_rejects_secret_markers() -> None:
with pytest.raises(ReleaseBlocked, match="ARTIFACT_SECRET_MARKER"):
ArtifactBuilder().build(
@@ -45,9 +45,6 @@ class ShadowClientRBACPublicInterfaceTests(unittest.TestCase):
identity_verifier=self.IdentityVerifier({"shadow-client"}),
execution_enabled=False,
identity_capabilities={"shadow-client": set(capabilities)},
identity_actor_contexts={
"shadow-client": {"scope_type": "system"}
},
)
service = self.TaskPlaneService(store, policy=policy)
@@ -1,298 +0,0 @@
from __future__ import annotations
import json
import os
import sqlite3
import sys
import threading
import urllib.error
import urllib.request
from http.server import ThreadingHTTPServer
import pytest
ROOT = os.getcwd()
if os.path.join(ROOT, "active-runtime") not in sys.path:
sys.path.append(os.path.join(ROOT, "active-runtime"))
from app.task_plane.api import TaskPlaneRequestHandler, TaskPlaneService
from app.task_plane.errors import ForbiddenError, NotFoundError, ValidationError
from app.task_plane.models import TaskPlan
from app.task_plane.policy import PolicyEngine
from app.task_plane.store import TaskPlaneStore
def _policy() -> PolicyEngine:
return PolicyEngine(
identity_capabilities={
"actor-a": {"task.creator", "task.read"},
"actor-b": {"task.creator", "task.read"},
"reader-a": {"task.read"},
"reader-b": {"task.read"},
"owner": {"task.creator", "task.read", "task.read.all"},
"unbound": {"task.creator", "task.read"},
},
identity_actor_contexts={
"actor-a": {
"scope_type": "tenant",
"tenant_id": "tenant-a",
"user_id": "user-a",
"project_id": "project-a",
"conversation_id": "conversation-a",
"run_id": "run-a",
},
"reader-a": {
"scope_type": "tenant",
"tenant_id": "tenant-a",
"user_id": "user-a",
"project_id": "project-a",
"conversation_id": "conversation-a",
"run_id": "run-a",
},
"actor-b": {
"scope_type": "tenant",
"tenant_id": "tenant-b",
"user_id": "user-b",
"project_id": "project-b",
},
"reader-b": {
"scope_type": "tenant",
"tenant_id": "tenant-b",
"user_id": "user-b",
"project_id": "project-b",
},
"owner": {"scope_type": "system"},
},
)
def _service() -> TaskPlaneService:
return TaskPlaneService(TaskPlaneStore(":memory:"), policy=_policy())
def test_taskplan_scope_fields_round_trip_as_first_class_values() -> None:
plan = TaskPlan.from_dict(
{
"task_id": "task-scope-roundtrip",
"scope_type": "tenant",
"tenant_id": "tenant-a",
"user_id": "user-a",
"project_id": "project-a",
"conversation_id": "conversation-a",
"run_id": "run-a",
}
)
payload = plan.to_dict()
assert TaskPlan.from_dict(payload).to_dict() == payload
def test_tenant_scope_requires_tenant_id_and_system_scope_does_not() -> None:
with pytest.raises(ValidationError) as exc:
TaskPlan.from_dict({"task_id": "task-missing-tenant", "scope_type": "tenant"})
assert exc.value.error_code == "TASK_SCOPE_TENANT_ID_REQUIRED"
system = TaskPlan.from_dict(
{"task_id": "task-system", "scope_type": "system", "run_id": "system-run"}
)
assert system.tenant_id is None
def test_ACTOR_BINDING_AUTHZ_MERGE_uses_server_scope() -> None:
service = _service()
created = service.create_task(
{"task_id": "task-a", "task_type": "code.change"},
actor_identity="actor-a",
)
assert created["plan"]["tenant_id"] == "tenant-a"
assert created["plan"]["user_id"] == "user-a"
assert created["plan"]["project_id"] == "project-a"
declared = service.create_task(
{
"task_id": "task-a-declared",
"tenant_id": "tenant-a",
"user_id": "user-a",
"project_id": "project-a",
"conversation_id": "conversation-a",
"run_id": "run-a",
},
actor_identity="actor-a",
)
assert declared["plan"]["scope_type"] == "tenant"
def test_TASK_SCOPE_MISMATCH_DENY_rejects_forged_client_scope() -> None:
service = _service()
with pytest.raises(ForbiddenError) as exc:
service.create_task(
{
"task_id": "task-forged",
"scope_type": "tenant",
"tenant_id": "tenant-b",
"user_id": "user-b",
"project_id": "project-b",
},
actor_identity="actor-a",
)
assert exc.value.error_code == "TASK_SCOPE_MISMATCH_DENY"
def test_public_create_fails_closed_without_server_actor_scope() -> None:
service = _service()
with pytest.raises(ForbiddenError) as exc:
service.create_task(
{"task_id": "task-unbound"}, actor_identity="unbound"
)
assert exc.value.error_code == "TASK_ACTOR_SCOPE_UNAVAILABLE"
def test_TASKPLANE_LIST_AUTHZ_DENY_and_TASK_OWNERSHIP_FILTER() -> None:
service = _service()
service.create_task({"task_id": "task-a"}, actor_identity="actor-a")
service.create_task({"task_id": "task-b"}, actor_identity="actor-b")
assert [item["task_id"] for item in service.list_tasks("reader-a")] == ["task-a"]
assert [item["task_id"] for item in service.list_tasks("reader-b")] == ["task-b"]
def test_TASKPLANE_GET_AUTHZ_DENY_does_not_disclose_cross_tenant_task() -> None:
service = _service()
service.create_task({"task_id": "task-a"}, actor_identity="actor-a")
with pytest.raises(NotFoundError):
service.get_task("task-a", "reader-b")
def test_http_authentication_drives_server_scope_and_cross_tenant_404() -> None:
service = _service()
class Handler(TaskPlaneRequestHandler):
pass
Handler.service = service
Handler.authenticator = lambda token: {
"token-a": "actor-a",
"token-b": "reader-b",
}.get(token)
server = ThreadingHTTPServer(("127.0.0.1", 0), Handler)
thread = threading.Thread(target=server.serve_forever, daemon=True)
thread.start()
base = f"http://127.0.0.1:{server.server_port}"
try:
request = urllib.request.Request(
base + "/tasks",
data=json.dumps({"task_id": "task-http-a"}).encode(),
headers={
"Authorization": "Bearer token-a",
"Content-Type": "application/json",
},
method="POST",
)
with urllib.request.urlopen(request, timeout=3) as response:
created = json.loads(response.read())
assert created["plan"]["tenant_id"] == "tenant-a"
denied = urllib.request.Request(
base + "/tasks/task-http-a",
headers={"Authorization": "Bearer token-b"},
)
with pytest.raises(urllib.error.HTTPError) as exc:
urllib.request.urlopen(denied, timeout=3)
assert exc.value.code == 404
finally:
server.shutdown()
server.server_close()
thread.join(timeout=3)
def test_mutating_endpoint_is_scoped_before_cancel() -> None:
policy = PolicyEngine(
execution_enabled=True,
identity_capabilities={
"actor-a": {"task.creator", "task.read"},
"actor-b": {"task.cancel"},
},
identity_actor_contexts={
"actor-a": {"scope_type": "tenant", "tenant_id": "tenant-a"},
"actor-b": {"scope_type": "tenant", "tenant_id": "tenant-b"},
},
)
service = TaskPlaneService(TaskPlaneStore(":memory:"), policy=policy)
service.create_task({"task_id": "task-a"}, actor_identity="actor-a")
with pytest.raises(NotFoundError):
service.cancel("task-a", {"actor_identity": "actor-b"})
def test_privileged_server_actor_can_cross_scope() -> None:
service = _service()
service.create_task({"task_id": "task-a"}, actor_identity="actor-a")
service.create_task({"task_id": "task-b"}, actor_identity="actor-b")
assert {item["task_id"] for item in service.list_tasks("owner")} == {
"task-a",
"task-b",
}
def test_scope_sql_parameters_do_not_interpolate_tenant_values() -> None:
service = _service()
service.create_task({"task_id": "task-a"}, actor_identity="actor-a")
policy = PolicyEngine(
identity_capabilities={"attacker": {"task.read"}},
identity_actor_contexts={
"attacker": {
"scope_type": "tenant",
"tenant_id": "tenant-a' OR 1=1 --",
}
},
)
attacker_service = TaskPlaneService(service.store, policy=policy)
assert attacker_service.list_tasks("attacker") == []
def test_existing_sqlite_rows_migrate_to_legacy_without_tenant_guess(tmp_path) -> None:
path = tmp_path / "legacy.sqlite3"
connection = sqlite3.connect(path)
connection.execute(
"""
CREATE TABLE tasks (
task_id TEXT PRIMARY KEY, revision INTEGER NOT NULL, status TEXT NOT NULL,
task_json TEXT NOT NULL, task_type TEXT NOT NULL, root_task_id TEXT NOT NULL,
parent_task_id TEXT, correlation_id TEXT, idempotency_key TEXT UNIQUE,
created_at REAL NOT NULL, updated_at REAL NOT NULL, failure_reason TEXT
)
"""
)
connection.execute(
"INSERT INTO tasks VALUES (?,?,?,?,?,?,?,?,?,?,?,?)",
(
"task-legacy",
1,
"CREATED",
json.dumps({"task_id": "task-legacy"}),
"code.change",
"task-legacy",
None,
None,
"legacy-key",
1.0,
1.0,
None,
),
)
connection.commit()
connection.close()
store = TaskPlaneStore(str(path))
task = store.get_task("task-legacy")
assert task.task_plan.scope_type == "legacy"
assert task.task_plan.tenant_id is None
replay = store.create_task(
TaskPlan.from_dict({"task_id": "task-legacy"}),
idempotency_key="legacy-key",
)
assert replay.task_id == "task-legacy"
assert tuple(
store.conn.execute(
"SELECT scope_type,tenant_id FROM tasks WHERE task_id='task-legacy'"
).fetchone()
) == ("legacy", None)
tenant_context = _policy().actor_context("reader-a")
with pytest.raises(NotFoundError):
store.get_task("task-legacy", tenant_context)
store.close()
-9
View File
@@ -20,12 +20,3 @@ def test_unified_event_flow():
dashboard = AgentEventDashboard(bus).overview()
assert dashboard["audit"]["enabled"] is True
def test_target_changes_event_identity(tmp_path):
bus = AgentEventBus(tmp_path)
first = bus.publish('test', 'same', 'user-a', 'same summary', {'value': 1})
second = bus.publish('test', 'same', 'user-b', 'same summary', {'value': 1})
first_id = first.get('id') if isinstance(first, dict) else getattr(first, 'id', first)
second_id = second.get('id') if isinstance(second, dict) else getattr(second, 'id', second)
assert first_id != second_id
-117
View File
@@ -1,117 +0,0 @@
from pathlib import Path
import hashlib, json, sqlite3, sys
import pytest
ROOT=Path(__file__).resolve().parents[1]
sys.path.insert(0,str(ROOT/"active-runtime/app"))
from aios_agent_task_retry_extension import RetryExtensionRequest, RetryExtensionStore, RetryExtensionError
AUTHORIZATION_ID="owner-scheme-a-20260817"
@pytest.fixture(autouse=True)
def _authorization_id(monkeypatch):
monkeypatch.setenv("AIOS_AGENT_TASK_AUTHORIZATION_ID", AUTHORIZATION_ID)
def canon(v):
return hashlib.sha256(json.dumps(v,sort_keys=True,separators=(",",":")).encode()).hexdigest()
def db(tmp_path):
p=tmp_path/"state.sqlite3"
c=sqlite3.connect(p)
c.executescript("""
CREATE TABLE tasks(
task_id TEXT PRIMARY KEY,status TEXT,attempt INTEGER,max_attempts INTEGER,revision INTEGER,
claimed_by TEXT,lease_token_hash TEXT,lease_expires_at TEXT,verification_json TEXT,result_json TEXT
);
CREATE TABLE task_events(
event_id INTEGER PRIMARY KEY AUTOINCREMENT,task_id TEXT,event_type TEXT,agent_id TEXT,
payload_json TEXT,created_at TEXT
);
""")
verification={"decision":"NEEDS_REWORK","reason":"BASE_OID_BINDING_INVALID"}
c.execute("INSERT INTO tasks VALUES (?,?,?,?,?,?,?,?,?,?)",
("task-a","FAILED",10,10,41,None,None,None,json.dumps(verification),json.dumps({"status":"FAILED"})))
c.commit(); c.close()
return p,verification
def request(v):
return RetryExtensionRequest.from_mapping({
"expected_status":"FAILED","expected_revision":41,"expected_attempt":10,"expected_max_attempts":10,
"next_max_attempts":11,"previous_verification_sha256":canon(v),
"authorization_id":AUTHORIZATION_ID,"idempotency_key":"retry-task-a-attempt11",
})
def test_retry_extension_cas_and_audit(tmp_path):
p,v=db(tmp_path)
out=RetryExtensionStore(p).extend("task-a",request(v))
assert out["status"]=="QUEUED" and out["max_attempts"]==11 and out["revision"]==42
c=sqlite3.connect(p)
assert c.execute("select status,attempt,max_attempts,revision from tasks").fetchone()==("QUEUED",10,11,42)
event=c.execute("select event_type,payload_json from task_events").fetchone()
assert event[0]=="TASK_RETRY_EXTENSION_AUTHORIZED"
assert json.loads(event[1])["authorization_id"]=="owner-scheme-a-20260817"
def test_retry_extension_idempotent_replay(tmp_path):
p,v=db(tmp_path); store=RetryExtensionStore(p); req=request(v)
first=store.extend("task-a",req); second=store.extend("task-a",req)
assert first["idempotent"] is False and second["idempotent"] is True
c=sqlite3.connect(p)
assert c.execute("select count(*) from task_retry_extensions").fetchone()[0]==1
def test_retry_extension_normalizes_stale_terminal_claim_without_active_lease(tmp_path):
p,v=db(tmp_path)
c=sqlite3.connect(p)
c.execute("update tasks set claimed_by='previous-worker'")
c.commit(); c.close()
out=RetryExtensionStore(p).extend("task-a",request(v))
assert out["status"]=="QUEUED"
c=sqlite3.connect(p)
assert c.execute(
"select claimed_by,lease_token_hash,lease_expires_at from tasks"
).fetchone()==(None,None,None)
payload=json.loads(c.execute("select payload_json from task_events").fetchone()[0])
assert payload["previous_claimed_by"]=="previous-worker"
assert payload["stale_terminal_claim_normalized"] is True
assert c.execute("select count(*) from task_events").fetchone()[0]==1
def test_retry_extension_verification_drift_denied(tmp_path):
p,v=db(tmp_path)
bad=request({"decision":"NEEDS_REWORK","reason":"other"})
with pytest.raises(RetryExtensionError,match="RETRY_EXTENSION_VERIFICATION_DRIFT"):
RetryExtensionStore(p).extend("task-a",bad)
def test_retry_extension_authorization_id_mismatch_rejected(tmp_path, monkeypatch):
p,v=db(tmp_path)
monkeypatch.setenv("AIOS_AGENT_TASK_AUTHORIZATION_ID", "other-owner-id")
with pytest.raises(RetryExtensionError,match="RETRY_EXTENSION_AUTHORIZATION_ID_MISMATCH"):
RetryExtensionStore(p).extend("task-a",request(v))
def test_retry_extension_bounded_one_step():
v={"decision":"NEEDS_REWORK"}
data={
"expected_status":"FAILED","expected_revision":41,"expected_attempt":10,"expected_max_attempts":10,
"next_max_attempts":12,"previous_verification_sha256":canon(v),
"authorization_id":"owner-scheme-a-20260817","idempotency_key":"retry-task-a-attempt11",
}
with pytest.raises(RetryExtensionError,match="RETRY_EXTENSION_MUST_BE_BOUNDED_ONE_STEP"):
RetryExtensionRequest.from_mapping(data)
def test_retry_extension_unit_is_unprivileged_and_requires_owner_binding():
unit=(ROOT/"deploy/systemd/aios-agent-task-retry-extension.service").read_text()
assert "User=aios-agent-task" in unit
assert "Group=aios-agent-task" in unit
assert "LoadCredential=agent_task_control_token:" in unit
assert "EnvironmentFile=/etc/aios-gitops/agent-task-retry-extension.env" in unit
assert "User=root" not in unit
+1 -1
View File
@@ -158,7 +158,7 @@ def test_execute_orders_registry_before_baseline_and_baseline_last(
)
monkeypatch.setattr(
"approved_release_executor.verify_artifact",
lambda binding: {"verified": True},
lambda binding, **kwargs: {"verified": True},
)
calls = []
+1
View File
@@ -73,6 +73,7 @@ class AuthoritativeGitContextTests(unittest.TestCase):
json.dumps({"branch": "patch/context"}), encoding="utf-8"
)
self.env = {
"AIOS_TEST_MODE": "1",
"AIOS_GITOPS_AUTHORITATIVE_GIT_DIR": str(self.bare),
"AIOS_GITOPS_REPOSITORY_IDENTITY": "aios/aios",
}
+2 -122
View File
@@ -2,7 +2,6 @@ import base64
from pathlib import Path
from aios_authority_git_executor import AuthorityGitExecutor
from aios_chatgpt_bridge_v2 import BridgeV2Adapter, OPERATION_REQUIRED_FIELDS
from aios_chatgpt_bridge_v2_server import BridgeHandler, RuntimeConfig
from aios_continuation_executor import GiteaContinuationRemote, ServerSideCredentialProjection
from aios_continuation_lease import WriterLeaseRequestV2
@@ -27,7 +26,6 @@ class Probe:
def test_create_new_lease_is_exact_base_and_absent():
request = WriterLeaseRequestV2(
job_id="job-bridge-runtime-lineage-test",
task_id="TASK-P0-BRIDGE-V21-DURABLE-RECONCILIATION-20260809",
repository="aios/aios",
branch_mode="CREATE_NEW",
@@ -41,7 +39,6 @@ def test_create_new_lease_is_exact_base_and_absent():
def test_create_new_lease_rejects_existing_target():
request = WriterLeaseRequestV2(
job_id="job-bridge-runtime-lineage-test",
task_id="task",
repository="aios/aios",
branch_mode="CREATE_NEW",
@@ -78,7 +75,8 @@ def test_slash_branch_uses_paginated_collection():
def test_http_route_contract_is_complete():
expected = {
"/v2/runtime", "/v2/capabilities", "/v2/governed-task",
"/v2/writer-lease", "/v2/writer-lease/bootstrap", "/v2/writer-lease/renew", "/v2/commit-file", "/v2/commit-files",
"/v2/writer-lease", "/v2/commit-file", "/v2/commit-files",
"/v2/branch/publish",
"/v2/job", "/v2/job/resume", "/v2/job/reconcile",
"/v2/tag/approval-request",
"/v2/tag/publish",
@@ -91,121 +89,3 @@ def test_http_route_contract_is_complete():
from aios_chatgpt_bridge_v2_server import ROUTES
assert set(ROUTES) == expected
def test_renew_writer_lease_is_exposed_in_action_and_capability_contracts():
adapter = BridgeV2Adapter(runtime_status={})
capabilities = adapter.getGovernedWriteCapabilities()
contract = capabilities["operation_contracts"]["renewWriterLease"]
assert contract["method"] == "POST"
assert contract["path"] == "/v2/writer-lease/renew"
assert tuple(contract["required_fields"]) == OPERATION_REQUIRED_FIELDS["renewWriterLease"]
assert "runtime_rehydrated" in contract["response_fields"]
action_schema = Path("docs/gitops/openapi-chatgpt-actions.yaml").read_text(encoding="utf-8")
assert "/v2/writer-lease/renew:" in action_schema
assert "operationId: renewWriterLease" in action_schema
assert "BridgeWriterLeaseRecoveryRequest" in action_schema
assert "BridgeWriterLeaseRecoveryResponse" in action_schema
assert {
capabilities["operation_contracts"][operation]["path"]
for operation in (
"acquireWriterLease",
"renewWriterLease",
"commitFileGoverned",
"commitFilesGoverned",
)
} == {
"/v2/writer-lease",
"/v2/writer-lease/renew",
"/v2/commit-file",
"/v2/commit-files",
}
def test_bootstrap_writer_lease_is_exposed_in_action_and_capability_contracts():
adapter = BridgeV2Adapter(runtime_status={})
capabilities = adapter.getGovernedWriteCapabilities()
contract = capabilities["operation_contracts"]["bootstrapWriterLease"]
assert contract["method"] == "POST"
assert contract["path"] == "/v2/writer-lease/bootstrap"
assert tuple(contract["required_fields"]) == OPERATION_REQUIRED_FIELDS["bootstrapWriterLease"]
assert "bootstrapped" in contract["response_fields"]
assert "durable_branch_lease_present" in contract["response_fields"]
action_schema = Path("docs/gitops/openapi-chatgpt-actions.yaml").read_text(encoding="utf-8")
assert "/v2/writer-lease/bootstrap:" in action_schema
assert "operationId: bootstrapWriterLease" in action_schema
assert "bootstrapped: {type: boolean}" in action_schema
assert "durable_branch_lease_present: {type: boolean}" in action_schema
def test_http_allows_explicit_job_id_on_lease_and_commit_requests():
handler = BridgeHandler.__new__(BridgeHandler)
lease_body = {
"schema": WriterLeaseRequestV2.schema,
"task_id": "task",
"job_id": "job-explicit-runtime-lineage",
"repository": "aios/aios",
"branch_mode": "CONTINUE_EXISTING",
"base_oid": "a" * 40,
"expected_branch_oid": "a" * 40,
"target_branch": "agent/explicit-runtime-lineage",
"generation": 2,
"lease_seconds": 120,
}
handler._allowed("acquireWriterLease", lease_body)
handler._allowed(
"commitFilesGoverned",
{
"task_id": "task",
"job_id": "job-explicit-runtime-lineage",
"repository": "aios/aios",
"expected_branch_oid": "a" * 40,
"target_branch": "agent/explicit-runtime-lineage",
"generation": 2,
"idempotency_key": "commit-explicit-runtime-lineage",
"files": {"docs/x.txt": "eA=="},
"message": "test",
},
)
def test_http_allows_omitted_job_id_for_durable_server_resolution():
handler = BridgeHandler.__new__(BridgeHandler)
lease_body = {
"schema": WriterLeaseRequestV2.schema,
"task_id": "task",
"repository": "aios/aios",
"branch_mode": "CONTINUE_EXISTING",
"base_oid": "a" * 40,
"expected_branch_oid": "a" * 40,
"target_branch": "agent/implicit-runtime-lineage",
"generation": 2,
"lease_seconds": 120,
}
handler._allowed("acquireWriterLease", lease_body)
handler._allowed(
"commitFilesGoverned",
{
"task_id": "task",
"repository": "aios/aios",
"expected_branch_oid": "a" * 40,
"target_branch": "agent/implicit-runtime-lineage",
"generation": 2,
"idempotency_key": "commit-implicit-runtime-lineage",
"files": {"docs/x.txt": "eA=="},
"message": "test",
},
)
def test_capability_contracts_return_exact_job_id_lineage():
contracts = BridgeV2Adapter(runtime_status={}).getGovernedWriteCapabilities()["operation_contracts"]
for operation in ("acquireWriterLease", "bootstrapWriterLease", "renewWriterLease"):
assert "job_id" in contracts[operation]["response_fields"]
for operation in ("commitFileGoverned", "commitFilesGoverned"):
assert "job_id" in contracts[operation]["response_fields"]
@@ -1,304 +0,0 @@
from __future__ import annotations
from pathlib import Path
import re
import json
from types import SimpleNamespace
import pytest
import yaml
from aios_chatgpt_bridge_v2_server import GovernedRuntime
from aios_continuation_executor import ContinuationExecutor
from aios_continuation_lease import WriterLeaseRequestV2
from aios_gitops_job_controller import JobState
TASK = "continuation-normalization-test"
JOB = "job-continuation-normalization-test"
REPO = "aios/aios"
BASE = "a" * 40
BRANCH = "agent/continuation-normalization-test"
class Authority:
def __init__(self, head: str | None) -> None:
self.head = head
def read_ref(self, ref: str) -> str | None:
assert ref == "refs/heads/" + BRANCH
return self.head
def commit_exists(self, oid: str) -> bool:
return oid == BASE
def branch_exists(self, branch: str) -> bool:
assert branch == BRANCH
return self.head is not None
def branch_head(self, branch: str) -> str:
assert branch == BRANCH and self.head is not None
return self.head
def is_ancestor(self, older: str, newer: str) -> bool:
return older == BASE and newer == BASE
def lease(**overrides: object) -> WriterLeaseRequestV2:
values = {
"task_id": TASK,
"job_id": JOB,
"repository": REPO,
"branch_mode": "CREATE_NEW",
"base_oid": BASE,
"expected_branch_oid": BASE,
"target_branch": BRANCH,
"generation": 1,
"lease_seconds": 120,
}
values.update(overrides)
return WriterLeaseRequestV2(**values)
def runtime(*, head: str | None = BASE, **binding_overrides: object) -> GovernedRuntime:
value = GovernedRuntime.__new__(GovernedRuntime)
value.authority = Authority(head)
binding = {
"task_id": TASK,
"job_id": JOB,
"repository": REPO,
"base_oid": BASE,
"expected_branch_oid": BASE,
"target_branch": BRANCH,
"generation": 1,
"branch_mode": "CREATE_NEW",
}
binding.update(binding_overrides)
value.bindings = {TASK: binding}
return value
def test_same_lineage_create_new_normalizes_to_continue_existing() -> None:
normalized = runtime()._normalize_runtime_lease(lease())
assert normalized.branch_mode == "CONTINUE_EXISTING"
assert normalized.task_id == TASK
assert normalized.job_id == JOB
assert normalized.repository == REPO
assert normalized.target_branch == BRANCH
assert normalized.expected_branch_oid == BASE
assert normalized.generation == 1
def test_unprovisioned_create_new_stays_create_new() -> None:
assert runtime(head=None)._normalize_runtime_lease(lease()).branch_mode == "CREATE_NEW"
@pytest.mark.parametrize(
("field", "value"),
(
("repository", "other/repo"),
("base_oid", "b" * 40),
("expected_branch_oid", "b" * 40),
("target_branch", "agent/foreign"),
("generation", 2),
),
)
def test_binding_mismatch_fails_closed(field: str, value: object) -> None:
with pytest.raises(RuntimeError, match="LEASE_BINDING_CONFLICT"):
runtime(**{field: value})._normalize_runtime_lease(lease())
def test_drifted_existing_head_fails_closed() -> None:
with pytest.raises(RuntimeError, match="EXPECTED_BRANCH_OID_CONFLICT"):
runtime(head="b" * 40)._normalize_runtime_lease(lease())
def test_openapi_contract_parses_and_requires_exact_commit_lineage() -> None:
document = yaml.safe_load(
Path("docs/gitops/openapi-chatgpt-actions.yaml").read_text(encoding="utf-8")
)
schema = document["components"]["schemas"]["BridgeCommitResponse"]
assert {
"job_id",
"commit_oid",
"commit_visible",
"branch_mode",
"target_branch",
"generation",
} <= set(schema["required"])
def test_changed_production_files_pass_secret_scan() -> None:
paths = [
Path("active-runtime/app/aios_authority_git_executor.py"),
Path("active-runtime/app/aios_chatgpt_bridge_v2_server.py"),
Path("active-runtime/app/aios_continuation_executor.py"),
Path("active-runtime/app/aios_continuation_lease.py"),
Path("active-runtime/app/authorized_task_writer_provisioning.py"),
Path("active-runtime/app/aios_chatgpt_bridge_v2.py"),
Path("docs/gitops/openapi-chatgpt-actions.yaml"),
]
expression = re.compile(
r"(?i)(BEGIN (RSA|OPENSSH|EC) PRIVATE KEY|"
r"authorization:\\s*bearer\\s+[A-Za-z0-9._-]{12,}|"
r"api[_-]?key\\s*[:=]\\s*['\\\"][^'\\\"]{12,})"
)
hits = []
for path in paths:
for number, line in enumerate(
path.read_text(encoding="utf-8", errors="replace").splitlines(), 1
):
if expression.search(line):
hits.append(f"{path}:{number}")
assert hits == []
def test_remote_verified_job_is_not_completed_when_head_cas_callback_fails() -> None:
class Store:
def read(self, _job_id):
return {
"branch": BRANCH,
"request_json": json.dumps(
{"binding": {"expected_branch_oid": BASE}}
),
}
class Controller:
def __init__(self):
self.store = Store()
self.states = []
def advance(self, _job_id, state, **_kwargs):
self.states.append(state)
return SimpleNamespace(as_dict=lambda: {"state": state.value})
executor = ContinuationExecutor.__new__(ContinuationExecutor)
executor.controller = Controller()
executor._observe_remote = lambda _branch, oid: (oid, oid, True)
def fail_cas(_oid):
raise RuntimeError("LEASE_CAS_CONFLICT")
with pytest.raises(RuntimeError, match="LEASE_CAS_CONFLICT"):
executor._finish_remote_verification(
JOB,
"b" * 40,
"b" * 40,
before_complete=fail_cas,
)
assert executor.controller.states == [JobState.REMOTE_VERIFIED]
@pytest.mark.parametrize("branch_mode", ("CREATE_NEW", "CONTINUE_EXISTING"))
def test_executor_preflight_accepts_the_exact_mode_dependent_normalized_shape(
branch_mode: str,
) -> None:
from datetime import datetime, timedelta, timezone
from aios_continuation_executor import (
ContinuationPermissions,
ContinuationRuntimeBinding,
)
request = lease(branch_mode=branch_mode)
now = datetime.now(timezone.utc)
executor = ContinuationExecutor.__new__(ContinuationExecutor)
executor.authority = Authority(None if branch_mode == "CREATE_NEW" else BASE)
executor.now = lambda: now
executor._max_generation = lambda _branch, *, exclude_job_id=None: 0
executor.workspace = SimpleNamespace(
is_detached=lambda: True,
head_oid=lambda: BASE,
)
executor.remote = SimpleNamespace(repository_identity=lambda: REPO)
binding = ContinuationRuntimeBinding(
request=request,
job_id=JOB,
workspace_path=Path("/tmp/continuation-normalization-test"),
lease_not_before=now - timedelta(seconds=1),
lease_expires_at=now + timedelta(seconds=60),
original_receipt_valid=True,
original_grant_consumed=True,
permissions=ContinuationPermissions(),
)
executor._validate_preflight(binding, job_id=JOB)
def test_create_new_executor_resumes_executing_and_uses_absence_bound_writers() -> None:
from datetime import datetime, timedelta, timezone
from aios_continuation_executor import (
ContinuationCommit,
ContinuationPermissions,
ContinuationRuntimeBinding,
)
new_oid = "b" * 40
class Store:
def read(self, _job_id: str) -> dict[str, object]:
return {"state": "EXECUTING", "revision": 4}
class Controller:
def __init__(self) -> None:
self.store = Store()
self.advanced: list[str] = []
def advance(self, _job_id: str, state: JobState, **_kwargs: object) -> object:
self.advanced.append(state.value)
return SimpleNamespace(as_dict=lambda: {"state": state.value})
class Workspace:
def commit_files(self, _files: object, _message: str) -> str:
return new_oid
class CreateAuthority:
def __init__(self) -> None:
self.created: list[tuple[str, str, str]] = []
def is_ancestor(self, older: str, newer: str) -> bool:
return older == BASE and newer == new_oid
def create_task_branch_cas(self, branch: str, base: str, new: str) -> str:
self.created.append((branch, base, new))
return new
def compare_and_swap_branch(self, *_args: object) -> str:
raise AssertionError("existing-branch CAS must not serve CREATE_NEW")
class CreateRemote:
def __init__(self) -> None:
self.created: list[tuple[str, str]] = []
def push_new_exact(self, branch: str, new: str) -> None:
self.created.append((branch, new))
def push_exact(self, *_args: object) -> None:
raise AssertionError("existing-branch push must not serve CREATE_NEW")
class Credential:
def project(self) -> None: pass
def revoke(self) -> None: pass
controller=Controller(); authority=CreateAuthority(); remote=CreateRemote()
executor=ContinuationExecutor(controller,authority,Workspace(),remote,Credential())
executor._validate_preflight=lambda *_args,**_kwargs: None
executor._finish_remote_verification=lambda *_args,**_kwargs: {
"job_id":JOB,"state":"COMPLETED","receipt":{"authority_oid":new_oid,"commit_visible":True}
}
now=datetime.now(timezone.utc)
binding=ContinuationRuntimeBinding(
request=lease(branch_mode="CREATE_NEW"),job_id=JOB,
workspace_path=Path("/tmp/create-new-executor-resume"),
lease_not_before=now-timedelta(seconds=1),lease_expires_at=now+timedelta(seconds=60),
original_receipt_valid=True,original_grant_consumed=True,
permissions=ContinuationPermissions(),
)
result=executor.execute(
binding,ContinuationCommit("create-new-test",{"docs/test.txt":b"ok"},"test")
)
assert result["state"]=="COMPLETED"
assert authority.created==[(BRANCH,BASE,new_oid)]
assert remote.created==[(BRANCH,new_oid)]
assert controller.advanced==["LOCAL_VERIFIED","REMOTE_SYNCING"]
+166
View File
@@ -0,0 +1,166 @@
from __future__ import annotations
import json
from pathlib import Path
import tempfile
from unittest.mock import patch
import pytest
from aios_existing_branch_publisher import ExistingBranchPublicationState, ExistingBranchPublisher
from gitea_tag_publication import GiteaConfigurationError, GiteaTagClient, InvalidReleaseVersion, PublicationRequest, TagPublisher, validate_version
from signer_contract_compatibility import run_compatible_signer
class Authority:
def __init__(self, oid: str):
self.oid = oid
def read_ref(self, ref: str) -> str | None:
return self.oid if ref.startswith("refs/heads/") else None
class Remote:
def __init__(self, oid: str | None = None, api_oid: str | None = None, visible: bool = True):
self.oid = oid
self.api_oid = api_oid
self.visible = visible
self.pushes: list[tuple[str, str | None, str]] = []
def repository_identity(self) -> str:
return "aios/aios"
def read_git_ref(self, branch: str) -> str | None:
return self.oid
def read_api_branch(self, branch: str) -> str | None:
return self.api_oid
def commit_visible(self, oid: str) -> bool:
return self.visible
def push_exact(self, branch: str, expected: str | None, new: str) -> None:
assert expected is None
self.pushes.append((branch, expected, new))
self.oid = new
self.api_oid = new
self.visible = True
def request(oid: str, key: str = "gate-closure-test-0001") -> dict[str, str]:
return {
"repository": "aios/aios",
"branch": "agent/gate-closure-test",
"expected_branch_oid": oid,
"owner_approval_reference": "owner-authorization:test",
"idempotency_key": key,
}
def test_existing_branch_publisher_creates_exact_missing_remote_and_replays_idempotently(tmp_path: Path):
oid = "a" * 40
remote = Remote()
publisher = ExistingBranchPublisher(Authority(oid), remote, tmp_path / "state")
first = publisher.publish(request(oid))
assert first["publication_state"] == ExistingBranchPublicationState.COMPLETED.value
assert remote.pushes == [("agent/gate-closure-test", None, oid)]
second = publisher.publish(request(oid))
assert second["publication_state"] == ExistingBranchPublicationState.COMPLETED.value
assert second["idempotent"] is True
def test_existing_branch_publisher_conflict_is_fail_closed(tmp_path: Path):
oid = "a" * 40
remote = Remote(oid="b" * 40, api_oid="b" * 40)
result = ExistingBranchPublisher(Authority(oid), remote, tmp_path / "state").publish(request(oid, "gate-closure-test-0002"))
assert result["publication_state"] == ExistingBranchPublicationState.MANUAL_OWNER_REVIEW_REQUIRED.value
assert remote.pushes == []
def test_existing_branch_publisher_rejects_main_and_wrong_authority_oid(tmp_path: Path):
oid = "a" * 40
with pytest.raises(Exception, match="BRANCH_NAMESPACE_REJECTED"):
ExistingBranchPublisher(Authority(oid), Remote(), tmp_path / "state").publish({**request(oid), "branch": "main", "idempotency_key": "gate-closure-test-0003"})
with pytest.raises(Exception, match="AUTHORITY_BRANCH_OID_MISMATCH"):
ExistingBranchPublisher(Authority("b" * 40), Remote(), tmp_path / "state").publish(request(oid, "gate-closure-test-0004"))
def test_signer_broker_path_has_no_direct_root_signer_call():
class FakeBroker:
def self_test(self):
return None
def sign(self, payload_kind, payload):
return b"signature", "test-broker-key"
request_doc = {
"schema": "aios.task-scoped-writer-grant-request.v1",
"task_id": "gate-closure-signer-unit",
"repository": "aios/aios",
"base": {"ref": "refs/heads/main", "oid": "a" * 40},
"target": {"branch": "agent/gate-closure-signer-unit"},
"owner_authorization": {"identity": "aiman", "reference": "owner-authorization:test"},
"authorization": {
"production_change_authorized": False,
"tag_change_authorized": False,
"baseline_change_authorized": False,
"main_write_authorized": False,
},
"allowed_operations": ["create_worktree"],
"denied_operations": ["direct_main_push", "force_push"],
"one_time_use": True,
"not_before": "2026-08-10T00:00:00Z",
"expires_at": "2026-08-10T00:00:00Z",
"nonce": "unit-nonce",
}
with tempfile.TemporaryDirectory() as td:
root = Path(td)
grant = root / "grant"
grant.mkdir()
(grant / "grant-request.json").write_text(json.dumps(request_doc), encoding="utf-8")
(grant / "approval-plan.json").write_text(json.dumps({"approval_plan_id": "sha256:" + "a" * 64}), encoding="utf-8")
with patch("signer_contract_compatibility._supports_deployed_signer", return_value=True):
result = run_compatible_signer(["unused-signer-command"], grant, root / "result.json", broker=FakeBroker())
assert result["signing_key_identity"] == "test-broker-key"
def test_gitea_configuration_has_explicit_error_codes():
with pytest.raises(GiteaConfigurationError, match="GITEA_API_URL_MISSING"):
GiteaTagClient.from_authoritative_environment({})
with pytest.raises(GiteaConfigurationError, match="GITEA_REPOSITORY_MISSING"):
GiteaTagClient.from_authoritative_environment({"AIOS_GITOPS_GITEA_API_URL": "https://gitea.invalid/api/v1"})
with pytest.raises(GiteaConfigurationError, match="GITEA_TOKEN_REFERENCE_MISSING"):
GiteaTagClient.from_authoritative_environment({"AIOS_GITOPS_GITEA_API_URL": "https://gitea.invalid/api/v1", "AIOS_GITOPS_GITEA_REPOSITORY": "aios/aios"})
assert validate_version("V2R2C1") == "V2R2C1"
with pytest.raises(InvalidReleaseVersion, match="invalid release version"):
validate_version("not-a-release")
def test_release_consistency_reports_tag_target_mismatch(tmp_path: Path):
target = "a" * 40
evidence = "b" * 40
tag_oid = "c" * 40
mismatched_target = "d" * 40
request = PublicationRequest("V2R2C1", target, evidence, "approval-plan:test")
(tmp_path / "baseline").mkdir()
(tmp_path / "versions").mkdir()
common = {
"version": request.version,
"target_oid": request.target_oid,
"evidence_closure_oid": request.evidence_closure_oid,
"approval_plan_id": request.approval_plan_id,
"tag_oid": tag_oid,
}
(tmp_path / "baseline/current.json").write_text(
json.dumps({**common, "current_baseline": request.version}), encoding="utf-8"
)
(tmp_path / "versions/V2R2C1.json").write_text(
json.dumps({**common, "registered": True, "enabled": True}), encoding="utf-8"
)
verified = {
**common,
"remote_tag_oid": tag_oid,
"remote_peeled_target_oid": mismatched_target,
"gitea_tag_oid": tag_oid,
"gitea_peeled_target_oid": target,
"gitea_listing_visible": True,
"repository_identity": "aios/aios",
}
result = TagPublisher(tmp_path, object(), object(), "gitea").get_release_consistency(
request, verified=verified
)
assert result["status"] == "FAIL"
assert result["error"] == "TAG_TARGET_MISMATCH"
+120
View File
@@ -0,0 +1,120 @@
from __future__ import annotations
import io
import json
import shutil
import subprocess
import tarfile
from pathlib import Path
import sys
import pytest
APP = Path(__file__).resolve().parents[1] / "active-runtime" / "app"
sys.path.insert(0, str(APP))
import governed_production_release as release # noqa: E402
from version_registry import build_source_registry_entry, source_registry_json # noqa: E402
def _git(work: Path, *args: str) -> str:
return subprocess.run(
["git", "-C", str(work), *args],
capture_output=True,
text=True,
check=True,
).stdout.strip()
def _commit(work: Path, message: str) -> str:
subprocess.run(["git", "-C", str(work), "add", "-A"], check=True)
subprocess.run(["git", "-C", str(work), "commit", "-qm", message], check=True)
return _git(work, "rev-parse", "HEAD")
def test_artifact_uses_pinned_source_registry_commit_when_main_advances(
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
if shutil.which("zstd") is None:
pytest.skip("zstd is required for governed artifact integration")
work = tmp_path / "work"
bare = tmp_path / "repo.git"
subprocess.run(["git", "init", "-q", "-b", "main", str(work)], check=True)
_git(work, "config", "user.name", "test")
_git(work, "config", "user.email", "test@example.invalid")
(work / "active-runtime/app").mkdir(parents=True)
(work / "active-runtime/app/deployment_state.py").write_text("state = 'candidate'\n")
(work / "active-runtime/app/v2r1c6_privacy_tools.py").write_text("def main(): return 0\n")
target_oid = _commit(work, "release target")
subprocess.run(
["git", "-C", str(work), "tag", "-a", "V2R2C1", target_oid, "-m", "release"],
check=True,
)
tag_oid = _git(work, "rev-parse", "refs/tags/V2R2C1")
(work / "evidence.txt").write_text("evidence\n")
evidence_oid = _commit(work, "evidence closure")
record = build_source_registry_entry(
version="V2R2C1",
tag_oid=tag_oid,
target_oid=target_oid,
peeled_target_oid=target_oid,
evidence_closure_oid=evidence_oid,
approval_plan_id="sha256:" + "4" * 64,
)
source_path = work / "versions/V2R2C1.json"
source_path.parent.mkdir(parents=True)
source_path.write_bytes(source_registry_json(record))
main_oid = _commit(work, "source registry convergence")
subprocess.run(["git", "clone", "-q", "--bare", str(work), str(bare)], check=True)
subprocess.run(["git", "-C", str(work), "remote", "add", "authority", str(bare)], check=True)
monkeypatch.setenv("AIOS_TEST_MODE", "1")
monkeypatch.setenv("AIOS_GITOPS_AUTHORITATIVE_GIT_DIR", str(bare))
monkeypatch.setattr(release, "TAG_OID", tag_oid)
monkeypatch.setattr(release, "TARGET_OID", target_oid)
monkeypatch.setattr(release, "EVIDENCE_CLOSURE_OID", evidence_oid)
monkeypatch.setattr(release, "APPROVAL_PLAN_ID", record["approval_plan_id"])
binding = release.ReleaseBinding(
tag_oid=tag_oid,
target_oid=target_oid,
evidence_closure_oid=evidence_oid,
approval_plan_id=record["approval_plan_id"],
)
original_payload = release._payload_file
advanced = False
def advance_after_first_payload(git_dir: Path, oid: str, path: str) -> bytes:
nonlocal advanced
result = original_payload(git_dir, oid, path)
if not advanced and path == "active-runtime/app/deployment_state.py":
updated = dict(record)
updated["review_marker"] = "main-advanced-after-validation"
source_path.write_bytes(source_registry_json(updated))
new_main = _commit(work, "advance main after source validation")
subprocess.run(["git", "-C", str(work), "push", "-q", "authority", "main"], check=True)
assert new_main != main_oid
advanced = True
return result
monkeypatch.setattr(release, "_payload_file", advance_after_first_payload)
receipt = release.build_production_artifact(bare, binding, tmp_path / "artifacts")
assert advanced is True
assert receipt["release_target_oid"] == target_oid
assert receipt["source_registry_main_oid"] == main_oid
assert receipt["source_registry_path"] == "versions/V2R2C1.json"
expected_bytes = subprocess.run(
["git", f"--git-dir={bare}", "show", f"{main_oid}:versions/V2R2C1.json"],
capture_output=True,
check=True,
).stdout
assert receipt["source_registry_sha256"] == __import__("hashlib").sha256(expected_bytes).hexdigest()
compressed = Path(receipt["artifact"]).read_bytes()
raw_tar = subprocess.run(["zstd", "-dc", "-"], input=compressed, capture_output=True, check=True).stdout
with tarfile.open(fileobj=io.BytesIO(raw_tar), mode="r:") as archive:
packaged = archive.extractfile("payload/versions/V2R2C1.json").read()
metadata = json.loads(archive.extractfile("payload/BUILD-METADATA.json").read())
assert packaged == expected_bytes
assert metadata["source_registry_main_oid"] == main_oid
assert metadata["source_registry_sha256"] == receipt["source_registry_sha256"]
-67
View File
@@ -1,67 +0,0 @@
from pathlib import Path
import hashlib,json,os,tarfile,sys,tempfile
import pytest
ROOT=Path(__file__).resolve().parents[1]
sys.path.insert(0,str(ROOT/"active-runtime/app"))
import aios_isolated_executor as mod
def make_archive(tmp_path):
root=tmp_path/"src"; root.mkdir()
(root/"hello.py").write_text("VALUE = 1\n")
archive=tmp_path/"source.tgz"
with tarfile.open(archive,"w:gz") as tf: tf.add(root/"hello.py",arcname="hello.py")
return archive
def test_isolated_executor_applies_hash_bound_change(tmp_path,monkeypatch):
archive=make_archive(tmp_path)
monkeypatch.delenv("CREDENTIALS_DIRECTORY",raising=False)
monkeypatch.setattr(mod,"assert_uncredentialed",lambda:None)
request={
"schema":"aios.isolated-execution.request.v1",
"request_id":"task-req-0000000000000000",
"source_archive":str(archive),
"source_archive_sha256":mod.sha256(archive.read_bytes()),
"change_set":[{
"path":"hello.py","operation":"replace_text","old":"VALUE = 1","new":"VALUE = 2","count":1,
"expected_sha256":mod.sha256(b"VALUE = 1\n")
}],
"verification":[{"label":"compile","argv":["/usr/bin/python3","-m","py_compile","hello.py"],"cwd":".","timeout_seconds":30}]
}
req=tmp_path/"req.json"; res=tmp_path/"res.json"
req.write_text(json.dumps(request))
mod.execute_request(req,res)
out=json.loads(res.read_text())
assert out["request_id"]=="task-req-0000000000000000"
assert out["credential_isolation"]["credential_env_absent"] is True
changed=out["changed_files"][0]
assert changed["path"]=="hello.py" and changed["deleted"] is False
def test_isolated_executor_source_drift_denied(tmp_path,monkeypatch):
archive=make_archive(tmp_path); monkeypatch.setattr(mod,"assert_uncredentialed",lambda:None)
request={
"schema":"aios.isolated-execution.request.v1","request_id":"task-req-0000000000000001",
"source_archive":str(archive),
"source_archive_sha256":mod.sha256(archive.read_bytes()),
"change_set":[{"path":"hello.py","operation":"write_file","content":"x\n","expected_sha256":"0"*64}],
"verification":[]
}
req=tmp_path/"req.json"; res=tmp_path/"res.json"; req.write_text(json.dumps(request))
with pytest.raises(mod.IsolatedExecutorError,match="SOURCE_DRIFT"):
mod.execute_request(req,res)
def test_credential_boundary_eacces_is_accepted(monkeypatch):
class DeniedDirectory:
def exists(self): return True
def is_symlink(self): return False
def is_dir(self): return True
def iterdir(self): raise PermissionError(13,"permission denied")
monkeypatch.setattr(mod.os,"access",lambda *_args,**_kwargs:False)
assert mod._credential_exposure(DeniedDirectory()) is None
def test_systemd_unit_has_no_credentials_and_private_network():
unit=(ROOT/"deploy/systemd/aios-online-agent-isolated-executor.service").read_text()
assert "PrivateNetwork=true" in unit
assert "InaccessiblePaths=/run/credentials" in unit
assert "LoadCredential" not in unit
assert "SupplementaryGroups=gitops" not in unit
-127
View File
@@ -1,127 +0,0 @@
from __future__ import annotations
import datetime as dt
import pathlib
import sys
import pytest
APP = pathlib.Path(__file__).resolve().parents[1] / "active-runtime" / "app"
if str(APP) not in sys.path:
sys.path.insert(0, str(APP))
from message_handoff import (
ConfidentialityScopeDenied,
DeliveryContext,
DurableHandoffStore,
HandoffFabric,
IdempotencyConflict,
MessageEnvelope,
MessageExpired,
MessageScopeMismatch,
SequenceGap,
authorize_delivery,
bind_envelope_scope,
stable_event_id,
)
def actor(**overrides):
values = dict(
tenant_id="tenant-a", user_id="user-a", project_id="project-a",
conversation_id="conversation-a", run_id="run-a",
capabilities=("task.read", "message.read"),
)
values.update(overrides)
return DeliveryContext(**values)
def envelope(**overrides):
values = dict(
source="agent-a", target="agent-b", message_type="handoff.request", payload={"value": 1},
tenant_id="tenant-a", user_id="user-a", project_id="project-a",
conversation_id="conversation-a", task_id="task-a", run_id="run-a",
sequence=1, idempotency_key="idem-1", capability_scope=("message.read",),
confidentiality_scope="private",
)
values.update(overrides)
return MessageEnvelope(**values)
def test_MESSAGE_TARGET_COLLISION_DENY():
a = stable_event_id(source="a", target="user-a", message_type="event", payload={"x": 1},
tenant_id="tenant-a", sequence=1, idempotency_key="same")
b = stable_event_id(source="a", target="user-b", message_type="event", payload={"x": 1},
tenant_id="tenant-a", sequence=1, idempotency_key="same")
assert a != b
def test_MESSAGE_SCOPE_MISMATCH_DENY():
with pytest.raises(MessageScopeMismatch, match="MESSAGE_SCOPE_MISMATCH_DENY"):
bind_envelope_scope(envelope(tenant_id="tenant-b"), actor())
def test_HANDOFF_CROSS_TENANT_DENY(tmp_path):
store = DurableHandoffStore(tmp_path / "handoff.sqlite3")
env = envelope()
store.put(env)
with pytest.raises(MessageScopeMismatch, match="MESSAGE_SCOPE_MISMATCH_DENY"):
store.get(env.envelope_id, actor=actor(tenant_id="tenant-b"))
def test_MESSAGE_CONFIDENTIALITY_SCOPE_ENFORCE():
with pytest.raises(ConfidentialityScopeDenied, match="MESSAGE_CONFIDENTIALITY_SCOPE_ENFORCE"):
authorize_delivery(actor(project_id="project-b"), envelope(confidentiality_scope="project"))
with pytest.raises(ConfidentialityScopeDenied, match="MESSAGE_CONFIDENTIALITY_SCOPE_ENFORCE"):
authorize_delivery(actor(), envelope(capability_scope=("message.read", "secret.read")))
def test_MESSAGE_IDEMPOTENCY_REPLAY(tmp_path):
store = DurableHandoffStore(tmp_path / "handoff.sqlite3")
env = envelope()
assert store.put(env)[0] == "CREATED"
status, replay = store.put(env)
assert status == "REPLAY"
assert replay.envelope_id == env.envelope_id
with pytest.raises(IdempotencyConflict, match="MESSAGE_IDEMPOTENCY_CONFLICT"):
store.put(envelope(payload={"value": 2}))
def test_MESSAGE_SEQUENCE_GAP_DETECT(tmp_path):
store = DurableHandoffStore(tmp_path / "handoff.sqlite3")
store.put(envelope(sequence=1, idempotency_key="one"))
with pytest.raises(SequenceGap, match="MESSAGE_SEQUENCE_GAP_DETECT"):
store.put(envelope(sequence=3, idempotency_key="three"))
def test_sequence_advances_and_scope_filters_pending(tmp_path):
store = DurableHandoffStore(tmp_path / "handoff.sqlite3")
store.put(envelope(sequence=1, idempotency_key="one"))
store.put(envelope(sequence=2, idempotency_key="two", payload={"value": 2}))
assert [item.sequence for item in store.pending(target="agent-b", actor=actor())] == [1, 2]
assert store.pending(target="agent-b", actor=actor(tenant_id="tenant-b")) == []
def test_expired_message_rejected(tmp_path):
expired = (dt.datetime.now(dt.timezone.utc) - dt.timedelta(seconds=1)).isoformat()
with pytest.raises(MessageExpired, match="MESSAGE_EXPIRED"):
DurableHandoffStore(tmp_path / "handoff.sqlite3").put(envelope(expires_at=expired))
def test_server_actor_binding_fills_scope_and_recomputes_identity():
public = MessageEnvelope(
source="agent-a", target="agent-b", message_type="handoff.request", payload={"value": 1},
sequence=1, idempotency_key="bind-1", confidentiality_scope="public",
)
bound = bind_envelope_scope(public, actor())
assert (bound.tenant_id, bound.user_id, bound.project_id, bound.run_id) == (
"tenant-a", "user-a", "project-a", "run-a"
)
assert bound.envelope_id != public.envelope_id
def test_handoff_fabric_publish_is_durable(tmp_path):
store = DurableHandoffStore(tmp_path / "handoff.sqlite3")
status, env = HandoffFabric(store).publish(envelope(), actor=actor())
assert status == "CREATED"
assert store.get(env.envelope_id, actor=actor()).payload == {"value": 1}
+12
View File
@@ -25,6 +25,18 @@ def test_model_api_staging_unit_is_non_production_and_read_only():
assert 'production' not in unit.lower().replace('model-api-v2r2c1-staging', '')
def test_model_api_source_convergence_never_falls_back_to_generic_git_dir():
source = (ROOT / 'active-runtime' / 'app' / 'aios_model_api_staging.py').read_text(encoding='utf-8')
assert "repo_git_dir=AUTHORITATIVE_GIT_DIR or GIT_DIR" not in source
assert "repo_git_dir=AUTHORITATIVE_GIT_DIR" in source
def test_production_adapter_never_claims_source_git_convergence():
source = (ROOT / 'active-runtime' / 'app' / 'v2r2c1_production_adapter.py').read_text(encoding='utf-8')
assert '"source_registry_converged": True' not in source
assert '"source_registry_converged": "not_checked"' in source
def test_model_api_builder_manifest_contract():
builder = (ROOT / 'scripts' / 'build_v2r2c1_model_api_staging.py').read_text(encoding='utf-8')
assert 'aios.v2r2c1.model-api-staging-manifest.v1' in builder
@@ -1,244 +0,0 @@
from __future__ import annotations
from pathlib import Path
import json
import sys
import pytest
ROOT = Path(__file__).resolve().parents[1]
APP = ROOT / "active-runtime/app"
sys.path.insert(0, str(APP))
from aios_online_agent_compact_map import (
MAX_RESULT_BYTES,
build_compact_interface_map,
)
from aios_online_agent_execution import (
ExecutionProjectionError,
GovernedExecutionBinding,
project_execution_envelope,
validate_execution_envelope,
)
import aios_online_agent_worker as worker
from aios_online_agent_worker import (
CompactInterfaceMapExecutor,
ExecutionEnvelope,
build_compact_interface_map_result,
)
BASE = "68b95fea0d9af3b52269e79b7cf878254f191b61"
def make_repo(tmp_path: Path) -> Path:
root = tmp_path / "repo"
(root / "active-runtime/app").mkdir(parents=True)
(root / "tests").mkdir(parents=True)
(root / "db/migrations").mkdir(parents=True)
(root / "deploy/systemd").mkdir(parents=True)
(root / "active-runtime/app/fleet.py").write_text(
"""
class WorkerRegistry:
def register_worker(self, worker_id, capabilities):
return {"worker_id": worker_id, "capabilities": capabilities}
class TaskScheduler:
def claim_next(self, tenant_id, capacity):
return None
def recover_assignment(task_id, worker_id):
return task_id, worker_id
TOKEN = "fixturetok"
""".strip() + "\n",
encoding="utf-8",
)
(root / "active-runtime/app/checkpoint.py").write_text(
"""
class CheckpointStore:
def resume(self, task_id, attempt):
return None
""".strip() + "\n",
encoding="utf-8",
)
(root / "tests/test_fleet.py").write_text(
"""
def test_scheduler_fairness():
assert True
def test_worker_reassignment():
assert True
""".strip() + "\n",
encoding="utf-8",
)
(root / "db/migrations/20260817_fleet.sql").write_text(
"CREATE TABLE IF NOT EXISTS worker_slots (worker_id TEXT, capacity INTEGER);\n",
encoding="utf-8",
)
(root / "deploy/systemd/aios-fleet-worker.service").write_text(
"[Service]\nExecStart=/usr/bin/true\n",
encoding="utf-8",
)
return root
def binding() -> GovernedExecutionBinding:
return GovernedExecutionBinding.from_mapping(
{
"task_id": "r42-p2-agent-fleet-interface-map-20260817",
"job_id": "job-2c451360f78ef595e41663e3b444a8fb",
"repository": "aios/aios",
"base_oid": BASE,
"expected_branch_oid": BASE,
"target_branch": "agent/r42-p2-agent-fleet-interface-map-20260817",
"branch_mode": "CREATE_NEW",
"generation": 1,
"idempotency_key": "r42-p2-agent-fleet-interface-map-68b95fe-v1",
}
)
def envelope_mapping():
return project_execution_envelope(
binding(),
{
"action": "source_capture",
"adapter": "compact_interface_map",
"capability_scope": ["git.read", "git.test"],
"source_signals": [
"worker registry scheduler capacity fairness fault isolation",
"checkpoint resume retry scope trace message",
],
},
required_capabilities=["git.read", "git.test"],
)
def task_for(envelope):
return {
"task_id": envelope["task_id"],
"metadata": {"execution": envelope},
"required_capabilities": ["git.read", "git.test"],
}
def test_compact_map_bounds_and_surfaces(tmp_path):
root = make_repo(tmp_path)
result = build_compact_interface_map(
root,
["worker scheduler capacity fairness checkpoint recovery"],
source_commit=BASE,
)
encoded = json.dumps(result, ensure_ascii=False, sort_keys=True, separators=(",", ":")).encode()
assert result["schema"] == "aios.engineering.interface-map.v1"
assert len(encoded) <= MAX_RESULT_BYTES
assert len(result["implementation_map"]["modules"]) <= 40
assert len(result["bounded_excerpts"]) <= 25
assert len(result["implementation_map"]["tests"]) <= 40
assert any(item["symbol"] == "WorkerRegistry" for item in result["implementation_map"]["classes"])
assert any(item["table"] == "worker_slots" for item in result["implementation_map"]["tables"])
assert any(item["unit"] == "aios-fleet-worker.service" for item in result["implementation_map"]["systemd_units"])
def test_compact_map_redacts_sensitive_source_patterns(tmp_path):
root = make_repo(tmp_path)
result = build_compact_interface_map(root, ["token fleet"], source_commit=BASE)
excerpts = json.dumps(result["bounded_excerpts"], ensure_ascii=False)
assert "fixturetok" not in excerpts
def test_compact_execution_projection_is_read_only():
envelope = envelope_mapping()
assert envelope["action"] == "source_capture"
assert envelope["adapter"] == "compact_interface_map"
assert envelope["governance"]["writer_lease"] is False
assert envelope["change_set"] == []
assert envelope["verification"] == []
validate_execution_envelope(envelope, binding=binding(), worker_capabilities=["git.read", "git.test"])
def test_compact_execution_rejects_mutation():
with pytest.raises(ExecutionProjectionError, match="SOURCE_CAPTURE_MUTATION_DENY"):
project_execution_envelope(
binding(),
{
"action": "source_capture",
"adapter": "compact_interface_map",
"change_set": [{"path": "x.py", "operation": "write_file", "content": "x=1\n"}],
},
)
def test_worker_accepts_compact_adapter_and_builds_result(tmp_path, monkeypatch):
root = make_repo(tmp_path)
monkeypatch.setattr(worker, "audit", lambda *args, **kwargs: None)
mapped = envelope_mapping()
envelope = ExecutionEnvelope.from_task(
task_for(mapped),
worker_capabilities=("git.read", "git.test"),
)
execution = CompactInterfaceMapExecutor().execute(envelope, root)
result = build_compact_interface_map_result(envelope.task_id, envelope, execution)
assert result["status"] == "SUCCEEDED"
assert result["classification"] == "P2_INTERFACE_MAP_CAPTURED"
assert result["execution_mode"] == "compact_interface_map"
assert result["writer_lease"] == "NONE"
assert "source_snapshot" not in result
assert result["result_bytes"] <= 120 * 1024
assert result["source_commit"] == BASE
def test_compact_contract_keys_are_complete(tmp_path):
root = make_repo(tmp_path)
result = build_compact_interface_map(root, ["fleet worker scheduler retry"], source_commit=BASE)
assert set(result["contracts"]) == {
"FLEET_AGENT_REGISTRATION",
"FLEET_CAPACITY_SCHEDULING",
"FLEET_FAIRNESS",
"FLEET_FAULT_ISOLATION",
"FLEET_WORKER_REASSIGNMENT",
"FLEET_SCOPE_ISOLATION",
"FLEET_IDEMPOTENT_DISPATCH",
"FLEET_FAILURE_RECOVERY",
}
def test_compact_result_has_no_source_archive(tmp_path):
root = make_repo(tmp_path)
result = build_compact_interface_map(root, ["fleet"], source_commit=BASE)
serialized = json.dumps(result)
assert "payload_b64" not in serialized
assert "tar.gz+base64" not in serialized
def test_create_new_retries_discard_known_policy_reconciliation_marker(tmp_path, monkeypatch):
task_id = "r42-p2-compact-interface-map-runtime-20260817"
job_id = "job-74d0982f616a7f2653ef346144139044"
monkeypatch.setattr(worker, "STATE_ROOT", tmp_path / "worker-state")
state_path = worker.task_state_path(task_id)
state_path.parent.mkdir(parents=True)
state_path.write_text(
json.dumps(
{
"task_id": task_id,
"execution_digest": "old-digest",
"phase": "reconciliation_required",
"attempt": 16,
"job_id": job_id,
"details": {
"api_status": 503,
"error": "SECRET_LIKE_PAYLOAD_REJECTED",
},
}
),
encoding="utf-8",
)
assert worker.load_task_state(
task_id,
"new-digest",
attempt=18,
continuation_head_oid=None,
continuation_job_id=job_id,
commit_idempotency_key="r42-p2-compact-interface-map-runtime-commit-v1",
) is None
@@ -1,216 +0,0 @@
from __future__ import annotations
from pathlib import Path
import json
import sys
import pytest
ROOT=Path(__file__).resolve().parents[1]
APP=ROOT/"active-runtime/app"
sys.path.insert(0,str(APP))
from aios_online_agent_checkpoint import (
CheckpointBinding,
CheckpointError,
CheckpointStore,
)
import aios_online_agent_worker as worker
def binding(attempt: int = 1, *, tenant_head: str = "b"*40):
return CheckpointBinding(
task_id="task-p1-a",
attempt=attempt,
execution_digest="d"*64,
job_id="job-p1-a",
repository="aios/aios",
base_oid="a"*40,
expected_branch_oid=tenant_head,
target_branch="agent/task-p1-a",
generation=1,
)
def test_CHECKPOINT_DURABLE_WRITE(tmp_path):
store=CheckpointStore(tmp_path/"checkpoints.sqlite3")
first=store.append(binding(), "claimed", {"phase":"claimed","attempt":1})
second=store.append(binding(), "workspace_prepared", {"phase":"workspace_prepared","attempt":1})
assert first["sequence"] == 1
assert second["sequence"] == 2
latest=store.latest("task-p1-a")
assert latest["phase"]=="workspace_prepared"
assert latest["payload"]["attempt"]==1
def test_CHECKPOINT_SCOPE_ISOLATION(tmp_path):
store=CheckpointStore(tmp_path/"checkpoints.sqlite3")
store.append(binding(), "claimed", {"phase":"claimed"})
changed=CheckpointBinding(
task_id="task-p1-a",attempt=1,execution_digest="d"*64,job_id="job-p1-a",
repository="aios/other",base_oid="a"*40,expected_branch_oid="b"*40,
target_branch="agent/task-p1-a",generation=1,
)
with pytest.raises(CheckpointError,match="CHECKPOINT_SCOPE_MISMATCH_DENY"):
store.append(changed,"workspace_prepared",{"phase":"workspace_prepared"})
def test_RESUME_FROM_CHECKPOINT_across_attempt_same_binding(tmp_path):
store=CheckpointStore(tmp_path/"checkpoints.sqlite3")
store.append(binding(1), "source_capture_done", {
"phase":"source_capture_done",
"attempt":1,
"result":{"status":"SUCCEEDED","classification":"NEEDS_REWORK_SOURCE_CAPTURE"},
})
resumed=store.verify_latest_binding(binding(2))
assert resumed is not None
assert resumed["attempt"]==1
assert resumed["phase"]=="source_capture_done"
def test_checkpoint_new_attempt_can_start_from_claimed(tmp_path):
store=CheckpointStore(tmp_path/"checkpoints.sqlite3")
store.append(binding(1),"result_submitted",{"phase":"result_submitted"})
cp=store.append(binding(2),"claimed",{"phase":"claimed"})
assert cp["attempt"]==2
assert cp["sequence"]==1
def envelope():
return worker.ExecutionEnvelope(
task_id="task-p1-a",
job_id="job-p1-a",
repository="aios/aios",
base_oid="a"*40,
expected_branch_oid="b"*40,
target_branch="agent/task-p1-a",
branch_mode="CONTINUE_EXISTING",
generation=1,
idempotency_key="governed-p1-a",
action="source_capture",
adapter="source_capture",
capability_scope=("git.read","git.test"),
source_signals=(),
change_set=(),
verification=(),
governance={"task_id":"task-p1-a","job_id":"job-p1-a"},
workspace={},
digest="d"*64,
)
def test_RESULT_SUBMISSION_REPLAY_SAFE_attempt_aware_key():
env=envelope()
result={"status":"SUCCEEDED","execution_digest":env.digest}
one=worker.result_idempotency_key(env.task_id,1,env,result)
two=worker.result_idempotency_key(env.task_id,2,env,result)
assert one != two
assert "-a1-result-" in one
assert "-a2-result-" in two
assert one == worker.result_idempotency_key(env.task_id,1,env,result)
class RetryClient:
def __init__(self):
self.calls=[]
def result(self,task_id,lease_token,key,result):
self.calls.append((task_id,key,dict(result)))
return (599,{"error":"SERVICE_UNAVAILABLE"}) if len(self.calls)==1 else (201,{"status":"VERIFYING"})
def get(self,task_id):
return 500,{}
def test_RETRY_IDEMPOTENT_NO_DUPLICATE_SIDE_EFFECT_reuses_same_key(monkeypatch):
client=RetryClient()
monkeypatch.setattr(worker.time,"sleep",lambda _:None)
status,response=worker.submit_result_replay_safe(
client,"task-p1-a","lease-token-"+"x"*32,"stable-key",{"status":"SUCCEEDED"}
)
assert status==201
assert [item[1] for item in client.calls]==["stable-key","stable-key"]
class ConflictClient:
def __init__(self,result):
self.result_value=result
def result(self,*args):
return 409,{"error":"IDEMPOTENCY_CONFLICT"}
def get(self,task_id):
return 200,{"status":"VERIFYING","result":dict(self.result_value)}
def test_RESULT_SUBMISSION_REPLAY_SAFE_reconciles_exact_result():
result={"status":"SUCCEEDED","value":1}
status,response=worker.submit_result_replay_safe(
ConflictClient(result),"task-p1-a","lease-token-"+"x"*32,"stable-key",result
)
assert status==200
assert response["idempotent"] is True
assert response["reconciled"] is True
def test_RESULT_SUBMISSION_REPLAY_SAFE_rejects_conflicting_result():
status,response=worker.submit_result_replay_safe(
ConflictClient({"status":"SUCCEEDED","value":2}),
"task-p1-a","lease-token-"+"x"*32,"stable-key",
{"status":"SUCCEEDED","value":1},
)
assert status==409
assert response["error"]=="IDEMPOTENCY_CONFLICT"
def test_WORKSPACE_CLEANUP_RECOVERY(tmp_path,monkeypatch):
root=tmp_path/"workspaces"; root.mkdir()
stale=root/"task-old"; stale.mkdir(); (stale/"x").write_text("x")
monkeypatch.setattr(worker,"WORKSPACE_ROOT",root)
monkeypatch.setattr(worker,"STATE_ROOT",tmp_path/"state")
removed=worker.recover_stale_workspaces()
assert removed==1
assert not stale.exists()
class LeaseBridge:
def __init__(self):
self.calls=0
def call(self,route,body):
assert route=="/v2/writer-lease"
self.calls+=1
if self.calls==1:
return 599,{"error":"SERVICE_UNAVAILABLE"}
return 200,{
"lease_id":"lease-p1-a",
"job_id":"job-p1-a",
"task_id":"task-p1-a",
"repository":"aios/aios",
"base_oid":"a"*40,
"expected_branch_oid":"b"*40,
"branch_mode":"CONTINUE_EXISTING",
"generation":1,
"target_branch":"agent/task-p1-a",
"state":"ACTIVE",
}
class LeaseHeartbeatStub:
def ensure(self):
return None
def test_LEASE_RENEWAL_RECOVERY_retries_same_binding(monkeypatch):
env=envelope()
env=worker.ExecutionEnvelope(
task_id=env.task_id,job_id=env.job_id,repository=env.repository,
base_oid=env.base_oid,expected_branch_oid=env.expected_branch_oid,
target_branch=env.target_branch,branch_mode=env.branch_mode,generation=env.generation,
idempotency_key=env.idempotency_key,action=env.action,adapter=env.adapter,
capability_scope=env.capability_scope,source_signals=env.source_signals,
change_set=env.change_set,verification=env.verification,
governance={"task_id":env.task_id,"job_id":env.job_id,"lease_seconds":900},
workspace=env.workspace,digest=env.digest,
)
bridge=LeaseBridge()
monkeypatch.setattr(worker.time,"sleep",lambda _:None)
monkeypatch.setattr(worker,"STATE_ROOT",Path("/tmp")/"aios-p1-test-state")
response=worker.acquire_writer_lease_replay_safe(env,bridge,LeaseHeartbeatStub())
assert response["lease_id"]=="lease-p1-a"
assert bridge.calls==2
-60
View File
@@ -1,60 +0,0 @@
from pathlib import Path
import sys
import pytest
ROOT=Path(__file__).resolve().parents[1]
sys.path.insert(0,str(ROOT/"active-runtime/app"))
from aios_online_agent_execution import GovernedExecutionBinding
from aios_online_agent_plan_store import ExecutionPlanStore, ExecutionPlanError
BASE="a642b045775b9154a00ae78543037fbef4fb447b"
HEAD="71523aef6e48b1e2fa516c456ca189b7a039104b"
def binding(head=HEAD):
return GovernedExecutionBinding.from_mapping({
"task_id":"task-plan-a","job_id":"job-plan-a","repository":"aios/aios","base_oid":BASE,
"expected_branch_oid":head,"target_branch":"agent/task-plan-a","branch_mode":"CONTINUE_EXISTING",
"generation":1,"idempotency_key":"governed-plan-a",
})
def request():
return {
"schema":"aios.online-agent.plan.v1","task_id":"task-plan-a","job_id":"job-plan-a",
"source_head_oid":HEAD,"generation":1,"idempotency_key":"plan-task-a-v1",
"execution":{
"action":"deterministic",
"change_set":[{"path":"x.py","operation":"write_file","content":"x=1\n"}],
"verification":[{"label":"compile","argv":["/usr/bin/python3","-m","py_compile","x.py"],"cwd":".","timeout_seconds":30}],
"capability_scope":["git.read","git.test"],
}
}
def test_plan_register_and_resolve(tmp_path):
store=ExecutionPlanStore(tmp_path)
created=store.register(binding(),request())
assert created["idempotent"] is False
resolved=store.resolve(binding())
assert resolved["plan_digest"]==created["plan_digest"]
assert resolved["execution"]["action"]=="deterministic"
def test_plan_is_idempotent(tmp_path):
store=ExecutionPlanStore(tmp_path); req=request()
first=store.register(binding(),req); second=store.register(binding(),req)
assert second["idempotent"] is True
assert first["plan_digest"]==second["plan_digest"]
def test_plan_source_head_drift_denied(tmp_path):
store=ExecutionPlanStore(tmp_path); store.register(binding(),request())
with pytest.raises(ExecutionPlanError,match="SOURCE_HEAD_DRIFT"):
store.resolve(binding("b"*40))
def test_plan_replacement_requires_cas(tmp_path):
store=ExecutionPlanStore(tmp_path); first=store.register(binding(),request())
changed=request(); changed["idempotency_key"]="plan-task-a-v2"
changed["execution"]["change_set"][0]["content"]="x=2\n"
with pytest.raises(ExecutionPlanError,match="REPLACEMENT_CAS_MISMATCH"):
store.register(binding(),changed)
changed["expected_previous_plan_digest"]=first["plan_digest"]
second=store.register(binding(),changed)
assert second["plan_digest"]!=first["plan_digest"]
+2
View File
@@ -47,6 +47,8 @@ def old_baseline(path: Path) -> str:
def test_baseline_record_uses_real_previous_production_baseline(tmp_path, monkeypatch):
# This test isolates previous-baseline semantics; convergence is tested separately.
monkeypatch.setattr(adapter, "assert_source_registry_convergence", lambda *a, **kw: None)
baseline = tmp_path / "baseline.json"
previous_sha = old_baseline(baseline)
registry_root = tmp_path / "versions"
+120
View File
@@ -0,0 +1,120 @@
from __future__ import annotations
import base64
from concurrent.futures import ThreadPoolExecutor
import hashlib
import json
from pathlib import Path
import threading
from aios_signer_broker_service import (
REQUEST_SCHEMA,
RESPONSE_SCHEMA,
SignerBrokerPolicy,
SignerBrokerService,
SignerBrokerServiceError,
)
class FakeRunner:
def __init__(self, failure: str | None = None):
self.failure = failure
self.calls: list[tuple[str, str]] = []
self.lock = threading.Lock()
def self_test(self) -> None:
with self.lock:
self.calls.append(("self_test", ""))
if self.failure:
raise SignerBrokerServiceError(self.failure)
def sign(self, payload_kind: str, payload: bytes) -> tuple[bytes, str]:
with self.lock:
self.calls.append(("sign", payload_kind))
if self.failure:
raise SignerBrokerServiceError(self.failure)
return b"signature:" + payload, "test-broker-key"
def make_policy(tmp_path: Path, *, allowed_uids: frozenset[int] = frozenset({1000})) -> SignerBrokerPolicy:
return SignerBrokerPolicy(
socket_path=tmp_path / "signer.sock",
signer_path=Path("/usr/local/libexec/aios-grant-signer-adapter"),
audit_path=tmp_path / "audit.jsonl",
state_root=tmp_path / "state",
allowed_uids=allowed_uids,
)
def sign_request(payload: bytes = b"canonical payload") -> dict[str, str]:
return {
"schema": REQUEST_SCHEMA,
"operation": "sign",
"payload_kind": "approval_plan",
"payload_sha256": hashlib.sha256(payload).hexdigest(),
"payload_base64": base64.b64encode(payload).decode("ascii"),
}
def read_audit(path: Path) -> list[dict[str, object]]:
return [json.loads(line) for line in path.read_text(encoding="utf-8").splitlines()]
def test_authorized_request_and_self_test_emit_metadata_only_audit(tmp_path: Path):
service = SignerBrokerService(make_policy(tmp_path), FakeRunner())
self_test = service.handle_request({"schema": REQUEST_SCHEMA, "operation": "self_test"}, 1000)
signed = service.handle_request(sign_request(b"secret-shaped canonical input"), 1000)
assert self_test == {"schema": RESPONSE_SCHEMA, "status": "PASS", "operation": "self_test"}
assert signed["status"] == "PASS"
assert base64.b64decode(signed["signature_base64"]) == b"signature:secret-shaped canonical input"
assert signed["key_id"] == "test-broker-key"
audit_text = (tmp_path / "audit.jsonl").read_text(encoding="utf-8")
assert "secret-shaped canonical input" not in audit_text
assert "payload_base64" not in audit_text
allowed = {"schema", "timestamp", "peer_uid", "operation", "status", "error_code", "payload_sha256"}
assert all(set(event) <= allowed for event in read_audit(tmp_path / "audit.jsonl"))
def test_unauthorized_caller_is_rejected_before_signer_use(tmp_path: Path):
runner = FakeRunner()
service = SignerBrokerService(make_policy(tmp_path), runner)
result = service.handle_request(sign_request(), 1001)
assert result["status"] == "FAIL"
assert result["error_code"] == "CALLER_UNAUTHORIZED"
assert runner.calls == []
def test_arbitrary_command_and_invalid_operation_are_rejected(tmp_path: Path):
runner = FakeRunner()
service = SignerBrokerService(make_policy(tmp_path), runner)
injected = {"schema": REQUEST_SCHEMA, "operation": "sign", "command": ["/bin/sh", "-c", "id"]}
invalid = {"schema": REQUEST_SCHEMA, "operation": "run", "command": ["id"]}
assert service.handle_request(injected, 1000)["error_code"] == "REQUEST_FIELDS_REJECTED"
assert service.handle_request(invalid, 1000)["error_code"] == "SIGNER_OPERATION_REJECTED"
assert runner.calls == []
def test_invalid_payload_and_hash_fail_closed(tmp_path: Path):
service = SignerBrokerService(make_policy(tmp_path), FakeRunner())
invalid = sign_request()
invalid["payload_base64"] = "not-base64"
assert service.handle_request(invalid, 1000)["error_code"] == "SIGNER_PAYLOAD_INVALID"
mismatch = sign_request()
mismatch["payload_sha256"] = "0" * 64
assert service.handle_request(mismatch, 1000)["error_code"] == "SIGNER_PAYLOAD_HASH_MISMATCH"
def test_signer_unavailable_and_timeout_are_stable_failures(tmp_path: Path):
for code in ("SIGNER_UNAVAILABLE", "SIGNER_TIMEOUT"):
result = SignerBrokerService(make_policy(tmp_path / code), FakeRunner(code)).handle_request(
{"schema": REQUEST_SCHEMA, "operation": "self_test"}, 1000
)
assert result == {"schema": RESPONSE_SCHEMA, "status": "FAIL", "error_code": code}
def test_concurrent_calls_are_safe_and_audited(tmp_path: Path):
service = SignerBrokerService(make_policy(tmp_path), FakeRunner())
with ThreadPoolExecutor(max_workers=8) as pool:
results = list(pool.map(lambda index: service.handle_request(sign_request(f"payload-{index}".encode()), 1000), range(24)))
assert all(result["status"] == "PASS" for result in results)
assert len(read_audit(tmp_path / "audit.jsonl")) == 24
+8 -3
View File
@@ -20,6 +20,7 @@ from signer_contract_compatibility import ( # noqa: E402
canonical_json_bytes,
deployed_signer_help,
run_compatible_signer,
run_signer_self_test,
resolve_ref_oid,
run_compatible_validator,
sha256_json,
@@ -113,9 +114,13 @@ class SignerContractCompatibilityTests(unittest.TestCase):
help_result = subprocess.run([signer, "sign", "--help"], capture_output=True, text=True, check=False)
self.assertEqual(help_result.returncode, 0)
self.assertTrue(deployed_signer_help(help_result.stdout, help_result.stderr))
self_test = subprocess.run([signer, "self-test"], capture_output=True, text=True, check=False)
self.assertEqual(self_test.returncode, 0)
self.assertIn("SIGNER_SELF_TEST=PASS", self_test.stdout)
try:
broker_result = run_signer_self_test()
except SignerContractError as exc:
if str(exc) == "SIGNER_BROKER_UNAVAILABLE":
self.skipTest("signer broker is not deployed in this engineering phase")
raise
self.assertEqual(broker_result["status"], "PASS")
def test_resolve_ref_oid_uses_full_synthetic_commit_and_fails_closed(self) -> None:
with tempfile.TemporaryDirectory() as temporary:
@@ -1,487 +0,0 @@
from datetime import datetime, timedelta, timezone
from pathlib import Path
import pytest
from active_runtime.app.aios_credential_broker import CredentialBroker
from active_runtime.app.agent_observability_hooks import AgentObservabilityHooks
from active_runtime.app.message_handoff import DurableHandoffStore, MessageEnvelope
from active_runtime.app.search_gateway import (
SearchEvidence,
SearchGateway,
SearchProviderAttempt,
SearchRequest,
)
from active_runtime.app.search_gateway_orchestrator import SearchGatewayOrchestrator
from active_runtime.app.trace_tool_credential_governance import (
CredentialGrant,
GovernanceActor,
GovernanceError,
GovernanceScope,
GovernanceStore,
ToolDescriptor,
TraceContext,
bind_trace_lineage,
)
TENANT_SCOPE = GovernanceScope(
"tenant",
tenant_id="tenant-a",
user_id="user-a",
project_id="project-a",
conversation_id="conversation-a",
run_id="run-a",
)
def actor(*capabilities: str, tenant_id: str = "tenant-a") -> GovernanceActor:
return GovernanceActor(
"actor-a",
GovernanceScope(
"tenant",
tenant_id=tenant_id,
user_id="user-a",
project_id="project-a",
conversation_id="conversation-a",
run_id="run-a",
),
tuple(capabilities),
)
def owner() -> GovernanceActor:
return GovernanceActor("owner-control", GovernanceScope("system"), ("system.admin",))
def message(**changes):
values = dict(
source="task-plane",
target="worker-a",
message_type="tool.invoke",
payload={"query": "safe"},
sequence=1,
idempotency_key="message-a-1",
tenant_id="tenant-a",
user_id="user-a",
project_id="project-a",
conversation_id="conversation-a",
task_id="task-a",
run_id="run-a",
confidentiality_scope="run",
)
values.update(changes)
return MessageEnvelope(**values)
def task(**changes):
value = {
"task_id": "task-a",
"claim_id": "claim-a",
"correlation_id": "correlation-a",
"scope_type": "tenant",
"tenant_id": "tenant-a",
"user_id": "user-a",
"project_id": "project-a",
"conversation_id": "conversation-a",
"run_id": "run-a",
}
value.update(changes)
return value
def root_trace():
return bind_trace_lineage(
actor=actor("tool.search"),
task=task(),
message=message(),
trace_id="trace-a",
span_id="span-root",
)
def test_trace_lineage_continuity_and_durable_restart(tmp_path: Path) -> None:
store = GovernanceStore(tmp_path / "governance.sqlite3")
root = root_trace()
child = root.child(span_id="span-tool", actor=actor("tool.search"))
store.record_trace(root, actor("tool.search"))
store.record_trace(child, actor("tool.search"))
reopened = GovernanceStore(tmp_path / "governance.sqlite3")
assert reopened.verify_audit_chain(("TRACE_RECORDED",))
assert child.trace_id == root.trace_id
assert child.parent_span_id == root.span_id
assert child.task_id == "task-a"
assert child.claim_id == "claim-a"
def test_trace_lineage_continuity_rejects_task_change() -> None:
with pytest.raises(GovernanceError, match="TRACE_LINEAGE_CONTINUITY"):
bind_trace_lineage(
actor=actor("tool.search"),
task=task(task_id="task-b"),
message=message(),
trace_id="trace-a",
span_id="span-root",
)
def test_trace_scope_mismatch_deny() -> None:
with pytest.raises(GovernanceError, match="TRACE_SCOPE_MISMATCH_DENY"):
bind_trace_lineage(
actor=actor("tool.search"),
task=task(),
message=message(tenant_id="tenant-b"),
trace_id="trace-a",
span_id="span-root",
)
def test_message_correlation_is_identity_bound() -> None:
first = message(correlation_id="correlation-a")
second = message(correlation_id="correlation-b")
assert first.envelope_id != second.envelope_id
assert first.as_record()["correlation_id"] == "correlation-a"
def test_message_handoff_legacy_schema_migrates_without_changing_id(tmp_path: Path) -> None:
import sqlite3
path = tmp_path / "legacy.sqlite3"
legacy = message()
record = legacy.as_record()
record.pop("correlation_id")
with sqlite3.connect(path) as connection:
connection.executescript(
"""
CREATE TABLE message_handoff (
envelope_id TEXT PRIMARY KEY, stream_key TEXT NOT NULL, source TEXT NOT NULL,
target TEXT NOT NULL, message_type TEXT NOT NULL, payload_json TEXT NOT NULL,
payload_sha256 TEXT NOT NULL, tenant_id TEXT, user_id TEXT, project_id TEXT,
conversation_id TEXT, task_id TEXT, run_id TEXT, causation_id TEXT,
sequence INTEGER NOT NULL, capability_scope_json TEXT NOT NULL,
confidentiality_scope TEXT NOT NULL, idempotency_key TEXT NOT NULL,
expires_at TEXT, created_at TEXT NOT NULL, status TEXT NOT NULL DEFAULT 'PENDING',
delivered_at TEXT, acked_at TEXT,
UNIQUE(stream_key,idempotency_key), UNIQUE(stream_key,sequence)
);
"""
)
columns = tuple(record)
connection.execute(
f"INSERT INTO message_handoff ({','.join(columns)}) VALUES ({','.join('?' for _ in columns)})",
tuple(record[name] for name in columns),
)
store = DurableHandoffStore(path)
loaded = store.get(
legacy.envelope_id,
actor={
"tenant_id": "tenant-a", "user_id": "user-a", "project_id": "project-a",
"conversation_id": "conversation-a", "run_id": "run-a",
"capabilities": [],
},
)
assert loaded.envelope_id == legacy.envelope_id
assert loaded.correlation_id is None
def test_message_handoff_rejects_raw_credential_material(tmp_path: Path) -> None:
with pytest.raises(ValueError, match="CREDENTIAL_SECRET_EXPOSURE_DENY"):
message(payload={"value": "Bearer " + "x" * 24})
DurableHandoffStore(tmp_path / "handoff.sqlite3")
import sqlite3
with sqlite3.connect(tmp_path / "handoff.sqlite3") as connection:
assert connection.execute("SELECT count(*) FROM message_handoff").fetchone()[0] == 0
def test_tool_mcp_authz_deny_and_scope_propagation(tmp_path: Path) -> None:
store = GovernanceStore(tmp_path / "governance.sqlite3")
store.register_tool(
ToolDescriptor("mcp.search", "mcp", TENANT_SCOPE, ("tool.search",)),
owner(),
)
trace = root_trace()
store.record_trace(trace, actor("tool.search"))
with pytest.raises(GovernanceError, match="TOOL_MCP_AUTHZ_DENY"):
store.authorize_tool(
actor=actor(), tool_name="mcp.search", trace=trace,
requested_scope=TENANT_SCOPE, input_digest="a" * 64,
invocation_key="invoke-deny-capability",
)
with pytest.raises(GovernanceError, match="TOOL_SCOPE_PROPAGATION"):
store.authorize_tool(
actor=actor("tool.search"), tool_name="mcp.search", trace=trace,
requested_scope=GovernanceScope("tenant", tenant_id="tenant-b"),
input_digest="a" * 64, invocation_key="invoke-deny-scope",
)
invocation = store.authorize_tool(
actor=actor("tool.search"), tool_name="mcp.search", trace=trace,
requested_scope=TENANT_SCOPE, input_digest="a" * 64,
invocation_key="invoke-search-a",
)
assert invocation["trace_id"] == "trace-a"
assert invocation["evidence"]["action"] == "TOOL_AUTHORIZED"
def test_tool_requires_recorded_trace_and_receipt_is_idempotent(tmp_path: Path) -> None:
store = GovernanceStore(tmp_path / "governance.sqlite3")
store.register_tool(
ToolDescriptor(
"mcp.search", "mcp", TENANT_SCOPE, ("tool.search",),
credential_ref="systemd-credential:search-provider-a",
),
owner(),
)
trace = root_trace()
with pytest.raises(GovernanceError, match="TOOL_MCP_AUTHZ_DENY:trace"):
store.authorize_tool(
actor=actor("tool.search"), tool_name="mcp.search", trace=trace,
requested_scope=TENANT_SCOPE, input_digest="a" * 64,
invocation_key="invoke-unrecorded",
)
store.record_trace(trace, actor("tool.search"))
authorized = store.authorize_tool(
actor=actor("tool.search"), tool_name="mcp.search", trace=trace,
requested_scope=TENANT_SCOPE, input_digest="a" * 64,
invocation_key="invoke-replay-a",
)
assert authorized["credential_required"] is True
assert "credential_ref" not in authorized
receipt = store.record_tool_receipt(
invocation_key="invoke-replay-a", result_digest="b" * 64,
actor=actor("tool.search"), trace=trace,
)
assert receipt["status"] == "VERIFIED"
replay = store.authorize_tool(
actor=actor("tool.search"), tool_name="mcp.search", trace=trace,
requested_scope=TENANT_SCOPE, input_digest="a" * 64,
invocation_key="invoke-replay-a",
)
assert replay["idempotent"] is True
assert replay["receipt_sha256"] == "b" * 64
assert store.verify_audit_chain(
("TRACE_RECORDED", "TOOL_AUTHORIZED", "TOOL_RESULT_VERIFIED")
)
def test_system_and_global_resources_do_not_use_fake_tenants(tmp_path: Path) -> None:
with pytest.raises(GovernanceError, match="GOVERNANCE_NON_TENANT_SCOPE_INVALID"):
GovernanceScope("system", tenant_id="fake-tenant")
store = GovernanceStore(tmp_path / "governance.sqlite3")
descriptor = ToolDescriptor("system.health", "tool", GovernanceScope("global"), ("system.admin",))
registered = store.register_tool(descriptor, owner())
assert registered["descriptor"]["scope_type"] == "global"
assert registered["descriptor"]["tenant_id"] is None
def test_credential_scope_and_secret_exposure_deny(tmp_path: Path) -> None:
store = GovernanceStore(tmp_path / "governance.sqlite3")
grant = CredentialGrant(
"grant-search-a",
"systemd-credential:search-provider-a",
TENANT_SCOPE,
("search.invoke",),
task_id="task-a",
tool_name="mcp.search",
expires_at=(datetime.now(timezone.utc) + timedelta(minutes=5)).isoformat(),
)
stored = store.store_grant(grant, owner())
assert "secret" not in stored["grant"]
trace = root_trace()
store.record_trace(trace, actor("tool.search"))
with pytest.raises(GovernanceError, match="CREDENTIAL_SCOPE_DENY"):
store.authorize_credential(
grant_id=grant.grant_id,
actor=actor("tool.search", tenant_id="tenant-b"),
trace=trace,
operation="search.invoke",
task_id="task-a",
tool_name="mcp.search",
)
with pytest.raises(GovernanceError, match="CREDENTIAL_SECRET_EXPOSURE_DENY"):
CredentialGrant("grant-raw", "Bearer raw-secret", TENANT_SCOPE, ("search.invoke",))
authorized = store.authorize_credential(
grant_id=grant.grant_id,
actor=actor("tool.search"),
trace=trace,
operation="search.invoke",
task_id="task-a",
tool_name="mcp.search",
)
assert authorized["credential_ref"] == "systemd-credential:search-provider-a"
assert "credential_ref" not in authorized["evidence"]["details"]
def test_audit_evidence_completeness_and_tamper_detection(tmp_path: Path) -> None:
path = tmp_path / "governance.sqlite3"
store = GovernanceStore(path)
trace = root_trace()
store.record_trace(trace, actor("tool.search"))
store.register_tool(
ToolDescriptor("mcp.search", "mcp", TENANT_SCOPE, ("tool.search",)), owner()
)
store.authorize_tool(
actor=actor("tool.search"), tool_name="mcp.search", trace=trace,
requested_scope=TENANT_SCOPE, input_digest="a" * 64,
invocation_key="invoke-audit-a",
)
assert store.verify_audit_chain(("TRACE_RECORDED", "TOOL_REGISTERED", "TOOL_AUTHORIZED"))
assert not store.verify_audit_chain(("CREDENTIAL_AUTHORIZED",))
import sqlite3
with sqlite3.connect(path) as connection:
connection.execute(
"UPDATE governance_audit_evidence SET outcome='DENY' WHERE sequence=1"
)
assert not GovernanceStore(path).verify_audit_chain()
def test_observability_record_carries_governed_trace(tmp_path: Path) -> None:
from active_runtime.app.agent_observability_store import AgentObservabilityStore
hooks = AgentObservabilityHooks(AgentObservabilityStore(tmp_path / "observability"))
trace = root_trace()
started = hooks.start(
"user-a", "project-a", "openclaw",
actor=actor("tool.search"), trace=trace,
)
record = hooks.finish(started["execution_id"])["record"]
assert record["trace_id"] == "trace-a"
assert record["tenant_id"] == "tenant-a"
assert record["task_id"] == "task-a"
assert record["message_id"] == message().envelope_id
def test_active_runtime_credential_broker_scope_and_raw_secret_deny() -> None:
class Provider:
def environment(self, lease):
return {"ACCESS_TOKEN": "raw-secret"}
broker = CredentialBroker(Provider())
lease = broker.issue(
task_id="task-a", repository="aios/aios", target_branch="agent/task-a",
generation=1, scopes=("tool.invoke",), scope_type="tenant",
tenant_id="tenant-a", user_id="user-a", project_id="project-a", run_id="run-a",
)
with pytest.raises(RuntimeError, match="CREDENTIAL_SCOPE_DENY"):
broker.authorize(
lease["lease_id"], task_id="task-a", operation="tool.invoke",
scope_type="tenant", tenant_id="tenant-b", user_id="user-a",
project_id="project-a", run_id="run-a",
)
with pytest.raises(RuntimeError, match="CREDENTIAL_SECRET_EXPOSURE_DENY"):
broker.authorize(
lease["lease_id"], task_id="task-a", operation="tool.invoke",
scope_type="tenant", tenant_id="tenant-a", user_id="user-a",
project_id="project-a", run_id="run-a",
)
def test_search_gateway_uses_governed_tool_boundary(tmp_path: Path) -> None:
class Provider:
name = "official_site"
called = False
def search(self, request, queries):
self.called = True
return (
[SearchEvidence("Official", "https://example.test", self.name, "safe")],
SearchProviderAttempt(self.name, "ok", 1),
)
store = GovernanceStore(tmp_path / "governance.sqlite3")
store.register_tool(
ToolDescriptor("search.official_site", "tool", TENANT_SCOPE, ("tool.search",)),
owner(),
)
provider = Provider()
gateway = SearchGateway([provider], governance_store=store)
trace = root_trace()
store.record_trace(trace, actor("tool.search"))
response = gateway.search(
SearchRequest(
query="AIOS", requester="actor-a", correlation_id="trace-a",
authorized_sources=["official_site"],
),
actor=actor("tool.search"),
trace=trace,
)
assert response.status == "found"
assert provider.called is True
assert store.verify_audit_chain(
("TOOL_REGISTERED", "TRACE_RECORDED", "TOOL_AUTHORIZED", "TOOL_RESULT_VERIFIED")
)
with pytest.raises(GovernanceError, match="TOOL_MCP_AUTHZ_DENY"):
gateway.search(
SearchRequest(query="AIOS", requester="forged", correlation_id="trace-a"),
actor=actor("tool.search"),
trace=trace,
)
def test_search_gateway_redacts_provider_exception() -> None:
class Provider:
name = "general_web"
def search(self, request, queries):
raise RuntimeError("Bearer should-not-leak")
response = SearchGateway([Provider()]).search(SearchRequest(query="AIOS", requester="legacy"))
assert response.attempts[0].reason == "PROVIDER_FAILURE"
assert "should-not-leak" not in str(response.to_dict())
def test_search_runtime_default_path_requires_governance(tmp_path: Path) -> None:
orchestrator = SearchGatewayOrchestrator(
governance_store=GovernanceStore(tmp_path / "search-governance.sqlite3")
)
assert orchestrator.status()["governance"] == "required"
with pytest.raises(GovernanceError, match="TOOL_MCP_AUTHZ_DENY"):
orchestrator.execute({"query": "AIOS", "requester": "actor-a"})
def test_governance_newer_schema_fails_closed(tmp_path: Path) -> None:
import sqlite3
path = tmp_path / "newer.sqlite3"
with sqlite3.connect(path) as connection:
connection.execute(
"CREATE TABLE governance_schema_metadata(singleton INTEGER PRIMARY KEY, schema_version INTEGER NOT NULL)"
)
connection.execute(
"INSERT INTO governance_schema_metadata(singleton,schema_version) VALUES(1,999)"
)
with pytest.raises(GovernanceError, match="GOVERNANCE_SCHEMA_VERSION_UNSUPPORTED"):
GovernanceStore(path)
def test_changed_production_files_have_no_embedded_secret_material() -> None:
import re
root = Path(__file__).resolve().parents[1]
paths = (
"active-runtime/app/agent_event_bus.py",
"active-runtime/app/agent_event_observability_bridge.py",
"active-runtime/app/agent_observability.py",
"active-runtime/app/agent_observability_api.py",
"active-runtime/app/agent_observability_hooks.py",
"active-runtime/app/agent_observability_store.py",
"active-runtime/app/aios_credential_broker.py",
"active-runtime/app/message_handoff.py",
"active-runtime/app/search_agent_adapter.py",
"active-runtime/app/search_gateway.py",
"active-runtime/app/search_gateway_api.py",
"active-runtime/app/search_gateway_orchestrator.py",
"active-runtime/app/trace_tool_credential_governance.py",
)
patterns = (
re.compile(r"BEGIN (?:RSA|OPENSSH|EC) PRIVATE KEY"),
re.compile(r"Bearer\s+[A-Za-z0-9._~+/-]{24,}"),
)
hits = [path for path in paths for pattern in patterns if pattern.search((root / path).read_text(encoding="utf-8"))]
assert hits == []
+392
View File
@@ -0,0 +1,392 @@
from __future__ import annotations
import hashlib
import io
import json
from pathlib import Path
import shutil
import subprocess
import tarfile
import pytest
from approved_release_executor import (
Binding,
ManualOwnerReviewRequired,
ReleaseBlocked,
ReconciliationRequired,
deploy_step,
execute,
tag_step,
verify_artifact,
verify_authorization,
)
def _binding(tmp_path: Path, **overrides) -> Binding:
artifact = tmp_path / "artifact.tar.zst"
artifact.write_bytes(b"x")
values = {
"version": "VTEST",
"target_oid": "a" * 40,
"evidence_closure_oid": "b" * 40,
"artifact_sha256": "c" * 64,
"approval_plan_id": "sha256:" + "d" * 64,
"artifact_path": str(artifact),
"artifact_size": 1,
"production_service": "test.service",
"production_port": 18927,
}
values.update(overrides)
return Binding(**values)
def _authorization(binding: Binding) -> dict[str, object]:
result: dict[str, object] = {
"accepted": True,
"binding_replaced": False,
"version": binding.version,
"target_oid": binding.target_oid,
"evidence_closure_oid": binding.evidence_closure_oid,
"artifact_sha256": binding.artifact_sha256,
"approval_plan_id": binding.approval_plan_id,
}
if binding.tag_oid is not None:
result.update(
{
"binding_schema": binding.binding_schema or "aios.release-binding.v2",
"tag_oid": binding.tag_oid,
}
)
if binding.source_registry_main_oid is not None:
result.update(
{
"source_registry_main_oid": binding.source_registry_main_oid,
"source_registry_path": binding.source_registry_path
or f"versions/{binding.version}.json",
"source_registry_sha256": binding.source_registry_sha256,
}
)
return result
def test_legacy_v2r2c1_style_authorization_remains_compatible(tmp_path: Path) -> None:
binding = _binding(tmp_path)
assert verify_authorization(_authorization(binding), binding)["accepted"] is True
def test_future_binding_requires_explicit_tag_oid(tmp_path: Path) -> None:
binding = _binding(
tmp_path,
version="V2R2C2",
tag_oid="1" * 40,
binding_schema="aios.release-binding.v2",
)
assert verify_authorization(_authorization(binding), binding)["tag_oid"] == "1" * 40
wrong = _authorization(binding)
wrong["tag_oid"] = "2" * 40
with pytest.raises(ManualOwnerReviewRequired, match="tag_oid"):
verify_authorization(wrong, binding)
def test_future_same_name_different_tag_object_requires_manual_review(monkeypatch, tmp_path: Path) -> None:
binding = _binding(
tmp_path,
version="V2R2C2",
tag_oid="1" * 40,
binding_schema="aios.release-binding.v2",
)
def fake(_command, _request):
return {
"status": "APPLIED_VERIFIED",
"version": binding.version,
"target_oid": binding.target_oid,
"artifact_sha256": binding.artifact_sha256,
"tag_oid": "2" * 40,
"peeled_target_oid": binding.target_oid,
}
monkeypatch.setattr("approved_release_executor.run_adapter", fake)
with pytest.raises(ManualOwnerReviewRequired, match="same-name tag object"):
tag_step(["/tag"], binding)
def test_executor_rechecks_source_registry_digest_and_build_provenance(
monkeypatch, tmp_path: Path
) -> None:
if shutil.which("zstd") is None:
pytest.skip("zstd is required for archive verification")
source = {
"schema": "aios.source-registry.v1",
"version": "V2R2C2",
"repository": "aios/aios",
"tag": "V2R2C2",
"tag_oid": "1" * 40,
"tag_object": "1" * 40,
"target_oid": "2" * 40,
"peeled_target_oid": "2" * 40,
"evidence_closure_oid": "3" * 40,
"approval_plan_id": "sha256:" + "4" * 64,
}
source_bytes = (json.dumps(source, ensure_ascii=False, sort_keys=True, indent=2) + "\n").encode()
source_digest = hashlib.sha256(source_bytes).hexdigest()
repo_work = tmp_path / "authority-work"
repo_bare = tmp_path / "authority.git"
(repo_work / "versions").mkdir(parents=True)
(repo_work / "versions" / "V2R2C2.json").write_bytes(source_bytes)
subprocess.run(["git", "init", "-q", str(repo_work)], check=True)
subprocess.run(["git", "-C", str(repo_work), "branch", "-M", "main"], check=True)
subprocess.run(["git", "-C", str(repo_work), "config", "user.name", "test"], check=True)
subprocess.run(
["git", "-C", str(repo_work), "config", "user.email", "test@example.invalid"],
check=True,
)
subprocess.run(["git", "-C", str(repo_work), "add", "versions"], check=True)
subprocess.run(["git", "-C", str(repo_work), "commit", "-qm", "source"], check=True)
source_main_oid = subprocess.run(
["git", "-C", str(repo_work), "rev-parse", "HEAD"],
capture_output=True,
text=True,
check=True,
).stdout.strip()
subprocess.run(["git", "clone", "-q", "--bare", str(repo_work), str(repo_bare)], check=True)
subprocess.run(
["git", f"--git-dir={repo_bare}", "config", "remote.gitea.url", "https://g.cnnetmkt.top/aios/aios.git"],
check=True,
)
monkeypatch.setenv("AIOS_TEST_MODE", "1")
monkeypatch.setenv("AIOS_GITOPS_AUTHORITATIVE_GIT_DIR", str(repo_bare))
metadata = {
"release_target_oid": "2" * 40,
"source_registry_main_oid": source_main_oid,
"source_registry_path": "versions/V2R2C2.json",
"source_registry_sha256": source_digest,
}
tar_path = tmp_path / "release.tar"
with tarfile.open(tar_path, "w") as archive:
for name, content in (
("payload/versions/V2R2C2.json", source_bytes),
("payload/BUILD-METADATA.json", (json.dumps(metadata, sort_keys=True) + "\n").encode()),
):
info = tarfile.TarInfo(name)
info.size = len(content)
archive.addfile(info, io.BytesIO(content))
artifact = tmp_path / "release.tar.zst"
subprocess.run(["zstd", "-q", "-19", "-f", str(tar_path), "-o", str(artifact)], check=True)
binding = _binding(
tmp_path,
version="V2R2C2",
tag_oid="1" * 40,
binding_schema="aios.release-binding.v2",
target_oid="2" * 40,
evidence_closure_oid="3" * 40,
approval_plan_id="sha256:" + "4" * 64,
source_registry_main_oid=source_main_oid,
source_registry_path="versions/V2R2C2.json",
source_registry_sha256=source_digest,
artifact_path=str(artifact),
artifact_size=artifact.stat().st_size,
artifact_sha256=hashlib.sha256(artifact.read_bytes()).hexdigest(),
)
result = verify_artifact(binding)
assert result["source_registry_sha256"] == source_digest
assert result["build_metadata_provenance_verified"] is True
bad_metadata = dict(metadata)
bad_metadata["source_registry_sha256"] = "0" * 64
bad_tar = tmp_path / "bad-metadata.tar"
with tarfile.open(bad_tar, "w") as archive:
for name, content in (
("payload/versions/V2R2C2.json", source_bytes),
("payload/BUILD-METADATA.json", (json.dumps(bad_metadata, sort_keys=True) + "\n").encode()),
):
info = tarfile.TarInfo(name)
info.size = len(content)
archive.addfile(info, io.BytesIO(content))
bad_artifact = tmp_path / "bad-metadata.tar.zst"
subprocess.run(["zstd", "-q", "-19", "-f", str(bad_tar), "-o", str(bad_artifact)], check=True)
bad_binding = _binding(
tmp_path,
version="V2R2C2",
tag_oid="1" * 40,
binding_schema="aios.release-binding.v2",
target_oid="2" * 40,
evidence_closure_oid="3" * 40,
approval_plan_id="sha256:" + "4" * 64,
source_registry_main_oid=source_main_oid,
source_registry_path="versions/V2R2C2.json",
source_registry_sha256=source_digest,
artifact_path=str(bad_artifact),
artifact_size=bad_artifact.stat().st_size,
artifact_sha256=hashlib.sha256(bad_artifact.read_bytes()).hexdigest(),
)
with pytest.raises(ReleaseBlocked, match="BUILD-METADATA"):
verify_artifact(bad_binding)
subprocess.run(["git", "-C", str(repo_work), "remote", "add", "origin", str(repo_bare)], check=True)
changed = dict(source)
changed["review_marker"] = "changed-after-M"
changed_bytes = (json.dumps(changed, ensure_ascii=False, sort_keys=True, indent=2) + "\n").encode()
(repo_work / "versions" / "V2R2C2.json").write_bytes(changed_bytes)
subprocess.run(["git", "-C", str(repo_work), "add", "versions"], check=True)
subprocess.run(["git", "-C", str(repo_work), "commit", "-qm", "change-source"], check=True)
subprocess.run(["git", "-C", str(repo_work), "push", "-q", "origin", "HEAD:main"], check=True)
with pytest.raises(ManualOwnerReviewRequired, match="current Authority main source registry"):
verify_artifact(binding)
def test_executor_requires_runtime_bundle_convergence_for_provenance_binding(tmp_path: Path):
binding = _binding(
tmp_path,
version="V2R2C2",
tag_oid="1" * 40,
binding_schema="aios.release-binding.v2",
source_registry_main_oid="5" * 40,
source_registry_path="versions/V2R2C2.json",
source_registry_sha256="6" * 64,
)
def fake_missing(_command, _request):
return {
"status": "ACTIVE_VERIFIED",
"version": binding.version,
"target_oid": binding.target_oid,
"evidence_closure_oid": binding.evidence_closure_oid,
"artifact_sha256": binding.artifact_sha256,
"approval_plan_id": binding.approval_plan_id,
"deployment_state": "ACTIVE",
"production_deployed": True,
}
monkeypatch = pytest.MonkeyPatch()
monkeypatch.setattr("approved_release_executor.run_adapter", fake_missing)
try:
with pytest.raises(ReconciliationRequired, match="runtime bundle/runtime registry"):
deploy_step(["/deploy"], binding)
finally:
monkeypatch.undo()
def fake_converged(_command, _request):
return {
"status": "ACTIVE_VERIFIED",
"version": binding.version,
"target_oid": binding.target_oid,
"evidence_closure_oid": binding.evidence_closure_oid,
"artifact_sha256": binding.artifact_sha256,
"approval_plan_id": binding.approval_plan_id,
"deployment_state": "ACTIVE",
"production_deployed": True,
"runtime_bundle_contains_current_version": True,
"runtime_registry_converged": True,
}
monkeypatch.setattr("approved_release_executor.run_adapter", fake_converged)
try:
assert deploy_step(["/deploy"], binding)["runtime_registry_converged"] is True
finally:
monkeypatch.undo()
def test_execute_rechecks_authoritative_git_after_runtime_before_registry(
monkeypatch, tmp_path: Path
):
work = tmp_path / "authority-work"
bare = tmp_path / "authority.git"
(work / "versions").mkdir(parents=True)
subprocess.run(["git", "init", "-q", "-b", "main", str(work)], check=True)
subprocess.run(["git", "-C", str(work), "config", "user.name", "test"], check=True)
subprocess.run(
["git", "-C", str(work), "config", "user.email", "test@example.invalid"],
check=True,
)
source = {
"schema": "aios.source-registry.v1",
"version": "VTEST",
"repository": "aios/aios",
"tag_oid": "1" * 40,
"tag_object": "1" * 40,
"target_oid": "a" * 40,
"peeled_target_oid": "a" * 40,
"evidence_closure_oid": "b" * 40,
"approval_plan_id": "sha256:" + "d" * 64,
}
source_path = work / "versions" / "VTEST.json"
source_path.write_bytes((json.dumps(source, sort_keys=True, indent=2) + "\n").encode())
subprocess.run(["git", "-C", str(work), "add", "versions"], check=True)
subprocess.run(["git", "-C", str(work), "commit", "-qm", "source"], check=True)
pinned_main = subprocess.run(
["git", "-C", str(work), "rev-parse", "HEAD"],
capture_output=True,
text=True,
check=True,
).stdout.strip()
subprocess.run(["git", "clone", "-q", "--bare", str(work), str(bare)], check=True)
source_sha = hashlib.sha256(source_path.read_bytes()).hexdigest()
binding = _binding(
tmp_path,
source_registry_main_oid=pinned_main,
source_registry_path="versions/VTEST.json",
source_registry_sha256=source_sha,
)
monkeypatch.setenv("AIOS_TEST_MODE", "1")
monkeypatch.setenv("AIOS_GITOPS_AUTHORITATIVE_GIT_DIR", str(bare))
monkeypatch.setattr("approved_release_executor.secure_json", lambda _path: _authorization(binding))
monkeypatch.setattr("approved_release_executor.verify_git_lineage", lambda *_args: {"verified": True})
monkeypatch.setattr("approved_release_executor.verify_artifact", lambda *_args, **_kwargs: {"verified": True})
deployed = False
calls = []
def fake_adapter(command, request):
nonlocal deployed
name = command[0]
operation = request["operation"]
calls.append((name, operation))
common = {
"version": binding.version,
"target_oid": binding.target_oid,
"evidence_closure_oid": binding.evidence_closure_oid,
"artifact_sha256": binding.artifact_sha256,
"approval_plan_id": binding.approval_plan_id,
}
if name == "/tag":
return {
**common,
"status": "APPLIED_VERIFIED",
"tag_oid": "1" * 40,
"peeled_target_oid": binding.target_oid,
}
if name == "/deploy":
if operation == "probe" and not deployed:
changed = dict(source)
changed["review_marker"] = "changed-after-runtime"
source_path.write_bytes((json.dumps(changed, sort_keys=True, indent=2) + "\n").encode())
subprocess.run(["git", "-C", str(work), "add", "versions"], check=True)
subprocess.run(["git", "-C", str(work), "commit", "-qm", "advance-main"], check=True)
subprocess.run(["git", "-C", str(work), "push", "-q", str(bare), "HEAD:main"], check=True)
deployed = True
return {
**common,
"status": "ACTIVE_VERIFIED",
"deployment_state": "ACTIVE",
"production_deployed": True,
"runtime_bundle_contains_current_version": True,
"runtime_registry_converged": True,
}
raise AssertionError(f"unexpected adapter call: {name}")
monkeypatch.setattr("approved_release_executor.run_adapter", fake_adapter)
with pytest.raises(ManualOwnerReviewRequired, match="current Authority main source registry"):
execute(
binding=binding,
authorization_path=tmp_path / "authorization.json",
repository=bare,
state_root=tmp_path / "state",
tag_command=["/tag"],
deploy_command=["/deploy"],
registry_command=["/registry"],
baseline_command=["/baseline"],
)
assert deployed is True
assert not any(name in {"/registry", "/baseline"} for name, _ in calls)
+850
View File
@@ -0,0 +1,850 @@
from __future__ import annotations
import hashlib
import inspect
import json
import os
import subprocess
import sys
from pathlib import Path
import pytest
APP = Path(__file__).resolve().parents[1] / "active-runtime" / "app"
sys.path.insert(0, str(APP))
import release_metadata_adapter as adapter # noqa: E402
import version_registry as registry_module # noqa: E402
from version_registry import ( # noqa: E402
RegistryConvergenceError,
assert_runtime_registry_convergence,
assert_source_registry_convergence,
build_source_registry_entry,
configured_approval_authority_root,
configured_authoritative_git_dir,
list_registry_versions,
list_registry_versions_report,
source_registry_snapshot,
verify_authoritative_repository,
verify_approval_authority,
verify_source_registry_main_convergence,
verify_source_registry_git_identity,
)
def _write_json(path: Path, obj: dict) -> None:
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(json.dumps(obj, sort_keys=True) + "\n", encoding="utf-8")
def _record(
*,
version: str = "V2R2C1",
tag_oid: str = "1" * 40,
target_oid: str = "2" * 40,
) -> dict:
return {
"schema": "aios.production-registry.v2",
"version": version,
"repository": "aios/aios",
"status": "PRODUCTION_DEPLOYED",
"production_deployed": True,
"tag_oid": tag_oid,
"target_oid": target_oid,
"peeled_target_oid": target_oid,
"evidence_closure_oid": "3" * 40,
"approval_plan_id": "sha256:" + "4" * 64,
}
def _canonical_approval(root: Path, record: dict) -> Path:
owner = {
"approved": True,
"status": "APPROVED",
"approved_by": "aiman",
"approval_source": "explicit_owner_approval_in_chat",
}
binding = {
key: record[key]
for key in (
"version",
"repository",
"tag_oid",
"target_oid",
"peeled_target_oid",
"evidence_closure_oid",
"approval_plan_id",
)
}
authority = {
"schema": "aios.v2r2c1.tag-approval-request.v1",
"state": "APPROVED",
"status": "APPROVED",
"execute": False,
"approval_plan_id": record["approval_plan_id"],
"binding": binding,
"owner_approval": owner,
"owner_approval_digest_sha256": hashlib.sha256(
(json.dumps(owner, sort_keys=True, separators=(",", ":")) + "\n").encode()
).hexdigest(),
}
path = root / f"{record['version']}.json"
_write_json(path, authority)
path.chmod(0o640)
return path
@pytest.fixture(autouse=True)
def _explicit_test_authority_mode(monkeypatch: pytest.MonkeyPatch):
"""Permit only these unit tests to inject temporary authority roots."""
monkeypatch.setenv("AIOS_TEST_MODE", "1")
def test_list_registry_versions_adds_only_baseline_attested_projection(tmp_path: Path):
ws = tmp_path / "ws"
(ws / "versions").mkdir(parents=True)
(ws / "versions" / "V2R1C7.json").write_text("{}\n")
projection_root = tmp_path / "projection"
projection = projection_root / "V2R2C1.json"
_write_json(projection, _record())
digest = hashlib.sha256(projection.read_bytes()).hexdigest()
baseline = tmp_path / "baseline" / "current.json"
_write_json(
baseline,
{
"version": "V2R2C1",
"status": "PRODUCTION_DEPLOYED",
"production_deployed": True,
"registry_source": str(projection),
"registry_digest": digest,
},
)
assert list_registry_versions(
ws,
projection_root=projection_root,
baseline_file=baseline,
) == ["V2R1C7.json", "V2R2C1.json"]
def test_list_registry_versions_rejects_projection_digest_drift(tmp_path: Path):
ws = tmp_path / "ws"
(ws / "versions").mkdir(parents=True)
projection_root = tmp_path / "projection"
projection = projection_root / "V2R2C1.json"
_write_json(projection, _record())
baseline = tmp_path / "baseline.json"
_write_json(
baseline,
{
"version": "V2R2C1",
"status": "PRODUCTION_DEPLOYED",
"production_deployed": True,
"registry_source": str(projection),
"registry_digest": "0" * 64,
},
)
assert list_registry_versions(
ws,
projection_root=projection_root,
baseline_file=baseline,
) == []
def test_projection_only_version_is_not_runtime_artifact_convergence(tmp_path: Path):
ws = tmp_path / "ws"
(ws / "versions").mkdir(parents=True)
projection_root = tmp_path / "projection"
projection = projection_root / "V2R2C1.json"
_write_json(projection, _record())
baseline = tmp_path / "baseline.json"
_write_json(
baseline,
{
"version": "V2R2C1",
"status": "PRODUCTION_DEPLOYED",
"production_deployed": True,
"registry_source": str(projection),
"registry_digest": hashlib.sha256(projection.read_bytes()).hexdigest(),
},
)
report = list_registry_versions_report(
ws,
projection_root=projection_root,
baseline_file=baseline,
)
assert report["registry_versions"] == ["V2R2C1.json"]
assert report["attested_projection_version"] == "V2R2C1"
assert report["runtime_bundle_contains_version"] is False
assert report["source_registry_converged"] == "unknown"
assert report["runtime_registry_converged"] is False
assert report["runtime_bundle_contains_current_version"] is False
assert report["registry_consistency_status"] == "RUNTIME_ARTIFACT_RECONCILIATION_REQUIRED"
with pytest.raises(RegistryConvergenceError, match="runtime_bundle_missing"):
assert_runtime_registry_convergence(
ws,
"V2R2C1",
projection_root=projection_root,
baseline_file=baseline,
)
def _attested_projection(tmp_path: Path, record: dict) -> tuple[Path, Path]:
projection_root = tmp_path / "projection"
projection = projection_root / f"{record['version']}.json"
_write_json(projection, record)
baseline = tmp_path / "baseline.json"
_write_json(
baseline,
{
"version": record["version"],
"status": "PRODUCTION_DEPLOYED",
"production_deployed": True,
"registry_source": str(projection),
"registry_digest": hashlib.sha256(projection.read_bytes()).hexdigest(),
},
)
return projection_root, baseline
def test_list_report_source_good_runtime_missing_is_not_fully_consistent(tmp_path: Path):
record = _record()
bare, branch = _init_bare_repo(tmp_path, record)
projection_root, baseline = _attested_projection(tmp_path, record)
ws = tmp_path / "runtime"
(ws / "versions").mkdir(parents=True)
report = list_registry_versions_report(
ws,
projection_root=projection_root,
baseline_file=baseline,
repo_git_dir=bare,
main_ref=f"refs/heads/{branch}",
)
assert report["source_registry_converged"] is True
assert report["runtime_registry_converged"] is False
assert report["runtime_bundle_contains_current_version"] is False
assert report["registry_consistency_status"] == "RUNTIME_ARTIFACT_RECONCILIATION_REQUIRED"
def test_list_report_source_runtime_projection_good_is_fully_consistent(tmp_path: Path):
record = _record()
bare, branch = _init_bare_repo(tmp_path, record)
projection_root, baseline = _attested_projection(tmp_path, record)
ws = tmp_path / "runtime"
versions = ws / "versions"
versions.mkdir(parents=True)
_write_json(versions / f"{record['version']}.json", record)
report = list_registry_versions_report(
ws,
projection_root=projection_root,
baseline_file=baseline,
repo_git_dir=bare,
main_ref=f"refs/heads/{branch}",
)
assert report["source_registry_converged"] is True
assert report["runtime_registry_converged"] is True
assert report["runtime_bundle_contains_current_version"] is True
assert report["registry_consistency_status"] == "FULLY_CONSISTENT"
def test_list_report_source_missing_is_not_converged(tmp_path: Path):
record = _record()
work = tmp_path / "no-source-work"
bare = tmp_path / "no-source.git"
subprocess.run(["git", "init", "-q", str(work)], check=True)
subprocess.run(["git", "-C", str(work), "config", "user.name", "test"], check=True)
subprocess.run(
["git", "-C", str(work), "config", "user.email", "test@example.invalid"],
check=True,
)
(work / "README").write_text("no source\n", encoding="utf-8")
subprocess.run(["git", "-C", str(work), "add", "README"], check=True)
subprocess.run(["git", "-C", str(work), "commit", "-qm", "no source"], check=True)
branch = subprocess.run(
["git", "-C", str(work), "symbolic-ref", "--short", "HEAD"],
capture_output=True,
text=True,
check=True,
).stdout.strip()
subprocess.run(["git", "clone", "-q", "--bare", str(work), str(bare)], check=True)
projection_root, baseline = _attested_projection(tmp_path, record)
ws = tmp_path / "runtime"
(ws / "versions").mkdir(parents=True)
_write_json(ws / "versions" / f"{record['version']}.json", record)
report = list_registry_versions_report(
ws,
projection_root=projection_root,
baseline_file=baseline,
repo_git_dir=bare,
main_ref=f"refs/heads/{branch}",
)
assert report["source_registry_converged"] is False
assert report["runtime_registry_converged"] is True
assert report["registry_consistency_status"] == "SOURCE_REGISTRY_NOT_CONVERGED"
def test_list_registry_versions_excludes_non_version_entries(tmp_path: Path):
ws = tmp_path / "ws"
versions = ws / "versions"
versions.mkdir(parents=True)
(versions / "README.txt").write_text("do not expose\n", encoding="utf-8")
(versions / "scratch.json").write_text("{}\n", encoding="utf-8")
(versions / "proposals").mkdir()
(versions / "V2R1C7.json").write_text("{}\n", encoding="utf-8")
assert list_registry_versions(
ws,
projection_root=tmp_path / "projection",
baseline_file=tmp_path / "baseline.json",
) == ["V2R1C7.json"]
def _init_bare_repo(tmp_path: Path, record: dict) -> tuple[Path, str]:
work = tmp_path / "work"
bare = tmp_path / "repo.git"
subprocess.run(["git", "init", "-q", str(work)], check=True)
subprocess.run(["git", "-C", str(work), "config", "user.name", "test"], check=True)
subprocess.run(["git", "-C", str(work), "config", "user.email", "test@example.invalid"], check=True)
_write_json(work / "versions" / f"{record['version']}.json", record)
subprocess.run(["git", "-C", str(work), "add", "versions"], check=True)
subprocess.run(["git", "-C", str(work), "commit", "-qm", "registry"], check=True)
branch = subprocess.run(
["git", "-C", str(work), "symbolic-ref", "--short", "HEAD"],
capture_output=True,
text=True,
check=True,
).stdout.strip()
subprocess.run(["git", "clone", "-q", "--bare", str(work), str(bare)], check=True)
return bare, branch
def _init_bare_repo_with_work(tmp_path: Path, record: dict) -> tuple[Path, Path, str]:
work = tmp_path / "history-work"
bare = tmp_path / "history.git"
subprocess.run(["git", "init", "-q", str(work)], check=True)
subprocess.run(["git", "-C", str(work), "config", "user.name", "test"], check=True)
subprocess.run(
["git", "-C", str(work), "config", "user.email", "test@example.invalid"],
check=True,
)
_write_json(work / "versions" / f"{record['version']}.json", record)
subprocess.run(["git", "-C", str(work), "add", "versions"], check=True)
subprocess.run(["git", "-C", str(work), "commit", "-qm", "registry"], check=True)
branch = subprocess.run(
["git", "-C", str(work), "symbolic-ref", "--short", "HEAD"],
capture_output=True,
text=True,
check=True,
).stdout.strip()
subprocess.run(["git", "clone", "-q", "--bare", str(work), str(bare)], check=True)
subprocess.run(["git", "-C", str(work), "remote", "add", "origin", str(bare)], check=True)
return work, bare, branch
def test_assert_source_registry_convergence_passes_matching_identity(tmp_path: Path):
record = _record()
bare, branch = _init_bare_repo(tmp_path, record)
projection = tmp_path / "projection.json"
_write_json(projection, record)
assert_source_registry_convergence(
projection,
repo_git_dir=bare,
main_ref=f"refs/heads/{branch}",
)
def test_assert_source_registry_convergence_fails_missing_entry(tmp_path: Path):
record = _record()
work = tmp_path / "work"
bare = tmp_path / "repo.git"
subprocess.run(["git", "init", "-q", str(work)], check=True)
subprocess.run(["git", "-C", str(work), "config", "user.name", "test"], check=True)
subprocess.run(["git", "-C", str(work), "config", "user.email", "test@example.invalid"], check=True)
(work / "README").write_text("x\n")
subprocess.run(["git", "-C", str(work), "add", "README"], check=True)
subprocess.run(["git", "-C", str(work), "commit", "-qm", "init"], check=True)
branch = subprocess.run(
["git", "-C", str(work), "symbolic-ref", "--short", "HEAD"],
capture_output=True,
text=True,
check=True,
).stdout.strip()
subprocess.run(["git", "clone", "-q", "--bare", str(work), str(bare)], check=True)
projection = tmp_path / "projection.json"
_write_json(projection, record)
with pytest.raises(RegistryConvergenceError, match="source_registry_entry_missing"):
assert_source_registry_convergence(
projection,
repo_git_dir=bare,
main_ref=f"refs/heads/{branch}",
)
def test_assert_source_registry_convergence_fails_identity_mismatch(tmp_path: Path):
source = _record(tag_oid="5" * 40)
projection_record = _record(tag_oid="1" * 40)
bare, branch = _init_bare_repo(tmp_path, source)
projection = tmp_path / "projection.json"
_write_json(projection, projection_record)
with pytest.raises(RegistryConvergenceError, match="source_registry_mismatch:tag_oid"):
assert_source_registry_convergence(
projection,
repo_git_dir=bare,
main_ref=f"refs/heads/{branch}",
)
def test_assert_source_registry_convergence_requires_all_identity_fields(tmp_path: Path):
source = _record()
source.pop("peeled_target_oid")
bare, branch = _init_bare_repo(tmp_path, source)
projection = tmp_path / "projection.json"
_write_json(projection, _record())
with pytest.raises(RegistryConvergenceError, match="peeled_target_oid"):
assert_source_registry_convergence(
projection,
repo_git_dir=bare,
main_ref=f"refs/heads/{branch}",
)
def test_assert_source_registry_convergence_rejects_wrong_repository(tmp_path: Path):
source = _record()
source["repository"] = "other/repository"
bare, branch = _init_bare_repo(tmp_path, source)
projection = tmp_path / "projection.json"
_write_json(projection, _record())
with pytest.raises(RegistryConvergenceError, match="repository"):
assert_source_registry_convergence(
projection,
repo_git_dir=bare,
main_ref=f"refs/heads/{branch}",
)
def test_source_registry_main_convergence_accepts_unrelated_later_main_commit(tmp_path: Path):
record = _record()
work, bare, branch = _init_bare_repo_with_work(tmp_path, record)
source_main = subprocess.run(
["git", "-C", str(work), "rev-parse", "HEAD"],
capture_output=True,
text=True,
check=True,
).stdout.strip()
(work / "README").write_text("unrelated later main commit\n", encoding="utf-8")
subprocess.run(["git", "-C", str(work), "add", "README"], check=True)
subprocess.run(["git", "-C", str(work), "commit", "-qm", "unrelated"], check=True)
subprocess.run(["git", "-C", str(work), "push", "-q", "origin", branch], check=True)
result = verify_source_registry_main_convergence(
bare,
record["version"],
main_ref=f"refs/heads/{branch}",
source_registry_main_oid=source_main,
expected_tag_oid=record["tag_oid"],
expected_target_oid=record["target_oid"],
expected_evidence_closure_oid=record["evidence_closure_oid"],
expected_approval_plan_id=record["approval_plan_id"],
)
assert result["source_registry_main_is_ancestor_of_current_main"] is True
def test_source_registry_main_convergence_rejects_side_branch_only_commit(tmp_path: Path):
record = _record()
work, bare, branch = _init_bare_repo_with_work(tmp_path, record)
base = subprocess.run(
["git", "-C", str(work), "rev-parse", "HEAD"],
capture_output=True,
text=True,
check=True,
).stdout.strip()
subprocess.run(["git", "-C", str(work), "checkout", "-qb", "side"], check=True)
(work / "side.txt").write_text("side\n", encoding="utf-8")
subprocess.run(["git", "-C", str(work), "add", "side.txt"], check=True)
subprocess.run(["git", "-C", str(work), "commit", "-qm", "side"], check=True)
side = subprocess.run(
["git", "-C", str(work), "rev-parse", "HEAD"],
capture_output=True,
text=True,
check=True,
).stdout.strip()
subprocess.run(["git", "-C", str(work), "checkout", "-q", branch], check=True)
(work / "main.txt").write_text("main\n", encoding="utf-8")
subprocess.run(["git", "-C", str(work), "add", "main.txt"], check=True)
subprocess.run(["git", "-C", str(work), "commit", "-qm", "main"], check=True)
subprocess.run(["git", "-C", str(work), "push", "-q", "origin", branch], check=True)
assert base != side
with pytest.raises(RegistryConvergenceError, match="not_ancestor"):
verify_source_registry_main_convergence(
bare,
record["version"],
main_ref=f"refs/heads/{branch}",
source_registry_main_oid=side,
)
def test_source_registry_main_convergence_rejects_removed_entry(tmp_path: Path):
record = _record()
work, bare, branch = _init_bare_repo_with_work(tmp_path, record)
source_main = subprocess.run(
["git", "-C", str(work), "rev-parse", "HEAD"],
capture_output=True,
text=True,
check=True,
).stdout.strip()
(work / "versions" / f"{record['version']}.json").unlink()
subprocess.run(["git", "-C", str(work), "add", "-u", "versions"], check=True)
subprocess.run(["git", "-C", str(work), "commit", "-qm", "remove"], check=True)
subprocess.run(["git", "-C", str(work), "push", "-q", "origin", branch], check=True)
with pytest.raises(RegistryConvergenceError, match="entry_missing"):
verify_source_registry_main_convergence(
bare,
record["version"],
main_ref=f"refs/heads/{branch}",
source_registry_main_oid=source_main,
)
def test_source_registry_main_convergence_rejects_altered_entry(tmp_path: Path):
record = _record()
work, bare, branch = _init_bare_repo_with_work(tmp_path, record)
source_main = subprocess.run(
["git", "-C", str(work), "rev-parse", "HEAD"],
capture_output=True,
text=True,
check=True,
).stdout.strip()
altered = dict(record)
altered["approval_plan_id"] = "sha256:" + "9" * 64
_write_json(work / "versions" / f"{record['version']}.json", altered)
subprocess.run(["git", "-C", str(work), "add", "versions"], check=True)
subprocess.run(["git", "-C", str(work), "commit", "-qm", "alter"], check=True)
subprocess.run(["git", "-C", str(work), "push", "-q", "origin", branch], check=True)
with pytest.raises(RegistryConvergenceError, match="entry_changed"):
verify_source_registry_main_convergence(
bare,
record["version"],
main_ref=f"refs/heads/{branch}",
source_registry_main_oid=source_main,
)
def test_source_registry_generator_predeclares_release_identity():
value = build_source_registry_entry(
version="V2R2C1",
tag_oid="1" * 40,
target_oid="2" * 40,
peeled_target_oid="2" * 40,
evidence_closure_oid="3" * 40,
approval_plan_id="sha256:" + "4" * 64,
)
assert set(
(
"version",
"tag_oid",
"target_oid",
"peeled_target_oid",
"evidence_closure_oid",
"approval_plan_id",
)
).issubset(value)
def test_authoritative_repository_rejects_arbitrary_repository(
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
):
record = _record()
bare, _ = _init_bare_repo(tmp_path, record)
monkeypatch.delenv("AIOS_TEST_MODE")
with pytest.raises(RegistryConvergenceError, match="path_mismatch"):
verify_authoritative_repository(bare)
def test_production_authority_defaults_are_fixed_and_env_overrides_fail(
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
):
monkeypatch.delenv("AIOS_TEST_MODE")
monkeypatch.delenv("AIOS_GITOPS_AUTHORITATIVE_GIT_DIR", raising=False)
monkeypatch.delenv("AIOS_GITOPS_APPROVAL_AUTHORITY_ROOT", raising=False)
assert configured_authoritative_git_dir() == Path("/data/aios-gitops/repo.git")
assert configured_approval_authority_root() == Path(
"/var/lib/aios-gitops/approval-authority"
)
monkeypatch.setenv("AIOS_GITOPS_AUTHORITATIVE_GIT_DIR", str(tmp_path / "fake.git"))
with pytest.raises(RegistryConvergenceError, match="override_forbidden"):
configured_authoritative_git_dir()
monkeypatch.setenv("AIOS_GITOPS_APPROVAL_AUTHORITY_ROOT", str(tmp_path / "fake-root"))
with pytest.raises(RegistryConvergenceError, match="override_forbidden"):
configured_approval_authority_root()
def test_production_canonical_repository_and_test_injected_authority(
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
):
record = _record()
bare, _ = _init_bare_repo(tmp_path, record)
subprocess.run(
[
"git",
f"--git-dir={bare}",
"config",
"remote.gitea.url",
"https://g.cnnetmkt.top/aios/aios.git",
],
check=True,
)
monkeypatch.delenv("AIOS_TEST_MODE")
monkeypatch.setattr(registry_module, "DEFAULT_AUTHORITATIVE_GIT_DIR", bare)
assert verify_authoritative_repository(bare)["repository_identity"] == "aios/aios"
monkeypatch.setenv("AIOS_TEST_MODE", "1")
monkeypatch.setenv("AIOS_GITOPS_AUTHORITATIVE_GIT_DIR", str(bare))
assert verify_authoritative_repository(bare)["configured_path"] == str(bare.resolve())
def test_authoritative_git_unavailable_stays_unknown_and_closure_fails(
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
):
record = _record()
projection_root, baseline = _attested_projection(tmp_path, record)
ws = tmp_path / "runtime"
(ws / "versions").mkdir(parents=True)
_write_json(ws / "versions" / f"{record['version']}.json", record)
missing = tmp_path / "missing-authority.git"
report = list_registry_versions_report(
ws,
projection_root=projection_root,
baseline_file=baseline,
repo_git_dir=missing,
)
assert report["source_registry_converged"] == "unknown"
assert report["source_registry_convergence_error"] == "authoritative_repository_unavailable"
monkeypatch.delenv("AIOS_TEST_MODE")
with pytest.raises(RegistryConvergenceError, match="path_unavailable"):
assert_source_registry_convergence(
projection_root / f"{record['version']}.json",
repo_git_dir=missing,
)
def test_source_registry_snapshot_stays_pinned_after_main_advances(tmp_path: Path):
work = tmp_path / "work"
bare = tmp_path / "repo.git"
subprocess.run(["git", "init", "-q", str(work)], check=True)
subprocess.run(["git", "-C", str(work), "config", "user.name", "test"], check=True)
subprocess.run(
["git", "-C", str(work), "config", "user.email", "test@example.invalid"],
check=True,
)
record = _record()
_write_json(work / "versions" / "V2R2C1.json", record)
subprocess.run(["git", "-C", str(work), "add", "versions"], check=True)
subprocess.run(["git", "-C", str(work), "commit", "-qm", "registry-1"], check=True)
branch = subprocess.run(
["git", "-C", str(work), "branch", "--show-current"],
capture_output=True,
text=True,
check=True,
).stdout.strip()
subprocess.run(["git", "clone", "-q", "--bare", str(work), str(bare)], check=True)
pinned = source_registry_snapshot(
bare,
"V2R2C1",
main_ref=f"refs/heads/{branch}",
)
updated = dict(record)
updated["review_marker"] = "main-advanced"
_write_json(work / "versions" / "V2R2C1.json", updated)
subprocess.run(["git", "-C", str(work), "add", "versions"], check=True)
subprocess.run(["git", "-C", str(work), "commit", "-qm", "registry-2"], check=True)
subprocess.run(["git", "-C", str(work), "remote", "add", "authority", str(bare)], check=True)
subprocess.run(["git", "-C", str(work), "push", "-q", "authority", branch], check=True)
repinned = source_registry_snapshot(
bare,
"V2R2C1",
main_ref=f"refs/heads/{branch}",
source_registry_main_oid=pinned["source_registry_main_oid"],
)
assert repinned["source_registry_main_oid"] == pinned["source_registry_main_oid"]
assert repinned["bytes"] == pinned["bytes"]
assert repinned["source_registry_sha256"] == pinned["source_registry_sha256"]
assert repinned["record"].get("review_marker") is None
def _git_commit_with_release_tag(tmp_path: Path, *, annotated: bool = True):
work = tmp_path / "authority-work"
bare = tmp_path / "authority.git"
subprocess.run(["git", "init", "-q", str(work)], check=True)
subprocess.run(["git", "-C", str(work), "config", "user.name", "test"], check=True)
subprocess.run(["git", "-C", str(work), "config", "user.email", "test@example.invalid"], check=True)
(work / "README").write_text("release\n", encoding="utf-8")
subprocess.run(["git", "-C", str(work), "add", "README"], check=True)
subprocess.run(["git", "-C", str(work), "commit", "-qm", "target"], check=True)
target = subprocess.run(
["git", "-C", str(work), "rev-parse", "HEAD"], capture_output=True, text=True, check=True
).stdout.strip()
tag_args = ["git", "-C", str(work), "tag"]
if annotated:
tag_args += ["-a", "V2R2C1", target, "-m", "release"]
else:
tag_args += ["V2R2C1", target]
subprocess.run(tag_args, check=True)
tag_oid = subprocess.run(
["git", "-C", str(work), "rev-parse", "refs/tags/V2R2C1"],
capture_output=True,
text=True,
check=True,
).stdout.strip()
subprocess.run(["git", "-C", str(work), "commit", "--allow-empty", "-qm", "evidence"], check=True)
evidence = subprocess.run(
["git", "-C", str(work), "rev-parse", "HEAD"], capture_output=True, text=True, check=True
).stdout.strip()
record = _record(tag_oid=tag_oid, target_oid=target)
record["evidence_closure_oid"] = evidence
_write_json(work / "versions" / "V2R2C1.json", record)
subprocess.run(["git", "-C", str(work), "add", "versions"], check=True)
subprocess.run(["git", "-C", str(work), "commit", "-qm", "source registry"], check=True)
branch = subprocess.run(
["git", "-C", str(work), "branch", "--show-current"], capture_output=True, text=True, check=True
).stdout.strip()
subprocess.run(["git", "clone", "-q", "--bare", str(work), str(bare)], check=True)
return work, bare, branch, record, target, evidence, tag_oid
def test_source_registry_git_identity_rejects_lightweight_tag(tmp_path: Path):
_, bare, _, record, _, _, _ = _git_commit_with_release_tag(tmp_path, annotated=False)
with pytest.raises(RegistryConvergenceError, match="tag_not_annotated"):
verify_source_registry_git_identity(bare, record)
def test_source_registry_git_identity_proves_annotated_tag_and_lineage(
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
):
_, bare, _, record, target, evidence, tag_oid = _git_commit_with_release_tag(tmp_path)
result = verify_source_registry_git_identity(bare, record)
assert result["tag_oid"] == tag_oid
assert result["target_oid"] == target
assert result["evidence_closure_oid"] == evidence
authority_root = tmp_path / "authority-root"
authority_root.mkdir()
monkeypatch.setenv("AIOS_GITOPS_APPROVAL_AUTHORITY_ROOT", str(authority_root))
authority = _canonical_approval(authority_root, record)
assert verify_approval_authority(authority, record)["release_binding_verified"] is True
def test_source_registry_generator_requires_authoritative_git_and_approval(
tmp_path: Path, monkeypatch: pytest.MonkeyPatch,
):
_, bare, _, record, _, _, _ = _git_commit_with_release_tag(tmp_path)
subprocess.run(
["git", f"--git-dir={bare}", "config", "remote.gitea.url", "https://g.cnnetmkt.top/aios/aios.git"],
check=True,
)
authority_root = tmp_path / "authority-root"
authority_root.mkdir()
monkeypatch.setenv("AIOS_GITOPS_AUTHORITATIVE_GIT_DIR", str(bare))
monkeypatch.setenv("AIOS_GITOPS_APPROVAL_AUTHORITY_ROOT", str(authority_root))
authority = _canonical_approval(authority_root, record)
output = tmp_path / "generated" / "versions" / "V2R2C1.json"
script = Path(__file__).resolve().parents[1] / "scripts" / "generate_source_registry.py"
command = [
sys.executable,
str(script),
"--output",
str(output),
"--version",
record["version"],
"--tag-oid",
record["tag_oid"],
"--target-oid",
record["target_oid"],
"--peeled-target-oid",
record["peeled_target_oid"],
"--evidence-closure-oid",
record["evidence_closure_oid"],
"--approval-plan-id",
record["approval_plan_id"],
"--repository",
str(bare),
"--approval-authority",
str(authority),
]
completed = subprocess.run(command, capture_output=True, text=True, check=False)
assert completed.returncode == 0, completed.stderr
assert json.loads(output.read_text(encoding="utf-8"))["tag_oid"] == record["tag_oid"]
wrong = list(command)
wrong[wrong.index("--approval-plan-id") + 1] = "sha256:" + "f" * 64
failed = subprocess.run(wrong, capture_output=True, text=True, check=False)
assert failed.returncode != 0
other = tmp_path / "other.git"
subprocess.run(["git", "clone", "-q", "--bare", str(bare), str(other)], check=True)
arbitrary = list(command)
arbitrary[arbitrary.index("--repository") + 1] = str(other)
failed_authority = subprocess.run(arbitrary, capture_output=True, text=True, check=False)
assert failed_authority.returncode != 0
def test_approval_authority_rejects_unassociated_plan(
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
):
record = _record()
authority_root = tmp_path / "authority-root"
authority_root.mkdir()
monkeypatch.setenv("AIOS_GITOPS_APPROVAL_AUTHORITY_ROOT", str(authority_root))
authority = authority_root / f"{record['version']}.json"
_write_json(
authority,
{
"schema": "aios.v2r2c1.tag-approval-request.v1",
"state": "APPROVED",
"approval_plan_id": record["approval_plan_id"],
},
)
authority.chmod(0o640)
with pytest.raises(RegistryConvergenceError, match="binding_missing"):
verify_approval_authority(authority, record)
def test_approval_authority_rejects_untrusted_path(tmp_path: Path, monkeypatch: pytest.MonkeyPatch):
authority_root = tmp_path / "authority-root"
authority_root.mkdir()
monkeypatch.setenv("AIOS_GITOPS_APPROVAL_AUTHORITY_ROOT", str(authority_root))
authority = tmp_path / "approval.json"
_write_json(authority, {"schema": "aios.v2r2c1.tag-approval-request.v1"})
with pytest.raises(RegistryConvergenceError, match="untrusted_path"):
verify_approval_authority(authority, _record())
def test_approval_authority_rejects_revoked_receipt(tmp_path: Path, monkeypatch: pytest.MonkeyPatch):
authority_root = tmp_path / "authority-root"
authority_root.mkdir()
monkeypatch.setenv("AIOS_GITOPS_APPROVAL_AUTHORITY_ROOT", str(authority_root))
record = _record()
authority = _canonical_approval(authority_root, record)
value = json.loads(authority.read_text(encoding="utf-8"))
value["revoked"] = True
authority.write_text(json.dumps(value) + "\n", encoding="utf-8")
authority.chmod(0o640)
with pytest.raises(RegistryConvergenceError, match="revoked"):
verify_approval_authority(authority, record)
def test_baseline_apply_wires_convergence_to_caller_repository():
source = inspect.getsource(adapter.baseline_apply)
assert "assert_source_registry_convergence(" in source
assert "repo_git_dir=args.repository" in source
# The gate must execute before the dict that writes final production status.
assert source.index("assert_source_registry_convergence(") < source.index(
'"status": "PRODUCTION_DEPLOYED"'
)
-763
View File
@@ -1,763 +0,0 @@
from __future__ import annotations
from concurrent.futures import ThreadPoolExecutor
from datetime import datetime, timedelta, timezone
import hashlib
from dataclasses import asdict
from pathlib import Path
import sqlite3
import json
from types import SimpleNamespace
import threading
import pytest
from aios_chatgpt_bridge_v2_server import GovernedRuntime
from aios_continuation_executor import ContinuationPermissions, ContinuationRuntimeBinding
from aios_continuation_lease import WriterLeaseRequestV2
from aios_gitops_job_controller import GitOpsJobController, JobError, JobStore
from aios_governed_task_bootstrap import GovernedTaskBootstrap
TASK_ID = "gov-writer-lease-recovery-test"
BRANCH = "agent/writer-lease-recovery-test"
BASE = "a" * 40
JOB_ID_PREFIX = "writer-lease-runtime-lineage"
class LocalAuthority:
def __init__(self, head: str | None = None) -> None:
self.head = head
def read_ref(self, _ref: str) -> str | None:
return self.head
def commit_exists(self, _oid: str) -> bool:
return True
def ancestry_check(self, _older: str, _newer: str) -> bool:
return True
def writer_request(task_id: str = TASK_ID, **overrides: object) -> dict[str, object]:
request: dict[str, object] = {
"schema": WriterLeaseRequestV2.schema,
"task_id": task_id,
"repository": "aios/aios",
"branch_mode": "CREATE_NEW",
"base_oid": BASE,
"expected_branch_oid": BASE,
"target_branch": BRANCH,
"generation": 1,
"lease_seconds": 120,
}
request.update(overrides)
return request
def make_runtime(tmp_path: Path, task_id: str = TASK_ID, *, authority_head: str | None = None) -> GovernedRuntime:
runtime = GovernedRuntime.__new__(GovernedRuntime)
runtime.config = SimpleNamespace(repository="aios/aios", state_root=tmp_path)
runtime.authority = LocalAuthority(authority_head)
runtime.bindings = {
task_id: {
"task_id": task_id,
"repository": "aios/aios",
"base_oid": BASE,
"expected_branch_oid": BASE,
"target_branch": BRANCH,
"branch_mode": "CREATE_NEW",
"generation": 1,
}
}
runtime.runtimes = {}
runtime.controllers = {}
runtime.bootstrap = GovernedTaskBootstrap(
runtime.authority,
tmp_path,
remote_name="test",
token_path=tmp_path / "token",
ssh_key_path=tmp_path / "key",
)
runtime.bindings = {}
runtime.create_task(
{
"task_id": task_id,
"repository": "aios/aios",
"base_oid": BASE,
"expected_branch_oid": BASE,
"target_branch": BRANCH,
"risk": "engineering",
"description": "governed runtime lineage test",
"idempotency_key": JOB_ID_PREFIX + "-" + task_id,
}
)
job_root = runtime._job_root(task_id)
runtime.controllers[task_id] = GitOpsJobController(
JobStore(job_root, payload_root=job_root / "payloads")
)
def build(
lease: WriterLeaseRequestV2,
*,
job_id: str,
lease_not_before: datetime | None = None,
lease_expires_at: datetime | None = None,
) -> tuple[ContinuationRuntimeBinding, GitOpsJobController, object]:
controller = runtime.controllers.get(lease.task_id)
if controller is None:
root = runtime._job_root(lease.task_id)
controller = GitOpsJobController(JobStore(root, payload_root=root / "payloads"))
now = datetime.now(timezone.utc)
return (
ContinuationRuntimeBinding(
request=lease,
job_id=job_id,
workspace_path=tmp_path / "worktrees" / lease.task_id,
lease_not_before=lease_not_before or now - timedelta(seconds=1),
lease_expires_at=lease_expires_at or now + timedelta(seconds=lease.lease_seconds),
original_receipt_valid=True,
original_grant_consumed=True,
permissions=ContinuationPermissions(),
),
controller,
object(),
)
runtime._build_runtime = build
return runtime
def create_durable_lease(
runtime: GovernedRuntime,
*,
expires_at: str = "2020-01-01T00:00:00+00:00",
) -> GitOpsJobController:
root = runtime._job_root(TASK_ID)
controller = GitOpsJobController(JobStore(root, payload_root=root / "payloads"))
receipt = SimpleNamespace(job_id=runtime.bindings[TASK_ID]["job_id"])
controller.lease_job(
receipt.job_id,
lease_id="writer-lease-recovery",
lineage_anchor_oid=BASE,
current_head_oid=BASE,
generation=1,
lease_expires_at=expires_at,
expected_revision=0,
)
runtime.controllers[TASK_ID] = controller
return controller
def durable_row(runtime: GovernedRuntime) -> tuple[str, int]:
database = runtime._job_root(TASK_ID) / "state.sqlite3"
with sqlite3.connect(f"file:{database}?mode=ro", uri=True) as connection:
row = connection.execute(
"SELECT lease_expires_at, revision FROM branch_leases WHERE lease_id=?",
("writer-lease-recovery",),
).fetchone()
assert row is not None
return row
def test_first_bootstrap_refreshes_only_runtime_without_durable_lease(tmp_path: Path) -> None:
runtime = make_runtime(tmp_path)
result = runtime.bootstrap_writer_lease(writer_request())
assert result["bootstrapped"] is True
assert result["runtime_rehydrated"] is False
assert result["durable_branch_lease_present"] is False
assert result["idempotent"] is False
database = runtime._job_root(TASK_ID) / "state.sqlite3"
with sqlite3.connect(f"file:{database}?mode=ro", uri=True) as connection:
assert connection.execute("SELECT COUNT(*) FROM branch_leases").fetchone()[0] == 0
def test_duplicate_bootstrap_is_idempotent_without_consuming_generation(tmp_path: Path) -> None:
runtime = make_runtime(tmp_path)
first = runtime.bootstrap_writer_lease(writer_request())
second = runtime.bootstrap_writer_lease(writer_request())
assert first["bootstrapped"] is True
assert first["generation"] == 1
assert second["bootstrapped"] is False
assert second["idempotent"] is True
assert second["generation"] == 1
database = runtime._job_root(TASK_ID) / "state.sqlite3"
with sqlite3.connect(f"file:{database}?mode=ro", uri=True) as connection:
assert connection.execute("SELECT COUNT(*) FROM branch_leases").fetchone()[0] == 0
def test_commit_after_bootstrap_reuses_the_exact_runtime_binding(tmp_path: Path) -> None:
runtime = make_runtime(tmp_path)
class RecordingWorkspace:
def __init__(self) -> None:
self.files: dict[str, bytes] = {}
def materialize_files(self, files: dict[str, bytes]) -> None:
self.files = dict(files)
class RecordingExecutor:
def __init__(self) -> None:
self.workspace = RecordingWorkspace()
self.binding = None
self.commit = None
def execute(self, binding: object, commit: object) -> dict[str, object]:
self.binding = binding
self.commit = commit
return {'task_id': binding.request.task_id, 'generation': binding.request.generation, 'files': sorted(self.workspace.files)}
executor = RecordingExecutor()
original_build = runtime._build_runtime
def build(lease: WriterLeaseRequestV2, *, job_id: str, lease_not_before: datetime | None=None, lease_expires_at: datetime | None=None) -> tuple[ContinuationRuntimeBinding, GitOpsJobController, object]:
binding, controller, _ = original_build(lease, job_id=job_id, lease_not_before=lease_not_before, lease_expires_at=lease_expires_at)
return (binding, controller, executor)
runtime._build_runtime = build
runtime.bootstrap_writer_lease(writer_request())
lease_response = runtime.acquire_lease(writer_request())
binding, controller, executor = runtime.runtimes[TASK_ID, 1]
assert lease_response['job_id'] == binding.job_id
assert lease_response['durable_branch_lease_present'] is True
_original_execute = executor.execute
def _execute_with_governed_commit_contract(*args, **kwargs):
_before_complete = kwargs.pop('before_complete', None)
_legacy = dict(_original_execute(*args, **kwargs))
_result = {**_legacy, 'job_id': binding.job_id, 'state': 'COMPLETED', 'side_effect_state': 'REMOTE_VERIFIED', 'receipt': {'authority_oid': 'b' * 40, 'git_remote_oid': 'b' * 40, 'api_branch_oid': 'b' * 40, 'commit_visible': True, 'remote_verified': True, 'force_push': False}}
if _before_complete is not None:
_before_complete('b' * 40)
return _result
executor.execute = _execute_with_governed_commit_contract
_bound_job_id = binding.job_id
result = runtime.commit_files({'task_id': TASK_ID, 'repository': 'aios/aios', 'generation': 1, 'target_branch': BRANCH, 'expected_branch_oid': BASE, 'idempotency_key': 'commit-after-bootstrap-v1', 'path': 'docs/recovery.txt', 'content_base64': 'YQ==', 'message': 'test commit after bootstrap'})
assert result['task_id'] == TASK_ID
assert result['generation'] == 1
assert result['files'] == ['docs/recovery.txt']
assert executor.binding.request.task_id == TASK_ID
assert executor.binding.request.generation == 1
database = runtime._job_root(TASK_ID) / 'state.sqlite3'
with sqlite3.connect(f'file:{database}?mode=ro', uri=True) as connection:
assert connection.execute('SELECT COUNT(*) FROM branch_leases').fetchone()[0] == 1
assert connection.execute('PRAGMA foreign_key_check(branch_leases)').fetchall() == []
assert result['commit_oid'] == 'b' * 40
assert result['commit_visible'] is True
assert result['branch_mode'] == 'CONTINUE_EXISTING'
assert result['target_branch'] == BRANCH
assert int(result['generation']) == 1
with controller.store._connect() as connection:
_persisted = connection.execute('SELECT current_head_oid,revision FROM branch_leases WHERE lease_id=?', ('lease-' + str(_bound_job_id),)).fetchone()
assert _persisted is not None
assert _persisted[0] == 'b' * 40
assert int(_persisted[1]) == 1
fresh_runtime = make_runtime(tmp_path, authority_head='b' * 40)
fresh_response = fresh_runtime.bootstrap_writer_lease(writer_request(branch_mode='CONTINUE_EXISTING', expected_branch_oid='b' * 40))
fresh_binding, _, _ = fresh_runtime.runtimes[TASK_ID, 1]
assert fresh_response['job_id'] == str(_bound_job_id)
assert fresh_response['runtime_rehydrated'] is True
assert fresh_binding.job_id == str(_bound_job_id)
assert fresh_binding.request.expected_branch_oid == 'b' * 40
with fresh_runtime.controllers[TASK_ID].store._connect() as connection:
_fresh_lease = connection.execute('SELECT job_id,current_head_oid,revision FROM branch_leases WHERE lease_id=?', ('lease-' + str(_bound_job_id),)).fetchone()
assert tuple(_fresh_lease) == (str(_bound_job_id), 'b' * 40, 1)
@pytest.mark.parametrize(
("field", "value", "error"),
(
("repository", "other/repository", "LEASE_BINDING_CONFLICT"),
("base_oid", "b" * 40, "LEASE_BINDING_CONFLICT"),
("expected_branch_oid", "b" * 40, "LEASE_BINDING_CONFLICT"),
("target_branch", "agent/wrong-target", "LEASE_BINDING_CONFLICT"),
("generation", 2, "GENERATION_BINDING_CONFLICT"),
),
)
def test_bootstrap_rejects_any_nonmatching_binding_field(
tmp_path: Path, field: str, value: object, error: str
) -> None:
runtime = make_runtime(tmp_path)
with pytest.raises(RuntimeError, match=error):
runtime.bootstrap_writer_lease(writer_request(**{field: value}))
def test_bootstrap_rejects_unknown_task_ownership(tmp_path: Path) -> None:
runtime = make_runtime(tmp_path)
with pytest.raises(RuntimeError, match="TASK_OWNERSHIP_INVALID"):
runtime.bootstrap_writer_lease(writer_request(task_id="unknown-task"))
def test_renew_missing_durable_lease_is_not_bootstrap(tmp_path: Path) -> None:
runtime = make_runtime(tmp_path)
with pytest.raises(RuntimeError, match="LEASE_NOT_FOUND"):
runtime.renew_writer_lease(writer_request())
def test_renew_is_generation_scoped_before_durable_lookup(tmp_path: Path) -> None:
runtime = make_runtime(tmp_path)
create_durable_lease(runtime)
with pytest.raises(RuntimeError, match="GENERATION_BINDING_CONFLICT"):
runtime.renew_writer_lease(writer_request(generation=2))
assert durable_row(runtime) == ("2020-01-01T00:00:00+00:00", 0)
def test_restart_rehydrates_only_from_matching_durable_lease(tmp_path: Path) -> None:
runtime = make_runtime(tmp_path)
create_durable_lease(runtime)
runtime.runtimes.clear()
result = runtime.renew_writer_lease(writer_request())
expires_at, revision = durable_row(runtime)
assert result["recovered"] is True
assert result["runtime_rehydrated"] is True
assert result["durable_branch_lease_present"] is True
assert revision == 1
assert datetime.fromisoformat(expires_at) > datetime.now(timezone.utc)
def test_duplicate_renewal_is_idempotent_after_first_durable_recovery(tmp_path: Path) -> None:
runtime = make_runtime(tmp_path)
create_durable_lease(runtime)
first = runtime.renew_writer_lease(writer_request())
second = runtime.renew_writer_lease(writer_request())
assert first["recovered"] is True
assert first["idempotent"] is False
assert second["recovered"] is False
assert second["idempotent"] is True
assert durable_row(runtime)[1] == 1
def test_update_lease_keeps_expiry_without_optional_argument_and_keeps_cas(tmp_path: Path) -> None:
runtime = make_runtime(tmp_path)
controller = create_durable_lease(runtime)
controller.update_lease(
"writer-lease-recovery",
expected_revision=0,
current_head_oid=BASE,
generation=1,
)
assert durable_row(runtime) == ("2020-01-01T00:00:00+00:00", 1)
with pytest.raises(JobError, match="LEASE_REVISION_CAS_CONFLICT"):
controller.update_lease(
"writer-lease-recovery",
expected_revision=0,
current_head_oid=BASE,
generation=1,
)
def test_two_runtime_connections_resolve_renewal_cas_race_idempotently(tmp_path: Path) -> None:
first = make_runtime(tmp_path)
first_controller = create_durable_lease(first)
second = make_runtime(tmp_path)
second_controller = GitOpsJobController(
JobStore(second._job_root(TASK_ID), payload_root=second._job_root(TASK_ID) / "payloads")
)
second.controllers[TASK_ID] = second_controller
gate = threading.Barrier(2)
def gate_update(controller: GitOpsJobController) -> None:
original = controller.update_lease
def update(*args: object, **kwargs: object) -> None:
gate.wait(timeout=5)
original(*args, **kwargs)
controller.update_lease = update
gate_update(first_controller)
gate_update(second_controller)
with ThreadPoolExecutor(max_workers=2) as pool:
results = list(pool.map(lambda runtime: runtime.renew_writer_lease(writer_request()), (first, second)))
assert sorted((item["recovered"], item["idempotent"]) for item in results) == [
(False, True),
(True, False),
]
assert durable_row(first)[1] == 1
def test_bootstrap_preserves_exact_governed_job_lineage(tmp_path: Path) -> None:
runtime = make_runtime(tmp_path)
expected_job_id = runtime.bindings[TASK_ID]["job_id"]
response = runtime.bootstrap_writer_lease(writer_request())
binding, _, _ = runtime.runtimes[(TASK_ID, 1)]
assert response["job_id"] == expected_job_id
assert binding.job_id == expected_job_id
assert binding.request.job_id == expected_job_id
def test_restart_rehydrate_preserves_exact_governed_job_lineage(tmp_path: Path) -> None:
first = make_runtime(tmp_path)
expected_job_id = first.bindings[TASK_ID]["job_id"]
first.acquire_lease(writer_request())
runtime = make_runtime(tmp_path)
assert runtime is not first
assert runtime.runtimes == {}
response = runtime.bootstrap_writer_lease(writer_request())
binding, _, _ = runtime.runtimes[(TASK_ID, 1)]
assert response["job_id"] == expected_job_id
assert binding.job_id == expected_job_id
assert binding.request.job_id == expected_job_id
def test_wrong_governed_job_id_is_denied_before_runtime_or_lease_side_effect(tmp_path: Path) -> None:
runtime = make_runtime(tmp_path)
with pytest.raises(RuntimeError, match="LEASE_BINDING_CONFLICT"):
runtime.bootstrap_writer_lease(writer_request(job_id="job-wrong-lineage"))
assert runtime.runtimes == {}
database = runtime._job_root(TASK_ID) / "state.sqlite3"
with sqlite3.connect(f"file:{database}?mode=ro", uri=True) as connection:
assert connection.execute("SELECT COUNT(*) FROM branch_leases").fetchone()[0] == 0
assert connection.execute("PRAGMA foreign_key_check(branch_leases)").fetchall() == []
def test_acquire_creates_exact_governed_writer_lease_with_fk_and_cas(tmp_path: Path) -> None:
runtime = make_runtime(tmp_path)
expected_job_id = runtime.bindings[TASK_ID]["job_id"]
response = runtime.acquire_lease(writer_request())
assert response["job_id"] == expected_job_id
assert response["durable_branch_lease_present"] is True
assert response["idempotent"] is False
database = runtime._job_root(TASK_ID) / "state.sqlite3"
with sqlite3.connect(f"file:{database}?mode=ro", uri=True) as connection:
foreign_keys = connection.execute(
"PRAGMA foreign_key_list(branch_leases)"
).fetchall()
row = connection.execute(
"SELECT job_id,branch,lineage_anchor_oid,current_head_oid,generation,state,revision "
"FROM branch_leases"
).fetchone()
job = connection.execute(
"SELECT state,side_effect_state,revision FROM jobs WHERE job_id=?",
(expected_job_id,),
).fetchone()
assert connection.execute("PRAGMA foreign_key_check(branch_leases)").fetchall() == []
assert any(
fk[2] == "jobs" and fk[3] == "job_id" and fk[4] == "job_id"
for fk in foreign_keys
)
assert row == (expected_job_id, BRANCH, BASE, BASE, 1, "ACTIVE", 0)
assert job == ("LEASED", "LEASED", 2)
def test_duplicate_acquire_observes_same_durable_writer_lease(tmp_path: Path) -> None:
runtime = make_runtime(tmp_path)
expected_job_id = runtime.bindings[TASK_ID]["job_id"]
first = runtime.acquire_lease(writer_request())
second = runtime.acquire_lease(writer_request())
assert first["job_id"] == expected_job_id
assert second["job_id"] == expected_job_id
assert second["idempotent"] is True
database = runtime._job_root(TASK_ID) / "state.sqlite3"
with sqlite3.connect(f"file:{database}?mode=ro", uri=True) as connection:
leases = connection.execute(
"SELECT lease_id,job_id,revision FROM branch_leases"
).fetchall()
job = connection.execute(
"SELECT state,revision FROM jobs WHERE job_id=?",
(expected_job_id,),
).fetchone()
assert leases == [("lease-" + expected_job_id, expected_job_id, 0)]
assert job == ("LEASED", 2)
def test_wrong_job_acquire_is_denied_before_authorize_or_lease(tmp_path: Path) -> None:
runtime = make_runtime(tmp_path)
expected_job_id = runtime.bindings[TASK_ID]["job_id"]
with pytest.raises(RuntimeError, match="LEASE_BINDING_CONFLICT"):
runtime.acquire_lease(writer_request(job_id="job-wrong-lineage"))
database = runtime._job_root(TASK_ID) / "state.sqlite3"
with sqlite3.connect(f"file:{database}?mode=ro", uri=True) as connection:
job = connection.execute(
"SELECT state,side_effect_state,revision FROM jobs WHERE job_id=?",
(expected_job_id,),
).fetchone()
lease_count = connection.execute("SELECT COUNT(*) FROM branch_leases").fetchone()[0]
assert job == ("REQUESTED", "NONE", 0)
assert lease_count == 0
def test_omitted_request_job_id_resolves_the_exact_durable_parent(tmp_path: Path) -> None:
runtime = make_runtime(tmp_path)
expected_job_id = runtime.bindings[TASK_ID]["job_id"]
raw = dict(writer_request())
response = runtime.acquire_lease(raw)
binding, _, _ = runtime.runtimes[(TASK_ID, 1)]
assert response["job_id"] == expected_job_id
assert binding.job_id == expected_job_id
assert binding.request.job_id == expected_job_id
def test_durable_job_expected_head_mismatch_is_denied(tmp_path: Path) -> None:
runtime = make_runtime(tmp_path)
expected_job_id = runtime.bindings[TASK_ID]["job_id"]
controller = runtime.controllers[TASK_ID]
with controller.store._connect() as connection:
row = connection.execute(
"SELECT request_json FROM jobs WHERE job_id=?", (expected_job_id,)
).fetchone()
request = json.loads(row[0])
request["expected_branch_oid"] = "c" * 40
request["binding"]["expected_branch_oid"] = "c" * 40
connection.execute(
"UPDATE jobs SET request_json=? WHERE job_id=?",
(json.dumps(request, sort_keys=True, separators=(",", ":")), expected_job_id),
)
connection.commit()
with pytest.raises(RuntimeError, match="RUNTIME_GOVERNED_JOB_UNRESOLVED"):
runtime.bootstrap_writer_lease(writer_request())
assert runtime.runtimes == {}
def _commit_ready_runtime(tmp_path: Path):
runtime = make_runtime(tmp_path)
runtime.bootstrap_writer_lease(writer_request())
binding, controller, _ = runtime.runtimes[(TASK_ID, 1)]
class Workspace:
def materialize_files(self, _files):
return None
class Executor:
def __init__(self):
self.workspace = Workspace()
self.calls = 0
def execute(self, _binding, _commit, *, before_complete=None):
self.calls += 1
result = {
"job_id": binding.job_id,
"state": "COMPLETED",
"side_effect_state": "REMOTE_VERIFIED",
"receipt": {
"authority_oid": "b" * 40,
"git_remote_oid": "b" * 40,
"api_branch_oid": "b" * 40,
"commit_visible": True,
"remote_verified": True,
"force_push": False,
},
}
if before_complete is not None:
before_complete("b" * 40)
return result
executor = Executor()
runtime.runtimes[(TASK_ID, 1)] = (binding, controller, executor)
runtime.acquire_lease(writer_request())
return runtime, binding, controller, executor
def _commit_request():
return {
"task_id": TASK_ID,
"repository": "aios/aios",
"generation": 1,
"target_branch": BRANCH,
"expected_branch_oid": BASE,
"idempotency_key": "commit-cas-test-v1",
"path": "docs/cas.txt",
"content_base64": "YQ==",
"message": "test commit cas",
}
def test_commit_lease_revision_cas_conflict_is_preserved(tmp_path: Path) -> None:
runtime, _binding, controller, _executor = _commit_ready_runtime(tmp_path)
def conflict(*_args, **_kwargs):
raise JobError("LEASE_REVISION_CAS_CONFLICT")
controller.update_lease = conflict
with pytest.raises(RuntimeError, match="LEASE_CAS_CONFLICT"):
runtime.commit_files(_commit_request())
def test_commit_divergent_durable_head_fails_before_executor(tmp_path: Path) -> None:
runtime, binding, controller, executor = _commit_ready_runtime(tmp_path)
controller.update_lease(
"lease-" + binding.job_id,
expected_revision=0,
current_head_oid="c" * 40,
generation=1,
)
with pytest.raises(RuntimeError, match="LEASE_HEAD_RECONCILIATION_REQUIRED"):
runtime.commit_files(_commit_request())
assert executor.calls == 0
def test_remote_pending_never_advances_durable_head(tmp_path: Path) -> None:
runtime, binding, controller, executor = _commit_ready_runtime(tmp_path)
def pending(_binding, _commit, **_kwargs):
executor.calls += 1
return {
"job_id": binding.job_id,
"state": "RECONCILIATION_REQUIRED",
"side_effect_state": "REMOTE_PUSH_PENDING",
"receipt": {
"authority_oid": "b" * 40,
"desired_remote_oid": "b" * 40,
"remote_verified": False,
},
}
executor.execute = pending
with pytest.raises(RuntimeError, match="RECONCILIATION_REQUIRED"):
runtime.commit_files(_commit_request())
with controller.store._connect() as connection:
row = connection.execute(
"SELECT current_head_oid,revision FROM branch_leases WHERE lease_id=?",
("lease-" + binding.job_id,),
).fetchone()
assert tuple(row) == (BASE, 0)
def test_retry_accepts_exact_head_already_advanced_at_remote_verified_boundary(tmp_path: Path) -> None:
runtime, binding, controller, executor = _commit_ready_runtime(tmp_path)
commit_oid = "b" * 40
controller.update_lease(
"lease-" + binding.job_id,
expected_revision=0,
current_head_oid=commit_oid,
generation=1,
)
receipt = {
"authority_oid": commit_oid,
"git_remote_oid": commit_oid,
"api_branch_oid": commit_oid,
"commit_visible": True,
"remote_verified": True,
"force_push": False,
}
with controller.store._connect() as connection:
connection.execute(
"UPDATE jobs SET state='REMOTE_VERIFIED',side_effect_state='REMOTE_VERIFIED',"
"receipt_json=? WHERE job_id=?",
(json.dumps(receipt, sort_keys=True, separators=(",", ":")), binding.job_id),
)
connection.commit()
result = runtime.commit_files(_commit_request())
assert result["commit_oid"] == commit_oid
assert executor.calls == 1
with controller.store._connect() as connection:
row = connection.execute(
"SELECT current_head_oid,revision FROM branch_leases WHERE lease_id=?",
("lease-" + binding.job_id,),
).fetchone()
assert tuple(row) == (commit_oid, 1)
def test_completed_response_with_invalid_oid_uses_canonical_oid_error(tmp_path: Path) -> None:
runtime, binding, controller, executor = _commit_ready_runtime(tmp_path)
def invalid_oid(_binding, _commit, **_kwargs):
executor.calls += 1
return {
"job_id": binding.job_id,
"state": "COMPLETED",
"side_effect_state": "REMOTE_VERIFIED",
"receipt": {
"authority_oid": "not-an-oid",
"commit_visible": True,
"remote_verified": True,
},
}
executor.execute = invalid_oid
with pytest.raises(RuntimeError, match="GOVERNED_COMMIT_OID_INVALID"):
runtime.commit_files(_commit_request())
with controller.store._connect() as connection:
row = connection.execute(
"SELECT current_head_oid,revision FROM branch_leases WHERE lease_id=?",
("lease-" + binding.job_id,),
).fetchone()
assert tuple(row) == (BASE, 0)
def test_acquire_renews_an_expired_matching_durable_lease_with_revision_cas(tmp_path: Path) -> None:
runtime = make_runtime(tmp_path)
create_durable_lease(runtime)
runtime.runtimes.clear()
result = runtime.acquire_lease(writer_request())
expires_at, revision = durable_row(runtime)
assert result["recovered"] is True
assert result["runtime_rehydrated"] is False
assert result["durable_branch_lease_present"] is True
assert revision == 1
assert datetime.fromisoformat(expires_at) > datetime.now(timezone.utc)
def test_authority_recovers_only_clean_detached_single_attempt_worktree(tmp_path: Path) -> None:
import subprocess
from aios_authority_git_executor import AuthorityGitExecutor
bare = tmp_path / "authority.git"
seed = tmp_path / "seed"
subprocess.run(["git", "init", "--bare", str(bare)], check=True, capture_output=True)
subprocess.run(["git", "init", str(seed)], check=True, capture_output=True)
(seed / "base.txt").write_text("base\n", encoding="utf-8")
subprocess.run(["git", "-C", str(seed), "add", "base.txt"], check=True)
subprocess.run(
["git", "-C", str(seed), "-c", "user.name=test", "-c", "user.email=test@example.invalid",
"commit", "-m", "base"], check=True, capture_output=True,
)
base = subprocess.check_output(["git", "-C", str(seed), "rev-parse", "HEAD"], text=True).strip()
subprocess.run(["git", "-C", str(seed), "push", str(bare), "HEAD:refs/heads/main"], check=True, capture_output=True)
root = tmp_path / "worktrees"
authority = AuthorityGitExecutor(bare, root)
worktree = authority.create_detached_worktree(base, "task")
(worktree / "attempt.txt").write_text("attempt\n", encoding="utf-8")
subprocess.run(["git", "-C", str(worktree), "add", "attempt.txt"], check=True)
subprocess.run(
["git", "-C", str(worktree), "-c", "user.name=test", "-c", "user.email=test@example.invalid",
"commit", "-m", "failed attempt"], check=True, capture_output=True,
)
stale = subprocess.check_output(["git", "-C", str(worktree), "rev-parse", "HEAD"], text=True).strip()
recovered = authority.recover_failed_detached_worktree_exact(
path=worktree,
expected_oid=base,
stale_oid=stale,
target_ref="refs/heads/agent/recovery-test",
)
assert recovered == stale
assert subprocess.check_output(["git", "-C", str(worktree), "rev-parse", "HEAD"], text=True).strip() == base
assert subprocess.run(["git", "-C", str(worktree), "symbolic-ref", "-q", "HEAD"]).returncode == 1
assert subprocess.check_output(["git", "-C", str(worktree), "status", "--porcelain"], text=True) == ""
+21
View File
@@ -0,0 +1,21 @@
{
"approval_plan_id": "sha256:c0df52008ae7f64cec5ee2d28947aa7b892066c670ab8295e4549140b1a7500e",
"artifact_sha256": "ab4ae257346c82863f05abf5e309381a1c86612957608c3a4f4ab5cc9233f052",
"deployment_state": "ACTIVE",
"enabled": true,
"evidence_closure_oid": "a8b906d1bc24088c19647e98de6a679268f6d0a2",
"peeled_target_oid": "998d30304787e31e5ae607c0bd86b5edb2b0e5ad",
"production_deployed": true,
"production_port": 18927,
"production_service": "aios-v2r1c7-production.service",
"registered": true,
"repository": "aios/aios",
"schema": "aios.production-registry.v2",
"status": "PRODUCTION_DEPLOYED",
"tag": "V2R2C1",
"tag_object": "07fc2746b688f5f243f29b39de39f32edc3dd9d2",
"tag_oid": "07fc2746b688f5f243f29b39de39f32edc3dd9d2",
"target_oid": "998d30304787e31e5ae607c0bd86b5edb2b0e5ad",
"updated_at": "2026-08-09T09:13:13Z",
"version": "V2R2C1"
}