Compare commits

...
Author SHA1 Message Date
AIOS Governed Continuation 540e28a724 Publish exact C5 issuer V3 bootstrap source
autonomy-smoke-tests Exact governed tree; evidence 7dbba7ff168e72af
autonomy-unit-tests Exact governed tree; evidence 7dbba7ff168e72af
campaign-approval-gate Exact governed tree; evidence 7dbba7ff168e72af
git-diff-check Exact governed tree; evidence 7dbba7ff168e72af
py-compile Exact governed tree; evidence 7dbba7ff168e72af
reconciliation-contract Exact governed tree; evidence 7dbba7ff168e72af
release-state-machine Exact governed tree; evidence 7dbba7ff168e72af
secret-scan Exact governed tree; evidence 7029b3eafb209066
signer-validator-contract Exact governed tree; evidence 7dbba7ff168e72af
2026-10-01 23:09:33 +08:00
Codex Security e33664f033 docs(c6): record main publication and Linux authorization acceptance 2026-10-01 21:30:28 +08:00
Codex Security 83b77a53d2 merge(c6): land accepted-C5 autonomy and authorization patch 2026-10-01 21:29:00 +08:00
Codex Security 30fff31145 fix(c6): enforce exclusive credential expiry and verify release gaps 2026-10-01 21:28:02 +08:00
Codex Security 765f337b82 test(c6): pin inherited C5 fixture file modes 2026-10-01 21:12:56 +08:00
Codex Security 034a07a05e merge(c6): apply autonomy patch on accepted V2R2C5 2026-10-01 20:57:05 +08:00
AIOS Governed Continuation 5f65e45895 Publish exact C5 issuer V3 bootstrap source 2026-10-01 11:41:03 +08:00
Codex Security d841948c21 test(c6): await real HTTP readiness before authorization assertion 2026-09-29 19:51:59 +08:00
Codex Security 5d821096ce feat(c6): bind host authority and exact durable patch execution 2026-09-29 19:46:14 +08:00
Codex Security e87c70778a merge(c6): integrate original candidate on provisional C5 capability v17
First parent: provisional C5 396ea98111 (not production accepted). Second parent: immutable original C6 a6851c0a7e.

Preserve C5 production.plan.apply, production.promote and production.rollback in the formal capability vocabulary and full17 fail-closed tests. Preserve C6 vocabulary digest negotiation, evidence verifier configuration and identity-aware recovery tests. No production publication or authorization is implied.
2026-09-29 17:48:54 +08:00
owner 396ea98111 feat(governed): add production.* capability family to canonical contract (C5 v17)
Add production.plan.apply/promote/rollback to _EXACT_LIFECYCLE_CAPABILITIES so
the canonical fail-closed contract self-maps them into _TASK_ALIASES and
_WORKER_ALIASES, recognizing the C5 production promotion capability family.

This closes CONTRACT_GAP_IN_ACTIVE_TASK_API_V16 at the canonical single source
(deployed-v16-overlay vs authoritative-repo-canonical convergence). No wildcard,
no implicit privilege, no change to fail-closed/unknown semantics, no change to
has_capabilities subset logic.

Tests: extend tests/test_v2r2c5_capability_contract.py with C5 production.* cases
(required/worker normalization recognized, full-17 has_capabilities==True,
unknown fail-closed, wildcard rejected, missing production denied).

Canonical contract SHA256: 67d8a3a296110e20a4ea59863cd7982b6bdada71abd457c0f7cec480b06d278a
2026-09-29 12:39:08 +08:00
Codex Security a6851c0a7e fix(release): reject concurrent reentry of live capability claims 2026-09-29 00:52:51 +08:00
Codex Security 7b0d387221 feat(release): derive renewable execution credentials from standing authorization 2026-09-29 00:47:20 +08:00
Codex Security faf6eb2289 feat(c6): integrate standing release authority and resumable task projections 2026-09-28 02:53:36 +08:00
Codex Security d54f832668 merge(c6): incorporate reviewed C5 automation baseline 2af1dfb 2026-09-28 02:07:46 +08:00
AIOS Governed Continuation b915386ccb V2R2C5 governed source publication 2026-09-28 00:16:42 +08:00
Codex Security 2af1dfbc40 Keep redaction fixture while passing governed content scan 2026-09-28 00:04:30 +08:00
Codex Security 3b36619487 Fix governed writer preflight binding contract 2026-09-27 23:53:54 +08:00
Codex Security 3e432fe42c Submit governed source publication task result 2026-09-26 15:04:31 +08:00
Codex Security 7dbe51de39 fix(c5): preserve legacy storage activation scope 2026-09-26 15:03:08 +08:00
Codex Security b707f8a05f Require governed write readiness in C5 staging artifact 2026-09-26 14:58:46 +08:00
Codex Security 83c41c8f2e Add typed rollback for applied C5 staging installs 2026-09-26 14:57:54 +08:00
Codex Security a96901ce52 Reconcile unknown C5 staging installations 2026-09-26 14:38:25 +08:00
Codex Security 084fbc9930 fix(c5): preserve issuer helper ABI 2026-09-26 14:38:12 +08:00
Codex Security 581e2d3c0c Keep installer replay state beneath root-owned boundary 2026-09-26 14:29:32 +08:00
Codex Security 7ffe99a0b5 fix(c5): preserve effective principal lineage 2026-09-26 14:19:30 +08:00
Codex Security 6582e8a5e1 Bind staging artifacts to exact dual-target installer contracts 2026-09-26 14:02:52 +08:00
Codex Security 8fd457f8f5 fix(c5): package effective issuer entrypoint 2026-09-26 13:55:55 +08:00
Codex Security 4e66d483e4 Normalize authoritative publication job identity 2026-09-26 13:55:15 +08:00
Codex Security a887646377 fix(c5): canonicalize enrollment job identity 2026-09-26 13:51:25 +08:00
Codex Security 1227714ce3 Align C5 principal paths with live admission state 2026-09-26 13:49:58 +08:00
Codex Security 757de6b8ed Bind C5 principal config to admission store 2026-09-26 13:49:58 +08:00
Codex Security eff070bc37 Compose exact live C5 governed publication controller 2026-09-26 13:49:19 +08:00
Codex Security 3b1c3a1b60 fix(c5): align issuer source health fields 2026-09-26 13:45:53 +08:00
Codex Security e55392184d Require issuer loaded-module readiness proof 2026-09-26 13:45:32 +08:00
Codex Security 7e0acaae4c Add C5 principal issuer installation target 2026-09-26 13:45:32 +08:00
Codex Security 3bd736db7f fix(c5): make overlay verification portable 2026-09-26 13:45:32 +08:00
Codex Security 7022801a2d build(c5): add deterministic staging artifact packager 2026-09-26 13:44:40 +08:00
Codex Security f9253dd28b Bind principal activation and preserve raw publication receipts 2026-09-26 13:43:39 +08:00
Codex Security c24ed5ee8e feat(c5): add governed principal scope enrollment overlay 2026-09-26 13:39:24 +08:00
Codex Security db18ca306d Add independently verified bounded C5 staging installation 2026-09-26 13:27:59 +08:00
Codex Security 8f3bd0d11f Prepare exact C5 authority and preserve governed publication evidence 2026-09-26 13:25:49 +08:00
Codex Security 7a64e326c2 Bind exact C5 source admission to live verified runtime overlays 2026-09-26 13:10:18 +08:00
Codex Security 3609f45d69 build(c5): generate hash-bound live epoch overlay 2026-09-26 12:58:36 +08:00
Codex Security 41df96a46a Harden registered Owner signer boundary 2026-09-26 12:46:26 +08:00
Codex Security 4c17ce3c98 Add registered Owner signer adapter 2026-09-26 12:46:26 +08:00
Codex Security 5d0f9970f6 Add governed publication transport adapter 2026-09-26 12:43:17 +08:00
Codex Security 7837f67119 feat(c5): bind source policy to epoch provenance 2026-09-26 12:43:17 +08:00
Codex Security 4a5e3cbfec Integrate governed unattended release with isolated signed admission proof 2026-09-26 12:12:36 +08:00
Codex Security 64eed3b119 docs(c6): record integration validation and remaining release gates 2026-09-26 12:06:11 +08:00
Codex Security cd0a90bc37 feat(c6): add governed automation and verifiable component contracts 2026-09-26 12:05:35 +08:00
Codex Security 6c1dcb829a Tighten source publication admission bindings 2026-09-26 11:53:21 +08:00
Codex Security fb8a0f3e47 Fail closed on unattended authority boundaries 2026-09-26 11:52:08 +08:00
Codex Security d2ad00139d Add governed source publication policy lifecycle 2026-09-26 11:49:28 +08:00
Codex Security b5c27fec1d fix(task-admission): align epoch v2 producer enums 2026-09-26 11:49:28 +08:00
Codex Security e43209ebdd Validate durable release candidate readback 2026-09-26 11:46:59 +08:00
Codex Security ab149d0a57 Add governed unattended release runner 2026-09-26 11:46:59 +08:00
Codex Security dbefdcda07 feat(task-admission): consume owner epoch v2 2026-09-26 11:43:01 +08:00
Codex Security 536a2af0f0 test(task-plane): prove unverifiable completion is atomic 2026-09-26 09:47:39 +08:00
Codex Security 7cc1730025 Enforce formal AgentTask capability contract 2026-09-26 09:45:15 +08:00
Codex Security 30f501c77e Restore formal AgentTask capability contract 2026-09-26 09:45:14 +08:00
Codex Security 58987681fe test(task-plane): inject authenticated evidence fixtures 2026-09-26 09:43:57 +08:00
Codex Security 440f1aedc9 fix(task-plane): require verifiers for evidence proofs 2026-09-26 09:43:57 +08:00
Codex Security 9e7024ba63 fix(task-plane): fail closed on unverified evidence and actions 2026-09-26 09:43:57 +08:00
190 changed files with 47993 additions and 702 deletions
+53 -14
View File
@@ -1,40 +1,79 @@
"""API adapter for agent context routing and visibility."""
from collections.abc import Mapping
from typing import Any
try:
from fastapi import APIRouter
from fastapi import APIRouter, HTTPException
except Exception:
APIRouter = None
APIRouter = HTTPException = None
from .agent_context_dashboard import AgentContextDashboard
from .agent_context_rbac import AgentContextRBAC
from .agent_context_resolver import AgentContextResolver
from .agent_observability_api import ApiSecurityError, authenticated_principal, set_authenticator
resolver = AgentContextResolver()
rbac = AgentContextRBAC()
dashboard = AgentContextDashboard()
_AUTHORITY_KEYS = frozenset({"user_id", "group_id", "role", "owner", "owner_policy", "group_policy", "agent_default"})
def resolve_agent_context(payload: dict[str, Any]) -> dict[str, Any]:
route = rbac.route(payload["user_id"], payload.get("group_id"), payload.get("role", "user"))
principal = authenticated_principal()
user_id = principal["user_id"]
group_id = principal.get("group_id")
role = principal["role"]
if role == "group_admin" and (not isinstance(group_id, str) or not group_id):
raise ApiSecurityError(403, "GROUP_SCOPE_REQUIRED")
route = rbac.route(user_id, group_id, role)
def server_policy(name: str) -> dict[str, Any] | None:
value = principal.get(name)
if value is None:
return None
if not isinstance(value, Mapping):
raise ApiSecurityError(403, "INVALID_AUTHENTICATED_POLICY")
return dict(value)
def client_overlay(name: str) -> dict[str, Any] | None:
value = payload.get(name)
if value is None:
return None
if not isinstance(value, Mapping):
raise ApiSecurityError(403, "INVALID_CONTEXT_OVERLAY")
return {key: item for key, item in value.items() if key not in _AUTHORITY_KEYS}
defaults = server_policy("agent_default") or {}
defaults.update(user_id=user_id, group_id=group_id or "", role=role)
context = resolver.resolve(
user_id=payload["user_id"],
group_id=payload.get("group_id", ""),
session=payload.get("session"),
user_profile=payload.get("user_profile"),
group_policy=payload.get("group_policy"),
agent_default=payload.get("agent_default"),
owner_policy=payload.get("owner_policy"),
owner=payload.get("role") == "owner",
user_id=user_id,
group_id=group_id or "",
session=client_overlay("session"),
user_profile=client_overlay("user_profile"),
group_policy=server_policy("group_policy"),
agent_default=defaults,
owner_policy=server_policy("owner_policy") if role == "owner" else None,
owner=role == "owner",
)
return {"routing": route.to_dict(), "context": context.to_dict()}
def get_context_dashboard() -> dict[str, Any]:
authenticated_principal()
return dashboard.overview()
def _http(call):
try:
return call()
except ApiSecurityError as exc:
if HTTPException is None:
raise
raise HTTPException(status_code=exc.status_code, detail=exc.code) from exc
def build_router():
if APIRouter is None:
return None
@@ -42,15 +81,15 @@ def build_router():
@router.post("/resolve", operation_id="resolveAgentContext")
def resolve(payload: dict[str, Any]):
return resolve_agent_context(payload)
return _http(lambda: resolve_agent_context(payload))
@router.get("/overview", operation_id="getAgentContextDashboard")
def overview():
return get_context_dashboard()
return _http(get_context_dashboard)
return router
router = build_router()
__all__ = ["router", "resolve_agent_context", "get_context_dashboard"]
__all__ = ["router", "set_authenticator", "resolve_agent_context", "get_context_dashboard"]
+123 -37
View File
@@ -1,47 +1,129 @@
"""Actions API for unified Agent Event Bus."""
"""Authenticated loopback API for the unified Agent Event Bus."""
from __future__ import annotations
from collections.abc import Callable, Mapping
import ipaddress
from typing import Any
try:
from fastapi import APIRouter
except Exception:
APIRouter = None
from fastapi import APIRouter, HTTPException, Request
except Exception: # pragma: no cover
APIRouter = HTTPException = Request = None
from .agent_event_bus import AgentEventBus
from .agent_observability_api import ApiSecurityError, authenticated_principal, set_authenticator
bus = AgentEventBus()
_CREATE_FIELDS = frozenset({"kind", "source", "target", "summary", "payload", "correlation_id"})
def create_agent_event(payload: dict[str, Any]) -> dict[str, Any]:
return {"status": "ok", "event": bus.publish(**payload)}
def _authorize(client_host: str | None) -> dict[str, Any]:
try:
address = ipaddress.ip_address(client_host or "")
except ValueError as exc:
raise ApiSecurityError(403, "LOOPBACK_REQUIRED") from exc
if not address.is_loopback:
raise ApiSecurityError(403, "LOOPBACK_REQUIRED")
return authenticated_principal()
def list_agent_events(kind: str | None = None, status: str = "pending") -> dict[str, Any]:
return {"status": "ok", "events": bus.list_events(kind, status)}
def _event_scope(principal: Mapping[str, Any]) -> tuple[str, frozenset[str]]:
tenant_id = principal.get("tenant_id")
targets = principal.get("allowed_event_targets")
if not isinstance(tenant_id, str) or not tenant_id:
raise ApiSecurityError(403, "EVENT_TENANT_SCOPE_REQUIRED")
if (not isinstance(targets, (list, tuple, set, frozenset)) or not targets
or any(not isinstance(target, str) or not target for target in targets)):
raise ApiSecurityError(403, "EVENT_TARGET_SCOPE_REQUIRED")
return tenant_id, frozenset(targets)
def get_agent_event(event_id: str) -> dict[str, Any]:
return {"status": "ok", "event": bus.get(event_id)}
def _authorized_event(
event: Mapping[str, Any] | None, tenant_id: str, targets: frozenset[str],
) -> Mapping[str, Any] | None:
if event is None:
return None
if event.get("tenant_id") != tenant_id or event.get("target") not in targets:
raise ApiSecurityError(403, "EVENT_SCOPE_DENIED")
return event
def transition_agent_event(event_id: str, status: str, actor: str = "system") -> dict[str, Any]:
return {"status": "ok", "event": bus.transition(event_id, status, actor)}
def create_agent_event(payload: Mapping[str, Any], *, client_host: str | None = None) -> dict[str, Any]:
principal = _authorize(client_host)
tenant_id, targets = _event_scope(principal)
if set(payload) - _CREATE_FIELDS:
raise ApiSecurityError(403, "EVENT_FIELDS_FORBIDDEN")
required = ("kind", "target", "summary", "payload")
if any(name not in payload for name in required):
raise ApiSecurityError(403, "INVALID_EVENT")
normalized = {name: payload[name] for name in required}
if not isinstance(normalized["target"], str) or normalized["target"] not in targets:
raise ApiSecurityError(403, "EVENT_TARGET_SCOPE_DENIED")
normalized["source"] = principal["user_id"]
normalized["correlation_id"] = payload.get("correlation_id")
normalized["tenant_id"] = tenant_id
return {"status": "ok", "event": bus.publish(**normalized)}
def agent_event_stats() -> dict[str, Any]:
return {"status": "ok", "stats": bus.stats()}
def list_agent_events(
kind: str | None = None, status: str = "pending", *, client_host: str | None = None,
) -> dict[str, Any]:
principal = _authorize(client_host)
tenant_id, targets = _event_scope(principal)
return {"status": "ok", "events": bus.list_events(
kind, status, tenant_id=tenant_id, targets=targets,
)}
# Compatibility adapters
def list_gpt_inbox_messages():
return list_agent_events(kind="gpt_inbox")
def get_agent_event(event_id: str, *, client_host: str | None = None) -> dict[str, Any]:
principal = _authorize(client_host)
tenant_id, targets = _event_scope(principal)
return {"status": "ok", "event": _authorized_event(bus.get(event_id), tenant_id, targets)}
def ack_gpt_inbox_message(event_id: str):
return transition_agent_event(event_id, "completed", "custom-gpt")
def transition_agent_event(
event_id: str, status: str, actor: str = "system", *, client_host: str | None = None,
) -> dict[str, Any]:
del actor
principal = _authorize(client_host)
tenant_id, targets = _event_scope(principal)
_authorized_event(bus.get(event_id), tenant_id, targets)
return {"status": "ok", "event": bus.transition(
event_id, status, principal["user_id"], expected_tenant_id=tenant_id,
expected_targets=targets,
)}
def agent_event_stats(*, client_host: str | None = None) -> dict[str, Any]:
principal = _authorize(client_host)
tenant_id, targets = _event_scope(principal)
return {"status": "ok", "stats": bus.stats(tenant_id=tenant_id, targets=targets)}
def list_gpt_inbox_messages(*, client_host: str | None = None):
return list_agent_events(kind="gpt_inbox", client_host=client_host)
def ack_gpt_inbox_message(event_id: str, *, client_host: str | None = None):
return transition_agent_event(event_id, "completed", client_host=client_host)
def _host(request: Any) -> str | None:
client = getattr(request, "client", None)
return getattr(client, "host", None)
def _http(call: Callable[[], Any]) -> Any:
try:
return call()
except ApiSecurityError as exc:
if HTTPException is None:
raise
raise HTTPException(status_code=exc.status_code, detail=exc.code) from exc
except ValueError as exc:
if HTTPException is None:
raise
raise HTTPException(status_code=400, detail=str(exc)) from exc
def build_router():
@@ -50,28 +132,32 @@ def build_router():
router = APIRouter(prefix="/agent-events", tags=["agent-events"])
@router.post("", operation_id="createAgentEvent")
def create(payload: dict[str, Any]):
return create_agent_event(payload)
def create(payload: dict[str, Any], request: Request):
return _http(lambda: create_agent_event(payload, client_host=_host(request)))
@router.get("", operation_id="listAgentEvents")
def listing(kind: str | None = None, status: str = "pending"):
return list_agent_events(kind, status)
@router.get("/{event_id}", operation_id="getAgentEvent")
def get(event_id: str):
return get_agent_event(event_id)
@router.post("/{event_id}/transition", operation_id="transitionAgentEvent")
def transition(event_id: str, payload: dict[str, Any]):
return transition_agent_event(event_id, payload["status"], payload.get("actor", "system"))
def listing(request: Request, kind: str | None = None, status: str = "pending"):
return _http(lambda: list_agent_events(kind, status, client_host=_host(request)))
@router.get("/stats/overview", operation_id="getAgentEventStats")
def stats():
return agent_event_stats()
def stats(request: Request):
return _http(lambda: agent_event_stats(client_host=_host(request)))
@router.get("/{event_id}", operation_id="getAgentEvent")
def get(event_id: str, request: Request):
return _http(lambda: get_agent_event(event_id, client_host=_host(request)))
@router.post("/{event_id}/transition", operation_id="transitionAgentEvent")
def transition(event_id: str, payload: dict[str, Any], request: Request):
return _http(lambda: transition_agent_event(
event_id, payload.get("status", ""), payload.get("actor", "system"),
client_host=_host(request),
))
return router
router = build_router()
__all__ = ["router", "create_agent_event", "list_agent_events", "get_agent_event", "transition_agent_event"]
__all__ = [
"router", "set_authenticator", "create_agent_event", "list_agent_events",
"get_agent_event", "transition_agent_event", "agent_event_stats",
]
+192 -57
View File
@@ -1,10 +1,4 @@
"""Unified Agent Event Bus.
Unifies commands, GPT messages, replies, approvals, audit and reports into one
state machine. Compatibility layers can filter events by kind instead of keeping
separate queues.
"""
"""Filesystem-backed Agent Event Bus with strict path contracts."""
from __future__ import annotations
from dataclasses import asdict, dataclass, field
@@ -12,8 +6,12 @@ from datetime import datetime, timezone
from hashlib import sha256
from pathlib import Path
import json
import os
import stat
from typing import Any
from .security_path_guards import contained_path, safe_segment, write_text_no_follow
def utc_now() -> str:
return datetime.now(timezone.utc).isoformat()
@@ -27,6 +25,7 @@ class AgentEvent:
target: str
summary: str
payload: dict[str, Any]
tenant_id: str | None = None
status: str = "pending"
correlation_id: str | None = None
created_at: str = field(default_factory=utc_now)
@@ -39,87 +38,223 @@ class AgentEvent:
class AgentEventBus:
STATES = ("pending", "running", "completed", "failed")
_DIR_FLAGS = os.O_RDONLY | getattr(os, "O_DIRECTORY", 0) | getattr(os, "O_NOFOLLOW", 0)
def __init__(self, root: str = "agents/events"):
self.root = Path(root)
for state in self.STATES:
(self.root / state).mkdir(parents=True, exist_ok=True)
(self.root / "audit").mkdir(parents=True, exist_ok=True)
self.root = Path(root).absolute()
if self.root.is_symlink():
raise ValueError("symlink event root forbidden")
def publish(
self,
kind: str,
source: str,
target: str,
summary: str,
payload: dict[str, Any],
correlation_id: str | None = None,
self, kind: str, source: str, target: str, summary: str,
payload: dict[str, Any], correlation_id: str | None = None,
tenant_id: str | None = None,
) -> dict[str, Any]:
event_id = "evt_" + sha256(
json.dumps(
{"kind": kind, "source": source, "summary": summary, "payload": payload},
sort_keys=True,
ensure_ascii=False,
).encode("utf-8")
).hexdigest()[:16]
event = AgentEvent(event_id, kind, source, target, summary, payload, correlation_id=correlation_id)
if not all(isinstance(value, str) and value for value in (kind, source, target, summary)):
raise ValueError("event text fields must be nonempty strings")
if not isinstance(payload, dict):
raise ValueError("event payload must be an object")
if tenant_id is not None and (not isinstance(tenant_id, str) or not tenant_id):
raise ValueError("invalid event tenant")
identity = json.dumps(
{"kind": kind, "source": source, "target": target, "summary": summary,
"payload": payload, "correlation_id": correlation_id, "tenant_id": tenant_id},
sort_keys=True, ensure_ascii=False, allow_nan=False, separators=(",", ":"),
)
event_id = "evt_" + sha256(identity.encode("utf-8")).hexdigest()[:16]
safe_segment(event_id)
event = AgentEvent(
event_id, kind, source, target, summary, payload,
tenant_id=tenant_id, correlation_id=correlation_id,
)
self._write("pending", event.to_dict())
self._audit("created", event.to_dict())
return event.to_dict()
def list_events(self, kind: str | None = None, status: str = "pending", limit: int = 100) -> list[dict[str, Any]]:
result = []
for path in sorted((self.root / status).glob("*.json")):
event = self._read(path)
def list_events(
self, kind: str | None = None, status: str = "pending", limit: int = 100,
*, tenant_id: str | None = None, targets: frozenset[str] | None = None,
) -> list[dict[str, Any]]:
self._state(status)
if not isinstance(limit, int) or isinstance(limit, bool) or limit < 0 or limit > 1000:
raise ValueError("invalid event limit")
if limit == 0:
return []
result: list[dict[str, Any]] = []
for name in sorted(self._names(status)):
if not name.endswith(".json"):
continue
event_id = safe_segment(name[:-5])
event = self._read(status, event_id)
if kind and event.get("kind") != kind:
continue
if tenant_id is not None and event.get("tenant_id") != tenant_id:
continue
if targets is not None and event.get("target") not in targets:
continue
result.append(event)
return result[:limit]
if len(result) == limit:
break
return result
def get(self, event_id: str) -> dict[str, Any] | None:
safe_segment(event_id)
for state in self.STATES:
path = self.root / state / f"{event_id}.json"
if path.exists():
return self._read(path)
if f"{event_id}.json" in self._names(state):
return self._read(state, event_id)
return None
def transition(self, event_id: str, status: str, actor: str | None = None) -> dict[str, Any]:
def transition(
self, event_id: str, status: str, actor: str | None = None,
*, expected_tenant_id: str | None = None,
expected_targets: frozenset[str] | None = None,
) -> dict[str, Any]:
safe_segment(event_id)
self._state(status) # validate before any lookup or mutation
event = self.get(event_id)
if not event:
return {"status": "not_found", "id": event_id}
if status not in self.STATES:
return {"status": "invalid_state"}
old = event.get("status", "pending")
old_path = self.root / old / f"{event_id}.json"
event["status"] = status
event["actor"] = actor
event["updated_at"] = utc_now()
if expected_tenant_id is not None and event.get("tenant_id") != expected_tenant_id:
raise ValueError("event tenant mismatch")
if expected_targets is not None and event.get("target") not in expected_targets:
raise ValueError("event target mismatch")
old = event.get("status")
self._state(old)
event.update(status=status, actor=actor, updated_at=utc_now())
self._write(status, event)
if old_path.exists() and old != status:
old_path.unlink()
if old != status:
self._unlink(old, event_id)
self._audit("transition", event, {"from": old, "to": status})
return event
def stats(self) -> dict[str, Any]:
def stats(
self, *, tenant_id: str | None = None, targets: frozenset[str] | None = None,
) -> dict[str, Any]:
if tenant_id is not None:
events = {
state: len(self.list_events(status=state, limit=1000, tenant_id=tenant_id, targets=targets))
for state in self.STATES
}
audit_events = 0
for name in self._names("audit"):
if not name.endswith(".json"):
continue
audit = self._read("audit", safe_segment(name[:-5]))
event = audit.get("event") if isinstance(audit, dict) else None
if (isinstance(event, dict) and event.get("tenant_id") == tenant_id
and (targets is None or event.get("target") in targets)):
audit_events += 1
return {"events": events, "audit_events": audit_events, "status": "healthy"}
return {
"events": {state: len(list((self.root / state).glob("*.json"))) for state in self.STATES},
"audit_events": len(list((self.root / "audit").glob("*.json"))),
"events": {state: sum(name.endswith(".json") for name in self._names(state)) for state in self.STATES},
"audit_events": sum(name.endswith(".json") for name in self._names("audit")),
"status": "healthy",
}
def _write(self, state: str, data: dict[str, Any]):
(self.root / state / f"{data['id']}.json").write_text(
json.dumps(data, ensure_ascii=False, indent=2), encoding="utf-8"
def _state(self, state: Any) -> str:
if state not in self.STATES:
raise ValueError("invalid event state")
return str(state)
def _ensure_root(self) -> None:
if os.name != "posix" or not hasattr(os, "O_NOFOLLOW"):
raise OSError("event storage requires POSIX no-follow support")
self.root.mkdir(parents=True, exist_ok=True)
if self.root.is_symlink() or not self.root.is_dir():
raise ValueError("invalid event root")
def _write(self, state: str, data: dict[str, Any]) -> None:
if state != "audit":
self._state(state)
if not isinstance(data.get("id"), str):
raise ValueError("event id must be a string")
event_id = safe_segment(data["id"])
self._ensure_root()
write_text_no_follow(
self.root, f"{state}/{event_id}.json",
json.dumps(data, ensure_ascii=False, indent=2, allow_nan=False) + "\n",
)
def _read(self, path: Path):
return json.loads(path.read_text(encoding="utf-8"))
def _open_state(self, state: str, *, create: bool = False) -> tuple[int, int] | None:
if state != "audit":
self._state(state)
if not self.root.exists():
if not create:
return None
self._ensure_root()
contained_path(self.root, state)
root_fd = os.open(self.root, self._DIR_FLAGS)
try:
try:
state_fd = os.open(state, self._DIR_FLAGS, dir_fd=root_fd)
except FileNotFoundError:
if not create:
os.close(root_fd)
return None
os.mkdir(state, mode=0o750, dir_fd=root_fd)
state_fd = os.open(state, self._DIR_FLAGS, dir_fd=root_fd)
return root_fd, state_fd
except Exception:
try:
os.close(root_fd)
except OSError:
pass
raise
def _audit(self, action: str, event: dict[str, Any], extra: dict[str, Any] | None = None):
data = {"action": action, "event": event, "extra": extra or {}, "time": utc_now()}
(self.root / "audit" / f"{event['id']}-{action}.json").write_text(
json.dumps(data, ensure_ascii=False, indent=2), encoding="utf-8"
)
def _names(self, state: str) -> set[str]:
if os.name != "posix" or not hasattr(os, "O_NOFOLLOW"):
if not self.root.exists():
return set()
raise OSError("event storage requires POSIX no-follow support")
opened = self._open_state(state)
if opened is None:
return set()
root_fd, state_fd = opened
try:
return set(os.listdir(state_fd))
finally:
os.close(state_fd); os.close(root_fd)
def _read(self, state: str, event_id: str) -> dict[str, Any]:
safe_segment(event_id)
opened = self._open_state(state)
if opened is None:
raise FileNotFoundError(event_id)
root_fd, state_fd = opened
try:
fd = os.open(f"{event_id}.json", os.O_RDONLY | os.O_NOFOLLOW, dir_fd=state_fd)
info = os.fstat(fd)
if not stat.S_ISREG(info.st_mode) or info.st_nlink != 1:
os.close(fd)
raise ValueError("unsafe event file")
with os.fdopen(fd, "r", encoding="utf-8") as stream:
value = json.load(stream)
if (not isinstance(value, dict) or value.get("id") != event_id
or (state != "audit" and value.get("status") != state)):
raise ValueError("invalid event record")
return value
finally:
os.close(state_fd); os.close(root_fd)
def _unlink(self, state: str, event_id: str) -> None:
opened = self._open_state(state)
if opened is None:
return
root_fd, state_fd = opened
try:
info = os.stat(f"{event_id}.json", dir_fd=state_fd, follow_symlinks=False)
if not stat.S_ISREG(info.st_mode) or info.st_nlink != 1:
raise ValueError("unsafe event file")
os.unlink(f"{event_id}.json", dir_fd=state_fd)
os.fsync(state_fd)
finally:
os.close(state_fd); os.close(root_fd)
def _audit(self, action: str, event: dict[str, Any], extra: dict[str, Any] | None = None) -> None:
safe_segment(action)
audit = {"id": f"{safe_segment(event['id'])}-{action}", "action": action,
"event": event, "extra": extra or {}, "time": utc_now()}
self._write("audit", audit)
__all__ = ["AgentEvent", "AgentEventBus"]
+151 -52
View File
@@ -1,77 +1,178 @@
"""API for agent orchestration observability."""
"""Authenticated API for agent orchestration observability."""
from __future__ import annotations
from collections.abc import Callable, Mapping
from typing import Any
try:
from fastapi import APIRouter
except Exception:
APIRouter = None
from fastapi import APIRouter, HTTPException
except Exception: # pragma: no cover
APIRouter = HTTPException = None
from .agent_observability import AgentExecutionRecord
from .agent_observability_dashboard import AgentObservabilityDashboard
from .agent_observability_store import AgentObservabilityStore
store = AgentObservabilityStore()
dashboard = AgentObservabilityDashboard(store.load())
_ROLE_WHITELIST = {"user", "group_admin", "owner"}
_authenticator = None # callable() -> {"user_id":..., "role":...} | None, injected by host
store: AgentObservabilityStore | None = None
dashboard = AgentObservabilityDashboard()
_ROLE_WHITELIST = frozenset({"user", "group_admin", "owner"})
_RECORD_FIELDS = frozenset(AgentExecutionRecord.__dataclass_fields__)
_authenticator: Callable[[], Mapping[str, Any] | None] | None = None
def set_authenticator(fn):
"""Install a server-side authenticator used to derive the querying tenant.
class ApiSecurityError(PermissionError):
def __init__(self, status_code: int, code: str):
super().__init__(code)
self.status_code, self.code = status_code, code
The callable returns the authenticated principal (dict with "user_id" and
a whitelisted "role") or None for anonymous. The client-supplied role and
user/group ids are NOT trusted to escalate scope.
"""
def _storage() -> AgentObservabilityStore:
global store
if store is None:
store = AgentObservabilityStore()
return store
def set_authenticator(fn: Callable[[], Mapping[str, Any] | None] | None) -> None:
"""Install the host authenticator; its result is fetched for every call."""
if fn is not None and not callable(fn):
raise TypeError("authenticator must be callable or None")
global _authenticator
_authenticator = fn
def _principal():
if _authenticator is not None:
p = _authenticator()
if isinstance(p, dict) and p.get("role") in _ROLE_WHITELIST:
return p
return None
def _group_id(principal: Mapping[str, Any]) -> str | None:
value = principal.get("group_id")
return value.strip() if isinstance(value, str) and value.strip() else None
def record_agent_execution(payload: dict[str, Any]):
record = AgentExecutionRecord(**payload)
return {"status": "ok", "record": store.append(record)}
def authenticated_principal() -> dict[str, Any]:
"""Return a request-scoped copy of the server principal, or fail closed."""
if _authenticator is None:
raise ApiSecurityError(401, "AUTHENTICATION_REQUIRED")
try:
raw = _authenticator()
except ApiSecurityError:
raise
except Exception as exc:
raise ApiSecurityError(401, "AUTHENTICATION_FAILED") from exc
if not isinstance(raw, Mapping):
raise ApiSecurityError(401, "AUTHENTICATION_REQUIRED")
principal = dict(raw)
user_id, role = principal.get("user_id"), principal.get("role")
if not isinstance(user_id, str) or not user_id.strip() or role not in _ROLE_WHITELIST:
raise ApiSecurityError(403, "INVALID_AUTHENTICATED_PRINCIPAL")
principal["user_id"] = user_id.strip()
if principal.get("group_id") is not None and _group_id(principal) is None:
raise ApiSecurityError(403, "INVALID_AUTHENTICATED_PRINCIPAL")
if role == "group_admin" and not _group_id(principal):
raise ApiSecurityError(403, "GROUP_SCOPE_REQUIRED")
return principal
def get_agent_observability(user_id: str | None = None, group_id: str | None = None, role: str = "user"):
# The authenticated principal (tenant) is authoritative; the request params
# role/user_id/group_id can never choose owner or another tenant.
principal = _principal()
pr_user = principal.get("user_id") if principal else None
pr_role = principal.get("role", "user") if principal else "user"
def _scope_set(principal: Mapping[str, Any], name: str) -> frozenset[str]:
raw = principal.get(name, ())
if not isinstance(raw, (list, tuple, set, frozenset)) or any(
not isinstance(item, str) or not item for item in raw
):
raise ApiSecurityError(403, "INVALID_AUTHENTICATED_SCOPE")
return frozenset(raw)
if pr_role == "owner":
# Server-verified owner may browse a requested scope.
q_user, q_group, q_role = user_id, group_id, "owner"
summary_scope = None
elif pr_user:
# Authenticated non-owner: strictly scoped to own tenant.
q_user, q_group, q_role = pr_user, None, "user"
summary_scope = pr_user
else:
# Anonymous: nothing scoped to them.
q_user, q_group, q_role = None, None, "user"
summary_scope = None
records = store.query(q_user, q_group, q_role)
def _owner_user(principal: Mapping[str, Any], requested: str | None) -> str:
own, target = str(principal["user_id"]), requested or str(principal["user_id"])
if target != own and target not in _scope_set(principal, "allowed_user_ids"):
raise ApiSecurityError(403, "OWNER_USER_SCOPE_DENIED")
return target
def _owner_group(principal: Mapping[str, Any], requested: str | None) -> str | None:
if requested is None:
return None
if requested != _group_id(principal) and requested not in _scope_set(principal, "allowed_group_ids"):
raise ApiSecurityError(403, "OWNER_GROUP_SCOPE_DENIED")
return requested
def _summary(records: list[dict[str, Any]]) -> dict[str, Any]:
count = len(records)
hermes = sum(1 for row in records if row.get("used_hermes"))
return {
"status": "ok",
"records": records,
"summary": store.load().summary(summary_scope),
"total_executions": count,
"openclaw_independent_loops": count - hermes,
"hermes_orchestrated_loops": hermes,
"average_duration_ms": (
sum(int(row.get("duration_ms") or 0) for row in records) / count if count else 0
),
"token_usage": {
"input": sum(int(row.get("input_tokens") or 0) for row in records),
"output": sum(int(row.get("output_tokens") or 0) for row in records),
"total": sum(int(row.get("total_tokens") or 0) for row in records),
},
}
def record_agent_execution(payload: Mapping[str, Any]):
principal = authenticated_principal()
if set(payload) - _RECORD_FIELDS:
raise ApiSecurityError(403, "EXECUTION_FIELDS_FORBIDDEN")
normalized, role = dict(payload), principal["role"]
if role == "owner":
requested_user = payload.get("user_id")
if requested_user is not None and not isinstance(requested_user, str):
raise ApiSecurityError(403, "INVALID_EXECUTION_SCOPE")
target_user = _owner_user(principal, requested_user)
requested_scope = payload.get("visible_scope")
if requested_scope is not None and not isinstance(requested_scope, str):
raise ApiSecurityError(403, "INVALID_EXECUTION_SCOPE")
allowed_group = _owner_group(principal, requested_scope) if requested_scope and requested_scope != target_user else None
normalized["user_id"], normalized["visible_scope"] = target_user, allowed_group or target_user
else:
normalized["user_id"] = principal["user_id"]
normalized["visible_scope"] = _group_id(principal) or principal["user_id"]
try:
record = AgentExecutionRecord(**normalized)
except (TypeError, ValueError) as exc:
raise ApiSecurityError(403, "INVALID_EXECUTION_RECORD") from exc
return {"status": "ok", "record": _storage().append(record)}
def get_agent_observability(
user_id: str | None = None, group_id: str | None = None,
role: str = "user",
):
del role # wire compatibility only; server principal is authoritative
principal, pr_role = authenticated_principal(), None
pr_role = principal["role"]
if pr_role == "user":
if user_id not in (None, principal["user_id"]) or group_id is not None:
raise ApiSecurityError(403, "TENANT_SCOPE_DENIED")
q_user, q_group, q_role = principal["user_id"], None, "user"
elif pr_role == "group_admin":
own_group = _group_id(principal)
if group_id not in (None, own_group) or user_id not in (None, principal["user_id"]):
raise ApiSecurityError(403, "TENANT_SCOPE_DENIED")
q_user, q_group, q_role = principal["user_id"], own_group, "group_admin"
elif user_id is None and group_id is None:
q_user, q_group, q_role = principal["user_id"], None, "user"
elif group_id is not None:
q_user = _owner_user(principal, user_id) if user_id is not None else principal["user_id"]
q_group, q_role = _owner_group(principal, group_id), "group_admin"
else:
q_user, q_group, q_role = _owner_user(principal, user_id), None, "user"
records = _storage().query(q_user, q_group, q_role)
return {"status": "ok", "records": records, "summary": _summary(records)}
def _http(call: Callable[[], Any]) -> Any:
try:
return call()
except ApiSecurityError as exc:
if HTTPException is None:
raise
raise HTTPException(status_code=exc.status_code, detail=exc.code) from exc
def build_router():
if APIRouter is None:
return None
@@ -79,15 +180,13 @@ def build_router():
@router.post("/execution", operation_id="recordAgentExecution")
def record(payload: dict[str, Any]):
return record_agent_execution(payload)
return _http(lambda: record_agent_execution(payload))
@router.get("/overview", operation_id="getAgentObservabilityOverview")
def overview(user_id: str | None = None, group_id: str | None = None, role: str = "user"):
return get_agent_observability(user_id, group_id, role)
return _http(lambda: get_agent_observability(user_id, group_id, role))
return router
router = build_router()
__all__ = ["router", "record_agent_execution", "get_agent_observability"]
__all__ = ["ApiSecurityError", "authenticated_principal", "set_authenticator", "router", "record_agent_execution", "get_agent_observability"]
+122 -11
View File
@@ -21,7 +21,11 @@ import sqlite3
from typing import Any, Callable, Iterable, Mapping
import uuid
from aios_capability_contract import has_capabilities
from aios_capability_contract import (
CAPABILITY_VOCABULARY_DIGEST,
has_capabilities,
normalize_required_capabilities,
)
TERMINAL_STATES = frozenset({"SUCCEEDED", "FAILED", "EXPIRED", "CANCELLED"})
@@ -148,6 +152,7 @@ class TaskStore:
agent_type TEXT NOT NULL,
agent_version TEXT NOT NULL,
capabilities_json TEXT NOT NULL,
capability_vocabulary_digest TEXT,
metadata_json TEXT NOT NULL,
token_hash TEXT NOT NULL UNIQUE,
status TEXT NOT NULL,
@@ -161,6 +166,7 @@ class TaskStore:
instructions TEXT NOT NULL,
success_criteria_json TEXT NOT NULL,
required_capabilities_json TEXT NOT NULL,
capability_vocabulary_digest TEXT,
constraints_json TEXT NOT NULL,
metadata_json TEXT NOT NULL,
deadline TEXT,
@@ -209,6 +215,59 @@ class TaskStore:
ON events(task_id, event_id);
"""
)
self._ensure_additive_column(
connection, "agents", "capability_vocabulary_digest", "TEXT"
)
self._ensure_additive_column(
connection, "tasks", "capability_vocabulary_digest", "TEXT"
)
@staticmethod
def _ensure_additive_column(
connection: sqlite3.Connection, table: str, column: str, definition: str
) -> None:
"""Add C6 handshake storage without rewriting historical task rows."""
columns = {
str(row["name"])
for row in connection.execute(f"PRAGMA table_info({table})").fetchall()
}
if column not in columns:
connection.execute(f"ALTER TABLE {table} ADD COLUMN {column} {definition}")
@staticmethod
def _normalize_capability_vocabulary_digest(value: object | None) -> str | None:
"""Validate an opt-in formal vocabulary handshake.
An omitted digest is the explicit C5-compatible legacy mode. A
supplied digest has no downgrade path: it must name this exact formal
vocabulary before any durable state is changed.
"""
if value is None:
return None
if not isinstance(value, str) or value.strip().lower() != CAPABILITY_VOCABULARY_DIGEST:
raise TaskProtocolError("CAPABILITY_VOCABULARY_MISMATCH", 409)
return CAPABILITY_VOCABULARY_DIGEST
@staticmethod
def _negotiated_capability_vocabulary_matches(
*,
agent_digest: str | None,
task_digest: str | None,
requested_digest: str | None,
) -> bool:
"""Match only an explicitly negotiated formal task vocabulary.
Historical agents and tasks carry ``NULL`` and keep their C5 behavior.
A task admitted with a digest is never offered to or claimed by an
agent that did not register the same current vocabulary.
"""
if task_digest is None:
return True
return (
task_digest == CAPABILITY_VOCABULARY_DIGEST
and agent_digest == task_digest
and (requested_digest is None or requested_digest == task_digest)
)
def _token_hash(self, token: str) -> str:
return hashlib.sha256((self.token_pepper + "\0" + token).encode("utf-8")).hexdigest()
@@ -221,6 +280,7 @@ class TaskStore:
"instructions": row["instructions"],
"success_criteria": json.loads(row["success_criteria_json"]),
"required_capabilities": json.loads(row["required_capabilities_json"]),
"capability_vocabulary_digest": row["capability_vocabulary_digest"],
"constraints": json.loads(row["constraints_json"]),
"metadata": json.loads(row["metadata_json"]),
"deadline": row["deadline"],
@@ -264,6 +324,7 @@ class TaskStore:
agent_version: str,
capabilities: Iterable[object],
metadata: Mapping[str, Any] | None = None,
capability_vocabulary_digest: str | None = None,
) -> dict[str, Any]:
agent_id = agent_id.strip()
agent_type = agent_type.strip()
@@ -272,6 +333,9 @@ class TaskStore:
raise TaskProtocolError("AGENT_IDENTITY_INVALID", 400)
caps = _string_list(capabilities, "capabilities")
meta = _json_object(metadata, "metadata")
vocabulary_digest = self._normalize_capability_vocabulary_digest(
capability_vocabulary_digest
)
token = "agt_" + secrets.token_urlsafe(32)
now = iso(self.now())
try:
@@ -279,13 +343,14 @@ class TaskStore:
connection.execute("BEGIN IMMEDIATE")
connection.execute(
"INSERT INTO agents(agent_id,agent_type,agent_version,capabilities_json,"
"metadata_json,token_hash,status,created_at,last_seen_at)"
" VALUES(?,?,?,?,?,?,?,?,?)",
"capability_vocabulary_digest,metadata_json,token_hash,status,created_at,last_seen_at)"
" VALUES(?,?,?,?,?,?,?,?,?,?)",
(
agent_id,
agent_type,
agent_version,
canonical_json(caps),
vocabulary_digest,
canonical_json(meta),
self._token_hash(token),
"ACTIVE",
@@ -307,6 +372,7 @@ class TaskStore:
"agent_type": agent_type,
"agent_version": agent_version,
"capabilities": caps,
"capability_vocabulary_digest": vocabulary_digest,
"agent_token": token,
"credential_returned_once": True,
}
@@ -331,6 +397,7 @@ class TaskStore:
"agent_type": row["agent_type"],
"agent_version": row["agent_version"],
"capabilities": json.loads(row["capabilities_json"]),
"capability_vocabulary_digest": row["capability_vocabulary_digest"],
"metadata": json.loads(row["metadata_json"]),
}
@@ -375,6 +442,7 @@ class TaskStore:
instructions: str,
success_criteria: object | None,
required_capabilities: Iterable[object] | None = None,
capability_vocabulary_digest: str | None = None,
constraints: Mapping[str, Any] | None = None,
metadata: Mapping[str, Any] | None = None,
deadline: str | None = None,
@@ -399,6 +467,12 @@ class TaskStore:
raise TaskProtocolError("DEADLINE_INVALID", 400)
criteria = _criteria(success_criteria)
required = _string_list(required_capabilities, "required_capabilities")
normalized_required = normalize_required_capabilities(required)
if any(value.startswith("exact:") for value in normalized_required):
raise TaskProtocolError("TASK_CAPABILITY_UNKNOWN", 403)
vocabulary_digest = self._normalize_capability_vocabulary_digest(
capability_vocabulary_digest
)
constraints_value = _json_object(constraints, "constraints")
metadata_value = _json_object(metadata, "metadata")
payload = {
@@ -407,6 +481,7 @@ class TaskStore:
"instructions": instructions,
"success_criteria": criteria,
"required_capabilities": required,
"capability_vocabulary_digest": vocabulary_digest,
"constraints": constraints_value,
"metadata": metadata_value,
"deadline": iso(parsed_deadline) if parsed_deadline else None,
@@ -429,15 +504,16 @@ class TaskStore:
try:
connection.execute(
"INSERT INTO tasks(task_id,goal,instructions,success_criteria_json,"
"required_capabilities_json,constraints_json,metadata_json,deadline,priority,"
"required_capabilities_json,capability_vocabulary_digest,constraints_json,metadata_json,deadline,priority,"
"max_attempts,attempt,status,revision,created_at,updated_at)"
" VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)",
" VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)",
(
task_id,
goal,
instructions,
canonical_json(criteria),
canonical_json(required),
vocabulary_digest,
canonical_json(constraints_value),
canonical_json(metadata_value),
payload["deadline"],
@@ -517,18 +593,28 @@ class TaskStore:
connection.commit()
return changed
def poll_tasks(self, *, agent_id: str, limit: int = 20) -> list[dict[str, Any]]:
def poll_tasks(
self,
*,
agent_id: str,
limit: int = 20,
capability_vocabulary_digest: str | None = None,
) -> list[dict[str, Any]]:
if not 1 <= int(limit) <= 100:
raise TaskProtocolError("LIMIT_INVALID", 400)
requested_digest = self._normalize_capability_vocabulary_digest(
capability_vocabulary_digest
)
with self._connect() as connection:
connection.execute("BEGIN IMMEDIATE")
self._expire_locked(connection)
agent = connection.execute(
"SELECT capabilities_json FROM agents WHERE agent_id=? AND status='ACTIVE'",
"SELECT capabilities_json,capability_vocabulary_digest FROM agents "
"WHERE agent_id=? AND status='ACTIVE'",
(agent_id,),
).fetchone()
if agent is None:
raise TaskProtocolError("AGENT_NOT_FOUND", 404)
self._expire_locked(connection)
capabilities = json.loads(agent["capabilities_json"])
rows = connection.execute(
"SELECT * FROM tasks WHERE status='QUEUED' ORDER BY priority DESC,created_at ASC"
@@ -536,7 +622,14 @@ class TaskStore:
result = []
for row in rows:
required = json.loads(row["required_capabilities_json"])
if has_capabilities(capabilities, required):
if (
self._negotiated_capability_vocabulary_matches(
agent_digest=agent["capability_vocabulary_digest"],
task_digest=row["capability_vocabulary_digest"],
requested_digest=requested_digest,
)
and has_capabilities(capabilities, required)
):
result.append(self._row_task(row, include_result=False))
if len(result) >= int(limit):
break
@@ -558,19 +651,37 @@ class TaskStore:
task_id: str,
agent_id: str,
lease_seconds: int = 300,
capability_vocabulary_digest: str | None = None,
) -> dict[str, Any]:
if not isinstance(lease_seconds, int) or isinstance(lease_seconds, bool) or not 30 <= lease_seconds <= 3600:
raise TaskProtocolError("LEASE_SECONDS_INVALID", 400)
requested_digest = self._normalize_capability_vocabulary_digest(
capability_vocabulary_digest
)
lease_token = "lease_" + secrets.token_urlsafe(32)
with self._connect() as connection:
connection.execute("BEGIN IMMEDIATE")
self._expire_locked(connection)
row = connection.execute("SELECT * FROM tasks WHERE task_id=?", (task_id,)).fetchone()
if row is None:
raise TaskProtocolError("TASK_NOT_FOUND", 404)
agent = connection.execute(
"SELECT capabilities_json,capability_vocabulary_digest FROM agents "
"WHERE agent_id=? AND status='ACTIVE'",
(agent_id,),
).fetchone()
if agent is None:
raise TaskProtocolError("AGENT_NOT_FOUND", 404)
if not self._negotiated_capability_vocabulary_matches(
agent_digest=agent["capability_vocabulary_digest"],
task_digest=row["capability_vocabulary_digest"],
requested_digest=requested_digest,
):
raise TaskProtocolError("CAPABILITY_VOCABULARY_MISMATCH", 409)
self._expire_locked(connection)
row = connection.execute("SELECT * FROM tasks WHERE task_id=?", (task_id,)).fetchone()
if row["status"] != "QUEUED":
raise TaskProtocolError("TASK_NOT_CLAIMABLE", 409)
capabilities = self._agent_capabilities(connection, agent_id)
capabilities = set(json.loads(agent["capabilities_json"]))
required = json.loads(row["required_capabilities_json"])
if not has_capabilities(capabilities, required):
raise TaskProtocolError("AGENT_CAPABILITY_MISMATCH", 403)
+36 -7
View File
@@ -6,7 +6,7 @@ The built-in host adapter is READ_ONLY. Mutating operations require separately
registered governed adapters with receiver fencing/readback, never arbitrary argv.
"""
from __future__ import annotations
from dataclasses import dataclass
from dataclasses import dataclass, replace
from contextlib import contextmanager
from datetime import datetime,timezone
import base64
@@ -18,7 +18,7 @@ import time
from pathlib import Path
from typing import Mapping
from task_plane.models import TaskPlan,WorkerRegistration,ProbeEvidence,ProbeOutcome,Budget
from task_plane.models import TaskPlan,WorkerRegistration,ProbeEvidence,ProbeOutcome,Budget,EvidenceEnvelope
from task_plane.errors import TaskPlaneHTTPError
from aios_gitops_job_controller import GitOpsJobController,JobStore,JobState,JobError
from .evidence import ReceiptSigner,canonical,digest
@@ -69,6 +69,7 @@ class BaselineService:
self.store.conn.execute('''CREATE TABLE IF NOT EXISTS baseline_worker_policy(
worker_id TEXT PRIMARY KEY,policy_json TEXT NOT NULL)''')
self.store.probe_attestation_verifier=self._verify_probe
self.store.evidence_attestation_verifier=self._verify_task_evidence
def _user(self,identity):
if not isinstance(identity,Identity) or identity.role not in {'user','owner'}:
@@ -379,11 +380,39 @@ class BaselineService:
def _close_task(self,task,claim,envelope):
if not self._receipt_valid(task,envelope): raise ServiceError('RESULT_ATTESTATION_INVALID',403)
current=self.store.get_task(task.task_id)
if current.status=='CANCELLED': raise ServiceError('TASK_CANCELLED')
if current.status=='CLAIMED': self.store.update_task_status(task.task_id,current.revision,'RUNNING')
self.store.complete_claim(task.task_id,claim.claim_id,'COMPLETED',envelope,
evidence_entries=[('diagnostic',envelope,claim.worker_identity)])
with self.store._transaction():
current=self.store.get_task(task.task_id)
if current.status=='CANCELLED': raise ServiceError('TASK_CANCELLED')
if current.status=='CLAIMED': self.store.update_task_status(task.task_id,current.revision,'RUNNING')
evidence=EvidenceEnvelope.create(task_id=task.task_id,claim_id=claim.claim_id,
kind='diagnostic',worker_identity=claim.worker_identity,payload=envelope,
previous_evidence_hash=self.store._last_evidence_hash(task.task_id),
attestation_ref='baseline-machine:'+self.signer.key_id)
proof=canonical({'domain':'aios.baseline.task-evidence.v1',
'key_id':self.signer.key_id,'evidence':evidence.digest_payload()})
evidence=replace(evidence,signature=base64.b64encode(self.signer.private_key.sign(proof)).decode())
self.store.complete_claim_with_evidence_and_event(task.task_id,claim.claim_id,
'COMPLETED',envelope,[evidence])
def _verify_task_evidence(self,evidence):
"""Verify the TaskPlane wrapper and its separately signed operation receipt."""
try:
if not isinstance(evidence,EvidenceEnvelope) or evidence.kind!='diagnostic': return False
prefix='baseline-machine:'
if not evidence.attestation_ref or not evidence.attestation_ref.startswith(prefix): return False
key_id=evidence.attestation_ref[len(prefix):]
key=self.signer.trusted_public_keys.get(key_id)
if key is None or not evidence.signature: return False
proof=canonical({'domain':'aios.baseline.task-evidence.v1',
'key_id':key_id,'evidence':evidence.digest_payload()})
key.verify(base64.b64decode(evidence.signature,validate=True),proof)
claim=self.store.get_claim(evidence.claim_id)
task=self.store.get_task(evidence.task_id)
if claim.task_id!=task.task_id or claim.worker_identity!=evidence.worker_identity: return False
if claim.operation_digest!=task.task_plan.operation_digest(): return False
return self._receipt_valid(task,evidence.payload)
except Exception:
return False
def cancel_task(self,identity,task_id):
self._user(identity)
@@ -0,0 +1,174 @@
"""Canonical fail-closed capability semantics for formal AgentTask eligibility."""
from __future__ import annotations
from collections.abc import Iterable
import hashlib
import json
# These aliases are the verified C4 contract captured in
# ``qa/capability-contract.txt``. Unknown task names become exact requirements;
# unknown worker names are never granted.
_TASK_ALIASES: dict[str, frozenset[str]] = {
"release:candidate": frozenset({"release:candidate"}),
"repository_read": frozenset({"repo.read"}),
"git.read": frozenset({"repo.read"}),
"repo.read": frozenset({"repo.read"}),
"test_execution": frozenset({"test.execute"}),
"git.test": frozenset({"test.execute"}),
"test.execute": frozenset({"test.execute"}),
"governed_git_write": frozenset({"governed.git.write.branch", "governed.git.write.file"}),
"governed.git.write": frozenset({"governed.git.write.branch", "governed.git.write.file"}),
"governed.write.branch": frozenset({"governed.git.write.branch"}),
"governed.write.file": frozenset({"governed.git.write.file"}),
"shell": frozenset({"runtime.shell"}),
"git": frozenset({"runtime.git"}),
"python": frozenset({"runtime.python"}),
"code.read": frozenset({"legacy.code.read"}),
"code.review": frozenset({"legacy.code.review"}),
"code.write": frozenset({"legacy.code.write"}),
"code.write.patch": frozenset({"legacy.code.write.patch"}),
"test.python": frozenset({"legacy.test.python"}),
"test.shell.allowlisted": frozenset({"legacy.test.shell.allowlisted"}),
"git.commit": frozenset({"legacy.git.commit"}),
"git.diff.read": frozenset({"legacy.git.diff.read"}),
"git.force_push": frozenset({"legacy.git.force_push"}),
"deploy.production": frozenset({"legacy.deploy.production"}),
"secret.transform": frozenset({"legacy.secret.transform"}),
"secure_file_transformer": frozenset({"legacy.secure_file_transformer"}),
"runtime.shadow": frozenset({"legacy.runtime.shadow"}),
"review.security": frozenset({"legacy.review.security"}),
"review.api-contract": frozenset({"legacy.review.api-contract"}),
"gpu.train": frozenset({"legacy.gpu.train"}),
}
_WORKER_ALIASES = {
key: value for key, value in _TASK_ALIASES.items()
if key not in {"release:candidate", "governed_git_write", "governed.git.write"}
}
_WORKER_ALIASES.update({
"release:candidate": frozenset({"release:candidate"}),
"governed.git.write.branch": frozenset({"governed.git.write.branch"}),
"governed.git.write.file": frozenset({"governed.git.write.file"}),
})
_EXACT_LIFECYCLE_CAPABILITIES = frozenset(
{
"agent-task:control",
"governed.task.create",
"governed.write.files",
"production.plan.apply",
"production.promote",
"production.rollback",
"task.principal.issue",
"testing",
"verification",
"writer.lease",
}
)
for _capability in _EXACT_LIFECYCLE_CAPABILITIES:
_TASK_ALIASES[_capability] = frozenset({_capability})
_WORKER_ALIASES[_capability] = frozenset({_capability})
_UNKNOWN_PREFIX = "exact:"
CAPABILITY_VOCABULARY_SCHEMA = "aios.agent-task.capability-vocabulary.v1"
def capability_vocabulary_descriptor() -> dict[str, object]:
"""Return the immutable inputs to the formal capability vocabulary digest.
The generic Task Plane has deliberately different wildcard semantics. This
descriptor names only the formal AgentTask vocabulary used in this module.
"""
return {
"schema": CAPABILITY_VOCABULARY_SCHEMA,
"task_aliases": {
key: sorted(value) for key, value in sorted(_TASK_ALIASES.items())
},
"worker_aliases": {
key: sorted(value) for key, value in sorted(_WORKER_ALIASES.items())
},
"unknown_required_prefix": _UNKNOWN_PREFIX,
"unknown_required": "exact-and-deny",
"unknown_worker": "never-granted",
"wildcard": "formal-deny",
}
def capability_vocabulary_digest() -> str:
"""Return a deterministic digest for the formal capability vocabulary."""
encoded = json.dumps(
capability_vocabulary_descriptor(),
ensure_ascii=False,
sort_keys=True,
separators=(",", ":"),
allow_nan=False,
).encode("utf-8")
return hashlib.sha256(encoded).hexdigest()
# A constant lets registration, admission, poll, and claim compare one exact
# formal vocabulary without importing or interpreting the generic Task Plane.
CAPABILITY_VOCABULARY_DIGEST = capability_vocabulary_digest()
def _clean(value: object) -> str:
if not isinstance(value, str):
raise ValueError("capability must be a string")
text = value.strip().lower()
if not text:
raise ValueError("capability must not be empty")
return text
def normalize_required_capabilities(values: Iterable[object]) -> frozenset[str]:
result: set[str] = set()
for value in values:
text = _clean(value)
if "*" in text:
result.add(_UNKNOWN_PREFIX + text)
else:
result.update(_TASK_ALIASES.get(text, frozenset({_UNKNOWN_PREFIX + text})))
return frozenset(result)
def normalize_worker_capabilities(values: Iterable[object]) -> frozenset[str]:
result: set[str] = set()
for value in values:
text = _clean(value)
if "*" in text:
continue
mapped = _WORKER_ALIASES.get(text)
if mapped is not None:
result.update(mapped)
if {"governed.git.write.branch", "governed.git.write.file"}.issubset(result):
result.add("governed.git.write")
return frozenset(result)
def has_capabilities(worker_capabilities: Iterable[object], required_capabilities: Iterable[object]) -> bool:
required = normalize_required_capabilities(required_capabilities)
effective = normalize_worker_capabilities(worker_capabilities)
if any(value.startswith(_UNKNOWN_PREFIX) for value in required):
return False
return required.issubset(effective)
def required_capabilities_subset(granted_requirements: Iterable[object], required_requirements: Iterable[object]) -> bool:
granted = normalize_required_capabilities(granted_requirements)
required = normalize_required_capabilities(required_requirements)
if any(value.startswith(_UNKNOWN_PREFIX) for value in granted | required):
return False
return required.issubset(granted)
def normalized_capability_matrix(worker_capabilities: Iterable[object], required_capabilities: Iterable[object]) -> dict[str, object]:
worker_values = tuple(worker_capabilities)
required_values = tuple(required_capabilities)
worker = normalize_worker_capabilities(worker_values)
required = normalize_required_capabilities(required_values)
return {
"worker_effective": sorted(worker),
"task_required": sorted(required),
"eligible": has_capabilities(worker_values, required_values),
}
+17 -6
View File
@@ -158,7 +158,10 @@ def reject_non_read_http(handler, method: str, path: str) -> bool:
def now() -> str: return time.strftime('%Y-%m-%dT%H:%M:%SZ', time.gmtime())
def uid(p='id') -> str: return f'{p}-{int(time.time()*1000)}-{uuid.uuid4().hex[:8]}'
def safe(s: str, n=160) -> str: return re.sub(r'[^A-Za-z0-9_./@-]+', '-', str(s or 'unknown')).strip('/-')[:n] or 'unknown'
from security_path_guards import safe_segment, relative_parts, write_text_no_follow
def safe(s: str, n=160) -> str:
return safe_segment(s, max_length=n)
def sh(args, check=False):
p = subprocess.run(['git'] + args, cwd=str(WS), text=True, capture_output=True)
if check and p.returncode != 0: raise RuntimeError(p.stderr or p.stdout or 'git failed')
@@ -206,7 +209,10 @@ def relpath(path: str) -> str:
if not x or x.startswith('/') or '..' in Path(x).parts or Path(x).parts[0] == '.git': raise ValueError('invalid path')
return str(Path(x))
def branch_name(name: str | None, prefix='patch/chatgpt') -> str:
b = safe(name or f'{prefix}-{uid("br")}', 180)
b = name or f'{prefix}-{uid("br")}'
if len(b) > 180: raise ValueError('branch name too long')
relative_parts(b)
if '..' in b or '@{' in b or b.endswith('.lock'): raise ValueError('invalid branch name')
if b in ('main','master') or b.startswith('main/') or b.startswith('master/'): raise ValueError('direct main/master writes are forbidden')
return b
def _json_has_plain_secret(obj: Any) -> bool:
@@ -254,8 +260,12 @@ def token_records():
else:
out.append({'account':'legacy-token','role':'owner','scopes':['*'],'token_plain':line})
return out
from security_path_guards import require_isolated_dev_profile
def authenticate(token: str):
if os.getenv('AIOS_GITOPS_DEV_ALLOW','0') == '1': return {'account':'dev-owner','role':'owner','scopes':['*']}
if os.getenv('AIOS_GITOPS_DEV_ALLOW','0') == '1':
require_isolated_dev_profile(HOST)
return {'account':'dev-owner','role':'owner','scopes':['*']}
if not token: return None
digest=hashlib.sha256(token.encode()).hexdigest()
for r in token_records():
@@ -284,7 +294,7 @@ def checkout(branch, base='main', create=False):
return b
def write_file(path, content):
if content_has_secret(content): raise ValueError('secret_rejected: use secret_ref; plaintext API keys must not enter Git')
r=relpath(path); t=WS/r; t.parent.mkdir(parents=True, exist_ok=True); t.write_text(content, encoding='utf-8'); return r
r=relpath(path); write_text_no_follow(WS, str(r).replace('\\','/'), content); return r
def write_json(path, data): return write_file(path, json.dumps(data, ensure_ascii=False, indent=2) + '\n')
def sync_branch(branch: str) -> dict:
if not branch or branch in ('main','master') or branch.startswith('main/') or branch.startswith('master/'):
@@ -835,7 +845,7 @@ class H(BaseHTTPRequestHandler):
a=self.need('branch:create');
if not a: return
b=checkout(data.get('branch'), data.get('base','main'), True); meta={'type':'branch','branch':b,'base':data.get('base','main'),'created_by':a['account'],'created_at':now(),'direct_main_push':False,'owner_approval_required':True}
r=write_json(f'patches/{safe(b).replace("/","_")}.branch.json', meta); c=commit([r], data.get('message') or f'chore(chatgpt): create {b}'); audit(a['account'],'branch',b,meta); self.sendj({'status':'ok','branch':b,'commit':c})
r=write_json(f'patches/{safe(b.replace("/","_"))}.branch.json', meta); c=commit([r], data.get('message') or f'chore(chatgpt): create {b}'); audit(a['account'],'branch',b,meta); self.sendj({'status':'ok','branch':b,'commit':c})
def file(self,data):
a=self.need('file:write');
if not a: return
@@ -942,7 +952,7 @@ class H(BaseHTTPRequestHandler):
if not name or not base: return self.sendj({'error':'provider_name/base_url/model_name required'},400)
owner=safe(data.get('username') or a['account']); secret_ref=data.get('api_key_secret_ref') or data.get('secret_ref')
if data.get('api_key'):
d=SECRETS/'llm'/owner; d.mkdir(parents=True, exist_ok=True); sid=uid('llmkey'); p=d/f'{sid}.secret'; p.write_text(str(data['api_key']), encoding='utf-8'); os.chmod(p,0o600); secret_ref=f'secret://llm/{owner}/{sid}'
sid=uid('llmkey'); write_text_no_follow(SECRETS, f'llm/{owner}/{sid}.secret', str(data['api_key'])); secret_ref=f'secret://llm/{owner}/{sid}'
if not secret_ref: return self.sendj({'error':'api_key_secret_ref or api_key required'},400)
mid=data.get('model_id') or f'custom.{owner}.{hashlib.sha1((prov+base+name).encode()).hexdigest()[:10]}'; b=checkout(data.get('branch') or f'patch/model-{safe(mid)}','main',True)
doc=sanitize({k:v for k,v in data.items() if k!='api_key'}); doc.update({'model_id':mid,'provider_name':prov,'base_url':base,'model_name':name,'api_key_secret_ref':secret_ref,'created_by':a['account'],'created_at':now()})
@@ -1080,6 +1090,7 @@ def api_spec():
return {'version':'V1R1C0-chatgpt-actions','openapi':'/api/openapi.yaml','read':['GET /api/base','GET /api/ver','GET /api/diff','GET /api/repository/tree','GET /api/repository/search','GET /api/repository/diff-index','GET /api/repository/file','GET /api/patch','GET /api/model','GET /api/run','GET /getReleaseCandidate','GET /getApprovalEnvelope','GET /getReleaseSaga','GET /admin/ai-evolution'],'write':['POST /api/branch','POST /api/file','POST /api/commit','POST /api/patch','POST /api/tag','POST /api/rollback','POST /api/model','PUT /api/model-priority','POST /api/nl','POST /createReleaseCandidate','POST /submitOwnerApproval','POST /startDeployment'],'policy':{'bearer_required_for_write':True,'direct_main_push':False,'delete_tag':False,'owner_approval_required_for_v_level':True,'api_keys_in_git':False,'git_secret_field':'secret_ref','online_git_sync':True,'silent_release_execution_enabled':SILENT_RELEASE_EXECUTION_ENABLED,'silent_release_shadow_default':True}}
def main():
require_isolated_dev_profile(HOST)
validate_gitea_api_url(os.getenv('AIOS_GITOPS_GITEA_API_URL', ''))
print(f'AIOS ChatGPT GitOps adapter listening on {HOST}:{PORT}')
ThreadingHTTPServer((HOST,PORT), H).serve_forever()
+36 -1
View File
@@ -216,6 +216,7 @@ class ControlPlaneReleaseManifest:
repository_identity: str
authoritative_git_dir: Path
deployment_state: str
trusted_runtime_identity: Mapping[str, object] | None = None
def as_dict(self) -> dict[str, object]:
return {
@@ -229,6 +230,35 @@ class ControlPlaneReleaseManifest:
"deployment_state": self.deployment_state,
}
def runtime_identity(self) -> dict[str, object]:
"""Return only a host-injected, already trusted comparison result."""
identity = self.trusted_runtime_identity
required = {
"status", "declared_identity", "observed_identity", "manifest_digest",
"config_digest", "observed_at", "expires_at", "evidence_refs", "reason",
"mismatch_fields",
}
if (
not isinstance(identity, Mapping)
or identity.get("status") != "PASS"
or not required.issubset(identity)
or not isinstance(identity.get("declared_identity"), Mapping)
or not isinstance(identity.get("observed_identity"), Mapping)
or not isinstance(identity.get("evidence_refs"), list)
or not identity["evidence_refs"]
):
return {
"status": "UNKNOWN",
"declared_identity": None,
"observed_identity": None,
"observed_at": None,
"expires_at": None,
"evidence_refs": [],
"reason": "TRUSTED_OBSERVATION_UNAVAILABLE",
"mismatch_fields": [],
}
return dict(identity)
def validate(self, artifact: Path, *, observed_oid: str, approved_root: Path = Path("/opt/aios/gitops-control-plane")) -> dict[str, object]:
try:
runtime = self.runtime_path.resolve(strict=True)
@@ -254,7 +284,12 @@ class ControlPlaneReleaseManifest:
)
if not valid:
raise RuntimeDriftError("CONTROL_PLANE_RUNTIME_DRIFT")
return {**self.as_dict(), "drift_status": "PASS"}
# Artifact and path validation proves only a local declaration. The
# composition root may inject the output of an already trusted runtime
# comparison, but this manifest must never promote its own checks to
# runtime-observation PASS.
identity = self.runtime_identity()
return {**self.as_dict(), "drift_status": identity["status"], "component_runtime_identity": identity}
class BridgeV2Adapter:
@@ -39,6 +39,7 @@ from aios_governed_task_bootstrap import (
)
from governed_production_release import build_executor_config
from aios_baseline.runtime_contract import evaluate_deployment
from component_runtime_identity import evaluate_component_runtime_identity
MAX_BODY = 2 * 1024 * 1024
TaskPrincipalVerifier = Callable[
@@ -104,6 +105,10 @@ class RuntimeConfig:
health_url: str = "http://127.0.0.1:18927/health"
release_plan_path: Path | None = None
repository_discovery_cursor_secret: str | bytes | None = None
component_manifest_path: Path | None = None
component_receipt_path: Path | None = None
component_id: str | None = None
component_identity_comparison: Callable[[Mapping[str, Any]], Mapping[str, Any] | None] | None = None
def _safe(value: str) -> str:
@@ -236,6 +241,12 @@ class GovernedRuntime:
blockers.append("LEGACY_GLOBAL_WRITES_MUST_REMAIN_DISABLED")
if not task_principal_writes_enabled:
blockers.append("TASK_PRINCIPAL_WRITES_DISABLED")
identity = evaluate_component_runtime_identity(
component_id=self.config.component_id,
manifest_path=self.config.component_manifest_path,
receipt_path=self.config.component_receipt_path,
trusted_comparison=self.config.component_identity_comparison,
)
return {
"status": "active",
"health": 200,
@@ -246,7 +257,8 @@ class GovernedRuntime:
"runtime_path": str(self.config.state_root),
"artifact_sha256": os.environ.get("AIOS_CONTROL_PLANE_ARTIFACT_SHA256"),
"deployment_state": "STAGING",
"drift_status": "PASS",
"drift_status": identity["status"],
"component_runtime_identity": identity,
"repository": self.config.repository,
"repository_identity": self.config.repository,
"authoritative_git_dir": str(self.config.authoritative_git_dir),
@@ -1332,6 +1344,17 @@ def main(
"/var/lib/aios-gitops-runtime-anchor/baseline/current.json",
)),
repository_discovery_cursor_secret=discovery_cursor_secret,
component_manifest_path=(
Path(os.environ["AIOS_COMPONENT_MANIFEST_PATH"])
if os.environ.get("AIOS_COMPONENT_MANIFEST_PATH", "").strip()
else None
),
component_receipt_path=(
Path(os.environ["AIOS_COMPONENT_ARTIFACT_RECEIPT_PATH"])
if os.environ.get("AIOS_COMPONENT_ARTIFACT_RECEIPT_PATH", "").strip()
else None
),
component_id=os.environ.get("AIOS_COMPONENT_ID", "").strip() or None,
)
runtime = GovernedRuntime(config)
# Prefer a server-side TaskPrincipal provider recovered from the same
@@ -1342,21 +1365,39 @@ def main(
BridgeHandler.principal_verifier = task_principal_verifier
if task_admission_handlers is None:
admission_runtime = None
if os.environ.get("AIOS_TASK_ADMISSION_EPOCH_ROOT", "").strip():
from aios_task_admission_live_adapters import (
build_task_admission_runtime_from_environment,
source_activation = os.environ.get("AIOS_SOURCE_PUBLICATION_ACTIVATION_FILE", "").strip()
try:
if os.environ.get("AIOS_TASK_ADMISSION_EPOCH_ROOT", "").strip():
from aios_task_admission_live_adapters import (
build_task_admission_runtime_from_environment,
)
admission_runtime = build_task_admission_runtime_from_environment(
post_admission=runtime.project_admitted_task,
)
elif activation_raw and Path(activation_raw).is_file():
from aios_task_admission_live_adapters import (
build_task_admission_runtime_from_activation_file,
)
admission_runtime = build_task_admission_runtime_from_activation_file(
Path(activation_raw),
post_admission=runtime.project_admitted_task,
)
except TaskAdmissionError as exc:
if not source_activation or str(exc) != "TASK_ADMISSION_EPOCH_EXPIRED":
raise
from source_publication_runtime import expired_admission_runtime
admission_runtime = expired_admission_runtime()
if source_activation:
from source_publication_runtime import (
AdmissionRuntimeRouter, build_source_runtime_from_activation_file,
)
admission_runtime = build_task_admission_runtime_from_environment(
post_admission=runtime.project_admitted_task,
)
elif activation_raw and Path(activation_raw).is_file():
from aios_task_admission_live_adapters import (
build_task_admission_runtime_from_activation_file,
)
admission_runtime = build_task_admission_runtime_from_activation_file(
Path(activation_raw),
post_admission=runtime.project_admitted_task,
if admission_runtime is None or task_principal_verifier is not None:
raise RuntimeError("SOURCE_ACTIVATION_ORDINARY_RUNTIME_REQUIRED")
source_runtime = build_source_runtime_from_activation_file(
Path(source_activation), post_admission=runtime.project_admitted_task,
ordinary_activation_file=Path(activation_raw),
)
admission_runtime = AdmissionRuntimeRouter(admission_runtime, source_runtime).bundle()
if admission_runtime is not None:
task_admission_handlers = admission_runtime.handlers
if BridgeHandler.principal_verifier is None:
@@ -11,10 +11,32 @@ class RuntimeStatus:
read_plane: Mapping[str, bool]
write_plane: Mapping[str, bool]
release_plane: Mapping[str, bool]
required_facts: Mapping[str, frozenset[str]] | None = None
def _readiness_reason(self) -> str:
if self.required_facts is None:
return "REQUIRED_FACT_INVENTORY_MISSING"
expected_groups = {"READ_PLANE", "WRITE_PLANE", "RELEASE_PLANE"}
if set(self.required_facts) != expected_groups:
return "REQUIRED_FACT_INVENTORY_INVALID"
planes = {
"READ_PLANE": self.read_plane,
"WRITE_PLANE": self.write_plane,
"RELEASE_PLANE": self.release_plane,
}
for group, facts in self.required_facts.items():
if not isinstance(facts, frozenset) or not facts or any(not isinstance(name, str) or not name for name in facts):
return "REQUIRED_FACT_INVENTORY_INVALID"
plane = planes[group]
if set(plane) != set(facts):
return "REQUIRED_FACT_INVENTORY_MISMATCH"
if any(type(value) is not bool or value is not True for value in plane.values()):
return "REQUIRED_FACT_NOT_TRUE"
return "READY"
@property
def release_control_plane_ready(self) -> bool:
return all(self.read_plane.values()) and all(self.write_plane.values()) and all(self.release_plane.values())
return self._readiness_reason() == "READY"
def as_dict(self) -> dict[str, object]:
return {
@@ -22,4 +44,5 @@ class RuntimeStatus:
"WRITE_PLANE": dict(self.write_plane),
"RELEASE_PLANE": dict(self.release_plane),
"release_control_plane_ready": self.release_control_plane_ready,
"readiness_reason": self._readiness_reason(),
}
+39 -5
View File
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
from __future__ import annotations
import json, os, re, sqlite3, subprocess, time, uuid, hashlib, urllib.parse, html
import json, os, re, sqlite3, subprocess, time, uuid, hashlib, urllib.parse, html, ipaddress
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
from pathlib import Path
from typing import Any
@@ -16,6 +16,25 @@ HOST = os.getenv('AIOS_GITOPS_HOST', '127.0.0.1')
PORT = int(os.getenv('AIOS_GITOPS_PORT', '18890'))
ENTRY = os.getenv('AIOS_ENTRY', 'https://cnnetmkt.top')
DEV_ALLOW = os.getenv('AIOS_GITOPS_DEV_ALLOW', '0') == '1'
def _loopback(value):
try: return ipaddress.ip_address(value).is_loopback
except (ValueError, TypeError): return False
def validate_dev_profile():
if DEV_ALLOW and not (os.getenv('AIOS_RUNTIME_PROFILE') == 'isolated-staging' and _loopback(HOST)):
raise RuntimeError('DEV_ALLOW requires isolated-staging and loopback')
def runtime_selection(principal, data):
if not principal.get('token_ok'): raise PermissionError('authenticated principal required')
if any(key in data for key in ('username', 'user_id', 'role', 'tier')):
raise ValueError('runtime identity must come from authenticated principal')
username=principal['username']; role=principal['role']
if role not in _ROLE_WHITELIST or re.fullmatch(r'[A-Za-z0-9_@.-]{1,120}', username) is None or username in ('.','..'):
raise ValueError('invalid authenticated principal')
tier='ultra' if role=='owner' else 'regular'
return username, role, tier
BASE_MODELS = [
{'id':'base.deepseekv4-flash','provider':'one-api','model':'deepseekv4-flash','display':'DeepSeek V4 Flash','tier':'regular','reasoning':1,'vision':0,'tools':1,'context':128000},
{'id':'base.qwen3.6-35b','provider':'one-api','model':'qwen3.6-35b','display':'Qwen 3.6 35B','tier':'pro','reasoning':1,'vision':0,'tools':1,'context':262144},
@@ -68,6 +87,7 @@ def token_identity(token: str):
DEV_ALLOW flag maps a fixed dev principal (must be explicitly enabled).
"""
if DEV_ALLOW:
validate_dev_profile()
return {'username': 'dev-owner', 'role': 'owner'}
if not token or not TOKEN_FILE.exists():
return None
@@ -85,7 +105,9 @@ def token_identity(token: str):
return None
def valid_token(token: str) -> bool:
if DEV_ALLOW: return True
if DEV_ALLOW:
validate_dev_profile()
return True
return token_identity(token) is not None
def git_cmd(args, check=False):
WORKSPACE.mkdir(parents=True, exist_ok=True); p = subprocess.run(['git'] + args, cwd=str(WORKSPACE), text=True, capture_output=True)
@@ -163,7 +185,19 @@ class Handler(BaseHTTPRequestHandler):
self.send_json({'error':'not_found','path':path},404)
except Exception as e: self.send_json({'error':str(e)},500)
def do_POST(self):
init_db(); path = urllib.parse.urlparse(self.path).path; data = parse_body(self)
path = urllib.parse.urlparse(self.path).path
if path == '/api/runtime/select':
if not (_loopback(HOST) and _loopback(self.client_address[0])):
self.send_json({'error':'loopback ingress required'},403); return
a = self.require_write()
if not a: return
data = parse_body(self)
try: runtime_selection(a, data)
except (ValueError, PermissionError):
self.send_json({'error':'invalid runtime selection identity'},403); return
else:
data = parse_body(self)
init_db()
try:
if path == '/api/model/custom/add':
a = self.require_write();
@@ -217,7 +251,7 @@ class Handler(BaseHTTPRequestHandler):
if not m: self.send_json({'error':'model not found'},404); return
self.send_json({'status':'ok' if m['base_url'] and m['model_name'] else 'failed','live_test':os.getenv('AIOS_ENABLE_MODEL_LIVE_TEST','0')=='1','model':m['model_name'],'base_url':m['base_url'],'note':'Network live call is disabled unless AIOS_ENABLE_MODEL_LIVE_TEST=1'}); return
if path == '/api/runtime/select':
a=self.actor(); username=safe_name(data.get('username') or a['username']); role=data.get('role') or a['role']; tier=data.get('tier') or ('ultra' if role=='owner' else 'regular'); risk=data.get('risk','normal')
username,role,tier=runtime_selection(a, data); risk=data.get('risk','normal')
if risk in ('high_tool','shell','docker','write_prod'): runtime,backend='ephemeral_sandbox','openclaw_executor'
elif role in ('owner','admin') or tier=='ultra': runtime,backend='dedicated','hermes'
elif tier=='pro': runtime,backend='dedicated_or_shared','hermes'
@@ -292,5 +326,5 @@ class Handler(BaseHTTPRequestHandler):
except Exception as e: self.send_json({'error':str(e)},500)
def api_spec(): return {'version':'V1R1C0','short_links':['/base','/ver','/diff','/patch','/run','/model','/api'],'gitops_get':['/api/status','/api/base','/api/ver','/api/diff','/api/patch'],'gitops_post':['/api/branch','/api/file','/api/commit','/api/tag','/api/rollback','/api/nl'],'model_get':['/api/model/base','/api/model/custom','/api/model/priority'],'model_post':['/api/model/custom/add','/api/model/custom/update','/api/model/delete','/api/model/priority/set','/api/model/route/resolve','/api/model/test'],'runtime':['/api/runtime/status','/api/runtime/select','/api/runtime/register','/api/runtime/heartbeat'],'auth':'write APIs require Bearer token unless AIOS_GITOPS_DEV_ALLOW=1 for local acceptance tests'}
def main(): init_db(); print(f'AIOS V1R1C0 GitOps Model API listening on {HOST}:{PORT}'); ThreadingHTTPServer((HOST, PORT), Handler).serve_forever()
def main(): validate_dev_profile(); init_db(); print(f'AIOS V1R1C0 GitOps Model API listening on {HOST}:{PORT}'); ThreadingHTTPServer((HOST, PORT), Handler).serve_forever()
if __name__ == '__main__': main()
+6 -1
View File
@@ -486,8 +486,12 @@ def token_records():
else:
out.append({'account':'legacy-token','role':'owner','scopes':['*'],'token_plain':line})
return out
from security_path_guards import require_isolated_dev_profile
def authenticate(token: str):
if os.getenv('AIOS_GITOPS_DEV_ALLOW','0') == '1': return {'account':'dev-owner','role':'owner','scopes':['*']}
if os.getenv('AIOS_GITOPS_DEV_ALLOW','0') == '1':
require_isolated_dev_profile(HOST)
return {'account':'dev-owner','role':'owner','scopes':['*']}
if not token: return None
digest=hashlib.sha256(token.encode()).hexdigest()
for r in token_records():
@@ -1303,6 +1307,7 @@ def api_spec():
def main():
require_isolated_dev_profile(HOST)
print(f'AIOS ChatGPT GitOps adapter listening on {HOST}:{PORT}')
ThreadingHTTPServer((HOST,PORT), H).serve_forever()
if __name__ == '__main__': main()
@@ -51,15 +51,30 @@ AUTHORIZATION_BUNDLE_SCHEMA = "aios.owner-authorization-bundle.v1"
MACHINE_CAPABILITY_SCHEMA = "aios.machine-derived-capability.v1"
BOOTSTRAP_STATE_SCHEMA = "aios.single-host-bootstrap-state.v1"
OWNER_EPOCH_SCHEMA = "aios.owner-delegation-epoch.v2"
SOURCE_PUBLICATION_POLICY_SCHEMA = "aios.source-publication-policy.v1"
SOURCE_PUBLICATION_POLICY_ID = "C5_SOURCE_PUBLICATION_ONLY"
SOURCE_PUBLICATION_CAPABILITIES = frozenset(
{
"agent-task:control",
"governed.task.create",
"governed.write.files",
"task.principal.issue",
"writer.lease",
}
)
DEFAULT_REGISTRY_PATH = Path("/etc/aios/single-host-signer-registry.json")
DEFAULT_OWNER_SIGNER_ID = "local-owner-authority"
DEFAULT_MACHINE_SIGNER_ID = "machine-operational"
_HEX64 = re.compile(r"^[0-9a-f]{64}$")
_HEX40 = re.compile(r"^[0-9a-f]{40}$")
_CAP = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._:-]{1,127}$")
_ID = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._:@/-]{2,255}$")
_RISK = {"low": 0, "medium": 1, "high": 2}
# ``engineering`` is deliberately between medium and high. Adding this rank
# does not raise any configured ceiling; existing policy files keep their
# existing value (the normal local-owner policy is ``medium``).
_RISK = {"low": 0, "medium": 1, "engineering": 2, "high": 3}
_MAX_JSON = 128 * 1024
_MAX_KEY = 64 * 1024
_MAGIC = b"SSHSIG"
@@ -691,6 +706,30 @@ class OwnerAuthorizationBundle:
return asdict(self)
@dataclass(frozen=True, slots=True)
class SourcePublicationPolicyRequest:
"""One exact, source-only unattended publication authorization request."""
policy_id: str
repository: str
authority_base_oid: str
source_oid: str
source_tree_oid: str
target_agent: str
target_branch: str
generation: int
execution_digest: str
exact_files: tuple[str, ...]
nonce: str
ttl_seconds: int
risk: str = "engineering"
production_impact: str = "none"
renewal: str = "none"
def __post_init__(self) -> None:
object.__setattr__(self, "exact_files", tuple(self.exact_files))
@dataclass(frozen=True, slots=True)
class MachineCapabilityRequest:
capability_id: str
@@ -755,20 +794,28 @@ class LocalOwnerAuthority(LocalFileSigner):
def __init__(
self,
key_path: Path,
key_path: Path | None,
policy: SingleHostOwnerPolicy,
*,
require_root_owner: bool = True,
clock: Any = time.time,
registered_signer: Any | None = None,
) -> None:
super().__init__(
key_path,
key_path or Path("/dev/null"),
signer_id=DEFAULT_OWNER_SIGNER_ID,
namespace=OWNER_NAMESPACE,
require_root_owner=require_root_owner,
)
self.policy = policy
self.clock = clock
self.registered_signer = registered_signer
@property
def fingerprint(self) -> str:
if self.registered_signer is not None:
return str(self.registered_signer.fingerprint)
return super().fingerprint
@staticmethod
def _prefix_narrows(requested: str, allowed: Sequence[str]) -> bool:
@@ -831,6 +878,111 @@ class LocalOwnerAuthority(LocalFileSigner):
value["canonical_payload_digest"] = sha256_json(value)
return self._sign_canonical(value)
def _sign_canonical(self, value: Mapping[str, Any]) -> SignedArtifact:
if self.registered_signer is None:
return super()._sign_canonical(value)
payload = canonical_json_bytes(dict(value))
signature = self.registered_signer._sign_canonical_payload(
self.namespace, payload
)
return SignedArtifact(
payload=dict(value),
canonical_payload=payload,
signature=signature,
key_id=self.fingerprint,
namespace=self.namespace,
)
def issue_source_publication_policy(
self, request: SourcePublicationPolicyRequest
) -> SignedArtifact:
"""Sign one exact C5 source-publication policy.
This is an issuance operation only. It cannot change the configured
owner policy, its ceiling, branch/file allowlists, or trust domain.
"""
now = int(self.clock())
files = tuple(request.exact_files)
def source_scope_narrows(value: str, allowed: Sequence[str]) -> bool:
base = value.rstrip("/")
return any(
base == prefix.rstrip("/")
or base.startswith(prefix.rstrip("/") + "/")
for prefix in allowed
)
if (
request.policy_id != SOURCE_PUBLICATION_POLICY_ID
or request.repository != self.policy.repository
or not _HEX40.fullmatch(request.authority_base_oid)
or not _HEX40.fullmatch(request.source_oid)
or not _HEX40.fullmatch(request.source_tree_oid)
or not _ID.fullmatch(request.target_agent)
or not request.target_agent.startswith("agent/")
or not source_scope_narrows(
request.target_branch, self.policy.allowed_branch_prefixes
)
or not request.target_branch.startswith("agent/")
or "\\" in request.target_branch
or ".." in PurePosixPath(request.target_branch).parts
or request.target_branch.endswith(("/", "."))
or type(request.generation) is not int
or request.generation < 1
or not _HEX64.fullmatch(request.execution_digest)
or not files
or any(
not item
or PurePosixPath(item).is_absolute()
or "\\" in item
or ".." in PurePosixPath(item).parts
or item.startswith(".git")
or not source_scope_narrows(item, self.policy.allowed_file_prefixes)
for item in files
)
or not SOURCE_PUBLICATION_CAPABILITIES.issubset(self.policy.allowed_capabilities)
or len(set(files)) != len(files)
or not _ID.fullmatch(request.nonce)
or request.risk != "engineering"
or request.risk not in _RISK
or _RISK[request.risk] > _RISK[self.policy.risk_ceiling]
or request.production_impact != "none"
or request.renewal != "none"
or type(request.ttl_seconds) is not int
or not 1 <= request.ttl_seconds <= 3600
or request.ttl_seconds > self.policy.maximum_epoch_ttl_seconds
):
_fail("SOURCE_PUBLICATION_REQUEST_WIDENS_POLICY")
value: dict[str, Any] = {
"schema": SOURCE_PUBLICATION_POLICY_SCHEMA,
"policy_id": SOURCE_PUBLICATION_POLICY_ID,
"owner_identity": OWNER_IDENTITY,
"repository": request.repository,
"trust_domain": self.policy.trust_domain,
"policy_digest": self.policy.digest,
"authority_base_oid": request.authority_base_oid,
"source_oid": request.source_oid,
"source_tree_oid": request.source_tree_oid,
"target_agent": request.target_agent,
"target_branch": request.target_branch,
"generation": request.generation,
"execution_digest": request.execution_digest,
"exact_files": list(files),
"capabilities": sorted(SOURCE_PUBLICATION_CAPABILITIES),
"risk": "engineering",
"production_impact": "none",
"renewal": "none",
"nonce": request.nonce,
"issued_at": now,
"not_before": max(0, now - 30),
"expires_at": now + request.ttl_seconds,
"status": "ACTIVE",
"signature_namespace": OWNER_NAMESPACE,
"signature_key_id": self.fingerprint,
}
value["canonical_payload_digest"] = sha256_json(value)
return self._sign_canonical(value)
issue_source_only_policy = issue_source_publication_policy
def issue_authorization_bundle(
self,
*,
@@ -35,6 +35,8 @@ from typing import Any, Callable, Mapping, Protocol, Sequence
SCHEMA_ENVELOPE = "aios.task-admission-envelope.v1"
SCHEMA_GRANT = "aios.task-admission-grant.v1"
SCHEMA_EPOCH = "aios.owner-delegation-epoch.v1"
SCHEMA_EPOCH_V2 = "aios.owner-delegation-epoch.v2"
SUPPORTED_EPOCH_SCHEMAS = frozenset({SCHEMA_EPOCH, SCHEMA_EPOCH_V2})
SCHEMA_RECEIPT = "aios.task-admission-receipt.v1"
OID_RE = re.compile(r"^[0-9a-f]{40}$")
@@ -142,9 +144,30 @@ class OwnerDelegationEpoch:
revocation_epoch: int
policy_digest: str
schema: str = SCHEMA_EPOCH
production_scope: str | None = None
destructive_policy: str | None = None
# Provenance retained by the consumer. These fields are populated by the
# signed-directory provider for v2 (and left at their v1-compatible
# defaults when reading legacy epochs); they are never used to translate or
# re-sign the original artifact.
status: str = ""
signature_namespace: str = ""
signature_key_id: str = ""
canonical_payload_digest: str = ""
canonical_payload: bytes = b""
@property
def issuer(self) -> str:
"""The signed epoch issuer, retained under the legacy identity name."""
return self.owner_identity
@property
def canonical_payload_bytes(self) -> bytes:
return self.canonical_payload
def as_public_dict(self) -> dict[str, Any]:
value = asdict(self)
value.pop("canonical_payload", None)
value["allowed_capabilities"] = sorted(self.allowed_capabilities)
value["allowed_branch_prefixes"] = list(self.allowed_branch_prefixes)
value["allowed_file_prefixes"] = list(self.allowed_file_prefixes)
@@ -309,7 +332,7 @@ def _normalize_capabilities(
def _risk_rank(value: str) -> int:
ranks = {"low": 0, "medium": 1, "high": 2}
ranks = {"low": 0, "medium": 1, "engineering": 2, "high": 3}
try:
return ranks[value]
except KeyError:
@@ -838,7 +861,7 @@ class TaskAdmissionBootstrapService:
now: int,
) -> OwnerDelegationEpoch:
epoch = self.authorization.verify_delegation_epoch(epoch_id)
if epoch.schema != SCHEMA_EPOCH:
if epoch.schema not in SUPPORTED_EPOCH_SCHEMAS:
raise TaskAdmissionError("TASK_ADMISSION_EPOCH_SCHEMA_INVALID", 403)
if epoch.epoch_id != epoch_id:
raise TaskAdmissionError("TASK_ADMISSION_EPOCH_ID_MISMATCH", 403)
@@ -26,6 +26,8 @@ import urllib.request
from aios_task_admission_bootstrap import (
OwnerDelegationEpoch,
SCHEMA_EPOCH,
SCHEMA_EPOCH_V2,
TaskAdmissionError,
TaskAdmissionGrant,
canonical_json_bytes,
@@ -44,6 +46,19 @@ from aios_gitops_job_identity import (
OWNER_NAMESPACE = "aios-owner-approval-v1"
OWNER_SIGNER_IDENTITY = "aios-task-grant-signer"
_EPOCH_V1_FIELDS = frozenset({
"epoch_id", "owner_identity", "trust_domain", "repository",
"allowed_branch_prefixes", "allowed_capabilities", "allowed_file_prefixes",
"risk_ceiling", "issued_at", "not_before", "expires_at",
"revocation_epoch", "policy_digest", "schema",
})
_EPOCH_V2_FIELDS = frozenset({
*_EPOCH_V1_FIELDS,
"business_goal", "production_scope", "destructive_policy",
"signature_namespace", "signature_key_id", "canonical_payload_digest",
"status",
})
_SSH_FINGERPRINT_RE = re.compile(r"^SHA256:[A-Za-z0-9+/]{43}$")
class OfficialTaskTransport(Protocol):
@@ -463,6 +478,7 @@ class TaskAdmissionPrincipalProvider:
class TaskAdmissionRuntimeBundle:
handlers: Mapping[str, Any]
principal_verifier: TaskAdmissionPrincipalProvider
service: Any | None = None
class TaskAdmissionHostError(RuntimeError):
@@ -622,11 +638,15 @@ class SignedEpochDirectoryProvider:
"TASK_ADMISSION_EPOCH_CANONICAL_MISMATCH",
403,
)
if value.get("schema") != "aios.owner-delegation-epoch.v1":
schema = value.get("schema")
if schema not in {SCHEMA_EPOCH, SCHEMA_EPOCH_V2}:
raise TaskAdmissionError(
"TASK_ADMISSION_EPOCH_SCHEMA_INVALID",
403,
)
expected_fields = _EPOCH_V1_FIELDS if schema == SCHEMA_EPOCH else _EPOCH_V2_FIELDS
if set(value) != expected_fields:
raise TaskAdmissionError("TASK_ADMISSION_EPOCH_FIELDS_INVALID", 403)
if value.get("epoch_id") != epoch_id:
raise TaskAdmissionError(
"TASK_ADMISSION_EPOCH_ID_MISMATCH",
@@ -642,7 +662,34 @@ class SignedEpochDirectoryProvider:
if epoch_id in self._revoked_ids():
raise TaskAdmissionError("TASK_ADMISSION_EPOCH_REVOKED", 403)
# v2 is a producer-authored envelope. Validate its self-digest and
# signature binding before the cryptographic verifier sees the exact
# bytes from disk. The digest is over the payload with only the digest
# member removed; no schema translation is permitted.
if schema == SCHEMA_EPOCH_V2:
if (
value.get("status") != "ACTIVE"
or not isinstance(value.get("business_goal"), str)
or not value["business_goal"]
or not isinstance(value.get("production_scope"), str)
or not value["production_scope"]
or not isinstance(value.get("destructive_policy"), str)
or value["destructive_policy"] not in {"forbidden", "rollback-only"}
or value.get("signature_namespace") != OWNER_NAMESPACE
or not isinstance(value.get("signature_key_id"), str)
or not _SSH_FINGERPRINT_RE.fullmatch(value["signature_key_id"])
or not isinstance(value.get("canonical_payload_digest"), str)
or not re.fullmatch(r"[0-9a-f]{64}", value["canonical_payload_digest"])
):
raise TaskAdmissionError("TASK_ADMISSION_EPOCH_V2_BINDING_INVALID", 403)
digest_value = dict(value)
supplied_digest = digest_value.pop("canonical_payload_digest", None)
if sha256_json(digest_value) != supplied_digest:
raise TaskAdmissionError("TASK_ADMISSION_EPOCH_PAYLOAD_DIGEST_INVALID", 403)
key_id = key_id_file.read_text(encoding="utf-8").strip()
if schema == SCHEMA_EPOCH_V2 and key_id != value["signature_key_id"]:
raise TaskAdmissionError("TASK_ADMISSION_EPOCH_KEY_BINDING_INVALID", 403)
armored = signature.read_text(encoding="utf-8")
if self.verifier.verify(
namespace=OWNER_NAMESPACE,
@@ -659,6 +706,23 @@ class SignedEpochDirectoryProvider:
not_before = value.get("not_before")
expires_at = value.get("expires_at")
if (
not isinstance(value.get("epoch_id"), str)
or not value["epoch_id"]
or not isinstance(value.get("owner_identity"), str)
or not value["owner_identity"]
or not isinstance(value.get("trust_domain"), str)
or not value["trust_domain"]
or not isinstance(value.get("repository"), str)
or not value["repository"]
or not isinstance(value.get("risk_ceiling"), str)
or value["risk_ceiling"] not in {"low", "medium", "engineering", "high"}
or not isinstance(value.get("issued_at"), int)
or isinstance(value.get("issued_at"), bool)
or not isinstance(value.get("revocation_epoch"), int)
or isinstance(value.get("revocation_epoch"), bool)
or not isinstance(value.get("policy_digest"), str)
or not value["policy_digest"]
or
not isinstance(not_before, int)
or isinstance(not_before, bool)
or not isinstance(expires_at, int)
@@ -672,13 +736,16 @@ class SignedEpochDirectoryProvider:
files = value.get("allowed_file_prefixes")
if (
not isinstance(branches, list)
or not branches
or not all(isinstance(item, str) and item for item in branches)
or not isinstance(capabilities, list)
or not capabilities
or not all(
isinstance(item, str) and item
for item in capabilities
)
or not isinstance(files, list)
or not files
or not all(isinstance(item, str) and item for item in files)
):
raise TaskAdmissionError(
@@ -701,6 +768,19 @@ class SignedEpochDirectoryProvider:
revocation_epoch=int(value["revocation_epoch"]),
policy_digest=str(value["policy_digest"]),
schema=str(value["schema"]),
production_scope=(
str(value["production_scope"])
if schema == SCHEMA_EPOCH_V2 else None
),
destructive_policy=(
str(value["destructive_policy"])
if schema == SCHEMA_EPOCH_V2 else None
),
status=str(value.get("status", "")),
signature_namespace=str(value.get("signature_namespace", "")),
signature_key_id=str(value.get("signature_key_id", "")),
canonical_payload_digest=str(value.get("canonical_payload_digest", "")),
canonical_payload=payload,
)
@@ -915,6 +995,7 @@ def _build_task_admission_runtime(
task_port=task_port,
epoch_id=epoch.epoch_id,
),
service=service,
)
@@ -27,6 +27,7 @@ CANONICAL_PROFILE = "aios.task-initial-execution.v1"
CANONICAL_PROFILE_VERSION = 1
SUPERSESSION_REASON = "formal-lineage-supersession-after-expired-unclaimed-source"
MAX_TTL = timedelta(minutes=5)
PARAMETERIZED_RECOVERY_NONCE_SCOPE = "aios.task-parameterized-recovery.v1:owner-nonce"
HEX64 = re.compile(r"^[0-9a-f]{64}$")
SAFE_ID = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._:@/+:-]{2,511}$")
@@ -199,6 +200,31 @@ def _verify_source(row: sqlite3.Row, d: Mapping[str, Any], identity_digest: Call
_fail("SOURCE_GENERATION_CONSTRAINT_MISMATCH")
def _parameterized_recovery_nonce_reserved(
connection: sqlite3.Connection, nonce: str
) -> bool:
"""Share the existing authority nonce domain when C6 NOOP support exists.
Historical lineage databases can predate TaskStore's idempotency table;
their established lineage-only operation remains readable. The C6 NOOP
adapter refuses to activate on those schemas, so an absent table cannot
create a cross-operation uniqueness gap.
"""
tables = {
str(row[0])
for row in connection.execute("SELECT name FROM sqlite_master WHERE type='table'")
}
if "idempotency" not in tables:
return False
columns = {str(row[1]) for row in connection.execute("PRAGMA table_info(idempotency)")}
if not {"scope", "key", "payload_digest", "response_json", "created_at"}.issubset(columns):
_fail("FORMAL_IDEMPOTENCY_SCHEMA_UNAVAILABLE", 503)
return connection.execute(
"SELECT 1 FROM idempotency WHERE scope=? AND key=?",
(PARAMETERIZED_RECOVERY_NONCE_SCOPE, nonce),
).fetchone() is not None
class TaskLineageSupersessionController:
def __init__(self, db_path: str | Path, verifier: SignatureVerifier, *, clock=None, identity_digest=None):
self.db_path = Path(db_path)
@@ -251,6 +277,8 @@ class TaskLineageSupersessionController:
connection.execute("BEGIN IMMEDIATE")
try:
self._assert_schema(connection)
if _parameterized_recovery_nonce_reserved(connection, decision["nonce"]):
_fail("PARAMETERIZED_RECOVERY_NONCE_CONSUMED")
replay = connection.execute(
"SELECT owner_decision_sha256,receipt_json FROM task_lineage_supersession_receipts WHERE decision_id=? OR nonce=?",
(decision["decision_id"], decision["nonce"]),
@@ -0,0 +1,317 @@
"""Narrow, terminal-only parameterized recovery over the formal Task store.
This adapter deliberately does *not* create a generic recovery authority. It
can only record an Owner-signed, exact-CAS terminal NOOP. A non-terminal
recovery remains the existing formal lineage/supersession operation.
The receipt uses the existing TaskStore ``idempotency`` uniqueness constraint
in the same authoritative database. Two fixed owner-authority scopes bind a
decision id and a nonce atomically; this is not a new nonce database or table.
"""
from __future__ import annotations
from datetime import datetime, timedelta, timezone
import hashlib
import json
import re
import sqlite3
from typing import Any, Callable, Mapping, Protocol
from aios_agent_task_protocol import TaskStore, canonical_json
from aios_task_lineage_supersession import (
OWNER_KEY_ID,
OWNER_NAMESPACE,
OWNER_PRINCIPAL,
REPOSITORY,
)
DECISION_SCHEMA = "aios.task-parameterized-recovery-decision.v1"
RECEIPT_SCHEMA = "aios.task-parameterized-recovery-receipt.v1"
ACTION_TERMINAL_NOOP = "TERMINAL_NOOP"
TERMINAL_STATUSES = frozenset({"COMPLETED", "CANCELLED", "FAILED_FINAL"})
MAX_TTL = timedelta(minutes=5)
NONCE_SCOPE = "aios.task-parameterized-recovery.v1:owner-nonce"
DECISION_SCOPE = "aios.task-parameterized-recovery.v1:owner-decision"
_HEX64 = re.compile(r"^[0-9a-f]{64}$")
_SAFE_ID = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._:@/+:-]{2,511}$")
_BRANCH = re.compile(r"^agent/[A-Za-z0-9._/-]{1,220}$")
_FIELDS = frozenset(
{
"schema", "decision_id", "nonce", "action", "task_id",
"task_identity_sha256", "expected_status", "expected_attempt",
"expected_generation", "expected_revision", "expected_claimed_by",
"expected_lease_token_hash", "expected_lease_expires_at", "repository",
"target_branch", "owner_identity", "signature_namespace",
"signature_principal", "key_id", "issued_at", "not_before",
"expires_at", "one_time_use", "signature",
}
)
class ParameterizedRecoveryError(RuntimeError):
def __init__(self, code: str, http_status: int = 409) -> None:
super().__init__(code)
self.code = code
self.http_status = int(http_status)
class SignatureVerifier(Protocol):
def verify(self, *, namespace: str, payload: bytes, signature: str, key_id: str) -> bool: ...
def _fail(code: str, status: int = 409) -> None:
raise ParameterizedRecoveryError(code, status)
def _utc(value: object, field: str) -> datetime:
if not isinstance(value, str) or not value or len(value) > 80:
_fail(f"{field.upper()}_INVALID", 400)
try:
parsed = datetime.fromisoformat(value.replace("Z", "+00:00"))
except ValueError as exc:
raise ParameterizedRecoveryError(f"{field.upper()}_INVALID", 400) from exc
if parsed.tzinfo is None:
_fail(f"{field.upper()}_INVALID", 400)
return parsed.astimezone(timezone.utc)
def _text(value: object, field: str, *, maximum: int = 1024) -> str:
if not isinstance(value, str) or not value or len(value) > maximum:
_fail(f"{field.upper()}_INVALID", 400)
return value
def canonical_decision_payload(raw: Mapping[str, Any]) -> dict[str, Any]:
return {key: raw[key] for key in raw if key != "signature"}
def _identity_digest(row: sqlite3.Row) -> str:
value = {
"task_id": row["task_id"],
"goal": row["goal"],
"instructions": row["instructions"],
"success_criteria": json.loads(row["success_criteria_json"]),
"required_capabilities": json.loads(row["required_capabilities_json"]),
"constraints": json.loads(row["constraints_json"]),
"metadata": json.loads(row["metadata_json"]),
"priority": row["priority"],
"max_attempts": row["max_attempts"],
}
return hashlib.sha256(canonical_json(value).encode("utf-8")).hexdigest()
def validate_decision(raw: Mapping[str, Any], *, now: datetime) -> dict[str, Any]:
if not isinstance(raw, Mapping) or set(raw) != _FIELDS:
_fail("DECISION_FIELDS_INVALID", 400)
exact = {
"schema": DECISION_SCHEMA,
"action": ACTION_TERMINAL_NOOP,
"repository": REPOSITORY,
"owner_identity": OWNER_PRINCIPAL,
"signature_namespace": OWNER_NAMESPACE,
"signature_principal": OWNER_PRINCIPAL,
"key_id": OWNER_KEY_ID,
"one_time_use": True,
}
for field, expected in exact.items():
if raw[field] != expected:
_fail(f"{field.upper()}_MISMATCH", 403)
for field in ("task_id", "decision_id", "nonce"):
if _SAFE_ID.fullmatch(_text(raw[field], field, maximum=512)) is None:
_fail(f"{field.upper()}_INVALID", 400)
if not isinstance(raw["task_identity_sha256"], str) or not _HEX64.fullmatch(raw["task_identity_sha256"]):
_fail("TASK_IDENTITY_SHA256_INVALID", 400)
if not isinstance(raw["target_branch"], str) or not _BRANCH.fullmatch(raw["target_branch"]):
_fail("TARGET_BRANCH_INVALID", 400)
for field, minimum in (("expected_attempt", 0), ("expected_generation", 1), ("expected_revision", 1)):
if isinstance(raw[field], bool) or not isinstance(raw[field], int) or raw[field] < minimum:
_fail(f"{field.upper()}_INVALID", 400)
for field in ("expected_claimed_by", "expected_lease_token_hash", "expected_lease_expires_at"):
if raw[field] is not None:
_text(raw[field], field, maximum=512)
if raw["expected_status"] not in TERMINAL_STATUSES:
_fail("EXPECTED_STATUS_NOT_TERMINAL", 409)
_text(raw["signature"], "signature", maximum=32768)
issued_at = _utc(raw["issued_at"], "issued_at")
not_before = _utc(raw["not_before"], "not_before")
expires_at = _utc(raw["expires_at"], "expires_at")
if issued_at > not_before or not_before > expires_at:
_fail("DECISION_TIME_ORDER_INVALID", 403)
if expires_at <= issued_at or expires_at - issued_at > MAX_TTL:
_fail("DECISION_TTL_INVALID", 403)
if now < not_before or now >= expires_at or issued_at > now + timedelta(seconds=5) or now - issued_at > MAX_TTL:
_fail("DECISION_EXPIRED_OR_NOT_YET_VALID", 403)
return dict(raw)
class ParameterizedRecoveryController:
"""Consumes a validated terminal-NOOP decision once, in the TaskStore DB."""
def __init__(
self,
store: TaskStore,
verifier: SignatureVerifier,
*,
clock: Callable[[], datetime] | None = None,
identity_digest: Callable[[sqlite3.Row], str] = _identity_digest,
) -> None:
if not isinstance(store, TaskStore) or verifier is None:
_fail("FORMAL_TASK_AUTHORITY_REQUIRED", 503)
self.store = store
self.verifier = verifier
self.clock = clock or (lambda: datetime.now(timezone.utc))
self.identity_digest = identity_digest
def _now(self) -> datetime:
value = self.clock()
if value.tzinfo is None:
value = value.replace(tzinfo=timezone.utc)
return value.astimezone(timezone.utc)
@staticmethod
def _assert_schema(connection: sqlite3.Connection) -> None:
tables = {str(row[0]) for row in connection.execute("SELECT name FROM sqlite_master WHERE type='table'")}
if not {"tasks", "events", "idempotency", "task_lineage_supersession_receipts"}.issubset(tables):
_fail("FORMAL_LINEAGE_SCHEMA_UNAVAILABLE", 503)
columns = {str(row[1]) for row in connection.execute("PRAGMA table_info(idempotency)")}
if not {"scope", "key", "payload_digest", "response_json", "created_at"}.issubset(columns):
_fail("FORMAL_IDEMPOTENCY_SCHEMA_UNAVAILABLE", 503)
def _verify_task(self, row: sqlite3.Row, decision: Mapping[str, Any]) -> None:
if self.identity_digest(row) != decision["task_identity_sha256"]:
_fail("TASK_IDENTITY_DIGEST_MISMATCH")
if (
row["task_id"] != decision["task_id"]
or row["status"] != decision["expected_status"]
or int(row["revision"]) != decision["expected_revision"]
or int(row["attempt"]) != decision["expected_attempt"]
or row["claimed_by"] != decision["expected_claimed_by"]
or row["lease_token_hash"] != decision["expected_lease_token_hash"]
or row["lease_expires_at"] != decision["expected_lease_expires_at"]
):
_fail("TASK_EXACT_CAS_MISMATCH")
try:
metadata = json.loads(row["metadata_json"])
constraints = json.loads(row["constraints_json"])
except (TypeError, ValueError) as exc:
raise ParameterizedRecoveryError("TASK_BINDING_JSON_INVALID") from exc
execution = metadata.get("execution") if isinstance(metadata, Mapping) else None
if not isinstance(execution, Mapping) or constraints.get("generation") != decision["expected_generation"]:
_fail("TASK_EXECUTION_BINDING_INVALID")
if (
execution.get("task_id") != decision["task_id"]
or execution.get("repository") != decision["repository"]
or execution.get("target_branch") != decision["target_branch"]
):
_fail("TASK_EXECUTION_BINDING_MISMATCH")
@staticmethod
def _receipt(decision: Mapping[str, Any], decision_sha: str, row: sqlite3.Row, now: datetime) -> dict[str, Any]:
return {
"schema": RECEIPT_SCHEMA,
"state": "NOOP",
"noop_reason": "TERMINAL_STATUS",
"decision_id": decision["decision_id"],
"nonce": decision["nonce"],
"task_id": decision["task_id"],
"status": row["status"],
"revision": int(row["revision"]),
"owner_decision_sha256": decision_sha,
"owner_signature_namespace": OWNER_NAMESPACE,
"owner_signature_principal": OWNER_PRINCIPAL,
"owner_signature_key_id": OWNER_KEY_ID,
"owner_signature_sha256": hashlib.sha256(decision["signature"].encode()).hexdigest(),
"exact_identity_cas": True,
"one_time_use": True,
"observed_at": now.isoformat(),
}
@staticmethod
def _read_exact_replay(
nonce_row: sqlite3.Row | None, decision_row: sqlite3.Row | None, decision: Mapping[str, Any], decision_sha: str
) -> dict[str, Any] | None:
if nonce_row is None and decision_row is None:
return None
if nonce_row is None or decision_row is None:
present = nonce_row if nonce_row is not None else decision_row
if present["payload_digest"] != decision_sha:
_fail("DECISION_REPLAY_MISMATCH")
_fail("RECOVERY_RECEIPT_INCOMPLETE", 500)
if nonce_row["payload_digest"] != decision_sha or decision_row["payload_digest"] != decision_sha:
_fail("DECISION_REPLAY_MISMATCH")
try:
nonce_receipt = json.loads(nonce_row["response_json"])
decision_receipt = json.loads(decision_row["response_json"])
except (TypeError, ValueError) as exc:
raise ParameterizedRecoveryError("RECOVERY_RECEIPT_INVALID", 500) from exc
if nonce_receipt != decision_receipt or (
nonce_receipt.get("schema") != RECEIPT_SCHEMA
or nonce_receipt.get("decision_id") != decision["decision_id"]
or nonce_receipt.get("nonce") != decision["nonce"]
or nonce_receipt.get("task_id") != decision["task_id"]
or nonce_receipt.get("owner_decision_sha256") != decision_sha
):
_fail("DECISION_REPLAY_MISMATCH")
return dict(nonce_receipt)
def recover_terminal_noop(self, raw: Mapping[str, Any]) -> dict[str, Any]:
now = self._now()
decision = validate_decision(raw, now=now)
payload = canonical_decision_payload(raw)
payload_bytes = canonical_json(payload).encode("utf-8")
if not self.verifier.verify(
namespace=OWNER_NAMESPACE,
payload=payload_bytes,
signature=decision["signature"],
key_id=OWNER_KEY_ID,
):
_fail("DECISION_SIGNATURE_INVALID", 403)
decision_sha = hashlib.sha256(payload_bytes).hexdigest()
with self.store._connect() as connection:
connection.execute("BEGIN IMMEDIATE")
try:
self._assert_schema(connection)
row = connection.execute("SELECT * FROM tasks WHERE task_id=?", (decision["task_id"],)).fetchone()
if row is None:
_fail("TASK_NOT_FOUND", 404)
self._verify_task(row, decision)
nonce_row = connection.execute(
"SELECT payload_digest,response_json FROM idempotency WHERE scope=? AND key=?",
(NONCE_SCOPE, decision["nonce"]),
).fetchone()
decision_row = connection.execute(
"SELECT payload_digest,response_json FROM idempotency WHERE scope=? AND key=?",
(DECISION_SCOPE, decision["decision_id"]),
).fetchone()
lineage_row = connection.execute(
"SELECT 1 FROM task_lineage_supersession_receipts WHERE decision_id=? OR nonce=?",
(decision["decision_id"], decision["nonce"]),
).fetchone()
if lineage_row is not None:
_fail("DECISION_REPLAY_MISMATCH")
replay = self._read_exact_replay(nonce_row, decision_row, decision, decision_sha)
if replay is not None:
connection.commit()
return {**replay, "idempotent": True}
receipt = self._receipt(decision, decision_sha, row, now)
receipt_json = canonical_json(receipt)
for scope, key in ((NONCE_SCOPE, decision["nonce"]), (DECISION_SCOPE, decision["decision_id"])):
connection.execute(
"INSERT INTO idempotency(scope,key,payload_digest,response_json,created_at) VALUES(?,?,?,?,?)",
(scope, key, decision_sha, receipt_json, now.isoformat()),
)
connection.execute(
"INSERT INTO events(task_id,agent_id,event_type,payload_json,created_at) VALUES(?,?,?,?,?)",
(decision["task_id"], None, "TASK_PARAMETERIZED_RECOVERY_NOOP", receipt_json, now.isoformat()),
)
connection.commit()
return {**receipt, "idempotent": False}
except Exception:
connection.rollback()
raise
# There is intentionally no generic resume/reclaim path. The existing
# lineage controller remains the only predecessor/successor authority.
recover = recover_terminal_noop
@@ -0,0 +1,167 @@
"""Read-only V3 issuer scope over the admitted producer's durable receipt.
The V1/V2 provider remains authoritative for those schemas. V3 validation
and Job readback are delegated to the existing admission producer contract;
this module never reserves a Job or changes the canonical plan digest.
"""
from __future__ import annotations
from pathlib import Path
import stat
from typing import Any, Callable, Mapping
from aios_governed_task_bootstrap import GovernedTaskBootstrap
from aios_task_admission_bootstrap import TaskAdmissionError
from aios_task_admission_live_adapters import (
ProducerReceiptError,
_FLProducerReceiptReserver,
load_v3_plan,
validate_v3_plan,
)
from governed_runtime_scope import (
GovernedScopeError,
RuntimeScope,
_branch,
_generation,
_hex64,
_job_id,
_normalize_caps,
_normalize_files,
_oid40,
_task_id,
_trust_digest,
)
def _deny_mutation(*_args: Any, **_kwargs: Any) -> None:
raise GovernedScopeError("ISSUER_PRODUCER_MUTATION_FORBIDDEN")
def _read_only_bootstrap(root: Path) -> GovernedTaskBootstrap:
"""Construct only the read-only binding view; __init__ creates directories."""
root = Path(root)
for path in (root, root / "task-bindings", root / "jobs"):
metadata = path.lstat()
if (stat.S_ISLNK(metadata.st_mode) or not stat.S_ISDIR(metadata.st_mode)
or stat.S_IMODE(metadata.st_mode) & 0o022):
raise GovernedScopeError("PRODUCER_STATE_ROOT_INVALID")
view = object.__new__(GovernedTaskBootstrap)
view.root = root
view.task_meta_root = root / "task-bindings"
view.job_root = root / "jobs"
view.create = _deny_mutation
view._store = _deny_mutation
view._write = _deny_mutation
return view
class IssuerV3ReceiptScopeProvider:
"""Delegate legacy plans, and resolve V3 with an existing Job only."""
def __init__(
self, *, legacy: Any, producer_state_root: Path, plan_root: Path,
grant_reader: Callable[..., Mapping[str, Any]],
plan_loader: Callable[[Path, Mapping[str, Any]], Mapping[str, Any]] = load_v3_plan,
) -> None:
self.legacy = legacy
self.producer_state_root = Path(producer_state_root)
self.plan_root = Path(plan_root)
self.grant_reader = grant_reader
self.plan_loader = plan_loader
def inspect_v3_binding(
self, *, grant_raw: Mapping[str, Any], plan: Mapping[str, Any],
) -> tuple[str, Mapping[str, Any]]:
"""Validate original bytes and durable receipt without Task mutation.
This method can inspect an expired historical fixture. ``resolve``
separately enforces live Task and grant state before issuing scope.
"""
try:
original = self.plan_loader(self.plan_root, grant_raw)
if dict(original) != dict(plan):
raise GovernedScopeError("EXECUTION_PLAN_READBACK_CONFLICT")
# Keep the main V3 validator in the issuer contract even when a
# test supplies an isolated loader. It binds the original plan,
# including risk, hashes, capabilities and execution digest.
from aios_task_admission_live_adapters import canonical_json_bytes
validate_v3_plan(grant=grant_raw, raw=canonical_json_bytes(original))
bootstrap = _read_only_bootstrap(self.producer_state_root)
runtime = type("ReadOnlyIssuerRuntime", (), {
"bootstrap": bootstrap, "bindings": {},
})()
reserver = _FLProducerReceiptReserver(runtime)
job_id = reserver.existing_v3_job(grant_raw, original)
if job_id is None:
raise GovernedScopeError("PRODUCER_RECEIPT_MISSING")
readback = reserver.readbacks.get(str(grant_raw["task_id"]))
proof = readback.get("reservation_readback") if isinstance(readback, Mapping) else None
if not isinstance(proof, Mapping) or proof.get("job_id") != job_id:
raise GovernedScopeError("PRODUCER_RECEIPT_BINDING_INVALID")
return job_id, dict(proof)
except (TaskAdmissionError, ProducerReceiptError, OSError, KeyError, TypeError, ValueError) as exc:
raise GovernedScopeError(str(exc) or "PRODUCER_RECEIPT_INVALID") from exc
def resolve(
self, *, task: Any, execution: Any, grant: Any,
plan: Mapping[str, Any], trust: Any, now: int,
) -> RuntimeScope:
if plan.get("schema") != "aios.task-admission-execution-plan.v3":
return self.legacy.resolve(
task=task, execution=execution, grant=grant,
plan=plan, trust=trust, now=now,
)
task_id = _task_id(task.task_id)
exec_job = _job_id(execution.external_job_id)
exec_digest = _hex64(execution.execution_digest, "execution_digest")
exec_branch = _branch(execution.target_branch)
exec_generation = _generation(execution.generation)
grant_raw = self.grant_reader(
task_id=task_id, execution_digest=exec_digest,
repository=execution.repository, target_branch=exec_branch,
generation=exec_generation, now=int(now),
)
producer_job, _proof = self.inspect_v3_binding(
grant_raw=grant_raw, plan=plan,
)
if exec_job != producer_job:
raise GovernedScopeError("EXECUTION_PRODUCER_JOB_MISMATCH")
if str(task.status).upper() not in {"CLAIMED", "RUNNING"}:
raise GovernedScopeError("TASK_NOT_CLAIMED")
if grant.status != "ADMITTED" or int(grant.expires_at) <= int(now):
raise GovernedScopeError("GRANT_EXPIRED_OR_NOT_ADMITTED")
expected = {
"task_id": task_id,
"repository": execution.repository,
"target_branch": exec_branch,
"generation": exec_generation,
"execution_digest": exec_digest,
"authority_base_oid": _oid40(grant.authority_base_oid, "authority_base_oid"),
"grant_id": grant.grant_id,
}
if (execution.repository != "aios/aios"
or execution.task_id != task_id
or any(grant_raw.get(key) != value for key, value in expected.items())):
raise GovernedScopeError("V3_GRANT_EXECUTION_BINDING_MISMATCH")
caps = _normalize_caps(grant.requested_capabilities)
files = _normalize_files(grant.file_allowlist)
if (caps != _normalize_caps(grant_raw["capabilities"])
or files != _normalize_files(grant_raw["file_allowlist"])):
raise GovernedScopeError("V3_GRANT_SCOPE_MISMATCH")
if (not trust.issuer_id or not trust.key_id or not trust.public_key
or "TASK_PRINCIPAL" not in str(trust.purpose).upper().replace("-", "_")
or not str(trust.bridge_id).strip()):
raise GovernedScopeError("TRUST_IDENTITY_INVALID")
trust_digest = _trust_digest(trust)
if (trust.scope_manifest_sha256 is not None
and _hex64(trust.scope_manifest_sha256, "scope_manifest_sha256") != trust_digest):
raise GovernedScopeError("TRUST_MANIFEST_DIGEST_MISMATCH")
return RuntimeScope(
scope_id=f"governed-runtime:{task_id}:g{exec_generation}:{exec_digest[:16]}",
task_id=task_id, external_job_id=producer_job,
repository=execution.repository, target_branch=exec_branch,
generation=exec_generation, execution_digest=exec_digest,
capabilities=caps, trust_manifest_sha256=trust_digest,
grant_id=grant.grant_id, grant_expires_at=int(grant.expires_at),
)
@@ -24,6 +24,19 @@ class BuilderError(RuntimeError):
"""A stable, non-sensitive error code returned by the builder boundary."""
C5_INTEGRATION_CONTRACT_FIELDS = frozenset({
"integration_source_commit", "integration_tree_oid",
"publication_source_commit", "publication_tree_oid",
"publication_plan_sha256", "repair_source_commit", "repair_tree_oid",
})
C5_INTEGRATION_FILES = {
"active-runtime/app/aios_capability_contract.py": ("publication_source_commit", "67d8a3a296110e20a4ea59863cd7982b6bdada71abd457c0f7cec480b06d278a"),
"tests/test_v2r2c5_capability_contract.py": ("publication_source_commit", "f91af641528ea9a625ca45a46b6c26f5dba568e6b69cba819ca33ab5cba28de3"),
"active-runtime/app/aios_baseline/service.py": ("repair_source_commit", "3c3918aec5503947b48ef4c73e34cac8f0d33fdfb99cfcd5ca8af699b663da72"),
"tests/baseline_v1_1/test_loop.py": ("repair_source_commit", "f867e92b9ec9b4dff248e6ca38280848458a535c610dc6d9fea2b270c29b664a"),
}
def _canonical_json(value: Mapping[str, Any]) -> bytes:
return json.dumps(value, sort_keys=True, separators=(",", ":"), ensure_ascii=True).encode("utf-8")
@@ -135,6 +148,7 @@ class TrustedCandidateBuilderConfig:
policy_sha256: str
source_admission_resolver: Callable[[str, str | None, str], Mapping[str, str]] | None = None
migration_plan_resolver: Callable[[str, str, str], Any] | None = None
c5_contract: Mapping[str, str] | None = None
@classmethod
def from_policy(cls, policy: Mapping[str, Any]) -> "TrustedCandidateBuilderConfig":
@@ -145,16 +159,52 @@ class TrustedCandidateBuilderConfig:
"allowed_peer_runtime_prefix", "allowed_peer_runtime_suffix",
"required_checks", "evidence_layout", "recipes", "governed_recipes",
}
if set(policy) != required or policy.get("schema") != "aios.trusted-candidate-builder-policy.v1":
is_integration = policy.get("schema") == "aios.trusted-candidate-builder-policy.c5-integration.v1"
is_c5 = is_integration or policy.get("schema") == "aios.trusted-candidate-builder-policy.c5.v1"
if set(policy) != (required | {"c5_contract"} if is_c5 else required) or policy.get("schema") not in {"aios.trusted-candidate-builder-policy.v1", "aios.trusted-candidate-builder-policy.c5.v1", "aios.trusted-candidate-builder-policy.c5-integration.v1"}:
raise BuilderError("POLICY_SCHEMA_INVALID")
recipes = policy.get("recipes")
if not isinstance(recipes, Mapping) or set(recipes) != {"V2R2C2", "V2R2C3"}:
expected_recipes = {"V2R2C2", "V2R2C3", "V2R2C5"} if is_c5 else {"V2R2C2", "V2R2C3"}
if not isinstance(recipes, Mapping) or set(recipes) != expected_recipes:
raise BuilderError("POLICY_RECIPES_INVALID")
c2, c3 = recipes["V2R2C2"], recipes["V2R2C3"]
if not isinstance(c2, Mapping) or set(c2) != {"mode", "task_id", "candidate_id", "base_oid", "source_commit", "version", "purpose"} or c2.get("mode") != "fixed":
raise BuilderError("POLICY_V2R2C2_INVALID")
if not isinstance(c3, Mapping) or set(c3) != {"mode", "derives_from", "version", "purpose"} or c3.get("mode") != "governed-derived-only" or c3.get("derives_from") != "V2R2C2":
raise BuilderError("POLICY_V2R2C3_INVALID")
c5_contract = None
if is_c5:
c5 = recipes["V2R2C5"]
expected_mode = "governed-source-integration-only" if is_integration else "governed-source-admission-only"
if not isinstance(c5, Mapping) or set(c5) != {"mode", "version", "purpose"} or c5.get("mode") != expected_mode or c5.get("version") != "V2R2C5" or c5.get("purpose") != "AIOS_TRUSTED_RELEASE_BUILDER":
raise BuilderError("POLICY_V2R2C5_INVALID")
required_c5 = {"task_id", "authority_base_oid", "accepted_base_oid", "rollback_version", "rollback_target_oid", "rollback_artifact_sha256"}
if is_integration:
required_c5 |= C5_INTEGRATION_CONTRACT_FIELDS
value = policy.get("c5_contract")
if not isinstance(value, Mapping) or set(value) != required_c5:
raise BuilderError("POLICY_V2R2C5_CONTRACT_INVALID")
if any(not isinstance(v, str) for v in value.values()):
raise BuilderError("POLICY_V2R2C5_CONTRACT_INVALID")
c5_contract = dict(value)
oid_fields = {"authority_base_oid", "accepted_base_oid", "rollback_target_oid"}
if is_integration:
oid_fields |= {
"integration_source_commit", "integration_tree_oid",
"publication_source_commit", "publication_tree_oid",
"repair_source_commit", "repair_tree_oid",
}
sha_fields = {"rollback_artifact_sha256"}
if is_integration:
sha_fields.add("publication_plan_sha256")
if (
not c5_contract["task_id"]
or c5_contract["rollback_version"] != "V2R2C4"
or c5_contract["accepted_base_oid"] != c5_contract["rollback_target_oid"]
or any(len(c5_contract[name]) != 40 or any(ch not in "0123456789abcdef" for ch in c5_contract[name]) for name in oid_fields)
or any(len(c5_contract[name]) != 64 or any(ch not in "0123456789abcdef" for ch in c5_contract[name]) for name in sha_fields)
):
raise BuilderError("POLICY_V2R2C5_CONTRACT_INVALID")
if (
policy["repository"] != "aios/aios"
or policy["operation"] != "BuildTrustedCandidateV1"
@@ -210,6 +260,7 @@ class TrustedCandidateBuilderConfig:
recipes={name: dict(value) for name, value in recipes.items()},
governed_recipes={name: dict(value) for name, value in policy["governed_recipes"].items()} if isinstance(policy["governed_recipes"], Mapping) else _invalid_governed_recipes(),
policy_sha256=hashlib.sha256(_canonical_json(dict(policy))).hexdigest(),
c5_contract=c5_contract,
)
def fixed_recipe(self, name: str) -> Mapping[str, str]:
@@ -231,6 +282,36 @@ class TrustedCandidateBuilderConfig:
def resolve_recipe(self, version: str, candidate_id: str | None, source_commit: str) -> Mapping[str, str]:
recipe = self.recipes.get(version)
if recipe is not None:
if recipe.get("mode") in {"governed-source-admission-only", "governed-source-integration-only"}:
if version != "V2R2C5" or self.c5_contract is None:
raise BuilderError("SOURCE_ADMISSION_REQUIRED")
resolver = self.source_admission_resolver
if resolver is None:
raise BuilderError("SOURCE_ADMISSION_REQUIRED")
try:
admitted = dict(resolver(version, candidate_id, source_commit))
except Exception as exc:
raise BuilderError("SOURCE_ADMISSION_REQUIRED") from exc
required = {"mode", "task_id", "candidate_id", "base_oid", "source_commit", "version", "purpose", "tree_oid"}
if set(admitted) != required or admitted.get("mode") != "governed-source-admission" or admitted.get("version") != version or admitted.get("source_commit") != source_commit or admitted.get("purpose") != "AIOS_TRUSTED_RELEASE_BUILDER":
raise BuilderError("SOURCE_ADMISSION_INVALID")
if (not isinstance(admitted.get("candidate_id"), str) or not admitted.get("candidate_id")
or admitted.get("task_id") != self.c5_contract["task_id"]
or admitted.get("base_oid") != self.c5_contract["authority_base_oid"]):
raise BuilderError("SOURCE_ADMISSION_BINDING_INVALID")
effective = str(candidate_id or admitted["candidate_id"])
if effective != admitted["candidate_id"]:
raise BuilderError("REQUEST_RELEASE_BINDING_INVALID")
for name in ("base_oid", "source_commit", "tree_oid"):
value = admitted.get(name)
if not isinstance(value, str) or len(value) != 40 or any(char not in "0123456789abcdef" for char in value.lower()):
raise BuilderError("SOURCE_ADMISSION_INVALID")
if recipe.get("mode") == "governed-source-integration-only" and (
source_commit != self.c5_contract["integration_source_commit"]
or admitted["tree_oid"] != self.c5_contract["integration_tree_oid"]
):
raise BuilderError("SOURCE_INTEGRATION_BINDING_INVALID")
return {**admitted, **self.c5_contract}
resolved = self.fixed_recipe(version)
# Fixed/governed-derived policy recipes remain immutable. A
# source admission can never override one of these identities.
@@ -247,6 +328,8 @@ class TrustedCandidateBuilderConfig:
raise BuilderError("REQUEST_RELEASE_BINDING_INVALID")
return resolved
if self.c5_contract is not None:
raise BuilderError("RECIPE_UNKNOWN")
resolver = self.source_admission_resolver
if resolver is None:
raise BuilderError("RECIPE_UNKNOWN")
@@ -263,8 +346,9 @@ class TrustedCandidateBuilderConfig:
"version",
"purpose",
}
allowed_fields = (required, required | {"tree_oid"}) if version == "V2R2C5" else (required,)
if (
set(admitted) != required
set(admitted) not in allowed_fields
or admitted.get("mode") != "governed-source-admission"
or admitted.get("version") != version
or admitted.get("source_commit") != source_commit
@@ -275,7 +359,7 @@ class TrustedCandidateBuilderConfig:
effective_candidate_id = str(candidate_id or admitted["candidate_id"])
if effective_candidate_id != admitted["candidate_id"]:
raise BuilderError("REQUEST_RELEASE_BINDING_INVALID")
for name in ("base_oid", "source_commit"):
for name in ("base_oid", "source_commit", *(["tree_oid"] if "tree_oid" in admitted else [])):
value = admitted.get(name)
if (
not isinstance(value, str)
@@ -395,7 +479,44 @@ class ServerOwnedEvidenceSource:
raise BuilderError("COLLECTOR_EVIDENCE_INVALID")
return value
def _validate_integration_source(self, recipe: Mapping[str, str]) -> None:
if "integration_source_commit" not in recipe:
return
def read(*arguments: str, binary: bool = False):
try:
result = self._git_runner(
["git", f"--git-dir={self._authority_git_dir}", *arguments],
check=True, capture_output=True, text=not binary, timeout=15,
env={"PATH": os.environ.get("PATH", "")},
)
return result.stdout if binary else result.stdout.strip()
except (OSError, subprocess.SubprocessError) as exc:
raise BuilderError("COLLECTOR_INTEGRATION_UNAVAILABLE") from exc
source = recipe["source_commit"]
if source != recipe["integration_source_commit"]:
raise BuilderError("COLLECTOR_INTEGRATION_MISMATCH")
for commit_field, tree_field in (
("integration_source_commit", "integration_tree_oid"),
("publication_source_commit", "publication_tree_oid"),
("repair_source_commit", "repair_tree_oid"),
):
if read("rev-parse", f"{recipe[commit_field]}^{{tree}}") != recipe[tree_field]:
raise BuilderError("COLLECTOR_INTEGRATION_MISMATCH")
if read("show", "-s", "--format=%P", source) != recipe["authority_base_oid"]:
raise BuilderError("COLLECTOR_INTEGRATION_MISMATCH")
changed = read("diff", "--name-only", recipe["authority_base_oid"], source).splitlines()
if sorted(changed) != sorted(C5_INTEGRATION_FILES):
raise BuilderError("COLLECTOR_INTEGRATION_FILE_SET_MISMATCH")
for path, (component, digest) in C5_INTEGRATION_FILES.items():
integrated = read("show", f"{source}:{path}", binary=True)
original = read("show", f"{recipe[component]}:{path}", binary=True)
if integrated != original or hashlib.sha256(integrated).hexdigest() != digest:
raise BuilderError("COLLECTOR_INTEGRATION_BYTES_MISMATCH")
def __call__(self, recipe_name: str, recipe: Mapping[str, str]) -> Mapping[str, Any]:
self._validate_integration_source(recipe)
try:
tree_result = self._git_runner(
["git", f"--git-dir={self._authority_git_dir}", "rev-parse", f"{recipe['source_commit']}^{{tree}}"],
@@ -412,6 +533,16 @@ class ServerOwnedEvidenceSource:
raise BuilderError("COLLECTOR_EVIDENCE_UNAVAILABLE") from exc
if getattr(ancestry_result, "returncode", 1) != 0:
raise BuilderError("COLLECTOR_GIT_MISMATCH")
if recipe.get("tree_oid") is not None and recipe.get("tree_oid") != tree_oid:
raise BuilderError("COLLECTOR_GIT_MISMATCH")
if recipe.get("accepted_base_oid") is not None:
accepted_ancestry = self._git_runner(
["git", f"--git-dir={self._authority_git_dir}", "merge-base", "--is-ancestor", recipe["accepted_base_oid"], recipe["source_commit"]],
check=False, capture_output=True, text=True, timeout=15,
env={"PATH": os.environ.get("PATH", "")},
)
if getattr(accepted_ancestry, "returncode", 1) != 0:
raise BuilderError("COLLECTOR_GIT_MISMATCH")
receipt = self._json(self._regular_path(f"{recipe_name}/receipt.json"))
if set(receipt) != {"schema", "version", "candidate", "release_plan"}:
@@ -488,6 +619,13 @@ class ServerOwnedEvidenceSource:
or rollback.get("rollback_sha256") != deployment.get("rollback_sha256")
):
raise BuilderError("COLLECTOR_ROLLBACK_MISMATCH")
if recipe.get("rollback_version") is not None:
if deployment.get("rollback_target") != {
"version": recipe["rollback_version"],
"target_oid": recipe["rollback_target_oid"],
"artifact_sha256": recipe["rollback_artifact_sha256"],
}:
raise BuilderError("COLLECTOR_ROLLBACK_MISMATCH")
if (
digests["gitea"] != hashlib.sha256(_canonical_json(dict(gitea))).hexdigest()
or digests["rollback"] != hashlib.sha256(_canonical_json(dict(rollback))).hexdigest()
@@ -18,7 +18,10 @@ from __future__ import annotations
import argparse
import dataclasses
import fcntl
try:
import fcntl
except ModuleNotFoundError: # pragma: no cover - production host is POSIX
fcntl = None
import hashlib
import json
import os
@@ -376,6 +379,8 @@ class FileLock:
self.handle = None
def __enter__(self) -> "FileLock":
if fcntl is None:
raise ReleaseBlocked("production executor file locking requires POSIX")
self.path.parent.mkdir(parents=True, exist_ok=True)
self.handle = self.path.open("a+", encoding="utf-8")
os.chmod(self.path, 0o600)
@@ -388,7 +393,7 @@ class FileLock:
return self
def __exit__(self, exc_type, exc, tb) -> None:
if self.handle is not None:
if self.handle is not None and fcntl is not None:
fcntl.flock(self.handle.fileno(), fcntl.LOCK_UN)
self.handle.close()
+74
View File
@@ -0,0 +1,74 @@
"""Fixed registered composition of the existing shadow API and renewal store."""
import base64
import importlib.util
import json
from pathlib import Path
import runpy
import sys
ORIGINAL_SHA = 'f6393fe614239036a1d8235c98c1f455dc32b700107d88f48190c4f61dae617e'
def service_type(types, renewal, public_key):
class RegisteredService(types.SilentReleaseActionService):
def __init__(self, root, **kwargs):
if 'completion_approvals' in kwargs:
raise renewal.RenewalRejected('FIXED_COMPLETION_DEPENDENCY_REQUIRED')
original = types.SignedApprovalStore(Path(root) / 'approvals')
completion = renewal.RegisteredCompletionApprovalStore(
original, public_key, types.SignedReleasePlanApproval,
)
super().__init__(root, completion_approvals=completion, **kwargs)
return RegisteredService
def main():
# Original Round3 startup and verifier remain loaded before the new root
# is added. The C5 types are a separate registered module as in R2.
import silent_release_hardening as original
import hashlib
if hashlib.sha256(Path(original.__file__).read_bytes()).hexdigest() != ORIGINAL_SHA:
raise RuntimeError('ROUND3_HARDENING_DRIFT')
if not getattr(original.BuilderAttestationVerifier.verify,
'__aios_round3_rollback_contract_patch__', False):
raise RuntimeError('ROUND3_VERIFIER_NOT_ACTIVE')
import c5_completion_renewal as renewal
renewal.configure_registered_bindings()
root = renewal.AUTHORITY_ROOT
sys.path.insert(0, str(root))
if Path(__file__) != root / 'c5_completion_api.py':
raise renewal.RenewalRejected('REGISTERED_API_WRAPPER_REQUIRED')
policy = renewal.RegisteredCompletionAuthority()()
parent = json.loads(renewal.secure_bytes(Path(policy['predecessor_snapshot'])))
keyring = json.loads(renewal.secure_bytes(root / 'approval-public-keyring.json'))
if keyring.get('algorithm') != 'Ed25519' or parent['key_id'] in keyring.get('revoked', []):
raise renewal.RenewalRejected('REGISTERED_APPROVAL_KEY_REQUIRED')
public_key = base64.b64decode(keyring['keys'][parent['key_id']], validate=True)
renewal.verify_signature(parent, public_key)
spec = importlib.util.spec_from_file_location('_registered_c5_completion_types',
root / 'silent_release_hardening.py')
types = importlib.util.module_from_spec(spec)
sys.modules[spec.name] = types
spec.loader.exec_module(types)
loaded = runpy.run_path(str(root / 'original_api.py'), run_name='_registered_original_api')
g = loaded['main'].__globals__
if g['SILENT_RELEASE_EXECUTION_ENABLED'] is not False or Path(g['SILENT_RELEASE_STATE_ROOT']) != renewal.STATE_ROOT:
raise renewal.RenewalRejected('EXISTING_SHADOW_PLANE_REQUIRED')
g['ReleasePlan'] = types.ReleasePlan
g['TrustedCandidateBundle'] = types.TrustedCandidateBundle
g['SilentReleaseActionService'] = service_type(types, renewal, public_key)
g['TRUSTED_RELEASE_POLICY'] = renewal.TRUSTED_RELEASE_POLICY_PATH
service = g['_build_silent_release_service']()
if service.execution_enabled or service.policy_sha256 != renewal.TRUSTED_RELEASE_POLICY_SHA256:
raise renewal.RenewalRejected('EXACT_API_POLICY_REQUIRED')
# Preserve the exact candidate boundary from the existing registered R2.
def reject_candidate_creation(payload):
raise original.CapabilityAuthorizationError('completion mode cannot create or replace candidates')
service.create_release_candidate = reject_candidate_creation
g['_SILENT_RELEASE_SERVICE'] = service
g['main']()
return 0
if __name__ == '__main__':
raise SystemExit(main())
+100
View File
@@ -0,0 +1,100 @@
"""Completion-only fixed tag and metadata adapter launcher, no runtime effects."""
import argparse
import base64
import io
import json
import os
from pathlib import Path
import runpy
import sys
import urllib.request
UNIT = 'aios-c5-exact-production-host-r1.service'
def main():
parser = argparse.ArgumentParser()
parser.add_argument('adapter')
parser.add_argument('mode', choices=('tag', 'registry', 'baseline'))
args = parser.parse_args()
import c5_completion_renewal as renewal
renewal.configure_registered_bindings()
root = renewal.AUTHORITY_ROOT
sys.path.insert(0, str(root))
from silent_release_hardening import SignedApprovalStore, SignedReleasePlanApproval
if Path(__file__) != root / 'c5_completion_phase.py' or os.geteuid() != 0:
raise renewal.RenewalRejected('REGISTERED_COMPLETION_PHASE_REQUIRED')
policy = renewal.RegisteredCompletionAuthority()()
credentials = Path(os.environ.get('CREDENTIALS_DIRECTORY', ''))
if credentials != Path('/run/credentials') / UNIT:
raise renewal.RenewalRejected('FIXED_COMPLETION_PURPOSE_UNIT_REQUIRED')
if not any(line.split(':', 2)[-1].rstrip('/').endswith('/' + UNIT)
for line in Path('/proc/self/cgroup').read_text().splitlines()):
raise renewal.RenewalRejected('FIXED_COMPLETION_CGROUP_REQUIRED')
module = 'governed_tag_adapter.py' if args.mode == 'tag' else 'release_metadata_adapter.py'
if Path(args.adapter) != ROOT / module:
raise renewal.RenewalRejected('FIXED_COMPLETION_ADAPTER_REQUIRED')
value = json.load(sys.stdin)
fixed = {'schema': 'aios.production-executor-request.v1', 'version': 'V2R2C5',
'target_oid': renewal.SOURCE, 'evidence_closure_oid': renewal.SOURCE,
'artifact_sha256': renewal.ARTIFACT,
'approval_plan_id': 'sha256:d8f64b8b92f9fdb17f5c6053b76a68ee5bcbc2f266d91438c4db7893463079f8',
'repository_identity': 'aios/aios', 'production_service': 'aios-v2r2c2-production.service',
'production_port': 18928}
if args.mode != 'tag':
fixed['tag_oid'] = renewal.TAG_OID
if (set(value) != set(fixed) | {'operation'} or value['operation'] not in {'probe', 'apply'}
or any(value[k] != v for k, v in fixed.items())):
raise renewal.RenewalRejected('EXACT_COMPLETION_REQUEST_REQUIRED')
parent = json.loads(renewal.secure_bytes(Path(policy['predecessor_snapshot'])))
ring = json.loads(renewal.secure_bytes(root / 'approval-public-keyring.json'))
if parent['key_id'] in ring.get('revoked', []):
raise renewal.RenewalRejected('APPROVAL_KEY_REVOKED')
public_key = base64.b64decode(ring['keys'][parent['key_id']], validate=True)
approval = renewal.RegisteredCompletionApprovalStore(
SignedApprovalStore(renewal.STATE_ROOT / 'approvals'), public_key, SignedReleasePlanApproval,
).load(renewal.CID)
if approval.fingerprint == renewal.OLD_FP:
raise renewal.RenewalRejected('FRESH_COMPLETION_SUCCESSOR_REQUIRED')
# Loading the store verifies both actual signatures, registered fixed nonce,
# append-only chain and current successor lifetime. The historical proof
# intentionally remains bound to the original approval, never rewritten.
runtime = json.load(urllib.request.urlopen('http://127.0.0.1:18928/health', timeout=10))
if any(runtime.get(k) != v for k, v in {
'version': 'V2R2C5', 'commit_oid': renewal.SOURCE, 'artifact_sha256': renewal.ARTIFACT,
'deployment_state': 'ACTIVE', 'production_deployed': True}.items()):
raise renewal.RenewalRejected('CURRENT_C5_RUNTIME_REQUIRED')
# Exact signed copies of these historical helpers are read-only here.
# Never call their activation, run(), or verification-phase effect methods.
import production_verification_phase_r1 as business
import public_business_acceptance_probe_r1 as public
business.RegisteredVerificationPhase.business_proof(None, True)
public.reconcile()
os.environ.clear()
os.environ.update(PATH='/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin',
PYTHONDONTWRITEBYTECODE='1', AIOS_GITOPS_WRITES_ENABLED='false',
AIOS_GITOPS_LEGACY_GLOBAL_WRITES_ENABLED='false',
AIOS_GITOPS_CONTROLLER_WORKSPACE=str(renewal.CONTROLLER_WORKSPACE),
AIOS_GITOPS_AUTHORITATIVE_GIT_DIR=str(renewal.AUTHORITATIVE_GIT_DIR),
AIOS_GITOPS_REPOSITORY_IDENTITY='aios/aios',
AIOS_RUNTIME_REGISTRY_ROOT=str(renewal.RUNTIME_REGISTRY_ROOT),
AIOS_RUNTIME_BASELINE_PATH=str(renewal.RUNTIME_BASELINE_PATH))
if args.mode == 'tag':
os.environ.update(GIT_COMMITTER_NAME='owner', GIT_COMMITTER_EMAIL='owner@cnnetmkt.top',
GIT_COMMITTER_DATE='1790826063 +0000', GIT_TERMINAL_PROMPT='0',
GIT_ASKPASS=str(root / 'publisher-askpass.py'))
sys.argv = [module, '--workspace', str(renewal.CONTROLLER_WORKSPACE),
'--repository', str(renewal.AUTHORITATIVE_GIT_DIR), '--remote', 'gitea-http',
'--gitea-api', 'http://127.0.0.1:3000/api/v1', '--gitea-repo', 'aios/aios',
'--gitea-token-file', str(credentials / 'gitea-release-publisher'),
'--state-root', str(renewal.HISTORICAL_ROOT / 'tag-publication')]
else:
sys.argv = [module, '--mode', args.mode, '--expected-previous-baseline', 'V2R2C4',
'--expected-previous-baseline-sha256', 'de0c7008facc44c5061239ee490135b58e521989952918f84a57cecb3358483c',
'--health-url', 'http://127.0.0.1:18928/health']
sys.stdin = io.StringIO(json.dumps(value))
runpy.run_path(str(root / module), run_name='__main__')
if __name__ == '__main__':
main()
+630
View File
@@ -0,0 +1,630 @@
"""Append-only C5 completion renewal boundary; activation requires registration.
The exact Owner-registered API, signer and completion host must call this
boundary. This module does not grant authority or perform deployment effects.
"""
import base64
import hashlib
import json
import math
import os
import stat
import time
from datetime import datetime, timezone
from pathlib import Path
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PublicKey
from cryptography.hazmat.primitives.serialization import Encoding, PublicFormat
CID = 'rc-v2r2c5-5f65e458957d'
# Immutable release content remains compiled into the validator. Renewable
# authority identities and filesystem locations are installed only from the
# digest-bound, root-owned registration file (see configure_registered_bindings).
OLD_FP = '0' * 64
SOURCE = '5f65e458957dd51c9973cfa956a624f3cbf54990'
TREE = 'ddd239d43d63a7fbd3cff46119c71466c9443737'
ARTIFACT = '7f9d8409f07a5e0d36f5db7c55eb82ea70935fa731e057998f29cd1a5ca0a9ad'
PLAN = '370521afcf9b2288f6ad3803ca3554a46ee05d2042d53b19b4f4cb80e37a9684'
MUTABLE = {'issued_at', 'not_before', 'expires_at', 'nonce', 'fingerprint', 'signature'}
CAPABILITIES = ['baseline_registry_finalize', 'runtime_switch', 'tag_publish']
AUTHORITY_ROOT = Path('/unconfigured/c5-completion-authority')
STATE_ROOT = Path('/unconfigured/c5-completion-state')
OWNER_KEY = 'UNCONFIGURED'
TAG_OID = '6d1745ff496103db7e721a2b2bc624fda52c5dc8'
SAGA_HEAD = '0' * 64
HISTORICAL_ROOT = Path('/unconfigured/c5-completion-history')
CONTROLLER_WORKSPACE = Path('/unconfigured/c5-controller-workspace')
AUTHORITATIVE_GIT_DIR = Path('/unconfigured/c5-authoritative-git')
RUNTIME_REGISTRY_ROOT = Path('/unconfigured/c5-runtime-registry')
RUNTIME_BASELINE_PATH = Path('/unconfigured/c5-runtime-baseline.json')
TRUSTED_RELEASE_POLICY_PATH = Path('/unconfigured/c5-trusted-release-policy.json')
TRUSTED_RELEASE_POLICY_SHA256 = '0' * 64
BINDINGS_PATH_ENV = 'AIOS_C5_COMPLETION_BINDINGS_PATH'
BINDINGS_SHA256_ENV = 'AIOS_C5_COMPLETION_BINDINGS_SHA256'
_BINDINGS_DIGEST = None
RENEWAL_SOURCE_FILES = ('c5_completion_renewal.py', 'c5_release_execution_host.py',
'silent_release_hardening.py', 'c5_completion_signer.py',
'c5_completion_api.py', 'c5_completion_phase.py') # gitleaks:allow -- filenames only
PHASE_DEPENDENCIES = ('governed_tag_adapter.py', 'release_metadata_adapter.py',
'production_verification_phase_r1.py', 'public_business_acceptance_probe_r1.py',
'publisher-askpass.py', 'c5_staging_installation.py',
'aios_single_host_authority.py', 'registered_owner_signer_adapter.py')
class RenewalRejected(ValueError):
pass
def canonical(value):
return json.dumps(value, sort_keys=True, separators=(',', ':'), allow_nan=False).encode()
def digest(raw):
return hashlib.sha256(raw).hexdigest()
def validate_completion_lineage(gate):
"""Bind a current completion-only journal suffix to the frozen Saga15 head.
The suffix is authenticated by the enclosing registered Owner contract;
these checks preserve its origin and exclude deployment/source transitions.
"""
revision = gate.get('saga_revision')
suffix = gate.get('completion_journal_suffix', [])
observed = gate.get('observed_at')
if (type(revision) is not int or revision < 15 or not isinstance(suffix, list)
or type(observed) not in {int, float} or not math.isfinite(observed)):
raise RenewalRejected('COMPLETION_LINEAGE_INVALID')
if revision == 15:
if gate.get('saga_head') != SAGA_HEAD or suffix:
raise RenewalRejected('COMPLETION_LINEAGE_INVALID')
return
if not 0 < len(suffix) <= 64 or len(suffix) != revision - 15:
raise RenewalRejected('COMPLETION_LINEAGE_INVALID')
allowed = {
'owner_registered_completion_successor_tag_reconciliation': ('TAG_PENDING', {'RECONCILIATION_REQUIRED'}),
'owner_registered_tag_capability_successor_verified': ('TAG_VERIFIED', {'RECONCILIATION_REQUIRED', 'TAG_PENDING'}),
'baseline_registry_finalization_pending': ('BASELINE_REGISTRY_PENDING', {'TAG_VERIFIED'}),
'owner_registered_completion_successor_metadata_reconciliation': ('BASELINE_REGISTRY_PENDING', {'RECONCILIATION_REQUIRED'}),
'release_reconciliation_required': ('RECONCILIATION_REQUIRED', {'TAG_PENDING', 'TAG_VERIFIED', 'BASELINE_REGISTRY_PENDING'}),
}
previous, state = SAGA_HEAD, 'RECONCILIATION_REQUIRED'
for offset, event in enumerate(suffix, 16):
if (not isinstance(event, dict)
or set(event) != {'schema_version', 'sequence', 'previous_hash', 'payload', 'event_hash'}
or type(event['schema_version']) is not int or event['schema_version'] != 1
or type(event['sequence']) is not int or event['sequence'] != offset + 1
or event['previous_hash'] != previous):
raise RenewalRejected('COMPLETION_LINEAGE_INVALID')
payload = event['payload']
if (not isinstance(payload, dict)
or set(payload) != {'candidate_id', 'release_plan_id', 'revision', 'state', 'event', 'timestamp', 'details'}
or payload['candidate_id'] != CID
or payload['release_plan_id'] != 'pending-' + CID
or type(payload['revision']) is not int or payload['revision'] != offset
or not isinstance(payload['details'], dict)
or not isinstance(payload['event'], str) or payload['event'] not in allowed):
raise RenewalRejected('COMPLETION_LINEAGE_INVALID')
target, origins = allowed[payload['event']]
if payload['state'] != target or state not in origins:
raise RenewalRejected('COMPLETION_LINEAGE_INVALID')
if ('release_plan_sha256' in payload['details']
and payload['details']['release_plan_sha256'] != PLAN):
raise RenewalRejected('COMPLETION_LINEAGE_INVALID')
try:
if timestamp(payload['timestamp']) > gate['observed_at']:
raise RenewalRejected('COMPLETION_LINEAGE_INVALID')
actual = digest(canonical({key: event[key] for key in ('sequence', 'previous_hash', 'payload')}))
except (TypeError, ValueError, AttributeError, KeyError) as exc:
raise RenewalRejected('COMPLETION_LINEAGE_INVALID') from exc
if event['event_hash'] != actual:
raise RenewalRejected('COMPLETION_LINEAGE_INVALID')
previous, state = actual, target
if gate.get('saga_head') != previous:
raise RenewalRejected('COMPLETION_LINEAGE_INVALID')
def timestamp(value):
parsed = datetime.fromisoformat(value.replace('Z', '+00:00'))
if parsed.tzinfo is None or parsed.utcoffset().total_seconds() != 0:
raise RenewalRejected('UTC_REQUIRED')
return parsed.timestamp()
def verify_signature(approval, public_key):
payload = {k: v for k, v in approval.items() if k not in {'signature', 'fingerprint'}}
raw = canonical(payload)
if approval.get('fingerprint') != digest(raw):
raise RenewalRejected('APPROVAL_DIGEST_INVALID')
Ed25519PublicKey.from_public_bytes(public_key).verify(
base64.b64decode(approval['signature'], validate=True), raw
)
def validate_successor(old, new, public_key, *, now):
"""Validate historical predecessor and fresh identical-binding successor.
Historical signature verification does not authorize an expired effect.
The caller must separately verify its fresh registered Owner capability.
"""
verify_signature(old, public_key)
verify_signature(new, public_key)
exact = {'candidate_id': CID, 'source_commit': SOURCE, 'tree_oid': TREE,
'artifact_sha256': ARTIFACT, 'release_plan_sha256': PLAN,
'fingerprint': OLD_FP, 'capabilities': CAPABILITIES}
if any(old.get(k) != v for k, v in exact.items()):
raise RenewalRejected('EXACT_PREDECESSOR_REQUIRED')
if set(old) != set(new) or any(new[k] != old[k] for k in set(old) - MUTABLE):
raise RenewalRejected('IMMUTABLE_APPROVAL_BINDING_DRIFT')
if timestamp(old['expires_at']) > now:
raise RenewalRejected('PREDECESSOR_NOT_EXPIRED')
issued, not_before, expires = (timestamp(new[k]) for k in ['issued_at', 'not_before', 'expires_at'])
if not issued <= not_before <= now < expires or not 0 < expires - issued <= 900:
raise RenewalRejected('FRESH_BOUNDED_APPROVAL_REQUIRED')
if new['nonce'] == old['nonce'] or new['fingerprint'] == old['fingerprint']:
raise RenewalRejected('FRESH_APPROVAL_IDENTITY_REQUIRED')
return new['fingerprint']
def secure_bytes(path, owner_uid=0):
path = Path(path)
descriptor = os.open(path, os.O_RDONLY | os.O_NOFOLLOW)
with os.fdopen(descriptor, 'rb') as stream:
metadata = os.fstat(stream.fileno())
if not stat.S_ISREG(metadata.st_mode) or metadata.st_uid != owner_uid or metadata.st_mode & 0o022:
raise RenewalRejected('UNSAFE_RENEWAL_FILE')
return stream.read()
def _safe_parents(path):
"""Reject writable or symlinked ancestors before opening authority files."""
path = Path(path)
if not path.is_absolute() or '..' in path.parts:
raise RenewalRejected('ABSOLUTE_CANONICAL_PATH_REQUIRED')
for parent in path.parents:
info = parent.lstat()
if not stat.S_ISDIR(info.st_mode) or info.st_uid != 0 or info.st_mode & 0o022:
raise RenewalRejected('UNSAFE_AUTHORITY_ANCESTOR')
def _sha(value):
return isinstance(value, str) and len(value) == 64 and all(c in '0123456789abcdef' for c in value)
def _oid(value):
return isinstance(value, str) and len(value) == 40 and all(c in '0123456789abcdef' for c in value)
def configure_registered_bindings(path=None, expected_sha256=None, *, owner_uid=0):
"""Install renewable completion bindings from one authenticated file.
No historical approval fingerprint, Saga head, Owner key, or authority/state
path is a runtime default. The service registration must provide both the
absolute file path and its SHA-256 digest.
"""
global _BINDINGS_DIGEST
path = Path(path or os.environ.get(BINDINGS_PATH_ENV, ''))
expected_sha256 = expected_sha256 or os.environ.get(BINDINGS_SHA256_ENV, '')
if not path.is_absolute() or not _sha(expected_sha256):
raise RenewalRejected('REGISTERED_COMPLETION_BINDINGS_REQUIRED')
_safe_parents(path)
raw = secure_bytes(path, owner_uid=owner_uid)
if digest(raw) != expected_sha256:
raise RenewalRejected('COMPLETION_BINDINGS_DIGEST_MISMATCH')
if _BINDINGS_DIGEST is not None and _BINDINGS_DIGEST != expected_sha256:
raise RenewalRejected('COMPLETION_BINDINGS_REBIND_FORBIDDEN')
try:
value = json.loads(raw)
except (UnicodeDecodeError, json.JSONDecodeError) as exc:
raise RenewalRejected('COMPLETION_BINDINGS_INVALID') from exc
fields = {
'schema', 'authority_root', 'state_root', 'owner_key_id',
'predecessor_fingerprint', 'saga_head', 'historical_root',
'controller_workspace', 'authoritative_git_dir',
'runtime_registry_root', 'runtime_baseline_path',
'trusted_release_policy_path', 'trusted_release_policy_sha256',
}
if not isinstance(value, dict) or set(value) != fields or value.get('schema') != 'aios.c5.completion-bindings.v1':
raise RenewalRejected('COMPLETION_BINDINGS_SCHEMA_INVALID')
authority_root = Path(value['authority_root'])
state_root = Path(value['state_root'])
governed_paths = {
name: Path(value[name]) for name in (
'historical_root', 'controller_workspace', 'authoritative_git_dir',
'runtime_registry_root', 'runtime_baseline_path', 'trusted_release_policy_path',
)
}
owner_key_id = value['owner_key_id']
try:
owner_key_valid = (isinstance(owner_key_id, str)
and owner_key_id.startswith('SHA256:')
and len(base64.b64decode(owner_key_id[7:], validate=True)) == 32)
except (ValueError, TypeError):
owner_key_valid = False
all_paths = (authority_root, state_root, *governed_paths.values())
if (any(not item.is_absolute() or '..' in item.parts for item in all_paths)
or not _sha(value['predecessor_fingerprint']) or not _sha(value['saga_head'])
or not _sha(value['trusted_release_policy_sha256'])
or not owner_key_valid):
raise RenewalRejected('COMPLETION_BINDINGS_VALUE_INVALID')
if path.parent != authority_root:
raise RenewalRejected('COMPLETION_BINDINGS_ROOT_MISMATCH')
global AUTHORITY_ROOT, STATE_ROOT, OWNER_KEY, OLD_FP, SAGA_HEAD
global HISTORICAL_ROOT, CONTROLLER_WORKSPACE, AUTHORITATIVE_GIT_DIR
global RUNTIME_REGISTRY_ROOT, RUNTIME_BASELINE_PATH
global TRUSTED_RELEASE_POLICY_PATH, TRUSTED_RELEASE_POLICY_SHA256
AUTHORITY_ROOT = authority_root
STATE_ROOT = state_root
OWNER_KEY = value['owner_key_id']
OLD_FP = value['predecessor_fingerprint']
SAGA_HEAD = value['saga_head']
HISTORICAL_ROOT = governed_paths['historical_root']
CONTROLLER_WORKSPACE = governed_paths['controller_workspace']
AUTHORITATIVE_GIT_DIR = governed_paths['authoritative_git_dir']
RUNTIME_REGISTRY_ROOT = governed_paths['runtime_registry_root']
RUNTIME_BASELINE_PATH = governed_paths['runtime_baseline_path']
TRUSTED_RELEASE_POLICY_PATH = governed_paths['trusted_release_policy_path']
TRUSTED_RELEASE_POLICY_SHA256 = value['trusted_release_policy_sha256']
_BINDINGS_DIGEST = expected_sha256
return dict(value)
class RegisteredCompletionAuthority:
"""Fixed root registration, Owner signature and immutable evidence verifier.
The completed production proofs are historical facts. This fresh authority
permits only approval renewal and remaining metadata effects; it cannot
authorize another runtime switch or public acceptance Task.
"""
@property
def root(self):
return AUTHORITY_ROOT
@property
def state_root(self):
return STATE_ROOT
def __call__(self):
from c5_staging_installation import RegistryOwnerVerifier
root = AUTHORITY_ROOT
_safe_parents(root / 'contract.json')
envelope = json.loads(secure_bytes(root / 'contract.json'))
if set(envelope) != {'payload', 'signature'}:
raise RenewalRejected('EXACT_AUTHORITY_ENVELOPE_REQUIRED')
p = envelope['payload']
fields = {'schema', 'host', 'repository', 'candidate_id', 'source_commit', 'tree_oid',
'artifact_sha256', 'release_plan_sha256', 'state_root', 'predecessor_fingerprint',
'predecessor_sha256', 'predecessor_uid', 'predecessor_snapshot', 'approval_nonce',
'issued_at', 'expires_at', 'owner_key_id', 'owner_namespace', 'allowed_effects',
'runtime_effects_allowed', 'files', 'gate_path', 'gate_observed_at',
'governed_source_commit', 'governed_source_tree'}
if not isinstance(p, dict) or set(p) != fields:
raise RenewalRejected('EXACT_AUTHORITY_SCHEMA_REQUIRED')
exact = {'schema': 'aios.c5.registered-completion-authority.v1', 'host': 'cn-network-mkt',
'repository': 'aios/aios', 'candidate_id': CID, 'source_commit': SOURCE,
'tree_oid': TREE, 'artifact_sha256': ARTIFACT, 'release_plan_sha256': PLAN,
'state_root': str(STATE_ROOT), 'predecessor_fingerprint': OLD_FP,
'owner_key_id': OWNER_KEY, 'owner_namespace': 'aios-owner-approval-v1',
'allowed_effects': ['approval_successor_append', 'baseline_finalize', 'registry_finalize', 'tag_publish'],
'runtime_effects_allowed': False}
if any(p[k] != v for k, v in exact.items()) or type(p['runtime_effects_allowed']) is not bool:
raise RenewalRejected('EXACT_COMPLETION_AUTHORITY_REQUIRED')
now = time.time()
if (type(p['issued_at']) is not int or type(p['expires_at']) is not int
or not p['issued_at'] <= now < p['expires_at']
or not 0 < p['expires_at'] - p['issued_at'] <= 1800
or type(p['gate_observed_at']) not in {int, float}
or not math.isfinite(p['gate_observed_at'])
or not p['issued_at'] - 60 <= p['gate_observed_at'] <= p['issued_at']):
raise RenewalRejected('FRESH_OWNER_AUTHORITY_REQUIRED')
if (not _sha(p['approval_nonce']) or not _sha(p['predecessor_sha256'])
or type(p['predecessor_uid']) is not int or p['predecessor_uid'] not in {0, 1000}
or any(not isinstance(p[k], str) or len(p[k]) != 40
or any(c not in '0123456789abcdef' for c in p[k])
for k in ('governed_source_commit', 'governed_source_tree'))):
raise RenewalRejected('AUTHORITY_IDENTITIES_INVALID')
verifier = RegistryOwnerVerifier(Path('/etc/aios/single-host-signer-registry.json'),
owner_signer_id='local-owner-authority')
if not verifier.verify(namespace=p['owner_namespace'], payload=canonical(p),
signature=envelope['signature'], key_id=p['owner_key_id']):
raise RenewalRejected('OWNER_SIGNATURE_REJECTED')
payload_sha = digest(canonical(p))
registered = json.loads(secure_bytes(root / 'registration.json'))
if (set(registered) != {'schema', 'status', 'payload_sha256', 'approval_nonce'}
or registered != {'schema': 'aios.c5.completion-registration.v1', 'status': 'REGISTERED',
'payload_sha256': payload_sha, 'approval_nonce': p['approval_nonce']}):
raise RenewalRejected('ROOT_REGISTRATION_REQUIRED')
if (root / 'rollback.json').exists():
raise RenewalRejected('REGISTRATION_ROLLED_BACK')
pins = p['files']
if not isinstance(pins, list) or not pins:
raise RenewalRejected('SIGNED_FILE_CLOSURE_REQUIRED')
checked = {}
for item in pins:
if (not isinstance(item, dict) or set(item) != {'path', 'sha256', 'uid'}
or not _sha(item['sha256']) or type(item['uid']) is not int or item['uid'] != 0):
raise RenewalRejected('SIGNED_FILE_PIN_INVALID')
path = Path(item['path'])
if path.parent != root or str(path) in checked:
raise RenewalRejected('SIGNED_FILE_PATH_INVALID')
_safe_parents(path)
raw = secure_bytes(path)
if digest(raw) != item['sha256']:
raise RenewalRejected('SIGNED_FILE_BYTES_DRIFT')
checked[str(path)] = raw
required = {str(root / name) for name in (*RENEWAL_SOURCE_FILES, *PHASE_DEPENDENCIES, 'source-material.json',
'historical-production-proof.json', 'predecessor-approval.json', 'current-gate.json',
'approval-public-keyring.json', 'original_api.py')}
if not required <= set(checked):
raise RenewalRejected('INCOMPLETE_REGISTERED_CLOSURE')
if Path(__file__) != root / 'c5_completion_renewal.py':
raise RenewalRejected('UNREGISTERED_AUTHORITY_MODULE')
if (p['predecessor_snapshot'] != str(root / 'predecessor-approval.json')
or p['gate_path'] != str(root / 'current-gate.json')
or digest(checked[p['predecessor_snapshot']]) != p['predecessor_sha256']):
raise RenewalRejected('REGISTERED_SNAPSHOT_DRIFT')
material = json.loads(checked[str(root / 'source-material.json')])
if (material.get('status') != 'MATERIALIZED' or material.get('task_status') != 'SUCCEEDED'
or material.get('remote_verified') is not True
or material.get('source_commit') != p['governed_source_commit']
or material.get('source_tree') != p['governed_source_tree']
or material.get('file_sha256') != {
name: digest(checked[str(root / name)])
for name in RENEWAL_SOURCE_FILES}):
raise RenewalRejected('GOVERNED_CODE_PROVENANCE_REQUIRED')
historical = json.loads(checked[str(root / 'historical-production-proof.json')])
if (historical.get('candidate_id') != CID or historical.get('approval_fingerprint') != OLD_FP
or historical.get('release_plan_sha256') != PLAN
or historical.get('status') != 'COMPLETED'
or any(historical.get(k) is not True for k in (
'public_business_verified', 'real_c4_rollback_verified', 'c5_restore_verified'))):
raise RenewalRejected('COMPLETED_PRODUCTION_PROOF_REQUIRED')
gate = json.loads(checked[p['gate_path']])
expected_gate = {'candidate_id': CID, 'source_commit': SOURCE, 'artifact_sha256': ARTIFACT,
'release_plan_sha256': PLAN,
'local_tag_oid': TAG_OID, 'runtime_verified': True, 'business_verified': True,
'global_writes_enabled': False, 'observed_at': p['gate_observed_at']}
if (any(gate.get(k) != v for k, v in expected_gate.items())
or any(type(gate.get(k)) is not bool for k in (
'runtime_verified', 'business_verified', 'global_writes_enabled'))
or gate.get('remote_tag_oid') not in {None, TAG_OID}):
raise RenewalRejected('EXACT_POSTVERIFICATION_GATE_REQUIRED')
validate_completion_lineage(gate)
return {**p, 'payload_sha256': payload_sha}
def validate_host(self, *, state_root, candidate_id, source_commit, tree_oid,
artifact_sha256, release_plan_sha256):
self()
if (Path(state_root), candidate_id, source_commit, tree_oid, artifact_sha256, release_plan_sha256) != (
STATE_ROOT, CID, SOURCE, TREE, ARTIFACT, PLAN):
raise RenewalRejected('COMPLETION_HOST_BINDING_DRIFT')
def deny_runtime_effect(self, **kwargs):
self()
raise RenewalRejected('COMPLETION_RUNTIME_EFFECT_FORBIDDEN')
class CompletionRuntimeGuard:
def __init__(self, authority):
if type(authority) is not RegisteredCompletionAuthority:
raise RenewalRejected('REGISTERED_COMPLETION_AUTHORITY_REQUIRED')
self.authority = authority
def execute(self, **kwargs):
return self.authority.deny_runtime_effect(**kwargs)
def append_file(path, raw):
"""An existing record must be identical; it is never replaced."""
path = Path(path)
try:
fd = os.open(path, os.O_WRONLY | os.O_CREAT | os.O_EXCL | os.O_NOFOLLOW, 0o600)
except FileExistsError:
if secure_bytes(path, owner_uid=os.geteuid()) != raw:
raise RenewalRejected('APPEND_ONLY_RECORD_CONFLICT')
return False
with os.fdopen(fd, 'wb') as stream:
stream.write(raw)
stream.flush()
os.fsync(stream.fileno())
fd = os.open(path.parent, os.O_DIRECTORY)
try:
os.fsync(fd)
finally:
os.close(fd)
return True
def publish_successor(directory, old, new, public_key, *, now, registration_digest, predecessor_bytes):
"""Append records inside the SAME plane under a verified caller lock.
Registration validation, current Saga CAS, production/business readbacks,
and permission to call this function remain mandatory caller gates.
"""
fp = validate_successor(old, new, public_key, now=now)
if json.loads(predecessor_bytes) != old:
raise RenewalRejected('PREDECESSOR_BYTES_DRIFT')
predecessor_bytes_digest = digest(predecessor_bytes)
if len(registration_digest) != 64 or any(c not in '0123456789abcdef' for c in registration_digest):
raise RenewalRejected('REGISTERED_DIGEST_REQUIRED')
directory = Path(directory)
metadata = directory.lstat()
if not stat.S_ISDIR(metadata.st_mode) or metadata.st_uid != os.geteuid() or metadata.st_mode & 0o022:
raise RenewalRejected('UNSAFE_APPEND_DIRECTORY')
receipt = {'schema': 'aios.c5.completion-approval-successor.v1', 'candidate_id': CID,
'predecessor_fingerprint': OLD_FP, 'predecessor_bytes_sha256': predecessor_bytes_digest,
'successor_fingerprint': fp, 'successor_sha256': digest(canonical(new)),
'owner_registration_payload_sha256': registration_digest,
'completion_only': True, 'runtime_effects_allowed': False}
# A crash after the first append is unresolved until its exact receipt is
# reconciled. A loader must require BOTH files; no mutable current pointer.
append_file(directory / (fp + '.approval.json'), canonical(new))
append_file(directory / (fp + '.receipt.json'), canonical(receipt))
return receipt
class CompletionApprovalStore:
"""Explicit store dependency for a registered completion host/API.
The original store and original candidate approval remain untouched.
No global class replacement or expired approval verification is used.
An authenticated registration fixes ONE successor nonce and immutable
receipt path before signing. The host must separately enforce TAG_PENDING
and forbid runtime effects; this store does not execute a Saga.
"""
def __init__(self, original, directory, public_key, authority, approval_type):
self.original = original
self.directory = Path(directory)
self.public_key = public_key
self.authority = authority
self.approval_type = approval_type
def _policy(self):
# authority must be the fixed registered verifier, not a caller-supplied
# HTTP callback. Wiring it is outside this unregistered draft.
policy = self.authority()
if policy.get('candidate_id') != CID or policy.get('predecessor_fingerprint') != OLD_FP:
raise RenewalRejected('REGISTERED_EXACT_POLICY_REQUIRED')
nonce = policy.get('approval_nonce', '')
if not isinstance(nonce, str) or len(nonce) != 64 or any(c not in '0123456789abcdef' for c in nonce):
raise RenewalRejected('REGISTERED_NONCE_REQUIRED')
return policy, self.directory / (nonce + '.activation.json')
def _parent(self):
old = self.original.load(CID)
policy, _ = self._policy()
owner = policy.get('predecessor_uid')
if type(owner) is not int or owner not in {0, 1000}:
raise RenewalRejected('REGISTERED_PREDECESSOR_OWNER_REQUIRED')
raw = secure_bytes(self.original.root / (CID + '.json'), owner_uid=owner)
if json.loads(raw) != old.to_dict() or digest(raw) != policy.get('predecessor_sha256'):
raise RenewalRejected('PREDECESSOR_FILE_DRIFT')
return old.to_dict(), raw
def load(self, candidate_id):
if candidate_id != CID:
return self.original.load(candidate_id)
policy, activation = self._policy()
if not activation.exists():
return self.original.load(candidate_id)
receipt = json.loads(secure_bytes(activation, owner_uid=policy['predecessor_uid']))
expected = {'schema', 'candidate_id', 'predecessor_fingerprint', 'predecessor_bytes_sha256',
'successor_fingerprint', 'successor_sha256', 'owner_registration_payload_sha256',
'completion_only', 'runtime_effects_allowed'}
if set(receipt) != expected or receipt.get('owner_registration_payload_sha256') != policy['payload_sha256']:
raise RenewalRejected('ACTIVATION_RECEIPT_INVALID')
fp = receipt.get('successor_fingerprint', '')
if len(fp) != 64 or any(c not in '0123456789abcdef' for c in fp):
raise RenewalRejected('SUCCESSOR_FINGERPRINT_INVALID')
raw = secure_bytes(self.directory / (fp + '.approval.json'), owner_uid=policy['predecessor_uid'])
new = json.loads(raw)
parent, parent_raw = self._parent()
valid = validate_successor(parent, new, self.public_key, now=datetime.now(timezone.utc).timestamp())
exact = {'candidate_id': CID, 'predecessor_fingerprint': OLD_FP,
'predecessor_bytes_sha256': digest(parent_raw), 'successor_fingerprint': valid,
'successor_sha256': digest(raw), 'completion_only': True, 'runtime_effects_allowed': False}
if any(receipt.get(k) != v for k, v in exact.items()) or new['nonce'] != policy['approval_nonce']:
raise RenewalRejected('SUCCESSOR_ACTIVATION_DRIFT')
durable = json.loads(secure_bytes(self.directory / (fp + '.receipt.json'), owner_uid=policy['predecessor_uid']))
if durable != receipt:
raise RenewalRejected('DURABLE_RECEIPT_DRIFT')
return self.approval_type.from_dict(new)
def save(self, candidate_id, approval):
if candidate_id != CID:
return self.original.save(candidate_id, approval)
policy, activation = self._policy()
parent, raw = self._parent()
if approval.fingerprint == parent['fingerprint']:
return self.original.save(candidate_id, approval)
new = approval.to_dict()
if new.get('nonce') != policy['approval_nonce']:
raise RenewalRejected('REGISTERED_NONCE_MISMATCH')
# The caller supplies its authenticated registered effect lock and
# invokes the existing ApprovalVerifier before this store boundary.
receipt = publish_successor(self.directory, parent, new, self.public_key,
now=datetime.now(timezone.utc).timestamp(),
registration_digest=policy['payload_sha256'], predecessor_bytes=raw)
created = append_file(activation, canonical(receipt))
if self.load(candidate_id).fingerprint != approval.fingerprint:
raise RenewalRejected('SUCCESSOR_READBACK_FAILED')
return activation, created
class RegisteredCompletionApprovalStore(CompletionApprovalStore):
"""Production dependency; no request-selected root or authority callback."""
def __init__(self, original, public_key, approval_type):
if Path(original.root) != STATE_ROOT / 'approvals':
raise RenewalRejected('SAME_APPROVAL_PLANE_REQUIRED')
super().__init__(original, STATE_ROOT / 'approvals' / 'c5-completion-successors',
public_key, RegisteredCompletionAuthority(), approval_type)
class CompletionSignerMixin:
"""Narrow additional method of the EXISTING credential-bound signer.
Activation must register this exact mixin, verifier and namespace snapshot
pins. The encrypted private key stays in the existing signer's unit. This
draft supplies no activation, credential access or HTTP route.
"""
def sign_exact_completion_successor(self, request):
expected = {'schema', 'operation', 'candidate_id', 'predecessor_fingerprint'}
if (set(request) != expected or request.get('operation') != 'renew_exact_completion_approval'
or request.get('candidate_id') != CID or request.get('predecessor_fingerprint') != OLD_FP
or request.get('schema') != 'aios.exact-release-signer.request.v1'):
raise RenewalRejected('EXACT_SIGNER_RENEWAL_REQUEST_REQUIRED')
policy = self.completion_authority()
now = datetime.now(timezone.utc).timestamp()
if (policy.get('candidate_id') != CID or policy.get('predecessor_fingerprint') != OLD_FP
or not policy['issued_at'] <= now < policy['expires_at']
or not 0 <= now - policy['gate_observed_at'] <= 60):
raise RenewalRejected('FRESH_REGISTERED_COMPLETION_GATE_REQUIRED')
old_raw = secure_bytes(policy['predecessor_snapshot'], owner_uid=0)
if digest(old_raw) != policy['predecessor_sha256']:
raise RenewalRejected('REGISTERED_SIGNER_PREIMAGE_DRIFT')
old = json.loads(old_raw)
public_key = self.private_key.public_key().public_bytes(Encoding.Raw, PublicFormat.Raw)
verify_signature(old, public_key)
exact = {'candidate_id': CID, 'source_commit': SOURCE, 'tree_oid': TREE,
'artifact_sha256': ARTIFACT, 'release_plan_sha256': PLAN,
'fingerprint': OLD_FP, 'capabilities': CAPABILITIES,
'key_id': self.policy.key_id, 'owner_identity': self.policy.owner_identity,
'trust_domain': self.policy.trust_domain,
'owner_approval_record_oid': self.authorization_oid}
if any(old.get(k) != v for k, v in exact.items()) or timestamp(old['expires_at']) > now:
raise RenewalRejected('EXACT_EXPIRED_SIGNER_PREDECESSOR_REQUIRED')
nonce = policy['approval_nonce']
if len(nonce) != 64 or any(c not in '0123456789abcdef' for c in nonce) or nonce == old['nonce']:
raise RenewalRejected('FRESH_REGISTERED_SIGNER_NONCE_REQUIRED')
output = self.state_root / 'completion-renewals' / (nonce + '.json')
if output.exists():
result = json.loads(secure_bytes(output, owner_uid=0))
if (result.get('registration_payload_sha256') != policy['payload_sha256']
or result['approval']['nonce'] != nonce):
raise RenewalRejected('SIGNER_REPLAY_CONFLICT')
validate_successor(old, result['approval'], public_key, now=now)
return {**result, 'status': 'SIGNED_IDEMPOTENT'}
expiry = min(now + 600, policy['expires_at'] - 5)
if expiry - now < 120:
raise RenewalRejected('INSUFFICIENT_SIGNER_WINDOW')
def utc(value):
return datetime.fromtimestamp(value, timezone.utc).replace(microsecond=0).isoformat().replace('+00:00', 'Z')
payload = {k:v for k,v in old.items() if k not in {'fingerprint', 'signature'}}
payload.update(issued_at=utc(now), not_before=utc(now), expires_at=utc(expiry), nonce=nonce)
raw = canonical(payload)
# All candidate, plan, historical signature and registered freshness
# checks precede this only signing operation.
approval = {**payload, 'fingerprint': digest(raw),
'signature': base64.b64encode(self.private_key.sign(raw)).decode()}
validate_successor(old, approval, public_key, now=now)
result = {'schema': 'aios.exact-release-signer.response.v1', 'status': 'SIGNED',
'approval': approval, 'registration_payload_sha256': policy['payload_sha256'],
'predecessor_fingerprint': OLD_FP, 'completion_only': True, 'arbitrary_signing': False}
# This registered directory must be created by the activation consumer
# inside the existing signer state root, never a request path.
append_file(output, canonical(result))
return result
@@ -0,0 +1,58 @@
"""Registered fixed extension of the existing credential-bound signer unit."""
import importlib.util
import os
from pathlib import Path
import sys
from c5_completion_renewal import (
CompletionSignerMixin, RegisteredCompletionAuthority,
RenewalRejected, secure_bytes, digest,
)
ORIGINAL = Path('/usr/local/libexec/aios_exact_release_signer.py')
ORIGINAL_SHA = '6e2a5a9df0edfe566175e5b556d15901b2c8fe4484388ad74af2ac64f810c930'
def signer_class(original):
class CompletionSigner(CompletionSignerMixin, original.ExactReleaseSigner):
def completion_authority(self):
return RegisteredCompletionAuthority()()
def handle(self, request):
if request.get('operation') == 'renew_exact_completion_approval':
return self.sign_exact_completion_successor(request)
return super().handle(request)
return CompletionSigner
def main():
import c5_completion_renewal as renewal
renewal.configure_registered_bindings()
if Path(__file__) != renewal.AUTHORITY_ROOT / 'c5_completion_signer.py':
raise RenewalRejected('REGISTERED_SIGNER_WRAPPER_REQUIRED')
RegisteredCompletionAuthority()()
if digest(secure_bytes(ORIGINAL)) != ORIGINAL_SHA:
raise RenewalRejected('ORIGINAL_SIGNER_PREIMAGE_DRIFT')
spec = importlib.util.spec_from_file_location('c5_original_exact_release_signer', ORIGINAL)
original = importlib.util.module_from_spec(spec)
sys.modules[spec.name] = original
spec.loader.exec_module(original)
credentials = Path(os.environ.get('CREDENTIALS_DIRECTORY', ''))
if not credentials.is_absolute() or credentials.parent != Path('/run/credentials'):
raise RenewalRejected('EXISTING_SIGNER_PURPOSE_CREDENTIAL_REQUIRED')
if credentials.name != 'aios-exact-release-signer.service':
raise RenewalRejected('EXISTING_SIGNER_UNIT_REQUIRED')
cgroup = Path('/proc/self/cgroup').read_text()
if not any(line.split(':', 2)[-1].rstrip('/').endswith('/aios-exact-release-signer.service')
for line in cgroup.splitlines()):
raise RenewalRejected('EXISTING_SIGNER_CGROUP_REQUIRED')
signer = signer_class(original)(
Path('/etc/aios-gitops/exact-release-signer-policy.json'),
credentials / 'owner_release_private_key',
)
original.serve(Path('/run/aios-exact-release-signer/signer.sock'), signer)
return 0
if __name__ == '__main__':
raise SystemExit(main())
@@ -0,0 +1,503 @@
"""Exact C5 runtime-switch adapter for the silent-release Saga.
The adapter owns only the production deployment phase. Tag publication and
Registry/Baseline finalization remain separate ``ReleaseOrchestrator`` Saga
steps. A host must register immutable executable bytes and provide the real
HMAC-authenticated source-admission store plus the signed Owner approval
verifier before this adapter can be constructed.
"""
from __future__ import annotations
from dataclasses import dataclass
import hashlib
import json
import os
from pathlib import Path
import re
import stat
import subprocess
from typing import Any, Mapping
from release_source_admission import ReleaseSourceAdmissionStore
from silent_release_controller import ReleaseCandidate
from silent_release_hardening import (
CapabilityAuthorizationError,
CapabilityLedger,
ClaimProbeResult,
ReleasePlan,
RuntimeSwitchController,
SignedApprovalVerifier,
SignedReleasePlanApproval,
_canonical_json,
)
C5_VERSION = "V2R2C5"
C5_PREPUBLICATION_SOURCE_COMMIT = "396ea9811124736aa140d2810ab53eaefb6b5c1c"
C4_ROLLBACK_TARGET = {
"version": "V2R2C4",
"target_oid": "e5f8209f57b43fc39e55c4555776e6362ce326eb",
"artifact_sha256": "0dfc0461f7f9aa25cfbfc37008dcbd2d48d5341383574163b7bba102852062c4",
}
C4_ROLLBACK_ARTIFACT_PATH = Path(
"/var/lib/aios-gitops/release-only/"
"v2r2c4-e5f8209f57b4/artifact.tar.gz"
)
C5_PRODUCTION_ENVIRONMENT_FILE = Path("/etc/aios/v2r2c2.env")
C5_PRODUCTION_SERVICE = "aios-v2r2c2-production.service"
C5_PRODUCTION_PORT = 18928
REQUEST_SCHEMA = "aios.c5.exact-production-deployment-request.v1"
HEX64 = re.compile(r"^[0-9a-f]{64}$")
class C5DeploymentBindingError(CapabilityAuthorizationError):
"""C5 source, plan, approval, executor, or receipt binding is invalid."""
class C5DeploymentOutcomeUnknown(RuntimeError):
"""The deployment effect must be reconciled before any retry."""
class C5DeploymentConflict(RuntimeError):
"""The observed runtime is bound to a different release."""
def _sha256_file(path: Path) -> str:
digest = hashlib.sha256()
with path.open("rb") as source:
for block in iter(lambda: source.read(1024 * 1024), b""):
digest.update(block)
return digest.hexdigest()
def _run_registered_adapter(
command: tuple[str, ...], request: Mapping[str, Any]
) -> dict[str, Any]:
"""Invoke only the code-pinned process and accept one JSON object."""
try:
completed = subprocess.run(
list(command),
input=_canonical_json(dict(request)),
check=False,
capture_output=True,
timeout=180,
env={"PATH": os.environ.get("PATH", "")},
)
except (OSError, subprocess.SubprocessError) as exc:
raise C5DeploymentOutcomeUnknown(
"C5_DEPLOYMENT_EXECUTOR_UNAVAILABLE"
) from exc
if completed.returncode != 0:
raise C5DeploymentOutcomeUnknown(
"C5_DEPLOYMENT_EXECUTOR_FAILED"
)
try:
lines = [
line
for line in completed.stdout.decode("utf-8").splitlines()
if line.strip()
]
value = json.loads(lines[-1]) if lines else None
except (UnicodeError, ValueError) as exc:
raise C5DeploymentOutcomeUnknown(
"C5_DEPLOYMENT_EXECUTOR_RESPONSE_INVALID"
) from exc
if not isinstance(value, dict):
raise C5DeploymentOutcomeUnknown(
"C5_DEPLOYMENT_EXECUTOR_RESPONSE_INVALID"
)
return value
def _secure_registered_file(
path: Path,
expected_sha256: str,
*,
require_root_owner: bool,
) -> None:
path = Path(path)
if (
not path.is_absolute()
or path.is_symlink()
or path.resolve(strict=False) != path
):
raise C5DeploymentBindingError("C5_EXECUTOR_PATH_INVALID")
try:
info = path.stat()
except OSError as exc:
raise C5DeploymentBindingError("C5_EXECUTOR_PATH_INVALID") from exc
if not stat.S_ISREG(info.st_mode):
raise C5DeploymentBindingError("C5_EXECUTOR_PATH_INVALID")
if os.name != "nt" and stat.S_IMODE(info.st_mode) & 0o022:
raise C5DeploymentBindingError("C5_EXECUTOR_PATH_WRITABLE")
if require_root_owner:
if not hasattr(os, "geteuid") or info.st_uid != 0:
raise C5DeploymentBindingError("C5_EXECUTOR_OWNER_INVALID")
parent = path.parent
while True:
parent_info = parent.stat()
if (
parent.is_symlink()
or parent_info.st_uid != 0
or stat.S_IMODE(parent_info.st_mode) & 0o022
):
raise C5DeploymentBindingError(
"C5_EXECUTOR_PARENT_SECURITY_INVALID"
)
if parent == parent.parent:
break
parent = parent.parent
if _sha256_file(path) != expected_sha256:
raise C5DeploymentBindingError("C5_EXECUTOR_CODE_MISMATCH")
@dataclass(frozen=True, slots=True)
class RegisteredC5DeploymentExecutor:
"""Pinned host configuration for one concrete process adapter.
``adapter_path`` is optional for a native executable. When present the
command is exactly ``executable adapter_path``; arbitrary argv and Python
callables are intentionally unsupported.
"""
executor_id: str
executable: Path
executable_sha256: str
artifact_path: Path
rollback_artifact_path: Path
production_environment_file: Path
adapter_path: Path | None = None
adapter_sha256: str | None = None
require_root_owner: bool = True
execution_scope: str = "PRODUCTION"
isolation_root: Path | None = None
rollback_target: Mapping[str, str] | None = None
def __post_init__(self) -> None:
if not self.executor_id or len(self.executor_id) > 255:
raise C5DeploymentBindingError("C5_EXECUTOR_ID_INVALID")
if HEX64.fullmatch(self.executable_sha256) is None:
raise C5DeploymentBindingError("C5_EXECUTOR_DIGEST_INVALID")
if (self.adapter_path is None) != (self.adapter_sha256 is None):
raise C5DeploymentBindingError("C5_EXECUTOR_ADAPTER_BINDING_INVALID")
if (
self.adapter_sha256 is not None
and HEX64.fullmatch(self.adapter_sha256) is None
):
raise C5DeploymentBindingError("C5_EXECUTOR_DIGEST_INVALID")
if not Path(self.artifact_path).is_absolute():
raise C5DeploymentBindingError("C5_ARTIFACT_PATH_INVALID")
if self.execution_scope not in {"PRODUCTION", "ISOLATED_REHEARSAL"}:
raise C5DeploymentBindingError("C5_EXECUTION_SCOPE_INVALID")
target = dict(self.rollback_target or C4_ROLLBACK_TARGET)
if target.keys() != C4_ROLLBACK_TARGET.keys() or target["version"] != C4_ROLLBACK_TARGET["version"] or target["target_oid"] != C4_ROLLBACK_TARGET["target_oid"] or HEX64.fullmatch(str(target["artifact_sha256"])) is None:
raise C5DeploymentBindingError("C5_ROLLBACK_TARGET_INVALID")
if self.execution_scope == "PRODUCTION":
if target != C4_ROLLBACK_TARGET:
raise C5DeploymentBindingError("C5_ROLLBACK_TARGET_INVALID")
if Path(self.rollback_artifact_path) != C4_ROLLBACK_ARTIFACT_PATH:
raise C5DeploymentBindingError("C5_ROLLBACK_ARTIFACT_PATH_INVALID")
if Path(self.production_environment_file) != C5_PRODUCTION_ENVIRONMENT_FILE:
raise C5DeploymentBindingError("C5_PRODUCTION_ENVIRONMENT_FILE_INVALID")
if self.isolation_root is not None:
raise C5DeploymentBindingError("C5_ISOLATION_ROOT_INVALID")
else:
if self.isolation_root is None or not Path(self.isolation_root).is_absolute():
raise C5DeploymentBindingError("C5_ISOLATION_ROOT_INVALID")
root = Path(self.isolation_root).resolve(strict=False)
for bound_path in (self.rollback_artifact_path, self.production_environment_file):
path = Path(bound_path)
if not path.is_absolute() or path.resolve(strict=False) == root:
raise C5DeploymentBindingError("C5_REHEARSAL_BINDING_INVALID")
try:
path.resolve(strict=False).relative_to(root)
except ValueError as exc:
raise C5DeploymentBindingError("C5_REHEARSAL_BINDING_INVALID") from exc
_secure_registered_file(
Path(self.rollback_artifact_path),
target["artifact_sha256"],
require_root_owner=self.require_root_owner,
)
object.__setattr__(self, "rollback_target", target)
self.verify_code()
def verify_code(self) -> None:
_secure_registered_file(
Path(self.executable),
self.executable_sha256,
require_root_owner=self.require_root_owner,
)
if self.adapter_path is not None:
_secure_registered_file(
Path(self.adapter_path),
str(self.adapter_sha256),
require_root_owner=self.require_root_owner,
)
@property
def command(self) -> tuple[str, ...]:
values = [str(Path(self.executable))]
if self.adapter_path is not None:
values.append(str(Path(self.adapter_path)))
return tuple(values)
class C5ExactDeploymentAdapter:
"""Callable runtime-switch phase with exact C5 admission and approval."""
def __init__(
self,
*,
candidate: ReleaseCandidate,
release_plan: ReleasePlan,
approval: SignedReleasePlanApproval,
approval_verifier: SignedApprovalVerifier,
source_admissions: ReleaseSourceAdmissionStore,
executor: RegisteredC5DeploymentExecutor,
) -> None:
if not isinstance(approval_verifier, SignedApprovalVerifier):
raise C5DeploymentBindingError("C5_APPROVAL_VERIFIER_REQUIRED")
if not isinstance(source_admissions, ReleaseSourceAdmissionStore):
raise C5DeploymentBindingError("C5_SOURCE_ADMISSION_STORE_REQUIRED")
self.candidate = candidate
self.release_plan = release_plan
self.approval = approval
self.approval_verifier = approval_verifier
self.source_admissions = source_admissions
self.executor = executor
self._binding = self._verify_binding(candidate, release_plan)
@staticmethod
def _global_writes_disabled() -> bool:
return os.environ.get("AIOS_GITOPS_WRITES_ENABLED", "false").lower() == "false"
def _verify_binding(
self,
candidate: ReleaseCandidate,
plan: ReleasePlan,
) -> dict[str, Any]:
if candidate.fingerprint != self.candidate.fingerprint or plan.sha256 != self.release_plan.sha256:
raise C5DeploymentBindingError("C5_RUNTIME_INPUT_BINDING_MISMATCH")
plan.validate()
if plan.tag.version != C5_VERSION or plan.finalization.version != C5_VERSION:
raise C5DeploymentBindingError("C5_VERSION_BINDING_INVALID")
if candidate.source_commit == C5_PREPUBLICATION_SOURCE_COMMIT:
raise C5DeploymentBindingError("C5_PREPUBLICATION_SOURCE_FORBIDDEN")
rollback_target = dict(self.executor.rollback_target or C4_ROLLBACK_TARGET)
if plan.deployment.rollback_target != rollback_target:
raise C5DeploymentBindingError("C5_ROLLBACK_TARGET_INVALID")
if plan.deployment.service != C5_PRODUCTION_SERVICE:
raise C5DeploymentBindingError("C5_PRODUCTION_SERVICE_INVALID")
if plan.deployment.writer_expected is not False:
raise C5DeploymentBindingError("C5_LEGACY_WRITES_MUST_REMAIN_DISABLED")
if not self._global_writes_disabled():
raise C5DeploymentBindingError("C5_LEGACY_WRITES_MUST_REMAIN_DISABLED")
admitted = self.source_admissions.resolve_recipe(
C5_VERSION,
candidate.candidate_id,
candidate.source_commit,
)
if (
admitted.get("source_commit") != candidate.source_commit
or admitted.get("tree_oid") != candidate.tree_oid
):
raise C5DeploymentBindingError("C5_ADMITTED_SOURCE_BINDING_INVALID")
if (
plan.deployment.source_commit != candidate.source_commit
or plan.deployment.tree_oid != candidate.tree_oid
or plan.deployment.artifact_sha256 != candidate.artifact_sha256
or plan.deployment.manifest_sha256 != candidate.manifest_sha256
):
raise C5DeploymentBindingError("C5_CANDIDATE_PLAN_BINDING_INVALID")
self.approval_verifier.verify_release_plan(
self.approval,
candidate,
plan,
plan.policy_sha256,
)
artifact = Path(self.executor.artifact_path)
_secure_registered_file(
artifact,
candidate.artifact_sha256,
require_root_owner=self.executor.require_root_owner,
)
artifact_size = artifact.stat().st_size
if artifact_size <= 0:
raise C5DeploymentBindingError("C5_ARTIFACT_BYTES_MISMATCH")
self.executor.verify_code()
return {
"version": C5_VERSION,
"candidate_id": candidate.candidate_id,
"source_commit": candidate.source_commit,
"target_oid": candidate.source_commit,
"tree_oid": candidate.tree_oid,
"artifact_sha256": candidate.artifact_sha256,
"artifact_path": str(artifact),
"artifact_size": artifact_size,
"manifest_sha256": candidate.manifest_sha256,
"deployment_plan_sha256": plan.deployment.sha256,
"release_plan_sha256": plan.sha256,
"policy_sha256": plan.policy_sha256,
"approval_plan_id": self.approval.approval_plan_id,
"approval_fingerprint": self.approval.fingerprint,
"owner_approval_record_oid": self.approval.owner_approval_record_oid,
"evidence_closure_oid": self.approval.evidence_closure_oid,
"repository_identity": plan.deployment.repository_identity,
"production_service": plan.deployment.service,
"production_port": C5_PRODUCTION_PORT,
"systemd_unit_sha256": plan.deployment.systemd_unit_sha256,
"dropin_sha256": plan.deployment.dropin_sha256,
"rollback_target": rollback_target,
"rollback_artifact_path": str(
self.executor.rollback_artifact_path
),
"production_environment_file": str(
self.executor.production_environment_file
),
"writer_expected": False,
"global_legacy_writes_enabled": False,
"registered_executor_id": self.executor.executor_id,
"executor_executable_sha256": self.executor.executable_sha256,
"executor_adapter_sha256": self.executor.adapter_sha256,
}
def _request(self, operation: str) -> dict[str, Any]:
return {
"schema": REQUEST_SCHEMA,
"operation": operation,
**self._binding,
}
def _invoke(self, operation: str) -> dict[str, Any]:
if not self._global_writes_disabled():
raise C5DeploymentBindingError("C5_LEGACY_WRITES_MUST_REMAIN_DISABLED")
self.executor.verify_code()
value = _run_registered_adapter(
self.executor.command, self._request(operation)
)
for key, expected in self._binding.items():
if value.get(key) != expected:
raise C5DeploymentBindingError(f"C5_DEPLOYMENT_RECEIPT_MISMATCH:{key}")
if value.get("writer_disabled") is not True:
raise C5DeploymentBindingError("C5_DEPLOYMENT_WRITER_STATE_INVALID")
receipt = value.get("deployment_receipt")
if not isinstance(receipt, Mapping) or not receipt:
raise C5DeploymentBindingError("C5_DEPLOYMENT_RECEIPT_REQUIRED")
return value
@staticmethod
def _classify(value: Mapping[str, Any]) -> ClaimProbeResult:
status = value.get("status")
if (
status == "ACTIVE_VERIFIED"
and value.get("verified") is True
and value.get("deployment_state") == "ACTIVE"
and value.get("production_deployed") is True
and value.get("writer_disabled") is True
and value.get("global_legacy_writes_enabled") is False
):
return ClaimProbeResult.APPLIED_VERIFIED
if status == "NOT_APPLIED":
return ClaimProbeResult.NOT_APPLIED
if status == "CONFLICT":
return ClaimProbeResult.CONFLICT
return ClaimProbeResult.INDETERMINATE
def recovery_probe(self) -> ClaimProbeResult:
"""Read-only exact probe used by ``CapabilityLedger`` after UNKNOWN."""
try:
self._verify_binding(self.candidate, self.release_plan)
return self._classify(self._invoke("probe"))
except C5DeploymentConflict:
return ClaimProbeResult.CONFLICT
except Exception:
return ClaimProbeResult.INDETERMINATE
def rollback(
self,
*,
candidate: ReleaseCandidate,
plan: ReleasePlan,
) -> dict[str, Any]:
"""Execute only the explicit Saga rollback phase and verify exact C4."""
self._verify_binding(candidate, plan)
value = self._invoke("rollback")
if (
value.get("status") != "ROLLED_BACK_VERIFIED"
or value.get("verified") is not True
or value.get("rollback_verified") is not True
or value.get("rollback_version") != self.executor.rollback_target["version"]
or value.get("rollback_target_oid")
!= self.executor.rollback_target["target_oid"]
or value.get("rollback_artifact_sha256")
!= self.executor.rollback_target["artifact_sha256"]
):
raise C5DeploymentOutcomeUnknown("C5_ROLLBACK_OUTCOME_UNKNOWN")
return {
"verified": True,
"rollback_verified": True,
"rollback_version": self.executor.rollback_target["version"],
"rollback_target_oid": self.executor.rollback_target["target_oid"],
"rollback_artifact_sha256": self.executor.rollback_target["artifact_sha256"],
"writer_disabled": True,
}
def __call__(
self,
*,
candidate: ReleaseCandidate,
plan: ReleasePlan,
) -> dict[str, Any]:
self._verify_binding(candidate, plan)
probe = self._invoke("probe")
classification = self._classify(probe)
if classification == ClaimProbeResult.CONFLICT:
raise C5DeploymentConflict("C5_RUNTIME_BINDING_CONFLICT")
if classification == ClaimProbeResult.INDETERMINATE:
raise C5DeploymentOutcomeUnknown("C5_DEPLOYMENT_OUTCOME_UNKNOWN")
if classification == ClaimProbeResult.NOT_APPLIED:
applied = self._invoke("apply")
if applied.get("status") == "CONFLICT":
raise C5DeploymentConflict("C5_RUNTIME_BINDING_CONFLICT")
if applied.get("status") not in {"APPLIED", "ACTIVE_VERIFIED"}:
raise C5DeploymentOutcomeUnknown("C5_DEPLOYMENT_OUTCOME_UNKNOWN")
probe = self._invoke("probe")
classification = self._classify(probe)
if classification == ClaimProbeResult.CONFLICT:
raise C5DeploymentConflict("C5_RUNTIME_BINDING_CONFLICT")
if classification != ClaimProbeResult.APPLIED_VERIFIED:
raise C5DeploymentOutcomeUnknown("C5_DEPLOYMENT_OUTCOME_UNKNOWN")
return {
"verified": True,
"version": C5_VERSION,
"candidate_id": candidate.candidate_id,
"source_commit": candidate.source_commit,
"tree_oid": candidate.tree_oid,
"artifact_sha256": candidate.artifact_sha256,
"release_plan_sha256": plan.sha256,
"approval_fingerprint": self.approval.fingerprint,
"rollback_target": dict(self.executor.rollback_target),
"writer_disabled": True,
"global_legacy_writes_enabled": False,
"registered_executor_id": self.executor.executor_id,
"deployment_receipt_sha256": hashlib.sha256(
_canonical_json(dict(probe))
).hexdigest(),
}
def controller(
self,
ledger: CapabilityLedger,
*,
lease_seconds: float = 300,
) -> RuntimeSwitchController:
"""Bind this exact adapter to the existing capability controller."""
return RuntimeSwitchController(
ledger,
self,
recovery_probe=self.recovery_probe,
executor_id=self.executor.executor_id,
lease_seconds=lease_seconds,
)
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,688 @@
"""Owner-registered dispatch boundary for the fixed-scope C5 installer.
The registration authorizes one exact signed installation contract and one
exact set of installed executor bytes. It is not an execution receipt; the
wrapped :class:`Installer` remains the component that durably records and
reconciles installation state.
"""
from __future__ import annotations
from dataclasses import dataclass
import argparse
import hashlib
import json
import os
from pathlib import Path
import re
import socket
import stat
import sys
import time
from typing import Any, Callable, Mapping
import c5_staging_installation as _installer_module
from c5_staging_installation import (
BRIDGE_EFFECTS,
BRIDGE_TARGET,
HOST,
InstallationError,
InstallationOutcomeUnknown,
ISSUER_EFFECTS,
ISSUER_TARGET,
OWNER_NAMESPACE,
R14_SOURCE_COMMIT,
REPOSITORY,
SUCCESSOR_SCHEMA,
Installer,
RegistryOwnerVerifier,
canonical_bytes,
verify_signed_contract,
)
from registered_owner_signer_adapter import RegisteredOwnerSignerAdapter
SCHEMA = "aios.c5.registered-installer-executor.v1"
PROVENANCE_SCHEMA = "aios.c5.executor-provenance.v1"
EXECUTION_DIGEST = "0779fa615e8e7971a8dcfccf0f295b1323cd9b64787e174aea4fd2affc414339"
BRIDGE_PORT = 18892
ISSUER_PORT = 18946
CAPABILITIES = frozenset({"install", "rollback", "reconcile"})
TARGET_EFFECTS = {BRIDGE_TARGET: BRIDGE_EFFECTS, ISSUER_TARGET: ISSUER_EFFECTS}
MAX_TTL_SECONDS = 900
MAX_CODE_BYTES = 16 * 1024 * 1024
DEFAULT_REGISTRY = Path("/etc/aios/single-host-signer-registry.json")
DEFAULT_PROVENANCE_MANIFEST = Path("/etc/aios/c5-executor-provenance.json")
HEX40 = re.compile(r"^[0-9a-f]{40}$")
HEX64 = re.compile(r"^[0-9a-f]{64}$")
NONCE = re.compile(r"^[A-Za-z0-9._:\-]{16,128}$")
SCOPE_ID = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._:\-]{2,127}$")
class ExecutorRegistrationError(RuntimeError):
def __init__(self, code: str) -> None:
super().__init__(code)
self.code = code
def _fail(code: str) -> None:
raise ExecutorRegistrationError(code)
def _sha256_bytes(data: bytes) -> str:
return hashlib.sha256(data).hexdigest()
def _require_digest(value: Any, pattern: re.Pattern[str], code: str) -> str:
if not isinstance(value, str) or pattern.fullmatch(value) is None:
_fail(code)
return value
def _strict_object(pairs: list[tuple[str, Any]]) -> dict[str, Any]:
result: dict[str, Any] = {}
for key, value in pairs:
if key in result:
_fail("EXECUTOR_JSON_DUPLICATE_FIELD")
result[key] = value
return result
def _secure_bytes(
path: Path, *, require_root_owner: bool = True, maximum: int = MAX_CODE_BYTES,
) -> bytes:
path = Path(path)
if not path.is_absolute():
_fail("EXECUTOR_CODE_PATH_INVALID")
try:
if path.is_symlink() or path.resolve(strict=True) != path:
_fail("EXECUTOR_CODE_PATH_INVALID")
before = path.stat()
except OSError as exc:
raise ExecutorRegistrationError("EXECUTOR_CODE_PATH_INVALID") from exc
flags = os.O_RDONLY
if hasattr(os, "O_NOFOLLOW"):
flags |= os.O_NOFOLLOW
try:
fd = os.open(path, flags)
except OSError as exc:
raise ExecutorRegistrationError("EXECUTOR_CODE_PATH_INVALID") from exc
try:
current = os.fstat(fd)
if (
not stat.S_ISREG(current.st_mode)
or current.st_dev != before.st_dev
or current.st_ino != before.st_ino
or current.st_size < 1
or current.st_size > maximum
):
_fail("EXECUTOR_CODE_FILE_INVALID")
if require_root_owner:
if os.name != "posix" or current.st_uid != 0:
_fail("EXECUTOR_CODE_PATH_OWNER_INVALID")
if stat.S_IMODE(current.st_mode) & 0o022:
_fail("EXECUTOR_CODE_PATH_WRITABLE")
parent = path.parent
while True:
parent_stat = parent.stat()
if parent.is_symlink() or parent_stat.st_uid != 0:
_fail("EXECUTOR_CODE_PATH_OWNER_INVALID")
if stat.S_IMODE(parent_stat.st_mode) & 0o022:
_fail("EXECUTOR_CODE_PATH_WRITABLE")
if parent == parent.parent:
break
parent = parent.parent
chunks: list[bytes] = []
remaining = current.st_size
while remaining:
chunk = os.read(fd, min(remaining, 1024 * 1024))
if not chunk:
_fail("EXECUTOR_CODE_FILE_INVALID")
chunks.append(chunk)
remaining -= len(chunk)
if os.read(fd, 1):
_fail("EXECUTOR_CODE_FILE_INVALID")
return b"".join(chunks)
finally:
os.close(fd)
@dataclass(frozen=True, slots=True)
class CodeProvenance:
environment: str
instance_id: str
integration_commit: str
integration_tree: str
installer_module_path: Path
installer_script_path: Path
executor_module_path: Path
installer_module_sha256: str
installer_script_sha256: str
executor_module_sha256: str
authority_manifest_sha256: str
def _actual_installer_module_path() -> Path:
value = getattr(_installer_module, "__file__", None)
if not isinstance(value, str):
_fail("EXECUTOR_LOADED_INSTALLER_PATH_INVALID")
return Path(value).resolve(strict=True)
def _actual_executor_module_path() -> Path:
return Path(__file__).resolve(strict=True)
def load_code_provenance(
manifest: Path, *, require_root_owner: bool = True,
) -> CodeProvenance:
"""Load identity from an authority manifest and actual loaded paths."""
manifest = Path(manifest)
raw = _secure_bytes(manifest, require_root_owner=require_root_owner)
try:
value = json.loads(raw, object_pairs_hook=_strict_object)
except (UnicodeError, json.JSONDecodeError) as exc:
raise ExecutorRegistrationError("EXECUTOR_PROVENANCE_INVALID") from exc
if (
not isinstance(value, dict)
or set(value) != {
"schema", "environment", "instance_id", "integration_commit",
"integration_tree", "entries",
}
or value["schema"] != PROVENANCE_SCHEMA
or not isinstance(value["entries"], dict)
or set(value["entries"]) != {
"installer_module", "installer_script", "executor_module",
}
):
_fail("EXECUTOR_PROVENANCE_INVALID")
expected_paths = {
"installer_module": _actual_installer_module_path(),
"executor_module": _actual_executor_module_path(),
}
resolved: dict[str, Path] = {}
hashes: dict[str, str] = {}
for label, entry in value["entries"].items():
if not isinstance(entry, dict) or set(entry) != {"path", "sha256"}:
_fail("EXECUTOR_PROVENANCE_INVALID")
if not isinstance(entry["path"], str):
_fail("EXECUTOR_PROVENANCE_INVALID")
path = Path(entry["path"])
if not path.is_absolute():
_fail("EXECUTOR_CODE_PATH_INVALID")
try:
canonical = path.resolve(strict=True)
except OSError as exc:
raise ExecutorRegistrationError("EXECUTOR_CODE_PATH_INVALID") from exc
if path != canonical:
_fail("EXECUTOR_CODE_PATH_INVALID")
if label in expected_paths and canonical != expected_paths[label]:
_fail("EXECUTOR_PROVENANCE_LOADED_PATH_MISMATCH")
if label == "installer_script" and canonical.name != "c5_staging_installation.py":
_fail("EXECUTOR_INSTALLER_SCRIPT_PATH_INVALID")
digest = _sha256_bytes(
_secure_bytes(canonical, require_root_owner=require_root_owner)
)
if entry["sha256"] != digest:
_fail("EXECUTOR_PROVENANCE_CODE_MISMATCH")
resolved[label] = canonical
hashes[label] = digest
if (
not isinstance(value["environment"], str)
or SCOPE_ID.fullmatch(value["environment"]) is None
or not isinstance(value["instance_id"], str)
or SCOPE_ID.fullmatch(value["instance_id"]) is None
):
_fail("EXECUTOR_PROVENANCE_SCOPE_INVALID")
return CodeProvenance(
environment=value["environment"],
instance_id=value["instance_id"],
integration_commit=_require_digest(
value["integration_commit"], HEX40,
"EXECUTOR_INTEGRATION_COMMIT_INVALID",
),
integration_tree=_require_digest(
value["integration_tree"], HEX40,
"EXECUTOR_INTEGRATION_TREE_INVALID",
),
installer_module_path=resolved["installer_module"],
installer_script_path=resolved["installer_script"],
executor_module_path=resolved["executor_module"],
installer_module_sha256=hashes["installer_module"],
installer_script_sha256=hashes["installer_script"],
executor_module_sha256=hashes["executor_module"],
authority_manifest_sha256=_sha256_bytes(raw),
)
def _contract_binding(payload: Mapping[str, Any], digest: str) -> dict[str, Any]:
return {
"target": payload["target"],
"contract_digest": digest,
"old_snapshot_sha256": _sha256_bytes(canonical_bytes(payload["existing"])),
"new_manifest_sha256": _sha256_bytes(canonical_bytes({
"release_root": payload["release_root"],
"files": payload["files"],
"readiness": payload["readiness"],
})),
"rollback_sha256": _sha256_bytes(canonical_bytes(payload["rollback"])),
"contract_issued_at": payload["issued_at"],
"contract_expires_at": payload["expires_at"],
"contract_nonce": payload["nonce"],
}
def _issuer_dependency(payload: Mapping[str, Any], digest: str) -> dict[str, Any]:
return {
"target": payload["target"],
"contract_digest": digest,
"source_commit": payload["source_commit"],
"support_commit": payload["support_commit"],
"owner_signer_id": payload["owner_signer_id"],
"owner_key_id": payload["owner_key_id"],
"contract_nonce": payload["nonce"],
"contract_issued_at": payload["issued_at"],
"contract_expires_at": payload["expires_at"],
"rollback_sha256": _sha256_bytes(canonical_bytes(payload["rollback"])),
}
def _registration_payload(
*, executor_id: Any, nonce: Any, issued_at: Any, expires_at: Any,
contract_payload: Mapping[str, Any], contract_digest: str,
provenance: CodeProvenance,
issuer_dependency: Mapping[str, Any] | None,
) -> dict[str, Any]:
return {
"schema": SCHEMA,
"executor_id": executor_id,
"owner_signer_id": contract_payload["owner_signer_id"],
"owner_key_id": contract_payload["owner_key_id"],
"host": HOST,
"repository": REPOSITORY,
"environment": provenance.environment,
"instance_id": provenance.instance_id,
"source_commit": R14_SOURCE_COMMIT,
"support_commit": contract_payload["support_commit"],
"integration_commit": provenance.integration_commit,
"integration_tree": provenance.integration_tree,
"installer_module_sha256": provenance.installer_module_sha256,
"installer_script_sha256": provenance.installer_script_sha256,
"executor_module_sha256": provenance.executor_module_sha256,
"authority_manifest_sha256": provenance.authority_manifest_sha256,
"execution_digest": EXECUTION_DIGEST,
"bridge_port": BRIDGE_PORT,
"issuer_port": ISSUER_PORT,
"capabilities": sorted(CAPABILITIES),
"effects": sorted(contract_payload["effects"]),
"issuer_dependency": dict(issuer_dependency) if issuer_dependency is not None else None,
"issued_at": issued_at,
"expires_at": expires_at,
"nonce": nonce,
**_contract_binding(contract_payload, contract_digest),
}
def _validate_issuer_dependency(
dependency: Any, contract_payload: Mapping[str, Any],
) -> None:
if contract_payload["target"] == ISSUER_TARGET:
if dependency is not None:
_fail("EXECUTOR_ISSUER_DEPENDENCY_INVALID")
return
if (
not isinstance(dependency, Mapping)
or set(dependency) != {
"target", "contract_digest", "source_commit", "support_commit",
"owner_signer_id", "owner_key_id", "contract_nonce",
"contract_issued_at", "contract_expires_at", "rollback_sha256",
}
or dependency["target"] != ISSUER_TARGET
or dependency["source_commit"] != contract_payload["source_commit"]
or dependency["support_commit"] != contract_payload["support_commit"]
or dependency["owner_signer_id"] != contract_payload["owner_signer_id"]
or dependency["owner_key_id"] != contract_payload["owner_key_id"]
or type(dependency["contract_issued_at"]) is not int
or type(dependency["contract_expires_at"]) is not int
):
_fail("EXECUTOR_ISSUER_DEPENDENCY_INVALID")
_require_digest(
dependency["contract_digest"], HEX64,
"EXECUTOR_ISSUER_DEPENDENCY_INVALID",
)
_require_digest(
dependency["rollback_sha256"], HEX64,
"EXECUTOR_ISSUER_DEPENDENCY_INVALID",
)
def _validate_payload(
payload: Mapping[str, Any], *, now: int, provenance: CodeProvenance,
contract_payload: Mapping[str, Any], contract_digest: str,
issuer_dependency: Mapping[str, Any] | None,
) -> None:
expected = _registration_payload(
executor_id=payload.get("executor_id"), nonce=payload.get("nonce"),
issued_at=payload.get("issued_at"), expires_at=payload.get("expires_at"),
contract_payload=contract_payload, contract_digest=contract_digest,
provenance=provenance, issuer_dependency=issuer_dependency,
)
if set(payload) != set(expected) or any(
payload.get(key) != value for key, value in expected.items()
):
_fail("EXECUTOR_REGISTRATION_BINDING_INVALID")
if (
not isinstance(payload["executor_id"], str)
or not payload["executor_id"]
or not isinstance(payload["nonce"], str)
or NONCE.fullmatch(payload["nonce"]) is None
):
_fail("EXECUTOR_REGISTRATION_IDENTITY_INVALID")
if (
type(payload["issued_at"]) is not int
or type(payload["expires_at"]) is not int
or payload["issued_at"] > now
or now >= payload["expires_at"]
or not 1 <= payload["expires_at"] - payload["issued_at"] <= MAX_TTL_SECONDS
or payload["issued_at"] < contract_payload["issued_at"]
or payload["expires_at"] > contract_payload["expires_at"]
):
_fail("EXECUTOR_REGISTRATION_EXPIRED")
if payload["support_commit"] != payload["integration_commit"]:
_fail("EXECUTOR_COMMIT_BINDING_INVALID")
if (
issuer_dependency is not None
and payload["expires_at"] > issuer_dependency["contract_expires_at"]
):
_fail("EXECUTOR_REGISTRATION_EXPIRED")
if (
not isinstance(payload["environment"], str)
or SCOPE_ID.fullmatch(payload["environment"]) is None
or not isinstance(payload["instance_id"], str)
or SCOPE_ID.fullmatch(payload["instance_id"]) is None
):
_fail("EXECUTOR_ENVIRONMENT_BINDING_INVALID")
if payload["source_commit"] != R14_SOURCE_COMMIT:
_fail("EXECUTOR_SOURCE_COMMIT_INVALID")
if (
contract_payload["schema"] != SUCCESSOR_SCHEMA
or contract_payload["source_commit"] != R14_SOURCE_COMMIT
):
_fail("EXECUTOR_CONTRACT_GENERATION_INVALID")
effects = TARGET_EFFECTS.get(payload["target"])
if (
effects is None
or not isinstance(payload["effects"], list)
or frozenset(payload["effects"]) != effects
or len(payload["effects"]) != len(effects)
):
_fail("EXECUTOR_EFFECTS_INVALID")
if (
not isinstance(payload["capabilities"], list)
or frozenset(payload["capabilities"]) != CAPABILITIES
or len(payload["capabilities"]) != len(CAPABILITIES)
):
_fail("EXECUTOR_CAPABILITIES_INVALID")
_validate_issuer_dependency(issuer_dependency, contract_payload)
for name in (
"installer_module_sha256", "installer_script_sha256",
"executor_module_sha256", "authority_manifest_sha256",
"execution_digest", "contract_digest", "old_snapshot_sha256",
"new_manifest_sha256", "rollback_sha256",
):
_require_digest(payload[name], HEX64, f"EXECUTOR_{name.upper()}_INVALID")
def _verified_contract(
contract: Path, installer: Installer, *, now: int,
) -> tuple[dict[str, Any], str]:
return verify_signed_contract(
contract, installer.registry_path, now=now,
require_root_owner=installer.require_root_owner,
_layout=installer.layout,
)
def _verified_issuer_dependency(
issuer_contract: Path | None, installer: Installer, *, now: int,
contract_payload: Mapping[str, Any],
) -> dict[str, Any] | None:
if contract_payload["target"] == ISSUER_TARGET:
if issuer_contract is not None:
_fail("EXECUTOR_ISSUER_DEPENDENCY_INVALID")
return None
if issuer_contract is None:
_fail("EXECUTOR_ISSUER_DEPENDENCY_REQUIRED")
payload, digest = _verified_contract(issuer_contract, installer, now=now)
if payload["schema"] != SUCCESSOR_SCHEMA or payload["source_commit"] != R14_SOURCE_COMMIT:
_fail("EXECUTOR_ISSUER_DEPENDENCY_INVALID")
dependency = _issuer_dependency(payload, digest)
_validate_issuer_dependency(dependency, contract_payload)
return dependency
def issue_registration(
*, executor_id: str, nonce: str, ttl_seconds: int, contract: Path,
installer: Installer, provenance_manifest: Path,
registered_signer: RegisteredOwnerSignerAdapter,
clock: Callable[[], int], issuer_contract: Path | None = None,
require_root_owner: bool = True,
) -> dict[str, Any]:
"""Issue authorization after deriving every scope field from trusted input."""
if type(installer) is not Installer:
_fail("EXECUTOR_INSTALLER_TYPE_INVALID")
if not isinstance(registered_signer, RegisteredOwnerSignerAdapter):
_fail("REGISTERED_SIGNER_ADAPTER_REQUIRED")
if type(ttl_seconds) is not int or not 1 <= ttl_seconds <= MAX_TTL_SECONDS:
_fail("EXECUTOR_REGISTRATION_TTL_INVALID")
now = int(clock())
provenance = load_code_provenance(
provenance_manifest, require_root_owner=require_root_owner,
)
contract_payload, contract_digest = _verified_contract(contract, installer, now=now)
dependency = _verified_issuer_dependency(
issuer_contract, installer, now=now, contract_payload=contract_payload,
)
payload = _registration_payload(
executor_id=executor_id, nonce=nonce, issued_at=now,
expires_at=now + ttl_seconds, contract_payload=contract_payload,
contract_digest=contract_digest, provenance=provenance,
issuer_dependency=dependency,
)
_validate_payload(
payload, now=now, provenance=provenance,
contract_payload=contract_payload, contract_digest=contract_digest,
issuer_dependency=dependency,
)
if registered_signer.fingerprint != payload["owner_key_id"]:
_fail("EXECUTOR_SIGNER_BINDING_INVALID")
signature = registered_signer._sign_canonical_payload(
OWNER_NAMESPACE, canonical_bytes(payload),
)
return {"payload": payload, "signature": signature}
def load_registration(path: Path, *, require_root_owner: bool = True) -> dict[str, Any]:
raw = _secure_bytes(
Path(path), require_root_owner=require_root_owner, maximum=1024 * 1024,
)
try:
value = json.loads(raw, object_pairs_hook=_strict_object)
except (UnicodeError, json.JSONDecodeError) as exc:
raise ExecutorRegistrationError("EXECUTOR_REGISTRATION_INVALID") from exc
if not isinstance(value, dict) or set(value) != {"payload", "signature"}:
_fail("EXECUTOR_REGISTRATION_FIELDS_INVALID")
return value
class RegisteredInstallerExecutor:
def __init__(
self, *, owner_verifier: RegistryOwnerVerifier, installer: Installer,
provenance_manifest: Path, clock: Callable[[], int],
host: Callable[[], str] = socket.gethostname,
require_root_owner: bool = True,
) -> None:
if type(installer) is not Installer:
_fail("EXECUTOR_INSTALLER_TYPE_INVALID")
if not isinstance(owner_verifier, RegistryOwnerVerifier):
_fail("EXECUTOR_OWNER_VERIFIER_INVALID")
self.owner_verifier = owner_verifier
self.installer = installer
self.provenance_manifest = Path(provenance_manifest)
self.clock = clock
self.host = host
self.require_root_owner = require_root_owner
self.provenance = load_code_provenance(
self.provenance_manifest, require_root_owner=require_root_owner,
)
def _check_code_provenance(self) -> None:
current = load_code_provenance(
self.provenance_manifest, require_root_owner=self.require_root_owner,
)
if current != self.provenance:
_fail("EXECUTOR_CODE_PROVENANCE_CHANGED")
def _verify(
self, registration: Mapping[str, Any], contract: Path,
) -> tuple[dict[str, Any], str, dict[str, Any]]:
self._check_code_provenance()
if (
not isinstance(registration, Mapping)
or set(registration) != {"payload", "signature"}
or not isinstance(registration["payload"], Mapping)
):
_fail("EXECUTOR_REGISTRATION_FIELDS_INVALID")
if self.host() != HOST:
_fail("EXECUTOR_HOST_MISMATCH")
now = int(self.clock())
contract_payload, contract_digest = _verified_contract(
contract, self.installer, now=now,
)
payload = dict(registration["payload"])
dependency = payload.get("issuer_dependency")
_validate_payload(
payload, now=now, provenance=self.provenance,
contract_payload=contract_payload, contract_digest=contract_digest,
issuer_dependency=dependency,
)
if payload["owner_key_id"] != self.owner_verifier.record.fingerprint:
_fail("EXECUTOR_OWNER_KEY_BINDING_INVALID")
signature = registration["signature"]
if (
not isinstance(signature, str)
or not self.owner_verifier.verify(
namespace=OWNER_NAMESPACE, payload=canonical_bytes(payload),
signature=signature, key_id=payload["owner_key_id"],
)
):
_fail("EXECUTOR_OWNER_SIGNATURE_INVALID")
return payload, contract_digest, contract_payload
def dispatch(
self, registration: Mapping[str, Any], contract: Path,
artifact: Path, operation: str, *,
issuer_contract: Path | None = None,
issuer_artifact: Path | None = None,
) -> dict[str, Any]:
payload, contract_digest, contract_payload = self._verify(registration, contract)
capability = "install" if operation in {"preflight", "install"} else operation
if capability not in CAPABILITIES:
_fail("EXECUTOR_OPERATION_INVALID")
if capability not in payload["capabilities"]:
_fail("EXECUTOR_CAPABILITY_INVALID")
if operation == "rollback" and contract_payload["target"] == BRIDGE_TARGET:
if issuer_artifact is None:
_fail("EXECUTOR_ISSUER_DEPENDENCY_REQUIRED")
actual_dependency = _verified_issuer_dependency(
issuer_contract, self.installer, now=int(self.clock()),
contract_payload=contract_payload,
)
if actual_dependency != payload["issuer_dependency"]:
_fail("EXECUTOR_ISSUER_DEPENDENCY_MISMATCH")
elif issuer_contract is not None or issuer_artifact is not None:
_fail("EXECUTOR_ISSUER_DEPENDENCY_UNEXPECTED")
if operation == "preflight":
result = self.installer.preflight(contract, artifact)
elif operation == "install":
result = self.installer.apply(contract, artifact)
elif operation == "rollback":
result = self.installer.rollback_applied(
contract, artifact, issuer_contract=issuer_contract,
issuer_artifact=issuer_artifact,
)
else:
result = self.installer.reconcile_unknown(contract, artifact)
return {
**result,
"operation": operation,
"executor_id": payload["executor_id"],
"contract_digest": contract_digest,
"code_binding": {
"integration_commit": payload["integration_commit"],
"integration_tree": payload["integration_tree"],
"installer_module_sha256": payload["installer_module_sha256"],
"installer_script_sha256": payload["installer_script_sha256"],
"executor_module_sha256": payload["executor_module_sha256"],
},
"registration_policy_digest": _sha256_bytes(canonical_bytes(payload)),
}
def _parser() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser(
description="Run one Owner-registered fixed-scope C5 installer operation.",
)
parser.add_argument(
"operation", choices=("preflight", "install", "rollback", "reconcile"),
)
parser.add_argument("--registration", type=Path, required=True)
parser.add_argument("--contract", type=Path, required=True)
parser.add_argument("--artifact", type=Path, required=True)
parser.add_argument("--issuer-contract", type=Path)
parser.add_argument("--issuer-artifact", type=Path)
return parser
def main(argv: list[str] | None = None) -> int:
"""Production CLI with fixed registry and provenance authority paths."""
args = _parser().parse_args(argv)
try:
registration = load_registration(args.registration)
payload = registration.get("payload")
if not isinstance(payload, Mapping) or not isinstance(payload.get("owner_signer_id"), str):
_fail("EXECUTOR_REGISTRATION_FIELDS_INVALID")
installer = Installer(DEFAULT_REGISTRY)
verifier = RegistryOwnerVerifier(
DEFAULT_REGISTRY, owner_signer_id=payload["owner_signer_id"],
)
executor = RegisteredInstallerExecutor(
owner_verifier=verifier, installer=installer,
provenance_manifest=DEFAULT_PROVENANCE_MANIFEST,
clock=lambda: int(time.time()),
)
result = executor.dispatch(
registration, args.contract, args.artifact, args.operation,
issuer_contract=args.issuer_contract,
issuer_artifact=args.issuer_artifact,
)
print(json.dumps(result, sort_keys=True))
return 0
except InstallationOutcomeUnknown as exc:
print(
json.dumps({"status": "UNKNOWN", "error": exc.code}, sort_keys=True),
file=sys.stderr,
)
return 3
except (ExecutorRegistrationError, InstallationError, OSError, ValueError, TypeError) as exc:
print(
json.dumps(
{"status": "REJECTED", "error": getattr(exc, "code", str(exc))},
sort_keys=True,
),
file=sys.stderr,
)
return 2
if __name__ == "__main__":
raise SystemExit(main())
@@ -0,0 +1,977 @@
#!/usr/bin/env python3
"""Root-owned one-shot assembly for the exact V2R2C5 release Saga.
This module only assembles existing hardened Saga phases. Each mutating phase
is a SHA-pinned process adapter; no caller-provided Python callable is accepted.
The staging HTTP service remains shadow-only and does not import this launcher.
"""
from __future__ import annotations
import argparse
from dataclasses import dataclass
import hashlib
import json
import os
from pathlib import Path
import stat
import subprocess
import time
from typing import Any, Mapping, Sequence
import approved_release_executor as approved_executor_module
from approved_release_executor import run_adapter
import c5_exact_deployment_adapter as exact_deployment_module
from c5_exact_deployment_adapter import (
C4_ROLLBACK_ARTIFACT_PATH,
C4_ROLLBACK_TARGET,
C5ExactDeploymentAdapter,
C5_PRODUCTION_ENVIRONMENT_FILE,
C5_PRODUCTION_PORT,
C5_PRODUCTION_SERVICE,
C5_VERSION,
RegisteredC5DeploymentExecutor,
)
from release_source_admission import open_source_admission_store
from silent_release_controller import ReleaseCandidate, ReleaseCandidateStore
from silent_release_hardening import (
ApprovalKeyRing,
ApprovalRevocationStore,
BaselineController,
CapabilityAuthorizationError,
CapabilityLedger,
ClaimProbeResult,
GovernedFinalizationCoordinator,
HardenedTagController,
PreSwitchValidator,
RegistryController,
ReleaseOrchestrator,
RecoverableReleaseError,
ReleasePlan,
ReleasePlanStore,
ReleaseSagaStore,
SignedApprovalStore,
SignedApprovalVerifier,
TagPlan,
TagPublicationConflict,
TagPublicationProbeEvidence,
TrustedReleasePolicy,
)
SCHEMA = "aios.c5.release-execution-host.v1"
PROVENANCE_SCHEMA = "aios.c5.release-execution-provenance.v1"
REQUEST_SCHEMA = "aios.production-executor-request.v1"
REPOSITORY = "aios/aios"
SCOPES = frozenset({"PRODUCTION", "ISOLATED_REHEARSAL"})
SHA256 = __import__("re").compile(r"^[0-9a-f]{64}$")
OID = __import__("re").compile(r"^[0-9a-f]{40}$")
class C5ExecutionHostBlocked(RuntimeError):
"""Exact host assembly or phase verification failed closed."""
def _sha256(path: Path) -> str:
digest = hashlib.sha256()
with path.open("rb") as source:
for chunk in iter(lambda: source.read(1024 * 1024), b""):
digest.update(chunk)
return digest.hexdigest()
def _secure_file(
path: Path,
expected_sha256: str,
*,
expected_owner_uid: int | None,
) -> Path:
path = Path(path)
if (
not path.is_absolute()
or path.is_symlink()
or path.resolve(strict=False) != path
):
raise C5ExecutionHostBlocked("HOST_FILE_PATH_INVALID")
try:
info = path.stat()
except OSError as exc:
raise C5ExecutionHostBlocked("HOST_FILE_UNAVAILABLE") from exc
if not stat.S_ISREG(info.st_mode) or (
os.name != "nt" and stat.S_IMODE(info.st_mode) & 0o022
):
raise C5ExecutionHostBlocked("HOST_FILE_SECURITY_INVALID")
if expected_owner_uid is not None and info.st_uid != expected_owner_uid:
raise C5ExecutionHostBlocked("HOST_FILE_OWNER_INVALID")
if expected_owner_uid is not None:
parent = path.parent
while True:
parent_info = parent.stat()
if (
parent.is_symlink()
or parent_info.st_uid != expected_owner_uid
or stat.S_IMODE(parent_info.st_mode) & 0o022
):
raise C5ExecutionHostBlocked("HOST_FILE_PARENT_SECURITY_INVALID")
if parent == parent.parent:
break
parent = parent.parent
if not SHA256.fullmatch(expected_sha256) or _sha256(path) != expected_sha256:
raise C5ExecutionHostBlocked("HOST_FILE_DIGEST_MISMATCH")
return path
def _absolute(value: Any, name: str) -> Path:
path = Path(str(value or ""))
if not path.is_absolute():
raise C5ExecutionHostBlocked(f"{name}_PATH_INVALID")
return path
@dataclass(frozen=True, slots=True)
class RegisteredPhaseCommand:
executable: Path
executable_sha256: str
adapter_path: Path
adapter_sha256: str
arguments: tuple[str, ...] = ()
@classmethod
def from_mapping(cls, raw: Any) -> "RegisteredPhaseCommand":
if not isinstance(raw, Mapping) or set(raw) != {
"executable",
"executable_sha256",
"adapter_path",
"adapter_sha256",
"arguments",
}:
raise C5ExecutionHostBlocked("PHASE_COMMAND_SCHEMA_INVALID")
arguments = raw["arguments"]
if not isinstance(arguments, list) or not all(
isinstance(item, str) and item and "\0" not in item
for item in arguments
):
raise C5ExecutionHostBlocked("PHASE_COMMAND_ARGUMENTS_INVALID")
return cls(
executable=_absolute(raw["executable"], "PHASE_EXECUTABLE"),
executable_sha256=str(raw["executable_sha256"]),
adapter_path=_absolute(raw["adapter_path"], "PHASE_ADAPTER"),
adapter_sha256=str(raw["adapter_sha256"]),
arguments=tuple(arguments),
)
def verify(self, *, expected_owner_uid: int | None) -> None:
_secure_file(
self.executable,
self.executable_sha256,
expected_owner_uid=expected_owner_uid,
)
_secure_file(
self.adapter_path,
self.adapter_sha256,
expected_owner_uid=expected_owner_uid,
)
@property
def command(self) -> tuple[str, ...]:
return (str(self.executable), str(self.adapter_path), *self.arguments)
@dataclass(frozen=True, slots=True)
class C5HostProvenance:
environment: str
instance_id: str
authority_commit: str
authority_tree: str
entries: Mapping[str, tuple[Path, str]]
manifest_sha256: str
@classmethod
def load(
cls,
path: Path,
*,
expected_sha256: str,
expected_owner_uid: int | None = 0,
) -> "C5HostProvenance":
path = _secure_file(
Path(path), expected_sha256,
expected_owner_uid=expected_owner_uid,
)
try:
raw = json.loads(path.read_text(encoding="utf-8"))
except (OSError, ValueError) as exc:
raise C5ExecutionHostBlocked("HOST_PROVENANCE_INVALID") from exc
labels = {
"host", "exact_deployment", "deployment", "approved_executor",
"tag", "registry", "baseline",
}
if (
not isinstance(raw, Mapping)
or set(raw) != {
"schema", "environment", "instance_id",
"authority_commit", "authority_tree", "entries",
}
or raw["schema"] != PROVENANCE_SCHEMA
or raw["environment"] not in SCOPES
or not isinstance(raw["entries"], Mapping)
or set(raw["entries"]) != labels
):
raise C5ExecutionHostBlocked("HOST_PROVENANCE_INVALID")
instance_id = str(raw["instance_id"] or "")
if not instance_id or len(instance_id) > 128:
raise C5ExecutionHostBlocked("HOST_PROVENANCE_INSTANCE_INVALID")
authority_commit = str(raw["authority_commit"])
authority_tree = str(raw["authority_tree"])
if not OID.fullmatch(authority_commit) or not OID.fullmatch(authority_tree):
raise C5ExecutionHostBlocked("HOST_PROVENANCE_AUTHORITY_INVALID")
actual_loaded = {
"host": Path(__file__).resolve(strict=True),
"exact_deployment": Path(exact_deployment_module.__file__).resolve(strict=True),
"approved_executor": Path(approved_executor_module.__file__).resolve(strict=True),
}
entries: dict[str, tuple[Path, str]] = {}
for label, item in raw["entries"].items():
if not isinstance(item, Mapping) or set(item) != {"path", "sha256"}:
raise C5ExecutionHostBlocked("HOST_PROVENANCE_ENTRY_INVALID")
entry_path = _absolute(item["path"], "HOST_PROVENANCE_ENTRY")
digest = str(item["sha256"])
_secure_file(
entry_path, digest, expected_owner_uid=expected_owner_uid
)
if label in actual_loaded and entry_path != actual_loaded[label]:
raise C5ExecutionHostBlocked("HOST_PROVENANCE_LOADED_PATH_MISMATCH")
entries[label] = (entry_path, digest)
return cls(
environment=str(raw["environment"]),
instance_id=instance_id,
authority_commit=authority_commit,
authority_tree=authority_tree,
entries=entries,
manifest_sha256=expected_sha256,
)
@dataclass(frozen=True, slots=True)
class C5ExecutionHostConfig:
host_provenance: C5HostProvenance
executor_authority_commit: str
executor_authority_tree: str
execution_scope: str
instance_id: str
provenance: Mapping[str, Any]
candidate_id: str
source_commit: str
tree_oid: str
artifact_sha256: str
release_plan_sha256: str
approval_fingerprint: str
state_root: Path
trusted_policy_path: Path
trusted_policy_sha256: str
approval_keyring_path: Path
approval_keyring_sha256: str
source_admission_root: Path
source_admission_policy_path: Path
source_admission_policy_sha256: str
source_admission_task_id: str
source_admission_hmac_path: Path
source_admission_hmac_sha256: str
deployment_executable: Path
deployment_executable_sha256: str
deployment_adapter_path: Path
deployment_adapter_sha256: str
artifact_path: Path
rollback_target: Mapping[str, str]
rollback_artifact_path: Path
production_environment_file: Path
authoritative_git_dir: Path
git_executable: Path
git_executable_sha256: str
tag_command: RegisteredPhaseCommand
registry_command: RegisteredPhaseCommand
baseline_command: RegisteredPhaseCommand
registry_record_path: Path
baseline_record_path: Path
@classmethod
def load(
cls,
path: Path,
*,
expected_sha256: str,
host_provenance: C5HostProvenance,
expected_owner_uid: int | None = 0,
) -> "C5ExecutionHostConfig":
path = _secure_file(
Path(path),
expected_sha256,
expected_owner_uid=expected_owner_uid,
)
try:
raw = json.loads(path.read_text(encoding="utf-8"))
except (OSError, ValueError) as exc:
raise C5ExecutionHostBlocked("HOST_CONFIG_INVALID") from exc
keys = {
"schema", "version", "execution_scope", "instance_id",
"executor_authority_commit", "executor_authority_tree",
"provenance", "candidate_id",
"source_commit", "tree_oid", "artifact_sha256",
"release_plan_sha256", "approval_fingerprint", "state_root",
"trusted_policy_path", "trusted_policy_sha256",
"approval_keyring_path", "approval_keyring_sha256",
"source_admission_root", "source_admission_policy_path",
"source_admission_policy_sha256", "source_admission_task_id", "source_admission_hmac_path",
"source_admission_hmac_sha256", "deployment_executable",
"deployment_executable_sha256", "deployment_adapter_path",
"deployment_adapter_sha256", "artifact_path",
"rollback_target", "rollback_artifact_path",
"production_environment_file", "production_service",
"production_port", "repository_identity", "global_legacy_writes_enabled",
"authoritative_git_dir", "git_executable", "git_executable_sha256",
"tag_command", "registry_command", "baseline_command",
"registry_record_path", "baseline_record_path",
}
if not isinstance(raw, Mapping) or set(raw) != keys:
raise C5ExecutionHostBlocked("HOST_CONFIG_SCHEMA_INVALID")
rollback_artifact_path = _absolute(raw["rollback_artifact_path"], "ROLLBACK_ARTIFACT")
production_environment_file = _absolute(raw["production_environment_file"], "PRODUCTION_ENVIRONMENT_FILE")
exact = (
raw["schema"] == SCHEMA
and raw["version"] == C5_VERSION
and raw["execution_scope"] in SCOPES
and isinstance(raw["rollback_target"], Mapping)
and raw["production_service"] == C5_PRODUCTION_SERVICE
and raw["production_port"] == C5_PRODUCTION_PORT
and raw["repository_identity"] == REPOSITORY
and raw["global_legacy_writes_enabled"] is False
)
if not exact:
raise C5ExecutionHostBlocked("HOST_CONFIG_BINDING_INVALID")
provenance = raw["provenance"]
expected_provenance = (
{
"environment": "ISOLATED_REHEARSAL",
"fixture": True,
"r14_source_receipt": False,
}
if raw["execution_scope"] == "ISOLATED_REHEARSAL"
else {
"environment": "PRODUCTION",
"fixture": False,
"r14_source_receipt": True,
}
)
if provenance != expected_provenance:
raise C5ExecutionHostBlocked("HOST_PROVENANCE_INVALID")
instance_id = str(raw["instance_id"] or "")
if not instance_id or len(instance_id) > 128 or any(
character not in "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_.-"
for character in instance_id
):
raise C5ExecutionHostBlocked("HOST_INSTANCE_ID_INVALID")
for name, pattern in (
("executor_authority_commit", OID),
("executor_authority_tree", OID),
("source_commit", OID), ("tree_oid", OID),
("artifact_sha256", SHA256),
("release_plan_sha256", SHA256),
("approval_fingerprint", SHA256),
):
if pattern.fullmatch(str(raw[name])) is None:
raise C5ExecutionHostBlocked(f"{name.upper()}_INVALID")
value = cls(
host_provenance=host_provenance,
executor_authority_commit=str(raw["executor_authority_commit"]),
executor_authority_tree=str(raw["executor_authority_tree"]),
execution_scope=str(raw["execution_scope"]),
instance_id=instance_id,
provenance=dict(provenance),
candidate_id=str(raw["candidate_id"]),
source_commit=str(raw["source_commit"]),
tree_oid=str(raw["tree_oid"]),
artifact_sha256=str(raw["artifact_sha256"]),
release_plan_sha256=str(raw["release_plan_sha256"]),
approval_fingerprint=str(raw["approval_fingerprint"]),
state_root=_absolute(raw["state_root"], "STATE_ROOT"),
trusted_policy_path=_absolute(raw["trusted_policy_path"], "TRUSTED_POLICY"),
trusted_policy_sha256=str(raw["trusted_policy_sha256"]),
approval_keyring_path=_absolute(raw["approval_keyring_path"], "APPROVAL_KEYRING"),
approval_keyring_sha256=str(raw["approval_keyring_sha256"]),
source_admission_root=_absolute(raw["source_admission_root"], "SOURCE_ADMISSION_ROOT"),
source_admission_policy_path=_absolute(raw["source_admission_policy_path"], "SOURCE_ADMISSION_POLICY"),
source_admission_policy_sha256=str(raw["source_admission_policy_sha256"]),
source_admission_task_id=str(raw["source_admission_task_id"]),
source_admission_hmac_path=_absolute(raw["source_admission_hmac_path"], "SOURCE_ADMISSION_HMAC"),
source_admission_hmac_sha256=str(raw["source_admission_hmac_sha256"]),
deployment_executable=_absolute(raw["deployment_executable"], "DEPLOYMENT_EXECUTABLE"),
deployment_executable_sha256=str(raw["deployment_executable_sha256"]),
deployment_adapter_path=_absolute(raw["deployment_adapter_path"], "DEPLOYMENT_ADAPTER"),
deployment_adapter_sha256=str(raw["deployment_adapter_sha256"]),
artifact_path=_absolute(raw["artifact_path"], "ARTIFACT"),
rollback_target=dict(raw["rollback_target"]),
rollback_artifact_path=rollback_artifact_path,
production_environment_file=production_environment_file,
authoritative_git_dir=_absolute(raw["authoritative_git_dir"], "AUTHORITATIVE_GIT_DIR"),
git_executable=_absolute(raw["git_executable"], "GIT_EXECUTABLE"),
git_executable_sha256=str(raw["git_executable_sha256"]),
tag_command=RegisteredPhaseCommand.from_mapping(raw["tag_command"]),
registry_command=RegisteredPhaseCommand.from_mapping(raw["registry_command"]),
baseline_command=RegisteredPhaseCommand.from_mapping(raw["baseline_command"]),
registry_record_path=_absolute(raw["registry_record_path"], "REGISTRY_RECORD"),
baseline_record_path=_absolute(raw["baseline_record_path"], "BASELINE_RECORD"),
)
if (not value.source_admission_task_id
or len(value.source_admission_task_id) > 160
or any(character not in "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_.:-"
for character in value.source_admission_task_id)):
raise C5ExecutionHostBlocked("SOURCE_ADMISSION_TASK_ID_INVALID")
if value.execution_scope == "ISOLATED_REHEARSAL":
rehearsal_root = value.state_root.resolve(strict=False)
lowered_parts = {part.lower() for part in rehearsal_root.parts}
if (
"isolated-rehearsal" not in lowered_parts
or value.instance_id not in rehearsal_root.parts
):
raise C5ExecutionHostBlocked("REHEARSAL_ROOT_NOT_ISOLATED")
for fixture_path in (
value.source_admission_root,
value.artifact_path,
value.rollback_artifact_path,
value.production_environment_file,
value.registry_record_path,
value.baseline_record_path,
):
try:
fixture_path.resolve(strict=False).relative_to(rehearsal_root)
except ValueError as exc:
raise C5ExecutionHostBlocked(
"REHEARSAL_FIXTURE_OUTSIDE_ISOLATION_ROOT"
) from exc
if value.execution_scope == "ISOLATED_REHEARSAL":
if set(value.rollback_target) != set(C4_ROLLBACK_TARGET) or value.rollback_target.get("version") != C4_ROLLBACK_TARGET["version"] or value.rollback_target.get("target_oid") != C4_ROLLBACK_TARGET["target_oid"]:
raise C5ExecutionHostBlocked("REHEARSAL_ROLLBACK_TARGET_INVALID")
if SHA256.fullmatch(str(value.rollback_target.get("artifact_sha256", ""))) is None:
raise C5ExecutionHostBlocked("REHEARSAL_ROLLBACK_TARGET_INVALID")
if (
host_provenance.environment != value.execution_scope
or host_provenance.instance_id != value.instance_id
or host_provenance.authority_commit
!= value.executor_authority_commit
or host_provenance.authority_tree != value.executor_authority_tree
):
raise C5ExecutionHostBlocked("HOST_PROVENANCE_BINDING_MISMATCH")
if value.execution_scope == "PRODUCTION" and (
value.rollback_target != C4_ROLLBACK_TARGET
or value.rollback_artifact_path != C4_ROLLBACK_ARTIFACT_PATH
or value.production_environment_file != C5_PRODUCTION_ENVIRONMENT_FILE
):
raise C5ExecutionHostBlocked("HOST_CONFIG_BINDING_INVALID")
expected_adapter_paths = {
"deployment": value.deployment_adapter_path,
"tag": value.tag_command.adapter_path,
"registry": value.registry_command.adapter_path,
"baseline": value.baseline_command.adapter_path,
}
if any(
host_provenance.entries[label][0] != path
for label, path in expected_adapter_paths.items()
):
raise C5ExecutionHostBlocked("HOST_PROVENANCE_ADAPTER_MISMATCH")
production_names = {
"deployment": "c5_production_deployment_adapter.py",
"tag": "governed_tag_adapter.py",
"registry": "release_metadata_adapter.py",
"baseline": "release_metadata_adapter.py",
}
observed_names = {
label: path.name for label, path in expected_adapter_paths.items()
}
if value.execution_scope == "PRODUCTION":
if observed_names != production_names:
raise C5ExecutionHostBlocked("PRODUCTION_ADAPTER_IDENTITY_INVALID")
elif any(
observed_names[label] == production_names[label]
for label in production_names
):
raise C5ExecutionHostBlocked("REHEARSAL_PRODUCTION_ADAPTER_FORBIDDEN")
value.verify_files(expected_owner_uid=expected_owner_uid)
return value
def verify_files(self, *, expected_owner_uid: int | None) -> None:
for path, digest in (
(self.trusted_policy_path, self.trusted_policy_sha256),
(self.approval_keyring_path, self.approval_keyring_sha256),
(self.source_admission_policy_path, self.source_admission_policy_sha256),
(self.source_admission_hmac_path, self.source_admission_hmac_sha256),
(self.git_executable, self.git_executable_sha256),
):
_secure_file(path, digest, expected_owner_uid=expected_owner_uid)
for command in (
self.tag_command, self.registry_command, self.baseline_command
):
command.verify(expected_owner_uid=expected_owner_uid)
def _load_policy(path: Path) -> TrustedReleasePolicy:
raw = json.loads(path.read_text(encoding="utf-8"))
required = {
"services", "release_roots", "cutover_links", "dropin_paths",
"authoritative_git_dirs", "repository_identities",
}
if not isinstance(raw, Mapping) or set(raw) != required:
raise C5ExecutionHostBlocked("TRUSTED_POLICY_SCHEMA_INVALID")
values: dict[str, frozenset[str]] = {}
for name in required:
items = raw[name]
if not isinstance(items, list) or not items or not all(
isinstance(item, str) and item for item in items
):
raise C5ExecutionHostBlocked("TRUSTED_POLICY_VALUES_INVALID")
values[name] = frozenset(items)
return TrustedReleasePolicy(**values)
def _phase_request(candidate: ReleaseCandidate, plan: ReleasePlan, approval, operation: str) -> dict[str, Any]:
return {
"schema": REQUEST_SCHEMA,
"operation": operation,
"version": C5_VERSION,
"target_oid": candidate.source_commit,
"evidence_closure_oid": approval.evidence_closure_oid,
"artifact_sha256": candidate.artifact_sha256,
"approval_plan_id": approval.approval_plan_id,
"repository_identity": plan.deployment.repository_identity,
"production_service": plan.deployment.service,
"production_port": C5_PRODUCTION_PORT,
}
def _checked_phase_response(
value: Mapping[str, Any], request: Mapping[str, Any]
) -> dict[str, Any]:
for name in (
"version", "target_oid", "evidence_closure_oid",
"artifact_sha256", "approval_plan_id",
):
if value.get(name) != request[name]:
raise C5ExecutionHostBlocked(f"PHASE_RECEIPT_MISMATCH:{name}")
return dict(value)
class _PreSwitchProbe:
def __init__(self, config: C5ExecutionHostConfig, source_admissions):
self.config = config
self.source_admissions = source_admissions
def _git(self, *args: str) -> str:
completed = subprocess.run(
[str(self.config.git_executable), f"--git-dir={self.config.authoritative_git_dir}", *args],
check=False,
capture_output=True,
text=True,
timeout=45,
env={"PATH": os.environ.get("PATH", "")},
)
if completed.returncode != 0:
raise C5ExecutionHostBlocked("PRE_SWITCH_GIT_UNAVAILABLE")
return completed.stdout.strip()
def collect(self, candidate: ReleaseCandidate, plan) -> Mapping[str, Any]:
admitted = self.source_admissions.resolve_recipe(
C5_VERSION, candidate.candidate_id, candidate.source_commit
)
tree = self._git("rev-parse", f"{candidate.source_commit}^{{tree}}")
artifact_ok = (
self.config.artifact_path.is_file()
and _sha256(self.config.artifact_path) == candidate.artifact_sha256
)
rollback_path = (
C4_ROLLBACK_ARTIFACT_PATH
if self.config.execution_scope == "PRODUCTION"
else self.config.rollback_artifact_path
)
rollback_target = (
C4_ROLLBACK_TARGET
if self.config.execution_scope == "PRODUCTION"
else self.config.rollback_target
)
rollback_ok = (
rollback_path.is_file()
and _sha256(rollback_path) == rollback_target["artifact_sha256"]
)
return {
"source_commit": candidate.source_commit,
"tree_oid": tree,
"artifact_sha256": candidate.artifact_sha256 if artifact_ok else "",
"manifest_sha256": candidate.manifest_sha256,
"deployment_plan_sha256": plan.sha256,
"policy_sha256": plan.policy_sha256,
"rollback_ready": rollback_ok,
"git_refs_consistent": (
admitted.get("source_commit") == candidate.source_commit
and admitted.get("tree_oid") == tree
),
"repository_identity": plan.repository_identity,
"writer_disabled": os.environ.get(
"AIOS_GITOPS_WRITES_ENABLED", "false"
).lower() == "false",
}
class _TagPhase:
def __init__(self, command: RegisteredPhaseCommand, candidate, plan, approval, owner_uid):
self.command_config = command
self.candidate = candidate
self.plan = plan
self.approval = approval
self.owner_uid = owner_uid
def _call(self, operation: str) -> dict[str, Any]:
self.command_config.verify(expected_owner_uid=self.owner_uid)
request = _phase_request(self.candidate, self.plan, self.approval, operation)
return _checked_phase_response(
run_adapter(self.command_config.command, request), request
)
def probe_evidence(self, tag_plan: TagPlan) -> TagPublicationProbeEvidence:
value = self._call("probe")
status = value.get("status")
listing = value.get("gitea_listing_visible")
if status == "NOT_APPLIED":
listing = False
return TagPublicationProbeEvidence(
repository_identity=tag_plan.repository_identity,
remote_reachable=True,
gitea_api_reachable=True,
gitea_listing_reachable=True,
remote_tag_oid=value.get("remote_tag_oid"),
annotated_tag_object_oid=value.get("tag_oid"),
remote_peeled_target_oid=value.get("remote_peeled_target_oid"),
gitea_api_tag_oid=value.get("gitea_tag_oid"),
gitea_api_peeled_target_oid=value.get("gitea_peeled_target_oid"),
gitea_listing_visible=listing,
gitea_listing_tag_oid=(value.get("gitea_tag_oid") if listing else None),
gitea_listing_peeled_target_oid=(value.get("gitea_peeled_target_oid") if listing else None),
observed_at=time.time(),
)
def __call__(self, *, tag_plan: TagPlan) -> Mapping[str, Any]:
value = self._call("probe")
if value.get("status") in {"NOT_APPLIED", "PARTIAL_LOCAL"}:
value = self._call("apply")
value = self._call("probe")
if value.get("status") == "CONFLICT":
raise TagPublicationConflict(
"tag publication conflicts with the exact C5 plan"
)
if value.get("status") != "APPLIED_VERIFIED":
raise RecoverableReleaseError(
"tag publication outcome is indeterminate"
)
proof = self.probe_evidence(tag_plan)
if proof.remote_tag_oid != tag_plan.expected_tag_oid:
raise C5ExecutionHostBlocked("TAG_OBJECT_BINDING_MISMATCH")
return {
"verified": True,
"evidence_closure_oid": self.approval.evidence_closure_oid,
"approval_plan_id": self.approval.approval_plan_id,
"repository_identity": tag_plan.repository_identity,
"remote_tag_oid": proof.remote_tag_oid,
"remote_peeled_target_oid": proof.remote_peeled_target_oid,
"gitea_api_tag_oid": proof.gitea_api_tag_oid,
"gitea_api_peeled_target_oid": proof.gitea_api_peeled_target_oid,
"gitea_listing_visible": proof.gitea_listing_visible,
}
class _MetadataPhase:
def __init__(self, *, mode: str, command: RegisteredPhaseCommand, record_path: Path,
candidate, plan, approval, tag_phase: _TagPhase, owner_uid):
self.mode = mode
self.command_config = command
self.record_path = record_path
self.candidate = candidate
self.plan = plan
self.approval = approval
self.tag_phase = tag_phase
self.owner_uid = owner_uid
def _call(self, operation: str) -> dict[str, Any]:
self.command_config.verify(expected_owner_uid=self.owner_uid)
request = _phase_request(self.candidate, self.plan, self.approval, operation)
request["tag_oid"] = self.plan.tag.expected_tag_oid
return _checked_phase_response(run_adapter(self.command_config.command, request), request)
def __call__(self, *, finalization_plan) -> Mapping[str, Any]:
value = self._call("probe")
if value.get("status") == "NOT_APPLIED":
self._call("apply")
value = self._call("probe")
if value.get("status") != "VERIFIED":
raise C5ExecutionHostBlocked(f"{self.mode.upper()}_FINALIZATION_NOT_VERIFIED")
try:
record = json.loads(self.record_path.read_text(encoding="utf-8"))
except (OSError, ValueError) as exc:
raise C5ExecutionHostBlocked(f"{self.mode.upper()}_RECORD_INVALID") from exc
expected_record = (
finalization_plan.registry_expected_record
if self.mode == "registry"
else finalization_plan.baseline_expected_record
)
if any(record.get(key) != item for key, item in expected_record.items()):
raise C5ExecutionHostBlocked(f"{self.mode.upper()}_RECORD_BINDING_MISMATCH")
tag = self.tag_phase.probe_evidence(self.plan.tag)
if (
tag.remote_tag_oid != self.plan.tag.expected_tag_oid
or tag.remote_peeled_target_oid != self.candidate.source_commit
or tag.gitea_api_peeled_target_oid != self.candidate.source_commit
or tag.gitea_listing_visible is not True
):
raise C5ExecutionHostBlocked("FINALIZATION_TAG_PROOF_INVALID")
proof = {
"verified": True,
"tag_verified": True,
"remote_tag_oid": tag.remote_tag_oid,
"remote_peeled_target_oid": tag.remote_peeled_target_oid,
"gitea_api_peeled_target_oid": tag.gitea_api_peeled_target_oid,
"gitea_listing_visible": True,
"evidence_closure_oid": self.approval.evidence_closure_oid,
"approval_plan_id": self.approval.approval_plan_id,
f"{self.mode}_target_oid": self.candidate.source_commit,
}
if self.mode == "baseline":
proof.update(
{
"registry_record": finalization_plan.registry_expected_record,
"baseline_record": finalization_plan.baseline_expected_record,
"baseline_written_last": True,
}
)
return proof
@dataclass(slots=True)
class C5ExecutionHost:
config: C5ExecutionHostConfig
orchestrator: ReleaseOrchestrator
def execute(self) -> dict[str, Any]:
result = dict(self.orchestrator.execute(self.config.candidate_id))
production_receipt = (
self.config.execution_scope == "PRODUCTION"
and result.get("state") == "COMPLETED"
)
return {
**result,
"execution_scope": self.config.execution_scope,
"instance_id": self.config.instance_id,
"provenance": dict(self.config.provenance),
"host_authority_commit": self.config.host_provenance.authority_commit,
"host_authority_tree": self.config.host_provenance.authority_tree,
"host_provenance_sha256": self.config.host_provenance.manifest_sha256,
"fixture_evidence": self.config.execution_scope == "ISOLATED_REHEARSAL",
"production_receipt": production_receipt,
"public_business_e2e_verified": False,
"production_acceptance": False,
}
def _validate_source_admission_task(config, candidate, admitted) -> None:
if admitted.get("task_id") != config.source_admission_task_id:
raise C5ExecutionHostBlocked("SOURCE_ADMISSION_TASK_BINDING_INVALID")
def build_execution_host(
config: C5ExecutionHostConfig,
*,
expected_owner_uid: int | None = 0,
completion_approvals: Any = None,
) -> C5ExecutionHost:
if os.environ.get("AIOS_GITOPS_WRITES_ENABLED", "false").lower() != "false":
raise C5ExecutionHostBlocked("LEGACY_GLOBAL_WRITES_ENABLED")
config.verify_files(expected_owner_uid=expected_owner_uid)
candidates = ReleaseCandidateStore(config.state_root / "candidates")
plans = ReleasePlanStore(config.state_root / "release-plans")
approvals = SignedApprovalStore(config.state_root / "approvals")
completion_authority = None
if completion_approvals is not None:
from c5_completion_renewal import (
RegisteredCompletionApprovalStore, RegisteredCompletionAuthority,
CompletionRuntimeGuard,
)
if (type(completion_approvals) is not RegisteredCompletionApprovalStore
or type(completion_approvals.authority) is not RegisteredCompletionAuthority
or expected_owner_uid != 0 or config.execution_scope != "PRODUCTION"
or Path(__file__) != completion_approvals.authority.root / "c5_release_execution_host.py"
or Path(completion_approvals.original.root) != approvals.root):
raise C5ExecutionHostBlocked("REGISTERED_COMPLETION_DEPENDENCY_REQUIRED")
completion_authority = completion_approvals.authority
completion_authority.validate_host(
state_root=config.state_root, candidate_id=config.candidate_id,
source_commit=config.source_commit, tree_oid=config.tree_oid,
artifact_sha256=config.artifact_sha256,
release_plan_sha256=config.release_plan_sha256,
)
approvals = completion_approvals
sagas = ReleaseSagaStore(config.state_root / "sagas")
candidate = candidates.load(config.candidate_id)
plan = plans.load(config.candidate_id).validate()
approval = approvals.load(config.candidate_id)
observed = (
candidate.source_commit, candidate.tree_oid, candidate.artifact_sha256,
plan.sha256, approval.fingerprint,
)
expected = (
config.source_commit, config.tree_oid, config.artifact_sha256,
config.release_plan_sha256, config.approval_fingerprint,
)
if observed != expected or plan.tag.version != C5_VERSION:
raise C5ExecutionHostBlocked("HOST_DURABLE_BINDING_MISMATCH")
if (
plan.policy_sha256 != config.trusted_policy_sha256
or Path(plan.deployment.authoritative_git_dir)
!= config.authoritative_git_dir
):
raise C5ExecutionHostBlocked("HOST_ACTUAL_PLAN_BINDING_MISMATCH")
policy = _load_policy(config.trusted_policy_path)
policy.validate_deployment(plan.deployment)
verifier = SignedApprovalVerifier(
ApprovalKeyRing.from_file(config.approval_keyring_path),
ApprovalRevocationStore(config.state_root / "revocations"),
)
verifier.verify_release_plan(approval, candidate, plan, plan.policy_sha256)
hmac_key = config.source_admission_hmac_path.read_bytes().strip()
source_admissions = open_source_admission_store(
config.source_admission_root,
repository=REPOSITORY,
policy=config.source_admission_policy_path,
hmac_key=hmac_key,
expected_owner_uid=expected_owner_uid,
writable=False,
)
admitted = source_admissions.resolve_recipe(
C5_VERSION, candidate.candidate_id, candidate.source_commit
)
_validate_source_admission_task(config, candidate, admitted)
deployment_executor = RegisteredC5DeploymentExecutor(
executor_id="c5-production-runtime-switch-v1",
executable=config.deployment_executable,
executable_sha256=config.deployment_executable_sha256,
adapter_path=config.deployment_adapter_path,
adapter_sha256=config.deployment_adapter_sha256,
artifact_path=config.artifact_path,
rollback_artifact_path=config.rollback_artifact_path,
production_environment_file=config.production_environment_file,
execution_scope=config.execution_scope,
isolation_root=(config.state_root if config.execution_scope == "ISOLATED_REHEARSAL" else None),
rollback_target=config.rollback_target,
require_root_owner=expected_owner_uid == 0,
)
runtime = C5ExactDeploymentAdapter(
candidate=candidate,
release_plan=plan,
approval=approval,
approval_verifier=verifier,
source_admissions=source_admissions,
executor=deployment_executor,
)
ledger = CapabilityLedger(config.state_root / "capabilities")
tag_phase = _TagPhase(
config.tag_command, candidate, plan, approval, expected_owner_uid
)
tag = HardenedTagController(
ledger,
tag_phase,
expected_repository=REPOSITORY,
publication_probe=tag_phase.probe_evidence,
executor_id="c5-production-tag-v1",
)
registry_adapter = _MetadataPhase(
mode="registry", command=config.registry_command,
record_path=config.registry_record_path, candidate=candidate,
plan=plan, approval=approval, tag_phase=tag_phase,
owner_uid=expected_owner_uid,
)
baseline_adapter = _MetadataPhase(
mode="baseline", command=config.baseline_command,
record_path=config.baseline_record_path, candidate=candidate,
plan=plan, approval=approval, tag_phase=tag_phase,
owner_uid=expected_owner_uid,
)
finalizer = GovernedFinalizationCoordinator(
RegistryController(ledger, registry_adapter, executor_id="c5-production-registry-v1"),
BaselineController(ledger, baseline_adapter, executor_id="c5-production-baseline-v1"),
)
def actual_plan_factory(actual_candidate: ReleaseCandidate) -> ReleasePlan:
actual = plans.load(actual_candidate.candidate_id).validate()
runtime._verify_binding(actual_candidate, actual)
return actual
def runtime_verify(*, candidate: ReleaseCandidate, plan: ReleasePlan) -> Mapping[str, Any]:
runtime._verify_binding(candidate, plan)
result = runtime._invoke("probe")
if runtime._classify(result) != ClaimProbeResult.APPLIED_VERIFIED:
raise C5ExecutionHostBlocked("RUNTIME_VERIFY_INDETERMINATE")
return {
"verified": True,
"source_commit": candidate.source_commit,
"tree_oid": candidate.tree_oid,
"artifact_sha256": candidate.artifact_sha256,
"manifest_sha256": candidate.manifest_sha256,
"repository_identity": plan.deployment.repository_identity,
"authoritative_git_dir": plan.deployment.authoritative_git_dir,
"writer_disabled": True,
}
orchestrator = ReleaseOrchestrator(
candidates=candidates,
plans=plans,
approvals=approvals,
sagas=sagas,
approval_verifier=verifier,
trusted_policy=policy,
actual_plan_factory=actual_plan_factory,
pre_switch_validator=PreSwitchValidator(
_PreSwitchProbe(config, source_admissions)
),
runtime_switch=(CompletionRuntimeGuard(completion_authority) if completion_authority
else runtime.controller(ledger)),
runtime_verify=runtime_verify,
runtime_rollback=(completion_authority.deny_runtime_effect if completion_authority
else runtime.rollback),
tag_publish=tag,
finalizer=finalizer,
)
return C5ExecutionHost(config=config, orchestrator=orchestrator)
def main(argv: Sequence[str] | None = None) -> int:
parser = argparse.ArgumentParser()
parser.add_argument("--config", type=Path, required=True)
parser.add_argument("--config-sha256", required=True)
parser.add_argument("--provenance", type=Path, required=True)
parser.add_argument("--provenance-sha256", required=True)
args = parser.parse_args(argv)
provenance = C5HostProvenance.load(
args.provenance,
expected_sha256=args.provenance_sha256,
expected_owner_uid=0,
)
config = C5ExecutionHostConfig.load(
args.config,
expected_sha256=args.config_sha256,
host_provenance=provenance,
expected_owner_uid=0,
)
result = build_execution_host(config, expected_owner_uid=0).execute()
print(json.dumps(result, sort_keys=True))
return 0
if __name__ == "__main__":
try:
raise SystemExit(main())
except (C5ExecutionHostBlocked, CapabilityAuthorizationError) as exc:
print(json.dumps({"status": "FAIL_CLOSED", "error": type(exc).__name__}))
raise SystemExit(2)
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,326 @@
"""Fail-closed component deployment identity projection.
This module deliberately does not discover a trust root or treat process
environment/health responses as runtime evidence. A host supplies a trusted
comparison callback after it has performed its own authenticated observation.
"""
from __future__ import annotations
import hashlib
import json
import math
import re
import time
from pathlib import Path
from typing import Any, Callable, Mapping
_OID = re.compile(r"^[0-9a-f]{40}$")
_SHA256 = re.compile(r"^[0-9a-f]{64}$")
_COMPONENT_SCHEMA = "aios.component-manifest.v1"
_RECEIPT_SCHEMA = "aios.component-artifact-receipt.v1"
class RuntimeIdentityError(ValueError):
"""A declared or observed identity has an invalid shape."""
def _strict_object(pairs: list[tuple[str, Any]]) -> dict[str, Any]:
result: dict[str, Any] = {}
for key, value in pairs:
if key in result:
raise RuntimeIdentityError("DUPLICATE_FIELD")
result[key] = value
return result
def _load_json(path: Path) -> Mapping[str, Any]:
try:
metadata = path.lstat()
if not path.is_file() or path.is_symlink() or metadata.st_size > 2 * 1024 * 1024:
raise RuntimeIdentityError("IDENTITY_FILE_INVALID")
value = json.loads(path.read_text(encoding="utf-8"), object_pairs_hook=_strict_object)
except (OSError, UnicodeError, TypeError, json.JSONDecodeError) as exc:
raise RuntimeIdentityError("IDENTITY_FILE_INVALID") from exc
if not isinstance(value, Mapping):
raise RuntimeIdentityError("IDENTITY_OBJECT_REQUIRED")
return value
def _require_sha(value: object, field: str) -> str:
if not isinstance(value, str) or _SHA256.fullmatch(value) is None:
raise RuntimeIdentityError(f"{field.upper()}_INVALID")
return value
def _require_oid(value: object, field: str) -> str:
if not isinstance(value, str) or _OID.fullmatch(value) is None:
raise RuntimeIdentityError(f"{field.upper()}_INVALID")
return value
def _require_text(value: object, field: str) -> str:
if not isinstance(value, str) or not value.strip():
raise RuntimeIdentityError(f"{field.upper()}_INVALID")
return value
def _canonical(value: object) -> str:
try:
return json.dumps(value, sort_keys=True, separators=(",", ":"), ensure_ascii=False, allow_nan=False)
except (TypeError, ValueError) as exc:
raise RuntimeIdentityError("IDENTITY_VALUE_INVALID") from exc
def _validate_modules(value: object) -> dict[str, str]:
if not isinstance(value, Mapping) or not value:
raise RuntimeIdentityError("MODULES_INVALID")
modules: dict[str, str] = {}
for path, digest in value.items():
name = _require_text(path, "module_path")
if name.startswith("/") or ".." in Path(name).parts:
raise RuntimeIdentityError("MODULE_PATH_INVALID")
modules[name] = _require_sha(digest, "module_sha256")
return dict(sorted(modules.items()))
def _validate_extra_files(value: object) -> dict[str, str]:
if not isinstance(value, Mapping):
raise RuntimeIdentityError("EXTRA_FILES_INVALID")
files: dict[str, str] = {}
for path, digest in value.items():
name = _require_text(path, "extra_file_path")
if name.startswith("/") or ".." in Path(name).parts:
raise RuntimeIdentityError("EXTRA_FILE_PATH_INVALID")
files[name] = _require_sha(digest, "extra_file_sha256")
return dict(sorted(files.items()))
def _relative_artifact_path(value: object, field: str, *, allow_current: bool = False) -> str:
name = _require_text(value, field)
path = Path(name)
if "\\" in name or path.is_absolute() or ".." in path.parts or (name == "." and not allow_current):
raise RuntimeIdentityError(f"{field.upper()}_INVALID")
return name
def _validate_start_identity(value: object) -> dict[str, Any]:
if not isinstance(value, Mapping) or set(value) != {"entrypoint", "config_path", "python_path"}:
raise RuntimeIdentityError("START_IDENTITY_INVALID")
entrypoint = _require_text(value.get("entrypoint"), "entrypoint")
config_path = _relative_artifact_path(value.get("config_path"), "config_path")
python_path = value.get("python_path")
if not isinstance(python_path, list) or not python_path:
raise RuntimeIdentityError("START_IDENTITY_INVALID")
result = {
"entrypoint": entrypoint,
"config_path": config_path,
"python_path": [
_relative_artifact_path(path, "python_path", allow_current=True)
for path in python_path
],
}
_canonical(result)
return result
def _validate_component(value: object) -> dict[str, Any]:
required = {
"component_id", "source_oid", "tree_oid", "modules", "extra_files", "config_sha256",
"start_identity", "dependency_contract_version", "rollback_artifact_sha256",
}
if not isinstance(value, Mapping) or set(value) != required:
raise RuntimeIdentityError("COMPONENT_MANIFEST_FIELDS_INVALID")
rollback = value.get("rollback_artifact_sha256")
if rollback is not None:
rollback = _require_sha(rollback, "rollback_artifact_sha256")
return {
"component_id": _require_text(value.get("component_id"), "component_id"),
"source_oid": _require_oid(value.get("source_oid"), "source_oid"),
"tree_oid": _require_oid(value.get("tree_oid"), "tree_oid"),
"modules": _validate_modules(value.get("modules")),
"extra_files": _validate_extra_files(value.get("extra_files")),
"config_sha256": _require_sha(value.get("config_sha256"), "config_sha256"),
"start_identity": _validate_start_identity(value.get("start_identity")),
"dependency_contract_version": _require_text(value.get("dependency_contract_version"), "dependency_contract_version"),
"rollback_artifact_sha256": rollback,
}
def validate_component_manifest(value: object) -> dict[str, Any]:
if not isinstance(value, Mapping) or set(value) != {"schema", "schema_version", "components"}:
raise RuntimeIdentityError("COMPONENT_MANIFEST_FIELDS_INVALID")
if value.get("schema") != _COMPONENT_SCHEMA or value.get("schema_version") != 1:
raise RuntimeIdentityError("COMPONENT_MANIFEST_SCHEMA_INVALID")
components = value.get("components")
if not isinstance(components, list) or not components:
raise RuntimeIdentityError("COMPONENT_MANIFEST_COMPONENTS_INVALID")
normalized = [_validate_component(component) for component in components]
ids = [component["component_id"] for component in normalized]
if len(set(ids)) != len(ids):
raise RuntimeIdentityError("COMPONENT_ID_DUPLICATE")
return {"schema": _COMPONENT_SCHEMA, "schema_version": 1, "components": normalized}
def load_component_manifest(path: Path) -> tuple[dict[str, Any], str]:
raw = path.read_bytes()
return validate_component_manifest(_load_json(path)), hashlib.sha256(raw).hexdigest()
def validate_artifact_receipt(value: object) -> dict[str, Any]:
required = {"schema", "schema_version", "artifact_sha256", "component_manifest_sha256", "manifest_sha256", "evidence_refs"}
optional = {"version", "source_commit", "tree_oid", "artifact_size", "production_deployed"}
if not isinstance(value, Mapping) or not required.issubset(value) or set(value) - required - optional:
raise RuntimeIdentityError("ARTIFACT_RECEIPT_FIELDS_INVALID")
if value.get("schema") != _RECEIPT_SCHEMA or value.get("schema_version") != 1:
raise RuntimeIdentityError("ARTIFACT_RECEIPT_SCHEMA_INVALID")
references = value.get("evidence_refs")
if not isinstance(references, list) or not references or any(not isinstance(ref, str) or not ref.strip() for ref in references):
raise RuntimeIdentityError("EVIDENCE_REFS_INVALID")
result = {
"schema": _RECEIPT_SCHEMA,
"schema_version": 1,
"artifact_sha256": _require_sha(value.get("artifact_sha256"), "artifact_sha256"),
"component_manifest_sha256": _require_sha(value.get("component_manifest_sha256"), "component_manifest_sha256"),
"manifest_sha256": _require_sha(value.get("manifest_sha256"), "manifest_sha256"),
"evidence_refs": list(references),
}
if "version" in value:
result["version"] = _require_text(value["version"], "version")
if "source_commit" in value:
result["source_commit"] = _require_oid(value["source_commit"], "source_commit")
if "tree_oid" in value:
result["tree_oid"] = _require_oid(value["tree_oid"], "tree_oid")
if "artifact_size" in value:
size = value["artifact_size"]
if type(size) is not int or size < 1:
raise RuntimeIdentityError("ARTIFACT_SIZE_INVALID")
result["artifact_size"] = size
if "production_deployed" in value:
deployed = value["production_deployed"]
if type(deployed) is not bool:
raise RuntimeIdentityError("PRODUCTION_DEPLOYED_INVALID")
result["production_deployed"] = deployed
return result
def load_artifact_receipt(path: Path) -> dict[str, Any]:
return validate_artifact_receipt(_load_json(path))
def _identity_result(
status: str,
*,
component_id: str | None,
declared: Mapping[str, Any] | None = None,
observed: Mapping[str, Any] | None = None,
reason: str,
evidence_refs: list[str] | None = None,
mismatches: list[str] | None = None,
) -> dict[str, Any]:
return {
"status": status,
"component_id": component_id,
"declared_identity": dict(declared) if declared is not None else None,
"observed_identity": dict(observed) if observed is not None else None,
"manifest_digest": declared.get("component_manifest_sha256") if declared else None,
"config_digest": declared.get("config_sha256") if declared else None,
"observed_at": observed.get("observed_at") if observed else None,
"expires_at": observed.get("expires_at") if observed else None,
"evidence_refs": list(evidence_refs or ()),
"reason": reason,
"mismatch_fields": list(mismatches or ()),
}
def declared_component_identity(manifest_path: Path | None, receipt_path: Path | None, component_id: str | None) -> dict[str, Any] | None:
if manifest_path is None or receipt_path is None or not component_id:
return None
manifest, manifest_digest = load_component_manifest(manifest_path)
receipt = load_artifact_receipt(receipt_path)
if receipt["component_manifest_sha256"] != manifest_digest:
raise RuntimeIdentityError("COMPONENT_MANIFEST_DIGEST_MISMATCH")
component = next((item for item in manifest["components"] if item["component_id"] == component_id), None)
if component is None:
raise RuntimeIdentityError("COMPONENT_ID_NOT_DECLARED")
return {
**component,
"artifact_sha256": receipt["artifact_sha256"],
"component_manifest_sha256": receipt["component_manifest_sha256"],
"manifest_sha256": receipt["manifest_sha256"],
}
def _timestamp(value: object, field: str) -> float:
if isinstance(value, bool) or not isinstance(value, (int, float)) or not math.isfinite(float(value)) or float(value) < 0:
raise RuntimeIdentityError(f"{field.upper()}_INVALID")
return float(value)
def _validate_observed(value: object) -> dict[str, Any]:
required = {
"component_id", "source_oid", "tree_oid", "modules", "extra_files", "config_sha256", "start_identity",
"artifact_sha256", "observed_at", "expires_at", "evidence_refs",
}
if not isinstance(value, Mapping) or set(value) != required:
raise RuntimeIdentityError("OBSERVED_IDENTITY_FIELDS_INVALID")
observed_at = _timestamp(value.get("observed_at"), "observed_at")
expires_at = _timestamp(value.get("expires_at"), "expires_at")
if expires_at <= observed_at:
raise RuntimeIdentityError("OBSERVATION_EXPIRY_INVALID")
refs = value.get("evidence_refs")
if not isinstance(refs, list) or not refs or any(not isinstance(ref, str) or not ref.strip() for ref in refs):
raise RuntimeIdentityError("EVIDENCE_REFS_INVALID")
return {
"component_id": _require_text(value.get("component_id"), "component_id"),
"source_oid": _require_oid(value.get("source_oid"), "source_oid"),
"tree_oid": _require_oid(value.get("tree_oid"), "tree_oid"),
"modules": _validate_modules(value.get("modules")),
"extra_files": _validate_extra_files(value.get("extra_files")),
"config_sha256": _require_sha(value.get("config_sha256"), "config_sha256"),
"start_identity": _validate_start_identity(value.get("start_identity")),
"artifact_sha256": _require_sha(value.get("artifact_sha256"), "artifact_sha256"),
"observed_at": observed_at,
"expires_at": expires_at,
"evidence_refs": list(refs),
}
TrustedComparison = Callable[[Mapping[str, Any]], Mapping[str, Any] | None]
def evaluate_component_runtime_identity(
*,
component_id: str | None,
manifest_path: Path | None,
receipt_path: Path | None,
trusted_comparison: TrustedComparison | None = None,
now: float | None = None,
) -> dict[str, Any]:
"""Project identity without granting trust to a declaration or self-report."""
try:
declared = declared_component_identity(manifest_path, receipt_path, component_id)
except RuntimeIdentityError as exc:
return _identity_result("FAIL", component_id=component_id, reason=str(exc))
if declared is None:
return _identity_result("UNKNOWN", component_id=component_id, reason="DECLARED_IDENTITY_UNAVAILABLE")
if trusted_comparison is None:
return _identity_result("UNKNOWN", component_id=component_id, declared=declared, reason="TRUSTED_OBSERVATION_UNAVAILABLE")
try:
raw_observed = trusted_comparison(dict(declared))
except Exception:
return _identity_result("UNKNOWN", component_id=component_id, declared=declared, reason="TRUSTED_COMPARISON_UNAVAILABLE")
if raw_observed is None:
return _identity_result("UNKNOWN", component_id=component_id, declared=declared, reason="OBSERVATION_MISSING")
try:
observed = _validate_observed(raw_observed)
except RuntimeIdentityError as exc:
return _identity_result("FAIL", component_id=component_id, declared=declared, reason=str(exc))
current = time.time() if now is None else now
if observed["expires_at"] <= current:
return _identity_result("UNKNOWN", component_id=component_id, declared=declared, observed=observed, reason="OBSERVATION_EXPIRED", evidence_refs=observed["evidence_refs"])
fields = ("component_id", "source_oid", "tree_oid", "modules", "extra_files", "config_sha256", "start_identity", "artifact_sha256")
mismatches = [field for field in fields if _canonical(declared[field]) != _canonical(observed[field])]
if mismatches:
return _identity_result("FAIL", component_id=component_id, declared=declared, observed=observed, reason="DECLARED_OBSERVED_DRIFT", evidence_refs=observed["evidence_refs"], mismatches=mismatches)
return _identity_result("PASS", component_id=component_id, declared=declared, observed=observed, reason="TRUSTED_COMPARISON_MATCH", evidence_refs=observed["evidence_refs"])
+11
View File
@@ -9,6 +9,8 @@ from dataclasses import dataclass
from pathlib import Path
from typing import Any, Mapping
from component_runtime_identity import evaluate_component_runtime_identity
_OID = re.compile(r"^[0-9a-f]{40}$")
_SHA = re.compile(r"^[0-9a-f]{64}$")
_STATES = {"STAGED", "ACTIVE", "ROLLED_BACK"}
@@ -25,6 +27,13 @@ class RuntimeDeploymentState:
return self.deployment_state == "ACTIVE"
def health_payload(self) -> dict[str, Any]:
# This listener owns liveness only. Its environment-derived release
# declaration must never become runtime identity evidence by itself.
identity = evaluate_component_runtime_identity(
component_id=None,
manifest_path=None,
receipt_path=None,
)
return {
"status": "ok",
"version": self.version,
@@ -33,6 +42,8 @@ class RuntimeDeploymentState:
"deployment_state": self.deployment_state,
"production_deployed": self.production_deployed,
"health_timestamp": dt.datetime.now(dt.timezone.utc).isoformat(),
"drift_status": identity["status"],
"component_runtime_identity": identity,
}
@classmethod
@@ -0,0 +1,379 @@
"""Concrete transport adapter for the governed source publication boundary.
The adapter composes the already existing live writer executor. It owns no
leases, TaskPrincipal, credentials, retry policy, or release state machine.
Those remain server side. ``authenticated_readback`` is deliberately an
injected port: a local commit response is not evidence of an authenticated
source/tree binding.
"""
from __future__ import annotations
from collections.abc import Callable, Mapping
import hashlib
import json
import re
from typing import Any, Protocol
from governed_unattended_release import SourcePublicationIntent
from task_plane.governed_writer_automation import GovernedWriterRequest
class GovernedPublicationError(RuntimeError):
"""Stable fail-closed publication error."""
def __init__(self, code: str):
self.code = code
super().__init__(code)
class GovernedWriterExecutor(Protocol):
def acquire(self, *, request: GovernedWriterRequest, job_id: str) -> Any: ...
def commit_and_readback(
self, *, session: Any, request: GovernedWriterRequest,
files: Mapping[str, bytes], expected_sha256: Mapping[str, str],
message: str,
) -> Mapping[str, Any]: ...
def submit_task_result(
self, *, session: Any, result: Mapping[str, Any], idempotency_key: str,
) -> Mapping[str, Any]: ...
class SourceFileReader(Protocol):
def __call__(self, intent: SourcePublicationIntent) -> Mapping[str, bytes]: ...
class AuthenticatedReadback(Protocol):
def __call__(
self, *, intent: SourcePublicationIntent, request: GovernedWriterRequest,
session: Any, commit_result: Mapping[str, Any],
) -> Mapping[str, Any]: ...
class AdmissionBindingReader(Protocol):
"""Read the server-side task/governed binding after admission."""
def __call__(
self, *, intent: SourcePublicationIntent,
admission_receipt: Mapping[str, Any],
) -> Mapping[str, Any]: ...
class GitTreeFileReader(Protocol):
"""Read bytes from an immutable committed tree object, never a workspace."""
def __call__(self, *, tree_oid: str, path: str) -> bytes: ...
class CommittedTreeSourceReader:
"""Adapt an authoritative git-object reader to the publication port."""
def __init__(self, reader: GitTreeFileReader) -> None:
if not callable(reader):
raise ValueError("git_object_reader_required")
self.reader = reader
def __call__(self, intent: SourcePublicationIntent) -> Mapping[str, bytes]:
return {
path: self.reader(tree_oid=intent.source_tree_oid, path=path)
for path in intent.file_allowlist
}
_OID = re.compile(r"^[0-9a-f]{40}$")
_SHA = re.compile(r"^[0-9a-f]{64}$")
_SAFE_CODE = re.compile(r"^(?:TASK_PRINCIPAL|EXECUTION_PLAN|GOVERNED)_[A-Z0-9_]{3,120}$")
_SAFE_OPERATION = re.compile(r"^[A-Za-z][A-Za-z0-9]{1,63}$")
def _oid(value: Any) -> str:
result = str(value or "").lower()
if not _OID.fullmatch(result):
raise GovernedPublicationError("PUBLICATION_OID_INVALID")
return result
def _sha(value: Any) -> str:
result = str(value or "").lower()
if not _SHA.fullmatch(result):
raise GovernedPublicationError("PUBLICATION_SHA_INVALID")
return result
def _canonical(value: Mapping[str, Any]) -> bytes:
try:
return json.dumps(
dict(value), sort_keys=True, separators=(",", ":"),
ensure_ascii=False, allow_nan=False,
).encode("utf-8")
except (TypeError, ValueError) as exc:
raise GovernedPublicationError("PUBLICATION_EVIDENCE_INVALID") from exc
class GovernedPublicationClient:
"""Run TaskLease -> TaskPrincipal -> WriterLease -> commit through ports.
``writer_executor`` is normally ``LiveGovernedWriterExecutor`` from
``task_plane.live_governed_writer_adapters``. ``source_files`` must return
the exact immutable bytes for the intent's allowlist; it is never allowed
to return a path or a mutable object. The authenticated readback port
must be backed by the authoritative provider/bridge and prove the final
source OID, tree OID, task, branch, base, generation, and execution digest.
"""
def __init__(
self,
*,
writer_executor: GovernedWriterExecutor,
source_files: SourceFileReader | None = None,
git_tree_file: GitTreeFileReader | None = None,
admission_binding: AdmissionBindingReader,
authenticated_readback: AuthenticatedReadback,
subject: str,
manifest_id: str,
runtime_fingerprint: str,
process_start_identity: str,
commit_message: str = "governed source publication",
diagnostic_sink: Callable[[Mapping[str, Any]], None] | None = None,
) -> None:
if (
writer_executor is None
or any(
not callable(getattr(writer_executor, name, None))
for name in (
"acquire", "commit_and_readback", "submit_task_result",
)
)
):
raise ValueError("writer_executor_required")
if source_files is not None:
raise ValueError("workspace_source_reader_forbidden")
if git_tree_file is None:
raise ValueError("git_object_source_reader_required")
if not callable(admission_binding) or not callable(authenticated_readback):
raise ValueError("authenticated_ports_required")
for name, value in (("subject", subject), ("manifest_id", manifest_id),
("runtime_fingerprint", runtime_fingerprint),
("process_start_identity", process_start_identity),
("commit_message", commit_message)):
if not isinstance(value, str) or not value:
raise ValueError(name + "_required")
self.writer_executor = writer_executor
self.source_files = None
self.git_tree_file = git_tree_file
self.admission_binding = admission_binding
self.authenticated_readback = authenticated_readback
self.subject = subject
self.manifest_id = manifest_id
self.runtime_fingerprint = runtime_fingerprint
self.process_start_identity = process_start_identity
self.commit_message = commit_message
if diagnostic_sink is not None and not callable(diagnostic_sink):
raise ValueError("diagnostic_sink_invalid")
self.diagnostic_sink = diagnostic_sink
def _record_uncertain_effect(self, *, exc: Exception, task_id: str,
stage: str) -> None:
"""Emit only allowlisted facts; never serialize an exception or body."""
if self.diagnostic_sink is None:
return
raw_status = getattr(exc, "status", None)
status = raw_status if type(raw_status) is int and 100 <= raw_status <= 599 else None
raw_code = getattr(exc, "error_code", None)
code = raw_code if isinstance(raw_code, str) and _SAFE_CODE.fullmatch(raw_code) else None
raw_operation = getattr(exc, "operation", None)
operation = (
raw_operation if isinstance(raw_operation, str)
and _SAFE_OPERATION.fullmatch(raw_operation) else stage
)
diagnostic = {
"task_id": task_id, "operation": operation,
"http_status": status,
"error_code": (
"transport_unknown" if status is None else (code or "issuer_error_unknown")
),
}
try:
self.diagnostic_sink(diagnostic)
except Exception:
# Diagnostic observation must never change the uncertainty policy.
pass
def _validate_admission(self, intent: SourcePublicationIntent, receipt: Mapping[str, Any]) -> Mapping[str, Any]:
if not isinstance(receipt, Mapping):
raise GovernedPublicationError("PUBLICATION_ADMISSION_INVALID")
for key, expected in (("task_id", intent.task_id), ("authority_base_oid", intent.authority_base_oid),
("target_branch", intent.target_branch), ("generation", intent.generation),
("execution_digest", intent.execution_digest)):
if receipt.get(key) != expected:
raise GovernedPublicationError("PUBLICATION_ADMISSION_BINDING_MISMATCH")
try:
binding = self.admission_binding(intent=intent, admission_receipt=receipt)
except Exception as exc:
raise GovernedPublicationError("PUBLICATION_ADMISSION_BINDING_UNAVAILABLE") from exc
if not isinstance(binding, Mapping):
raise GovernedPublicationError("PUBLICATION_ADMISSION_BINDING_INVALID")
expected = {
"task_id": intent.task_id, "authority_base_oid": intent.authority_base_oid,
"target_branch": intent.target_branch, "generation": intent.generation,
"execution_digest": intent.execution_digest,
}
if any(binding.get(key) != value for key, value in expected.items()):
raise GovernedPublicationError("PUBLICATION_ADMISSION_BINDING_MISMATCH")
job_id = binding.get("job_id")
if not isinstance(job_id, str) or not job_id:
raise GovernedPublicationError("PUBLICATION_JOB_BINDING_MISSING")
return dict(binding)
def _read_files(self, intent: SourcePublicationIntent) -> tuple[dict[str, bytes], dict[str, str]]:
try:
raw = {
path: self.git_tree_file(tree_oid=intent.source_tree_oid, path=path)
for path in intent.file_allowlist
}
except KeyError as exc:
raise GovernedPublicationError("PUBLICATION_FILE_SET_MISMATCH") from exc
except Exception as exc:
raise GovernedPublicationError("PUBLICATION_SOURCE_READ_FAILED") from exc
if not isinstance(raw, Mapping) or set(raw) != set(intent.file_allowlist):
raise GovernedPublicationError("PUBLICATION_FILE_SET_MISMATCH")
files: dict[str, bytes] = {}
hashes: dict[str, str] = {}
for path in intent.file_allowlist:
data = raw.get(path)
if isinstance(data, bytearray):
data = bytes(data)
if not isinstance(data, bytes):
raise GovernedPublicationError("PUBLICATION_FILE_BYTES_REQUIRED")
files[path] = bytes(data)
hashes[path] = hashlib.sha256(files[path]).hexdigest()
return files, hashes
def publish(self, intent: SourcePublicationIntent, admission_receipt: Mapping[str, Any]) -> dict[str, Any]:
if not isinstance(intent, SourcePublicationIntent):
raise GovernedPublicationError("PUBLICATION_INTENT_REQUIRED")
admission_binding = self._validate_admission(intent, admission_receipt)
job_id = str(admission_binding["job_id"])
files, hashes = self._read_files(intent)
request = GovernedWriterRequest(
subject=self.subject, task_id=intent.task_id, repository=intent.repository,
target_branch=intent.target_branch, risk=intent.risk, generation=intent.generation,
base_oid=intent.authority_base_oid,
required_capabilities=frozenset(intent.requested_capabilities),
file_allowlist=tuple(intent.file_allowlist),
plan_digest="sha256:" + intent.execution_digest,
manifest_id=self.manifest_id, runtime_fingerprint=self.runtime_fingerprint,
process_start_identity=self.process_start_identity,
)
stage = "acquireWriterLease"
try:
session = self.writer_executor.acquire(request=request, job_id=job_id)
stage = "commitFiles"
commit_result = self.writer_executor.commit_and_readback(
session=session, request=request, files=files,
expected_sha256=hashes, message=self.commit_message,
)
if not isinstance(commit_result, Mapping):
raise GovernedPublicationError("PUBLICATION_COMMIT_READBACK_INVALID")
readback = self.authenticated_readback(
intent=intent, request=request, session=session,
commit_result=commit_result,
)
except GovernedPublicationError:
raise
except Exception as exc:
# A transport exception after a mutation is indeterminate. Never
# resubmit it here; the caller must reconcile through the existing
# Job/recovery service.
self._record_uncertain_effect(exc=exc, task_id=intent.task_id, stage=stage)
raise GovernedPublicationError("PUBLICATION_EFFECT_UNKNOWN") from exc
if not isinstance(readback, Mapping) or readback.get("authenticated") is not True:
raise GovernedPublicationError("PUBLICATION_AUTHENTICATED_READBACK_REQUIRED")
required = {
"task_id": intent.task_id, "authority_base_oid": intent.authority_base_oid,
"target_branch": intent.target_branch, "generation": intent.generation,
"execution_digest": intent.execution_digest,
}
if any(readback.get(key) != value for key, value in required.items()):
raise GovernedPublicationError("PUBLICATION_READBACK_BINDING_MISMATCH")
for key in ("task_lease", "task_principal", "writer_lease"):
if not isinstance(readback.get(key), Mapping) or readback[key].get("task_id") != intent.task_id:
raise GovernedPublicationError("PUBLICATION_LEASE_CHAIN_MISSING")
source_oid = _oid(readback.get("source_oid"))
tree_oid = _oid(readback.get("tree_oid"))
nested_commit = readback.get("commit_receipt")
if nested_commit is not None and not isinstance(nested_commit, Mapping):
raise GovernedPublicationError("PUBLICATION_COMMIT_BINDING_MISMATCH")
commit_oid = _oid(readback.get("commit_oid") or (nested_commit or {}).get("commit_oid"))
if source_oid != commit_oid or tree_oid != intent.source_tree_oid:
raise GovernedPublicationError("PUBLICATION_SOURCE_TREE_BINDING_MISMATCH")
commit_receipt = readback.get("commit_receipt")
if not isinstance(commit_receipt, Mapping):
raise GovernedPublicationError("PUBLICATION_COMMIT_RECEIPT_REQUIRED")
# The live governed receipt binds commit_oid; the independently read
# Git tree is carried by authenticated readback. Preserve the original
# receipt instead of inventing a tree field absent from its schema.
if (commit_receipt.get("commit_oid") != commit_oid
or ("tree_oid" in commit_receipt and commit_receipt["tree_oid"] != tree_oid)):
raise GovernedPublicationError("PUBLICATION_COMMIT_BINDING_MISMATCH")
publication = {
**dict(readback), "authenticated": True, "task_id": intent.task_id,
"job_id": job_id, "source_oid": source_oid, "tree_oid": tree_oid,
"generation": intent.generation, "target_branch": intent.target_branch,
"authority_base_oid": intent.authority_base_oid,
"execution_digest": intent.execution_digest,
"commit_receipt": dict(commit_receipt),
}
task_result = {
"schema": "aios.governed-source-publication-result.v1",
"task_id": intent.task_id,
"job_id": job_id,
"repository": intent.repository,
"target_branch": intent.target_branch,
"generation": intent.generation,
"execution_digest": intent.execution_digest,
"authority_base_oid": _oid(intent.authority_base_oid),
"candidate_source_oid": _oid(intent.source_commit),
"published_source_oid": source_oid,
"source_tree_oid": tree_oid,
"commit_receipt_sha256": hashlib.sha256(
_canonical(commit_receipt)
).hexdigest(),
}
result_sha256 = hashlib.sha256(_canonical(task_result)).hexdigest()
idempotency_key = "governed-source-result-" + result_sha256
try:
submitted = self.writer_executor.submit_task_result(
session=session, result=task_result,
idempotency_key=idempotency_key,
)
if (
not isinstance(submitted, Mapping)
or submitted.get("task_id") != intent.task_id
or submitted.get("status") != "VERIFYING"
or submitted.get("result") != task_result
):
raise GovernedPublicationError(
"PUBLICATION_TASK_RESULT_READBACK_INVALID"
)
except GovernedPublicationError:
raise
except Exception as exc:
# A lost result response may have changed the durable Task. Never
# re-register, reclaim or retry here.
self._record_uncertain_effect(
exc=exc, task_id=intent.task_id, stage="submitTaskResult",
)
raise GovernedPublicationError("PUBLICATION_EFFECT_UNKNOWN") from exc
publication.update(
task_result=task_result,
task_result_idempotency_key=idempotency_key,
task_result_submission={
"task_id": submitted["task_id"], "status": submitted["status"],
"revision": submitted.get("revision"),
"idempotent": submitted.get("idempotent", False),
"result_sha256": result_sha256,
},
)
return publication
@@ -0,0 +1,405 @@
"""Narrow C5 unattended source-publication policy lifecycle.
The policy is an Owner-signed, exact-scope admission object. It is checked
against the configured Owner policy at issuance and against a live revocation
source for every request. It is not an activation/configuration mechanism
and does not create release or production authority.
"""
from __future__ import annotations
from dataclasses import asdict, dataclass
import hashlib
import re
import time
from pathlib import PurePosixPath
from typing import Any, Callable, Mapping
from aios_single_host_authority import (
OWNER_NAMESPACE,
OWNER_IDENTITY,
SOURCE_PUBLICATION_CAPABILITIES,
SOURCE_PUBLICATION_POLICY_ID,
SOURCE_PUBLICATION_POLICY_SCHEMA,
SignedArtifact,
SingleHostAuthorityError,
SourcePublicationPolicyRequest,
canonical_json_bytes,
sha256_json,
)
_HEX40 = re.compile(r"^[0-9a-f]{40}$")
_HEX64 = re.compile(r"^[0-9a-f]{64}$")
_RISK_ORDER = {"low": 0, "medium": 1, "engineering": 2, "high": 3}
_POLICY_FIELDS = frozenset(
{
"schema", "policy_id", "owner_identity", "repository", "trust_domain",
"policy_digest",
"authority_base_oid", "source_oid", "source_tree_oid", "target_agent",
"target_branch", "generation", "execution_digest", "exact_files",
"capabilities", "risk", "production_impact", "renewal", "nonce",
"issued_at", "not_before", "expires_at", "status", "signature_namespace",
"signature_key_id", "canonical_payload_digest",
}
)
_REQUEST_FIELDS = frozenset(SourcePublicationPolicyRequest.__dataclass_fields__)
# The typed Owner epoch producer uses the configured policy's production_scope.
# The installed C5 source-only policy contract uses ``none`` for production
# impact; keep this exact value bound to the C5 policy rather than accepting an
# arbitrary non-empty scope from a raw signer.
SOURCE_PUBLICATION_EPOCH_PRODUCTION_SCOPE = "none"
SOURCE_PUBLICATION_EPOCH_DESTRUCTIVE_POLICY = "forbidden"
@dataclass(frozen=True, slots=True)
class TaskAdmissionPolicyProjection:
"""The only policy projection exposed after an exact request guard."""
max_risk: str
repository: str
target_branch: str
capabilities: tuple[str, ...]
exact_files: tuple[str, ...]
def to_dict(self) -> dict[str, Any]:
return asdict(self)
def _request_dict(request: SourcePublicationPolicyRequest | Mapping[str, Any]) -> dict[str, Any]:
if isinstance(request, SourcePublicationPolicyRequest):
value = asdict(request)
elif isinstance(request, Mapping):
value = dict(request)
else:
raise TypeError("request must be SourcePublicationPolicyRequest or mapping")
if set(value) != _REQUEST_FIELDS:
raise ValueError("SOURCE_PUBLICATION_REQUEST_FIELDS_INVALID")
if "exact_files" in value:
value["exact_files"] = tuple(value["exact_files"])
return value
class SourcePublicationPolicyVerifier:
"""Strict verifier over the existing Owner registry verifier interface."""
def __init__(
self,
owner_verifier: Any,
*,
revocation_check: Callable[[Mapping[str, Any]], bool] | None,
expected_policy_digest: str | None = None,
clock: Callable[[], float] = time.time,
) -> None:
self.owner_verifier = owner_verifier
self.revocation_check = revocation_check
self.expected_policy_digest = expected_policy_digest
self.clock = clock
def _deny(self, code: str) -> None:
raise SingleHostAuthorityError(code)
def _validate_payload(self, artifact: SignedArtifact) -> dict[str, Any]:
if not isinstance(artifact, SignedArtifact):
self._deny("SOURCE_PUBLICATION_ARTIFACT_INVALID")
payload = dict(artifact.payload)
if set(payload) != _POLICY_FIELDS:
self._deny("SOURCE_PUBLICATION_UNKNOWN_FIELDS")
try:
if artifact.canonical_payload != canonical_json_bytes(payload):
self._deny("SOURCE_PUBLICATION_CANONICAL_BYTES_MISMATCH")
if payload["canonical_payload_digest"] != sha256_json(
{k: v for k, v in payload.items() if k != "canonical_payload_digest"}
):
self._deny("SOURCE_PUBLICATION_PAYLOAD_DIGEST_INVALID")
if payload["schema"] != SOURCE_PUBLICATION_POLICY_SCHEMA:
self._deny("SOURCE_PUBLICATION_SCHEMA_INVALID")
if payload["policy_id"] != SOURCE_PUBLICATION_POLICY_ID:
self._deny("SOURCE_PUBLICATION_POLICY_ID_INVALID")
if payload["owner_identity"] != OWNER_IDENTITY:
self._deny("SOURCE_PUBLICATION_OWNER_INVALID")
if not isinstance(payload["policy_digest"], str) or not _HEX64.fullmatch(payload["policy_digest"]):
self._deny("SOURCE_PUBLICATION_POLICY_DIGEST_INVALID")
if self.expected_policy_digest is not None and payload["policy_digest"] != self.expected_policy_digest:
self._deny("SOURCE_PUBLICATION_POLICY_DIGEST_MISMATCH")
if payload["signature_namespace"] != OWNER_NAMESPACE:
self._deny("SOURCE_PUBLICATION_NAMESPACE_INVALID")
if payload["signature_key_id"] != artifact.key_id or artifact.namespace != OWNER_NAMESPACE:
self._deny("SOURCE_PUBLICATION_ISSUER_MISMATCH")
for name in ("authority_base_oid", "source_oid", "source_tree_oid"):
if not isinstance(payload[name], str) or not _HEX40.fullmatch(payload[name]):
self._deny("SOURCE_PUBLICATION_OID_INVALID")
if not isinstance(payload["execution_digest"], str) or not _HEX64.fullmatch(payload["execution_digest"]):
self._deny("SOURCE_PUBLICATION_EXECUTION_DIGEST_INVALID")
branch = payload["target_branch"]
agent = payload["target_agent"]
if (
not isinstance(branch, str) or not branch.startswith("agent/")
or "\\" in branch or ".." in PurePosixPath(branch).parts
or branch.endswith(("/", "."))
or not isinstance(agent, str) or not agent.startswith("agent/")
):
self._deny("SOURCE_PUBLICATION_TARGET_INVALID")
if payload["risk"] != "engineering" or payload["production_impact"] != "none" or payload["renewal"] != "none":
self._deny("SOURCE_PUBLICATION_SCOPE_INVALID")
if payload["capabilities"] != sorted(SOURCE_PUBLICATION_CAPABILITIES):
self._deny("SOURCE_PUBLICATION_CAPABILITIES_INVALID")
if not isinstance(payload["exact_files"], list) or not payload["exact_files"] or len(set(payload["exact_files"])) != len(payload["exact_files"]):
self._deny("SOURCE_PUBLICATION_FILES_INVALID")
if type(payload["generation"]) is not int or payload["generation"] < 1:
self._deny("SOURCE_PUBLICATION_GENERATION_INVALID")
if type(payload["issued_at"]) is not int or type(payload["not_before"]) is not int or type(payload["expires_at"]) is not int:
self._deny("SOURCE_PUBLICATION_TIME_INVALID")
now = int(self.clock())
if not payload["not_before"] <= payload["issued_at"] <= now < payload["expires_at"]:
self._deny("SOURCE_PUBLICATION_EXPIRED")
if payload["expires_at"] <= payload["issued_at"]:
self._deny("SOURCE_PUBLICATION_LIFETIME_INVALID")
if payload["expires_at"] - payload["issued_at"] > 3600 or payload["status"] != "ACTIVE":
self._deny("SOURCE_PUBLICATION_LIFETIME_INVALID")
if self.revocation_check is None:
self._deny("SOURCE_PUBLICATION_REVOCATION_CHECK_MISSING")
if self.revocation_check(payload) is not True:
self._deny("SOURCE_PUBLICATION_REVOKED")
except SingleHostAuthorityError:
raise
except Exception as exc:
raise SingleHostAuthorityError("SOURCE_PUBLICATION_POLICY_INVALID") from exc
return payload
def verify(
self,
artifact: SignedArtifact,
request: SourcePublicationPolicyRequest | Mapping[str, Any] | None = None,
) -> bool:
try:
payload = self._validate_payload(artifact)
# Verify exactly the bytes supplied by the signed artifact. Do not
# reconstruct or reserialize them before invoking the registry API.
if self.owner_verifier.verify(
namespace=artifact.namespace,
payload=artifact.canonical_payload,
signature=artifact.signature,
key_id=artifact.key_id,
) is not True:
return False
if request is not None:
expected = _request_dict(request)
for field in (
"policy_id", "repository", "authority_base_oid", "source_oid",
"source_tree_oid", "target_agent", "target_branch", "generation",
"execution_digest", "exact_files", "nonce", "risk",
"production_impact", "renewal",
):
actual = tuple(payload[field]) if field == "exact_files" else payload[field]
if actual != expected.get(field):
return False
if int(payload["expires_at"]) - int(payload["issued_at"]) != int(expected["ttl_seconds"]):
return False
return True
except (SingleHostAuthorityError, TypeError, ValueError, KeyError):
return False
except Exception:
return False
def require(
self,
artifact: SignedArtifact,
request: SourcePublicationPolicyRequest | Mapping[str, Any],
) -> dict[str, Any]:
if not self.verify(artifact, request):
raise SingleHostAuthorityError("SOURCE_PUBLICATION_POLICY_DENIED")
return dict(artifact.payload)
def require_source_publication_epoch_scope(epoch: Any, policy_payload: Mapping[str, Any]) -> None:
"""Require v2 epoch provenance to match the signed C5 policy exactly."""
schema = getattr(epoch, "schema", None)
if schema is None and isinstance(epoch, Mapping):
schema = epoch.get("schema")
if schema != "aios.owner-delegation-epoch.v2":
raise SingleHostAuthorityError("SOURCE_PUBLICATION_EPOCH_SCHEMA_REQUIRED")
def field(name: str) -> Any:
if isinstance(epoch, Mapping):
return epoch.get(name)
return getattr(epoch, name, None)
if (
field("production_scope") != SOURCE_PUBLICATION_EPOCH_PRODUCTION_SCOPE
or field("destructive_policy") != SOURCE_PUBLICATION_EPOCH_DESTRUCTIVE_POLICY
or field("policy_digest") != policy_payload.get("policy_digest")
or field("repository") != policy_payload.get("repository")
or field("trust_domain") != policy_payload.get("trust_domain")
):
raise SingleHostAuthorityError("SOURCE_PUBLICATION_EPOCH_SCOPE_MISMATCH")
class ScopedSourcePublicationPolicyAdmission:
"""Rechecks a policy for every exact request before scoped admission."""
def __init__(
self,
verifier: SourcePublicationPolicyVerifier,
*,
bootstrap: Any | None = None,
artifact: SignedArtifact | None = None,
request: SourcePublicationPolicyRequest | Mapping[str, Any] | None = None,
expected_agent_id: str | None = None,
) -> None:
self.verifier = verifier
self.bootstrap = bootstrap
self.artifact = artifact
self.request = request
self.expected_agent_id = expected_agent_id
def _check_bootstrap_binding(self, payload: Mapping[str, Any]) -> None:
if self.bootstrap is None:
return
policy = getattr(self.bootstrap, "policy", None)
digest_provider = getattr(policy, "normalized_policy_digest", None)
digest = digest_provider() if callable(digest_provider) else None
trust_domain = getattr(policy, "trust_domain", None)
if not isinstance(digest, str) or digest != payload.get("policy_digest"):
raise SingleHostAuthorityError("SOURCE_PUBLICATION_BOOTSTRAP_POLICY_DIGEST_MISMATCH")
if not isinstance(trust_domain, str) or trust_domain != payload.get("trust_domain"):
raise SingleHostAuthorityError("SOURCE_PUBLICATION_BOOTSTRAP_TRUST_DOMAIN_MISMATCH")
if self.expected_agent_id is None or payload.get("target_agent") != self.expected_agent_id:
raise SingleHostAuthorityError("SOURCE_PUBLICATION_TARGET_AGENT_MISMATCH")
def authorize(
self,
artifact: SignedArtifact,
request: SourcePublicationPolicyRequest | Mapping[str, Any],
) -> bool:
try:
payload = self.verifier._validate_payload(artifact)
if self.expected_agent_id is not None and payload.get("target_agent") != self.expected_agent_id:
return False
self._check_bootstrap_binding(payload)
except SingleHostAuthorityError:
return False
return self.verifier.verify(artifact, request)
verify = authorize
def _request_for_raw(self, raw: Mapping[str, Any]) -> SourcePublicationPolicyRequest:
if self.artifact is None:
raise SingleHostAuthorityError("SOURCE_PUBLICATION_ARTIFACT_REQUIRED")
expected_task = getattr(self, "expected_task_id", None)
if expected_task is not None and raw.get("task_id") != expected_task:
raise SingleHostAuthorityError("SOURCE_PUBLICATION_PLAN_TASK_MISMATCH")
payload = self.verifier._validate_payload(self.artifact)
self._check_bootstrap_binding(payload)
if self.request is not None:
base = _request_dict(self.request)
else:
base = {
"policy_id": payload["policy_id"], "repository": payload["repository"],
"authority_base_oid": payload["authority_base_oid"], "source_oid": payload["source_oid"],
"source_tree_oid": payload["source_tree_oid"], "target_agent": payload["target_agent"],
"target_branch": payload["target_branch"], "generation": payload["generation"],
"execution_digest": payload["execution_digest"], "exact_files": tuple(payload["exact_files"]),
"nonce": payload["nonce"], "ttl_seconds": payload["expires_at"] - payload["issued_at"],
"risk": payload["risk"], "production_impact": payload["production_impact"], "renewal": payload["renewal"],
}
checks = {
"repository": base["repository"], "authority_base_oid": base["authority_base_oid"],
"target_branch": base["target_branch"], "generation": base["generation"],
"execution_digest": base["execution_digest"], "risk": base["risk"],
}
for field, expected in checks.items():
if raw.get(field) != expected:
raise SingleHostAuthorityError("SOURCE_PUBLICATION_REQUEST_SCOPE_MISMATCH")
caps = tuple(raw.get("requested_capabilities", ()))
if set(caps) != set(base["capabilities"] if "capabilities" in base else payload["capabilities"]):
raise SingleHostAuthorityError("SOURCE_PUBLICATION_CAPABILITY_SCOPE_MISMATCH")
if tuple(raw.get("file_allowlist", ())) != tuple(base["exact_files"]):
raise SingleHostAuthorityError("SOURCE_PUBLICATION_FILE_SCOPE_MISMATCH")
return SourcePublicationPolicyRequest(**base)
def create_envelope(self, raw: Mapping[str, Any]) -> dict[str, Any]:
if self.bootstrap is None:
raise SingleHostAuthorityError("SOURCE_PUBLICATION_BOOTSTRAP_REQUIRED")
request = self._request_for_raw(raw)
if not self.verifier.verify(self.artifact, request):
raise SingleHostAuthorityError("SOURCE_PUBLICATION_POLICY_DENIED")
return self.bootstrap.create_envelope(raw)
def authorize_and_admit(self, raw: Mapping[str, Any]) -> dict[str, Any]:
if self.bootstrap is None or self.artifact is None:
raise SingleHostAuthorityError("SOURCE_PUBLICATION_BOOTSTRAP_REQUIRED")
if raw.get("authorization_kind") != "delegation_epoch":
raise SingleHostAuthorityError("SOURCE_PUBLICATION_OWNER_SIGNATURE_BYPASS_DENIED")
envelope = self.bootstrap._load_envelope(str(raw.get("envelope_id") or ""))
envelope_raw = envelope.as_dict() if hasattr(envelope, "as_dict") else dict(envelope)
request = self._request_for_raw(envelope_raw)
if not self.verifier.verify(self.artifact, request):
raise SingleHostAuthorityError("SOURCE_PUBLICATION_POLICY_DENIED")
epoch = self.bootstrap._verify_epoch(
epoch_id=str(raw.get("delegation_epoch_id") or ""),
envelope=envelope,
now=int(self.verifier.clock()),
)
require_source_publication_epoch_scope(
epoch, self.verifier._validate_payload(self.artifact)
)
return self.bootstrap.authorize_and_admit(raw)
def status(self, raw: Mapping[str, Any] | str) -> Any:
"""Read status only after the same policy and epoch guard as admission."""
if self.bootstrap is None or self.artifact is None:
raise SingleHostAuthorityError("SOURCE_PUBLICATION_BOOTSTRAP_REQUIRED")
if isinstance(raw, str):
raw = {"envelope_id": raw}
envelope = self.bootstrap._load_envelope(str(raw.get("envelope_id") or ""))
envelope_raw = envelope.as_dict() if hasattr(envelope, "as_dict") else dict(envelope)
request = self._request_for_raw(envelope_raw)
self.verifier.require(self.artifact, request)
epoch_id = str(raw.get("delegation_epoch_id") or "")
if not epoch_id:
grant = self.bootstrap.store.get_grant_by_task_id(envelope.task_id)
epoch_id = grant.parent_epoch_id if grant is not None else ""
if not epoch_id or not hasattr(self.bootstrap, "_verify_epoch"):
raise SingleHostAuthorityError("SOURCE_PUBLICATION_STATUS_EPOCH_REQUIRED")
epoch = self.bootstrap._verify_epoch(
epoch_id=epoch_id,
envelope=envelope,
now=int(self.verifier.clock()),
)
require_source_publication_epoch_scope(
epoch, self.verifier._validate_payload(self.artifact)
)
if hasattr(self.bootstrap, "status"):
return self.bootstrap.status(envelope.envelope_id)
return envelope_raw
def reconcile_status(self, envelope_id: str, authorization: Mapping[str, Any]) -> Any:
"""Carry the verified epoch identity when a Task effect has no receipt yet."""
if authorization.get("authorization_kind") != "delegation_epoch":
raise SingleHostAuthorityError("SOURCE_PUBLICATION_OWNER_SIGNATURE_BYPASS_DENIED")
return self.status({"envelope_id": envelope_id,
"delegation_epoch_id": authorization.get("delegation_epoch_id")})
def project_task_admission_policy(
self,
artifact: SignedArtifact,
request: SourcePublicationPolicyRequest | Mapping[str, Any],
) -> TaskAdmissionPolicyProjection:
payload = self.verifier.require(artifact, request)
return TaskAdmissionPolicyProjection(
max_risk="engineering",
repository=payload["repository"],
target_branch=payload["target_branch"],
capabilities=tuple(payload["capabilities"]),
exact_files=tuple(payload["exact_files"]),
)
to_task_admission_policy = project_task_admission_policy
# Short aliases for callers that prefer the lifecycle terminology.
SourcePublicationPolicyAdmission = ScopedSourcePublicationPolicyAdmission
SourcePublicationVerifier = SourcePublicationPolicyVerifier
@@ -0,0 +1,372 @@
"""Thin unattended release runner.
This module is an adapter around the existing admission and silent-release
services. It deliberately owns no release state machine and never creates
approval evidence. The runner only binds immutable source facts, delegates
authority, and validates durable readbacks before moving to the next phase.
"""
from __future__ import annotations
from dataclasses import dataclass
import hashlib
import json
import re
import time
from typing import Any, Callable, Mapping
OID_RE = re.compile(r"^[0-9a-fA-F]{40}$")
SHA256_RE = re.compile(r"^[0-9a-fA-F]{64}$")
IDENT_RE = re.compile(r"^[A-Za-z0-9][A-Za-z0-9_.:-]{0,127}$")
RETRYABLE_CODES = {408, 425, 429, 500, 502, 503, 504}
RETRYABLE_NAMES = {
"UNAVAILABLE", "BUSY", "TEMPORARY_FAILURE", "TASK_ADMISSION_IN_PROGRESS",
}
RISKS = {"low", "medium", "engineering", "high"}
class UnattendedReleaseError(RuntimeError):
"""A fail-closed runner error with a stable code."""
def __init__(self, code: str, message: str | None = None):
self.code = code
super().__init__(message or code)
def _canon(value: Any) -> bytes:
return json.dumps(value, sort_keys=True, separators=(",", ":"), ensure_ascii=True).encode()
def _digest(value: Any) -> str:
return hashlib.sha256(_canon(value)).hexdigest()
def _require_oid(name: str, value: Any) -> str:
value = str(value or "")
if not OID_RE.fullmatch(value):
raise UnattendedReleaseError("INVALID_" + name.upper())
return value.lower()
def _require_sha(name: str, value: Any) -> str:
value = str(value or "")
if not SHA256_RE.fullmatch(value):
raise UnattendedReleaseError("INVALID_" + name.upper())
return value.lower()
@dataclass(frozen=True)
class SourcePublicationIntent:
"""Immutable, deterministic source and execution binding."""
repository: str
source_commit: str
source_tree_oid: str
authority_base_oid: str
target_branch: str
file_allowlist: tuple[str, ...]
requested_capabilities: tuple[str, ...]
risk: str
execution_digest: str
generation: int = 1
event_id: str = "release"
version: str = ""
admission_task_id: str = ""
admission_idempotency_key: str = ""
def __post_init__(self) -> None:
if not self.repository or not isinstance(self.repository, str):
raise UnattendedReleaseError("INVALID_REPOSITORY")
for name in ("source_commit", "source_tree_oid", "authority_base_oid"):
object.__setattr__(self, name, _require_oid(name, getattr(self, name)))
if not isinstance(self.target_branch, str) or not self.target_branch or not self.target_branch.startswith("agent/") or self.target_branch.startswith("-"):
raise UnattendedReleaseError("INVALID_TARGET_BRANCH")
if ".." in self.target_branch or "//" in self.target_branch or "@{" in self.target_branch:
raise UnattendedReleaseError("INVALID_TARGET_BRANCH")
if not isinstance(self.generation, int) or isinstance(self.generation, bool) or self.generation < 1:
raise UnattendedReleaseError("INVALID_GENERATION")
object.__setattr__(self, "execution_digest", _require_sha("execution_digest", self.execution_digest))
if not self.file_allowlist or any(not isinstance(p, str) or not p or p.startswith(("/", "\\")) or ".." in p or "\\" in p or p == ".git" or p.startswith(".git/") for p in self.file_allowlist):
raise UnattendedReleaseError("INVALID_FILE_ALLOWLIST")
if len(set(self.file_allowlist)) != len(self.file_allowlist):
raise UnattendedReleaseError("DUPLICATE_FILE_ALLOWLIST")
if not self.requested_capabilities or any(not isinstance(c, str) or not c or "*" in c for c in self.requested_capabilities):
raise UnattendedReleaseError("INVALID_CAPABILITIES")
if not isinstance(self.risk, str) or self.risk.lower() not in RISKS:
raise UnattendedReleaseError("INVALID_RISK")
object.__setattr__(self, "risk", self.risk.lower())
object.__setattr__(self, "file_allowlist", tuple(sorted(self.file_allowlist)))
object.__setattr__(self, "requested_capabilities", tuple(sorted(set(self.requested_capabilities))))
if not IDENT_RE.fullmatch(self.event_id):
raise UnattendedReleaseError("INVALID_EVENT_ID")
if self.version and not IDENT_RE.fullmatch(self.version):
raise UnattendedReleaseError("INVALID_VERSION")
if bool(self.admission_task_id) != bool(self.admission_idempotency_key):
raise UnattendedReleaseError("INCOMPLETE_ADMISSION_IDENTITY")
for value in (self.admission_task_id, self.admission_idempotency_key):
if value and (not isinstance(value, str) or not IDENT_RE.fullmatch(value)):
raise UnattendedReleaseError("INVALID_ADMISSION_IDENTITY")
def as_dict(self) -> dict[str, Any]:
value = {
"repository": self.repository, "source_commit": self.source_commit.lower(),
"source_tree_oid": self.source_tree_oid.lower(), "authority_base_oid": self.authority_base_oid.lower(),
"target_branch": self.target_branch, "file_allowlist": list(self.file_allowlist),
"requested_capabilities": list(self.requested_capabilities), "risk": self.risk,
"execution_digest": self.execution_digest.lower(), "generation": self.generation,
"event_id": self.event_id, "version": self.version,
}
if self.admission_task_id:
value.update(admission_task_id=self.admission_task_id,
admission_idempotency_key=self.admission_idempotency_key)
return value
@property
def task_id(self) -> str:
return self.admission_task_id or "release-task-" + _digest(self.as_dict())[:32]
@property
def idempotency_key(self) -> str:
return self.admission_idempotency_key or "release-intake-" + _digest({"task_id": self.task_id, "execution_digest": self.execution_digest.lower()})[:32]
def admission_request(self) -> dict[str, Any]:
"""Return the sole normalized request accepted by Task admission."""
return {"idempotency_key": self.idempotency_key, "task_id": self.task_id,
"repository": self.repository, "authority_base_oid": self.authority_base_oid,
"target_branch": self.target_branch, "generation": self.generation,
"execution_digest": self.execution_digest, "requested_capabilities": list(self.requested_capabilities),
"file_allowlist": list(self.file_allowlist), "risk": self.risk}
def _error_code(exc: BaseException) -> str:
code = getattr(exc, "code", None) or getattr(exc, "status", None) or exc.__class__.__name__
value = str(code).upper()
return value if re.fullmatch(r"[A-Z0-9_]{1,100}", value) else "BACKEND_ERROR"
def _bound_receipt(receipt: Any, intent: SourcePublicationIntent) -> bool:
if not isinstance(receipt, Mapping):
return False
expected = {"task_id": intent.task_id, "authority_base_oid": intent.authority_base_oid,
"target_branch": intent.target_branch, "generation": intent.generation,
"execution_digest": intent.execution_digest}
return (receipt.get("schema") == "aios.task-admission-receipt.v1"
and all(_canon(receipt.get(k)) == _canon(v) for k, v in expected.items())
and receipt.get("task_state") in {"QUEUED", "CLAIMED", "RUNNING"}
and all(isinstance(receipt.get(k), str) and receipt[k] for k in ("envelope_id", "grant_id")))
def _retryable(exc: BaseException) -> bool:
status = getattr(exc, "status_code", None) or getattr(exc, "http_status", None) or getattr(exc, "status", None)
if isinstance(status, int):
return status in RETRYABLE_CODES
code = _error_code(exc)
return code in RETRYABLE_NAMES
def _unknown_outcome(exc: BaseException) -> bool:
return isinstance(exc, (TimeoutError, ConnectionError, OSError)) or _error_code(exc) in {"TASK_ADMISSION_TASK_STATE_UNKNOWN", "TIMEOUT", "TIMED_OUT", "CONNECTION_LOST"}
class GovernedUnattendedReleaseRunner:
"""Phase-separated runner; preparation has no release/deployment rights."""
def __init__(self, *, admission: Any, release_service: Any = None,
publication_adapter: Any = None, owner_approval_broker: Callable[[Mapping[str, Any]], Mapping[str, Any]] | None = None,
deployment_enabled: bool = False, deployment_service: Any = None,
max_attempts: int = 3, deadline_seconds: float = 30.0,
environment_mode: str = "SIMULATION",
clock: Callable[[], float] = time.monotonic, sleep: Callable[[float], None] = time.sleep):
if admission is None:
raise ValueError("admission is required")
if max_attempts < 1 or max_attempts > 10 or deadline_seconds <= 0:
raise ValueError("invalid retry bounds")
self.admission = admission
self.environment_mode = str(environment_mode).upper()
if self.environment_mode not in {"SIMULATION", "AUTHORITY"}:
raise ValueError("environment_mode must be SIMULATION or AUTHORITY")
self.release_service = release_service
self.publication_adapter = publication_adapter
self.owner_approval_broker = owner_approval_broker
self.deployment_enabled = bool(deployment_enabled)
self.deployment_service = deployment_service
self.max_attempts = max_attempts
self.deadline_seconds = deadline_seconds
self.clock, self.sleep = clock, sleep
def intake(self, intent: SourcePublicationIntent) -> dict[str, Any]:
if not isinstance(intent, SourcePublicationIntent):
raise UnattendedReleaseError("INTENT_TYPE_REQUIRED")
return {"phase": "PREPARATION", "mode": self.environment_mode, "task_id": intent.task_id,
"idempotency_key": intent.idempotency_key, "intent": intent.as_dict(),
"release_rights": False, "deployment_rights": False}
def admit(self, intent: SourcePublicationIntent, authorization: Mapping[str, Any]) -> dict[str, Any]:
if not isinstance(authorization, Mapping):
raise UnattendedReleaseError("AUTHORIZATION_REQUIRED")
if self.environment_mode == "SIMULATION" and getattr(self.admission, "simulation_only", False) is not True:
return {"phase": "BLOCKED", "mode": self.environment_mode, "task_id": intent.task_id, "error": "AUTHORITY_MODE_REQUIRED"}
raw = intent.admission_request()
deadline = self.clock() + self.deadline_seconds
envelope_id = None
for attempt in range(1, self.max_attempts + 1):
if self.clock() >= deadline:
return {"phase": "RECONCILIATION_REQUIRED" if envelope_id else "BLOCKED", "mode": "AUTHORITY", "task_id": intent.task_id, "error": "ADMISSION_DEADLINE_EXCEEDED"}
try:
created = self.admission.create_envelope(raw)
envelope = created.get("envelope", created) if isinstance(created, Mapping) else {}
envelope_id = envelope.get("envelope_id") or envelope_id
if not isinstance(envelope_id, str) or not envelope_id:
raise UnattendedReleaseError("ENVELOPE_BINDING_MISSING")
auth = dict(authorization)
auth["envelope_id"] = envelope_id
if self.clock() >= deadline:
return self._reconcile(envelope_id, intent, "ADMISSION_DEADLINE_EXCEEDED", authorization)
receipt = self.admission.authorize_and_admit(auth)
if not _bound_receipt(receipt, intent) or receipt["envelope_id"] != envelope_id:
raise UnattendedReleaseError("ADMISSION_RECEIPT_BINDING_MISMATCH")
if self.clock() >= deadline:
return self._reconcile(envelope_id, intent, "ADMISSION_DEADLINE_EXCEEDED", authorization)
return {"phase": "ADMITTED", "mode": self.environment_mode, "attempt": attempt,
"envelope": created, "receipt": receipt}
except Exception as exc:
status_code = getattr(exc, "status_code", None) or getattr(exc, "http_status", None) or getattr(exc, "status", None)
if status_code == 403 or _error_code(exc) in {"403", "FORBIDDEN"}:
return {"phase": "BLOCKED", "mode": "AUTHORITY", "task_id": intent.task_id, "error": _error_code(exc)}
# Once authorization may have run, every failure reconciles.
# A 5xx is never sufficient proof of NOT_APPLIED.
if envelope_id:
return self._reconcile(envelope_id, intent, _error_code(exc), authorization)
if not _retryable(exc) or self.clock() >= deadline or attempt >= self.max_attempts:
return {"phase": "BLOCKED", "mode": "AUTHORITY", "task_id": intent.task_id, "error": _error_code(exc)}
self.sleep(min(0.05 * (2 ** (attempt - 1)), max(0.0, deadline - self.clock())))
raise UnattendedReleaseError("ADMISSION_RETRY_EXHAUSTED")
def _reconcile(self, envelope_id: str | None, intent: SourcePublicationIntent, error: str,
authorization: Mapping[str, Any]) -> dict[str, Any]:
if not envelope_id:
return {"phase": "RECONCILIATION_REQUIRED", "mode": "AUTHORITY", "task_id": intent.task_id, "error": error}
try:
reconcile = getattr(self.admission, "reconcile_status", None)
status = reconcile(envelope_id, authorization) if callable(reconcile) else self.admission.status(envelope_id)
except Exception as exc:
return {"phase": "RECONCILIATION_REQUIRED", "mode": "AUTHORITY", "task_id": intent.task_id, "error": error, "status_error": _error_code(exc)}
state = str(status.get("status") or status.get("task_state") or "").upper() if isinstance(status, Mapping) else ""
if state in {"NOT_APPLIED", "ABSENT", "NOT_FOUND"}:
return {"phase": "RETRYABLE_RECONCILIATION", "mode": "AUTHORITY", "task_id": intent.task_id, "status": status, "error": error}
if state == "ADMITTED" and _bound_receipt(status.get("task"), intent):
return {"phase": "RECONCILED", "mode": self.environment_mode, "task_id": intent.task_id, "status": status, "receipt": status["task"]}
return {"phase": "RECONCILIATION_REQUIRED", "mode": "AUTHORITY", "task_id": intent.task_id, "status": status, "error": error}
def publish(self, intent: SourcePublicationIntent, admission_receipt: Mapping[str, Any]) -> dict[str, Any]:
if self.environment_mode != "AUTHORITY":
return {"phase": "BLOCKED", "mode": self.environment_mode, "error": "AUTHORITY_MODE_REQUIRED"}
if self.publication_adapter is None:
return {"phase": "BLOCKED", "mode": "AUTHORITY", "error": "PUBLICATION_BACKEND_MISSING"}
required = ("task_lease", "task_principal", "writer_lease", "commit_receipt", "source_oid", "tree_oid", "authenticated")
try:
if not _bound_receipt(admission_receipt, intent):
return {"phase": "BLOCKED", "mode": "AUTHORITY", "error": "ADMISSION_RECEIPT_BINDING_MISMATCH"}
result = self.publication_adapter.publish(intent, admission_receipt)
if not isinstance(result, Mapping) or result.get("authenticated") is not True or any(not result.get(k) for k in required[:-1]):
return {"phase": "BLOCKED", "mode": "AUTHORITY", "error": "PUBLICATION_LEASE_CHAIN_MISSING"}
if any(not isinstance(result[k], Mapping) or result[k].get("task_id") != intent.task_id
for k in ("task_lease", "task_principal", "writer_lease")):
return {"phase": "BLOCKED", "mode": "AUTHORITY", "error": "PUBLICATION_LEASE_BINDING_MISMATCH"}
commit = result["commit_receipt"]
# Existing continuation receipts carry remote OIDs, while the
# authenticated task/job readback carries scope. Preserve both
# originals; never fabricate missing fields inside a receipt.
optional_bindings = {"task_id": intent.task_id, "tree_oid": result["tree_oid"],
"execution_digest": intent.execution_digest,
"target_branch": intent.target_branch,
"base_oid": intent.authority_base_oid}
if (not isinstance(commit, Mapping) or commit.get("commit_oid") != result["source_oid"]
or any(key in commit and commit[key] != value for key, value in optional_bindings.items())
or result.get("generation") != intent.generation):
return {"phase": "BLOCKED", "mode": "AUTHORITY", "error": "PUBLICATION_COMMIT_BINDING_MISMATCH"}
if not OID_RE.fullmatch(str(result["source_oid"])) or str(result["tree_oid"]).lower() != intent.source_tree_oid.lower() or any(str(result.get(k, "")) != str(v) for k, v in (("task_id", intent.task_id), ("authority_base_oid", intent.authority_base_oid), ("target_branch", intent.target_branch), ("execution_digest", intent.execution_digest))):
return {"phase": "BLOCKED", "mode": "AUTHORITY", "error": "PUBLICATION_RC_BINDING_MISMATCH"}
return {"phase": "PUBLISHED", "mode": "AUTHORITY", "readback": dict(result)}
except Exception as exc:
return {"phase": "RECONCILIATION_REQUIRED", "mode": "AUTHORITY", "error": _error_code(exc)}
def postpublication_rc(self, *, event_id: str, source_commit: str, version: str = "", source_readback: Mapping[str, Any] | None = None) -> dict[str, Any]:
if self.environment_mode != "AUTHORITY":
return {"phase": "BLOCKED", "mode": self.environment_mode, "error": "AUTHORITY_MODE_REQUIRED"}
if not isinstance(source_readback, Mapping) or source_readback.get("authenticated") is not True or str(source_readback.get("source_commit", "")).lower() != str(source_commit).lower():
return {"phase": "BLOCKED", "mode": "AUTHORITY", "error": "SOURCE_READBACK_REQUIRED"}
if self.release_service is None:
return {"phase": "BLOCKED", "mode": "AUTHORITY", "error": "RELEASE_BACKEND_MISSING"}
try:
# Only a host-injected authenticated resolver can admit a source.
# The caller's readback object alone is never authorization.
resolver = getattr(self.release_service, "source_admission_resolver", None)
admitted_source = resolver(source_commit) if callable(resolver) else None
if (not isinstance(admitted_source, Mapping) or not admitted_source.get("admission_id")
or admitted_source.get("source_commit") != source_commit
or admitted_source.get("tree_oid") != source_readback.get("tree_oid")):
return {"phase": "BLOCKED", "mode": "AUTHORITY", "error": "AUTHENTICATED_SOURCE_ADMISSION_REQUIRED"}
intent = self.release_service.create_release_intent({"event_id": event_id, "source_commit": source_commit, **({"version": version} if version else {})})
candidate_id = str(intent.get("candidate_id") or intent.get("candidate", {}).get("candidate_id") or "")
if not candidate_id:
return {"phase": "BLOCKED", "mode": "AUTHORITY", "error": "CANDIDATE_BINDING_MISSING"}
readers = ("get_release_candidate", "get_release_plan", "get_acceptance")
if any(not callable(getattr(self.release_service, name, None)) for name in readers):
return {"phase": "BLOCKED", "mode": "AUTHORITY", "candidate_id": candidate_id, "error": "RC_READBACK_BACKEND_MISSING"}
candidate = self.release_service.get_release_candidate(candidate_id)
plan = self.release_service.get_release_plan(candidate_id)
acceptance = self.release_service.get_acceptance(candidate_id)
deployment = plan.get("deployment") if isinstance(plan, Mapping) else None
if (not isinstance(candidate, Mapping) or not isinstance(plan, Mapping) or not isinstance(deployment, Mapping) or not isinstance(acceptance, Mapping)
or str(candidate.get("candidate_id")) != candidate_id
or str(candidate.get("source_commit")) .lower() != str(source_commit).lower()
or str(plan.get("candidate_id", candidate_id)) != candidate_id
or str(deployment.get("source_commit")) .lower() != str(source_commit).lower()
or str(deployment.get("tree_oid", "")).lower() != str(source_readback.get("tree_oid", "")).lower()
or candidate.get("tree_oid") != deployment.get("tree_oid")
or candidate.get("artifact_sha256") != deployment.get("artifact_sha256")
or candidate.get("manifest_sha256") != deployment.get("manifest_sha256")
or str(acceptance.get("candidate_id")) != candidate_id
or acceptance.get("release_plan_sha256") != plan.get("sha256")
or str(acceptance.get("status")) != "ACCEPTED"):
return {"phase": "BLOCKED", "mode": "AUTHORITY", "candidate_id": candidate_id, "error": "RC_READBACK_BINDING_MISMATCH"}
# Public readback is redacted. Reverify original signed bytes inside
# the existing service, not by reconstructing them from API output.
verify_acceptance = getattr(self.release_service, "verify_release_acceptance", None)
if not callable(verify_acceptance):
return {"phase": "BLOCKED", "mode": "AUTHORITY", "error": "BUILDER_ATTESTATION_REQUIRED"}
verified = verify_acceptance(candidate_id)
if (not isinstance(verified, Mapping) or verified.get("status") != "VERIFIED"
or verified.get("candidate_id") != candidate_id
or verified.get("release_plan_sha256") != plan.get("sha256")):
return {"phase": "BLOCKED", "mode": "AUTHORITY", "error": "BUILDER_ATTESTATION_NOT_VERIFIED"}
envelope = self.release_service.get_approval_envelope(candidate_id)
if self.owner_approval_broker is None:
return {"phase": "BLOCKED", "mode": "AUTHORITY", "candidate_id": candidate_id, "error": "OWNER_APPROVAL_BACKEND_MISSING"}
approval = self.owner_approval_broker(dict(envelope))
if not isinstance(approval, Mapping):
return {"phase": "BLOCKED", "mode": "AUTHORITY", "candidate_id": candidate_id, "error": "OWNER_APPROVAL_MISSING"}
accepted = self.release_service.submit_owner_approval(dict(approval))
if not isinstance(accepted, Mapping) or accepted.get("status") != "VERIFIED_AND_STORED":
return {"phase": "BLOCKED", "mode": "AUTHORITY", "error": "APPROVAL_NOT_VERIFIED"}
return {"phase": "APPROVED", "mode": "AUTHORITY", "candidate_id": candidate_id,
"readback": {"candidate": dict(candidate), "plan": dict(plan), "acceptance": dict(acceptance)},
"approval": accepted}
except Exception as exc:
return {"phase": "BLOCKED", "mode": "AUTHORITY", "error": _error_code(exc)}
def deploy(self, candidate_id: str) -> dict[str, Any]:
if (self.environment_mode != "AUTHORITY" or not self.deployment_enabled or self.deployment_service is None
or getattr(self.deployment_service, "execution_enabled", False) is not True):
return {"phase": "BLOCKED", "mode": "AUTHORITY", "error": "DEPLOYMENT_NOT_ENABLED"}
try:
result = self.deployment_service.start_deployment(candidate_id)
saga = self.deployment_service.get_release_saga(candidate_id)
if (not isinstance(result, Mapping) or result.get("candidate_id") != candidate_id
or result.get("state") != "COMPLETED" or result.get("status") not in {"VERIFIED", "IDEMPOTENT"}
or not isinstance(saga, Mapping) or saga.get("state") != "COMPLETED"):
return {"phase": "BLOCKED", "mode": "AUTHORITY", "error": "DEPLOYMENT_NOT_VERIFIED", "result": dict(result) if isinstance(result, Mapping) else {}}
return {"phase": "DEPLOYED", "mode": "AUTHORITY", "result": dict(result)}
except Exception as exc:
return {"phase": "BLOCKED", "mode": "AUTHORITY", "error": _error_code(exc)}
@@ -0,0 +1,136 @@
"""Local adapter for the root-owned registered Owner signer service.
The adapter intentionally has no public arbitrary-payload signing method.
``LocalOwnerAuthority`` calls the private typed seam after validating and
canonicalizing a structured Owner authorization object.
"""
from __future__ import annotations
import os
from pathlib import Path
import stat
import subprocess
import tempfile
from typing import Any, Callable
DEFAULT_REGISTERED_SIGNER_COMMAND = Path("/usr/local/libexec/aios-grant-signer-adapter")
OWNER_NAMESPACE = "aios-owner-approval-v1"
MAX_SIGNATURE_BYTES = 128 * 1024
MAX_KEY_ID_BYTES = 512
class RegisteredOwnerSignerError(RuntimeError):
pass
class RegisteredOwnerSignerAdapter:
"""Invoke the registered signer without reading or copying private keys."""
def __init__(
self,
command: Path | str = DEFAULT_REGISTERED_SIGNER_COMMAND,
*,
key_id: str,
owner_verifier: Any,
require_root: bool = True,
test_mode: bool = False,
runner: Callable[..., Any] = subprocess.run,
) -> None:
self.command = str(command)
self.key_id = key_id
self.owner_verifier = owner_verifier
self.require_root = require_root
self.test_mode = test_mode
self.runner = runner
if not isinstance(key_id, str) or not key_id:
raise RegisteredOwnerSignerError("REGISTERED_SIGNER_KEY_ID_REQUIRED")
if not test_mode:
self._validate_command()
@property
def fingerprint(self) -> str:
return self.key_id
def _validate_command(self) -> None:
path = Path(self.command)
if not path.is_absolute() or path.is_symlink() or not path.is_file():
raise RegisteredOwnerSignerError("REGISTERED_SIGNER_COMMAND_INVALID")
st = path.stat()
if not hasattr(os, "geteuid") or st.st_uid != os.geteuid():
raise RegisteredOwnerSignerError("REGISTERED_SIGNER_COMMAND_OWNER_INVALID")
if stat.S_IMODE(st.st_mode) & 0o022:
raise RegisteredOwnerSignerError("REGISTERED_SIGNER_COMMAND_PERMISSIONS_INVALID")
def _read_output(self, path: Path, maximum: int) -> bytes:
if path.is_symlink() or not path.is_file():
raise RegisteredOwnerSignerError("REGISTERED_SIGNER_OUTPUT_INVALID")
st = path.stat()
if not self.test_mode and (not hasattr(os, "geteuid") or st.st_uid != os.geteuid()):
raise RegisteredOwnerSignerError("REGISTERED_SIGNER_OUTPUT_OWNER_INVALID")
if st.st_size > maximum:
raise RegisteredOwnerSignerError("REGISTERED_SIGNER_OUTPUT_TOO_LARGE")
return path.read_bytes()
def _sign_canonical_payload(self, namespace: str, payload: bytes) -> str:
"""Private seam used only by typed ``LocalOwnerAuthority`` issuance."""
if namespace != OWNER_NAMESPACE:
raise RegisteredOwnerSignerError("REGISTERED_SIGNER_NAMESPACE_INVALID")
if not isinstance(payload, bytes) or not payload:
raise RegisteredOwnerSignerError("REGISTERED_SIGNER_PAYLOAD_INVALID")
if self.require_root:
if not hasattr(os, "geteuid") or os.geteuid() != 0:
raise RegisteredOwnerSignerError("REGISTERED_SIGNER_ROOT_REQUIRED")
if not self.test_mode:
self._validate_command()
with tempfile.TemporaryDirectory(prefix="aios-owner-sign-") as directory:
root = Path(directory)
input_path = root / "payload"
signature_path = root / "payload.sig"
key_id_path = root / "key-id"
input_path.write_bytes(payload)
try:
result = self.runner(
[
self.command,
"sign",
"--namespace",
namespace,
"--input",
str(input_path),
"--signature-out",
str(signature_path),
"--key-id-out",
str(key_id_path),
],
check=False,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
timeout=30,
)
except Exception as exc:
raise RegisteredOwnerSignerError("REGISTERED_SIGNER_FAILED") from exc
if getattr(result, "returncode", None) != 0:
raise RegisteredOwnerSignerError("REGISTERED_SIGNER_FAILED")
if not signature_path.is_file() or not key_id_path.is_file():
raise RegisteredOwnerSignerError("REGISTERED_SIGNER_OUTPUT_MISSING")
supplied_key_id = self._read_output(key_id_path, MAX_KEY_ID_BYTES).decode("utf-8").strip()
if supplied_key_id != self.key_id:
raise RegisteredOwnerSignerError("REGISTERED_SIGNER_KEY_ID_MISMATCH")
signature = self._read_output(signature_path, MAX_SIGNATURE_BYTES).decode("utf-8")
if not signature.startswith("-----BEGIN SSH SIGNATURE-----"):
raise RegisteredOwnerSignerError("REGISTERED_SIGNER_SIGNATURE_INVALID")
try:
verified = self.owner_verifier.verify(
namespace=namespace,
payload=payload,
signature=signature,
key_id=self.key_id,
)
except Exception as exc:
raise RegisteredOwnerSignerError("REGISTERED_SIGNER_VERIFY_FAILED") from exc
if verified is not True:
raise RegisteredOwnerSignerError("REGISTERED_SIGNER_VERIFY_REJECTED")
return signature
File diff suppressed because it is too large Load Diff
@@ -553,7 +553,7 @@ class ReleaseSourceAdmissionStore:
supplied_candidate = str(candidate_id or "")
if supplied_candidate and supplied_candidate != record["candidate_id"]:
raise ReleaseSourceAdmissionError("SOURCE_ADMISSION_CANDIDATE_MISMATCH")
return {
result = {
"mode": RECIPE_MODE,
"task_id": record["task_id"],
"candidate_id": record["candidate_id"],
@@ -562,6 +562,9 @@ class ReleaseSourceAdmissionStore:
"version": record["version"],
"purpose": record["purpose"],
}
if version == "V2R2C5":
result["tree_oid"] = record["tree_oid"]
return result
@dataclass(slots=True)
@@ -0,0 +1,716 @@
#!/usr/bin/env python3
"""Host-injected broker for short release approvals under standing authority.
The broker deliberately owns no business-authority ledger. Its readers must
return verified current snapshots from the host's durable authority and
release-lineage stores. It only derives, verifies, and persists short-lived
``SignedReleasePlanApproval`` children for the exact local ``ReleasePlan``.
"""
from __future__ import annotations
import dataclasses
import hashlib
import secrets
from dataclasses import dataclass
from datetime import datetime, timedelta, timezone
from pathlib import Path, PurePosixPath
from typing import Any, Callable, Mapping, Protocol
from silent_release_controller import CANDIDATE_ID_RE, OID_RE, SHA256_RE, _parse_utc
from silent_release_hardening import (
ALLOWED_CAPABILITIES,
CapabilityAuthorizationError,
ReleasePlan,
SignedReleasePlanApproval,
SilentReleaseActionService,
_canonical_json,
release_file_lock,
)
from task_plane.governance_control_kernel import (
AuthorityDecisionContext,
AuthorityLifecycleState,
ExactScopeState,
OwnerBoundaryReason,
)
_EFFECT_STATES = frozenset(
{"KNOWN_NOT_APPLIED", "KNOWN_IN_PROGRESS", "APPLIED_VERIFIED", "UNKNOWN"}
)
_LINEAGE_KINDS = frozenset({"ROOT", "SUCCESSOR"})
_BINDING_KEYS = frozenset(
{
"authorization_id",
"generation",
"authorization_sha256",
"version",
"repository",
"production_scope",
"authority_base_oid",
"trust_root_sha256",
"actor_id",
"mission_id",
"run_id",
}
)
class StandingAuthorityError(CapabilityAuthorizationError):
"""A current standing-authority or exact-scope check failed closed."""
def __init__(self, code: str, message: str):
super().__init__(message)
self.code = code
def _text(name: str, value: str) -> str:
if not isinstance(value, str) or not value or value != value.strip() or "\x00" in value:
raise ValueError(f"{name} must be a non-empty canonical string")
return value
def _sha(name: str, value: str) -> str:
value = _text(name, value)
if not SHA256_RE.fullmatch(value):
raise ValueError(f"{name} must be a SHA-256 digest")
return value
def _oid(name: str, value: str) -> str:
value = _text(name, value)
if not OID_RE.fullmatch(value):
raise ValueError(f"{name} must be a Git object id")
return value
def _tuple_of_text(name: str, value: tuple[str, ...]) -> tuple[str, ...]:
if not isinstance(value, tuple) or not value:
raise ValueError(f"{name} must be a non-empty tuple")
normalized = tuple(_text(name, item) for item in value)
if len(set(normalized)) != len(normalized):
raise ValueError(f"{name} contains duplicates")
return normalized
def _utc(value: datetime) -> datetime:
if not isinstance(value, datetime) or value.tzinfo is None:
raise ValueError("clock must return a timezone-aware datetime")
return value.astimezone(timezone.utc)
def _format_utc(value: datetime) -> str:
return _utc(value).replace(microsecond=0).isoformat().replace("+00:00", "Z")
@dataclass(frozen=True)
class CanonicalPlanScope:
version: str
repository: str
service: str
release_root: str
cutover_link: str
authoritative_git_dir: str
dropin_relative_path: str
rollback_target: str
required_capabilities: tuple[str, ...]
def __post_init__(self) -> None:
for name in (
"version",
"repository",
"service",
"release_root",
"cutover_link",
"authoritative_git_dir",
"dropin_relative_path",
"rollback_target",
):
_text(name, getattr(self, name))
capabilities = _tuple_of_text(
"required_capabilities", self.required_capabilities
)
if tuple(sorted(capabilities)) != capabilities:
raise ValueError("required_capabilities must be sorted")
object.__setattr__(self, "required_capabilities", capabilities)
@property
def canonical_payload(self) -> dict[str, Any]:
return {
"cutover_link": self.cutover_link,
"authoritative_git_dir": self.authoritative_git_dir,
"dropin_relative_path": self.dropin_relative_path,
"release_root": self.release_root,
"repository": self.repository,
"required_capabilities": list(self.required_capabilities),
"rollback_target": self.rollback_target,
"service": self.service,
"version": self.version,
}
@property
def sha256(self) -> str:
return hashlib.sha256(_canonical_json(self.canonical_payload)).hexdigest()
def canonical_plan_scope(
plan: ReleasePlan, required_capabilities: tuple[str, ...]
) -> CanonicalPlanScope:
"""Derive production scope only from a validated local ReleasePlan."""
plan.validate()
release_path = PurePosixPath(plan.deployment.release_path)
dropin_path = PurePosixPath(plan.deployment.dropin_path)
try:
dropin_relative = dropin_path.relative_to(release_path).as_posix()
except ValueError as exc:
raise ValueError("drop-in path is outside the candidate release path") from exc
if dropin_relative in {"", "."}:
raise ValueError("drop-in path must be a child of the release path")
return CanonicalPlanScope(
version=plan.tag.version,
repository=plan.deployment.repository_identity,
service=plan.deployment.service,
release_root=plan.deployment.release_root,
cutover_link=plan.deployment.cutover_link,
authoritative_git_dir=plan.deployment.authoritative_git_dir,
dropin_relative_path=dropin_relative,
rollback_target=plan.deployment.rollback_path,
required_capabilities=tuple(sorted(required_capabilities)),
)
@dataclass(frozen=True)
class StandingReleaseAuthorization:
authorization_id: str
generation: int
authorization_sha256: str
version: str
repository: str
production_scope: str
authority_base_oid: str
trust_root_sha256: str
trust_domain: str
lineage_root_candidate_fingerprint: str
allowed_capabilities: tuple[str, ...]
rollback_target: str
owner_identity: str
allowed_actor_ids: tuple[str, ...]
mission_id: str
run_id: str
lifecycle_state: AuthorityLifecycleState
acceptance_complete: bool
acceptance_evidence_ref: str | None = None
expires_at: str | None = None
evidence_refs: tuple[str, ...] = ()
def __post_init__(self) -> None:
for name in (
"authorization_id",
"version",
"repository",
"trust_domain",
"rollback_target",
"owner_identity",
"mission_id",
"run_id",
):
_text(name, getattr(self, name))
if (
not isinstance(self.generation, int)
or isinstance(self.generation, bool)
or self.generation < 1
):
raise ValueError("generation must be a positive integer")
_sha("authorization_sha256", self.authorization_sha256)
_sha("production_scope", self.production_scope)
_oid("authority_base_oid", self.authority_base_oid)
_sha("trust_root_sha256", self.trust_root_sha256)
_sha(
"lineage_root_candidate_fingerprint",
self.lineage_root_candidate_fingerprint,
)
capabilities = _tuple_of_text(
"allowed_capabilities", self.allowed_capabilities
)
if not set(capabilities).issubset(ALLOWED_CAPABILITIES):
raise ValueError("allowed_capabilities contains an unsupported capability")
object.__setattr__(self, "allowed_capabilities", capabilities)
object.__setattr__(
self, "allowed_actor_ids", _tuple_of_text("allowed_actor_ids", self.allowed_actor_ids)
)
if not isinstance(self.lifecycle_state, AuthorityLifecycleState):
raise ValueError("lifecycle_state must be AuthorityLifecycleState")
if type(self.acceptance_complete) is not bool:
raise ValueError("acceptance_complete must be a boolean")
if self.acceptance_complete:
_text("acceptance_evidence_ref", self.acceptance_evidence_ref or "")
elif self.acceptance_evidence_ref is not None:
raise ValueError("acceptance evidence is only valid for completed acceptance")
if self.expires_at is not None:
_parse_utc(self.expires_at)
if not isinstance(self.evidence_refs, tuple):
raise ValueError("evidence_refs must be a tuple")
for value in self.evidence_refs:
_text("evidence_ref", value)
@dataclass(frozen=True)
class ReleaseExecutionScope:
release_plan_id: str
candidate_id: str
candidate_fingerprint: str
source_commit: str
tree_oid: str
artifact_sha256: str
manifest_sha256: str
deployment_plan_sha256: str
release_plan_sha256: str
approval_plan_id: str
evidence_closure_oid: str
owner_approval_record_oid: str
authority_base_oid: str
trust_root_sha256: str
lineage_root_candidate_fingerprint: str
actor_id: str
mission_id: str
run_id: str
effect_state: str
lineage_kind: str
predecessor_candidate_fingerprint: str | None = None
successor_lineage_sha256: str | None = None
evidence_refs: tuple[str, ...] = ()
def __post_init__(self) -> None:
for name in (
"release_plan_id",
"candidate_id",
"approval_plan_id",
"actor_id",
"mission_id",
"run_id",
):
_text(name, getattr(self, name))
if not CANDIDATE_ID_RE.fullmatch(self.candidate_id):
raise ValueError("candidate_id is invalid")
for name in ("source_commit", "tree_oid", "evidence_closure_oid", "owner_approval_record_oid", "authority_base_oid"):
_oid(name, getattr(self, name))
for name in (
"candidate_fingerprint",
"artifact_sha256",
"manifest_sha256",
"deployment_plan_sha256",
"release_plan_sha256",
"trust_root_sha256",
"lineage_root_candidate_fingerprint",
):
_sha(name, getattr(self, name))
if self.effect_state not in _EFFECT_STATES:
raise ValueError("effect_state is invalid")
if self.lineage_kind not in _LINEAGE_KINDS:
raise ValueError("lineage_kind is invalid")
if self.lineage_kind == "ROOT":
if self.predecessor_candidate_fingerprint is not None or self.successor_lineage_sha256 is not None:
raise ValueError("root candidate cannot carry successor lineage")
else:
_sha("predecessor_candidate_fingerprint", self.predecessor_candidate_fingerprint or "")
_sha("successor_lineage_sha256", self.successor_lineage_sha256 or "")
if self.predecessor_candidate_fingerprint == self.candidate_fingerprint:
raise ValueError("successor predecessor must be a different candidate")
if not isinstance(self.evidence_refs, tuple):
raise ValueError("evidence_refs must be a tuple")
for value in self.evidence_refs:
_text("evidence_ref", value)
class RegisteredApprovalSigner(Protocol):
@property
def key_id(self) -> str: ...
def sign(
self, unsigned: SignedReleasePlanApproval
) -> SignedReleasePlanApproval | Mapping[str, Any]: ...
class StandingReleaseAuthorizationBroker:
"""Issue and renew exact short approvals from two trusted read layers."""
def __init__(
self,
service: SilentReleaseActionService,
authority_reader: Callable[[], StandingReleaseAuthorization],
execution_scope_reader: Callable[[Mapping[str, Any]], ReleaseExecutionScope],
signer: RegisteredApprovalSigner,
*,
clock: Callable[[], datetime] = lambda: datetime.now(timezone.utc),
execution_ttl_seconds: int = 1200,
renewal_margin_seconds: int = 120,
):
if not callable(authority_reader) or not callable(execution_scope_reader):
raise TypeError("standing authority readers must be callable")
if not callable(getattr(signer, "sign", None)):
raise TypeError("registered approval signer must provide sign()")
if not callable(clock):
raise TypeError("clock must be callable")
if not isinstance(service, SilentReleaseActionService):
raise TypeError("service must be SilentReleaseActionService")
if service.execution_enabled and getattr(signer, "test_only", False) is True:
raise CapabilityAuthorizationError(
"test-only approval signer is forbidden for production execution"
)
if not isinstance(execution_ttl_seconds, int) or isinstance(execution_ttl_seconds, bool) or not 1 <= execution_ttl_seconds <= 3600:
raise ValueError("execution_ttl_seconds must be between 1 and 3600")
if not isinstance(renewal_margin_seconds, int) or isinstance(renewal_margin_seconds, bool) or not 0 <= renewal_margin_seconds < execution_ttl_seconds:
raise ValueError("renewal_margin_seconds must be shorter than the execution TTL")
self.service = service
self.authority_reader = authority_reader
self.execution_scope_reader = execution_scope_reader
self.signer = signer
self.clock = clock
self.execution_ttl_seconds = execution_ttl_seconds
self.renewal_margin_seconds = renewal_margin_seconds
self._lock_root = Path(service.approvals_root) / ".standing-authority-locks"
@staticmethod
def _result(status: str, context: AuthorityDecisionContext, approval=None):
# silent_release_execution imports this module during host composition.
from silent_release_execution import StandingAuthorizationResult
return StandingAuthorizationResult(
status=status, authority_context=context, approval=approval
)
@staticmethod
def _context(
authorization: StandingReleaseAuthorization | None,
scope_state: ExactScopeState,
*,
lifecycle_state: AuthorityLifecycleState | None = None,
boundary: OwnerBoundaryReason | None = None,
) -> AuthorityDecisionContext:
refs = () if authorization is None else authorization.evidence_refs
return AuthorityDecisionContext(
lifecycle_state=lifecycle_state
or (authorization.lifecycle_state if authorization else AuthorityLifecycleState.UNKNOWN),
scope_state=scope_state,
owner_boundary_reason=boundary,
evidence_refs=refs,
)
@staticmethod
def _binding(
authorization: StandingReleaseAuthorization, scope: ReleaseExecutionScope
) -> dict[str, Any]:
return {
"authorization_id": authorization.authorization_id,
"generation": authorization.generation,
"authorization_sha256": authorization.authorization_sha256,
"version": authorization.version,
"repository": authorization.repository,
"production_scope": authorization.production_scope,
"authority_base_oid": authorization.authority_base_oid,
"trust_root_sha256": authorization.trust_root_sha256,
"actor_id": scope.actor_id,
"mission_id": authorization.mission_id,
"run_id": authorization.run_id,
}
def _read(self, envelope: Mapping[str, Any]):
authorization = self.authority_reader()
# The trusted scope reader receives only the locally checked release
# identity. Caller-added envelope keys cannot become authority input.
query_keys = (
"approval_plan_id",
"artifact_sha256",
"candidate_fingerprint",
"candidate_id",
"deployment_plan_sha256",
"evidence_closure_oid",
"manifest_sha256",
"policy_sha256",
"release_plan_sha256",
"required_capabilities",
"schema",
"source_commit",
"tree_oid",
)
scope = self.execution_scope_reader(
{name: envelope[name] for name in query_keys if name in envelope}
)
if not isinstance(authorization, StandingReleaseAuthorization):
raise StandingAuthorityError("AUTHORITY_SNAPSHOT_INVALID", "authority reader returned an invalid snapshot")
if not isinstance(scope, ReleaseExecutionScope):
raise StandingAuthorityError("RELEASE_SCOPE_INVALID", "scope reader returned an invalid snapshot")
return authorization, scope
def _load_plan_truth(self, envelope: Mapping[str, Any]):
candidate_id = str(envelope.get("candidate_id") or "")
if not CANDIDATE_ID_RE.fullmatch(candidate_id):
raise StandingAuthorityError("APPROVAL_ENVELOPE_INVALID", "candidate id is invalid")
candidate = self.service.candidates.load(candidate_id)
plan = self.service.release_plans.load(candidate_id)
required = tuple(sorted(str(item) for item in envelope.get("required_capabilities", ())))
if not required or len(required) != len(set(required)) or not set(required).issubset(ALLOWED_CAPABILITIES):
raise StandingAuthorityError("APPROVAL_ENVELOPE_INVALID", "required capabilities are invalid")
plan_scope = canonical_plan_scope(plan, required)
expected_envelope = {
"candidate_id": candidate.candidate_id,
"candidate_fingerprint": candidate.fingerprint,
"source_commit": candidate.source_commit,
"tree_oid": candidate.tree_oid,
"artifact_sha256": candidate.artifact_sha256,
"manifest_sha256": candidate.manifest_sha256,
"deployment_plan_sha256": plan.deployment.sha256,
"release_plan_sha256": plan.sha256,
"approval_plan_id": plan.tag.approval_plan_id,
"evidence_closure_oid": plan.tag.evidence_closure_oid,
"policy_sha256": self.service.policy_sha256,
}
for name, expected in expected_envelope.items():
if envelope.get(name) != expected:
raise StandingAuthorityError("APPROVAL_ENVELOPE_STALE", f"approval envelope {name} is stale")
return candidate, plan, plan_scope, required
def _assert_current(
self,
authorization: StandingReleaseAuthorization,
scope: ReleaseExecutionScope,
envelope: Mapping[str, Any],
plan_scope: CanonicalPlanScope,
required: tuple[str, ...],
now: datetime,
) -> None:
if authorization.acceptance_complete:
raise StandingAuthorityError("AUTHORITY_ACCEPTED_AND_CLOSED", "trusted release acceptance is complete")
if authorization.lifecycle_state is not AuthorityLifecycleState.ACTIVE:
raise StandingAuthorityError(f"AUTHORITY_{authorization.lifecycle_state.value}", "standing authorization is not active")
if authorization.expires_at is not None and _parse_utc(authorization.expires_at) <= now:
raise StandingAuthorityError("AUTHORITY_EXPIRED", "standing authorization has expired")
if authorization.version != plan_scope.version or authorization.repository != plan_scope.repository:
raise StandingAuthorityError("AUTHORITY_RELEASE_SCOPE_MISMATCH", "release identity is outside standing authority")
if authorization.production_scope != plan_scope.sha256 or authorization.rollback_target != plan_scope.rollback_target:
raise StandingAuthorityError("AUTHORITY_PRODUCTION_SCOPE_MISMATCH", "production or rollback scope drifted")
if not set(required).issubset(authorization.allowed_capabilities):
raise StandingAuthorityError("AUTHORITY_CAPABILITY_DENIED", "required capability is outside standing authority")
exact_scope = (
scope.candidate_id == envelope["candidate_id"]
and scope.candidate_fingerprint == envelope["candidate_fingerprint"]
and scope.source_commit == envelope["source_commit"]
and scope.tree_oid == envelope["tree_oid"]
and scope.artifact_sha256 == envelope["artifact_sha256"]
and scope.manifest_sha256 == envelope["manifest_sha256"]
and scope.deployment_plan_sha256 == envelope["deployment_plan_sha256"]
and scope.release_plan_sha256 == envelope["release_plan_sha256"]
and scope.approval_plan_id == envelope["approval_plan_id"]
and scope.evidence_closure_oid == envelope["evidence_closure_oid"]
and scope.authority_base_oid == authorization.authority_base_oid
and scope.trust_root_sha256 == authorization.trust_root_sha256
and scope.actor_id in authorization.allowed_actor_ids
and scope.mission_id == authorization.mission_id
and scope.run_id == authorization.run_id
)
if not exact_scope:
raise StandingAuthorityError("AUTHORITY_EXACT_SCOPE_MISMATCH", "trusted release execution scope does not match")
durable_release_plan_id = self.service.sagas.load(
scope.candidate_id
).release_plan_id
if scope.release_plan_id != durable_release_plan_id:
raise StandingAuthorityError(
"RELEASE_PLAN_ID_MISMATCH",
"trusted scope does not match the durable Saga release plan",
)
if scope.lineage_root_candidate_fingerprint != authorization.lineage_root_candidate_fingerprint:
raise StandingAuthorityError("AUTHORITY_LINEAGE_ROOT_MISMATCH", "candidate lineage is not rooted in standing authority")
if scope.lineage_kind == "ROOT" and scope.candidate_fingerprint != authorization.lineage_root_candidate_fingerprint:
raise StandingAuthorityError("AUTHORITY_LINEAGE_REQUIRED", "changed candidate requires verified successor lineage")
if scope.effect_state == "UNKNOWN":
raise StandingAuthorityError("RELEASE_EFFECT_UNKNOWN", "release effect must be reconciled before issuance")
def _unsigned(
self,
authorization: StandingReleaseAuthorization,
scope: ReleaseExecutionScope,
plan: ReleasePlan,
required: tuple[str, ...],
now: datetime,
) -> SignedReleasePlanApproval:
expires = now + timedelta(seconds=self.execution_ttl_seconds)
if authorization.expires_at is not None:
expires = min(expires, _parse_utc(authorization.expires_at))
if expires <= now:
raise StandingAuthorityError("AUTHORITY_EXPIRED", "standing authorization cannot cover a new child")
issued_at = _format_utc(now)
return SignedReleasePlanApproval(
release_plan_id=scope.release_plan_id,
candidate_id=scope.candidate_id,
source_commit=scope.source_commit,
tree_oid=scope.tree_oid,
artifact_sha256=scope.artifact_sha256,
manifest_sha256=scope.manifest_sha256,
deployment_plan_sha256=plan.deployment.sha256,
policy_sha256=self.service.policy_sha256,
approval_plan_id=scope.approval_plan_id,
evidence_closure_oid=scope.evidence_closure_oid,
owner_approval_record_oid=scope.owner_approval_record_oid,
capabilities=required,
expires_at=_format_utc(expires),
nonce="standing-" + secrets.token_hex(16),
key_id=_text("signer.key_id", self.signer.key_id),
signature="",
owner_identity=authorization.owner_identity,
trust_domain=authorization.trust_domain,
issued_at=issued_at,
not_before=issued_at,
release_plan_sha256=plan.sha256,
authorization_binding=self._binding(authorization, scope),
)
@staticmethod
def _signed(value: SignedReleasePlanApproval | Mapping[str, Any]) -> SignedReleasePlanApproval:
return value if isinstance(value, SignedReleasePlanApproval) else SignedReleasePlanApproval.from_dict(value)
def _verify_signed(self, approval, candidate, plan, now):
verifier = self.service.approval_verifier
if verifier is None:
raise StandingAuthorityError("APPROVAL_VERIFIER_MISSING", "approval verifier is not configured")
verifier.verify_release_plan(approval, candidate, plan, self.service.policy_sha256, now=now)
def __call__(self, envelope: Mapping[str, Any]):
if not isinstance(envelope, Mapping):
raise TypeError("approval envelope must be a mapping")
candidate_id = str(envelope.get("candidate_id") or "")
lock_name = hashlib.sha256(candidate_id.encode("utf-8")).hexdigest() + ".lock"
lock_path = self._lock_root / lock_name
with release_file_lock(lock_path, blocking=True):
try:
candidate, plan, plan_scope, required = self._load_plan_truth(envelope)
now = _utc(self.clock())
authorization, scope = self._read(envelope)
self._assert_current(authorization, scope, envelope, plan_scope, required, now)
try:
existing = self.service.approvals.load(candidate_id)
except FileNotFoundError:
existing = None
if existing is not None:
# Expiry is the one expected reason an otherwise valid
# short child needs replacement. Verify its signature,
# immutable evidence, key and revocation state at the end
# of its signed window; a future/not-yet-valid or malformed
# child still fails the verifier.
# Expiry is exclusive. When renewing an already expired
# child, verify its signature at the final instant inside
# its original signed window rather than at expiry itself.
old_expiry = _parse_utc(existing.expires_at)
old_verification_time = min(
now, old_expiry - timedelta(microseconds=1)
)
self._verify_signed(existing, candidate, plan, old_verification_time)
self._check_binding(existing, authorization, scope, required)
if _parse_utc(existing.expires_at) - now > timedelta(seconds=self.renewal_margin_seconds):
return self._result("APPROVAL_ISSUED", self._context(authorization, ExactScopeState.IN_SCOPE), existing.to_dict())
unsigned = self._unsigned(authorization, scope, plan, required, now)
signed = self._signed(self.signer.sign(unsigned))
# A signer may only add its signature to the complete request.
if signed.canonical_payload != unsigned.canonical_payload:
raise StandingAuthorityError("SIGNER_MUTATED_APPROVAL", "registered signer changed the approval request")
self._verify_signed(signed, candidate, plan, now)
after_authorization, after_scope = self._read(envelope)
after_now = _utc(self.clock())
self._assert_current(after_authorization, after_scope, envelope, plan_scope, required, after_now)
if after_authorization != authorization or after_scope != scope:
raise StandingAuthorityError("AUTHORITY_CHANGED_DURING_SIGN", "authority changed while the child was signed")
self._verify_signed(signed, candidate, plan, after_now)
if existing is None:
self.service.submit_owner_approval(signed.to_dict())
else:
self.service.renew_owner_approval(candidate_id, signed.to_dict(), expected_fingerprint=existing.fingerprint)
persisted = self.service.approvals.load(candidate_id)
if persisted.fingerprint != signed.fingerprint:
raise StandingAuthorityError("APPROVAL_PERSISTENCE_MISMATCH", "persisted approval differs from signed approval")
return self._result("APPROVAL_ISSUED", self._context(authorization, ExactScopeState.IN_SCOPE), persisted.to_dict())
except StandingAuthorityError as exc:
authorization = locals().get("authorization")
closed_states = {
"AUTHORITY_ACCEPTED_AND_CLOSED": AuthorityLifecycleState.CLOSED,
"AUTHORITY_EXPIRED": AuthorityLifecycleState.CLOSED,
"AUTHORITY_REVOKED": AuthorityLifecycleState.REVOKED,
"AUTHORITY_CLOSED": AuthorityLifecycleState.CLOSED,
"AUTHORITY_CANCELLED": AuthorityLifecycleState.CANCELLED,
}
if exc.code in closed_states:
context = self._context(
authorization,
ExactScopeState.IN_SCOPE,
lifecycle_state=closed_states[exc.code],
)
return self._result("AUTHORIZATION_DENIED", context)
if exc.code in {
"AUTHORITY_UNKNOWN",
"AUTHORITY_SNAPSHOT_INVALID",
"AUTHORITY_CHANGED_DURING_SIGN",
}:
return self._result(
"WAITING_AUTHORITY",
self._context(
authorization,
ExactScopeState.UNKNOWN,
lifecycle_state=AuthorityLifecycleState.UNKNOWN,
),
)
if exc.code.startswith("AUTHORITY_") and exc.code not in {"AUTHORITY_SNAPSHOT_INVALID", "AUTHORITY_CHANGED_DURING_SIGN"}:
goal_change = {
"AUTHORITY_RELEASE_SCOPE_MISMATCH",
"AUTHORITY_PRODUCTION_SCOPE_MISMATCH",
"AUTHORITY_CAPABILITY_DENIED",
"AUTHORITY_LINEAGE_REQUIRED",
}
context = self._context(
authorization,
ExactScopeState.OUT_OF_SCOPE,
boundary=(
OwnerBoundaryReason.GOAL_CHANGE
if exc.code in goal_change
else OwnerBoundaryReason.TRUST_BOUNDARY_CHANGE
),
)
return self._result("AUTHORIZATION_DENIED", context)
context = self._context(authorization, ExactScopeState.UNKNOWN)
return self._result("WAITING_AUTHORITY", context)
except Exception:
return self._result("WAITING_AUTHORITY", self._context(None, ExactScopeState.UNKNOWN))
def _check_binding(self, approval, authorization, scope, required=None):
binding = getattr(approval, "authorization_binding", None)
if not isinstance(binding, Mapping) or set(binding) != _BINDING_KEYS:
raise StandingAuthorityError("AUTHORIZATION_BINDING_INVALID", "approval lacks an exact standing authorization binding")
if dict(binding) != self._binding(authorization, scope):
raise StandingAuthorityError("AUTHORIZATION_BINDING_STALE", "approval standing authorization binding is stale")
if approval.owner_identity != authorization.owner_identity or approval.trust_domain != authorization.trust_domain:
raise StandingAuthorityError("AUTHORIZATION_IDENTITY_MISMATCH", "approval owner or trust binding is stale")
if (
approval.release_plan_id != scope.release_plan_id
or approval.evidence_closure_oid != scope.evidence_closure_oid
or approval.owner_approval_record_oid != scope.owner_approval_record_oid
):
raise StandingAuthorityError(
"AUTHORIZATION_OPERATION_BINDING_MISMATCH",
"approval operation identity is stale",
)
if required is not None and tuple(sorted(approval.capabilities)) != tuple(required):
raise StandingAuthorityError(
"AUTHORIZATION_CAPABILITY_BINDING_MISMATCH",
"approval capability set does not match the exact plan scope",
)
def check_current(self, approval: SignedReleasePlanApproval, capability: str, operation: str) -> None:
if not isinstance(approval, SignedReleasePlanApproval):
raise StandingAuthorityError("APPROVAL_INVALID", "current authority guard requires a signed approval")
_text("operation", operation)
if capability not in approval.capabilities:
raise StandingAuthorityError("CAPABILITY_NOT_GRANTED", "approval does not grant the requested capability")
envelope = self.service.get_approval_envelope(approval.candidate_id)
candidate, plan, plan_scope, required = self._load_plan_truth(envelope)
now = _utc(self.clock())
authorization, scope = self._read(envelope)
self._assert_current(authorization, scope, envelope, plan_scope, required, now)
self._check_binding(approval, authorization, scope, required)
self._verify_signed(approval, candidate, plan, now)
@@ -0,0 +1,91 @@
"""Distinct filename/path contracts and no-follow writes at the filesystem edge."""
from __future__ import annotations
import os
import ipaddress
from pathlib import Path, PurePosixPath
import re
import secrets
import stat
def require_isolated_dev_profile(host: str) -> None:
if os.getenv("AIOS_GITOPS_DEV_ALLOW", "0") != "1":
return
try:
loopback = ipaddress.ip_address(host).is_loopback
except (TypeError, ValueError):
loopback = False
if os.getenv("AIOS_RUNTIME_PROFILE") != "isolated-staging" or not loopback:
raise RuntimeError("DEV_ALLOW requires isolated-staging and loopback")
def safe_segment(value: str, *, max_length: int = 160) -> str:
if (not isinstance(value, str) or not value or len(value) > max_length
or value in (".", "..") or value.endswith((".", " "))
or re.fullmatch(r"[A-Za-z0-9_@.-]+", value) is None
or value.split(".")[0].upper() in {"CON", "PRN", "AUX", "NUL", *[f"COM{i}" for i in range(1, 10)], *[f"LPT{i}" for i in range(1, 10)]}):
raise ValueError("invalid single path segment")
return value
def relative_parts(value: str) -> tuple[str, ...]:
if not isinstance(value, str) or "\\" in value or ":" in value or value.startswith("/"):
raise ValueError("invalid relative path")
parts = tuple(value.split("/"))
for part in parts:
safe_segment(part, max_length=255)
if parts[0] == ".git":
raise ValueError("Git metadata path forbidden")
return parts
def contained_path(root: Path, segment: str) -> Path:
safe_segment(segment)
root = Path(root)
if root.is_symlink():
raise ValueError("symlink root forbidden")
path = root / segment
if path.is_symlink() or not path.resolve().is_relative_to(root.resolve()):
raise ValueError("path escapes root")
return path
def write_text_no_follow(root: Path, relative: str, content: str) -> None:
"""Atomic POSIX write using directory descriptors; no lexical-only fallback."""
parts = relative_parts(relative)
if os.name != "posix" or not hasattr(os, "O_NOFOLLOW"):
raise OSError("secure directory-relative writes require POSIX no-follow support")
root = Path(root)
flags = os.O_RDONLY | os.O_DIRECTORY | os.O_NOFOLLOW
current = os.open(root, flags)
temporary = ".c6-" + secrets.token_hex(16)
try:
for part in parts[:-1]:
try:
os.mkdir(part, mode=0o750, dir_fd=current)
except FileExistsError:
pass
child = os.open(part, flags, dir_fd=current)
os.close(current)
current = child
try:
info = os.stat(parts[-1], dir_fd=current, follow_symlinks=False)
if not stat.S_ISREG(info.st_mode) or info.st_nlink != 1:
raise ValueError("nonregular or multiply-linked target forbidden")
except FileNotFoundError:
pass
fd = os.open(temporary, os.O_WRONLY | os.O_CREAT | os.O_EXCL | os.O_NOFOLLOW,
0o600, dir_fd=current)
try:
with os.fdopen(fd, "w", encoding="utf-8", newline="\n") as stream:
stream.write(content); stream.flush(); os.fsync(stream.fileno())
os.replace(temporary, parts[-1], src_dir_fd=current, dst_dir_fd=current)
os.fsync(current)
finally:
try:
os.unlink(temporary, dir_fd=current)
except FileNotFoundError:
pass
finally:
os.close(current)
+438 -36
View File
@@ -22,6 +22,16 @@ from cryptography.hazmat.primitives.asymmetric.ed25519 import (
)
from silent_release_controller import _sanitize, utc_now
from task_plane.governance_control_kernel import (
AutonomousDecisionEngine,
AuthorityDecisionContext,
AuthorityLifecycleState,
DecisionInput,
DecisionEffectIntent,
ExactScopeState,
NextAction,
OwnerBoundaryReason,
)
from silent_release_hardening import (
BuilderAttestation,
CapabilityAuthorizationError,
@@ -33,9 +43,60 @@ from silent_release_hardening import (
_canonical_json,
_durable_json,
_safe_child,
release_file_lock,
)
@dataclasses.dataclass(frozen=True)
class StandingAuthorizationResult:
"""Typed result from a current authority reader plus formal issuer.
This object is routing evidence, not an authorization token. A result that
carries an approval still has to pass ``SignedApprovalVerifier`` inside the
release service before the saga can advance.
"""
status: str
authority_context: AuthorityDecisionContext
approval: Mapping[str, Any] | None = None
effect_intent: DecisionEffectIntent = DecisionEffectIntent.SIDE_EFFECT
def __post_init__(self) -> None:
allowed = {
"APPROVAL_ISSUED",
"AUTHORIZATION_DENIED",
"OWNER_REQUIRED",
"WAITING_AUTHORITY",
}
if self.status not in allowed:
raise ValueError("invalid standing authorization status")
if not isinstance(self.authority_context, AuthorityDecisionContext):
raise ValueError("authority_context must be AuthorityDecisionContext")
if self.effect_intent is not DecisionEffectIntent.SIDE_EFFECT:
raise ValueError("standing authorization must be a SIDE_EFFECT decision")
if self.status == "APPROVAL_ISSUED":
if not isinstance(self.approval, Mapping):
raise ValueError("issued authorization requires a signed approval")
if (
self.authority_context.lifecycle_state
is not AuthorityLifecycleState.ACTIVE
or self.authority_context.scope_state is not ExactScopeState.IN_SCOPE
or self.authority_context.owner_boundary_reason is not None
):
raise ValueError("issued approval is inconsistent with authority context")
elif self.approval is not None:
raise ValueError("non-issued authorization cannot carry an approval")
if self.status == "OWNER_REQUIRED" and (
self.authority_context.owner_boundary_reason is None
):
raise ValueError("OWNER_REQUIRED needs an explicit boundary reason")
StandingAuthorizationBroker = Callable[
[Mapping[str, Any]], StandingAuthorizationResult | Mapping[str, Any]
]
class BuilderSigner(Protocol):
@property
def key_id(self) -> str:
@@ -262,38 +323,56 @@ class LeaderLock:
def __init__(self, path: Path):
self.path = Path(path)
self.handle = None
self._lock_context = None
def __enter__(self):
self.path.parent.mkdir(parents=True, exist_ok=True)
os.chmod(self.path.parent, 0o700)
self.handle = self.path.open("a+", encoding="utf-8")
os.chmod(self.path, 0o600)
self._lock_context = release_file_lock(self.path, blocking=False)
try:
import fcntl
fcntl.flock(
self.handle.fileno(), fcntl.LOCK_EX | fcntl.LOCK_NB
)
self.handle = self._lock_context.__enter__()
except BlockingIOError as exc:
self.handle.close()
self.handle = None
self._lock_context = None
raise RuntimeError("silent release leader is already active") from exc
return self
def __exit__(self, exc_type, exc, tb):
if self.handle is not None:
try:
import fcntl
fcntl.flock(self.handle.fileno(), fcntl.LOCK_UN)
self._lock_context.__exit__(exc_type, exc, tb)
finally:
self.handle.close()
self.handle = None
self._lock_context = None
class PersistentSilentReleaseController:
"""Idempotent main-event controller with durable startup/recovery reports."""
_UNKNOWN_EFFECT_ACTIONS = frozenset(
{
"reconcile_baseline_registry",
"reconcile_release",
"reconcile_tag",
"resume_rollback",
"resume_runtime_switch",
"verify_runtime",
}
)
_RECOVERY_SIDE_EFFECT_STATES = frozenset(
{
"APPROVED",
"PRE_SWITCH_REVALIDATING",
"SWITCH_PENDING",
"SWITCHED",
"RUNTIME_VERIFYING",
"RUNTIME_VERIFIED",
"TAG_PENDING",
"TAG_VERIFIED",
"BASELINE_REGISTRY_PENDING",
"ROLLBACK_PENDING",
"RECONCILIATION_REQUIRED",
}
)
def __init__(
self,
root: Path,
@@ -301,14 +380,42 @@ class PersistentSilentReleaseController:
*,
main_probe: Callable[[], str],
recovery_worker: ReleaseRecoveryWorker | None = None,
owner_approval_broker: StandingAuthorizationBroker | None = None,
decision_engine: AutonomousDecisionEngine | None = None,
):
self.root = Path(root)
self.service = service
self.main_probe = main_probe
self.recovery_worker = recovery_worker
self.owner_approval_broker = owner_approval_broker
self.decision_engine = decision_engine or AutonomousDecisionEngine()
self.leader_path = self.root / "controller.leader.lock"
self.status_path = self.root / "controller-status.json"
def run_once_safely(self) -> dict[str, Any]:
"""Keep read-only diagnostics available when a control tick fails.
A dependency failure never grants write permission or repairs a corrupt
journal. Only a later successful, fully checked tick may advance work.
Exception text can contain credentials, so persist its type only.
"""
try:
return self.run_once()
except Exception as exc:
result = {
"error": type(exc).__name__,
"execution_enabled": self.service.execution_enabled,
"shadow_mode": not self.service.execution_enabled,
# A failed production tick may have reached an adapter before
# losing its acknowledgement. Never assert zero effects here.
"side_effects_executed": (
None if self.service.execution_enabled else False
),
"status": "FAIL_CLOSED",
}
_durable_json(self.status_path, result)
return _sanitize(result)
def startup_verify(self) -> dict[str, Any]:
checked = 0
failed = 0
@@ -329,6 +436,196 @@ class PersistentSilentReleaseController:
_durable_json(self.status_path, status)
return status
@classmethod
def _unknown_effects_pending(cls, recovery: Mapping[str, Any]) -> bool:
return any(
isinstance(item, Mapping)
and item.get("recovery_action") in cls._UNKNOWN_EFFECT_ACTIONS
for item in recovery.get("sagas", ())
)
@classmethod
def _recovery_side_effect_candidates(
cls, recovery: Mapping[str, Any]
) -> tuple[str, ...]:
return tuple(
str(item["candidate_id"])
for item in recovery.get("sagas", ())
if isinstance(item, Mapping)
and item.get("state") in cls._RECOVERY_SIDE_EFFECT_STATES
and item.get("candidate_id")
)
@staticmethod
def _authority_context_payload(
context: AuthorityDecisionContext,
) -> dict[str, Any]:
return {
"lifecycle_state": context.lifecycle_state.value,
"scope_state": context.scope_state.value,
"owner_boundary_reason": (
context.owner_boundary_reason.value
if context.owner_boundary_reason is not None
else None
),
"evidence_refs": list(context.evidence_refs),
}
def _try_standing_authorization(self, candidate_id: str) -> dict[str, Any]:
try:
existing = self.service.approvals.load(candidate_id)
except FileNotFoundError:
existing = None
# Default-off compatibility: an already accepted exact approval may be
# revalidated, but absence of a current broker never derives new
# authority. When a broker is configured it must be consulted on every
# side-effect decision so a still-valid short approval cannot mask a
# revoked/closed root authorization or an exact-scope change.
if self.owner_approval_broker is None and existing is not None:
try:
accepted = self.service.submit_owner_approval(existing.to_dict())
except Exception:
accepted = None
else:
return {
"candidate_id": candidate_id,
"approval_fingerprint": accepted.get("fingerprint"),
"status": "APPROVED",
"idempotent": True,
}
if self.owner_approval_broker is None:
return {
"candidate_id": candidate_id,
"reason_code": "OWNER_APPROVAL_BACKEND_MISSING",
"status": "WAITING_AUTHORITY",
}
envelope = self.service.get_approval_envelope(candidate_id)
try:
issued = self.owner_approval_broker(dict(envelope))
except Exception as exc:
code = str(getattr(exc, "code", "") or type(exc).__name__)
return {
"candidate_id": candidate_id,
"reason_code": code,
"status": "WAITING_AUTHORITY",
}
authority_context = None
decision = None
if isinstance(issued, StandingAuthorizationResult):
authority_context = issued.authority_context
context_payload = self._authority_context_payload(authority_context)
decision = self.decision_engine.decide(
DecisionInput(
goal_id=candidate_id,
admission_ready=True,
effect_intent=DecisionEffectIntent.SIDE_EFFECT,
authority_context=authority_context,
evidence_refs=authority_context.evidence_refs,
)
)
decision_payload = decision.to_dict()
if decision.next_valid_action is NextAction.DENY_EFFECT:
return {
"candidate_id": candidate_id,
"authority_context": context_payload,
"decision": decision_payload,
"reason_code": decision.blocker_code
or "STANDING_AUTHORIZATION_DENIED",
"status": "AUTHORIZATION_DENIED",
}
if decision.next_valid_action is NextAction.OWNER_BOUNDARY:
boundary_reason = decision.owner_boundary_reason
if boundary_reason is None:
return {
"candidate_id": candidate_id,
"authority_context": context_payload,
"decision": decision_payload,
"reason_code": "OWNER_BOUNDARY_REASON_MISSING",
"status": "WAITING_AUTHORITY",
}
return {
"candidate_id": candidate_id,
"authority_context": context_payload,
"decision": decision_payload,
"reason_code": boundary_reason.value,
"status": "OWNER_REQUIRED",
}
if decision.next_valid_action is not NextAction.CREATE_OR_RESUME_NEXT_CHILD:
return {
"candidate_id": candidate_id,
"authority_context": context_payload,
"decision": decision_payload,
"reason_code": decision.blocker_code
or decision.next_valid_action.value,
"status": "WAITING_AUTHORITY",
}
if issued.status != "APPROVAL_ISSUED":
status = (
"AUTHORIZATION_DENIED"
if issued.status == "AUTHORIZATION_DENIED"
else "WAITING_AUTHORITY"
)
return {
"candidate_id": candidate_id,
"authority_context": context_payload,
"decision": decision_payload,
"reason_code": issued.status,
"status": status,
}
approval = dict(issued.approval or {})
elif isinstance(issued, Mapping):
# Backward-compatible C5 callback contract. Mapping coverage does
# not grant authority; submit_owner_approval verifies the signed
# object against the current exact candidate and ReleasePlan.
approval = dict(issued)
else:
return {
"candidate_id": candidate_id,
"reason_code": "OWNER_APPROVAL_MISSING",
"status": "WAITING_AUTHORITY",
}
try:
accepted = self.service.submit_owner_approval(approval)
except Exception as exc:
code = str(getattr(exc, "code", "") or type(exc).__name__)
result = {
"candidate_id": candidate_id,
"reason_code": code,
"status": "WAITING_AUTHORITY",
}
if authority_context is not None:
result["authority_context"] = self._authority_context_payload(
authority_context
)
if decision is not None:
result["decision"] = decision.to_dict()
return result
if accepted.get("status") not in {"VERIFIED_AND_STORED", "IDEMPOTENT"}:
return {
"candidate_id": candidate_id,
"reason_code": "APPROVAL_NOT_VERIFIED",
"status": "WAITING_AUTHORITY",
}
result = {
"candidate_id": candidate_id,
"approval_fingerprint": accepted.get("fingerprint"),
"idempotent": accepted.get("status") == "IDEMPOTENT",
"saga_state": accepted.get("saga_state"),
"status": "APPROVED",
}
if authority_context is not None:
result["authority_context"] = self._authority_context_payload(
authority_context
)
if decision is not None:
result["decision"] = decision.to_dict()
return result
def run_once(self) -> dict[str, Any]:
with LeaderLock(self.leader_path):
startup = self.startup_verify()
@@ -345,26 +642,98 @@ class PersistentSilentReleaseController:
}
_durable_json(self.status_path, result)
return _sanitize(result)
# ReleaseRecoveryWorker can resume an externally visible effect.
# Re-resolve current root lifecycle and exact scope for every such
# candidate before invoking it; executor/verifier checks remain the
# final fence. This also prevents a cached short approval from
# bypassing root revocation during restart recovery.
recovery_status = self.service.get_recovery_status()
unknown_effect = self._unknown_effects_pending(recovery_status)
recovery_authority_decisions: list[dict[str, Any]] = []
recovery_candidates = (
self._recovery_side_effect_candidates(recovery_status)
if self.recovery_worker is not None
else ()
)
for candidate_id in recovery_candidates:
authorization = self._try_standing_authorization(candidate_id)
recovery_authority_decisions.append(authorization)
if authorization.get("status") != "APPROVED":
notification = None
if (
not unknown_effect
and authorization.get("status") == "OWNER_REQUIRED"
):
notification = self.service.ensure_owner_notification(
candidate_id,
reason_code=str(authorization["reason_code"]),
)
result = {
"authority_decision": authorization,
"execution_enabled": self.service.execution_enabled,
"intent": None,
"notification": notification,
"recovery": recovery_status,
"recovery_authority_decisions": (
recovery_authority_decisions
),
"shadow_mode": not self.service.execution_enabled,
"side_effects_executed": False,
"startup": startup,
"status": (
"RECONCILIATION_REQUIRED"
if unknown_effect
else authorization.get("status")
),
}
_durable_json(self.status_path, result)
return _sanitize(result)
recovery = (
self.recovery_worker.run_once()
if self.recovery_worker is not None
else recovery_status
)
recovery_status = self.service.get_recovery_status()
if self._unknown_effects_pending(recovery_status):
result = {
"authority_decision": None,
"execution_enabled": self.service.execution_enabled,
"intent": None,
"notification": None,
"recovery": recovery,
"recovery_authority_decisions": (
recovery_authority_decisions
),
"shadow_mode": not self.service.execution_enabled,
"side_effects_executed": False,
"startup": startup,
"status": "RECONCILIATION_REQUIRED",
}
_durable_json(self.status_path, result)
return _sanitize(result)
source_commit = str(self.main_probe())
event_id = f"main-{source_commit}"
intent = self.service.create_release_intent(
{"event_id": event_id, "source_commit": source_commit}
)
notification = None
authorization = None
if intent.get("candidate_id"):
notification = self.service.ensure_owner_notification(
str(intent["candidate_id"])
)
recovery = (
self.recovery_worker.run_once()
if self.recovery_worker is not None
else self.service.get_recovery_status()
)
candidate_id = str(intent["candidate_id"])
authorization = self._try_standing_authorization(candidate_id)
if authorization.get("status") == "OWNER_REQUIRED":
notification = self.service.ensure_owner_notification(
candidate_id,
reason_code=str(authorization["reason_code"]),
)
result = {
"authority_decision": authorization,
"execution_enabled": self.service.execution_enabled,
"intent": intent,
"notification": notification,
"recovery": recovery,
"recovery_authority_decisions": recovery_authority_decisions,
"shadow_mode": not self.service.execution_enabled,
"side_effects_executed": False,
"startup": startup,
@@ -373,6 +742,51 @@ class PersistentSilentReleaseController:
return _sanitize(result)
def compose_standing_release_controller(
root: Path,
service: SilentReleaseActionService,
*,
main_probe: Callable[[], str],
authority_reader: Callable[[], Any],
execution_scope_reader: Callable[[Mapping[str, Any]], Any],
signer: Any,
recovery_worker: ReleaseRecoveryWorker | None = None,
execution_ttl_seconds: int = 1200,
renewal_margin_seconds: int = 120,
clock: Callable[[], Any] | None = None,
) -> PersistentSilentReleaseController:
"""Compose trusted host adapters into the existing controller and effects.
Readers must return the validated snapshot types defined by
``release_standing_authority``. The signer must already belong to the
service's release approval trust domain. This function neither registers
keys nor performs network/authority reads during cold startup.
"""
from release_standing_authority import StandingReleaseAuthorizationBroker
options = {
"execution_ttl_seconds": execution_ttl_seconds,
"renewal_margin_seconds": renewal_margin_seconds,
}
if clock is not None:
options["clock"] = clock
broker = StandingReleaseAuthorizationBroker(
service,
authority_reader,
execution_scope_reader,
signer,
**options,
)
service.install_current_authority_guard(broker.check_current)
return PersistentSilentReleaseController(
root,
service,
main_probe=main_probe,
recovery_worker=recovery_worker,
owner_approval_broker=broker,
)
class ShadowActionHandler(BaseHTTPRequestHandler):
service: SilentReleaseActionService
controller: PersistentSilentReleaseController
@@ -485,24 +899,12 @@ def serve_shadow(
service,
main_probe=git_main_probe(repository),
)
controller.run_once()
controller.run_once_safely()
stop = threading.Event()
def controller_loop() -> None:
while not stop.wait(poll_seconds):
try:
controller.run_once()
except Exception as exc: # fail closed and keep the API available
_durable_json(
controller.status_path,
{
"error": type(exc).__name__,
"execution_enabled": False,
"shadow_mode": True,
"side_effects_executed": False,
"status": "FAIL_CLOSED",
},
)
controller.run_once_safely()
handler = type(
"ConfiguredShadowActionHandler",
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,271 @@
"""Exact source-only admission composition within the existing Bridge.
This module neither installs itself nor issues authority. A host activation must
pin an Owner-signed policy and an independently verified epoch. The existing
Task admission store, V3 producer, Task Plane and principal verifier are reused.
"""
from __future__ import annotations
import hashlib
import base64
import json
from pathlib import Path
from typing import Any, Mapping
from aios_single_host_authority import (
SignedArtifact, canonical_json_bytes, _open_secure,
)
from aios_task_admission_bootstrap import TaskAdmissionError
from aios_task_admission_bridge_contract import TaskAdmissionBridgeContract
from aios_task_admission_live_adapters import TaskAdmissionRuntimeBundle
from governed_source_publication_policy import (
ScopedSourcePublicationPolicyAdmission, SourcePublicationPolicyVerifier,
)
def _read_object(path: Path) -> tuple[bytes, dict[str, Any]]:
raw = _open_secure(path, private=False, require_root_owner=True, maximum=1024 * 1024)
value = json.loads(raw)
if not isinstance(value, dict):
raise ValueError("SOURCE_ACTIVATION_OBJECT_REQUIRED")
return raw, value
class ScopedPrincipalProvider:
"""Policy revocation also fences principals from already admitted Tasks."""
def __init__(self, guarded: ScopedSourcePublicationPolicyAdmission, delegate: Any):
self.guarded = guarded
self.delegate = delegate
def readiness(self) -> Mapping[str, Any]:
try:
self.guarded.verifier.require(self.guarded.artifact, self.guarded.request)
return self.delegate.readiness()
except Exception as exc:
return {"ready": False, "mode": "source-publication-only", "error": str(exc)}
def __call__(self, transport: Mapping[str, Any], request: Mapping[str, Any]) -> Any:
grant = self.delegate.store.get_grant_by_task_id(str(request.get("task_id") or ""))
if grant is None or grant.parent_epoch_id != self.delegate.epoch_id:
raise TaskAdmissionError("SOURCE_PRINCIPAL_GRANT_REQUIRED", 403)
# status rechecks the signed policy, exact durable envelope and original
# signed epoch. The delegate then verifies the live TaskLease/state.
self.guarded.status({"envelope_id": grant.envelope_id,
"delegation_epoch_id": grant.parent_epoch_id})
return self.delegate(transport, request)
class _AdmissionContext:
"""Metadata only for legacy hosts retaining their signed principal verifier."""
def __init__(self, bundle):
self.store, self.authorization = bundle.store, bundle.authorization
self.task_port, self.epoch_id = bundle.task_port, bundle.epoch_id
def readiness(self):
return {"ready": False, "error": "REGISTERED_SIGNED_PRINCIPAL_VERIFIER_REQUIRED"}
def __call__(self, *_args, **_kwargs):
raise TaskAdmissionError("REGISTERED_SIGNED_PRINCIPAL_VERIFIER_REQUIRED", 403)
def guard_source_runtime(
runtime: TaskAdmissionRuntimeBundle, *, artifact: SignedArtifact,
verifier: SourcePublicationPolicyVerifier, expected_agent_id: str,
) -> TaskAdmissionRuntimeBundle:
service = runtime.service
if service is None:
raise ValueError("SOURCE_ACTIVATION_BOOTSTRAP_REQUIRED")
guarded = ScopedSourcePublicationPolicyAdmission(
verifier, bootstrap=service, artifact=artifact,
expected_agent_id=expected_agent_id,
)
# Build the typed request through the same exact-envelope guard used by the
# HTTP contract, instead of accepting a caller-supplied request projection.
payload = artifact.payload
request = guarded._request_for_raw({
"repository": payload["repository"],
"authority_base_oid": payload["authority_base_oid"],
"target_branch": payload["target_branch"], "generation": payload["generation"],
"execution_digest": payload["execution_digest"], "risk": payload["risk"],
"file_allowlist": payload["exact_files"], "requested_capabilities": payload["capabilities"],
})
guarded.request = request
verifier.require(artifact, request)
principal = runtime.principal_verifier or _AdmissionContext(runtime)
epoch = principal.authorization.verify_delegation_epoch(principal.epoch_id)
# The complete per-envelope check runs again on authorization and every
# principal use; reject a production-capable epoch already at composition.
if (getattr(epoch, "production_scope", None) != "none"
or getattr(epoch, "destructive_policy", None) != "forbidden"):
raise TaskAdmissionError("SOURCE_ACTIVATION_EPOCH_SCOPE_DENIED", 403)
return TaskAdmissionRuntimeBundle(
handlers=TaskAdmissionBridgeContract(guarded).handlers(),
principal_verifier=ScopedPrincipalProvider(guarded, principal),
service=guarded,
)
def build_source_runtime_from_activation_file(path: Path, *, post_admission: Any,
ordinary_activation_file: Path):
from aios_task_admission_live_adapters import (
build_task_admission_runtime_from_activation_file,
load_owner_approval_verifier,
)
_, config = _read_object(path)
if set(config) != {"schema", "policy_artifact", "policy_sha256", "policy_revocations",
"admission_activation", "expected_agent_id"} or config["schema"] != "aios.source-publication-host-activation.v1":
raise ValueError("SOURCE_ACTIVATION_FIELDS_INVALID")
for key in ("policy_artifact", "policy_revocations", "admission_activation"):
value = config[key]
if not isinstance(value, str) or not value.startswith("/") or ".." in Path(value).parts:
raise ValueError("SOURCE_ACTIVATION_PATH_INVALID")
raw, signed = _read_object(Path(config["policy_artifact"]))
if hashlib.sha256(raw).hexdigest() != config["policy_sha256"]:
raise ValueError("SOURCE_ACTIVATION_POLICY_HASH_MISMATCH")
if set(signed) != {"payload", "signature", "key_id", "namespace"}:
raise ValueError("SOURCE_ACTIVATION_ARTIFACT_INVALID")
artifact = SignedArtifact(canonical_payload=canonical_json_bytes(signed["payload"]), **signed)
_, ordinary_config = _read_object(ordinary_activation_file)
_, source_config = _read_object(Path(config["admission_activation"]))
for field in ("state_db", "task_api_url", "task_control_token_file", "owner_verifier_module"):
if not ordinary_config.get(field) or source_config.get(field) != ordinary_config[field]:
raise ValueError("SOURCE_ACTIVATION_CONTROL_PLANE_BINDING_MISMATCH")
def not_revoked(payload: Mapping[str, Any]) -> bool:
_, value = _read_object(Path(config["policy_revocations"]))
if (set(value) != {"schema", "revoked_nonces"}
or value["schema"] != "aios.source-publication-revocations.v1"
or not isinstance(value["revoked_nonces"], list)
or any(not isinstance(nonce, str) or not nonce for nonce in value["revoked_nonces"])):
return False
return payload["nonce"] not in value["revoked_nonces"]
runtime = build_task_admission_runtime_from_activation_file(
Path(config["admission_activation"]), post_admission=post_admission)
verifier = SourcePublicationPolicyVerifier(
load_owner_approval_verifier(Path(source_config["owner_verifier_module"])),
revocation_check=not_revoked,
expected_policy_digest=runtime.service.policy.normalized_policy_digest(),
)
guarded_runtime = guard_source_runtime(runtime, artifact=artifact, verifier=verifier,
expected_agent_id=config["expected_agent_id"])
payload = artifact.payload
plan_path = Path(source_config["execution_plan_dir"]) / (payload["execution_digest"] + ".json")
plan_bytes, plan = _read_object(plan_path)
if (hashlib.sha256(plan_bytes).hexdigest() != payload["execution_digest"]
or canonical_json_bytes(plan) != plan_bytes):
raise ValueError("SOURCE_ACTIVATION_PLAN_DIGEST_MISMATCH")
hashes = plan.get("file_sha256")
if not isinstance(hashes, dict) or set(hashes) != set(payload["exact_files"]):
raise ValueError("SOURCE_ACTIVATION_FILE_HASH_SET_MISMATCH")
for field in ("repository", "authority_base_oid", "target_branch", "generation", "risk"):
if plan.get(field) != payload[field]:
raise ValueError("SOURCE_ACTIVATION_PLAN_SCOPE_MISMATCH")
if sorted(plan.get("capabilities", [])) != sorted(payload["capabilities"]):
raise ValueError("SOURCE_ACTIVATION_PLAN_CAPABILITIES_MISMATCH")
if not isinstance(plan.get("task_id"), str) or not plan["task_id"]:
raise ValueError("SOURCE_ACTIVATION_PLAN_TASK_REQUIRED")
guarded_runtime.service.expected_file_sha256 = dict(hashes)
guarded_runtime.service.expected_task_id = plan["task_id"]
return guarded_runtime
class AdmissionRuntimeRouter:
"""Route an exact branch/epoch without widening the ordinary policy."""
def __init__(self, ordinary: TaskAdmissionRuntimeBundle, source: TaskAdmissionRuntimeBundle):
self.ordinary = ordinary
self.source = source
self.guard = source.service
self.store = self.guard.bootstrap.store
def _source_request(self, operation: str, request: Mapping[str, Any]) -> bool:
if operation == "getTaskAdmissionEnvelope":
return request.get("target_branch") == self.guard.artifact.payload["target_branch"]
envelope_id = str(request.get("envelope_id") or "")
row = self.store.get_envelope(envelope_id)
if row is None:
# Never fall through after finding a source envelope, including
# when it has expired or been revoked.
return False
envelope = self.guard.bootstrap._load_envelope(envelope_id)
return envelope.target_branch == self.guard.artifact.payload["target_branch"]
def handlers(self) -> dict[str, Any]:
def dispatch(operation: str, request: Mapping[str, Any]):
runtime = self.source if self._source_request(operation, request) else self.ordinary
return runtime.handlers[operation](request)
return {op: (lambda request, op=op: dispatch(op, request)) for op in self.source.handlers}
def readiness(self) -> Mapping[str, Any]:
return {"mode": "scoped-admission-router", "ordinary": self.ordinary.principal_verifier.readiness(),
"source_publication": self.source.principal_verifier.readiness()}
def __call__(self, transport: Mapping[str, Any], request: Mapping[str, Any]):
grant = self.store.get_grant_by_task_id(str(request.get("task_id") or ""))
provider = (self.source.principal_verifier if grant is not None
and grant.target_branch == self.guard.artifact.payload["target_branch"]
else self.ordinary.principal_verifier)
return provider(transport, request)
def bundle(self) -> TaskAdmissionRuntimeBundle:
return TaskAdmissionRuntimeBundle(handlers=self.handlers(), principal_verifier=self)
class ExistingPrincipalPolicyFence:
"""Add a C5 policy fence while retaining the registered signed verifier.
Layered staging hosts use a signed TaskPrincipal verifier with their own
storage scope. They must not be migrated to a server attestation merely to
add source admission. Every request still reaches the original verifier.
"""
def __init__(self, delegate: Any, source_runtime: TaskAdmissionRuntimeBundle):
self.delegate = delegate
self.guard = source_runtime.service
def __getattr__(self, name: str):
return getattr(self.delegate, name)
def __call__(self, transport: Mapping[str, Any], request: Mapping[str, Any]):
grant = self.guard.bootstrap.store.get_grant_by_task_id(str(request.get("task_id") or ""))
if grant is not None and grant.target_branch == self.guard.artifact.payload["target_branch"]:
self.guard.status({"envelope_id": grant.envelope_id,
"delegation_epoch_id": grant.parent_epoch_id})
if transport.get("operation") in {"commitFilesGoverned", "commitFileGoverned"}:
expected = getattr(self.guard, "expected_file_sha256", None)
if not isinstance(expected, dict) or not expected:
raise TaskAdmissionError("SOURCE_COMMIT_FILE_HASHES_REQUIRED", 403)
try:
raw = request.get("files")
if raw is None:
raw = {request["path"]: request["content_base64"]}
pairs = list(raw.items()) if isinstance(raw, dict) else [(item["path"], item["content_base64"]) for item in raw]
if len({path for path, _ in pairs}) != len(pairs):
raise ValueError("duplicate path")
actual = {path: hashlib.sha256(base64.b64decode(data, validate=True)).hexdigest()
for path, data in pairs}
except Exception as exc:
raise TaskAdmissionError("SOURCE_COMMIT_FILE_PAYLOAD_INVALID", 403) from exc
if actual != expected:
raise TaskAdmissionError("SOURCE_COMMIT_CONTENT_SCOPE_MISMATCH", 403)
return self.delegate(transport, request)
def expired_admission_runtime() -> TaskAdmissionRuntimeBundle:
"""Keep a verified-expired ordinary epoch denied while serving other routes."""
def deny(*_args: Any, **_kwargs: Any):
raise TaskAdmissionError("TASK_ADMISSION_EPOCH_EXPIRED", 403)
class ClosedPrincipal:
__call__ = staticmethod(deny)
def readiness(self):
return {"ready": False, "error": "TASK_ADMISSION_EPOCH_EXPIRED"}
from aios_task_admission_bridge_contract import TASK_ADMISSION_OPERATION_REQUIRED_FIELDS
return TaskAdmissionRuntimeBundle(
handlers={op: deny for op in TASK_ADMISSION_OPERATION_REQUIRED_FIELDS},
principal_verifier=ClosedPrincipal(),
)
+271 -9
View File
@@ -1,20 +1,257 @@
"""Task Center API."""
"""Authenticated read-only Task Center projections and event transport."""
from __future__ import annotations
import json
from collections.abc import Callable, Mapping
from typing import Any
try:
from fastapi import APIRouter
except Exception:
APIRouter = None
from fastapi import APIRouter, HTTPException, Request
from fastapi.responses import StreamingResponse
except Exception: # pragma: no cover - importable without FastAPI for model tests
APIRouter = HTTPException = Request = StreamingResponse = None
from .task_center import TaskCenter
from .task_dashboard_view import ProjectionCursorExpired, ProjectionCursorTransportLimit, TaskDashboardView
from .task_plane.errors import ForbiddenError, UnauthorizedError
class ProjectionUnavailable(RuntimeError):
"""The host did not install the source and identity bindings required to read."""
task_center = TaskCenter()
dashboard = TaskDashboardView()
_projection_reader: Callable[[Mapping[str, Any]], Mapping[str, Any]] | None = None
_principal_resolver: Callable[[Any], Mapping[str, Any] | None] | None = None
def get_task_overview(tasks: list[dict[str, Any]] | None = None):
return task_center.summarize(tasks)
def set_task_projection_reader(reader: Callable[[Mapping[str, Any]], Mapping[str, Any]] | None) -> None:
"""Install a host-owned reader that returns already-authorized task/event data."""
global _projection_reader
_projection_reader = reader
def set_task_projection_principal_resolver(
resolver: Callable[[Any], Mapping[str, Any] | None] | None,
) -> None:
"""Install the host's authenticated-principal resolver for all projection reads."""
global _principal_resolver
_principal_resolver = resolver
def get_task_overview(
tasks: list[dict[str, Any]] | None = None,
events_by_task: Mapping[str, list[dict[str, Any]]] | None = None,
) -> dict[str, Any]:
"""Project provided durable records; route callers use ``_read_projection``."""
summary = task_center.summarize(tasks)
projection = dashboard.build(tasks, events_by_task)
return {
**summary,
"tasks": projection["tasks"],
"views": projection["views"],
"snapshot_cursor": dashboard.snapshot_cursor(tasks, events_by_task),
}
def get_task_events(
cursor: str | None,
*,
principal: Mapping[str, Any],
limit: int = 100,
) -> dict[str, Any]:
source = _read_projection(principal)
feed = dashboard.event_feed(source["tasks"], source["events_by_task"], cursor, limit=limit)
cards = {card["task_id"]: card for card in dashboard.build(source["tasks"], source["events_by_task"])["tasks"]}
for event in feed["events"]:
card = cards.get(event["entity_id"])
if card is None:
continue
# Event sequence and task revision have different authority domains.
# The client fences entity snapshots by task revision, while resumable
# transport remains governed by the per-task append sequence.
event["event_sequence"] = event["sequence"]
event["event_revision"] = event.get("revision")
event["task_revision"] = card.get("revision")
event["revision"] = card.get("revision")
event["task"] = card
event["related_tasks"] = _ancestor_cards(card, cards)
return feed
def _ancestor_cards(card: Mapping[str, Any], cards: Mapping[str, dict[str, Any]]) -> list[dict[str, Any]]:
related: list[dict[str, Any]] = []
parent_id = card.get("lineage", {}).get("parent_task_id") if isinstance(card.get("lineage"), Mapping) else None
visited: set[str] = set()
while isinstance(parent_id, str) and parent_id and parent_id not in visited:
visited.add(parent_id)
parent = cards.get(parent_id)
if parent is None:
break
related.append(parent)
lineage = parent.get("lineage")
parent_id = lineage.get("parent_task_id") if isinstance(lineage, Mapping) else None
return related
def configure_task_plane_projection(service: Any, principal_resolver: Callable[[Any], Mapping[str, Any] | None]) -> None:
"""Bind this FastAPI router to an existing authorized ``TaskPlaneService``.
The resolver must return a host-authenticated principal containing the
server-derived ``task_plane_identity``. No browser-provided role or scope
participates in these reads.
"""
def reader(principal: Mapping[str, Any]) -> Mapping[str, Any]:
actor = principal.get("task_plane_identity")
if not isinstance(actor, str) or not actor:
raise PermissionError("task projection identity is missing")
return _read_task_plane_snapshot(service, actor)
set_task_projection_principal_resolver(principal_resolver)
set_task_projection_reader(reader)
_TASKPLANE_DISPLAY_STATUSES = {
"CREATED": "pending",
"QUEUED": "pending",
"CLAIMED": "running",
"RUNNING": "running",
"WAITING_CHILD": "running",
"VERIFYING": "running",
"COMPLETED": "completed",
"FAILED_RECOVERABLE": "blocked",
"FAILED_FINAL": "blocked",
"CANCELLED": "blocked",
"MANUAL_REVIEW_REQUIRED": "waiting_approval",
}
def _read_task_plane_snapshot(service: Any, actor: str) -> Mapping[str, Any]:
"""Read an authorized, bounded-consistency TaskPlane projection.
``list_tasks`` and ``get_events`` are separate authorized reads. We only
publish their result when a second task and event-position read confirms
that no visible task revision, membership, or per-task append position
moved while the first snapshot was collected. A busy plane is retried
once and then fails closed rather than emitting a mixed snapshot.
"""
for _attempt in range(2):
raw_tasks = _visible_tasks(service.list_tasks(actor))
task_ids = [task["task_id"] for task in raw_tasks]
events_by_task = {task_id: service.get_events(task_id, actor) for task_id in task_ids}
verification_events = {task_id: service.get_events(task_id, actor) for task_id in task_ids}
verified_tasks = _visible_tasks(service.list_tasks(actor))
if _task_revisions(raw_tasks) == _task_revisions(verified_tasks) and all(
_event_positions(events_by_task[task_id]) == _event_positions(verification_events[task_id])
for task_id in task_ids
):
return {"tasks": raw_tasks, "events_by_task": events_by_task}
raise ProjectionUnavailable("task projection changed during snapshot")
def _visible_tasks(records: Any) -> list[dict[str, Any]]:
if not isinstance(records, list):
raise ProjectionUnavailable("task projection source returned invalid tasks")
tasks: list[dict[str, Any]] = []
for raw in records:
if not isinstance(raw, Mapping) or not isinstance(raw.get("task_id"), str) or not raw["task_id"]:
raise ProjectionUnavailable("task projection source returned invalid task")
source_status = str(raw.get("status") or "").upper()
if source_status not in _TASKPLANE_DISPLAY_STATUSES:
raise ProjectionUnavailable("task projection source returned unknown TaskPlane status")
tasks.append({
**raw,
"source_status": source_status or None,
"status": _TASKPLANE_DISPLAY_STATUSES[source_status],
})
return tasks
def _task_revisions(tasks: list[Mapping[str, Any]]) -> tuple[tuple[str, Any], ...]:
return tuple(sorted((task["task_id"], task.get("revision")) for task in tasks))
def _event_positions(events: Any) -> tuple[tuple[str, int], ...]:
if not isinstance(events, list):
raise ProjectionUnavailable("task projection source returned invalid events")
positions: list[tuple[str, int]] = []
for event in events:
if not isinstance(event, Mapping) or not isinstance(event.get("event_id"), str):
raise ProjectionUnavailable("task projection source returned invalid event")
sequence = event.get("seq")
if not isinstance(sequence, int) or isinstance(sequence, bool) or sequence < 0:
raise ProjectionUnavailable("task projection event has no durable sequence")
positions.append((event["event_id"], sequence))
return tuple(positions)
def _read_projection(principal: Mapping[str, Any]) -> Mapping[str, Any]:
if _projection_reader is None:
raise ProjectionUnavailable("task projection source is not configured")
source = _projection_reader(principal)
if not isinstance(source, Mapping):
raise ProjectionUnavailable("task projection source returned invalid data")
tasks, events = source.get("tasks"), source.get("events_by_task")
if not isinstance(tasks, list) or not isinstance(events, Mapping):
raise ProjectionUnavailable("task projection source returned incomplete data")
return {"tasks": tasks, "events_by_task": events}
def _principal(request: Any) -> Mapping[str, Any]:
if _principal_resolver is None:
raise ProjectionUnavailable("task projection authentication is not configured")
principal = _principal_resolver(request)
if not isinstance(principal, Mapping):
raise PermissionError("task projection authentication failed")
return principal
def _sse(payload: Mapping[str, Any]):
# The bounded response is deliberately resumable. The browser reconnects
# with the returned cursor; polling remains available when it cannot.
for event in payload["events"]:
yield f"id: {event['cursor']}\ndata: {json.dumps(event, ensure_ascii=False, separators=(',', ':'))}\n\n"
yield ": end-of-batch\n\n"
def _http(call: Callable[[], Any]) -> Any:
try:
return call()
except ProjectionCursorTransportLimit as exc:
if HTTPException is None:
raise
raise HTTPException(status_code=413, detail="TASK_EVENT_CURSOR_TOO_LARGE") from exc
except ProjectionCursorExpired as exc:
if HTTPException is None:
raise
raise HTTPException(status_code=410, detail="EVENT_CURSOR_EXPIRED") from exc
except ProjectionUnavailable as exc:
if HTTPException is None:
raise
raise HTTPException(status_code=503, detail="TASK_PROJECTION_UNAVAILABLE") from exc
except UnauthorizedError as exc:
if HTTPException is None:
raise
raise HTTPException(status_code=401, detail="TASK_PROJECTION_UNAUTHORIZED") from exc
except ForbiddenError as exc:
if HTTPException is None:
raise
raise HTTPException(status_code=403, detail="TASK_PROJECTION_DENIED") from exc
except PermissionError as exc:
if HTTPException is None:
raise
raise HTTPException(status_code=403, detail="TASK_PROJECTION_DENIED") from exc
except ValueError as exc:
if HTTPException is None:
raise
raise HTTPException(status_code=400, detail=str(exc)) from exc
def _overview(principal: Mapping[str, Any]) -> dict[str, Any]:
source = _read_projection(principal)
return get_task_overview(source["tasks"], source["events_by_task"])
def build_router():
@@ -23,12 +260,37 @@ def build_router():
router = APIRouter(prefix="/admin/ai-evolution/tasks", tags=["task-center"])
@router.get("/overview", operation_id="getTaskCenterOverview")
def overview():
return get_task_overview()
def overview(request: Request):
return _http(lambda: _overview(_principal(request)))
@router.get("/events", operation_id="streamTaskCenterEvents")
def events(request: Request, cursor: str | None = None, after_cursor: str | None = None, limit: int = 100):
payload = _http(lambda: get_task_events(
_effective_cursor(request, cursor, after_cursor), principal=_principal(request), limit=limit,
))
return StreamingResponse(_sse(payload), media_type="text/event-stream")
@router.get("/events/poll", operation_id="pollTaskCenterEvents")
def poll(request: Request, cursor: str | None = None, after_cursor: str | None = None, limit: int = 100):
return _http(lambda: get_task_events(
_effective_cursor(request, cursor, after_cursor), principal=_principal(request), limit=limit,
))
return router
router = build_router()
__all__ = ["router", "get_task_overview"]
__all__ = [
"router", "get_task_overview", "get_task_events", "set_task_projection_reader",
"set_task_projection_principal_resolver", "configure_task_plane_projection",
"ProjectionUnavailable",
]
def _effective_cursor(request: Any, cursor: str | None, after_cursor: str | None) -> str | None:
header_cursor = request.headers.get("Last-Event-ID") if request is not None else None
supplied = [value for value in (cursor, after_cursor, header_cursor) if value]
if len(set(supplied)) > 1:
raise ValueError("conflicting event cursors")
return supplied[0] if supplied else None
+405 -48
View File
@@ -1,81 +1,438 @@
"""Task Center visualization model."""
"""Read-only Task Center projections built from durable task and event records."""
from __future__ import annotations
from typing import Any
import base64
import json
import zlib
from collections import defaultdict
from typing import Any, Mapping
class ProjectionCursorExpired(ValueError):
"""The requested event cursor no longer names a visible durable event."""
class ProjectionCursorTransportLimit(ValueError):
"""The authorized per-task watermark cannot fit a request/header cursor."""
_MAX_CURSOR_BYTES = 8192
_MAX_CURSOR_PAYLOAD_BYTES = 32768
_INITIAL_INTERACTION_KIND = "initial_request"
_ADDITIONAL_INTERACTION_KINDS = frozenset({"additional_interaction", "follow_up"})
class TaskDashboardView:
def build(self, tasks: list[dict[str, Any]] | None = None) -> dict[str, Any]:
tasks = tasks or []
"""Build the Task Center read model without maintaining another state store."""
def build(
self,
tasks: list[dict[str, Any]] | None = None,
events_by_task: Mapping[str, list[dict[str, Any]]] | None = None,
) -> dict[str, Any]:
raw_tasks = [dict(task) for task in tasks or [] if isinstance(task, Mapping)]
by_id = {
task_id: task for task in raw_tasks
if isinstance((task_id := task.get("task_id")), str) and task_id
}
children = self._children_by_parent(by_id)
cards = [
self._task_card(task, by_id, children, events_by_task)
for task in raw_tasks
]
return {
"module": "task-center",
"route": "/admin/ai-evolution/tasks",
"title": "AIOS Task Center",
"views": {
"kanban": self._kanban(tasks),
"timeline": self._timeline(tasks),
"flow": self._flow(tasks),
"statistics": self._statistics(tasks),
"kanban": self._kanban(cards),
"timeline": self._timeline(cards),
"flow": self._flow(cards),
"statistics": self._statistics(cards),
},
"tasks": [self._task_card(task) for task in tasks],
"tasks": cards,
}
def _task_card(self, task: dict[str, Any]) -> dict[str, Any]:
def event_feed(
self,
tasks: list[dict[str, Any]] | None,
events_by_task: Mapping[str, list[dict[str, Any]]] | None,
cursor: str | None = None,
*,
limit: int = 100,
) -> dict[str, Any]:
"""Return an authorization-filtered, resumable page of durable events."""
if not 1 <= limit <= 100:
raise ValueError("invalid event page limit")
rows = self._event_rows(tasks, events_by_task)
watermarks = self._decode_cursor(cursor) if cursor else {}
self._validate_watermarks(rows, watermarks)
pending = [
row for row in rows
if row["sequence"] > watermarks.get(row["entity_id"], -1)
]
page = pending[:limit]
next_watermarks = dict(watermarks)
emitted_events: list[dict[str, Any]] = []
for row in page:
next_watermarks[row["entity_id"]] = max(next_watermarks.get(row["entity_id"], -1), row["sequence"])
# ``Last-Event-ID`` holds the cursor from the final SSE frame. It
# must therefore include every preceding entity watermark, not just
# the frame's own task, or a reconnect would replay another task.
emitted_events.append({**row, "cursor": self._encode_cursor(next_watermarks)})
next_cursor = self._encode_cursor(next_watermarks) if next_watermarks else None
return {
"task_id": task.get("task_id"),
"title": task.get("title"),
"events": emitted_events,
"cursor": cursor,
"next_cursor": next_cursor,
"has_more": len(pending) > len(page),
}
def snapshot_cursor(
self,
tasks: list[dict[str, Any]] | None,
events_by_task: Mapping[str, list[dict[str, Any]]] | None,
) -> str | None:
rows = self._event_rows(tasks, events_by_task)
watermarks: dict[str, int] = {}
for row in rows:
watermarks[row["entity_id"]] = max(watermarks.get(row["entity_id"], -1), row["sequence"])
return self._encode_cursor(watermarks) if watermarks else None
def _task_card(
self,
task: dict[str, Any],
by_id: Mapping[str, dict[str, Any]],
children: Mapping[str, list[str]],
events_by_task: Mapping[str, list[dict[str, Any]]] | None,
) -> dict[str, Any]:
task_id = task.get("task_id")
progress = task.get("progress") if isinstance(task.get("progress"), Mapping) else {}
parent_task_id = task.get("parent_task_id")
event_result = self._project_events(
task_id,
events_by_task,
complete=task.get("interaction_history_coverage") == "complete",
)
direct_children = list(children.get(task_id, ())) if isinstance(task_id, str) else []
grandchildren = [grandchild for child in direct_children for grandchild in children.get(child, ())]
authorization = self._authorization(task)
return {
"task_id": task_id,
"title": task.get("title") or task.get("display_name"),
"root_task_id": task.get("root_task_id"),
"created_at": task.get("created_at"),
"updated_at": task.get("updated_at"),
"assigned_by": task.get("assigned_by"),
"executor": task.get("executor"),
"status": task.get("status"),
"progress": task.get("progress", {}),
"elapsed_seconds": task.get("elapsed_seconds", 0),
"remaining_steps": task.get("progress", {}).get("remaining_steps", []),
"source_status": task.get("source_status", task.get("status")),
"revision": task.get("revision"),
"progress": progress,
"elapsed_seconds": self._known_nonnegative_int(task.get("elapsed_seconds")),
"remaining_steps": progress.get("remaining_steps", []),
"notification": task.get("notification", {}),
"actions": [
"start",
"update_progress",
"block",
"request_approval",
"approve",
"reject",
"retry",
"complete",
"notify",
"close",
],
"lineage": {
"parent_task_id": parent_task_id if isinstance(parent_task_id, str) and parent_task_id in by_id else None,
"child_task_ids": direct_children,
"grandchild_task_ids": grandchildren,
},
"responsibility": {
"assigned_by": task.get("assigned_by"),
"executor": task.get("executor"),
"owner_principal_id": task.get("owner_principal_id"),
},
"authorization": authorization,
"actions": self._actions(task, authorization),
"interaction_history": event_result,
}
def _kanban(self, tasks):
@staticmethod
def _known_nonnegative_int(value: Any) -> int | None:
return value if isinstance(value, int) and not isinstance(value, bool) and value >= 0 else None
@staticmethod
def _children_by_parent(by_id: Mapping[str, dict[str, Any]]) -> dict[str, list[str]]:
result: dict[str, list[str]] = defaultdict(list)
for task_id, task in by_id.items():
parent_id = task.get("parent_task_id")
if isinstance(parent_id, str) and parent_id in by_id:
result[parent_id].append(task_id)
return {parent: sorted(child_ids) for parent, child_ids in result.items()}
def _project_events(
self,
task_id: Any,
events_by_task: Mapping[str, list[dict[str, Any]]] | None,
*,
complete: bool = False,
) -> dict[str, Any]:
if not isinstance(task_id, str) or events_by_task is None or task_id not in events_by_task:
return self._unknown_history()
raw_events = events_by_task[task_id]
if not isinstance(raw_events, list):
return self._unknown_history()
seen: dict[str, dict[str, Any]] = {}
semantic_ids: dict[tuple[str, str], dict[str, Any]] = {}
events: list[dict[str, Any]] = []
conflicts: list[dict[str, str]] = []
for raw in raw_events:
if not isinstance(raw, Mapping) or not isinstance(raw.get("event_id"), str):
conflicts.append({"kind": "invalid_event", "event_id": ""})
continue
event = dict(raw)
event_id = event["event_id"]
previous = seen.get(event_id)
if previous is not None:
if previous != event:
conflicts.append({"kind": "duplicate_event_id_conflict", "event_id": event_id})
continue
seen[event_id] = event
item = self._event_item(event)
if self._malformed_human_interaction(event):
conflicts.append({"kind": "invalid_human_interaction_evidence", "event_id": event_id})
if self._malformed_metric_evidence(event):
conflicts.append({"kind": "invalid_metric_evidence", "event_id": event_id})
semantic_id = item.get("semantic_id")
if semantic_id:
semantic_namespace = "human_interaction" if item["category"] in {"initial_request", "additional_interaction"} else item["category"]
previous_semantic = semantic_ids.get((semantic_namespace, semantic_id))
if previous_semantic is not None:
if self._semantic_content(previous_semantic) != self._semantic_content(item):
conflicts.append({"kind": "duplicate_semantic_id_conflict", "event_id": event_id})
continue
semantic_ids[(semantic_namespace, semantic_id)] = item
events.append(item)
events.sort(key=lambda event: (event.get("created_at") or "", event["event_id"]))
interactions = sum(event["category"] in {"initial_request", "additional_interaction"} for event in events)
is_exact = complete and not conflicts
return {
"history_status": "partial" if not complete else ("conflicted" if conflicts else "complete"),
"interaction_count": interactions if is_exact else None,
"initial_request_count": sum(event["category"] == "initial_request" for event in events) if is_exact else None,
"additional_interaction_count": sum(event["category"] == "additional_interaction" for event in events) if is_exact else None,
"decision_count": sum(event["category"] == "decision" for event in events) if is_exact else None,
"offered_choice_count": sum(event.get("choice_count", 0) for event in events if event["category"] == "offered_choice") if is_exact else None,
"selection_submission_count": sum(event["category"] == "selection_submission" for event in events) if is_exact else None,
"events": events,
"source_conflicts": conflicts,
}
@staticmethod
def _unknown_history() -> dict[str, Any]:
return {
"history_status": "partial",
"interaction_count": None,
"initial_request_count": None,
"additional_interaction_count": None,
"decision_count": None,
"offered_choice_count": None,
"selection_submission_count": None,
"events": [],
"source_conflicts": [],
}
@staticmethod
def _semantic_content(event: Mapping[str, Any]) -> dict[str, Any]:
return {
key: value for key, value in event.items()
if key not in {"event_id", "sequence", "revision", "created_at"}
}
@staticmethod
def _malformed_human_interaction(event: Mapping[str, Any]) -> bool:
data = event.get("data") if isinstance(event.get("data"), Mapping) else {}
source = str(data.get("source") or event.get("source") or "").upper()
if source != "HUMAN_INTERACTION":
return False
interaction_id = data.get("interaction_id")
kind = data.get("kind")
return not (
isinstance(interaction_id, str) and interaction_id
and isinstance(kind, str)
and (kind == _INITIAL_INTERACTION_KIND or kind in _ADDITIONAL_INTERACTION_KINDS)
)
@staticmethod
def _malformed_metric_evidence(event: Mapping[str, Any]) -> bool:
data = event.get("data") if isinstance(event.get("data"), Mapping) else {}
event_type = str(event.get("type") or "").upper()
if event_type in {"DECISION", "TASK_DECISION", "APPROVAL_DECIDED"}:
return not isinstance(data.get("decision_id"), str) or not data["decision_id"]
if event_type in {"CHOICE_OFFERED", "OPTIONS_OFFERED"}:
choice_count = data.get("choice_count")
return not (
isinstance(data.get("choice_offer_id"), str) and data["choice_offer_id"]
and isinstance(choice_count, int) and not isinstance(choice_count, bool) and choice_count >= 0
)
if event_type in {"CHOICE_SELECTED", "SELECTION_SUBMITTED"}:
return not isinstance(data.get("selection_submission_id"), str) or not data["selection_submission_id"]
return False
@staticmethod
def _event_item(event: Mapping[str, Any]) -> dict[str, Any]:
event_type = str(event.get("type") or "UNKNOWN")
data = event.get("data") if isinstance(event.get("data"), Mapping) else {}
normalized = event_type.upper()
source = str(data.get("source") or event.get("source") or "").upper()
interaction_id = data.get("interaction_id")
decision_id = data.get("decision_id")
selection_id = data.get("selection_submission_id")
kind = data.get("kind")
if source == "HUMAN_INTERACTION" and isinstance(interaction_id, str) and interaction_id and kind == _INITIAL_INTERACTION_KIND:
category = "initial_request"
semantic_id = interaction_id
elif source == "HUMAN_INTERACTION" and isinstance(interaction_id, str) and interaction_id and isinstance(kind, str) and kind in _ADDITIONAL_INTERACTION_KINDS:
category = "additional_interaction"
semantic_id = interaction_id
elif normalized in {"DECISION", "TASK_DECISION", "APPROVAL_DECIDED"} and isinstance(decision_id, str) and decision_id:
category = "decision"
semantic_id = decision_id
elif normalized in {"CHOICE_OFFERED", "OPTIONS_OFFERED"} and isinstance(data.get("choice_offer_id"), str) and isinstance(data.get("choice_count"), int) and not isinstance(data["choice_count"], bool) and data["choice_count"] >= 0:
category = "offered_choice"
semantic_id = data["choice_offer_id"]
elif normalized in {"CHOICE_SELECTED", "SELECTION_SUBMITTED"} and isinstance(selection_id, str) and selection_id:
category = "selection_submission"
semantic_id = selection_id
else:
category = "other"
semantic_id = None
return {
"event_id": event["event_id"],
"sequence": event.get("seq"),
"revision": event.get("revision", event.get("seq")),
"type": event_type,
"category": category,
"semantic_id": semantic_id,
"subject": event.get("subject"),
"created_at": event.get("created_at"),
"decision": data.get("decision") if category == "decision" else None,
"choice_id": data.get("choice_id") if category in {"offered_choice", "selection_submission"} else None,
"choice_count": data.get("choice_count", 0) if category == "offered_choice" else 0,
}
@staticmethod
def _authorization(task: Mapping[str, Any]) -> dict[str, Any]:
raw = task.get("authorization")
raw = raw if isinstance(raw, Mapping) else {}
capabilities = raw.get("capabilities", task.get("capabilities", []))
return {
"owner_gate_required": bool(task.get("owner_gate_required", raw.get("owner_gate_required", False))),
"owner_gate_status": task.get("owner_gate_status", raw.get("owner_gate_status")),
"decision": raw.get("decision"),
"capabilities": list(capabilities) if isinstance(capabilities, (list, tuple, set, frozenset)) else [],
}
@staticmethod
def _actions(task: Mapping[str, Any], authorization: Mapping[str, Any]) -> list[str]:
explicit = task.get("authorized_actions")
if not isinstance(explicit, (list, tuple, set, frozenset)):
return []
allowed = [action for action in explicit if isinstance(action, str)]
capabilities = set(authorization.get("capabilities", []))
decision = authorization.get("decision")
if "approve" in allowed and not (
{"approve", "decision.approve", "task.approve"} & capabilities
and isinstance(decision, Mapping)
and decision.get("status") in {"pending", "eligible"}
):
allowed.remove("approve")
return allowed
@staticmethod
def _kanban(tasks: list[dict[str, Any]]) -> dict[str, list[str]]:
columns = {"pending": [], "running": [], "blocked": [], "waiting_approval": [], "completed": []}
for task in tasks:
columns.setdefault(task.get("status", "pending"), []).append(task.get("task_id"))
return columns
def _timeline(self, tasks):
return [
{
"task_id": task.get("task_id"),
"updated_at": task.get("updated_at"),
"status": task.get("status"),
}
for task in tasks
]
@staticmethod
def _timeline(tasks: list[dict[str, Any]]) -> list[dict[str, Any]]:
return [{"task_id": task.get("task_id"), "status": task.get("status"), "revision": task.get("revision")} for task in tasks]
def _flow(self, tasks):
return {
"nodes": ["created", "assigned", "running", "blocked", "approval", "completed", "closed"],
"task_count": len(tasks),
}
@staticmethod
def _flow(tasks: list[dict[str, Any]]) -> dict[str, Any]:
return {"nodes": ["created", "assigned", "running", "blocked", "approval", "completed", "closed"], "task_count": len(tasks)}
def _statistics(self, tasks):
@staticmethod
def _statistics(tasks: list[dict[str, Any]]) -> dict[str, int]:
return {
"total": len(tasks),
"running": len([t for t in tasks if t.get("status") == "running"]),
"blocked": len([t for t in tasks if t.get("status") == "blocked"]),
"waiting_approval": len([t for t in tasks if t.get("status") == "waiting_approval"]),
"completed": len([t for t in tasks if t.get("status") == "completed"]),
"running": sum(task.get("status") == "running" for task in tasks),
"blocked": sum(task.get("status") == "blocked" for task in tasks),
"waiting_approval": sum(task.get("status") == "waiting_approval" for task in tasks),
"completed": sum(task.get("status") == "completed" for task in tasks),
}
def _event_rows(self, tasks, events_by_task) -> list[dict[str, Any]]:
visible_ids = {task.get("task_id") for task in tasks or [] if isinstance(task, Mapping)}
result: list[dict[str, Any]] = []
for task_id, events in (events_by_task or {}).items():
if task_id not in visible_ids or not isinstance(events, list):
continue
# The stream carries every durable append. Semantic deduplication
# belongs only to human-history metrics: a later conflicting event
# must reach the client so its current task card becomes conflicted.
for event in events:
if not isinstance(event, Mapping) or not isinstance(event.get("event_id"), str):
continue
sequence = event.get("seq")
if not isinstance(sequence, int) or isinstance(sequence, bool) or sequence < 0:
continue
result.append({"event_id": event["event_id"], "entity_id": task_id, "sequence": sequence, "revision": event.get("revision"), "type": event.get("type", "UNKNOWN"), "created_at": event.get("created_at")})
# A per-task append sequence is the cursor authority. Timestamp order
# can be displayed by clients, but cannot drive paging: a backdated
# seq=6 must never be emitted before seq=5 for the same task.
result.sort(key=lambda event: (event["sequence"], event["entity_id"], event["event_id"]))
return result
__all__ = ["TaskDashboardView"]
@staticmethod
def _encode_cursor(watermarks: Mapping[str, int]) -> str:
raw = json.dumps({"v": 2, "after": dict(watermarks)}, sort_keys=True, separators=(",", ":")).encode("utf-8")
if len(raw) > _MAX_CURSOR_PAYLOAD_BYTES:
raise ProjectionCursorTransportLimit("event cursor exceeds transport limit")
token = base64.urlsafe_b64encode(zlib.compress(raw, level=9)).decode("ascii").rstrip("=")
if len(token) > _MAX_CURSOR_BYTES:
raise ProjectionCursorTransportLimit("event cursor exceeds transport limit")
return token
@staticmethod
def _decode_cursor(cursor: str) -> dict[str, int]:
if not isinstance(cursor, str) or not cursor or len(cursor) > _MAX_CURSOR_BYTES:
raise ProjectionCursorExpired("event cursor expired")
try:
padded = cursor + "=" * (-len(cursor) % 4)
encoded = base64.urlsafe_b64decode(padded.encode("ascii"))
decompressor = zlib.decompressobj()
raw = decompressor.decompress(encoded, _MAX_CURSOR_PAYLOAD_BYTES + 1)
if len(raw) > _MAX_CURSOR_PAYLOAD_BYTES:
raise ValueError("cursor payload invalid")
raw += decompressor.flush(_MAX_CURSOR_PAYLOAD_BYTES + 1 - len(raw))
if len(raw) > _MAX_CURSOR_PAYLOAD_BYTES or decompressor.unconsumed_tail or not decompressor.eof:
raise ValueError("cursor payload invalid")
value = json.loads(raw)
except (UnicodeEncodeError, ValueError, zlib.error, json.JSONDecodeError) as exc:
raise ProjectionCursorExpired("event cursor expired") from exc
after = value.get("after") if isinstance(value, Mapping) and value.get("v") == 2 else None
if not isinstance(after, Mapping) or not after or any(
not isinstance(task_id, str) or not task_id or not isinstance(sequence, int)
or isinstance(sequence, bool) or sequence < 0
for task_id, sequence in after.items()
):
raise ProjectionCursorExpired("event cursor expired")
return dict(after)
@staticmethod
def _validate_watermarks(rows: list[dict[str, Any]], watermarks: Mapping[str, int]) -> None:
positions_by_task: dict[str, set[int]] = defaultdict(set)
for row in rows:
positions_by_task[row["entity_id"]].add(row["sequence"])
# A retained sequence floor is not otherwise exposed by TaskPlane.
# Requiring the acknowledged position itself prevents silently skipping
# purged seqs 4..9 when a stale cursor still says seq=3.
if any(sequence not in positions_by_task.get(task_id, set()) for task_id, sequence in watermarks.items()):
raise ProjectionCursorExpired("event cursor expired")
__all__ = ["ProjectionCursorExpired", "ProjectionCursorTransportLimit", "TaskDashboardView"]
+66 -15
View File
@@ -35,7 +35,7 @@ from .models import (
contains_secret_text,
validate_identifier,
)
from .policy import PolicyEngine
from .policy import IdentityVerifier, PolicyEngine
from .root_action_consumer import (
RootActionConsumer,
RootActionExecutor,
@@ -71,7 +71,9 @@ class TaskPlaneService:
root_action_executor: RootActionExecutor | None = None,
):
self.store = store
self.c6_runtime = None
self.policy = policy or PolicyEngine(
identity_verifier=IdentityVerifier({"anon"}),
execution_enabled=False,
identity_capabilities={
"anon": {"task.creator", "task.read", "task.audit"}
@@ -117,6 +119,9 @@ class TaskPlaneService:
else None
)
self.store.probe_attestation_verifier = self.policy.probe_attestation_verifier
self.store.configure_evidence_attestation_verifier(
self.policy.evidence_attestation_verifier
)
def consume_one_root_action(
self,
@@ -412,6 +417,11 @@ class TaskPlaneService:
lease_seconds = _bounded_int(
body.get("lease_seconds", 30), "lease_seconds", 1, 3600
)
if self.c6_runtime is not None and self.c6_runtime.bindings.enabled:
return self.c6_runtime.claim_task(
task_id=task_id, worker_id=worker_id, actor_identity=actor,
lease_seconds=lease_seconds,
).to_dict()
claim = self.lease.claim(
task_id=task_id,
worker_id=worker_id,
@@ -436,6 +446,10 @@ class TaskPlaneService:
lease_seconds = _bounded_int(
body.get("lease_seconds", 30), "lease_seconds", 1, 3600
)
if self.c6_runtime is not None and self.c6_runtime.bindings.enabled:
return self.c6_runtime.renew_claim(task_id=task_id, claim_id=claim_id,
actor_identity=actor, receipt=body.get("claim_receipt"),
lease_seconds=lease_seconds)
claim = self.lease.heartbeat(task_id, claim_id, lease_seconds)
return {"claim_id": claim_id, "lease_until": claim.lease_until}
@@ -538,16 +552,32 @@ class TaskPlaneService:
previous = self.store._last_evidence_hash(task_id)
evidence_entries = []
for evidence in evidences:
envelope = EvidenceEnvelope.create(
task_id=task_id,
claim_id=claim_id,
kind=str(evidence.get("kind") or evidence.get("type") or "generic"),
worker_identity=actor,
payload=evidence,
previous_evidence_hash=previous,
signature=evidence.get("signature"),
attestation_ref=evidence.get("attestation_ref"),
)
if "schema_version" in evidence:
try:
envelope = EvidenceEnvelope.from_dict(evidence)
except ValueError as exc:
raise ValidationError("invalid evidence envelope") from exc
else:
envelope = EvidenceEnvelope.create(
task_id=task_id,
claim_id=claim_id,
kind=str(
evidence.get("kind") or evidence.get("type") or "generic"
),
worker_identity=actor,
payload=evidence.get("payload", evidence),
previous_evidence_hash=previous,
evidence_id=evidence.get("evidence_id"),
created_at=evidence.get("created_at"),
expires_at=evidence.get("expires_at"),
signature=evidence.get("signature"),
attestation_ref=evidence.get("attestation_ref"),
signature_algorithm=evidence.get("signature_algorithm"),
signing_key_id=evidence.get("signing_key_id"),
verifier_identity=evidence.get("verifier_identity"),
authority_domain=evidence.get("authority_domain"),
sequence=evidence.get("sequence"),
)
evidence_entries.append(envelope)
previous = envelope.evidence_digest
final_status = (
@@ -910,8 +940,29 @@ def run_server(
*,
authenticator: Callable[[str], str | None] | None = None,
):
handler = TaskPlaneRequestHandler
handler.service = service
handler.authenticator = authenticator
# A separate handler class prevents one server/tenant from overwriting
# another concurrently running server's service or authenticator.
handler = type("BoundTaskPlaneRequestHandler", (TaskPlaneRequestHandler,), {
"service": service, "authenticator": authenticator,
})
server = ThreadingHTTPServer((host, port), handler)
server.serve_forever()
runtime = service.c6_runtime
stop = __import__("threading").Event()
runner = None
if runtime is not None and runtime.bindings.enabled:
import threading
def run_ticks():
while not stop.is_set():
runtime.tick()
stop.wait(1.0)
runner = threading.Thread(target=run_ticks, name="c6-host-tick", daemon=True)
runner.start()
try:
server.serve_forever()
finally:
stop.set()
if runtime is not None:
runtime.close()
if runner is not None:
runner.join(timeout=2.0)
server.server_close()
@@ -0,0 +1,666 @@
"""C6 ARO Phase A: bounded read-only audit evaluation and immutable reports."""
from __future__ import annotations
from collections.abc import Callable, Mapping
from dataclasses import dataclass
from datetime import datetime, timedelta, timezone
from enum import Enum
import hashlib
import json
import math
import os
from pathlib import Path
import re
import tempfile
from typing import Literal, Protocol
from .operation_view import DomainStamp, validate_digest
class Dimension(str, Enum):
FUNCTION = "function"
PERFORMANCE = "performance"
EXCEPTION = "exception"
USAGE = "usage"
CODE_DEFECT = "code_defect"
HISTORY = "history"
DEADLOCK = "deadlock"
LONG_RUNNING = "long_running"
_DIMENSIONS = tuple(Dimension)
_SAFE_REF_RE = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._:/@+-]{0,255}$")
_METRICS: dict[Dimension, frozenset[str]] = {
Dimension.FUNCTION: frozenset({"required_checks", "passed_checks"}),
Dimension.PERFORMANCE: frozenset({"duration_p95_ms"}),
Dimension.EXCEPTION: frozenset({"unhandled_exception_count"}),
Dimension.USAGE: frozenset({"operational_prompt_count", "manual_continue_count"}),
Dimension.CODE_DEFECT: frozenset({"validated_open_defect_count"}),
Dimension.HISTORY: frozenset({"same_fingerprint_failure_count"}),
Dimension.DEADLOCK: frozenset({"wait_cycle_count", "oldest_no_progress_seconds"}),
Dimension.LONG_RUNNING: frozenset({"oldest_nonterminal_age_seconds"}),
}
_COUNT_METRICS = frozenset(
{
"required_checks",
"passed_checks",
"unhandled_exception_count",
"operational_prompt_count",
"manual_continue_count",
"validated_open_defect_count",
"same_fingerprint_failure_count",
"wait_cycle_count",
}
)
_THRESHOLDS = frozenset(
{
"max_duration_p95_ms",
"repeated_failure_threshold",
"deadlock_seconds",
"max_nonterminal_age_seconds",
}
)
def _text(value: object, name: str) -> str:
if not isinstance(value, str) or not value or value != value.strip() or "\x00" in value:
raise ValueError(f"{name} must be a canonical non-empty string")
return value
def _safe_ref(value: object) -> str:
ref = _text(value, "evidence_ref")
lowered = ref.casefold()
if (
_SAFE_REF_RE.fullmatch(ref) is None
or ".." in ref
or ref.startswith(("/", "\\"))
or "://" in ref
or any(
marker in lowered
for marker in (
"token=",
"secret=",
"password=",
"cookie=",
"authorization=",
"private_key=",
"bearer ",
)
)
):
raise ValueError("evidence_ref is not an allowed opaque reference")
return ref
def _aware(value: object, name: str) -> datetime:
if not isinstance(value, datetime) or value.tzinfo is None or value.utcoffset() is None:
raise ValueError(f"{name} must be timezone-aware")
return value
def _number(value: object, name: str, *, non_negative: bool = True) -> float:
if isinstance(value, bool) or not isinstance(value, (int, float)):
raise ValueError(f"{name} must be numeric")
result = float(value)
if not math.isfinite(result) or (non_negative and result < 0):
raise ValueError(f"{name} must be finite and non-negative")
return result
@dataclass(frozen=True)
class Sample:
metric: str
value: float
observed_at: datetime
evidence_ref: str
def __post_init__(self) -> None:
_text(self.metric, "metric")
_number(self.value, "sample value")
_aware(self.observed_at, "observed_at")
_safe_ref(self.evidence_ref)
@dataclass(frozen=True)
class DimensionInput:
dimension: Dimension
samples: tuple[Sample, ...]
stamp: DomainStamp
captured_at: datetime
expires_at: datetime
def __post_init__(self) -> None:
if not isinstance(self.dimension, Dimension):
raise ValueError("dimension must be Dimension")
if not isinstance(self.samples, tuple):
raise ValueError("samples must be a tuple")
if any(not isinstance(sample, Sample) for sample in self.samples):
raise ValueError("samples must contain Sample values")
if not isinstance(self.stamp, DomainStamp):
raise ValueError("stamp must be DomainStamp")
captured = _aware(self.captured_at, "captured_at")
expires = _aware(self.expires_at, "expires_at")
if expires <= captured:
raise ValueError("expires_at must be after captured_at")
if any(sample.observed_at > captured for sample in self.samples):
raise ValueError("sample cannot be observed after capture")
def _canonical_datetime(value: datetime) -> str:
return value.astimezone(timezone.utc).isoformat().replace("+00:00", "Z")
def _snapshot_payload(
*,
window_start: datetime,
window_end: datetime,
dimensions: tuple[DimensionInput, ...],
rules_revision: str,
) -> dict[str, object]:
return {
"window_start": _canonical_datetime(window_start),
"window_end": _canonical_datetime(window_end),
"rules_revision": rules_revision,
"dimensions": [
{
"dimension": item.dimension.value,
"stamp": {
"domain": item.stamp.domain,
"object_id": item.stamp.object_id,
"generation": item.stamp.generation,
"revision": item.stamp.revision,
"event_id": item.stamp.event_id,
},
"captured_at": _canonical_datetime(item.captured_at),
"expires_at": _canonical_datetime(item.expires_at),
"samples": [
{
"metric": sample.metric,
"value": sample.value,
"observed_at": _canonical_datetime(sample.observed_at),
"evidence_ref": sample.evidence_ref,
}
for sample in sorted(
item.samples,
key=lambda value: (
value.metric,
value.observed_at,
value.evidence_ref,
),
)
],
}
for item in sorted(
dimensions,
key=lambda value: (
value.dimension.value,
value.stamp.domain,
value.stamp.object_id,
value.stamp.generation,
value.stamp.revision,
value.stamp.event_id,
),
)
],
}
def _digest(value: Mapping[str, object]) -> str:
encoded = json.dumps(
value,
sort_keys=True,
separators=(",", ":"),
ensure_ascii=False,
allow_nan=False,
).encode("utf-8")
return hashlib.sha256(encoded).hexdigest()
@dataclass(frozen=True)
class AuditSnapshot:
snapshot_id: str
window_start: datetime
window_end: datetime
dimensions: tuple[DimensionInput, ...]
rules_revision: str
def __post_init__(self) -> None:
validate_digest(self.snapshot_id)
start = _aware(self.window_start, "window_start")
end = _aware(self.window_end, "window_end")
if end <= start:
raise ValueError("audit window must be increasing")
if not isinstance(self.dimensions, tuple):
raise ValueError("dimensions must be a tuple")
if any(not isinstance(item, DimensionInput) for item in self.dimensions):
raise ValueError("dimensions must contain DimensionInput values")
_text(self.rules_revision, "rules_revision")
expected = _digest(
_snapshot_payload(
window_start=start,
window_end=end,
dimensions=self.dimensions,
rules_revision=self.rules_revision,
)
)
if self.snapshot_id != expected:
raise ValueError("snapshot_id does not match complete canonical inputs")
@classmethod
def create(
cls,
*,
window_start: datetime,
window_end: datetime,
dimensions: tuple[DimensionInput, ...],
rules_revision: str,
) -> "AuditSnapshot":
payload = _snapshot_payload(
window_start=_aware(window_start, "window_start"),
window_end=_aware(window_end, "window_end"),
dimensions=dimensions,
rules_revision=_text(rules_revision, "rules_revision"),
)
return cls(
snapshot_id=_digest(payload),
window_start=window_start,
window_end=window_end,
dimensions=dimensions,
rules_revision=rules_revision,
)
@dataclass(frozen=True)
class RuleLimits:
rules_revision: str
max_snapshot_skew_seconds: int
threshold_by_metric: tuple[tuple[str, float], ...]
def __post_init__(self) -> None:
_text(self.rules_revision, "rules_revision")
if (
isinstance(self.max_snapshot_skew_seconds, bool)
or not isinstance(self.max_snapshot_skew_seconds, int)
or self.max_snapshot_skew_seconds < 0
):
raise ValueError("max_snapshot_skew_seconds must be non-negative int")
if not isinstance(self.threshold_by_metric, tuple):
raise ValueError("threshold_by_metric must be a tuple")
names: list[str] = []
for item in self.threshold_by_metric:
if not isinstance(item, tuple) or len(item) != 2:
raise ValueError("threshold entries must be pairs")
name = _text(item[0], "threshold metric")
if name not in _THRESHOLDS:
raise ValueError("unknown threshold metric")
_number(item[1], "threshold")
names.append(name)
if len(names) != len(set(names)):
raise ValueError("duplicate threshold metric")
@property
def thresholds(self) -> dict[str, float]:
return {name: float(value) for name, value in self.threshold_by_metric}
@dataclass(frozen=True)
class Finding:
dimension: Dimension
status: Literal["CLEAR", "FINDING", "UNKNOWN"]
reason: str
evidence_refs: tuple[str, ...]
def __post_init__(self) -> None:
if not isinstance(self.dimension, Dimension):
raise ValueError("dimension must be Dimension")
if self.status not in ("CLEAR", "FINDING", "UNKNOWN"):
raise ValueError("invalid finding status")
_text(self.reason, "reason")
if not isinstance(self.evidence_refs, tuple):
raise ValueError("evidence_refs must be a tuple")
for ref in self.evidence_refs:
_safe_ref(ref)
@dataclass(frozen=True)
class AuditReport:
report_id: str
snapshot_id: str
rules_revision: str
findings: tuple[Finding, ...]
mutation_count: Literal[0]
def __post_init__(self) -> None:
validate_digest(self.report_id)
validate_digest(self.snapshot_id)
_text(self.rules_revision, "rules_revision")
if not isinstance(self.findings, tuple) or any(
not isinstance(item, Finding) for item in self.findings
):
raise ValueError("findings must be a tuple of Finding")
if type(self.mutation_count) is not int or self.mutation_count != 0:
raise ValueError("ARO Phase A mutation_count must be exactly zero")
def _unknown(dimension: Dimension, reason: str, refs: tuple[str, ...] = ()) -> Finding:
return Finding(dimension, "UNKNOWN", reason, refs)
def _metric_values(item: DimensionInput) -> tuple[dict[str, float] | None, tuple[str, ...]]:
expected = _METRICS[item.dimension]
if not item.samples:
return None, ()
names = [sample.metric for sample in item.samples]
refs = tuple(dict.fromkeys(sample.evidence_ref for sample in item.samples))
if set(names) != expected or len(names) != len(set(names)):
return None, refs
values = {sample.metric: float(sample.value) for sample in item.samples}
for name in _COUNT_METRICS & expected:
if not values[name].is_integer():
return None, refs
return values, refs
def _evaluate_dimension(
item: DimensionInput, thresholds: Mapping[str, float]
) -> Finding:
values, refs = _metric_values(item)
if values is None:
return _unknown(item.dimension, "METRICS_MISSING_DUPLICATE_OR_UNKNOWN", refs)
dimension = item.dimension
if dimension == Dimension.FUNCTION:
required = values["required_checks"]
passed = values["passed_checks"]
if required <= 0 or passed > required:
return _unknown(dimension, "FUNCTION_COUNTS_INVALID", refs)
status = "FINDING" if passed < required else "CLEAR"
return Finding(dimension, status, "REQUIRED_CHECKS_INCOMPLETE" if status == "FINDING" else "ALL_REQUIRED_CHECKS_PASSED", refs)
if dimension == Dimension.PERFORMANCE:
threshold = thresholds.get("max_duration_p95_ms")
if threshold is None:
return _unknown(dimension, "THRESHOLD_MISSING", refs)
status = "FINDING" if values["duration_p95_ms"] > threshold else "CLEAR"
return Finding(dimension, status, "P95_DURATION_EXCEEDED" if status == "FINDING" else "P95_DURATION_WITHIN_LIMIT", refs)
if dimension == Dimension.EXCEPTION:
status = "FINDING" if values["unhandled_exception_count"] > 0 else "CLEAR"
return Finding(dimension, status, "UNHANDLED_EXCEPTIONS_PRESENT" if status == "FINDING" else "NO_UNHANDLED_EXCEPTIONS", refs)
if dimension == Dimension.USAGE:
status = "FINDING" if any(value > 0 for value in values.values()) else "CLEAR"
return Finding(dimension, status, "OPERATIONAL_PROMPT_OR_CONTINUE_PRESENT" if status == "FINDING" else "NO_OPERATIONAL_PROMPT_FRICTION", refs)
if dimension == Dimension.CODE_DEFECT:
status = "FINDING" if values["validated_open_defect_count"] > 0 else "CLEAR"
return Finding(dimension, status, "VALIDATED_OPEN_DEFECTS_PRESENT" if status == "FINDING" else "NO_VALIDATED_OPEN_DEFECTS", refs)
if dimension == Dimension.HISTORY:
threshold = thresholds.get("repeated_failure_threshold")
if threshold is None or threshold <= 0 or not threshold.is_integer():
return _unknown(dimension, "THRESHOLD_MISSING_OR_INVALID", refs)
status = "FINDING" if values["same_fingerprint_failure_count"] >= threshold else "CLEAR"
return Finding(dimension, status, "FAILURE_FINGERPRINT_REPEATED" if status == "FINDING" else "FAILURE_FINGERPRINT_BELOW_LIMIT", refs)
if dimension == Dimension.DEADLOCK:
threshold = thresholds.get("deadlock_seconds")
if threshold is None:
return _unknown(dimension, "THRESHOLD_MISSING", refs)
status = "FINDING" if (
values["wait_cycle_count"] > 0
and values["oldest_no_progress_seconds"] > threshold
) else "CLEAR"
return Finding(dimension, status, "WAIT_CYCLE_WITHOUT_PROGRESS" if status == "FINDING" else "NO_STALE_WAIT_CYCLE", refs)
if dimension == Dimension.LONG_RUNNING:
threshold = thresholds.get("max_nonterminal_age_seconds")
if threshold is None:
return _unknown(dimension, "THRESHOLD_MISSING", refs)
status = "FINDING" if values["oldest_nonterminal_age_seconds"] > threshold else "CLEAR"
return Finding(dimension, status, "NONTERMINAL_AGE_EXCEEDED" if status == "FINDING" else "NONTERMINAL_AGE_WITHIN_LIMIT", refs)
raise AssertionError("unhandled dimension")
def evaluate_snapshot(
snapshot: AuditSnapshot, *, limits: RuleLimits, now: datetime
) -> AuditReport:
"""Evaluate all eight dimensions; missing or inconsistent evidence is UNKNOWN."""
if not isinstance(snapshot, AuditSnapshot) or not isinstance(limits, RuleLimits):
raise TypeError("snapshot and limits must be typed values")
now = _aware(now, "now")
grouped: dict[Dimension, list[DimensionInput]] = {dimension: [] for dimension in _DIMENSIONS}
for item in snapshot.dimensions:
grouped[item.dimension].append(item)
global_reason: str | None = None
if snapshot.rules_revision != limits.rules_revision:
global_reason = "RULES_REVISION_UNKNOWN"
elif snapshot.window_end > now:
global_reason = "AUDIT_WINDOW_NOT_CLOSED"
else:
observed = [sample.observed_at for item in snapshot.dimensions for sample in item.samples]
if observed and (max(observed) - min(observed)).total_seconds() > limits.max_snapshot_skew_seconds:
global_reason = "CROSS_DOMAIN_SNAPSHOT_SKEW"
findings: list[Finding] = []
for dimension in _DIMENSIONS:
items = grouped[dimension]
if global_reason is not None:
refs = tuple(
dict.fromkeys(sample.evidence_ref for item in items for sample in item.samples)
)
findings.append(_unknown(dimension, global_reason, refs))
continue
if len(items) != 1:
findings.append(
_unknown(
dimension,
"DIMENSION_MISSING" if not items else "DIMENSION_CONFLICT",
)
)
continue
item = items[0]
refs = tuple(dict.fromkeys(sample.evidence_ref for sample in item.samples))
if item.captured_at > now or now >= item.expires_at:
findings.append(_unknown(dimension, "DIMENSION_STALE_OR_FUTURE", refs))
continue
if any(
sample.observed_at < snapshot.window_start
or sample.observed_at > snapshot.window_end
for sample in item.samples
):
findings.append(_unknown(dimension, "SAMPLE_OUTSIDE_WINDOW", refs))
continue
findings.append(_evaluate_dimension(item, limits.thresholds))
report_payload = {
"window_start": _canonical_datetime(snapshot.window_start),
"window_end": _canonical_datetime(snapshot.window_end),
"snapshot_id": snapshot.snapshot_id,
"rules_revision": limits.rules_revision,
}
return AuditReport(
report_id=_digest(report_payload),
snapshot_id=snapshot.snapshot_id,
rules_revision=limits.rules_revision,
findings=tuple(findings),
mutation_count=0,
)
class ReadonlyAuditSource(Protocol):
def collect(
self, *, window_start: datetime, window_end: datetime
) -> AuditSnapshot: ...
class AuditReportSink(Protocol):
def put_if_absent(self, report: AuditReport) -> str: ...
def _report_bytes(report: AuditReport) -> bytes:
payload = {
"schema": "aios.aro-readonly-report.v1",
"report_id": report.report_id,
"snapshot_id": report.snapshot_id,
"rules_revision": report.rules_revision,
"mutation_count": report.mutation_count,
"findings": [
{
"dimension": finding.dimension.value,
"status": finding.status,
"reason": finding.reason,
"evidence_refs": list(finding.evidence_refs),
}
for finding in report.findings
],
}
return (
json.dumps(
payload,
sort_keys=True,
separators=(",", ":"),
ensure_ascii=False,
allow_nan=False,
)
+ "\n"
).encode("utf-8")
class FilesystemAuditReportSink:
"""Write one immutable, deterministic report under a host-fixed root."""
def __init__(self, root: Path) -> None:
if not isinstance(root, Path) or not root.is_absolute() or ".." in root.parts:
raise ValueError("audit root must be an absolute fixed path")
self.root = root
self._prepare_root()
def _prepare_root(self) -> None:
existing = self.root
while not existing.exists() and existing != existing.parent:
existing = existing.parent
cursor = existing
while True:
if cursor.is_symlink():
raise ValueError("audit root ancestry must not contain symlinks")
if cursor == cursor.parent:
break
cursor = cursor.parent
self.root.mkdir(parents=True, exist_ok=True)
if self.root.is_symlink() or not self.root.is_dir():
raise ValueError("audit root must be a real directory")
if self.root.resolve(strict=True) != self.root.absolute():
raise ValueError("audit root must not traverse reparse points or symlinks")
def put_if_absent(self, report: AuditReport) -> str:
if not isinstance(report, AuditReport):
raise TypeError("report must be AuditReport")
self._prepare_root()
validate_digest(report.report_id)
body = _report_bytes(report)
target = self.root / f"{report.report_id}.json"
if target.parent != self.root or target.is_symlink():
raise ValueError("REPORT_PATH_INVALID")
if target.exists():
if not target.is_file() or target.read_bytes() != body:
raise ValueError("REPORT_CONFLICT")
return f"aro:{report.report_id}"
file_descriptor, temporary_name = tempfile.mkstemp(
prefix=f".{report.report_id}.", suffix=".tmp", dir=self.root
)
temporary = Path(temporary_name)
try:
with os.fdopen(file_descriptor, "wb") as handle:
handle.write(body)
handle.flush()
os.fsync(handle.fileno())
try:
os.link(temporary, target)
except FileExistsError:
if target.is_symlink() or not target.is_file() or target.read_bytes() != body:
raise ValueError("REPORT_CONFLICT")
if target.read_bytes() != body:
raise ValueError("REPORT_CONFLICT")
finally:
try:
temporary.unlink()
except FileNotFoundError:
pass
return f"aro:{report.report_id}"
class AroReadonlyTick:
"""Default-off in-process tick over the most recently closed UTC window."""
def __init__(
self,
source: ReadonlyAuditSource,
sink: AuditReportSink,
limits: RuleLimits,
interval_seconds: int,
clock: Callable[[], datetime],
*,
enabled: bool = False,
) -> None:
if not hasattr(source, "collect") or not callable(source.collect):
raise ValueError("source must implement read-only collect")
if not hasattr(sink, "put_if_absent") or not callable(sink.put_if_absent):
raise ValueError("sink must implement put_if_absent")
if not isinstance(limits, RuleLimits):
raise ValueError("limits must be RuleLimits")
if isinstance(interval_seconds, bool) or not isinstance(interval_seconds, int) or interval_seconds <= 0:
raise ValueError("interval_seconds must be positive int")
if not callable(clock) or type(enabled) is not bool:
raise ValueError("clock must be callable and enabled must be bool")
self.source = source
self.sink = sink
self.limits = limits
self.interval_seconds = interval_seconds
self.clock = clock
self.enabled = enabled
self._last_window_end: datetime | None = None
def run_due(self) -> str | None:
if not self.enabled:
return None
now = _aware(self.clock(), "clock result").astimezone(timezone.utc)
end_timestamp = int(now.timestamp()) // self.interval_seconds * self.interval_seconds
window_end = datetime.fromtimestamp(end_timestamp, tz=timezone.utc)
window_start = window_end - timedelta(seconds=self.interval_seconds)
if self._last_window_end == window_end:
return None
try:
snapshot = self.source.collect(
window_start=window_start, window_end=window_end
)
if (
not isinstance(snapshot, AuditSnapshot)
or snapshot.window_start != window_start
or snapshot.window_end != window_end
):
raise ValueError("source returned a snapshot for the wrong window")
except Exception:
snapshot = AuditSnapshot.create(
window_start=window_start,
window_end=window_end,
dimensions=(),
rules_revision=self.limits.rules_revision,
)
report = evaluate_snapshot(snapshot, limits=self.limits, now=now)
try:
reference = self.sink.put_if_absent(report)
except Exception:
return None
self._last_window_end = window_end
return reference
__all__ = [
"AroReadonlyTick",
"AuditReport",
"AuditReportSink",
"AuditSnapshot",
"Dimension",
"DimensionInput",
"FilesystemAuditReportSink",
"Finding",
"ReadonlyAuditSource",
"RuleLimits",
"Sample",
"evaluate_snapshot",
]
@@ -0,0 +1,173 @@
"""Narrow production seam from a validated C6 plan to existing acquisition."""
from __future__ import annotations
from dataclasses import replace
import math
import time
from typing import Protocol
from .authorization_planner import (
AuthorizationPlan,
AuthorizationPlanInput,
CapabilityNamespace,
GitManifestRegistry,
RuntimeObservation,
StaticCapabilityManifest,
VerifiedRootSnapshot,
VerifiedRootSnapshotReader,
authorization_input_digest,
authorization_scope_digest,
build_authorization_plan,
verified_root_snapshot_digest,
)
from .governed_writer_automation import (
AuthorizationDenied,
EpochStatus,
GovernedWriterAdapters,
GovernedWriterAutomation,
GovernedWriterRequest,
GovernedWriterSession,
GovernedWriteReadiness,
RecoveryAuthorization,
)
class RuntimeObservationReader(Protocol):
"""Host-owned fresh runtime authority used at writer acquisition."""
def read_runtime_observation(
self,
*,
plan_input: AuthorizationPlanInput,
manifest: StaticCapabilityManifest,
root: VerifiedRootSnapshot | None,
now: float,
) -> RuntimeObservation | None: ...
def acquire_authorized_writer(
*,
automation: GovernedWriterAutomation,
request: GovernedWriterRequest,
authorization_ref: str,
readiness: GovernedWriteReadiness,
epoch: EpochStatus | None,
adapters: GovernedWriterAdapters,
plan: AuthorizationPlan,
plan_input: AuthorizationPlanInput,
root_reader: VerifiedRootSnapshotReader,
runtime_reader: RuntimeObservationReader,
registry: GitManifestRegistry,
expected_registry_oid: str,
expected_manifest_sha256: str,
recovery_authorization: RecoveryAuthorization | None = None,
now: float | None = None,
) -> GovernedWriterSession:
"""Re-read existing verified Root immediately before `acquire_verified`.
This function deliberately has no signer, claim, lease, or direct
`automation.acquire` path. The existing automation remains the sole
composition root for effects.
"""
supplied_now = time.time() if now is None else now
if isinstance(supplied_now, bool):
raise AuthorizationDenied("TIME_INVALID")
try:
current = float(supplied_now)
except (TypeError, ValueError):
raise AuthorizationDenied("TIME_INVALID") from None
if not math.isfinite(current):
raise AuthorizationDenied("TIME_INVALID")
input_digest = authorization_input_digest(plan_input)
scope_digest = authorization_scope_digest(plan_input.scope)
if (plan_input.root_approval_ref != authorization_ref
or plan.input_digest != input_digest
or plan.scope_digest != scope_digest):
raise AuthorizationDenied("PLAN_INPUT_DRIFT")
if (request.subject != plan_input.actor_subject or request.task_id != plan_input.task_id
or request.repository != plan_input.scope.repository
or request.target_branch != plan_input.scope.target_branch
or request.base_oid != plan_input.scope.source_oid
or request.risk != plan_input.scope.risk_level
or request.file_allowlist != plan_input.scope.file_allowlist
or request.plan_digest != input_digest):
raise AuthorizationDenied("WRITER_REQUEST_PLAN_DRIFT")
if any(ref.namespace != CapabilityNamespace.AGENT_TASK for ref in plan_input.required_capabilities):
raise AuthorizationDenied("WRITER_REQUEST_CAPABILITY_NAMESPACE_INVALID")
requested_capabilities = frozenset(ref.token for ref in plan_input.required_capabilities)
if request.required_capabilities != requested_capabilities:
raise AuthorizationDenied("WRITER_REQUEST_CAPABILITY_DRIFT")
# This is a second, explicit frozen-runtime read. The existing
# acquire_verified path repeats it before effects as its own invariant.
automation.manifest_store.assert_current(
manifest_id=request.manifest_id,
runtime_fingerprint=request.runtime_fingerprint,
process_start_identity=request.process_start_identity,
)
manifest = registry.read_manifest(
expected_registry_oid=expected_registry_oid,
expected_manifest_sha256=expected_manifest_sha256,
)
if manifest.manifest_id != request.manifest_id or manifest.declaration_digest != plan.policy_digest:
raise AuthorizationDenied("POLICY_MANIFEST_DRIFT")
root = root_reader.read_verified_root(
authorization_ref=authorization_ref,
root_request_sha256=plan_input.root_request_sha256,
now=current,
)
if root is None or root.status != "VALID" or root.approval_ref != authorization_ref:
raise AuthorizationDenied("ROOT_REVOKED_OR_UNAVAILABLE")
if (root.root_request_sha256 != plan_input.root_request_sha256
or root.subject != plan_input.actor_subject
or root.audience != plan_input.audience
or root.mission_id != plan_input.mission_id
or root.scope != plan_input.scope):
raise AuthorizationDenied("ROOT_SCOPE_DRIFT")
if verified_root_snapshot_digest(root) != plan.root_snapshot_digest:
raise AuthorizationDenied("ROOT_SNAPSHOT_DRIFT")
if not (root.not_before <= current and root.observed_at <= current < root.snapshot_expires_at <= root.expires_at):
raise AuthorizationDenied("ROOT_EXPIRED")
reader = getattr(runtime_reader, "read_runtime_observation", None)
if not callable(reader):
raise AuthorizationDenied("RUNTIME_OBSERVATION_UNAVAILABLE")
try:
runtime = reader(
plan_input=plan_input,
manifest=manifest,
root=root,
now=current,
)
except Exception:
raise AuthorizationDenied("RUNTIME_OBSERVATION_UNAVAILABLE") from None
if not isinstance(runtime, RuntimeObservation):
raise AuthorizationDenied("RUNTIME_OBSERVATION_UNAVAILABLE")
fresh_input = replace(plan_input, now=current)
fresh_plan = build_authorization_plan(
request=fresh_input,
manifest=manifest,
runtime=runtime,
root=root,
)
if fresh_plan.next_action != "EXECUTE_GOVERNED_PLAN":
raise AuthorizationDenied(
fresh_plan.reason_code or "authorization plan is not executable"
)
if (
plan.policy_digest != fresh_plan.policy_digest
or plan.required_authorization != fresh_plan.required_authorization
or plan.scope_digest != fresh_plan.scope_digest
or plan.root_snapshot_digest != fresh_plan.root_snapshot_digest
):
raise AuthorizationDenied("AUTHORIZATION_PLAN_DRIFT")
# `acquire_verified` repeats the existing Owner authority verification
# before its own effect path. Do not substitute a locally reconstructed
# policy or call its test-only `acquire` method here.
return automation.acquire_verified(
request=request,
authorization_ref=authorization_ref,
readiness=readiness,
epoch=epoch,
adapters=adapters,
recovery_authorization=recovery_authorization,
now=current,
)
@@ -0,0 +1,325 @@
"""Read-only C6 authorization planning over existing authority boundaries."""
from __future__ import annotations
from dataclasses import asdict, dataclass
from enum import Enum
import hashlib
import json
import math
from pathlib import Path, PurePosixPath
import re
import subprocess
from typing import Any, Literal, Mapping, Protocol
from aios_capability_contract import normalize_required_capabilities
from .governed_writer_automation import RISK_ORDER
_OID = re.compile(r"^[0-9a-f]{40}$")
_SHA = re.compile(r"^[0-9a-f]{64}$")
_REGISTRY_PATH = "governance/capabilities/registry.v1.json"
class RegistryReadError(ValueError):
pass
class CapabilityNamespace(str, Enum):
AGENT_TASK = "agent-task"
TASK_PLANE_WILDCARD = "task-plane-wildcard"
def _text(value: object, field: str) -> str:
if not isinstance(value, str) or not value or value != value.strip() or "\x00" in value:
raise ValueError(f"{field}_INVALID")
return value
def _oid(value: object, field: str) -> str:
value = _text(value, field)
if _OID.fullmatch(value) is None:
raise ValueError(f"{field}_INVALID")
return value
def _digest(value: object, field: str) -> str:
value = _text(value, field)
if _SHA.fullmatch(value) is None:
raise ValueError(f"{field}_INVALID")
return value
def _finite(value: object, field: str) -> float:
if isinstance(value, bool) or not isinstance(value, (float, int)) or not math.isfinite(float(value)):
raise ValueError(f"{field}_INVALID")
return float(value)
def _canonical(value: object) -> bytes:
return json.dumps(value, sort_keys=True, separators=(",", ":"), ensure_ascii=False, allow_nan=False).encode("utf-8")
def authorization_scope_digest(scope: "ScopeSpec") -> str:
return hashlib.sha256(_canonical({
"repository": scope.repository, "target_branch": scope.target_branch,
"source_oid": scope.source_oid, "file_allowlist": list(scope.file_allowlist),
"budget_units": scope.budget_units, "risk_level": scope.risk_level,
})).hexdigest()
@dataclass(frozen=True)
class CapabilityRef:
namespace: CapabilityNamespace
token: str
def __post_init__(self) -> None:
if not isinstance(self.namespace, CapabilityNamespace):
raise ValueError("CAPABILITY_NAMESPACE_INVALID")
_text(self.token, "capability")
@dataclass(frozen=True)
class ScopeSpec:
repository: str
target_branch: str
source_oid: str
file_allowlist: tuple[str, ...]
budget_units: int
risk_level: str
def __post_init__(self) -> None:
_text(self.repository, "repository"); _text(self.target_branch, "target_branch")
_oid(self.source_oid, "source_oid"); _text(self.risk_level, "risk_level")
if type(self.budget_units) is not int or self.budget_units < 0:
raise ValueError("budget_units_INVALID")
if not self.file_allowlist or len(set(self.file_allowlist)) != len(self.file_allowlist):
raise ValueError("file_allowlist_INVALID")
for path in self.file_allowlist:
if not isinstance(path, str) or not path or path != PurePosixPath(path).as_posix() or PurePosixPath(path).is_absolute() or ".." in PurePosixPath(path).parts:
raise ValueError("file_allowlist_INVALID")
@dataclass(frozen=True)
class RuntimeObservation:
observation_id: str; observed_at: float; expires_at: float; source_revision: str
actor_subject: str; audience: str; operation_id: str; scope_digest: str
manifest_revision: str; manifest_digest: str; executor_installed: bool | None
executor_callable: bool | None; authorization_live: bool | None
preconditions_pass: bool | None; revocation_epoch: int | None
def __post_init__(self) -> None:
for name in ("observation_id", "source_revision", "actor_subject", "audience", "operation_id", "manifest_revision"):
_text(getattr(self, name), name)
_finite(self.observed_at, "observed_at"); _finite(self.expires_at, "expires_at")
if self.expires_at <= self.observed_at: raise ValueError("OBSERVATION_EXPIRY_INVALID")
_digest(self.scope_digest, "scope_digest"); _digest(self.manifest_digest, "manifest_digest")
for name in ("executor_installed", "executor_callable", "authorization_live", "preconditions_pass"):
if getattr(self, name) is not None and type(getattr(self, name)) is not bool: raise ValueError(f"{name}_INVALID")
if self.revocation_epoch is not None and (type(self.revocation_epoch) is not int or self.revocation_epoch < 0): raise ValueError("revocation_epoch_INVALID")
@dataclass(frozen=True)
class VerifiedRootSnapshot:
approval_ref: str; root_request_sha256: str; subject: str; audience: str; mission_id: str
scope: ScopeSpec; capabilities: tuple[CapabilityRef, ...]; nonce: str; not_before: float
expires_at: float; revocation_epoch: int; receipt_ref: str; observed_at: float
snapshot_expires_at: float; status: Literal["VALID", "REVOKED"]
def __post_init__(self) -> None:
for name in ("approval_ref", "subject", "audience", "mission_id", "nonce", "receipt_ref"):
_text(getattr(self, name), name)
_digest(self.root_request_sha256, "root_request_sha256")
for name in ("not_before", "expires_at", "observed_at", "snapshot_expires_at"): _finite(getattr(self, name), name)
if not self.not_before <= self.snapshot_expires_at <= self.expires_at: raise ValueError("ROOT_TIME_INVALID")
if type(self.revocation_epoch) is not int or self.revocation_epoch < 0: raise ValueError("revocation_epoch_INVALID")
if self.status not in ("VALID", "REVOKED"): raise ValueError("ROOT_STATUS_INVALID")
@dataclass(frozen=True)
class StaticCapabilityManifest:
manifest_id: str; revision: str; capabilities: tuple[CapabilityRef, ...]
executor_refs: tuple[str, ...]; source_oid: str; declaration_digest: str
allowed_scopes: tuple[ScopeSpec, ...]
def __post_init__(self) -> None:
_text(self.manifest_id, "manifest_id"); _text(self.revision, "revision")
_oid(self.source_oid, "source_oid"); _digest(self.declaration_digest, "declaration_digest")
if len(set(self.capabilities)) != len(self.capabilities): raise ValueError("CAPABILITY_DUPLICATE")
for ref in self.executor_refs: _text(ref, "executor_ref")
@dataclass(frozen=True)
class AuthorizationPlanInput:
operation_id: str; actor_subject: str; audience: str; mission_id: str; task_id: str
scope: ScopeSpec; required_capabilities: tuple[CapabilityRef, ...]
root_approval_ref: str; root_request_sha256: str; now: float
def __post_init__(self) -> None:
for name in ("operation_id", "actor_subject", "audience", "mission_id", "task_id", "root_approval_ref"):
_text(getattr(self, name), name)
_digest(self.root_request_sha256, "root_request_sha256"); _finite(self.now, "now")
if not self.required_capabilities: raise ValueError("REQUIRED_CAPABILITIES_EMPTY")
@dataclass(frozen=True)
class AuthorizationPlan:
input_digest: str; scope_digest: str; policy_digest: str; required_authorization: str
root_snapshot_digest: str | None
reason_code: str | None; next_action: Literal["EXECUTE_GOVERNED_PLAN", "WAITING_ROOT_AUTHORIZATION", "AUTHORIZATION_REQUIRED", "CAPABILITY_UNAVAILABLE", "PRECONDITION_FAILED", "UNKNOWN"]
evidence_refs: tuple[str, ...]
class GitManifestRegistry(Protocol):
def read_manifest(self, *, expected_registry_oid: str, expected_manifest_sha256: str) -> StaticCapabilityManifest: ...
def read_supersession(self, *, manifest_id: str, revision: str) -> str | None: ...
class VerifiedRootSnapshotReader(Protocol):
def read_verified_root(self, *, authorization_ref: str, root_request_sha256: str, now: float) -> VerifiedRootSnapshot | None: ...
def verified_root_snapshot_digest(root: VerifiedRootSnapshot | None) -> str | None:
return None if root is None else hashlib.sha256(_canonical(asdict(root))).hexdigest()
def authorization_input_digest(request: AuthorizationPlanInput) -> str:
return hashlib.sha256(_canonical(asdict(request))).hexdigest()
def _plan(request: AuthorizationPlanInput, manifest: StaticCapabilityManifest, action: str, reason: str | None, refs: tuple[str, ...], root: VerifiedRootSnapshot | None) -> AuthorizationPlan:
return AuthorizationPlan(
input_digest=authorization_input_digest(request), scope_digest=authorization_scope_digest(request.scope),
policy_digest=manifest.declaration_digest, required_authorization=request.root_approval_ref,
root_snapshot_digest=verified_root_snapshot_digest(root),
reason_code=reason, next_action=action, evidence_refs=refs,
)
def _capabilities_cover(granted: tuple[CapabilityRef, ...], required: tuple[CapabilityRef, ...]) -> tuple[bool, str | None]:
for capability in required:
if capability.namespace == CapabilityNamespace.AGENT_TASK:
if "*" in capability.token:
return False, "CAPABILITY_NAMESPACE_MISMATCH"
normalized = normalize_required_capabilities([capability.token])
if any(value.startswith("exact:") for value in normalized): return False, "CAPABILITY_UNKNOWN"
same_namespace = [item.token for item in granted if item.namespace == CapabilityNamespace.AGENT_TASK]
if not same_namespace and granted:
return False, "CAPABILITY_NAMESPACE_MISMATCH"
effective_grants = normalize_required_capabilities(same_namespace)
if any(value.startswith("exact:") for value in effective_grants):
effective_grants = frozenset(value for value in effective_grants if not value.startswith("exact:"))
if not normalized.issubset(effective_grants): return False, "CAPABILITY_UNAVAILABLE"
elif capability.namespace == CapabilityNamespace.TASK_PLANE_WILDCARD:
if not any(item.namespace == capability.namespace and item.token == capability.token for item in granted): return False, "CAPABILITY_UNAVAILABLE"
return True, None
def _scope_covers(parent: ScopeSpec, child: ScopeSpec) -> bool:
# Use the same risk vocabulary as the authority that will issue the lease.
rank = RISK_ORDER
return (parent.repository == child.repository and parent.target_branch == child.target_branch and parent.source_oid == child.source_oid
and set(child.file_allowlist).issubset(parent.file_allowlist) and child.budget_units <= parent.budget_units
and rank.get(child.risk_level.lower()) is not None
and rank.get(parent.risk_level.lower(), 0) >= rank[child.risk_level.lower()])
def build_authorization_plan(*, request: AuthorizationPlanInput, manifest: StaticCapabilityManifest, runtime: RuntimeObservation, root: VerifiedRootSnapshot | None) -> AuthorizationPlan:
refs = () if root is None else (root.receipt_ref,)
if manifest.source_oid != request.scope.source_oid or not any(_scope_covers(scope, request.scope) for scope in manifest.allowed_scopes):
return _plan(request, manifest, "UNKNOWN", "REGISTRY_DRIFT", refs, root)
allowed, reason = _capabilities_cover(manifest.capabilities, request.required_capabilities)
if not allowed: return _plan(request, manifest, "CAPABILITY_UNAVAILABLE", reason, refs, root)
if root is None: return _plan(request, manifest, "WAITING_ROOT_AUTHORIZATION", "ROOT_MISSING", refs, root)
if root.status == "REVOKED": return _plan(request, manifest, "AUTHORIZATION_REQUIRED", "ROOT_REVOKED", refs, root)
if root.approval_ref != request.root_approval_ref or root.root_request_sha256 != request.root_request_sha256: return _plan(request, manifest, "AUTHORIZATION_REQUIRED", "ROOT_DIGEST_MISMATCH", refs, root)
if root.subject != request.actor_subject or root.audience != request.audience: return _plan(request, manifest, "AUTHORIZATION_REQUIRED", "ROOT_SUBJECT_AUDIENCE_MISMATCH", refs, root)
if root.mission_id != request.mission_id or not _scope_covers(root.scope, request.scope): return _plan(request, manifest, "AUTHORIZATION_REQUIRED", "ROOT_SCOPE_EXCEEDED", refs, root)
allowed, reason = _capabilities_cover(root.capabilities, request.required_capabilities)
if not allowed: return _plan(request, manifest, "AUTHORIZATION_REQUIRED", reason, refs, root)
if not (root.observed_at <= request.now < root.snapshot_expires_at <= root.expires_at) or request.now < root.not_before: return _plan(request, manifest, "WAITING_ROOT_AUTHORIZATION", "ROOT_EXPIRED", refs, root)
if runtime.expires_at <= request.now or runtime.observed_at > request.now: return _plan(request, manifest, "UNKNOWN", "PRECONDITION_UNKNOWN", refs, root)
if runtime.revocation_epoch is None or runtime.revocation_epoch != root.revocation_epoch: return _plan(request, manifest, "UNKNOWN", "EPOCH_REVOKED", refs, root)
if (runtime.actor_subject != request.actor_subject or runtime.audience != request.audience or runtime.operation_id != request.operation_id
or runtime.scope_digest != authorization_scope_digest(request.scope) or runtime.manifest_revision != manifest.revision
or runtime.manifest_digest != manifest.declaration_digest): return _plan(request, manifest, "UNKNOWN", "PLAN_INPUT_DRIFT", refs, root)
if any(value is None for value in (runtime.executor_installed, runtime.executor_callable, runtime.authorization_live, runtime.preconditions_pass)):
return _plan(request, manifest, "UNKNOWN", "PRECONDITION_UNKNOWN", refs, root)
if not all((runtime.executor_installed, runtime.executor_callable, runtime.authorization_live, runtime.preconditions_pass)):
return _plan(request, manifest, "PRECONDITION_FAILED", "PRECONDITION_FAILED", refs, root)
return _plan(request, manifest, "EXECUTE_GOVERNED_PLAN", None, refs, root)
class GitManifestRegistryReader:
"""Fixed-path, immutable Git blob reader; intentionally has no write API."""
def __init__(self, *, configured_repo_root: Path, manifest_id: str) -> None:
self.root = Path(configured_repo_root); self.manifest_id = _text(manifest_id, "manifest_id")
self._last_registry: Mapping[str, Any] | None = None
def _blob(self, oid: str) -> bytes:
_oid(oid, "expected_registry_oid")
if self.root.is_symlink() or not self.root.is_dir(): raise RegistryReadError("REGISTRY_ROOT_INVALID")
result = subprocess.run(["git", "-C", str(self.root), "show", f"{oid}:{_REGISTRY_PATH}"], capture_output=True, timeout=15)
if result.returncode: raise RegistryReadError("REGISTRY_UNAVAILABLE")
return result.stdout
def _registry(self, *, expected_registry_oid: str, expected_manifest_sha256: str) -> Mapping[str, Any]:
raw = self._blob(expected_registry_oid)
if hashlib.sha256(raw).hexdigest() != _digest(expected_manifest_sha256, "expected_manifest_sha256"): raise RegistryReadError("REGISTRY_DRIFT")
try: value = json.loads(raw)
except (UnicodeError, json.JSONDecodeError) as exc: raise RegistryReadError("REGISTRY_INVALID") from exc
if not isinstance(value, Mapping) or set(value) != {"schema", "records", "supersessions"} or value["schema"] != "aios.capability-registry.v1": raise RegistryReadError("REGISTRY_INVALID")
return value
def read_manifest(self, *, expected_registry_oid: str, expected_manifest_sha256: str) -> StaticCapabilityManifest:
registry = self._registry(expected_registry_oid=expected_registry_oid, expected_manifest_sha256=expected_manifest_sha256)
records = registry["records"]
if not isinstance(records, list): raise RegistryReadError("REGISTRY_INVALID")
supersessions = registry["supersessions"]
if not isinstance(supersessions, list): raise RegistryReadError("REGISTRY_INVALID")
record_index: dict[tuple[str, str], Mapping[str, Any]] = {}
for record in records:
if not isinstance(record, Mapping) or not isinstance(record.get("manifest_id"), str) or not isinstance(record.get("revision"), str):
raise RegistryReadError("REGISTRY_INVALID")
key = (record["manifest_id"], record["revision"])
if key in record_index: raise RegistryReadError("REGISTRY_DRIFT")
record_index[key] = record
replaced: dict[tuple[str, str], tuple[str, str]] = {}
for item in supersessions:
if not isinstance(item, Mapping) or set(item) != {"manifest_id", "prior_revision", "new_revision", "evidence_ref"}:
raise RegistryReadError("REGISTRY_INVALID")
try:
manifest_id = _text(item["manifest_id"], "manifest_id")
prior = _text(item["prior_revision"], "prior_revision")
newer = _text(item["new_revision"], "new_revision")
_text(item["evidence_ref"], "evidence_ref")
except ValueError as exc:
raise RegistryReadError("REGISTRY_INVALID") from exc
prior_key, new_key = (manifest_id, prior), (manifest_id, newer)
if prior_key not in record_index or new_key not in record_index or prior_key == new_key or prior_key in replaced:
raise RegistryReadError("REGISTRY_DRIFT")
replaced[prior_key] = new_key
candidates = [record for record in records if isinstance(record, Mapping) and record.get("manifest_id") == self.manifest_id]
if not candidates: raise RegistryReadError("REGISTRY_DRIFT")
current = [record for record in candidates if (self.manifest_id, record.get("revision")) not in replaced]
if len(current) != 1: raise RegistryReadError("REGISTRY_DRIFT")
record = current[0]; required = {"manifest_id", "revision", "capabilities", "executor_refs", "source_oid", "declaration_digest", "allowed_scopes"}
if set(record) != required: raise RegistryReadError("REGISTRY_INVALID")
unsigned = {key: value for key, value in record.items() if key != "declaration_digest"}
if hashlib.sha256(_canonical(unsigned)).hexdigest() != _digest(record["declaration_digest"], "declaration_digest"): raise RegistryReadError("REGISTRY_DRIFT")
try:
manifest = StaticCapabilityManifest(str(record["manifest_id"]), str(record["revision"]), tuple(CapabilityRef(CapabilityNamespace(item["namespace"]), item["token"]) for item in record["capabilities"]), tuple(record["executor_refs"]), str(record["source_oid"]), str(record["declaration_digest"]), tuple(ScopeSpec(**item) for item in record["allowed_scopes"]))
except (KeyError, TypeError, ValueError) as exc: raise RegistryReadError("REGISTRY_INVALID") from exc
self._last_registry = registry
return manifest
def read_supersession(self, *, manifest_id: str, revision: str) -> str | None:
_text(manifest_id, "manifest_id"); _text(revision, "revision")
if self._last_registry is None:
raise RegistryReadError("REGISTRY_READ_REQUIRES_PINNED_DIGEST")
matches = [
item["new_revision"] for item in self._last_registry["supersessions"]
if item["manifest_id"] == manifest_id and item["prior_revision"] == revision
]
if len(matches) > 1:
raise RegistryReadError("REGISTRY_DRIFT")
return matches[0] if matches else None
@@ -0,0 +1,462 @@
"""Explicit C6 composition in the existing TaskPlane host, default disabled.
All authorities are host-owned dependencies. No request can register an
executor, construct a worker identity, or turn an observation into authority.
"""
from __future__ import annotations
from dataclasses import dataclass
from datetime import datetime, timezone
import threading
from typing import Callable, Literal, Protocol
from aios_capability_contract import has_capabilities as formal_has_capabilities, normalize_required_capabilities
from .aro_readonly import AroReadonlyTick
from .authorization_planner import CapabilityNamespace, CapabilityRef, GitManifestRegistryReader, authorization_scope_digest
from .auto_claim import AutoClaimCoordinator, DiscoveryResult
from .errors import ForbiddenError
from .operation_readback import OperationReaders, collect_closure, collect_observation
from .operation_view import (ClosureObservation, OperationBinding, PreflightResult, ClosureResult,
binding_digest, derive_closure, derive_preflight, reconcile_effect, validate_digest, validate_oid)
from .schedulability import MemoryProofRepository, SchedulabilityService
from .schedulability_store_adapter import (TaskPlaneSchedulabilityAuthority,
TaskPlaneClaimReceiptV3, ScopedLeaseController, validate_task_plane_receipt)
@dataclass(frozen=True)
class AuthenticatedWorker:
worker_id: str
actor_identity: str
authority_receipt_ref: str
expires_at: datetime
def __post_init__(self):
if any(not isinstance(v, str) or not v.strip() for v in
(self.worker_id, self.actor_identity, self.authority_receipt_ref)):
raise ValueError("authenticated worker receipt required")
if not isinstance(self.expires_at, datetime) or self.expires_at.tzinfo is None:
raise ValueError("worker expiry must be timezone aware")
@dataclass(frozen=True)
class OperationCapabilityBinding:
operation_id: str
namespace: CapabilityNamespace
capability_id: str
registry_oid: str
manifest_id: str
manifest_revision: str
def __post_init__(self):
validate_oid(self.registry_oid)
if not isinstance(self.namespace, CapabilityNamespace):
raise ValueError("explicit capability namespace required")
if any(not isinstance(v, str) or not v.strip() for v in
(self.operation_id, self.capability_id, self.manifest_id, self.manifest_revision)):
raise ValueError("operation capability identity required")
@dataclass(frozen=True)
class DurableExecutionIntent:
binding_digest: str
effect_key: str
claim_id: str
receipt_ref: str
def __post_init__(self):
validate_digest(self.binding_digest)
if any(not isinstance(v, str) or not v.strip() for v in
(self.effect_key, self.claim_id, self.receipt_ref)):
raise ValueError("exact durable execution intent required")
class GovernedSourcePatchPilot(Protocol):
"""Existing governed writer/executor adapter, never a general shell port.
Implementation must revalidate Root, revocation, scope, generation and
WriterLease/CAS at write linearization, and persist intent before effect.
Readback uses the same immutable effect key through OperationReaders.
"""
def prepare_intent(self, *, binding: OperationBinding, claim: TaskPlaneClaimReceiptV3) -> DurableExecutionIntent: ...
def execute(self, *, binding: OperationBinding, claim: TaskPlaneClaimReceiptV3,
intent: DurableExecutionIntent) -> None: ...
def reconcile(self, *, binding: OperationBinding,
intent: DurableExecutionIntent) -> None: ...
@dataclass(frozen=True)
class C6Bindings:
claim_authority: TaskPlaneSchedulabilityAuthority
registry_reader: GitManifestRegistryReader
registry_oid: str
registry_digest: str
operation_capabilities: tuple[OperationCapabilityBinding, ...]
operation_readers: OperationReaders
worker_provider: Callable[[], AuthenticatedWorker | None]
operation_provider: Callable[[str, AuthenticatedWorker], OperationBinding]
pilot: GovernedSourcePatchPilot | None = None
aro_tick: AroReadonlyTick | None = None
enabled: bool = False
request_worker_provider: Callable[[str, str], AuthenticatedWorker | None] | None = None
# Readers are injected from the host's existing durable intent and worker
# authority. A provider error is UNKNOWN, never an absent record.
intent_reader: Callable[[OperationBinding], DurableExecutionIntent | None] | None = None
pending_intent_provider: Callable[
[], tuple[tuple[OperationBinding, DurableExecutionIntent], ...]
] | None = None
pending_intent_completion: Callable[
[OperationBinding, DurableExecutionIntent, ClosureObservation], None
] | None = None
worker_authority_verifier: Callable[[AuthenticatedWorker, OperationBinding], bool] | None = None
@dataclass(frozen=True)
class C6TickResult:
status: Literal["DISABLED", "NO_CANDIDATES", "CLAIMED", "OBSERVED", "BLOCKED", "UNKNOWN"]
reason_code: str
discovery: DiscoveryResult | None = None
claim: TaskPlaneClaimReceiptV3 | None = None
preflight: PreflightResult | None = None
closure: ClosureResult | None = None
audit_report_ref: str | None = None
evidence_refs: tuple[str, ...] = ()
class C6Runtime:
def __init__(self, service, bindings: C6Bindings, *, clock=None):
if not isinstance(bindings, C6Bindings) or type(bindings.enabled) is not bool:
raise ValueError("explicit C6 bindings required")
if not isinstance(bindings.claim_authority, TaskPlaneSchedulabilityAuthority) or bindings.claim_authority.store is not service.store:
raise ValueError("C6 must share the host's existing TaskPlane authority")
if not callable(bindings.worker_provider) or not callable(bindings.operation_provider):
raise ValueError("host authenticated worker and operation providers required")
if bindings.enabled and not service.policy.execution_enabled:
raise ValueError("execution policy and explicit trust must be enabled first")
if bindings.pilot is not None and not all(callable(getattr(bindings.pilot, method, None)) for method in ("prepare_intent", "execute", "reconcile")):
raise ValueError("governed source patch pilot required")
if bindings.enabled and bindings.pilot is not None and not all(
callable(value)
for value in (
bindings.intent_reader,
bindings.pending_intent_provider,
bindings.pending_intent_completion,
bindings.worker_authority_verifier,
)
):
raise ValueError(
"enabled C6 pilot requires intent, recovery, and worker authority providers"
)
validate_oid(bindings.registry_oid); validate_digest(bindings.registry_digest)
if not isinstance(bindings.operation_capabilities, tuple) or not bindings.operation_capabilities:
raise ValueError("immutable operation capability map required")
self.service, self.bindings = service, bindings
self.clock = clock or (lambda: datetime.now(timezone.utc))
self._lock = threading.Lock()
self._closed = False
self._pending: OperationBinding | None = None
self._validate_map()
self.schedulability = SchedulabilityService(bindings.claim_authority,
MemoryProofRepository(), clock=lambda: self.clock().timestamp(),
capability_matcher=bindings.claim_authority.capability_matcher)
self.coordinator = AutoClaimCoordinator(self.schedulability)
self.lease_controller = ScopedLeaseController(service.store, bindings.claim_authority.binding)
def _validate_map(self):
manifest = self.bindings.registry_reader.read_manifest(
expected_registry_oid=self.bindings.registry_oid,
expected_manifest_sha256=self.bindings.registry_digest)
seen = set()
for item in self.bindings.operation_capabilities:
key = (item.operation_id, item.namespace)
if key in seen or item.registry_oid != self.bindings.registry_oid or item.manifest_id != manifest.manifest_id or item.manifest_revision != manifest.revision:
raise ValueError("operation capability map revision conflict")
if CapabilityRef(item.namespace, item.capability_id) not in manifest.capabilities:
raise ValueError("operation capability not declared in authoritative manifest")
if item.namespace == CapabilityNamespace.AGENT_TASK and any(
value.startswith("exact:") for value in normalize_required_capabilities([item.capability_id])):
raise ValueError("unknown formal operation capability")
seen.add(key)
return manifest
def _worker(self, worker=None):
worker = self.bindings.worker_provider() if worker is None else worker
if not isinstance(worker, AuthenticatedWorker) or worker.expires_at <= self.clock():
raise ForbiddenError("fresh authenticated worker required")
registered = self.service.store.get_worker(worker.worker_id)
if registered.identity != worker.actor_identity or registered.disabled:
raise ForbiddenError("registered worker identity mismatch")
self.service.policy.assert_allowed("claim", worker.actor_identity)
return worker
def _prepare(self, task_id, worker):
manifest = self._validate_map()
binding = self.bindings.operation_provider(task_id, worker)
if not isinstance(binding, OperationBinding) or binding.task_id != task_id or binding.actor_id != worker.actor_identity or binding.source_oid != manifest.source_oid:
raise ForbiddenError("operation identity mismatch")
if binding.operation_id != "governed_source_patch_apply":
raise ForbiddenError("operation outside C6 pilot")
task = self.service.store.get_task(task_id)
metadata = task.task_plan.metadata
if (metadata.get("c6_operation_binding_digest") != binding_digest(binding)
or metadata.get("generation") != binding.generation
or binding.policy_digest != manifest.declaration_digest
or binding.scope_digest not in {authorization_scope_digest(scope) for scope in manifest.allowed_scopes}):
raise ForbiddenError("operation scope or immutable TaskPlan binding mismatch")
self.service.policy.assert_allowed("claim", worker.actor_identity, task=task)
required = task.task_plan.required_capabilities
if binding.capability_namespace == CapabilityNamespace.AGENT_TASK.value:
registered = self.service.store.get_worker(worker.worker_id)
if (not formal_has_capabilities(registered.capabilities, [binding.capability_id])
or not normalize_required_capabilities([binding.capability_id]).issubset(
normalize_required_capabilities(required))):
raise ForbiddenError("formal capability cannot use wildcard worker authority")
elif binding.capability_id not in required:
raise ForbiddenError("operation capability absent from TaskPlan")
verifier = self.bindings.worker_authority_verifier
if verifier is None or verifier(worker, binding) is not True:
raise ForbiddenError("worker receipt not bound to operation authority")
if not any(item.operation_id == binding.operation_id and item.namespace.value == binding.capability_namespace and item.capability_id == binding.capability_id for item in self.bindings.operation_capabilities):
raise ForbiddenError("operation capability namespace mismatch")
preflight = derive_preflight(collect_observation(binding, self.bindings.operation_readers, now=self.clock()), now=self.clock())
return binding, preflight
def _intent(self, binding):
reader = self.bindings.intent_reader
if reader is None:
raise ForbiddenError("durable execution intent reader required")
intent = reader(binding)
if intent is not None and (not isinstance(intent, DurableExecutionIntent)
or intent.binding_digest != binding_digest(binding)
or intent.effect_key != binding.effect_key):
raise ForbiddenError("durable execution intent binding mismatch")
return intent
def _durable_pending(self):
provider = self.bindings.pending_intent_provider
if provider is None:
raise ForbiddenError("durable pending intent provider required")
values = provider()
if not isinstance(values, tuple) or len(values) > 16:
raise ForbiddenError("bounded durable pending intent enumeration required")
result = []
seen = set()
for value in values:
if not isinstance(value, tuple) or len(value) != 2:
raise ForbiddenError("durable pending intent entry is invalid")
binding, intent = value
if (
not isinstance(binding, OperationBinding)
or not isinstance(intent, DurableExecutionIntent)
or intent.binding_digest != binding_digest(binding)
or intent.effect_key != binding.effect_key
or binding.effect_key in seen
):
raise ForbiddenError("durable pending intent binding mismatch")
seen.add(binding.effect_key)
result.append((binding, intent))
return tuple(result)
def _reconcile_pending(self, binding, intent, worker):
current, _preflight = self._prepare(binding.task_id, worker)
if current != binding or self._intent(binding) != intent:
raise ForbiddenError("durable pending authority changed")
assert self.bindings.pilot is not None
try:
self.bindings.pilot.reconcile(binding=binding, intent=intent)
except Exception:
pass
observation = collect_closure(
binding, self.bindings.operation_readers, now=self.clock()
)
return observation, derive_closure(binding, observation, now=self.clock())
def renew_claim(self, *, task_id, claim_id, actor_identity, receipt, lease_seconds):
if self._closed or not self.bindings.enabled or not isinstance(receipt, dict):
raise ForbiddenError("C6 v3 claim receipt required")
if receipt.get("task_id") != task_id or receipt.get("claim_id") != claim_id:
raise ForbiddenError("C6 claim receipt identity mismatch")
provider = self.bindings.request_worker_provider
worker_id = receipt.get("worker_id")
candidate = provider(worker_id, actor_identity) if provider is not None else None
if candidate is None:
raise ForbiddenError("authenticated request worker required")
worker = self._worker(candidate)
if (worker.worker_id, worker.actor_identity) != (worker_id, actor_identity):
raise ForbiddenError("request worker identity mismatch")
self._prepare(task_id, worker)
return self.lease_controller.renew(receipt, worker_id=worker_id,
actor_identity=actor_identity, lease_seconds=lease_seconds)
def claim_task(self, *, task_id: str, worker_id: str, actor_identity: str, lease_seconds: int = 30):
"""Manual and automatic host claims share this exact authority seam."""
if self._closed or not self.bindings.enabled or self.bindings.pilot is None:
raise ForbiddenError("C6 claim binding disabled or unavailable")
provider = self.bindings.request_worker_provider
if provider is None:
raise ForbiddenError("authenticated request worker provider required")
candidate = provider(worker_id, actor_identity)
if candidate is None:
raise ForbiddenError("authenticated request worker unavailable")
worker = self._worker(candidate)
if (worker.worker_id, worker.actor_identity) != (worker_id, actor_identity):
raise ForbiddenError("worker provider binding mismatch")
binding, preflight = self._prepare(task_id, worker)
return self._claim_prepared(binding, preflight, worker, lease_seconds=lease_seconds)
def _claim_prepared(self, binding, preflight, worker, *, lease_seconds=30):
task_id, worker_id, actor_identity = binding.task_id, worker.worker_id, worker.actor_identity
if preflight.state != "READY":
raise ForbiddenError("C6 preflight: " + preflight.reason_code)
if self._intent(binding) is not None:
raise ForbiddenError("existing execution intent requires readback")
closure = collect_closure(binding, self.bindings.operation_readers, now=self.clock())
if reconcile_effect(closure.effect, binding=binding, now=self.clock()) != "REVALIDATE_BEFORE_RETRY":
raise ForbiddenError("C6 effect must be authoritatively absent before claim")
proof = self.schedulability.preflight(task_id=task_id, worker_id=worker_id,
actor_identity=actor_identity, persist=True)
if proof.observation.scope_digest != binding.scope_digest:
raise ForbiddenError("claim authority scope differs from operation scope")
result = self.coordinator.claim(proof_id=proof.proof_id, worker_id=worker_id,
actor_identity=actor_identity, lease_seconds=lease_seconds)
parsed = validate_task_plane_receipt(result, proof=proof)
return parsed
def tick(self) -> C6TickResult:
if self._closed or not self.bindings.enabled:
return C6TickResult("DISABLED", "C6_DISABLED")
if not self._lock.acquire(blocking=False):
return C6TickResult("BLOCKED", "LOCAL_TICK_IN_PROGRESS")
try:
audit = self.bindings.aro_tick.run_due() if self.bindings.aro_tick else None
if self.bindings.pilot is None:
return C6TickResult("BLOCKED", "EXECUTION_ENTRYPOINT_ABSENT", audit_report_ref=audit)
if self.bindings.pending_intent_provider is None:
return C6TickResult(
"BLOCKED", "DURABLE_PENDING_INTENT_PROVIDER_ABSENT",
audit_report_ref=audit,
)
# Restart recovery is ledger-first. It does not depend on the Task
# remaining discoverable and it never calls execute/resubmits.
pending = self._durable_pending()
if pending:
worker = self._worker()
closures = []
for binding, intent in pending:
observation, closed = self._reconcile_pending(binding, intent, worker)
closures.append(closed)
if closed.outcome == "SUCCEEDED":
assert self.bindings.pending_intent_completion is not None
self.bindings.pending_intent_completion(
binding, intent, observation
)
all_succeeded = all(item.outcome == "SUCCEEDED" for item in closures)
return C6TickResult(
"OBSERVED" if all_succeeded else "UNKNOWN",
"DURABLE_PENDING_INTENTS_RECONCILED"
if all_succeeded
else "DURABLE_PENDING_INTENT_READBACK",
closure=closures[-1],
audit_report_ref=audit,
)
if self._pending is not None:
# ACK loss is sticky. Reconciliation is a distinct, pure
# owner seam: it may observe/reconcile the exact durable action
# but may never acquire a new WriterLease or resend the commit.
intent = self._intent(self._pending)
if intent is None:
return C6TickResult("UNKNOWN", "PENDING_INTENT_UNAVAILABLE",
audit_report_ref=audit)
worker = self._worker()
observation, closure = self._reconcile_pending(
self._pending, intent, worker
)
if closure.outcome == "SUCCEEDED":
assert self.bindings.pending_intent_completion is not None
self.bindings.pending_intent_completion(
self._pending, intent, observation
)
self._pending = None
return C6TickResult("OBSERVED" if closure.outcome == "SUCCEEDED" else "UNKNOWN",
"PENDING_EFFECT_READBACK", closure=closure, audit_report_ref=audit)
worker = self._worker()
discovered = self.coordinator.discover(worker_id=worker.worker_id, actor_identity=worker.actor_identity)
if discovered.proof is None:
return C6TickResult("NO_CANDIDATES" if discovered.candidate_count == 0 else "BLOCKED",
discovered.status, discovery=discovered, audit_report_ref=audit)
task_id = discovered.proof.task_id
binding, preflight = self._prepare(task_id, worker)
if self._intent(binding) is not None:
self._pending = binding
intent = self._intent(binding)
try:
if intent is not None:
self.bindings.pilot.reconcile(binding=binding, intent=intent)
except Exception:
pass
closed = derive_closure(binding, collect_closure(binding,
self.bindings.operation_readers, now=self.clock()), now=self.clock())
return C6TickResult("OBSERVED" if closed.outcome == "SUCCEEDED" else "UNKNOWN",
"DURABLE_INTENT_READBACK", discovered, closure=closed, audit_report_ref=audit)
if preflight.state != "READY":
return C6TickResult("BLOCKED", preflight.reason_code, discovered, preflight=preflight, audit_report_ref=audit)
claim = self._claim_prepared(binding, preflight, worker)
# Re-read every precondition immediately before the effect. The
# executor independently enforces its linearization-point fences.
binding_after, refreshed = self._prepare(task_id, worker)
self._worker(worker)
if binding_after != binding or refreshed.state != "READY" or self._closed:
return C6TickResult("BLOCKED", "PRE_EFFECT_BINDING_CHANGED", discovered, claim, refreshed)
try:
with self.lease_controller.execution_guard(claim.to_dict(),
worker_id=worker.worker_id, actor_identity=worker.actor_identity):
binding_after, refreshed = self._prepare(task_id, self._worker(worker))
effect = collect_closure(binding, self.bindings.operation_readers, now=self.clock()).effect
if (binding_after != binding or refreshed.state != "READY" or self._closed
or reconcile_effect(effect, binding=binding, now=self.clock()) != "REVALIDATE_BEFORE_RETRY"
or self._intent(binding) is not None):
return C6TickResult("BLOCKED", "PRE_EFFECT_AUTHORITY_CHANGED", discovered, claim, refreshed)
self._pending = binding
intent = self.bindings.pilot.prepare_intent(binding=binding, claim=claim)
observed_intent = self._intent(binding)
if (not isinstance(intent, DurableExecutionIntent) or intent != observed_intent
or intent.claim_id != claim.claim_id):
raise ForbiddenError("execution intent not durably observed")
# Intent persistence may take time: acquire a new guard and
# read authority/effect again before calling the governed writer.
with self.lease_controller.execution_guard(claim.to_dict(),
worker_id=worker.worker_id, actor_identity=worker.actor_identity):
after, ready = self._prepare(task_id, self._worker(worker))
effect = collect_closure(binding, self.bindings.operation_readers, now=self.clock()).effect
if (after != binding or ready.state != "READY" or self._closed
or self._intent(binding) != intent
or reconcile_effect(effect, binding=binding, now=self.clock()) != "REVALIDATE_BEFORE_RETRY"):
raise ForbiddenError("pre-effect authority changed during intent persistence")
self.bindings.pilot.execute(binding=binding, claim=claim, intent=intent)
except Exception:
return C6TickResult("UNKNOWN", "EXECUTION_OUTCOME_UNKNOWN", discovered, claim, refreshed)
observed = collect_closure(binding, self.bindings.operation_readers, now=self.clock())
closed = derive_closure(binding, observed, now=self.clock())
if closed.outcome == "SUCCEEDED":
assert self.bindings.pending_intent_completion is not None
self.bindings.pending_intent_completion(binding, intent, observed)
self._pending = None
return C6TickResult("OBSERVED" if closed.outcome == "SUCCEEDED" else "UNKNOWN",
"THREE_LAYER_CLOSURE" if closed.outcome == "SUCCEEDED" else "CLOSURE_INCOMPLETE",
discovered, claim, refreshed, closed, audit)
except Exception:
return C6TickResult("UNKNOWN", "AUTHORITY_OR_OBSERVATION_UNAVAILABLE")
finally:
self._lock.release()
def close(self):
# Local tick lifecycle only. Authority leases are never implicitly
# released or renewed by closing an observer/coordinator.
self._closed = True
def bind_c6(service, bindings: C6Bindings, *, clock=None) -> C6Runtime:
if getattr(service, "c6_runtime", None) is not None:
raise ValueError("C6 runtime already bound")
runtime = C6Runtime(service, bindings, clock=clock)
service.c6_runtime = runtime
return runtime
@@ -0,0 +1,121 @@
"""Pure six-value C6 decision explanation policy.
The result is advice over already-read facts. It is not an authorization,
does not map implicitly to legacy NextAction, and owns no write capability.
"""
from __future__ import annotations
from dataclasses import dataclass
from enum import Enum
from typing import Literal
RootState = Literal["VALID", "MISSING", "EXPIRED", "REVOKED", "UNKNOWN"]
class DecisionKind(str, Enum):
AUTO_FIX = "AUTO_FIX"
PLAN_ONLY = "PLAN_ONLY"
OWNER_APPROVAL_REQUIRED = "OWNER_APPROVAL_REQUIRED"
HARD_BLOCK = "HARD_BLOCK"
WAITING_ROOT_AUTHORIZATION = "WAITING_ROOT_AUTHORIZATION"
BLOCKED_WITH_EVIDENCE = "BLOCKED_WITH_EVIDENCE"
def _evidence_refs(value: object) -> tuple[str, ...]:
if not isinstance(value, tuple):
raise ValueError("evidence_refs must be a tuple")
for ref in value:
if not isinstance(ref, str) or not ref or ref != ref.strip() or "\x00" in ref:
raise ValueError("invalid evidence reference")
return value
@dataclass(frozen=True)
class DecisionInput:
root_state: RootState
facts_complete: bool
scope_covered: bool
policy_denied: bool
proposal_only: bool
executor_ready: bool
aro_phase_a: bool
evidence_refs: tuple[str, ...]
def __post_init__(self) -> None:
if self.root_state not in ("VALID", "MISSING", "EXPIRED", "REVOKED", "UNKNOWN"):
raise ValueError("invalid root_state")
for name in (
"facts_complete",
"scope_covered",
"policy_denied",
"proposal_only",
"executor_ready",
"aro_phase_a",
):
if type(getattr(self, name)) is not bool:
raise ValueError(f"{name} must be bool")
_evidence_refs(self.evidence_refs)
@dataclass(frozen=True)
class Decision:
kind: DecisionKind
reason: str
evidence_refs: tuple[str, ...]
def __post_init__(self) -> None:
if not isinstance(self.kind, DecisionKind):
raise ValueError("kind must be DecisionKind")
if not isinstance(self.reason, str) or not self.reason:
raise ValueError("reason must be non-empty")
_evidence_refs(self.evidence_refs)
def decide(value: DecisionInput) -> Decision:
if not isinstance(value, DecisionInput):
raise TypeError("value must be DecisionInput")
if value.policy_denied or value.root_state == "REVOKED":
return Decision(
DecisionKind.HARD_BLOCK,
"POLICY_DENIED_OR_ROOT_REVOKED",
value.evidence_refs,
)
if not value.facts_complete or value.root_state == "UNKNOWN":
return Decision(
DecisionKind.BLOCKED_WITH_EVIDENCE,
"CRITICAL_FACTS_INCOMPLETE",
value.evidence_refs,
)
if value.root_state in ("MISSING", "EXPIRED"):
return Decision(
DecisionKind.WAITING_ROOT_AUTHORIZATION,
"ROOT_AUTHORIZATION_MISSING_OR_EXPIRED",
value.evidence_refs,
)
if not value.scope_covered:
return Decision(
DecisionKind.OWNER_APPROVAL_REQUIRED,
"SCOPE_OR_BUDGET_REQUIRES_OWNER_DECISION",
value.evidence_refs,
)
if value.proposal_only or value.aro_phase_a:
return Decision(
DecisionKind.PLAN_ONLY,
"NON_EXECUTABLE_PLAN_OR_READONLY_ARO",
value.evidence_refs,
)
if not value.executor_ready:
return Decision(
DecisionKind.BLOCKED_WITH_EVIDENCE,
"GOVERNED_EXECUTOR_NOT_READY",
value.evidence_refs,
)
return Decision(
DecisionKind.AUTO_FIX,
"EXISTING_AUTHORIZED_SCOPE_ELIGIBLE_FOR_GOVERNED_EXECUTION",
value.evidence_refs,
)
__all__ = ["Decision", "DecisionInput", "DecisionKind", "decide"]
+39 -2
View File
@@ -5,6 +5,7 @@ from __future__ import annotations
import hashlib
import json
from .models import EvidenceEnvelope
from .store import TaskPlaneStore
@@ -17,8 +18,44 @@ class EvidenceService:
def __init__(self, store: TaskPlaneStore):
self.store = store
def add(self, task_id: str, kind: str, payload: dict, worker_identity: str) -> str:
return self.store.add_evidence(task_id, kind, payload, worker_identity)
def add(
self,
task_id: str,
kind: str,
payload: dict,
worker_identity: str,
*,
envelope: EvidenceEnvelope | None = None,
claim_id: str | None = None,
evidence_id: str | None = None,
created_at: float | None = None,
expires_at: float | None = None,
signature: str | None = None,
attestation_ref: str | None = None,
signature_algorithm: str | None = None,
signing_key_id: str | None = None,
verifier_identity: str | None = None,
authority_domain: str | None = None,
sequence: int | None = None,
) -> str:
return self.store.add_evidence(
task_id,
kind,
payload,
worker_identity,
envelope=envelope,
claim_id=claim_id,
evidence_id=evidence_id,
created_at=created_at,
expires_at=expires_at,
signature=signature,
attestation_ref=attestation_ref,
signature_algorithm=signature_algorithm,
signing_key_id=signing_key_id,
verifier_identity=verifier_identity,
authority_domain=authority_domain,
sequence=sequence,
)
def list(self, task_id: str) -> list[dict]:
return self.store.list_evidence(task_id)
@@ -0,0 +1,147 @@
"""Authenticated Task Plane evidence envelope adapter.
The adapter deliberately receives key material through an external resolver.
It does not create, persist, or select a production trust root.
"""
from __future__ import annotations
import hashlib
import hmac
import math
import re
import time
from dataclasses import replace
from typing import Callable
from .models import EvidenceEnvelope, canonical_json, compute_sha256_hex
HMAC_SHA256_V1 = "hmac-sha256-v1"
_SIGNATURE_DOMAIN = b"aios.task-plane.evidence-envelope.v1"
_HEX64 = re.compile(r"^[0-9a-f]{64}$")
def _signature_payload(envelope: EvidenceEnvelope) -> bytes:
return (
_SIGNATURE_DOMAIN
+ b"\0"
+ canonical_json(envelope.digest_payload()).encode("utf-8")
)
class HmacEvidenceVerifier:
"""Verify full evidence envelopes using an externally managed HMAC key."""
def __init__(
self,
*,
verifier_identity: str,
key_id: str,
key_resolver: Callable[[str], bytes],
max_age_seconds: int = 300,
now_fn: Callable[[], float] | None = None,
) -> None:
if not verifier_identity or not key_id:
raise ValueError("verifier_identity and key_id are required")
if not callable(key_resolver):
raise ValueError("key_resolver is required")
if (
isinstance(max_age_seconds, bool)
or not isinstance(max_age_seconds, int)
or max_age_seconds <= 0
):
raise ValueError("max_age_seconds must be a positive integer")
self.verifier_identity = verifier_identity
self.key_id = key_id
self._key_resolver = key_resolver
self.max_age_seconds = max_age_seconds
self._now_fn = now_fn or time.time
def __call__(self, envelope: EvidenceEnvelope) -> bool:
try:
now = float(self._now_fn())
finite_timestamps = all(
math.isfinite(value)
for value in (now, envelope.created_at, envelope.expires_at)
)
expected_digest = compute_sha256_hex(envelope.digest_payload())
except (TypeError, ValueError, OverflowError):
return False
if (
not finite_timestamps
or envelope.expires_at <= envelope.created_at
or envelope.signature_algorithm != HMAC_SHA256_V1
or envelope.signing_key_id != self.key_id
or envelope.verifier_identity != self.verifier_identity
or envelope.authority_domain != self.verifier_identity
or not isinstance(envelope.sequence, int)
or isinstance(envelope.sequence, bool)
or envelope.sequence < 1
or not isinstance(envelope.signature, str)
or not _HEX64.fullmatch(envelope.signature)
or envelope.created_at > now
or envelope.expires_at <= now
or envelope.expires_at - envelope.created_at > self.max_age_seconds
or envelope.evidence_digest != expected_digest
):
return False
try:
key = self._key_resolver(self.key_id)
except Exception:
return False
if not isinstance(key, (bytes, bytearray)) or not key:
return False
expected = hmac.new(
bytes(key), _signature_payload(envelope), hashlib.sha256
).hexdigest()
return hmac.compare_digest(envelope.signature, expected)
def sign_evidence_envelope(
envelope: EvidenceEnvelope,
*,
key_id: str,
key: bytes,
verifier_identity: str,
) -> EvidenceEnvelope:
"""Return a signed envelope for an already authorized producer.
Production code should obtain ``key`` from its existing credential provider;
this helper intentionally performs no key generation or storage.
"""
if not isinstance(key, (bytes, bytearray)) or not key:
raise ValueError("key is required")
if not key_id or not verifier_identity:
raise ValueError("key_id and verifier_identity are required")
if envelope.authority_domain != verifier_identity:
raise ValueError("envelope authority_domain must match verifier_identity")
if not math.isfinite(envelope.created_at) or not math.isfinite(
envelope.expires_at
):
raise ValueError("evidence timestamps must be finite")
if envelope.expires_at <= envelope.created_at:
raise ValueError("evidence expiry must follow creation")
if (
not isinstance(envelope.sequence, int)
or isinstance(envelope.sequence, bool)
or envelope.sequence < 1
):
raise ValueError("evidence sequence must be a positive integer")
unsigned = replace(
envelope,
signature=None,
signature_algorithm=HMAC_SHA256_V1,
signing_key_id=key_id,
verifier_identity=verifier_identity,
evidence_digest="",
)
unsigned = replace(
unsigned,
evidence_digest=compute_sha256_hex(unsigned.digest_payload()),
)
signature = hmac.new(
bytes(key), _signature_payload(unsigned), hashlib.sha256
).hexdigest()
return replace(unsigned, signature=signature)
@@ -11,7 +11,7 @@ from contextlib import closing
from dataclasses import asdict, dataclass, field
from enum import Enum
from pathlib import Path
from typing import Any, Callable, Iterable, Mapping, Sequence
from typing import Any, Callable, Iterable, Literal, Mapping, Sequence
SCHEMA_RUNTIME_MANIFEST = "aios.runtime-manifest.v1"
@@ -57,6 +57,13 @@ class ConcurrentWriterDenied(GovernanceControlError):
pass
class MutationOwnershipError(GovernanceControlError):
code = "MUTATION_TICKET_NOT_OWNED"
def __init__(self) -> None:
super().__init__(self.code)
class InvalidStateTransition(GovernanceControlError):
pass
@@ -631,6 +638,7 @@ class BlockerClassification(str, Enum):
TRANSIENT = "TRANSIENT"
OWNER_BOUNDARY = "OWNER_BOUNDARY"
SELF_ACTIONABLE = "SELF_ACTIONABLE"
AUTHORIZATION_DENIED = "AUTHORIZATION_DENIED"
class ProducerKind(str, Enum):
@@ -648,6 +656,70 @@ class NextAction(str, Enum):
RECOVER_EXISTING_CHILD = "RECOVER_EXISTING_CHILD"
OWNER_BOUNDARY = "OWNER_BOUNDARY"
REPAIR_MECHANISM = "REPAIR_MECHANISM"
DENY_EFFECT = "DENY_EFFECT"
class DecisionEffectIntent(str, Enum):
"""Whether this decision can lead to a new side effect.
UNSPECIFIED retains the advisory behavior of legacy callers. Production
callers that may cause a write must pass SIDE_EFFECT and authoritative
reader results; this value is not an authorization token.
"""
UNSPECIFIED = "UNSPECIFIED"
READ_ONLY = "READ_ONLY"
SIDE_EFFECT = "SIDE_EFFECT"
class AuthorityLifecycleState(str, Enum):
ACTIVE = "ACTIVE"
REVOKED = "REVOKED"
CLOSED = "CLOSED"
CANCELLED = "CANCELLED"
UNKNOWN = "UNKNOWN"
class ExactScopeState(str, Enum):
IN_SCOPE = "IN_SCOPE"
OUT_OF_SCOPE = "OUT_OF_SCOPE"
UNKNOWN = "UNKNOWN"
class OwnerBoundaryReason(str, Enum):
GOAL_CHANGE = "GOAL_CHANGE"
TRUST_BOUNDARY_CHANGE = "TRUST_BOUNDARY_CHANGE"
IRREVERSIBLE_OPERATION = "IRREVERSIBLE_OPERATION"
BUSINESS_RISK_BUDGET_EXCEEDED = "BUSINESS_RISK_BUDGET_EXCEEDED"
@dataclass(frozen=True)
class AuthorityDecisionContext:
"""Read-only projection of current authority facts used for routing.
The context must be populated from authority readers. It carries no write
capability and cannot replace the executor's final authority/fencing check.
"""
lifecycle_state: AuthorityLifecycleState
scope_state: ExactScopeState
owner_boundary_reason: OwnerBoundaryReason | None = None
evidence_refs: tuple[str, ...] = ()
def __post_init__(self) -> None:
if not isinstance(self.lifecycle_state, AuthorityLifecycleState):
raise ValueError("lifecycle_state must be AuthorityLifecycleState")
if not isinstance(self.scope_state, ExactScopeState):
raise ValueError("scope_state must be ExactScopeState")
if self.owner_boundary_reason is not None and not isinstance(
self.owner_boundary_reason, OwnerBoundaryReason
):
raise ValueError("owner_boundary_reason must be OwnerBoundaryReason")
if not isinstance(self.evidence_refs, tuple) or any(
not isinstance(ref, str) or not ref or ref != ref.strip() or "\x00" in ref
for ref in self.evidence_refs
):
raise ValueError("invalid authority evidence reference")
@dataclass(frozen=True)
@@ -671,12 +743,12 @@ DEFAULT_PRODUCER_TABLE: Mapping[str, ProducerBinding] = {
"EXPECTED_BRANCH_OID_UNRESOLVED": ProducerBinding(BlockerClassification.EXTERNAL_DEPENDENCY, ProducerKind.AUTHORITY_SIDE, "governed-bridge-provisioner", "PROVISION_CANDIDATE_BINDING"),
"GENERATION_UNRESOLVED": ProducerBinding(BlockerClassification.EXTERNAL_DEPENDENCY, ProducerKind.AUTHORITY_SIDE, "governed-bridge-provisioner", "PROVISION_CANDIDATE_BINDING"),
"TARGET_BRANCH_UNRESOLVED": ProducerBinding(BlockerClassification.EXTERNAL_DEPENDENCY, ProducerKind.AUTHORITY_SIDE, "governed-bridge-provisioner", "PROVISION_CANDIDATE_BINDING"),
"FILE_ALLOWLIST_MISSING": ProducerBinding(BlockerClassification.OWNER_BOUNDARY, ProducerKind.OWNER, "owner-delegation-epoch", "ISSUE_OWNER_EPOCH"),
"ACCEPTANCE_REVISION_MISSING": ProducerBinding(BlockerClassification.OWNER_BOUNDARY, ProducerKind.OWNER, "owner-delegation-epoch", "ISSUE_OWNER_EPOCH"),
"FILE_ALLOWLIST_MISSING": ProducerBinding(BlockerClassification.MECHANISM_DEFECT, ProducerKind.IMPLEMENTER, "release-contract-builder", "POPULATE_FILE_ALLOWLIST_WITHIN_SCOPE"),
"ACCEPTANCE_REVISION_MISSING": ProducerBinding(BlockerClassification.EXTERNAL_DEPENDENCY, ProducerKind.AUTHORITY_SIDE, "acceptance-authority", "PROVIDE_ACCEPTANCE_REVISION"),
"RECOVERY_RESULT_UNKNOWN": ProducerBinding(BlockerClassification.SELF_ACTIONABLE, ProducerKind.SELF, "agent-recovery-executor", "RECONCILE_BEFORE_RETRY"),
"CAPABILITY_OUT_OF_POLICY": ProducerBinding(BlockerClassification.EXTERNAL_DEPENDENCY, ProducerKind.AUTHORITY_SIDE, "admission-policy-authority", "ALIGN_POLICY_AND_EPOCH"),
"EXECUTION_DIGEST_INVALID": ProducerBinding(BlockerClassification.MECHANISM_DEFECT, ProducerKind.IMPLEMENTER, "candidate-plan-builder", "REGENERATE_CANONICAL_PLAN"),
"TASK_EXPIRED_OWNER_RECOVERY_REQUIRED": ProducerBinding(BlockerClassification.OWNER_BOUNDARY, ProducerKind.OWNER, "agent-task-terminal-recovery", "AUTHORIZE_TERMINAL_RECOVERY_OR_SUCCESSOR"),
"TASK_EXPIRED_OWNER_RECOVERY_REQUIRED": ProducerBinding(BlockerClassification.EXTERNAL_DEPENDENCY, ProducerKind.AUTHORITY_SIDE, "task-mission-authority", "DERIVE_IN_SCOPE_SUCCESSOR"),
"EPOCH_EXPIRED": ProducerBinding(BlockerClassification.SELF_ACTIONABLE, ProducerKind.SELF, "trusted-grant-signer-adapter", "JIT_REISSUE_EPOCH_IF_SCOPE_UNCHANGED"),
}
@@ -693,6 +765,16 @@ class DecisionInput:
descendant_statuses: tuple[str, ...] = ()
unchanged_cycles: int = 0
evidence_refs: tuple[str, ...] = ()
effect_intent: DecisionEffectIntent = DecisionEffectIntent.UNSPECIFIED
authority_context: AuthorityDecisionContext | None = None
def __post_init__(self) -> None:
if not isinstance(self.effect_intent, DecisionEffectIntent):
raise ValueError("effect_intent must be DecisionEffectIntent")
if self.authority_context is not None and not isinstance(
self.authority_context, AuthorityDecisionContext
):
raise ValueError("authority_context must be AuthorityDecisionContext")
@dataclass(frozen=True)
@@ -706,6 +788,7 @@ class GovernanceDecision:
producer_id: str | None
action_required: str | None
evidence_refs: tuple[str, ...]
owner_boundary_reason: OwnerBoundaryReason | None = None
schema: str = SCHEMA_DECISION
def to_dict(self) -> dict[str, Any]:
@@ -713,12 +796,20 @@ class GovernanceDecision:
result["next_valid_action"] = self.next_valid_action.value
result["classification"] = None if self.classification is None else self.classification.value
result["producer_kind"] = None if self.producer_kind is None else self.producer_kind.value
result["owner_boundary_reason"] = (
None if self.owner_boundary_reason is None else self.owner_boundary_reason.value
)
return result
class AutonomousDecisionEngine:
RECOVERY_STATUSES = frozenset({"FAILED_RECOVERABLE", "EXPIRED", "RECOVERY_REQUIRED"})
BLOCKED_STATUSES = frozenset({"BLOCKED", "WAIT_DEPENDENCY", "TERMINAL_UNPROVEN"})
CONTEXTUAL_SCOPE_BLOCKERS = frozenset({
"FILE_ALLOWLIST_MISSING",
"ACCEPTANCE_REVISION_MISSING",
"TASK_EXPIRED_OWNER_RECOVERY_REQUIRED",
})
def __init__(self, producer_table: Mapping[str, ProducerBinding] | None = None):
self.producer_table = dict(DEFAULT_PRODUCER_TABLE)
@@ -733,16 +824,88 @@ class AutonomousDecisionEngine:
def decide(self, inp: DecisionInput) -> GovernanceDecision:
blockers = tuple(dict.fromkeys(inp.blocker_codes))
context = inp.authority_context
side_effect = inp.effect_intent == DecisionEffectIntent.SIDE_EFFECT
if side_effect and context is not None and context.lifecycle_state in {
AuthorityLifecycleState.REVOKED,
AuthorityLifecycleState.CLOSED,
AuthorityLifecycleState.CANCELLED,
}:
return self._decision(
inp,
f"AUTHORITY_{context.lifecycle_state.value}",
ProducerBinding(
BlockerClassification.AUTHORIZATION_DENIED,
ProducerKind.AUTHORITY_SIDE,
"current-authority-reader",
"DENY_NEW_EFFECT",
),
NextAction.DENY_EFFECT,
)
explicit_boundary_reason = (
context.owner_boundary_reason
if side_effect and context is not None
else None
)
scope_boundary = (
side_effect
and context is not None
and context.lifecycle_state == AuthorityLifecycleState.ACTIVE
and context.scope_state == ExactScopeState.OUT_OF_SCOPE
)
if inp.owner_boundary_required or explicit_boundary_reason is not None or scope_boundary:
return self._owner_boundary_decision(
inp,
explicit_boundary_reason
if explicit_boundary_reason is not None
else OwnerBoundaryReason.GOAL_CHANGE
if scope_boundary
else None,
)
if "RECOVERY_RESULT_UNKNOWN" in blockers:
blocker = "RECOVERY_RESULT_UNKNOWN"
return self._decision(
inp, blocker, self._binding(blocker), NextAction.RECOVER_EXISTING_CHILD
)
contextual_blocker = next(
(blocker for blocker in blockers if blocker in self.CONTEXTUAL_SCOPE_BLOCKERS),
None,
)
if contextual_blocker is not None:
if side_effect:
if context is None or context.lifecycle_state == AuthorityLifecycleState.UNKNOWN:
return self._authority_facts_unresolved(inp, contextual_blocker)
if context.scope_state == ExactScopeState.UNKNOWN:
return self._authority_facts_unresolved(inp, contextual_blocker)
if context.scope_state == ExactScopeState.OUT_OF_SCOPE:
return self._owner_boundary_decision(inp, OwnerBoundaryReason.GOAL_CHANGE)
binding = self._binding(contextual_blocker)
next_action = (
NextAction.REPAIR_MECHANISM
if binding.classification == BlockerClassification.MECHANISM_DEFECT
else NextAction.RECOVER_EXISTING_CHILD
if binding.classification == BlockerClassification.SELF_ACTIONABLE
else NextAction.WAIT_DEPENDENCY
)
return self._decision(inp, contextual_blocker, binding, next_action)
for blocker in blockers:
binding = self._binding(blocker)
if binding.classification == BlockerClassification.MECHANISM_DEFECT:
return self._decision(inp, blocker, binding, NextAction.REPAIR_MECHANISM)
if inp.owner_boundary_required:
blocker = blockers[0] if blockers else "OWNER_BOUNDARY"
binding = self._binding(blocker) if blockers else ProducerBinding(
BlockerClassification.OWNER_BOUNDARY, ProducerKind.OWNER, "owner", "OWNER_DECISION_REQUIRED"
)
return self._decision(inp, blocker, binding, NextAction.OWNER_BOUNDARY)
if side_effect:
if context is None or context.lifecycle_state == AuthorityLifecycleState.UNKNOWN:
return self._authority_facts_unresolved(inp)
if context.scope_state == ExactScopeState.UNKNOWN:
return self._authority_facts_unresolved(inp)
if context.scope_state == ExactScopeState.OUT_OF_SCOPE:
return self._owner_boundary_decision(inp, OwnerBoundaryReason.GOAL_CHANGE)
for blocker in blockers:
binding = self._binding(blocker)
if binding.classification == BlockerClassification.OWNER_BOUNDARY:
@@ -766,13 +929,14 @@ class AutonomousDecisionEngine:
owner_boundary=False,blocker_code=None,
classification=BlockerClassification.EXTERNAL_DEPENDENCY,
producer_kind=ProducerKind.AUTHORITY_SIDE,producer_id="admission-authority",
action_required="MAKE_ADMISSION_READY",evidence_refs=inp.evidence_refs,
action_required="MAKE_ADMISSION_READY",evidence_refs=self._evidence_refs(inp),
)
if inp.acceptance_complete:
return GovernanceDecision(
goal_id=inp.goal_id,next_valid_action=NextAction.ROOT_COMPLETE,
owner_boundary=False,blocker_code=None,classification=None,
producer_kind=None,producer_id=None,action_required=None,evidence_refs=inp.evidence_refs,
producer_kind=None,producer_id=None,action_required=None,
evidence_refs=self._evidence_refs(inp),
)
statuses = {str(x).upper() for x in inp.descendant_statuses}
if statuses & self.RECOVERY_STATUSES:
@@ -781,7 +945,7 @@ class AutonomousDecisionEngine:
owner_boundary=False,blocker_code=None,
classification=BlockerClassification.SELF_ACTIONABLE,
producer_kind=ProducerKind.SELF,producer_id="agent-recovery-executor",
action_required="RECOVER_EXISTING_CHILD",evidence_refs=inp.evidence_refs,
action_required="RECOVER_EXISTING_CHILD",evidence_refs=self._evidence_refs(inp),
)
if statuses & self.BLOCKED_STATUSES:
return GovernanceDecision(
@@ -789,14 +953,14 @@ class AutonomousDecisionEngine:
owner_boundary=False,blocker_code=None,
classification=BlockerClassification.EXTERNAL_DEPENDENCY,
producer_kind=ProducerKind.AUTHORITY_SIDE,producer_id="dependency-authority",
action_required="UNBLOCK_DEPENDENCY",evidence_refs=inp.evidence_refs,
action_required="UNBLOCK_DEPENDENCY",evidence_refs=self._evidence_refs(inp),
)
return GovernanceDecision(
goal_id=inp.goal_id,next_valid_action=NextAction.CREATE_OR_RESUME_NEXT_CHILD,
owner_boundary=False,blocker_code=None,
classification=BlockerClassification.SELF_ACTIONABLE,
producer_kind=ProducerKind.SELF,producer_id="autonomy-controller",
action_required="CREATE_OR_RESUME_NEXT_CHILD",evidence_refs=inp.evidence_refs,
action_required="CREATE_OR_RESUME_NEXT_CHILD",evidence_refs=self._evidence_refs(inp),
)
def _decision(self, inp: DecisionInput, blocker: str, binding: ProducerBinding, next_action: NextAction) -> GovernanceDecision:
@@ -805,15 +969,129 @@ class AutonomousDecisionEngine:
owner_boundary=binding.classification == BlockerClassification.OWNER_BOUNDARY,
blocker_code=blocker,classification=binding.classification,
producer_kind=binding.producer_kind,producer_id=binding.producer_id,
action_required=binding.action_required,evidence_refs=inp.evidence_refs,
action_required=binding.action_required,
evidence_refs=self._evidence_refs(inp),
)
def _owner_boundary_decision(
self,
inp: DecisionInput,
reason: OwnerBoundaryReason | None,
) -> GovernanceDecision:
return GovernanceDecision(
goal_id=inp.goal_id,
next_valid_action=NextAction.OWNER_BOUNDARY,
owner_boundary=True,
blocker_code="OWNER_BOUNDARY" if reason is None else reason.value,
classification=BlockerClassification.OWNER_BOUNDARY,
producer_kind=ProducerKind.OWNER,
producer_id="owner",
action_required="OWNER_DECISION_REQUIRED",
evidence_refs=self._evidence_refs(inp),
owner_boundary_reason=reason,
)
@staticmethod
def _evidence_refs(inp: DecisionInput) -> tuple[str, ...]:
context_refs = () if inp.authority_context is None else inp.authority_context.evidence_refs
return tuple(dict.fromkeys((*inp.evidence_refs, *context_refs)))
def _authority_facts_unresolved(
self,
inp: DecisionInput,
source_blocker: str | None = None,
) -> GovernanceDecision:
return self._decision(
inp,
"AUTHORITY_FACTS_UNRESOLVED" if source_blocker is None else source_blocker,
ProducerBinding(
BlockerClassification.EXTERNAL_DEPENDENCY,
ProducerKind.AUTHORITY_SIDE,
"current-authority-reader",
"RESOLVE_CURRENT_AUTHORITY_AND_EXACT_SCOPE",
),
NextAction.WAIT_DEPENDENCY,
)
@dataclass(frozen=True)
class MutationScope:
"""A host-validated repository and branch used to partition local locks."""
repository: str
branch: str
def __post_init__(self) -> None:
if (
not isinstance(self.repository, str)
or not isinstance(self.branch, str)
or not self.repository
or not self.branch
or "\x00" in self.repository
or "\x00" in self.branch
):
raise ValueError("mutation scope must contain repository and branch")
@dataclass
class _ScopedMutationLock:
lock: Any
references: int = 0
@dataclass
class _OwnedMutation:
ticket: ActionTicket
owner_thread_id: int
lock_key: tuple[str, str]
lock: Any
nonce: object
state: Literal["ACTIVE", "FINISHING"] = "ACTIVE"
class MutationGuard:
def __init__(self, manifest_store: ManifestStore, action_ledger: ActionLedger):
self.manifest_store = manifest_store
self.action_ledger = action_ledger
self._writer_lock = threading.Lock()
self._metadata_lock = threading.Lock()
self._scope_locks: dict[tuple[str, str], _ScopedMutationLock] = {}
self._owned_mutations: dict[int, _OwnedMutation] = {}
@staticmethod
def _lock_key(lock_scope: MutationScope | None) -> tuple[str, str]:
# The omitted C5 parameter retains one process-wide compatibility lock.
if lock_scope is None:
return ("", "")
if not isinstance(lock_scope, MutationScope):
raise TypeError("lock_scope must be a MutationScope")
return (lock_scope.repository, lock_scope.branch)
def _acquire_scope_lock(self, lock_key: tuple[str, str]) -> Any:
with self._metadata_lock:
entry = self._scope_locks.get(lock_key)
if entry is None:
entry = _ScopedMutationLock(lock=threading.Lock())
self._scope_locks[lock_key] = entry
entry.references += 1
lock = entry.lock
if lock.acquire(blocking=False):
return lock
with self._metadata_lock:
self._drop_lock_reference_locked(lock_key, lock)
raise ConcurrentWriterDenied("another governed mutation is in progress")
def _drop_lock_reference_locked(self, lock_key: tuple[str, str], lock: Any) -> None:
entry = self._scope_locks.get(lock_key)
if entry is None or entry.lock is not lock or entry.references <= 0:
raise GovernanceControlError("mutation lock bookkeeping violated")
entry.references -= 1
if entry.references == 0:
del self._scope_locks[lock_key]
def _release_unowned_scope_lock(self, lock_key: tuple[str, str], lock: Any) -> None:
# This call can only release the lock acquired by the current begin.
lock.release()
with self._metadata_lock:
self._drop_lock_reference_locked(lock_key, lock)
def begin_mutation(
self,
@@ -828,44 +1106,79 @@ class MutationGuard:
precondition_hash: str,
expected_readback: Mapping[str, Any] | None = None,
rollback_ref: str | None = None,
lock_scope: MutationScope | None = None,
) -> ActionTicket:
self.manifest_store.assert_current(
manifest_id=manifest_id,
runtime_fingerprint=runtime_fingerprint,
process_start_identity=process_start_identity,
)
if not self._writer_lock.acquire(blocking=False):
raise ConcurrentWriterDenied("another governed mutation is in progress")
lock_key = self._lock_key(lock_scope)
lock = self._acquire_scope_lock(lock_key)
released = False
registered = False
try:
ticket = self.action_ledger.begin(
idempotency_key=idempotency_key,manifest_id=manifest_id,phase=phase,
kind=kind,target=target,precondition_hash=precondition_hash,
expected_readback=expected_readback,rollback_ref=rollback_ref,
)
if ticket.reused:
self._writer_lock.release()
if ticket.reused or ticket.requires_reconcile:
self._release_unowned_scope_lock(lock_key, lock)
released = True
return ticket
with self._metadata_lock:
self._owned_mutations[id(ticket)] = _OwnedMutation(
ticket=ticket,
owner_thread_id=threading.get_ident(),
lock_key=lock_key,
lock=lock,
nonce=object(),
)
registered = True
return ticket
except Exception:
self._writer_lock.release()
if not registered and not released:
self._release_unowned_scope_lock(lock_key, lock)
raise
def finish_success(self, ticket: ActionTicket, *, result: Mapping[str, Any], evidence_refs: Sequence[str] = ()) -> None:
try:
self.action_ledger.mark_succeeded(ticket.action_id, result=result, evidence_refs=evidence_refs)
finally:
if self._writer_lock.locked():
self._writer_lock.release()
self._finish_owned(ticket, self.action_ledger.mark_succeeded, result=result, evidence_refs=evidence_refs)
def finish_failed(self, ticket: ActionTicket, *, result: Mapping[str, Any] | None = None, evidence_refs: Sequence[str] = ()) -> None:
try:
self.action_ledger.mark_failed(ticket.action_id, result=result, evidence_refs=evidence_refs)
finally:
if self._writer_lock.locked():
self._writer_lock.release()
self._finish_owned(ticket, self.action_ledger.mark_failed, result=result, evidence_refs=evidence_refs)
def finish_effect_unknown(self, ticket: ActionTicket, *, result: Mapping[str, Any] | None = None, evidence_refs: Sequence[str] = ()) -> None:
self._finish_owned(ticket, self.action_ledger.mark_effect_unknown, result=result, evidence_refs=evidence_refs)
def _finish_owned(
self,
ticket: ActionTicket,
finish: Callable[..., None],
*,
result: Mapping[str, Any] | None,
evidence_refs: Sequence[str],
) -> None:
with self._metadata_lock:
owned = self._owned_mutations.get(id(ticket))
if (
owned is None
or owned.ticket is not ticket
or owned.owner_thread_id != threading.get_ident()
or owned.state != "ACTIVE"
):
raise MutationOwnershipError()
owned.state = "FINISHING"
try:
self.action_ledger.mark_effect_unknown(ticket.action_id, result=result, evidence_refs=evidence_refs)
finish(ticket.action_id, result=result, evidence_refs=evidence_refs)
finally:
if self._writer_lock.locked():
self._writer_lock.release()
with self._metadata_lock:
current = self._owned_mutations.get(id(ticket))
if (
current is owned
and current.ticket is ticket
and current.nonce is owned.nonce
):
del self._owned_mutations[id(ticket)]
current.lock.release()
self._drop_lock_reference_locked(current.lock_key, current.lock)
@@ -46,6 +46,7 @@ class GovernedWriterState(str, Enum):
RISK_ORDER = {
"low": 10,
"medium": 20,
"engineering": 25,
"high": 30,
"critical": 40,
}
@@ -0,0 +1,789 @@
"""Fail-closed host adapter for the C6 governed source-patch pilot.
The adapter owns no worker registration, authority database, effect ledger, or
Git mutation implementation. Those are host-owned. It only joins an exact C6
operation binding and v3 TaskPlane claim to an existing durable external
transaction owner.
The transaction owner is a security boundary. Its ``execute_prepared`` method
must durably record the send phase before an external call, call
``before_external_call`` while holding its own lease/fence, acquire and persist
the real WriterLease, enforce expected-OID CAS at the write linearization point,
and leave an ambiguous call in EFFECT_UNKNOWN. A callback by itself is not an
atomic revocation or CAS fence.
``RootActionConsumer`` exposes an exact-key facade only after the host installs
a trusted operation-to-review binding provider. C6 callers cannot synthesize
review_id, mission_id, root_task_id, root_revision, plan digest, or execution
key. The facade reuses the consumer's existing review, fence, and effect rows.
"""
from __future__ import annotations
from collections.abc import Callable, Mapping
from dataclasses import asdict, dataclass
from datetime import datetime, timezone
import hashlib
import json
import math
import re
import time
from typing import Literal, Protocol
from release_host_authority import VerifiedCurrentRecordReader
from .c6_composition import DurableExecutionIntent
from .operation_view import ClosureObservation, OperationBinding, binding_digest, validate_oid
from .patch_contract import ExecutablePatchV1, parse_executable_patch
from .schedulability_store_adapter import TaskPlaneClaimReceiptV3
_ACTION_ID = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._:/@+-]{0,191}$")
_DIGEST = re.compile(r"^[0-9a-f]{64}$")
HOST_PATCH_AUTHORITY_PURPOSE = "aios.c6.host-patch-authority.current.v1"
HOST_PATCH_RECONCILE_AUTHORITY_PURPOSE = (
"aios.c6.host-patch-reconcile-authority.current.v1"
)
HOST_PATCH_AUTHORITY_SCHEMA = "aios.c6.host-patch-authority.v1"
_AUTHORITY_RECORD_FIELDS = frozenset(
{
"schema",
"authorization_ref",
"authority_generation",
"task_generation",
"claim_id",
"claim_fence",
"expected_branch_oid",
"binding_digest",
"lifecycle_state",
"scope_state",
"expires_at",
}
)
def _canonical_json_bytes(value: Mapping[str, object]) -> bytes:
return json.dumps(
dict(value), ensure_ascii=False, sort_keys=True, separators=(",", ":")
).encode("utf-8")
ActionState = Literal[
"PREPARED", "IN_PROGRESS", "EFFECT_UNKNOWN", "COMMITTED", "FAILED_FINAL"
]
class HostPatchIntegrationError(RuntimeError):
"""The host binding is incomplete, stale, or contradictory."""
class HostPatchEffectUnknown(HostPatchIntegrationError):
"""The same immutable action must be reconciled, never resubmitted."""
class HostPatchConfigurationError(HostPatchIntegrationError):
"""No production-safe exact-key transaction owner is configured."""
@dataclass(frozen=True)
class HostPatchArtifact:
document: Mapping[str, object]
patch_bytes: bytes
allowed_files: frozenset[str]
commit_message: str
def __post_init__(self) -> None:
if not isinstance(self.document, Mapping):
raise ValueError("patch document provider must return a mapping")
if not isinstance(self.patch_bytes, bytes) or not self.patch_bytes:
raise ValueError("patch provider must return non-empty bytes")
if not isinstance(self.allowed_files, frozenset) or not self.allowed_files:
raise ValueError("patch provider must return a frozen non-empty allowlist")
if (
not isinstance(self.commit_message, str)
or not self.commit_message.strip()
or self.commit_message != self.commit_message.strip()
):
raise ValueError("canonical commit message required")
@dataclass(frozen=True)
class HostPatchAuthority:
"""Fresh host readback before a transaction-owner call.
``claim_fence`` is the TaskPlane receipt revision. It is deliberately
separate from the WriterLease fence returned by the external transaction.
"""
authorization_ref: str
authority_key_id: str
authority_key_fingerprint: str
authority_record_sha256: str
authority_generation: int
task_generation: int
claim_id: str
claim_fence: int
expected_branch_oid: str
expires_at: float
def __post_init__(self) -> None:
if (
not isinstance(self.authorization_ref, str)
or not self.authorization_ref.strip()
or not isinstance(self.authority_key_id, str)
or not self.authority_key_id.strip()
or not isinstance(self.claim_id, str)
or not self.claim_id.strip()
):
raise ValueError("host authority identity required")
if (
not isinstance(self.authority_key_fingerprint, str)
or _DIGEST.fullmatch(self.authority_key_fingerprint) is None
):
raise ValueError("verified host authority key fingerprint required")
if (
not isinstance(self.authority_record_sha256, str)
or _DIGEST.fullmatch(self.authority_record_sha256) is None
):
raise ValueError("verified host authority record digest required")
for name in ("authority_generation", "task_generation", "claim_fence"):
value = getattr(self, name)
if isinstance(value, bool) or not isinstance(value, int) or value < 0:
raise ValueError(f"invalid host authority {name}")
validate_oid(self.expected_branch_oid)
if (
isinstance(self.expires_at, bool)
or not isinstance(self.expires_at, (int, float))
or not math.isfinite(float(self.expires_at))
):
raise ValueError("invalid host authority expiry")
@dataclass(frozen=True)
class HostPatchAction:
"""Readback owned by the existing external transaction boundary."""
action_id: str
state: ActionState
binding_digest: str
effect_key: str
authorization_ref: str
authority_key_id: str
authority_key_fingerprint: str
authority_record_sha256: str
claim_id: str
claim_fence: int
task_generation: int
authority_generation: int
expected_branch_oid: str
claim_receipt: Mapping[str, object]
writer_lease_id: str | None = None
writer_lease_fence: int | None = None
result_oid: str | None = None
receipt_ref: str | None = None
def __post_init__(self) -> None:
if not isinstance(self.action_id, str) or _ACTION_ID.fullmatch(self.action_id) is None:
raise ValueError("canonical immutable action_id required")
if self.state not in {
"PREPARED", "IN_PROGRESS", "EFFECT_UNKNOWN", "COMMITTED", "FAILED_FINAL"
}:
raise ValueError("invalid host patch action state")
if not isinstance(self.binding_digest, str) or _DIGEST.fullmatch(self.binding_digest) is None:
raise ValueError("action binding digest required")
if not isinstance(self.effect_key, str) or not self.effect_key:
raise ValueError("action effect key required")
if (
not isinstance(self.authorization_ref, str)
or not self.authorization_ref
or not isinstance(self.authority_key_id, str)
or not self.authority_key_id
or not isinstance(self.authority_key_fingerprint, str)
or _DIGEST.fullmatch(self.authority_key_fingerprint) is None
or not isinstance(self.authority_record_sha256, str)
or _DIGEST.fullmatch(self.authority_record_sha256) is None
or not isinstance(self.claim_id, str)
or not self.claim_id
or not isinstance(self.claim_receipt, Mapping)
):
raise ValueError("action authority/claim identity required")
for name in ("claim_fence", "task_generation", "authority_generation"):
value = getattr(self, name)
if isinstance(value, bool) or not isinstance(value, int) or value < 0:
raise ValueError(f"invalid action {name}")
validate_oid(self.expected_branch_oid)
if self.writer_lease_fence is not None and (
isinstance(self.writer_lease_fence, bool)
or not isinstance(self.writer_lease_fence, int)
or self.writer_lease_fence < 0
):
raise ValueError("invalid WriterLease fence")
if self.result_oid is not None:
validate_oid(self.result_oid)
if self.state == "COMMITTED":
if (
not self.writer_lease_id
or self.writer_lease_fence is None
or self.result_oid is None
or not self.receipt_ref
):
raise ValueError("committed action lacks WriterLease/OID/receipt binding")
class SignedHostPatchAuthorityReader:
"""Build host authority only from a verified, purpose-bound current record.
The underlying reader enforces a registered key, signature, short lifetime,
exact query hash, and a fresh nonce. This remains a freshness challenge;
the root-action transaction owner still supplies the atomic fence and CAS.
Host configuration must register both effect and reconcile purposes to the
same authority key id/raw-key fingerprint. Reconcile has a distinct signed
query and grants observation only, while prepared identity remains the same
stable authorization semantics across the two purposes.
"""
def __init__(self, current_records: VerifiedCurrentRecordReader) -> None:
if not isinstance(current_records, VerifiedCurrentRecordReader):
raise TypeError("current_records must be VerifiedCurrentRecordReader")
purposes = (
HOST_PATCH_AUTHORITY_PURPOSE,
HOST_PATCH_RECONCILE_AUTHORITY_PURPOSE,
)
try:
key_ids = [current_records.purpose_key_ids[purpose] for purpose in purposes]
fingerprints = [
current_records.purpose_key_fingerprints[purpose]
for purpose in purposes
]
except KeyError as exc:
raise HostPatchConfigurationError(
"effect and reconcile authority purposes must both be registered"
) from exc
if (
any(len(ids) != 1 for ids in key_ids)
or key_ids[0] != key_ids[1]
or set(fingerprints[0]) != set(key_ids[0])
or fingerprints[0] != fingerprints[1]
):
raise HostPatchConfigurationError(
"effect and reconcile purposes require one identical key/fingerprint"
)
self._records = current_records
@staticmethod
def _query(
binding: OperationBinding, claim: Mapping[str, object], *, reconcile: bool
) -> dict[str, object]:
return {
"schema": (
"aios.c6.host-patch-reconcile-authority-query.v1"
if reconcile
else "aios.c6.host-patch-authority-query.v1"
),
"binding": asdict(binding),
"binding_digest": binding_digest(binding),
"claim": dict(claim),
}
def _read(
self, binding: OperationBinding, claim: Mapping[str, object], *, reconcile: bool
) -> HostPatchAuthority:
purpose = (
HOST_PATCH_RECONCILE_AUTHORITY_PURPOSE
if reconcile
else HOST_PATCH_AUTHORITY_PURPOSE
)
verified = self._records.read(
purpose, self._query(binding, claim, reconcile=reconcile)
)
record = verified.record
if frozenset(record) != _AUTHORITY_RECORD_FIELDS:
missing = sorted(_AUTHORITY_RECORD_FIELDS - frozenset(record))
extra = sorted(frozenset(record) - _AUTHORITY_RECORD_FIELDS)
raise HostPatchIntegrationError(
f"host authority record fields are not exact; missing={missing!r} extra={extra!r}"
)
if (
record["schema"] != HOST_PATCH_AUTHORITY_SCHEMA
or record["lifecycle_state"] != "ACTIVE"
or record["scope_state"] != "IN_SCOPE"
or record["binding_digest"] != binding_digest(binding)
or record["task_generation"] != binding.generation
or record["claim_id"] != claim.get("claim_id")
or record["claim_fence"] != claim.get("receipt_revision")
or record["expected_branch_oid"] != binding.source_oid
):
raise HostPatchIntegrationError("signed host authority binding mismatch")
if any(
not isinstance(record[name], str) or not record[name].strip()
for name in (
"authorization_ref", "claim_id", "expected_branch_oid",
"binding_digest", "lifecycle_state", "scope_state",
)
):
raise HostPatchIntegrationError("signed host authority text fields are invalid")
for name in ("authority_generation", "task_generation", "claim_fence"):
if (
isinstance(record[name], bool)
or not isinstance(record[name], int)
or record[name] < 0
):
raise HostPatchIntegrationError(
f"signed host authority {name} is invalid"
)
if (
isinstance(record["expires_at"], bool)
or not isinstance(record["expires_at"], (int, float))
or not math.isfinite(float(record["expires_at"]))
):
raise HostPatchIntegrationError("signed host authority expiry is invalid")
try:
envelope_expiry = datetime.fromisoformat(
verified.expires_at.replace("Z", "+00:00")
).astimezone(timezone.utc).timestamp()
authority = HostPatchAuthority(
authorization_ref=record["authorization_ref"],
authority_key_id=verified.key_id,
authority_key_fingerprint=verified.key_fingerprint,
# The signed envelope and its short observation expiry change
# across fresh reads. Prepared identity binds only the stable
# authorization semantics; freshness is independently checked
# on every read.
authority_record_sha256=hashlib.sha256(
_canonical_json_bytes(
{key: value for key, value in record.items() if key != "expires_at"}
)
).hexdigest(),
authority_generation=record["authority_generation"], # type: ignore[arg-type]
task_generation=record["task_generation"], # type: ignore[arg-type]
claim_id=record["claim_id"],
claim_fence=record["claim_fence"], # type: ignore[arg-type]
expected_branch_oid=record["expected_branch_oid"],
expires_at=min(float(record["expires_at"]), envelope_expiry),
)
except (TypeError, ValueError) as exc:
raise HostPatchIntegrationError("signed host authority record is invalid") from exc
if (
not reconcile
and authority.expires_at
> float(claim.get("lease_until", authority.expires_at))
):
raise HostPatchIntegrationError("host authority outlives the TaskPlane claim")
return authority
def __call__(
self, binding: OperationBinding, claim: TaskPlaneClaimReceiptV3
) -> HostPatchAuthority:
if not isinstance(claim, TaskPlaneClaimReceiptV3):
raise TypeError("v3 TaskPlane claim receipt required")
return self._read(binding, claim.to_dict(), reconcile=False)
def read_for_action(
self, binding: OperationBinding, action: HostPatchAction
) -> HostPatchAuthority:
if not isinstance(action, HostPatchAction):
raise TypeError("host patch action required")
return self._read(binding, action.claim_receipt, reconcile=True)
def signed_host_patch_authority_provider(
current_records: VerifiedCurrentRecordReader,
) -> SignedHostPatchAuthorityReader:
"""Factory used by host composition without weakening reader type checks."""
return SignedHostPatchAuthorityReader(current_records)
class HostPatchTransactionOwner(Protocol):
"""Exact-key facade implemented by an existing durable transaction owner."""
def read_exact(self, effect_key: str) -> HostPatchAction | None: ...
def iter_pending_exact(
self, *, limit: int
) -> tuple[tuple[OperationBinding, HostPatchAction], ...]: ...
def mark_exact_closed(
self, *, binding: OperationBinding, action_id: str,
observation: ClosureObservation
) -> None: ...
def prepare_exact(
self,
*,
binding: OperationBinding,
claim: TaskPlaneClaimReceiptV3,
authority: HostPatchAuthority,
contract: ExecutablePatchV1,
patch_bytes: bytes,
commit_message: str,
) -> HostPatchAction: ...
def reconcile_exact(
self,
*,
action_id: str,
binding: OperationBinding,
authority: HostPatchAuthority,
before_external_call: Callable[[], None],
) -> HostPatchAction: ...
def execute_prepared(
self,
*,
action_id: str,
binding: OperationBinding,
claim: TaskPlaneClaimReceiptV3,
authority: HostPatchAuthority,
contract: ExecutablePatchV1,
patch_bytes: bytes,
commit_message: str,
before_external_call: Callable[[], None],
) -> HostPatchAction: ...
_EXACT_OWNER_METHODS = (
"read_exact", "iter_pending_exact", "mark_exact_closed", "prepare_exact", "reconcile_exact",
"execute_prepared"
)
def require_exact_root_action_owner(service: object) -> HostPatchTransactionOwner:
"""Return an exact-key root-action facade or fail before enabling C6.
``TaskPlaneService.root_actions`` currently exposes ``run_once`` only, so a
stock service is rejected. A production composition may install a facade
backed by the same RootActionConsumer/PublicBridgeRootActionExecutor store;
it must not create another effect ledger.
"""
owner = getattr(service, "root_actions", None)
missing = [name for name in _EXACT_OWNER_METHODS if not callable(getattr(owner, name, None))]
if missing:
raise HostPatchConfigurationError(
"exact root-action transaction owner unavailable: " + ",".join(missing)
)
if getattr(owner, "store", None) is not getattr(service, "store", None):
raise HostPatchConfigurationError(
"exact root-action owner must reuse the host TaskPlane store"
)
configured = getattr(owner, "assert_exact_owner_configured", None)
if not callable(configured):
raise HostPatchConfigurationError(
"exact root-action owner configuration assertion unavailable"
)
try:
configured()
except Exception as exc:
raise HostPatchConfigurationError(
"exact root-action transaction owner is not configured"
) from exc
return owner # type: ignore[return-value]
class HostPatchExecutor:
"""Concrete ``GovernedSourcePatchPilot`` over host-owned formal providers."""
def __init__(
self,
*,
artifact_provider: Callable[[OperationBinding], HostPatchArtifact],
authority_provider: Callable[
[OperationBinding, TaskPlaneClaimReceiptV3], HostPatchAuthority
],
transaction_owner: HostPatchTransactionOwner,
clock: Callable[[], float] | None = None,
) -> None:
if not callable(artifact_provider) or not callable(authority_provider):
raise HostPatchConfigurationError("artifact and authority providers required")
if not callable(getattr(authority_provider, "read_for_action", None)):
raise HostPatchConfigurationError(
"authority provider must support signed restart reconciliation"
)
if any(not callable(getattr(transaction_owner, name, None)) for name in _EXACT_OWNER_METHODS):
raise HostPatchConfigurationError("exact external transaction owner required")
self._artifacts = artifact_provider
self._authority_provider = authority_provider
self._transactions = transaction_owner
self._clock = clock or time.time
def _artifact(self, binding: OperationBinding) -> tuple[HostPatchArtifact, ExecutablePatchV1]:
artifact = self._artifacts(binding)
if not isinstance(artifact, HostPatchArtifact):
raise HostPatchIntegrationError("invalid host patch artifact")
contract = parse_executable_patch(
artifact.document,
patch_bytes=artifact.patch_bytes,
allowed_files=artifact.allowed_files,
expected_binding=binding,
)
return artifact, contract
def _authority(
self, binding: OperationBinding, claim: TaskPlaneClaimReceiptV3
) -> HostPatchAuthority:
value = self._authority_provider(binding, claim)
if not isinstance(value, HostPatchAuthority):
raise HostPatchIntegrationError("invalid host authority readback")
if (
value.claim_id != claim.claim_id
or value.claim_fence != claim.receipt_revision
or value.task_generation != binding.generation
or value.expected_branch_oid != binding.source_oid
or value.expires_at <= float(self._clock())
):
raise HostPatchIntegrationError("stale or mismatched host authority")
return value
@staticmethod
def _validate_action(
action: HostPatchAction,
binding: OperationBinding,
*,
claim_id: str | None = None,
claim_fence: int | None = None,
authority: HostPatchAuthority | None = None,
) -> HostPatchAction:
if not isinstance(action, HostPatchAction):
raise HostPatchIntegrationError("transaction owner returned invalid action")
if (
action.binding_digest != binding_digest(binding)
or action.effect_key != binding.effect_key
or action.task_generation != binding.generation
or action.expected_branch_oid != binding.source_oid
or (claim_id is not None and action.claim_id != claim_id)
or (claim_fence is not None and action.claim_fence != claim_fence)
):
raise HostPatchIntegrationError("external action binding mismatch")
if authority is not None and (
action.authorization_ref != authority.authorization_ref
or action.authority_key_id != authority.authority_key_id
or action.authority_key_fingerprint != authority.authority_key_fingerprint
or action.authority_record_sha256 != authority.authority_record_sha256
or action.authority_generation != authority.authority_generation
or action.claim_id != authority.claim_id
or action.claim_fence != authority.claim_fence
or action.expected_branch_oid != authority.expected_branch_oid
):
raise HostPatchIntegrationError("external action authority changed")
if action.state == "FAILED_FINAL":
raise HostPatchIntegrationError("external action failed final")
return action
def read_intent(self, binding: OperationBinding) -> DurableExecutionIntent | None:
"""C6 ``intent_reader`` wiring backed only by the transaction owner."""
action = self._transactions.read_exact(binding.effect_key)
if action is None:
return None
action = self._validate_action(action, binding)
return DurableExecutionIntent(
binding_digest(binding), binding.effect_key, action.claim_id, action.action_id
)
def pending_intents(
self, *, limit: int = 16
) -> tuple[tuple[OperationBinding, DurableExecutionIntent], ...]:
if isinstance(limit, bool) or not isinstance(limit, int) or not 1 <= limit <= 64:
raise HostPatchConfigurationError("bounded pending intent limit required")
result: list[tuple[OperationBinding, DurableExecutionIntent]] = []
for pending_binding, action in self._transactions.iter_pending_exact(limit=limit):
checked = self._validate_action(action, pending_binding)
if checked.state not in {
"PREPARED", "IN_PROGRESS", "EFFECT_UNKNOWN", "COMMITTED"
}:
raise HostPatchIntegrationError("transaction owner enumerated failed action")
result.append(
(
pending_binding,
DurableExecutionIntent(
binding_digest(pending_binding),
pending_binding.effect_key,
checked.claim_id,
checked.action_id,
),
)
)
return tuple(result)
def mark_intent_closed(
self, binding: OperationBinding, intent: DurableExecutionIntent,
observation: ClosureObservation,
) -> None:
if (
not isinstance(intent, DurableExecutionIntent)
or intent.binding_digest != binding_digest(binding)
or intent.effect_key != binding.effect_key
or not isinstance(observation, ClosureObservation)
):
raise HostPatchIntegrationError("invalid durable closure acknowledgement")
self._transactions.mark_exact_closed(
binding=binding, action_id=intent.receipt_ref, observation=observation
)
def prepare_intent(
self, *, binding: OperationBinding, claim: TaskPlaneClaimReceiptV3
) -> DurableExecutionIntent:
artifact, contract = self._artifact(binding)
authority = self._authority(binding, claim)
action = self._transactions.prepare_exact(
binding=binding,
claim=claim,
authority=authority,
contract=contract,
patch_bytes=artifact.patch_bytes,
commit_message=artifact.commit_message,
)
action = self._validate_action(
action,
binding,
claim_id=claim.claim_id,
claim_fence=claim.receipt_revision,
authority=authority,
)
return DurableExecutionIntent(
binding_digest(binding), binding.effect_key, claim.claim_id, action.action_id
)
def execute(
self,
*,
binding: OperationBinding,
claim: TaskPlaneClaimReceiptV3,
intent: DurableExecutionIntent,
) -> None:
if (
not isinstance(intent, DurableExecutionIntent)
or intent.binding_digest != binding_digest(binding)
or intent.effect_key != binding.effect_key
or intent.claim_id != claim.claim_id
):
raise HostPatchIntegrationError("execution intent binding mismatch")
artifact, contract = self._artifact(binding)
authority = self._authority(binding, claim)
action = self._transactions.read_exact(binding.effect_key)
if action is None or action.action_id != intent.receipt_ref:
raise HostPatchIntegrationError("durable external action unavailable")
action = self._validate_action(
action, binding, claim_id=claim.claim_id,
claim_fence=claim.receipt_revision, authority=authority,
)
def revalidate() -> None:
fresh = self._authority(binding, claim)
if (
fresh.authorization_ref != authority.authorization_ref
or fresh.authority_key_id != authority.authority_key_id
or fresh.authority_key_fingerprint != authority.authority_key_fingerprint
or fresh.authority_record_sha256 != authority.authority_record_sha256
or fresh.authority_generation != authority.authority_generation
or fresh.task_generation != authority.task_generation
or fresh.claim_id != authority.claim_id
or fresh.claim_fence != authority.claim_fence
or fresh.expected_branch_oid != authority.expected_branch_oid
):
raise HostPatchIntegrationError("host authority changed before external call")
observed = self._transactions.reconcile_exact(
action_id=action.action_id,
binding=binding,
authority=authority,
before_external_call=revalidate,
)
observed = self._validate_action(
observed, binding, claim_id=claim.claim_id,
claim_fence=claim.receipt_revision, authority=authority,
)
if observed.action_id != action.action_id:
raise HostPatchIntegrationError("immutable external action_id changed")
if observed.state == "COMMITTED":
return
if observed.state in {"IN_PROGRESS", "EFFECT_UNKNOWN"}:
raise HostPatchEffectUnknown("external effect requires exact-key reconciliation")
if observed.state != "PREPARED":
raise HostPatchIntegrationError("external action is not executable")
completed = self._transactions.execute_prepared(
action_id=action.action_id,
binding=binding,
claim=claim,
authority=authority,
contract=contract,
patch_bytes=artifact.patch_bytes,
commit_message=artifact.commit_message,
before_external_call=revalidate,
)
completed = self._validate_action(
completed, binding, claim_id=claim.claim_id,
claim_fence=claim.receipt_revision, authority=authority,
)
if completed.action_id != action.action_id:
raise HostPatchIntegrationError("immutable external action_id changed")
if completed.state != "COMMITTED":
raise HostPatchEffectUnknown("external transaction did not return committed readback")
def reconcile(
self, *, binding: OperationBinding, intent: DurableExecutionIntent
) -> None:
"""Reconcile a durable intent without acquiring a lease or resending a commit."""
if (
not isinstance(intent, DurableExecutionIntent)
or intent.binding_digest != binding_digest(binding)
or intent.effect_key != binding.effect_key
):
raise HostPatchIntegrationError("reconciliation intent binding mismatch")
action = self._transactions.read_exact(binding.effect_key)
if action is None or action.action_id != intent.receipt_ref:
raise HostPatchIntegrationError("durable external action unavailable")
action = self._validate_action(action, binding, claim_id=intent.claim_id)
reader = self._authority_provider
authority = reader.read_for_action(binding, action) # type: ignore[attr-defined]
if not isinstance(authority, HostPatchAuthority):
raise HostPatchIntegrationError("invalid reconciliation authority readback")
action = self._validate_action(action, binding, authority=authority)
def revalidate() -> None:
fresh = reader.read_for_action(binding, action) # type: ignore[attr-defined]
if (
not isinstance(fresh, HostPatchAuthority)
or fresh.authorization_ref != authority.authorization_ref
or fresh.authority_key_id != authority.authority_key_id
or fresh.authority_key_fingerprint != authority.authority_key_fingerprint
or fresh.authority_record_sha256 != authority.authority_record_sha256
or fresh.authority_generation != authority.authority_generation
or fresh.task_generation != authority.task_generation
or fresh.claim_id != authority.claim_id
or fresh.claim_fence != authority.claim_fence
or fresh.expected_branch_oid != authority.expected_branch_oid
):
raise HostPatchIntegrationError(
"host authority changed before reconciliation call"
)
observed = self._transactions.reconcile_exact(
action_id=action.action_id,
binding=binding,
authority=authority,
before_external_call=revalidate,
)
observed = self._validate_action(observed, binding, authority=authority)
if observed.action_id != action.action_id:
raise HostPatchIntegrationError("immutable external action_id changed")
if observed.state != "COMMITTED":
raise HostPatchEffectUnknown("external effect remains unresolved")
__all__ = [
"HostPatchAction",
"HostPatchArtifact",
"HostPatchAuthority",
"HostPatchConfigurationError",
"HostPatchEffectUnknown",
"HostPatchExecutor",
"HostPatchIntegrationError",
"HostPatchTransactionOwner",
"HOST_PATCH_AUTHORITY_PURPOSE",
"HOST_PATCH_AUTHORITY_SCHEMA",
"SignedHostPatchAuthorityReader",
"require_exact_root_action_owner",
"signed_host_patch_authority_provider",
]
@@ -0,0 +1,113 @@
"""Authenticated host readback for independent verification and acceptance.
The host supplies separate, registered signing identities and current-record
transports. This adapter never creates receipts or treats an ``authenticated``
payload flag as evidence. Missing providers remain UNKNOWN through OperationReaders.
"""
from __future__ import annotations
from collections.abc import Mapping
from dataclasses import asdict, dataclass
from release_host_authority import VerifiedCurrentRecordReader
from .operation_view import OperationBinding, binding_digest, validate_digest
@dataclass(frozen=True)
class ReceiptAuthorityPolicy:
verifier_id: str
verifier_key_id: str
acceptor_id: str
acceptor_key_id: str
acceptance_revision: str
verifier_key_fingerprint: str
acceptor_key_fingerprint: str
def __post_init__(self) -> None:
for name, value in asdict(self).items():
if not isinstance(value, str) or not value or value.strip() != value or "\x00" in value:
raise ValueError(f"invalid {name}")
if self.verifier_id == self.acceptor_id or self.verifier_key_id == self.acceptor_key_id:
raise ValueError("verification and acceptance must use independent identities and keys")
validate_digest(self.verifier_key_fingerprint)
validate_digest(self.acceptor_key_fingerprint)
if self.verifier_key_fingerprint == self.acceptor_key_fingerprint:
raise ValueError("verification and acceptance require independent public keys")
class HostOperationReceiptReaders:
"""Bind verified transport signatures to host-pinned role identities.
Policies are trusted host configuration, never request or receipt fields.
Receipt/result linkage and freshness are also checked by collect_closure /
derive_closure against the separately observed Git effect.
"""
def __init__(
self,
*,
verifier_reader: VerifiedCurrentRecordReader,
acceptance_reader: VerifiedCurrentRecordReader,
policy: ReceiptAuthorityPolicy,
) -> None:
self._verifier = verifier_reader
self._acceptance = acceptance_reader
self._policy = policy
def _read(self, binding: OperationBinding, *, acceptance: bool) -> dict[str, object]:
policy = self._policy
identity = policy.acceptor_id if acceptance else policy.verifier_id
key_id = policy.acceptor_key_id if acceptance else policy.verifier_key_id
if binding.actor_id in (policy.verifier_id, policy.acceptor_id):
raise ValueError("executor cannot verify or accept its own operation")
kind = "acceptance" if acceptance else "verification"
query = {
"binding": asdict(binding),
"binding_digest": binding_digest(binding),
"expected_identity": identity,
"expected_key_id": key_id,
"expected_key_fingerprint": policy.acceptor_key_fingerprint if acceptance else policy.verifier_key_fingerprint,
}
if acceptance:
query["expected_acceptance_revision"] = policy.acceptance_revision
reader = self._acceptance if acceptance else self._verifier
verified = reader.read(f"operation-{kind}", query)
if verified.key_id != key_id:
raise ValueError("receipt signer does not match the pinned role key")
fingerprint = policy.acceptor_key_fingerprint if acceptance else policy.verifier_key_fingerprint
if verified.key_fingerprint != fingerprint:
raise ValueError("receipt signer public key fingerprint mismatch")
document = verified.record
if not isinstance(document, Mapping) or set(document) != {"schema", kind}:
raise ValueError("invalid signed receipt document")
schema = f"aios.operation-{kind}-observation.v1"
if document["schema"] != schema:
raise ValueError("invalid receipt schema")
observation = document[kind]
common = {"binding_digest", "receipt_ref", "result_artifact_digest", "outcome", "observed_at", "expires_at"}
fields = common | (
{"acceptor_id", "acceptance_revision", "verifier_receipt_ref"}
if acceptance else {"verifier_id", "verification_receipt_ref"}
)
if not isinstance(observation, Mapping) or set(observation) != fields:
raise ValueError("invalid signed receipt observation")
if observation["binding_digest"] != binding_digest(binding):
raise ValueError("receipt binding mismatch")
if observation["acceptor_id" if acceptance else "verifier_id"] != identity:
raise ValueError("receipt identity mismatch")
if acceptance and observation["acceptance_revision"] != policy.acceptance_revision:
raise ValueError("acceptance revision mismatch")
result: dict[str, object] = {
"schema": schema,
kind: {**observation, "authenticated": True},
}
if acceptance:
result["expected_acceptor_id"] = policy.acceptor_id
result["expected_acceptance_revision"] = policy.acceptance_revision
return result
def verifier(self, binding: OperationBinding) -> Mapping[str, object]:
return self._read(binding, acceptance=False)
def acceptance(self, binding: OperationBinding) -> Mapping[str, object]:
return self._read(binding, acceptance=True)
@@ -29,6 +29,7 @@ import base64
import hashlib
import json
import os
import re
import secrets
import subprocess
import time
@@ -37,7 +38,7 @@ import urllib.parse
import urllib.request
from dataclasses import dataclass, field
from pathlib import Path
from typing import Any, Mapping, Sequence
from typing import Any, Callable, Mapping, Sequence
from app.task_plane.governed_writer_automation import (
AuthorizationDenied,
@@ -63,6 +64,17 @@ class LiveProtocolUnavailable(LiveProtocolError):
pass
class IssuerResponseError(LiveProtocolError):
"""Safe, structured issuer failure metadata for isolated diagnostics."""
def __init__(self, message: str, *, status: int, error_code: str | None,
operation: str) -> None:
super().__init__(message)
self.status = status
self.error_code = error_code
self.operation = operation
@dataclass(frozen=True)
class HttpResult:
status: int
@@ -205,11 +217,14 @@ class LiveClaim:
fencing_generation: int | None
lease_expires_at: float | None
task_revision: int | None
public_receipt: Mapping[str, Any] = field(default_factory=dict)
@dataclass(frozen=True)
class LiveWriterLease:
writer_lease_id: str
# /v2/writer-lease is a validated binding projection. The durable branch
# lease is created only by the continuation executor during commit.
writer_lease_id: str | None
task_id: str
job_id: str
branch_mode: str
@@ -229,13 +244,16 @@ class LiveWriterSession:
job_id: str
branch_mode: str
expected_branch_oid: str
writer_lease_id: str
writer_lease_id: str | None
continuation_key: str
principal_issue_schema: str
agent_token_handle: str = field(repr=False)
lease_handle: str = field(repr=False)
task_lease_receipt: Mapping[str, Any] = field(default_factory=dict)
writer_lease_binding: Mapping[str, Any] = field(default_factory=dict)
def public_dict(self) -> dict[str, Any]:
return {
public = {
"authorization_id": self.authorization_id,
"task_id": self.task_id,
"agent_id": self.agent_id,
@@ -243,10 +261,11 @@ class LiveWriterSession:
"job_id": self.job_id,
"branch_mode": self.branch_mode,
"expected_branch_oid": self.expected_branch_oid,
"writer_lease_id": self.writer_lease_id,
"continuation_key": self.continuation_key,
"principal_issue_schema": self.principal_issue_schema,
"writer_lease_binding": dict(self.writer_lease_binding),
}
return public
def _parse_epoch(value: Any) -> float | None:
@@ -420,6 +439,10 @@ class LiveAgentTaskClient:
if result.body.get("receipt_revision") is not None
else None
),
public_receipt={k: result.body[k] for k in (
"task_id", "status", "claim_id", "lease_id", "fencing_generation",
"receipt_revision", "lease_expires_at", "attempt", "revision",
) if k in result.body},
)
def heartbeat(
@@ -456,6 +479,49 @@ class LiveAgentTaskClient:
)
return task
def submit_result(
self,
*,
task_id: str,
agent_id: str,
agent_token_handle: str,
lease_handle: str,
idempotency_key: str,
result_value: Mapping[str, Any],
) -> Mapping[str, Any]:
if not isinstance(idempotency_key, str) or not 1 <= len(idempotency_key) <= 160:
raise LiveProtocolError("task result idempotency key invalid")
if not isinstance(result_value, Mapping):
raise LiveProtocolError("task result mapping required")
quoted = urllib.parse.quote(task_id, safe="")
agent_token = self.secrets.get(agent_token_handle)
lease_token = self.secrets.get(lease_handle)
result = self.transport.request(
"POST",
f"{self.config.agent_task_url}/v1/tasks/{quoted}/result",
body={
"lease_token": lease_token,
"idempotency_key": idempotency_key,
"result": dict(result_value),
},
headers={"Authorization": "Bearer " + agent_token},
timeout=15,
)
if result.status == 409:
raise LiveProtocolConflict(
str(result.body.get("error") or "task result conflict")
)
if result.status not in (200, 201):
raise LiveProtocolError(f"task result HTTP {result.status}")
if (
result.body.get("task_id") != task_id
or result.body.get("status") != "VERIFYING"
or result.body.get("claimed_by") != agent_id
or result.body.get("result") != dict(result_value)
):
raise LiveProtocolConflict("task result readback mismatch")
return dict(result.body)
class LiveTaskPrincipalClient:
def __init__(
@@ -468,6 +534,7 @@ class LiveTaskPrincipalClient:
self.config = config.normalized()
self.transport = transport or JsonHttpTransport()
self.secrets = secrets_store
self.public_receipts: list[dict[str, Any]] = []
def issue_assertion(
self,
@@ -499,6 +566,11 @@ class LiveTaskPrincipalClient:
last = result
assertion = result.body.get("assertion")
if result.status in (200, 201) and isinstance(assertion, str) and assertion:
self.public_receipts.append({
"task_id": task_id, "operation": operation, "request_id": request_id,
"assertion_sha256": hashlib.sha256(assertion.encode()).hexdigest(),
"issuer_response": {k: result.body[k] for k in ("jti", "expires_at", "replayed") if k in result.body},
})
return assertion
if not (
attempt == 0
@@ -508,8 +580,14 @@ class LiveTaskPrincipalClient:
break
time.sleep(1)
status = 0 if last is None else last.status
raise LiveProtocolError(
f"TaskPrincipal assertion issue failed operation={operation} HTTP {status}"
raw_code = None if last is None else last.body.get("error")
error_code = (
raw_code if isinstance(raw_code, str)
and re.fullmatch(r"(?:TASK_PRINCIPAL|EXECUTION_PLAN|GOVERNED)_[A-Z0-9_]{3,120}", raw_code) else None
)
raise IssuerResponseError(
f"TaskPrincipal assertion issue failed operation={operation} HTTP {status}",
status=status, error_code=error_code, operation=operation,
)
@@ -608,23 +686,35 @@ class LiveBridgeClient:
if result.status not in (200, 201):
raise LiveProtocolError(f"writer lease HTTP {result.status}")
body = result.body
if body.get("schema") != "aios.task-writer-lease.v2":
raise LiveProtocolError("writer lease schema mismatch")
if body.get("task_id") != task_id:
raise LiveProtocolError("writer lease task mismatch")
if body.get("expected_branch_oid") not in (None, expected_branch_oid):
raise LiveProtocolConflict("writer lease expected_branch_oid mismatch")
writer_lease_id = body.get("writer_lease_id") or body.get("lease_id")
if not isinstance(writer_lease_id, str) or not writer_lease_id:
raise LiveProtocolError("writer lease response missing lease identifier")
expected_binding = {
"schema": "aios.task-writer-lease.v2",
"task_id": task_id,
"repository": repository,
"base_oid": base_oid,
"expected_branch_oid": expected_branch_oid,
"target_branch": target_branch,
"generation": int(generation),
"lease_seconds": int(self.config.lease_seconds),
}
for field, expected in expected_binding.items():
if body.get(field) != expected:
raise LiveProtocolConflict(f"writer lease {field} mismatch")
projected_branch_mode = body.get("branch_mode")
allowed_branch_modes = {branch_mode}
# CREATE_NEW is materialized by the Bridge before it returns the
# descriptor, so the returned binding is CONTINUE_EXISTING.
if branch_mode == "CREATE_NEW":
allowed_branch_modes.add("CONTINUE_EXISTING")
if projected_branch_mode not in allowed_branch_modes:
raise LiveProtocolConflict("writer lease branch_mode mismatch")
return LiveWriterLease(
writer_lease_id=writer_lease_id,
writer_lease_id=None,
task_id=task_id,
job_id=job_id,
branch_mode=branch_mode,
branch_mode=str(projected_branch_mode),
expected_branch_oid=expected_branch_oid,
generation=int(generation),
expires_at=_parse_epoch(body.get("expires_at")),
expires_at=None,
continuation_key=continuation_key,
raw=dict(body),
)
@@ -696,10 +786,16 @@ class LiveGovernedWriterExecutor:
owner_policy: OwnerAuthorizationPolicy,
authorization_ref: str,
transport: JsonHttpTransport | None = None,
agent_id: str | None = None,
scope_enrollment: Callable[..., Mapping[str, Any]] | None = None,
) -> None:
self.config = config.normalized()
self.owner_policy = owner_policy
self.authorization_ref = authorization_ref
if scope_enrollment is not None and (not callable(scope_enrollment) or not agent_id):
raise ValueError("scope enrollment requires a configured agent identity")
self.agent_id = agent_id
self.scope_enrollment = scope_enrollment
self.transport = transport or JsonHttpTransport()
self.secret_store = SecretHandleStore()
self.agent = LiveAgentTaskClient(
@@ -759,6 +855,7 @@ class LiveGovernedWriterExecutor:
task_id=request.task_id,
capabilities=sorted(request.required_capabilities),
route=route,
agent_id=self.agent_id,
)
claim = self.agent.claim(
task_id=request.task_id,
@@ -778,6 +875,21 @@ class LiveGovernedWriterExecutor:
"remaining governed task window too short after heartbeat"
)
if self.scope_enrollment is not None:
# The existing issuer and Bridge still verify every assertion.
# Enrollment only happens after a real claim has established the
# agent/attempt/lease binding; failures stop before any assertion.
enrollment = self.scope_enrollment(
request=request, job_id=job_id, agent_id=agent_id,
claim=claim, task=running,
)
if (not isinstance(enrollment, Mapping)
or enrollment.get("status") != "ENROLLED"
or enrollment.get("task_id") != request.task_id
or enrollment.get("agent_id") != agent_id
or enrollment.get("job_id") != job_id):
raise LiveProtocolConflict("verified principal scope enrollment required")
live_head = self.git.branch_head(request.target_branch)
if live_head is None:
branch_mode = "CREATE_NEW"
@@ -815,7 +927,10 @@ class LiveGovernedWriterExecutor:
writer_lease_id=writer.writer_lease_id,
continuation_key=writer.continuation_key,
principal_issue_schema=self.config.issue_schema,
agent_token_handle=agent_token_handle,
lease_handle=claim.lease_handle,
task_lease_receipt=dict(claim.public_receipt),
writer_lease_binding=dict(writer.raw),
)
def commit_and_readback(
@@ -857,3 +972,19 @@ class LiveGovernedWriterExecutor:
"branch_head": new_head,
"readback_sha256": readback,
}
def submit_task_result(
self,
*,
session: LiveWriterSession,
result: Mapping[str, Any],
idempotency_key: str,
) -> Mapping[str, Any]:
return self.agent.submit_result(
task_id=session.task_id,
agent_id=session.agent_id,
agent_token_handle=session.agent_token_handle,
lease_handle=session.lease_handle,
idempotency_key=idempotency_key,
result_value=result,
)
+99 -7
View File
@@ -4,6 +4,7 @@ from __future__ import annotations
import hashlib
import json
import math
import re
import time
import uuid
@@ -74,7 +75,13 @@ def _now_ts() -> float:
def canonical_json(value: Any) -> str:
return json.dumps(value, ensure_ascii=False, sort_keys=True, separators=(",", ":"))
return json.dumps(
value,
ensure_ascii=False,
sort_keys=True,
separators=(",", ":"),
allow_nan=False,
)
def compute_sha256_hex(value: Any) -> str:
@@ -532,10 +539,16 @@ class EvidenceEnvelope:
worker_identity: str
payload: dict[str, Any]
created_at: float
expires_at: float
previous_evidence_hash: str
evidence_digest: str
signature: str | None = None
attestation_ref: str | None = None
signature_algorithm: str | None = None
signing_key_id: str | None = None
verifier_identity: str | None = None
authority_domain: str | None = None
sequence: int | None = None
@classmethod
def create(
@@ -551,7 +564,17 @@ class EvidenceEnvelope:
attestation_ref: str | None = None,
evidence_id: str | None = None,
created_at: float | None = None,
expires_at: float | None = None,
signature_algorithm: str | None = None,
signing_key_id: str | None = None,
verifier_identity: str | None = None,
authority_domain: str | None = None,
sequence: int | None = None,
) -> "EvidenceEnvelope":
issued_at = float(_now_ts() if created_at is None else created_at)
valid_until = float(issued_at + 300.0 if expires_at is None else expires_at)
if not math.isfinite(issued_at) or not math.isfinite(valid_until):
raise ValueError("evidence timestamps must be finite")
envelope = cls(
schema_version=1,
evidence_id=evidence_id or f"evd-{uuid.uuid4()}",
@@ -560,11 +583,17 @@ class EvidenceEnvelope:
kind=validate_type(kind, "evidence kind"),
worker_identity=str(worker_identity),
payload=redact_value(payload),
created_at=float(created_at or _now_ts()),
created_at=issued_at,
expires_at=valid_until,
previous_evidence_hash=str(previous_evidence_hash or ""),
evidence_digest="",
signature=signature,
attestation_ref=attestation_ref,
signature_algorithm=signature_algorithm,
signing_key_id=signing_key_id,
verifier_identity=verifier_identity,
authority_domain=authority_domain,
sequence=sequence,
)
return dataclass_replace(
envelope, evidence_digest=compute_sha256_hex(envelope.digest_payload())
@@ -580,8 +609,14 @@ class EvidenceEnvelope:
"worker_identity": self.worker_identity,
"payload": self.payload,
"created_at": self.created_at,
"expires_at": self.expires_at,
"previous_evidence_hash": self.previous_evidence_hash,
"attestation_ref": self.attestation_ref,
"signature_algorithm": self.signature_algorithm,
"signing_key_id": self.signing_key_id,
"verifier_identity": self.verifier_identity,
"authority_domain": self.authority_domain,
"sequence": self.sequence,
}
def verify(
@@ -591,18 +626,40 @@ class EvidenceEnvelope:
expected_claim_id: str,
expected_previous_hash: str,
attestation_verifier: Any = None,
now: float | None = None,
) -> bool:
try:
observed_at = float(_now_ts() if now is None else now)
finite_timestamps = all(
math.isfinite(value)
for value in (observed_at, self.created_at, self.expires_at)
)
expected_digest = compute_sha256_hex(self.digest_payload())
except (TypeError, ValueError, OverflowError):
return False
if (
self.schema_version != 1
not finite_timestamps
or self.schema_version != 1
or self.task_id != expected_task_id
or self.claim_id != expected_claim_id
or self.previous_evidence_hash != expected_previous_hash
or self.evidence_digest != compute_sha256_hex(self.digest_payload())
or self.created_at > observed_at
or self.expires_at <= observed_at
or self.expires_at <= self.created_at
or self.evidence_digest != expected_digest
):
return False
if self.signature or self.attestation_ref:
return True if attestation_verifier is None else bool(attestation_verifier(self))
return True
# An unsigned envelope has no proof of origin and must never verify.
if not (self.signature or self.attestation_ref):
return False
# Every proof form requires an injected verifier. A reference or
# signature must never become trusted merely because it is present.
if attestation_verifier is None:
return False
try:
return bool(attestation_verifier(self))
except Exception:
return False
def to_dict(self) -> dict[str, Any]:
return {
@@ -611,6 +668,41 @@ class EvidenceEnvelope:
"signature": self.signature,
}
@classmethod
def from_dict(cls, value: dict[str, Any]) -> "EvidenceEnvelope":
if not isinstance(value, dict):
raise ValueError("evidence envelope must be an object")
try:
envelope = cls(
schema_version=int(value["schema_version"]),
evidence_id=validate_identifier(value["evidence_id"], "evidence_id"),
task_id=validate_identifier(value["task_id"], "task_id"),
claim_id=validate_identifier(value["claim_id"], "claim_id"),
kind=validate_type(value["kind"], "evidence kind"),
worker_identity=str(value["worker_identity"]),
payload=redact_value(value["payload"]),
created_at=float(value["created_at"]),
expires_at=float(value["expires_at"]),
previous_evidence_hash=str(value["previous_evidence_hash"]),
evidence_digest=str(value["evidence_digest"]),
signature=value.get("signature"),
attestation_ref=value.get("attestation_ref"),
signature_algorithm=value.get("signature_algorithm"),
signing_key_id=value.get("signing_key_id"),
verifier_identity=value.get("verifier_identity"),
authority_domain=value.get("authority_domain"),
sequence=(
None if value.get("sequence") is None else int(value["sequence"])
),
)
if not math.isfinite(envelope.created_at) or not math.isfinite(
envelope.expires_at
):
raise ValueError("evidence timestamps must be finite")
return envelope
except (KeyError, TypeError, ValueError) as exc:
raise ValueError("invalid evidence envelope") from exc
class ProbeOutcome(StrEnum):
NOT_APPLIED = "NOT_APPLIED"
@@ -0,0 +1,480 @@
"""Strict adapters from existing authority readers into C6 observations.
Readers are injected by the trusted composition root. They are query-only:
this module has no controller/reconcile, signer, store, or network factory.
"""
from __future__ import annotations
from collections.abc import Callable, Mapping, Sequence
from dataclasses import dataclass
from datetime import datetime
from typing import Any
from .operation_view import (
AcceptanceObservation,
ClosureObservation,
DomainStamp,
EffectObservation,
Fact,
OperationBinding,
OperationObservation,
VerificationObservation,
binding_digest,
)
Reader = Callable[[OperationBinding], Mapping[str, object]]
_STAMP_FIELDS = frozenset({"domain", "object_id", "generation", "revision", "event_id"})
_FACT_FIELDS = frozenset(
{"value", "stamp", "binding_digest", "observed_at", "expires_at", "evidence_refs"}
)
_DECLARATION_FIELDS = frozenset({"schema", "declared", "installed", "callable"})
_TASK_FIELDS = frozenset({"schema", "preconditions"})
_AUTHORIZATION_FIELDS = frozenset({"schema", "authorized"})
_EFFECT_FIELDS = frozenset(
{
"binding_digest",
"outcome",
"receipt_ref",
"result_artifact_digest",
"actor_id",
"stamp",
"observed_at",
"expires_at",
"trust_status",
}
)
_GITOPS_FIELDS = frozenset({"schema", "effect"})
_VERIFICATION_FIELDS = frozenset(
{
"binding_digest",
"receipt_ref",
"result_artifact_digest",
"verifier_id",
"outcome",
"authenticated",
"observed_at",
"expires_at",
"verification_receipt_ref",
}
)
_VERIFIER_FIELDS = frozenset({"schema", "verification"})
_ACCEPTANCE_OBSERVATION_FIELDS = frozenset(
{
"binding_digest",
"receipt_ref",
"result_artifact_digest",
"verifier_receipt_ref",
"acceptance_revision",
"acceptor_id",
"outcome",
"authenticated",
"observed_at",
"expires_at",
}
)
_ACCEPTANCE_FIELDS = frozenset(
{
"schema",
"acceptance",
"expected_acceptor_id",
"expected_acceptance_revision",
}
)
@dataclass(frozen=True)
class OperationReaders:
declaration: Reader | None
task: Reader | None
authorization: Reader | None
gitops: Reader | None
verifier: Reader | None
acceptance: Reader | None
def __post_init__(self) -> None:
for name in (
"declaration",
"task",
"authorization",
"gitops",
"verifier",
"acceptance",
):
value = getattr(self, name)
if value is not None and not callable(value):
raise ValueError(f"{name} reader must be callable or None")
def _exact_mapping(value: object, fields: frozenset[str], name: str) -> Mapping[str, object]:
if not isinstance(value, Mapping) or set(value) != fields:
raise ValueError(f"{name} has unknown or missing fields")
if any(not isinstance(key, str) for key in value):
raise ValueError(f"{name} keys must be strings")
return value
def _text(value: object, name: str) -> str:
if not isinstance(value, str) or not value or value != value.strip() or "\x00" in value:
raise ValueError(f"{name} must be a canonical non-empty string")
return value
def _optional_text(value: object, name: str) -> str | None:
if value is None:
return None
return _text(value, name)
def _datetime(value: object, name: str) -> datetime | None:
if value is None:
return None
if isinstance(value, str):
text = value[:-1] + "+00:00" if value.endswith("Z") else value
try:
value = datetime.fromisoformat(text)
except ValueError as exc:
raise ValueError(f"{name} is not an RFC3339 datetime") from exc
if not isinstance(value, datetime) or value.tzinfo is None or value.utcoffset() is None:
raise ValueError(f"{name} must be timezone-aware")
return value
def _tuple_of_text(value: object, name: str) -> tuple[str, ...]:
if not isinstance(value, (tuple, list)) or isinstance(value, (str, bytes)):
raise ValueError(f"{name} must be an array")
return tuple(_text(item, name) for item in value)
def _stamp(value: object) -> DomainStamp | None:
if value is None:
return None
raw = _exact_mapping(value, _STAMP_FIELDS, "stamp")
return DomainStamp(
domain=_text(raw["domain"], "domain"),
object_id=_text(raw["object_id"], "object_id"),
generation=raw["generation"], # type: ignore[arg-type]
revision=raw["revision"], # type: ignore[arg-type]
event_id=_text(raw["event_id"], "event_id"),
)
def _fact(value: object) -> Fact:
raw = _exact_mapping(value, _FACT_FIELDS, "fact")
return Fact(
value=raw["value"], # type: ignore[arg-type]
stamp=_stamp(raw["stamp"]),
binding_digest=_text(raw["binding_digest"], "binding_digest"),
observed_at=_datetime(raw["observed_at"], "observed_at"),
expires_at=_datetime(raw["expires_at"], "expires_at"),
evidence_refs=_tuple_of_text(raw["evidence_refs"], "evidence_refs"),
)
def _unknown_fact(binding: OperationBinding, code: str) -> Fact:
return Fact(
value="UNKNOWN",
stamp=None,
binding_digest=binding_digest(binding),
observed_at=None,
expires_at=None,
evidence_refs=(f"operation-readback:{code}",),
)
def _fact_candidates(value: object) -> tuple[Fact, ...]:
if isinstance(value, Mapping):
return (_fact(value),)
if isinstance(value, Sequence) and not isinstance(value, (str, bytes, bytearray)):
if not value:
raise ValueError("fact candidate list is empty")
return tuple(_fact(item) for item in value)
raise ValueError("fact must be an object or non-empty candidate list")
def _select_fact(
value: object, binding: OperationBinding, *, expected_domain: str
) -> Fact:
candidates = _fact_candidates(value)
digest = binding_digest(binding)
if any(item.binding_digest != digest for item in candidates):
return _unknown_fact(binding, "BINDING_MISMATCH")
if any(item.stamp is None for item in candidates):
if len(candidates) == 1:
return candidates[0]
return _unknown_fact(binding, "INVALID_CANDIDATE_SET")
stamps = [item.stamp for item in candidates]
assert all(stamp is not None for stamp in stamps)
domains = {(stamp.domain, stamp.object_id) for stamp in stamps if stamp is not None}
if len(domains) != 1 or next(iter(domains))[0] != expected_domain:
return _unknown_fact(binding, "CROSS_DOMAIN_CANDIDATES")
highest = max((stamp.generation, stamp.revision) for stamp in stamps if stamp is not None)
current = [
item
for item in candidates
if item.stamp is not None
and (item.stamp.generation, item.stamp.revision) == highest
]
if len(current) > 1:
first = current[0]
# Event identity may differ for an idempotent duplicate. Every semantic
# field must still agree at the same authority revision.
semantic = (
first.value,
first.binding_digest,
first.observed_at,
first.expires_at,
first.evidence_refs,
)
if any(
(
item.value,
item.binding_digest,
item.observed_at,
item.expires_at,
item.evidence_refs,
)
!= semantic
for item in current[1:]
):
return _unknown_fact(binding, "SAME_REVISION_CONFLICT")
return current[0]
def _read(reader: Reader | None, binding: OperationBinding) -> Mapping[str, object] | None:
if reader is None:
return None
try:
value = reader(binding)
except Exception:
return None
return value if isinstance(value, Mapping) else None
def collect_observation(
binding: OperationBinding,
readers: OperationReaders,
*,
now: datetime,
) -> OperationObservation:
"""Query three authority seams and build a typed preflight observation."""
_datetime(now, "now")
declared = installed = callable_fact = _unknown_fact(binding, "DECLARATION_UNAVAILABLE")
raw = _read(readers.declaration, binding)
if raw is not None:
try:
doc = _exact_mapping(raw, _DECLARATION_FIELDS, "declaration reader result")
if doc["schema"] != "aios.operation-declaration-observation.v1":
raise ValueError("unsupported declaration schema")
declared = _select_fact(
doc["declared"], binding, expected_domain="declaration"
)
installed = _select_fact(
doc["installed"], binding, expected_domain="runtime"
)
callable_fact = _select_fact(
doc["callable"], binding, expected_domain="runtime"
)
except (TypeError, ValueError):
declared = installed = callable_fact = _unknown_fact(
binding, "INVALID_DECLARATION_OBSERVATION"
)
preconditions = _unknown_fact(binding, "TASK_ADAPTER_UNAVAILABLE")
raw = _read(readers.task, binding)
if raw is not None:
try:
doc = _exact_mapping(raw, _TASK_FIELDS, "task reader result")
if doc["schema"] != "aios.operation-task-observation.v1":
raise ValueError("unsupported task schema")
preconditions = _select_fact(
doc["preconditions"], binding, expected_domain="task"
)
except (TypeError, ValueError):
preconditions = _unknown_fact(binding, "INVALID_TASK_OBSERVATION")
authorized = _unknown_fact(binding, "AUTHORIZATION_ADAPTER_UNAVAILABLE")
raw = _read(readers.authorization, binding)
if raw is not None:
try:
doc = _exact_mapping(raw, _AUTHORIZATION_FIELDS, "authorization reader result")
if doc["schema"] != "aios.operation-authorization-observation.v1":
raise ValueError("unsupported authorization schema")
authorized = _select_fact(
doc["authorized"], binding, expected_domain="authorization"
)
except (TypeError, ValueError):
authorized = _unknown_fact(binding, "INVALID_AUTHORIZATION_OBSERVATION")
return OperationObservation(
binding=binding,
declared=declared,
installed=installed,
callable=callable_fact,
authorized=authorized,
preconditions=preconditions,
)
def _unknown_effect(binding: OperationBinding) -> EffectObservation:
return EffectObservation(
binding_digest=binding_digest(binding),
outcome="UNKNOWN",
receipt_ref=None,
result_artifact_digest=None,
actor_id=None,
stamp=None,
observed_at=None,
expires_at=None,
trust_status="UNTRUSTED",
)
def _effect(value: object) -> EffectObservation:
raw = _exact_mapping(value, _EFFECT_FIELDS, "effect")
return EffectObservation(
binding_digest=_text(raw["binding_digest"], "binding_digest"),
outcome=raw["outcome"], # type: ignore[arg-type]
receipt_ref=_optional_text(raw["receipt_ref"], "receipt_ref"),
result_artifact_digest=_optional_text(
raw["result_artifact_digest"], "result_artifact_digest"
),
actor_id=_optional_text(raw["actor_id"], "actor_id"),
stamp=_stamp(raw["stamp"]),
observed_at=_datetime(raw["observed_at"], "observed_at"),
expires_at=_datetime(raw["expires_at"], "expires_at"),
trust_status=raw["trust_status"], # type: ignore[arg-type]
)
def _unknown_verification(binding: OperationBinding) -> VerificationObservation:
return VerificationObservation(
binding_digest=binding_digest(binding),
receipt_ref=None,
result_artifact_digest=None,
verifier_id=None,
outcome="UNAVAILABLE",
authenticated=False,
observed_at=None,
expires_at=None,
verification_receipt_ref=None,
)
def _verification(value: object) -> VerificationObservation:
raw = _exact_mapping(value, _VERIFICATION_FIELDS, "verification")
return VerificationObservation(
binding_digest=_text(raw["binding_digest"], "binding_digest"),
receipt_ref=_optional_text(raw["receipt_ref"], "receipt_ref"),
result_artifact_digest=_optional_text(
raw["result_artifact_digest"], "result_artifact_digest"
),
verifier_id=_optional_text(raw["verifier_id"], "verifier_id"),
outcome=raw["outcome"], # type: ignore[arg-type]
authenticated=raw["authenticated"], # type: ignore[arg-type]
observed_at=_datetime(raw["observed_at"], "observed_at"),
expires_at=_datetime(raw["expires_at"], "expires_at"),
verification_receipt_ref=_optional_text(
raw["verification_receipt_ref"], "verification_receipt_ref"
),
)
def _unknown_acceptance(binding: OperationBinding) -> AcceptanceObservation:
return AcceptanceObservation(
binding_digest=binding_digest(binding),
receipt_ref=None,
result_artifact_digest=None,
verifier_receipt_ref=None,
acceptance_revision=None,
acceptor_id=None,
outcome="UNKNOWN",
authenticated=False,
observed_at=None,
expires_at=None,
)
def _acceptance(value: object) -> AcceptanceObservation:
raw = _exact_mapping(value, _ACCEPTANCE_OBSERVATION_FIELDS, "acceptance")
return AcceptanceObservation(
binding_digest=_text(raw["binding_digest"], "binding_digest"),
receipt_ref=_optional_text(raw["receipt_ref"], "receipt_ref"),
result_artifact_digest=_optional_text(
raw["result_artifact_digest"], "result_artifact_digest"
),
verifier_receipt_ref=_optional_text(
raw["verifier_receipt_ref"], "verifier_receipt_ref"
),
acceptance_revision=_optional_text(
raw["acceptance_revision"], "acceptance_revision"
),
acceptor_id=_optional_text(raw["acceptor_id"], "acceptor_id"),
outcome=raw["outcome"], # type: ignore[arg-type]
authenticated=raw["authenticated"], # type: ignore[arg-type]
observed_at=_datetime(raw["observed_at"], "observed_at"),
expires_at=_datetime(raw["expires_at"], "expires_at"),
)
def collect_closure(
binding: OperationBinding,
readers: OperationReaders,
*,
now: datetime,
) -> ClosureObservation:
"""Read closure claims independently; absent adapters remain UNKNOWN."""
_datetime(now, "now")
effect = _unknown_effect(binding)
raw = _read(readers.gitops, binding)
if raw is not None:
try:
doc = _exact_mapping(raw, _GITOPS_FIELDS, "gitops reader result")
if doc["schema"] != "aios.operation-effect-observation.v1":
raise ValueError("unsupported effect schema")
effect = _effect(doc["effect"])
except (TypeError, ValueError):
effect = _unknown_effect(binding)
verification = _unknown_verification(binding)
raw = _read(readers.verifier, binding)
if raw is not None:
try:
doc = _exact_mapping(raw, _VERIFIER_FIELDS, "verifier reader result")
if doc["schema"] != "aios.operation-verification-observation.v1":
raise ValueError("unsupported verifier schema")
verification = _verification(doc["verification"])
except (TypeError, ValueError):
verification = _unknown_verification(binding)
acceptance = _unknown_acceptance(binding)
expected_acceptor_id = "unresolved-acceptor"
expected_acceptance_revision = "unresolved-revision"
raw = _read(readers.acceptance, binding)
if raw is not None:
try:
doc = _exact_mapping(raw, _ACCEPTANCE_FIELDS, "acceptance reader result")
if doc["schema"] != "aios.operation-acceptance-observation.v1":
raise ValueError("unsupported acceptance schema")
acceptance = _acceptance(doc["acceptance"])
expected_acceptor_id = _text(
doc["expected_acceptor_id"], "expected_acceptor_id"
)
expected_acceptance_revision = _text(
doc["expected_acceptance_revision"],
"expected_acceptance_revision",
)
except (TypeError, ValueError):
acceptance = _unknown_acceptance(binding)
return ClosureObservation(
effect=effect,
verification=verification,
acceptance=acceptance,
expected_acceptor_id=expected_acceptor_id,
expected_acceptance_revision=expected_acceptance_revision,
)
__all__ = ["OperationReaders", "collect_closure", "collect_observation"]
@@ -0,0 +1,547 @@
"""Read-only C6 operation observations and fail-closed reductions.
This module deliberately owns no persistence, signer, lease, or mutation port.
Its digests are local to the C6 operation-binding schema and do not replace any
existing domain encoder.
"""
from __future__ import annotations
from dataclasses import asdict, dataclass
from datetime import datetime
import hashlib
import json
import re
from typing import Literal
FactValue = Literal["TRUE", "FALSE", "UNKNOWN"]
PreflightState = Literal[
"EXECUTION_ENTRYPOINT_ABSENT",
"CAPABILITY_UNAVAILABLE",
"AUTHORIZATION_REQUIRED",
"PRECONDITION_FAILED",
"UNKNOWN",
"READY",
]
EffectOutcome = Literal["COMMITTED", "NOT_COMMITTED", "UNKNOWN"]
VerificationOutcome = Literal["PASS", "FAIL", "UNKNOWN", "UNAVAILABLE"]
AcceptanceOutcome = Literal["ACCEPTED", "REJECTED", "UNKNOWN"]
ClosureOutcome = Literal["SUCCEEDED", "PARTIAL", "OUTCOME_UNKNOWN"]
EffectReconciliation = Literal[
"REBUILD_RECEIPT_REFERENCE",
"REVALIDATE_BEFORE_RETRY",
"OUTCOME_UNKNOWN",
]
_OID_RE = re.compile(r"^[0-9a-f]{40}$")
_DIGEST_RE = re.compile(r"^[0-9a-f]{64}$")
_EFFECT_KEY_RE = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._:/@+-]{0,159}$")
_CAPABILITY_NAMESPACES = frozenset({"agent-task", "task-plane-wildcard"})
def _string(value: object, name: str) -> str:
if not isinstance(value, str) or not value or value != value.strip():
raise ValueError(f"{name} must be a non-empty canonical string")
if "\x00" in value:
raise ValueError(f"{name} contains NUL")
return value
def _non_negative_int(value: object, name: str) -> int:
if isinstance(value, bool) or not isinstance(value, int) or value < 0:
raise ValueError(f"{name} must be a non-negative integer")
return value
def _aware_utc(value: object, name: str) -> datetime:
if not isinstance(value, datetime) or value.tzinfo is None:
raise ValueError(f"{name} must be timezone-aware")
if value.utcoffset() is None:
raise ValueError(f"{name} must have a UTC offset")
return value
def validate_oid(value: str) -> str:
if not isinstance(value, str) or _OID_RE.fullmatch(value) is None:
raise ValueError("OID must be 40 lowercase hexadecimal characters")
return value
def validate_digest(value: str) -> str:
if not isinstance(value, str) or _DIGEST_RE.fullmatch(value) is None:
raise ValueError("digest must be 64 lowercase hexadecimal characters")
return value
def validate_effect_key(value: str) -> str:
if not isinstance(value, str) or _EFFECT_KEY_RE.fullmatch(value) is None:
raise ValueError("effect key has invalid length or characters")
return value
@dataclass(frozen=True)
class OperationBinding:
operation_id: str
actor_id: str
tenant_id: str
repository: str
capability_namespace: str
capability_id: str
task_id: str
job_id: str
generation: int
input_kind: str
source_oid: str
patch_digest: str
target_branch: str
scope_digest: str
policy_digest: str
plan_digest: str
effect_key: str
def __post_init__(self) -> None:
for name in (
"operation_id",
"actor_id",
"tenant_id",
"repository",
"capability_id",
"task_id",
"job_id",
"input_kind",
"target_branch",
):
_string(getattr(self, name), name)
if self.capability_namespace not in _CAPABILITY_NAMESPACES:
raise ValueError("unsupported capability namespace")
_non_negative_int(self.generation, "generation")
validate_oid(self.source_oid)
for name in ("patch_digest", "scope_digest", "policy_digest", "plan_digest"):
validate_digest(getattr(self, name))
validate_effect_key(self.effect_key)
def binding_digest(binding: OperationBinding) -> str:
if not isinstance(binding, OperationBinding):
raise TypeError("binding must be OperationBinding")
payload = json.dumps(
asdict(binding),
sort_keys=True,
separators=(",", ":"),
ensure_ascii=False,
allow_nan=False,
).encode("utf-8")
return hashlib.sha256(payload).hexdigest()
@dataclass(frozen=True)
class DomainStamp:
domain: str
object_id: str
generation: int
revision: int
event_id: str
def __post_init__(self) -> None:
_string(self.domain, "domain")
_string(self.object_id, "object_id")
_non_negative_int(self.generation, "generation")
_non_negative_int(self.revision, "revision")
_string(self.event_id, "event_id")
@dataclass(frozen=True)
class Fact:
value: FactValue
stamp: DomainStamp | None
binding_digest: str
observed_at: datetime | None
expires_at: datetime | None
evidence_refs: tuple[str, ...]
def __post_init__(self) -> None:
if self.value not in ("TRUE", "FALSE", "UNKNOWN"):
raise ValueError("invalid fact value")
validate_digest(self.binding_digest)
if not isinstance(self.evidence_refs, tuple):
raise ValueError("evidence_refs must be a tuple")
for ref in self.evidence_refs:
_string(ref, "evidence_ref")
if self.value != "UNKNOWN" and self.stamp is None:
raise ValueError("known fact requires an authority stamp")
if (self.observed_at is None) != (self.expires_at is None):
raise ValueError("fact timestamps must both be present or absent")
if self.observed_at is None:
if self.value != "UNKNOWN":
raise ValueError("known fact requires timestamps")
return
observed_at = _aware_utc(self.observed_at, "observed_at")
expires_at = _aware_utc(self.expires_at, "expires_at")
if expires_at <= observed_at:
raise ValueError("expires_at must be after observed_at")
@dataclass(frozen=True)
class OperationObservation:
binding: OperationBinding
declared: Fact
installed: Fact
callable: Fact
authorized: Fact
preconditions: Fact
@dataclass(frozen=True)
class PreflightResult:
state: PreflightState
next_action: str
reason_code: str
retryable: bool
binding_digest: str
evidence_refs: tuple[str, ...]
def _now(value: datetime) -> datetime:
return _aware_utc(value, "now")
def _fact_invalid_reason(fact: Fact, expected_digest: str, now: datetime) -> str | None:
if fact.binding_digest != expected_digest:
return "BINDING_MISMATCH"
if fact.value == "UNKNOWN":
return "OBSERVATION_UNKNOWN"
if fact.stamp is None or fact.observed_at is None or fact.expires_at is None:
return "INVALID_OBSERVATION"
if fact.observed_at > now:
return "FUTURE_OBSERVATION"
if now >= fact.expires_at:
return "STALE_OBSERVATION"
return None
def derive_preflight(
observation: OperationObservation, *, now: datetime
) -> PreflightResult:
"""Reduce fresh authority observations without invoking any external port."""
now = _now(now)
if not isinstance(observation, OperationObservation):
raise TypeError("observation must be OperationObservation")
digest = binding_digest(observation.binding)
evidence: list[str] = []
frontier = (
(
"declared",
observation.declared,
"EXECUTION_ENTRYPOINT_ABSENT",
"ENGINEERING_CHANGE_REQUIRED",
"EXECUTION_ENTRYPOINT_ABSENT",
),
(
"installed",
observation.installed,
"CAPABILITY_UNAVAILABLE",
"REPAIR_DECLARED_EXECUTOR",
"EXECUTOR_NOT_INSTALLED",
),
(
"callable",
observation.callable,
"CAPABILITY_UNAVAILABLE",
"REPAIR_DECLARED_EXECUTOR",
"EXECUTOR_NOT_CALLABLE",
),
(
"authorized",
observation.authorized,
"AUTHORIZATION_REQUIRED",
"PREPARE_EXACT_AUTH_REQUEST",
"EXACT_AUTHORIZATION_MISSING",
),
(
"preconditions",
observation.preconditions,
"PRECONDITION_FAILED",
"REPAIR_PRECONDITION",
"PRECONDITION_FAILED",
),
)
for name, fact, false_state, false_action, false_reason in frontier:
evidence.extend(fact.evidence_refs)
invalid_reason = _fact_invalid_reason(fact, digest, now)
if invalid_reason is not None:
return PreflightResult(
state="UNKNOWN",
next_action="OBSERVE_OR_RECONCILE",
reason_code=f"{name.upper()}_{invalid_reason}",
retryable=False,
binding_digest=digest,
evidence_refs=tuple(dict.fromkeys(evidence)),
)
if fact.value == "FALSE":
return PreflightResult(
state=false_state, # type: ignore[arg-type]
next_action=false_action,
reason_code=false_reason,
retryable=False,
binding_digest=digest,
evidence_refs=tuple(dict.fromkeys(evidence)),
)
return PreflightResult(
state="READY",
next_action="EXECUTE_GOVERNED_PLAN",
reason_code="ALL_REQUIRED_FACTS_FRESH_AND_TRUE",
retryable=False,
binding_digest=digest,
evidence_refs=tuple(dict.fromkeys(evidence)),
)
@dataclass(frozen=True)
class EffectObservation:
binding_digest: str
outcome: EffectOutcome
receipt_ref: str | None
result_artifact_digest: str | None
actor_id: str | None
stamp: DomainStamp | None
observed_at: datetime | None
expires_at: datetime | None
trust_status: Literal["TRUSTED", "UNTRUSTED", "SHADOW"] = "TRUSTED"
def __post_init__(self) -> None:
validate_digest(self.binding_digest)
if self.outcome not in ("COMMITTED", "NOT_COMMITTED", "UNKNOWN"):
raise ValueError("invalid effect outcome")
if self.trust_status not in ("TRUSTED", "UNTRUSTED", "SHADOW"):
raise ValueError("invalid effect trust status")
for value, name in ((self.receipt_ref, "receipt_ref"), (self.actor_id, "actor_id")):
if value is not None:
_string(value, name)
if self.result_artifact_digest is not None:
validate_digest(self.result_artifact_digest)
if (self.observed_at is None) != (self.expires_at is None):
raise ValueError("effect timestamps must both be present or absent")
if self.observed_at is not None:
observed_at = _aware_utc(self.observed_at, "observed_at")
expires_at = _aware_utc(self.expires_at, "expires_at")
if expires_at <= observed_at:
raise ValueError("expires_at must be after observed_at")
@dataclass(frozen=True)
class VerificationObservation:
binding_digest: str
receipt_ref: str | None
result_artifact_digest: str | None
verifier_id: str | None
outcome: VerificationOutcome
authenticated: bool
observed_at: datetime | None
expires_at: datetime | None
verification_receipt_ref: str | None = None
def __post_init__(self) -> None:
validate_digest(self.binding_digest)
if self.outcome not in ("PASS", "FAIL", "UNKNOWN", "UNAVAILABLE"):
raise ValueError("invalid verification outcome")
if type(self.authenticated) is not bool:
raise ValueError("authenticated must be bool")
for value, name in (
(self.receipt_ref, "receipt_ref"),
(self.verifier_id, "verifier_id"),
(self.verification_receipt_ref, "verification_receipt_ref"),
):
if value is not None:
_string(value, name)
if self.result_artifact_digest is not None:
validate_digest(self.result_artifact_digest)
_validate_optional_times(self.observed_at, self.expires_at, "verification")
@dataclass(frozen=True)
class AcceptanceObservation:
binding_digest: str
receipt_ref: str | None
result_artifact_digest: str | None
verifier_receipt_ref: str | None
acceptance_revision: str | None
acceptor_id: str | None
outcome: AcceptanceOutcome
authenticated: bool
observed_at: datetime | None
expires_at: datetime | None
def __post_init__(self) -> None:
validate_digest(self.binding_digest)
if self.outcome not in ("ACCEPTED", "REJECTED", "UNKNOWN"):
raise ValueError("invalid acceptance outcome")
if type(self.authenticated) is not bool:
raise ValueError("authenticated must be bool")
for value, name in (
(self.receipt_ref, "receipt_ref"),
(self.verifier_receipt_ref, "verifier_receipt_ref"),
(self.acceptance_revision, "acceptance_revision"),
(self.acceptor_id, "acceptor_id"),
):
if value is not None:
_string(value, name)
if self.result_artifact_digest is not None:
validate_digest(self.result_artifact_digest)
_validate_optional_times(self.observed_at, self.expires_at, "acceptance")
def _validate_optional_times(
observed_at: datetime | None, expires_at: datetime | None, prefix: str
) -> None:
if (observed_at is None) != (expires_at is None):
raise ValueError(f"{prefix} timestamps must both be present or absent")
if observed_at is None:
return
observed = _aware_utc(observed_at, "observed_at")
expires = _aware_utc(expires_at, "expires_at")
if expires <= observed:
raise ValueError("expires_at must be after observed_at")
@dataclass(frozen=True)
class ClosureObservation:
effect: EffectObservation
verification: VerificationObservation
acceptance: AcceptanceObservation
expected_acceptor_id: str
expected_acceptance_revision: str
def __post_init__(self) -> None:
_string(self.expected_acceptor_id, "expected_acceptor_id")
_string(self.expected_acceptance_revision, "expected_acceptance_revision")
@dataclass(frozen=True)
class ClosureResult:
execution_recorded: bool
independently_verified: bool
business_accepted: bool
outcome: ClosureOutcome
reason_code: str
def _fresh(
observed_at: datetime | None, expires_at: datetime | None, now: datetime
) -> bool:
return (
observed_at is not None
and expires_at is not None
and observed_at <= now < expires_at
)
def reconcile_effect(
effect: EffectObservation, *, binding: OperationBinding, now: datetime
) -> EffectReconciliation:
"""Choose a safe recovery route; this function never retries the effect."""
now = _now(now)
digest = binding_digest(binding)
if (
effect.binding_digest != digest
or effect.stamp is None
or effect.trust_status != "TRUSTED"
or not _fresh(effect.observed_at, effect.expires_at, now)
):
return "OUTCOME_UNKNOWN"
if effect.outcome == "COMMITTED":
if (
effect.receipt_ref is not None
and effect.result_artifact_digest is not None
and effect.actor_id is not None
):
return "REBUILD_RECEIPT_REFERENCE"
return "OUTCOME_UNKNOWN"
if effect.outcome == "NOT_COMMITTED":
# stamp + fresh trusted authority receipt is the deterministic negative
# observation. Revalidation is still required before any governed retry.
if effect.receipt_ref is not None and effect.result_artifact_digest is None:
return "REVALIDATE_BEFORE_RETRY"
return "OUTCOME_UNKNOWN"
def derive_closure(
binding: OperationBinding,
observation: ClosureObservation,
*,
now: datetime,
) -> ClosureResult:
"""Reduce execution, verification, and acceptance as three distinct claims."""
now = _now(now)
digest = binding_digest(binding)
effect = observation.effect
execution_recorded = (
effect.binding_digest == digest
and effect.outcome == "COMMITTED"
and effect.trust_status == "TRUSTED"
and effect.stamp is not None
and effect.receipt_ref is not None
and effect.result_artifact_digest is not None
and effect.actor_id is not None
and _fresh(effect.observed_at, effect.expires_at, now)
)
if not execution_recorded:
outcome: ClosureOutcome = (
"OUTCOME_UNKNOWN" if effect.outcome == "UNKNOWN" else "PARTIAL"
)
return ClosureResult(False, False, False, outcome, "EXECUTION_NOT_RECORDED")
verification = observation.verification
verifier_receipt_ref = (
verification.verification_receipt_ref or verification.receipt_ref
)
independently_verified = (
verification.binding_digest == digest
and verification.receipt_ref == effect.receipt_ref
and verification.result_artifact_digest == effect.result_artifact_digest
and verification.verifier_id is not None
and verification.verifier_id != effect.actor_id
and verification.outcome == "PASS"
and verification.authenticated is True
and verifier_receipt_ref is not None
and _fresh(verification.observed_at, verification.expires_at, now)
)
if not independently_verified:
return ClosureResult(True, False, False, "PARTIAL", "INDEPENDENT_VERIFICATION_MISSING")
acceptance = observation.acceptance
business_accepted = (
acceptance.binding_digest == digest
and acceptance.receipt_ref == effect.receipt_ref
and acceptance.result_artifact_digest == effect.result_artifact_digest
and acceptance.verifier_receipt_ref == verifier_receipt_ref
and acceptance.acceptance_revision
== observation.expected_acceptance_revision
and acceptance.acceptor_id == observation.expected_acceptor_id
and acceptance.outcome == "ACCEPTED"
and acceptance.authenticated is True
and _fresh(acceptance.observed_at, acceptance.expires_at, now)
)
if not business_accepted:
return ClosureResult(True, True, False, "PARTIAL", "AUTHORIZED_ACCEPTANCE_MISSING")
return ClosureResult(True, True, True, "SUCCEEDED", "THREE_LAYER_CLOSURE_VALID")
__all__ = [
"AcceptanceObservation",
"ClosureObservation",
"ClosureResult",
"DomainStamp",
"EffectObservation",
"Fact",
"OperationBinding",
"OperationObservation",
"PreflightResult",
"VerificationObservation",
"binding_digest",
"derive_closure",
"derive_preflight",
"reconcile_effect",
"validate_digest",
"validate_effect_key",
"validate_oid",
]
@@ -0,0 +1,533 @@
"""Strict, pure validation for the C6 executable source-patch contract.
Validation here proves contract and unified-diff shape only. The governed
executor remains responsible for secure checkout containment, isolated apply,
result-file hashing, live authorization, leases/fences, and CAS.
"""
from __future__ import annotations
from collections.abc import Mapping, Sequence
from dataclasses import dataclass
import hashlib
import json
from pathlib import PurePosixPath
import re
from typing import Literal
from .operation_view import (
OperationBinding,
validate_digest,
validate_effect_key,
validate_oid,
)
_TOP_FIELDS = frozenset(
{
"schema",
"input_kind",
"type",
"target",
"before_hash",
"after_hash",
"operations",
"rollback",
"verification",
"operation_id",
"actor_id",
"tenant_id",
"capability_namespace",
"capability_id",
"task_id",
"job_id",
"generation",
"executor_ref",
"repository",
"base_oid",
"target_branch",
"patch_digest",
"file_digests",
"scope_digest",
"policy_digest",
"plan_digest",
"effect_key",
"rollback_artifact_digest",
"verification_contract_revision",
"acceptance_contract_revision",
}
)
_TARGET_FIELDS = frozenset({"repository", "branch"})
_OPERATION_FIELDS = frozenset({"path", "kind", "before_hash", "after_hash"})
_ROLLBACK_FIELDS = frozenset({"artifact_digest", "expected_after_hash"})
_VERIFICATION_FIELDS = frozenset(
{"revision", "acceptance_revision", "required_checks"}
)
_PROTECTED_PATHS = frozenset({".git", ".gitmodules"})
_DIFF_HEADER_RE = re.compile(r"^diff --git a/([^\s]+) b/([^\s]+)$")
def _exact_mapping(value: object, fields: frozenset[str], name: str) -> Mapping[str, object]:
if not isinstance(value, Mapping) or set(value) != fields:
raise ValueError(f"{name} has unknown or missing fields")
if any(not isinstance(key, str) for key in value):
raise ValueError(f"{name} keys must be strings")
return value
def _text(value: object, name: str) -> str:
if not isinstance(value, str) or not value or value != value.strip() or "\x00" in value:
raise ValueError(f"{name} must be a canonical non-empty string")
return value
def _integer(value: object, name: str) -> int:
if isinstance(value, bool) or not isinstance(value, int) or value < 0:
raise ValueError(f"{name} must be a non-negative integer")
return value
def _path(value: object) -> str:
path = _text(value, "path")
if "\\" in path or ":" in path or path.startswith(("/", "//")):
raise ValueError("path must be repository-relative canonical POSIX")
parsed = PurePosixPath(path)
if path != parsed.as_posix() or any(part in ("", ".", "..") for part in parsed.parts):
raise ValueError("path must be repository-relative canonical POSIX")
if any(part.casefold() == ".git" for part in parsed.parts) or path.casefold() in _PROTECTED_PATHS:
raise ValueError("protected Git metadata is outside patch scope")
return path
def _content_hash(value: object, name: str, *, literal: str | None = None) -> str:
text = _text(value, name)
if literal is not None and text == literal:
return text
return validate_digest(text)
@dataclass(frozen=True)
class PatchTarget:
repository: str
branch: str
def __post_init__(self) -> None:
_text(self.repository, "repository")
_text(self.branch, "branch")
@dataclass(frozen=True)
class PatchFileOperation:
path: str
kind: Literal["ADD", "MODIFY", "DELETE"]
before_hash: str
after_hash: str
def __post_init__(self) -> None:
_path(self.path)
if self.kind not in ("ADD", "MODIFY", "DELETE"):
raise ValueError("unsupported patch operation")
if self.kind == "ADD":
if self.before_hash != "ABSENT":
raise ValueError("ADD before_hash must be ABSENT")
validate_digest(self.after_hash)
elif self.kind == "DELETE":
validate_digest(self.before_hash)
if self.after_hash != "DELETED":
raise ValueError("DELETE after_hash must be DELETED")
else:
validate_digest(self.before_hash)
validate_digest(self.after_hash)
@dataclass(frozen=True)
class RollbackContract:
artifact_digest: str
expected_after_hash: str
def __post_init__(self) -> None:
validate_digest(self.artifact_digest)
validate_digest(self.expected_after_hash)
@dataclass(frozen=True)
class VerificationContract:
revision: str
acceptance_revision: str
required_checks: tuple[str, ...]
def __post_init__(self) -> None:
_text(self.revision, "verification revision")
_text(self.acceptance_revision, "acceptance revision")
if not isinstance(self.required_checks, tuple) or not self.required_checks:
raise ValueError("required_checks must be a non-empty tuple")
if len(set(self.required_checks)) != len(self.required_checks):
raise ValueError("required_checks contains duplicates")
for check in self.required_checks:
_text(check, "required check")
@dataclass(frozen=True)
class ExecutablePatchV1:
schema: Literal["aios.executable-patch.v1"]
input_kind: Literal["EXECUTABLE_PATCH"]
type: Literal["git_patch"]
target: PatchTarget
before_hash: str
after_hash: str
operations: tuple[PatchFileOperation, ...]
rollback: RollbackContract
verification: VerificationContract
operation_id: str
actor_id: str
tenant_id: str
capability_namespace: str
capability_id: str
task_id: str
job_id: str
generation: int
executor_ref: Literal["governed_source_patch_apply"]
repository: str
base_oid: str
target_branch: str
patch_digest: str
file_digests: tuple[tuple[str, str], ...]
scope_digest: str
policy_digest: str
plan_digest: str
effect_key: str
rollback_artifact_digest: str
verification_contract_revision: str
acceptance_contract_revision: str
def _aggregate_hash(
operations: tuple[PatchFileOperation, ...], attribute: Literal["before_hash", "after_hash"]
) -> str:
values = [
{"path": item.path, "hash": getattr(item, attribute)}
for item in sorted(operations, key=lambda operation: operation.path)
]
encoded = json.dumps(
values,
sort_keys=True,
separators=(",", ":"),
ensure_ascii=False,
allow_nan=False,
).encode("utf-8")
return hashlib.sha256(encoded).hexdigest()
def _parse_target(value: object) -> PatchTarget:
raw = _exact_mapping(value, _TARGET_FIELDS, "target")
return PatchTarget(
repository=_text(raw["repository"], "target.repository"),
branch=_text(raw["branch"], "target.branch"),
)
def _parse_operation(value: object) -> PatchFileOperation:
raw = _exact_mapping(value, _OPERATION_FIELDS, "operation")
kind = raw["kind"]
if kind not in ("ADD", "MODIFY", "DELETE"):
raise ValueError("unsupported patch operation")
before = _content_hash(
raw["before_hash"],
"before_hash",
literal="ABSENT" if kind == "ADD" else None,
)
after = _content_hash(
raw["after_hash"],
"after_hash",
literal="DELETED" if kind == "DELETE" else None,
)
return PatchFileOperation(
path=_path(raw["path"]),
kind=kind, # type: ignore[arg-type]
before_hash=before,
after_hash=after,
)
def _parse_operations(value: object) -> tuple[PatchFileOperation, ...]:
if not isinstance(value, (tuple, list)) or isinstance(value, (str, bytes)) or not value:
raise ValueError("operations must be a non-empty array")
operations = tuple(_parse_operation(item) for item in value)
paths = [item.path for item in operations]
if len(paths) != len(set(paths)) or len(paths) != len({item.casefold() for item in paths}):
raise ValueError("operation paths must be unique, including case-folding")
return operations
def _parse_rollback(value: object) -> RollbackContract:
raw = _exact_mapping(value, _ROLLBACK_FIELDS, "rollback")
return RollbackContract(
artifact_digest=validate_digest(_text(raw["artifact_digest"], "artifact_digest")),
expected_after_hash=validate_digest(
_text(raw["expected_after_hash"], "expected_after_hash")
),
)
def _parse_verification(value: object) -> VerificationContract:
raw = _exact_mapping(value, _VERIFICATION_FIELDS, "verification")
checks = raw["required_checks"]
if not isinstance(checks, (list, tuple)) or isinstance(checks, (str, bytes)):
raise ValueError("required_checks must be an array")
return VerificationContract(
revision=_text(raw["revision"], "verification revision"),
acceptance_revision=_text(
raw["acceptance_revision"], "acceptance revision"
),
required_checks=tuple(_text(item, "required check") for item in checks),
)
def _parse_file_digests(value: object) -> tuple[tuple[str, str], ...]:
if not isinstance(value, (list, tuple)) or isinstance(value, (str, bytes)):
raise ValueError("file_digests must be an array of pairs")
pairs: list[tuple[str, str]] = []
for item in value:
if not isinstance(item, (list, tuple)) or len(item) != 2:
raise ValueError("file_digests entries must be path/digest pairs")
path = _path(item[0])
digest = _content_hash(item[1], "file digest", literal="DELETED")
pairs.append((path, digest))
paths = [path for path, _ in pairs]
if len(paths) != len(set(paths)) or len(paths) != len({item.casefold() for item in paths}):
raise ValueError("file_digests paths must be unique")
return tuple(sorted(pairs))
def _strip_patch_path(line: str, prefix: str) -> str:
value = line[len(prefix) :]
if "\t" in value:
raise ValueError("timestamped unified-diff paths are unsupported")
if value == "/dev/null":
return value
expected = "a/" if prefix == "--- " else "b/"
if not value.startswith(expected):
raise ValueError("diff path has unexpected prefix")
return _path(value[2:])
def _actual_diff_operations(patch_bytes: bytes) -> tuple[tuple[str, str], ...]:
if not isinstance(patch_bytes, bytes) or not patch_bytes:
raise ValueError("patch_bytes must be non-empty bytes")
if b"\x00" in patch_bytes:
raise ValueError("binary patches are unsupported")
try:
text = patch_bytes.decode("utf-8")
except UnicodeDecodeError as exc:
raise ValueError("patch must be UTF-8 text") from exc
lines = text.splitlines()
starts = [index for index, line in enumerate(lines) if line.startswith("diff --git ")]
if not starts or any(line for line in lines[: starts[0]]):
raise ValueError("patch must consist only of git unified-diff blocks")
starts.append(len(lines))
actual: list[tuple[str, str]] = []
for position in range(len(starts) - 1):
block = lines[starts[position] : starts[position + 1]]
match = _DIFF_HEADER_RE.fullmatch(block[0])
if match is None:
raise ValueError("quoted, whitespace, or malformed diff paths are unsupported")
old_header_path = _path(match.group(1))
new_header_path = _path(match.group(2))
forbidden = (
"GIT binary patch",
"Binary files ",
"rename from ",
"rename to ",
"copy from ",
"copy to ",
"similarity index ",
"dissimilarity index ",
"old mode ",
"new mode ",
)
if any(line.startswith(forbidden) for line in block[1:]):
raise ValueError("rename, copy, binary, or mode-only patches are unsupported")
mode_lines = [
line
for line in block[1:]
if line.startswith(("new file mode ", "deleted file mode "))
]
if any(not line.endswith("100644") for line in mode_lines):
raise ValueError("symlink, executable, and submodule modes are unsupported")
old_lines = [line for line in block[1:] if line.startswith("--- ")]
new_lines = [line for line in block[1:] if line.startswith("+++ ")]
if len(old_lines) != 1 or len(new_lines) != 1:
raise ValueError("each diff block requires one old and new path")
old_path = _strip_patch_path(old_lines[0], "--- ")
new_path = _strip_patch_path(new_lines[0], "+++ ")
if not any(line.startswith("@@ ") for line in block[1:]):
raise ValueError("mode-only and empty diff blocks are unsupported")
if old_path == "/dev/null":
if new_path != new_header_path or old_header_path != new_header_path:
raise ValueError("ADD paths do not agree")
if mode_lines != ["new file mode 100644"]:
raise ValueError("ADD requires regular-text new file mode")
actual.append((new_path, "ADD"))
elif new_path == "/dev/null":
if old_path != old_header_path or old_header_path != new_header_path:
raise ValueError("DELETE paths do not agree")
if mode_lines != ["deleted file mode 100644"]:
raise ValueError("DELETE requires regular-text deleted file mode")
actual.append((old_path, "DELETE"))
else:
if mode_lines or len({old_path, new_path, old_header_path, new_header_path}) != 1:
raise ValueError("rename or mode-changing MODIFY is unsupported")
actual.append((old_path, "MODIFY"))
paths = [path for path, _ in actual]
if len(paths) != len(set(paths)) or len(paths) != len({item.casefold() for item in paths}):
raise ValueError("diff paths must be unique, including case-folding")
return tuple(sorted(actual))
def validate_patch_shape(patch_bytes: bytes, contract: ExecutablePatchV1) -> None:
actual = _actual_diff_operations(patch_bytes)
declared = tuple(sorted((item.path, item.kind) for item in contract.operations))
if actual != declared:
raise ValueError("actual diff file set or operation kinds do not match contract")
def parse_executable_patch(
raw: Mapping[str, object],
*,
patch_bytes: bytes,
allowed_files: frozenset[str],
expected_binding: OperationBinding,
) -> ExecutablePatchV1:
"""Parse a complete FD-005 contract without applying or executing it."""
document = _exact_mapping(raw, _TOP_FIELDS, "executable patch")
if document["schema"] != "aios.executable-patch.v1":
raise ValueError("unsupported executable patch schema")
if document["input_kind"] != "EXECUTABLE_PATCH":
raise ValueError("PATCH_PROPOSAL is not executable")
if document["type"] != "git_patch":
raise ValueError("unsupported executable patch type")
if document["executor_ref"] != "governed_source_patch_apply":
raise ValueError("executor_ref is not the governed pilot")
if not isinstance(allowed_files, frozenset):
raise ValueError("allowed_files must be a frozen manifest set")
canonical_allowed = frozenset(_path(item) for item in allowed_files)
if len(canonical_allowed) != len(allowed_files) or len(canonical_allowed) != len(
{item.casefold() for item in canonical_allowed}
):
raise ValueError("allowed_files contains aliases or case-fold collisions")
target = _parse_target(document["target"])
operations = _parse_operations(document["operations"])
rollback = _parse_rollback(document["rollback"])
verification = _parse_verification(document["verification"])
file_digests = _parse_file_digests(document["file_digests"])
before_hash = validate_digest(_text(document["before_hash"], "before_hash"))
after_hash = validate_digest(_text(document["after_hash"], "after_hash"))
patch_digest = validate_digest(_text(document["patch_digest"], "patch_digest"))
binding_fields = {
"operation_id": expected_binding.operation_id,
"actor_id": expected_binding.actor_id,
"tenant_id": expected_binding.tenant_id,
"capability_namespace": expected_binding.capability_namespace,
"capability_id": expected_binding.capability_id,
"task_id": expected_binding.task_id,
"job_id": expected_binding.job_id,
"generation": expected_binding.generation,
"input_kind": expected_binding.input_kind,
"repository": expected_binding.repository,
"target_branch": expected_binding.target_branch,
"patch_digest": expected_binding.patch_digest,
"scope_digest": expected_binding.scope_digest,
"policy_digest": expected_binding.policy_digest,
"plan_digest": expected_binding.plan_digest,
"effect_key": expected_binding.effect_key,
}
for name, expected in binding_fields.items():
if document[name] != expected:
raise ValueError(f"binding mismatch: {name}")
generation = _integer(document["generation"], "generation")
if generation != expected_binding.generation:
raise ValueError("binding mismatch: generation")
base_oid = validate_oid(_text(document["base_oid"], "base_oid"))
if base_oid != expected_binding.source_oid:
raise ValueError("binding mismatch: base_oid")
if target.repository != expected_binding.repository or target.branch != expected_binding.target_branch:
raise ValueError("target does not match top-level binding")
actual_patch_digest = hashlib.sha256(patch_bytes).hexdigest()
if patch_digest != actual_patch_digest:
raise ValueError("patch_digest does not match raw patch bytes")
if any(item.path not in canonical_allowed for item in operations):
raise ValueError("operation path is outside allowed_files")
expected_file_digests = tuple(sorted((item.path, item.after_hash) for item in operations))
if file_digests != expected_file_digests:
raise ValueError("file_digests do not match operation result hashes")
if before_hash != _aggregate_hash(operations, "before_hash"):
raise ValueError("before_hash does not match the operation manifest")
if after_hash != _aggregate_hash(operations, "after_hash"):
raise ValueError("after_hash does not match the operation manifest")
if rollback.expected_after_hash != after_hash:
raise ValueError("rollback expected_after_hash mismatch")
rollback_artifact_digest = validate_digest(
_text(document["rollback_artifact_digest"], "rollback_artifact_digest")
)
if rollback_artifact_digest != rollback.artifact_digest:
raise ValueError("rollback_artifact_digest mismatch")
verification_revision = _text(
document["verification_contract_revision"],
"verification_contract_revision",
)
acceptance_revision = _text(
document["acceptance_contract_revision"],
"acceptance_contract_revision",
)
if verification_revision != verification.revision:
raise ValueError("verification_contract_revision mismatch")
if acceptance_revision != verification.acceptance_revision:
raise ValueError("acceptance_contract_revision mismatch")
validate_effect_key(_text(document["effect_key"], "effect_key"))
contract = ExecutablePatchV1(
schema="aios.executable-patch.v1",
input_kind="EXECUTABLE_PATCH",
type="git_patch",
target=target,
before_hash=before_hash,
after_hash=after_hash,
operations=operations,
rollback=rollback,
verification=verification,
operation_id=expected_binding.operation_id,
actor_id=expected_binding.actor_id,
tenant_id=expected_binding.tenant_id,
capability_namespace=expected_binding.capability_namespace,
capability_id=expected_binding.capability_id,
task_id=expected_binding.task_id,
job_id=expected_binding.job_id,
generation=generation,
executor_ref="governed_source_patch_apply",
repository=expected_binding.repository,
base_oid=base_oid,
target_branch=expected_binding.target_branch,
patch_digest=patch_digest,
file_digests=file_digests,
scope_digest=expected_binding.scope_digest,
policy_digest=expected_binding.policy_digest,
plan_digest=expected_binding.plan_digest,
effect_key=expected_binding.effect_key,
rollback_artifact_digest=rollback_artifact_digest,
verification_contract_revision=verification_revision,
acceptance_contract_revision=acceptance_revision,
)
validate_patch_shape(patch_bytes, contract)
return contract
__all__ = [
"ExecutablePatchV1",
"PatchFileOperation",
"PatchTarget",
"RollbackContract",
"VerificationContract",
"parse_executable_patch",
"validate_patch_shape",
]
+39 -6
View File
@@ -54,13 +54,27 @@ class ReadonlyWorkerProfile:
class IdentityVerifier:
def __init__(self, trusted_identities: set[str] | None = None):
self.trusted_identities = trusted_identities
def __init__(
self,
trusted_identities: set[str] | None = None,
*,
verifier_identity: str | None = None,
):
self.trusted_identities = frozenset(trusted_identities or ())
self.verifier_identity = verifier_identity
@property
def is_configured(self) -> bool:
return any(identity != "*" for identity in self.trusted_identities)
def verify(self, identity: str) -> bool:
if self.trusted_identities is None:
return True
return identity in self.trusted_identities
return bool(
identity
and (
identity in self.trusted_identities
or "*" in self.trusted_identities
)
)
class EvidenceVerifier:
@@ -92,6 +106,7 @@ class PolicyEngine:
max_risk: str = "medium",
result_signature_verifier: Callable[[ResultEnvelope], bool] | None = None,
probe_attestation_verifier: Callable[[ProbeEvidence], bool] | None = None,
evidence_attestation_verifier: Callable[[object], bool] | None = None,
identity_capabilities: dict[str, set[str]] | None = None,
admin_identities: set[str] | None = None,
readonly_worker_execution_enabled: bool = False,
@@ -99,12 +114,28 @@ class PolicyEngine:
) -> None:
if max_risk not in RISK_RANK:
raise ValueError("invalid max_risk")
self.identity_verifier = identity_verifier or IdentityVerifier()
if execution_enabled and (
identity_verifier is None or not identity_verifier.is_configured
):
raise ValueError(
"execution_enabled requires an explicit trusted identity verifier"
)
if execution_enabled and evidence_attestation_verifier is None:
raise ValueError(
"execution_enabled requires an evidence attestation verifier"
)
configured_identities = {
identity for identity in (identity_capabilities or {}) if identity
}
self.identity_verifier = identity_verifier or IdentityVerifier(
configured_identities if not execution_enabled else None
)
self.evidence_verifier = evidence_verifier or EvidenceVerifier()
self.execution_enabled = execution_enabled
self.max_risk = max_risk
self.result_signature_verifier = result_signature_verifier
self.probe_attestation_verifier = probe_attestation_verifier
self.evidence_attestation_verifier = evidence_attestation_verifier
self.identity_capabilities = {
identity: set(capabilities)
for identity, capabilities in (identity_capabilities or {}).items()
@@ -195,6 +226,8 @@ class PolicyEngine:
"worker.register": "worker.self_register",
"worker.admin": "worker.admin",
}
if action not in action_capabilities:
return PolicyDecision(False, f"unknown action {action}")
required_capability = action_capabilities.get(action)
if required_capability:
granted = set(
@@ -2,6 +2,8 @@
from __future__ import annotations
import base64
import binascii
import hashlib
import json
import uuid
@@ -39,6 +41,22 @@ def _digest(value: Mapping[str, object]) -> str:
return hashlib.sha256(canonical_json(dict(value)).encode("utf-8")).hexdigest()
def _canonical_observation_value(value: object) -> object:
"""Convert a typed readback observation to stable JSON data."""
if isinstance(value, datetime):
if value.tzinfo is None or value.utcoffset() is None:
raise ConflictError("exact host closure time is not timezone-aware")
return value.astimezone(timezone.utc).isoformat().replace("+00:00", "Z")
if isinstance(value, Mapping):
return {
str(key): _canonical_observation_value(item)
for key, item in value.items()
}
if isinstance(value, (list, tuple)):
return [_canonical_observation_value(item) for item in value]
return value
@dataclass(frozen=True)
class AuthoritativeRootActionPlan:
review_id: str
@@ -265,6 +283,37 @@ class RootActionRunResult:
detail: str | None = None
@dataclass(frozen=True)
class ExactRootActionBinding:
"""Trusted host resolution from a C6 operation to an existing root review."""
review_id: str
mission_id: str
root_task_id: str
root_revision: int
plan_digest: str
execution_key: str
def __post_init__(self) -> None:
if any(
not isinstance(value, str) or not value
for value in (
self.review_id,
self.mission_id,
self.root_task_id,
self.plan_digest,
self.execution_key,
)
):
raise ValueError("exact root action binding identity required")
if (
isinstance(self.root_revision, bool)
or not isinstance(self.root_revision, int)
or self.root_revision < 0
):
raise ValueError("exact root action revision is invalid")
class RootActionExecutor(Protocol):
def reconcile(
self,
@@ -278,7 +327,7 @@ class RootActionExecutor(Protocol):
plan: AuthoritativeRootActionPlan,
*,
before_external_call: Callable[[], None],
) -> None: ...
) -> Mapping[str, object] | None: ...
def commit_effect(
self,
@@ -292,6 +341,7 @@ PlanBuilder = Callable[[RootReviewRecord], AuthoritativeRootActionPlan]
Rereviewer = Callable[
[AuthoritativeRootActionPlan, Mapping[str, object] | None], RootActionRereview
]
ExactReviewBindingProvider = Callable[[object], ExactRootActionBinding]
class RootActionConsumer:
@@ -304,12 +354,14 @@ class RootActionConsumer:
plan_builder: PlanBuilder,
rereviewer: Rereviewer,
executor: RootActionExecutor | None,
exact_review_binding_provider: ExactReviewBindingProvider | None = None,
clock: Callable[[], datetime] | None = None,
) -> None:
) -> Mapping[str, object]:
self.store = store
self._plan_builder = plan_builder
self._rereviewer = rereviewer
self._executor = executor
self._exact_review_binding_provider = exact_review_binding_provider
self._clock = clock or (lambda: datetime.now(tz=timezone.utc))
@staticmethod
@@ -416,9 +468,20 @@ class RootActionConsumer:
raise ConflictError("root action claim renewal CAS lost")
def _claim_next(
self, *, consumer_id: str, now: float, lease_seconds: int
self,
*,
consumer_id: str,
now: float,
lease_seconds: int,
exact_binding: ExactRootActionBinding | None = None,
) -> RootActionClaim | RootActionRunResult | None:
with self.store._transaction():
exact_sql = " AND d.review_id=?" if exact_binding is not None else ""
args: tuple[object, ...] = (
(now, exact_binding.review_id)
if exact_binding is not None
else (now,)
)
rows = self.store.conn.execute(
"SELECT d.*,r.record_json,r.root_revision_after "
"FROM task_root_action_dispatches d JOIN task_root_reviews r "
@@ -426,11 +489,13 @@ class RootActionConsumer:
"WHERE d.status IN ('EFFECT_UNKNOWN','DISPATCHED',"
"'EXECUTED_PENDING_REREVIEW','CLAIMED','PENDING') "
"AND (d.retry_at IS NULL OR d.retry_at<=?) "
+ exact_sql
+ " "
"ORDER BY r.root_revision_after DESC,CASE d.status "
"WHEN 'EFFECT_UNKNOWN' THEN 0 WHEN 'DISPATCHED' THEN 0 "
"WHEN 'EXECUTED_PENDING_REREVIEW' THEN 1 "
"WHEN 'CLAIMED' THEN 2 ELSE 3 END,d.created_at ASC",
(now,),
args,
).fetchall()
for row in rows:
if (
@@ -443,7 +508,11 @@ class RootActionConsumer:
}
and float(row["claim_expires_at"] or 0) > now
):
continue
if not (
exact_binding is not None
and row["claim_owner"] == consumer_id
):
continue
root_task_id = str(row["root_task_id"])
active = self.store.conn.execute(
"SELECT * FROM task_root_action_fences WHERE root_task_id=?",
@@ -488,6 +557,43 @@ class RootActionConsumer:
or plan.action != review.next_legal_action
):
raise ConflictError("root action plan/review authority mismatch")
host_effect = self.store.conn.execute(
"SELECT host_effect_key FROM task_root_action_effects "
"WHERE execution_key=?",
(plan.execution_key,),
).fetchone()
if (
exact_binding is None
and host_effect is not None
and host_effect["host_effect_key"] is not None
):
# A C6-prepared action may only be resumed through the
# exact facade, which supplies fresh signed authority.
continue
if exact_binding is not None and (
plan.review_id != exact_binding.review_id
or plan.mission_id != exact_binding.mission_id
or plan.root_task_id != exact_binding.root_task_id
or plan.root_revision != exact_binding.root_revision
or plan.plan_digest != exact_binding.plan_digest
or plan.execution_key != exact_binding.execution_key
):
raise ConflictError("trusted exact root action binding mismatch")
if (
exact_binding is not None
and row["claim_owner"] == consumer_id
and float(row["claim_expires_at"] or 0) > now
):
return RootActionClaim(
plan=plan,
consumer_id=consumer_id,
claim_token=str(row["claim_token"]),
fence=int(row["claim_fence"]),
lease_expires_at=float(row["claim_expires_at"]),
lease_seconds=lease_seconds,
reconcile_only=row["status"]
in {"EFFECT_UNKNOWN", "DISPATCHED"},
)
if active is not None and active["active_review_id"] not in {
None,
row["review_id"],
@@ -582,7 +688,13 @@ class RootActionConsumer:
return claim
return None
def _prepare_effect(self, claim: RootActionClaim, now: float):
def _prepare_effect(
self,
claim: RootActionClaim,
now: float,
*,
host_binding: Mapping[str, object] | None = None,
):
plan = claim.plan
request_digest = _digest(
{
@@ -635,6 +747,41 @@ class RootActionConsumer:
if effect["receipt_json"] is not None
else None
)
if host_binding is not None:
host_json = canonical_json(dict(host_binding))
host_digest = hashlib.sha256(host_json.encode("utf-8")).hexdigest()
host_effect_key = host_binding.get("effect_key")
if not isinstance(host_effect_key, str) or not host_effect_key:
raise ConflictError("exact host effect key is invalid")
current = self.store.conn.execute(
"SELECT host_effect_key,host_binding_json,host_binding_digest "
"FROM task_root_action_effects WHERE execution_key=?",
(plan.execution_key,),
).fetchone()
if current["host_effect_key"] is None:
try:
changed = self.store.conn.execute(
"UPDATE task_root_action_effects SET host_effect_key=?,"
"host_binding_json=?,host_binding_digest=?,updated_at=? "
"WHERE execution_key=? AND host_effect_key IS NULL",
(
host_effect_key,
host_json,
host_digest,
now,
plan.execution_key,
),
)
except Exception as exc:
raise ConflictError("exact host effect key already bound") from exc
if changed.rowcount != 1:
raise ConflictError("exact host binding CAS lost")
elif (
current["host_effect_key"] != host_effect_key
or current["host_binding_json"] != host_json
or current["host_binding_digest"] != host_digest
):
raise ConflictError("exact host action binding conflict")
status = (
"EXECUTED_PENDING_REREVIEW"
if effect_state == "SUCCEEDED"
@@ -654,6 +801,52 @@ class RootActionConsumer:
)
return effect_state, receipt, effect_phase
def _record_writer_lease(
self,
claim: RootActionClaim,
lease: Mapping[str, object] | None,
now: float,
) -> None:
if lease is None:
return
lease_json = canonical_json(dict(lease))
lease_digest = hashlib.sha256(lease_json.encode("utf-8")).hexdigest()
with self.store._transaction():
changed = self.store.conn.execute(
"UPDATE task_root_action_effects SET writer_lease_json=?,"
"writer_lease_digest=?,updated_at=? WHERE execution_key=? "
"AND writer_lease_json IS NULL",
(lease_json, lease_digest, now, claim.plan.execution_key),
)
if changed.rowcount != 1:
row = self.store.conn.execute(
"SELECT writer_lease_json,writer_lease_digest "
"FROM task_root_action_effects WHERE execution_key=?",
(claim.plan.execution_key,),
).fetchone()
if (
row is None
or row["writer_lease_json"] != lease_json
or row["writer_lease_digest"] != lease_digest
):
raise ConflictError("WriterLease durable binding conflict")
@staticmethod
def _validate_exact_writer_lease(lease: object) -> Mapping[str, object]:
if not isinstance(lease, Mapping):
raise ConflictError("exact host WriterLease identity unavailable")
lease_id = lease.get("writer_lease_id")
fence = lease.get("writer_lease_fence")
if (
not isinstance(lease_id, str)
or not lease_id.strip()
or isinstance(fence, bool)
or not isinstance(fence, int)
or fence < 0
):
raise ConflictError("exact host WriterLease identity unavailable")
return lease
def _transition_effect_phase(
self,
claim: RootActionClaim,
@@ -910,18 +1103,25 @@ class RootActionConsumer:
plan.execution_key,
)
def run_once(
def _run(
self,
*,
consumer_id: str,
now: datetime,
lease_seconds: int = 60,
exact_binding: ExactRootActionBinding | None = None,
host_binding: Mapping[str, object] | None = None,
prepare_only: bool = False,
external_revalidator: Callable[[], None] | None = None,
) -> RootActionRunResult:
if not consumer_id or lease_seconds < 1:
raise ValueError("invalid root action consumer claim")
now_value = self._now(now)
claimed = self._claim_next(
consumer_id=consumer_id, now=now_value, lease_seconds=lease_seconds
consumer_id=consumer_id,
now=now_value,
lease_seconds=lease_seconds,
exact_binding=exact_binding,
)
if claimed is None:
return RootActionRunResult(RootActionRunState.IDLE)
@@ -949,7 +1149,11 @@ class RootActionConsumer:
claim, status="EXECUTED_PENDING_REREVIEW", now=now_value
)
return self._rereview(claim, now_value)
effect_state, receipt, effect_phase = self._prepare_effect(claim, now_value)
effect_state, receipt, effect_phase = (
self._prepare_effect(claim, now_value)
if host_binding is None
else self._prepare_effect(claim, now_value, host_binding=host_binding)
)
if effect_state == "SUPERSEDED":
return RootActionRunResult(
RootActionRunState.CLOSED,
@@ -970,6 +1174,13 @@ class RootActionConsumer:
extra_args=(receipt_json, _digest(receipt)),
)
return self._rereview(claim, now_value)
if prepare_only:
return RootActionRunResult(
RootActionRunState.EXECUTED,
plan.review_id,
plan.execution_key,
"PREPARED",
)
if self._executor is None:
with self.store._transaction():
self._set_dispatch(
@@ -986,9 +1197,10 @@ class RootActionConsumer:
plan.execution_key,
"ROOT_ACTION_EXECUTOR_UNAVAILABLE",
)
before_external_call = lambda: self._assert_live_claim(
claim, now_value, renew=True
)
def before_external_call() -> None:
self._assert_live_claim(claim, now_value, renew=True)
if external_revalidator is not None:
external_revalidator()
if effect_phase == "BEFORE_LEASE":
# Admission and the immutable plan are the authority for this one
# first send. A pre-lease GET cannot prove absence: the live
@@ -997,10 +1209,13 @@ class RootActionConsumer:
claim, expected="BEFORE_LEASE", target="LEASE_INTENT", now=now_value
)
before_external_call()
self._executor.acquire_lease(
lease = self._executor.acquire_lease(
plan, before_external_call=before_external_call
)
before_external_call()
if host_binding is not None:
lease = self._validate_exact_writer_lease(lease)
self._record_writer_lease(claim, lease, now_value)
self._transition_effect_phase(
claim, expected="LEASE_INTENT", target="LEASE_ACQUIRED", now=now_value
)
@@ -1033,6 +1248,594 @@ class RootActionConsumer:
return result
return self._rereview(claim, now_value)
def run_once(
self,
*,
consumer_id: str,
now: datetime,
lease_seconds: int = 60,
) -> RootActionRunResult:
return self._run(
consumer_id=consumer_id,
now=now,
lease_seconds=lease_seconds,
)
def configure_exact_review_binding_provider(
self, provider: ExactReviewBindingProvider
) -> None:
if not callable(provider):
raise TypeError("trusted exact review binding provider required")
if self._exact_review_binding_provider is not None:
raise ConflictError("exact review binding provider already configured")
self._exact_review_binding_provider = provider
def assert_exact_owner_configured(self) -> None:
if self._exact_review_binding_provider is None or self._executor is None:
raise ConflictError("exact root action owner is not configured")
def _exact_resolution(self, binding: object) -> ExactRootActionBinding:
self.assert_exact_owner_configured()
assert self._exact_review_binding_provider is not None
resolved = self._exact_review_binding_provider(binding)
if not isinstance(resolved, ExactRootActionBinding):
raise ConflictError("trusted exact review binding is invalid")
return resolved
def _exact_plan(
self, binding: object, resolved: ExactRootActionBinding
) -> AuthoritativeRootActionPlan:
from .operation_view import OperationBinding
if not isinstance(binding, OperationBinding):
raise TypeError("operation binding required")
row = self.store.conn.execute(
"SELECT d.*,r.record_json FROM task_root_action_dispatches d "
"JOIN task_root_reviews r ON r.review_id=d.review_id "
"WHERE d.review_id=?",
(resolved.review_id,),
).fetchone()
if row is None:
raise ConflictError("trusted exact root review is unavailable")
if row["plan_json"]:
raw = json.loads(str(row["plan_json"]))
if not isinstance(raw, Mapping):
raise ConflictError("persisted root action plan is invalid")
plan = AuthoritativeRootActionPlan.from_dict(raw)
else:
plan = self._plan_builder(self._review(row))
if (
plan.review_id != resolved.review_id
or plan.mission_id != resolved.mission_id
or plan.root_task_id != resolved.root_task_id
or plan.root_revision != resolved.root_revision
or plan.plan_digest != resolved.plan_digest
or plan.execution_key != resolved.execution_key
or plan.target_task_id != binding.task_id
or plan.target_job_id != binding.job_id
or plan.external_idempotency_key != binding.effect_key
):
raise ConflictError("C6 operation/root action authority mismatch")
for name in ("writer_lease_request", "commit_request"):
request = plan.effect_payload.get(name)
if not isinstance(request, Mapping):
raise ConflictError(f"exact root action {name} unavailable")
expected = {
"task_id": binding.task_id,
"job_id": binding.job_id,
"repository": binding.repository,
"expected_branch_oid": binding.source_oid,
"target_branch": binding.target_branch,
"generation": binding.generation,
}
allowed = set(expected)
if name == "writer_lease_request":
expected.update(
{"branch_mode": "CREATE_NEW", "base_oid": binding.source_oid}
)
allowed.update({"branch_mode", "base_oid"})
else:
allowed.update({"files", "message"})
if set(request) != allowed:
raise ConflictError(f"exact root action {name} fields mismatch")
if any(request.get(key) != value for key, value in expected.items()):
raise ConflictError(f"exact root action {name} binding mismatch")
return plan
@staticmethod
def _c6_commit_mutation(
*, binding, contract, patch_bytes: bytes, commit_message: str,
plan: AuthoritativeRootActionPlan,
) -> dict[str, object]:
"""Bind the real Bridge mutation to the signed C6 operation identity."""
from .operation_view import binding_digest
if hashlib.sha256(patch_bytes).hexdigest() != contract.patch_digest:
raise ConflictError("C6 patch bytes digest mismatch")
commit = plan.effect_payload.get("commit_request")
if not isinstance(commit, Mapping):
raise ConflictError("exact root action commit request unavailable")
if commit.get("message") != commit_message:
raise ConflictError("C6 commit message/root mutation mismatch")
raw_files = commit.get("files")
if not isinstance(raw_files, (list, tuple)) or isinstance(
raw_files, (str, bytes)
):
raise ConflictError("C6 exact commit files require Bridge entry array")
actual: list[dict[str, str]] = []
seen: set[str] = set()
seen_folded: set[str] = set()
for raw in raw_files:
if not isinstance(raw, Mapping) or set(raw) != {"path", "content_base64"}:
raise ConflictError("C6 exact commit file entry is invalid")
path = raw.get("path")
encoded = raw.get("content_base64")
if not isinstance(path, str) or not path or not isinstance(encoded, str):
raise ConflictError("C6 exact commit file entry is invalid")
if path in seen or path.casefold() in seen_folded:
raise ConflictError("C6 exact commit files contain duplicate paths")
try:
content = base64.b64decode(encoded.encode("ascii"), validate=True)
except (UnicodeEncodeError, binascii.Error, ValueError) as exc:
raise ConflictError("C6 exact commit file content is invalid") from exc
seen.add(path)
seen_folded.add(path.casefold())
actual.append({"path": path, "sha256": hashlib.sha256(content).hexdigest()})
actual.sort(key=lambda item: item["path"])
expected = [
{"path": path, "sha256": digest}
for path, digest in contract.file_digests
]
if any(item["sha256"] == "DELETED" for item in expected) or actual != expected:
raise ConflictError("C6 patch contract/root files mismatch")
mutation = {
"schema": "aios.c6.commit-mutation.v1",
"files": actual,
"message": commit_message,
}
mutation_digest = hashlib.sha256(
canonical_json(mutation).encode("utf-8")
).hexdigest()
expected_projection = {
"schema": "aios.c6.root-plan-binding.v1",
"operation_binding_digest": binding_digest(binding),
"plan_digest": binding.plan_digest,
"patch_digest": contract.patch_digest,
"commit_mutation_digest": mutation_digest,
}
projection = plan.effect_payload.get("c6_operation_binding")
if not isinstance(projection, Mapping) or dict(projection) != expected_projection:
raise ConflictError("C6 operation/root plan mutation identity mismatch")
return {**mutation, "digest": mutation_digest}
@staticmethod
def _host_binding_document(
*, binding, claim, authority, contract, patch_bytes: bytes, commit_message: str,
plan: AuthoritativeRootActionPlan,
) -> dict[str, object]:
from .operation_view import binding_digest
mutation = RootActionConsumer._c6_commit_mutation(
binding=binding,
contract=contract,
patch_bytes=patch_bytes,
commit_message=commit_message,
plan=plan,
)
return {
"schema": "aios.c6.root-action-host-binding.v2",
"action_id": plan.execution_key,
"operation_binding": asdict(binding),
"binding_digest": binding_digest(binding),
"effect_key": binding.effect_key,
"claim_receipt": claim.to_dict(),
"authorization_ref": authority.authorization_ref,
"authority_key_id": authority.authority_key_id,
"authority_key_fingerprint": authority.authority_key_fingerprint,
"authority_record_sha256": authority.authority_record_sha256,
"authority_generation": authority.authority_generation,
"task_generation": authority.task_generation,
"expected_branch_oid": authority.expected_branch_oid,
"review_id": plan.review_id,
"mission_id": plan.mission_id,
"root_task_id": plan.root_task_id,
"root_revision": plan.root_revision,
"plan_digest": plan.plan_digest,
"root_execution_key": plan.execution_key,
"patch_digest": contract.patch_digest,
"c6_plan_digest": binding.plan_digest,
"commit_mutation_digest": mutation["digest"],
"commit_message_sha256": hashlib.sha256(
commit_message.encode("utf-8")
).hexdigest(),
}
@staticmethod
def _validate_host_document(value: object) -> dict[str, object]:
if not isinstance(value, Mapping):
raise ConflictError("exact host binding is unavailable")
result = dict(value)
if result.get("schema") != "aios.c6.root-action-host-binding.v2":
raise ConflictError("exact host binding schema is invalid")
return result
def _action_from_row(self, row):
from .host_patch_executor import HostPatchAction
raw = str(row["host_binding_json"] or "")
digest = hashlib.sha256(raw.encode("utf-8")).hexdigest()
if not raw or row["host_binding_digest"] != digest:
raise ConflictError("exact host binding digest mismatch")
host = self._validate_host_document(json.loads(raw))
if host.get("effect_key") != row["host_effect_key"]:
raise ConflictError("exact host effect key mismatch")
lease = (
json.loads(str(row["writer_lease_json"]))
if row["writer_lease_json"] is not None
else None
)
receipt = (
json.loads(str(row["receipt_json"]))
if row["receipt_json"] is not None
else None
)
if row["state"] == "SUCCEEDED":
state = "COMMITTED"
elif row["state"] == "BLOCKED":
state = "FAILED_FINAL"
elif row["effect_phase"] == "BEFORE_LEASE":
state = "PREPARED"
else:
state = "EFFECT_UNKNOWN"
lease_id = None
lease_fence = None
if isinstance(lease, Mapping):
lease_id = lease.get("writer_lease_id")
lease_fence = lease.get("writer_lease_fence")
return HostPatchAction(
action_id=str(host["action_id"]),
state=state,
binding_digest=str(host["binding_digest"]),
effect_key=str(host["effect_key"]),
authorization_ref=str(host["authorization_ref"]),
authority_key_id=str(host["authority_key_id"]),
authority_key_fingerprint=str(host["authority_key_fingerprint"]),
authority_record_sha256=str(host["authority_record_sha256"]),
claim_id=str(host["claim_receipt"]["claim_id"]),
claim_fence=int(host["claim_receipt"]["receipt_revision"]),
task_generation=int(host["task_generation"]),
authority_generation=int(host["authority_generation"]),
expected_branch_oid=str(host["expected_branch_oid"]),
claim_receipt=dict(host["claim_receipt"]),
writer_lease_id=(str(lease_id) if lease_id is not None else None),
writer_lease_fence=(int(lease_fence) if lease_fence is not None else None),
result_oid=(
str(receipt["commit_oid"])
if isinstance(receipt, Mapping) and receipt.get("commit_oid") is not None
else None
),
receipt_ref=(
str(row["receipt_digest"])
if row["receipt_digest"] is not None
else None
),
)
def read_exact(self, effect_key: str):
row = self.store.conn.execute(
"SELECT * FROM task_root_action_effects WHERE host_effect_key=?",
(effect_key,),
).fetchone()
return None if row is None else self._action_from_row(row)
def iter_pending_exact(self, *, limit: int = 16):
"""Enumerate durable host intents from the existing effect ledger."""
from .operation_view import OperationBinding
if isinstance(limit, bool) or not isinstance(limit, int) or not 1 <= limit <= 64:
raise ConflictError("bounded exact pending limit required")
rows = self.store.conn.execute(
"SELECT * FROM task_root_action_effects "
"WHERE host_effect_key IS NOT NULL AND host_binding_json IS NOT NULL "
"AND host_closure_digest IS NULL "
"ORDER BY created_at,execution_key LIMIT ?",
(limit,),
).fetchall()
result = []
for row in rows:
action = self._action_from_row(row)
if action.state not in {
"PREPARED", "IN_PROGRESS", "EFFECT_UNKNOWN", "COMMITTED"
}:
continue
raw = self._validate_host_document(
json.loads(str(row["host_binding_json"]))
).get("operation_binding")
if not isinstance(raw, Mapping):
raise ConflictError("pending exact operation binding unavailable")
try:
operation = OperationBinding(**dict(raw))
except (TypeError, ValueError) as exc:
raise ConflictError("pending exact operation binding invalid") from exc
result.append((operation, action))
return tuple(result)
def mark_exact_closed(self, *, binding, action_id: str, observation) -> None:
from .operation_view import (
ClosureObservation,
binding_digest,
derive_closure,
)
if not isinstance(observation, ClosureObservation):
raise ConflictError("typed exact host closure observation required")
observed_at = self._clock()
now = self._now(observed_at)
closure = derive_closure(binding, observation, now=observed_at)
if (
closure.execution_recorded,
closure.independently_verified,
closure.business_accepted,
closure.outcome,
closure.reason_code,
) != (True, True, True, "SUCCEEDED", "THREE_LAYER_CLOSURE_VALID"):
raise ConflictError("exact host closure is incomplete")
with self.store._transaction():
row = self.store.conn.execute(
"SELECT * FROM task_root_action_effects WHERE host_effect_key=?",
(binding.effect_key,),
).fetchone()
if row is None:
raise ConflictError("exact host action unavailable")
action = self._action_from_row(row)
expected_binding_digest = binding_digest(binding)
effect = observation.effect
verification = observation.verification
acceptance = observation.acceptance
verifier_receipt_ref = (
verification.verification_receipt_ref
or verification.receipt_ref
)
if (
action.action_id != action_id
or action.state != "COMMITTED"
or action.binding_digest != expected_binding_digest
or not action.receipt_ref
or effect.binding_digest != expected_binding_digest
or effect.receipt_ref != action.receipt_ref
or effect.result_artifact_digest is None
or verification.binding_digest != expected_binding_digest
or verification.receipt_ref != action.receipt_ref
or verification.result_artifact_digest
!= effect.result_artifact_digest
or not verifier_receipt_ref
or acceptance.binding_digest != expected_binding_digest
or acceptance.receipt_ref != action.receipt_ref
or acceptance.result_artifact_digest
!= effect.result_artifact_digest
or acceptance.verifier_receipt_ref != verifier_receipt_ref
):
raise ConflictError("exact host closure/action binding mismatch")
observation_value = _canonical_observation_value(asdict(observation))
observation_raw = canonical_json(observation_value)
observation_digest = hashlib.sha256(
observation_raw.encode("utf-8")
).hexdigest()
document = {
"schema": "aios.c6.three-layer-closure.v2",
"binding_digest": action.binding_digest,
"effect_key": action.effect_key,
"action_id": action.action_id,
"root_receipt_ref": action.receipt_ref,
"effect_receipt_ref": effect.receipt_ref,
"result_artifact_digest": effect.result_artifact_digest,
"verification_receipt_ref": verifier_receipt_ref,
"verifier_id": verification.verifier_id,
"acceptance_revision": acceptance.acceptance_revision,
"acceptor_id": acceptance.acceptor_id,
"expected_acceptance_revision": (
observation.expected_acceptance_revision
),
"expected_acceptor_id": observation.expected_acceptor_id,
"closure_observation_digest": observation_digest,
"execution_recorded": closure.execution_recorded,
"independently_verified": closure.independently_verified,
"business_accepted": closure.business_accepted,
"outcome": closure.outcome,
"reason_code": closure.reason_code,
}
raw = canonical_json(document)
digest = hashlib.sha256(raw.encode("utf-8")).hexdigest()
changed = self.store.conn.execute(
"UPDATE task_root_action_effects SET host_closure_json=?,"
"host_closure_digest=?,host_closure_observed_at=?,updated_at=? "
"WHERE host_effect_key=? AND state='SUCCEEDED' "
"AND host_binding_json=? AND host_binding_digest=? "
"AND receipt_digest=? AND host_closure_digest IS NULL",
(
raw, digest, now, now, binding.effect_key,
row["host_binding_json"], row["host_binding_digest"],
action.receipt_ref,
),
)
if changed.rowcount != 1:
current = self.store.conn.execute(
"SELECT host_closure_json,host_closure_digest "
"FROM task_root_action_effects WHERE host_effect_key=?",
(binding.effect_key,),
).fetchone()
if (
current is None
or current["host_closure_json"] != raw
or current["host_closure_digest"] != digest
):
raise ConflictError("exact host closure CAS conflict")
@staticmethod
def _assert_action_authority(action, binding, authority) -> None:
from .operation_view import binding_digest
if (
action.binding_digest != binding_digest(binding)
or action.effect_key != binding.effect_key
or action.authorization_ref != authority.authorization_ref
or action.authority_key_id != authority.authority_key_id
or action.authority_key_fingerprint != authority.authority_key_fingerprint
or action.authority_record_sha256 != authority.authority_record_sha256
or action.authority_generation != authority.authority_generation
or action.task_generation != authority.task_generation
or action.claim_id != authority.claim_id
or action.claim_fence != authority.claim_fence
or action.expected_branch_oid != authority.expected_branch_oid
):
raise ConflictError("exact host action authority mismatch")
def prepare_exact(
self,
*,
binding,
claim,
authority,
contract,
patch_bytes: bytes,
commit_message: str,
):
resolved = self._exact_resolution(binding)
plan = self._exact_plan(binding, resolved)
host = self._host_binding_document(
binding=binding,
claim=claim,
authority=authority,
contract=contract,
patch_bytes=patch_bytes,
commit_message=commit_message,
plan=plan,
)
now = datetime.now(tz=timezone.utc)
lease_seconds = max(1, min(60, int(authority.expires_at - now.timestamp())))
result = self._run(
consumer_id=f"c6:{claim.claim_id}",
now=now,
lease_seconds=lease_seconds,
exact_binding=resolved,
host_binding=host,
prepare_only=True,
)
if result.detail != "PREPARED":
raise ConflictError("exact root action was not prepared")
action = self.read_exact(binding.effect_key)
if action is None:
raise ConflictError("prepared exact root action is unavailable")
self._assert_action_authority(action, binding, authority)
return action
def _claim_exact_existing(self, binding, action, authority, now: float):
resolved = self._exact_resolution(binding)
plan = self._exact_plan(binding, resolved)
if plan.execution_key != action.action_id:
raise ConflictError("exact root action identity changed")
claimed = self._claim_next(
consumer_id=f"c6:{action.claim_id}",
now=now,
lease_seconds=60,
exact_binding=resolved,
)
if not isinstance(claimed, RootActionClaim):
raise ConflictError("exact root action claim unavailable")
self._assert_action_authority(action, binding, authority)
return claimed
def reconcile_exact(
self,
*,
action_id: str,
binding,
authority,
before_external_call: Callable[[], None],
):
action = self.read_exact(binding.effect_key)
if action is None or action.action_id != action_id:
raise ConflictError("exact root action unavailable")
self._assert_action_authority(action, binding, authority)
if action.state in {"COMMITTED", "FAILED_FINAL", "PREPARED"}:
return action
now = self._now(self._clock())
claim = self._claim_exact_existing(binding, action, authority, now)
_state, _receipt, phase = self._prepare_effect(claim, now)
if phase in {"BEFORE_LEASE", "LEASE_ACQUIRED"}:
return self.read_exact(binding.effect_key)
if self._executor is None:
raise ConflictError("root action executor unavailable")
def revalidate() -> None:
self._assert_live_claim(claim, now, renew=True)
before_external_call()
revalidate()
observation = self._executor.reconcile(
claim.plan, before_external_call=revalidate
)
revalidate()
result = self._record_observation(claim, observation, now)
if result is None:
self._rereview(claim, now)
observed = self.read_exact(binding.effect_key)
if observed is None:
raise ConflictError("reconciled exact root action is unavailable")
return observed
def execute_prepared(
self,
*,
action_id: str,
binding,
claim,
authority,
contract,
patch_bytes: bytes,
commit_message: str,
before_external_call: Callable[[], None],
):
action = self.read_exact(binding.effect_key)
if action is None or action.action_id != action_id:
raise ConflictError("exact root action unavailable")
self._assert_action_authority(action, binding, authority)
if action.state != "PREPARED":
return action
resolved = self._exact_resolution(binding)
plan = self._exact_plan(binding, resolved)
host = self._host_binding_document(
binding=binding,
claim=claim,
authority=authority,
contract=contract,
patch_bytes=patch_bytes,
commit_message=commit_message,
plan=plan,
)
persisted = self.store.conn.execute(
"SELECT host_binding_json FROM task_root_action_effects "
"WHERE host_effect_key=?",
(binding.effect_key,),
).fetchone()
if persisted is None or persisted["host_binding_json"] != canonical_json(host):
raise ConflictError("prepared exact host binding changed")
self._run(
consumer_id=f"c6:{claim.claim_id}",
now=self._clock(),
lease_seconds=60,
exact_binding=resolved,
host_binding=host,
external_revalidator=before_external_call,
)
completed = self.read_exact(binding.effect_key)
if completed is None:
raise ConflictError("exact root action completion unavailable")
return completed
class PublicBridgeRootActionExecutor:
"""Contract adapter for the authenticated public Bridge operation surface."""
@@ -1329,6 +2132,7 @@ class PublicBridgeRootActionExecutor:
raise ConflictError("Bridge writer lease response binding mismatch")
if "job_id" in result and result["job_id"] != plan.target_job_id:
raise ConflictError("Bridge writer lease response Job mismatch")
return dict(result)
def commit_effect(
self,
@@ -15,7 +15,7 @@ claim transaction / repository operation.
from __future__ import annotations
from dataclasses import dataclass, asdict
from typing import Any, Mapping, Protocol, Sequence
from typing import Any, Callable, Iterable, Mapping, Protocol, Sequence
import hashlib
import time
import uuid
@@ -94,6 +94,7 @@ class ClaimConditions:
authorization_revision: str
scope_digest: str
capacity_limit: int
operation_digest: str | None = None
def to_dict(self) -> dict[str, object]:
return asdict(self)
@@ -121,6 +122,7 @@ class SchedulabilityObservation:
dependencies_satisfied: bool
freeze_blocked: bool
conflicting_lease: bool
operation_digest: str | None = None
@property
def capacity_remaining(self) -> int:
@@ -128,7 +130,7 @@ class SchedulabilityObservation:
def stable_binding(self) -> dict[str, object]:
"""Fields whose change invalidates an earlier proof."""
return {
binding = {
"task_id": self.task_id,
"task_revision": self.task_revision,
"task_generation": self.task_generation,
@@ -149,6 +151,9 @@ class SchedulabilityObservation:
"conflicting_lease": self.conflicting_lease,
"heartbeat_fresh": self.heartbeat_fresh,
}
if self.operation_digest is not None:
binding["operation_digest"] = self.operation_digest
return binding
def claim_conditions(self) -> ClaimConditions:
return ClaimConditions(
@@ -161,6 +166,7 @@ class SchedulabilityObservation:
authorization_revision=self.authorization_revision,
scope_digest=self.scope_digest,
capacity_limit=self.capacity_limit,
operation_digest=self.operation_digest,
)
@@ -194,7 +200,11 @@ class SchedulabilityProof:
if ttl_seconds <= 0 or ttl_seconds > 300:
raise ValueError("schedulability proof ttl must be in (0,300]")
payload = {
"schema": "aios.schedulability-proof.v2",
"schema": (
"aios.schedulability-proof.v3"
if observation.operation_digest is not None
else "aios.schedulability-proof.v2"
),
"proof_id": proof_id or f"sched-{uuid.uuid4()}",
"task_id": _text(task_id, "task_id"),
"worker_id": _text(worker_id, "worker_id"),
@@ -301,12 +311,18 @@ class SchedulabilityService:
clock=time.time,
proof_ttl_seconds: float = 30.0,
heartbeat_grace_factor: float = 3.0,
capability_matcher: Callable[[Iterable[str], Iterable[str]], bool] | None = None,
) -> None:
self.authority = authority
self.proofs = proofs
self.clock = clock
self.proof_ttl_seconds = proof_ttl_seconds
self.heartbeat_grace_factor = heartbeat_grace_factor
if capability_matcher is None:
from .capabilities import has_capabilities
capability_matcher = has_capabilities
self.capability_matcher = capability_matcher
@staticmethod
def _caps(value: object) -> tuple[str, ...]:
@@ -366,9 +382,8 @@ class SchedulabilityService:
blockers.append(capacity_error)
if not fresh:
blockers.append(Blocker("STALE_OR_INACTIVE_WORKER", "worker", "worker heartbeat is not fresh"))
missing = sorted(set(required) - set(offered))
if missing:
blockers.append(Blocker("MISSING_CAPABILITY", "worker", ",".join(missing)))
if not self.capability_matcher(offered, required):
blockers.append(Blocker("MISSING_CAPABILITY", "worker", ",".join(required)))
if not scope_ok:
blockers.append(Blocker("EXACT_SCOPE_MISMATCH", "authorization", "worker/task signed scope mismatch"))
if not auth_allowed:
@@ -403,6 +418,11 @@ class SchedulabilityService:
dependencies_satisfied=deps,
freeze_blocked=freeze,
conflicting_lease=lease,
operation_digest=(
str(task["operation_digest"])
if task.get("operation_digest") is not None
else None
),
)
return obs, tuple(blockers)
@@ -468,7 +488,14 @@ class SchedulabilityService:
return proof
@staticmethod
def _validate_claim_receipt(result: Mapping[str, Any]) -> None:
def _validate_claim_receipt(
result: Mapping[str, Any], proof: SchedulabilityProof
) -> None:
if proof.schema == "aios.schedulability-proof.v3":
from .schedulability_store_adapter import validate_task_plane_receipt
validate_task_plane_receipt(result, proof=proof)
return
required = ("claim_id", "lease_id", "fencing_generation", "receipt_revision")
missing = [name for name in required if result.get(name) in (None, "")]
if missing:
@@ -493,6 +520,11 @@ class SchedulabilityService:
raise SchedulabilityChanged("proof expired")
if proof.worker_id != worker_id or proof.actor_identity != actor_identity:
raise SchedulabilityChanged("proof identity mismatch")
if (
getattr(self.authority, "requires_v3_proof", False)
and proof.schema != "aios.schedulability-proof.v3"
):
raise SchedulabilityChanged("v3 claim authority requires v3 proof")
current, blockers = self.observe(proof.task_id, worker_id, actor_identity)
if blockers:
raise SchedulabilityChanged(
@@ -509,5 +541,5 @@ class SchedulabilityService:
lease_seconds=lease_seconds,
schedulability_proof_digest=proof.proof_digest,
)
self._validate_claim_receipt(result)
self._validate_claim_receipt(result, proof)
return result
@@ -0,0 +1,691 @@
"""Task Plane-backed atomic schedulability authority for the C6 pilot.
This adapter introduces no lease or authority database. It is enabled only
when a host supplies a binding that holds the TaskPlane transaction together
with its real authorization and freeze boundary.
"""
from __future__ import annotations
from contextlib import AbstractContextManager, contextmanager
from dataclasses import asdict, dataclass
from typing import Any, Callable, Iterable, Literal, Mapping, Protocol
import math
import time
from .capabilities import has_capabilities
from .errors import ConflictError, LockedError, ServiceUnavailableError
from .schedulability import ClaimConditions, SchedulabilityChanged, SchedulabilityProof
from .store import TaskPlaneStore
_RECEIPT_SCHEMA = "aios.task-plane-claim-receipt.v3"
_RECEIPT_FIELDS = frozenset(
{
"schema",
"authority_namespace",
"task_id",
"claim_id",
"worker_id",
"worker_identity",
"claimed_revision",
"receipt_revision",
"attempt",
"operation_digest",
"lease_until",
"proof_digest",
}
)
__all__ = [
"AuthorityAtomicBindingUnavailable",
"ClaimAuthorityBinding",
"ClaimTransaction",
"ScopedLeaseController",
"TaskPlaneClaimReceiptV3",
"TaskPlaneSchedulabilityAuthority",
"validate_task_plane_receipt",
]
class AuthorityAtomicBindingUnavailable(ServiceUnavailableError):
code = "AUTHORITY_ATOMIC_BINDING_UNAVAILABLE"
def __init__(self) -> None:
super().__init__(self.code)
class ClaimTransaction(Protocol):
"""A host-owned context that holds the store and authority lock together."""
store: TaskPlaneStore
authority_id: str
def assert_conditions(
self,
task_id: str,
worker_id: str,
actor_identity: str,
expected: ClaimConditions,
) -> None: ...
def assert_lease_holder(
self, task_id: str, claim_id: str, worker_id: str, actor_identity: str
) -> None: ...
def assert_lease_authorized(
self,
task_id: str,
claim_id: str,
worker_id: str,
actor_identity: str,
receipt: TaskPlaneClaimReceiptV3,
) -> None: ...
class ClaimAuthorityBinding(Protocol):
"""Host integration point; request-provided callbacks are never accepted."""
def claim_context(self, store: TaskPlaneStore) -> AbstractContextManager[ClaimTransaction]: ...
def read_authorization(
self, *, task: Mapping[str, Any], worker: Mapping[str, Any], actor_identity: str
) -> Mapping[str, Any]: ...
def exact_scope_digest(
self, *, task: Mapping[str, Any], worker: Mapping[str, Any], actor_identity: str
) -> tuple[bool, str]: ...
def freeze_blocked(self, task: Mapping[str, Any]) -> bool: ...
_BINDING_METHODS = (
"claim_context",
"read_authorization",
"exact_scope_digest",
"freeze_blocked",
)
def _require_binding(store: TaskPlaneStore, binding: ClaimAuthorityBinding) -> None:
"""Reject a partial host wiring before it can produce a lease effect."""
if store is None or binding is None or any(
not callable(getattr(binding, name, None)) for name in _BINDING_METHODS
):
raise AuthorityAtomicBindingUnavailable()
@dataclass(frozen=True)
class TaskPlaneClaimReceiptV3:
schema: Literal["aios.task-plane-claim-receipt.v3"]
authority_namespace: Literal["task-plane"]
task_id: str
claim_id: str
worker_id: str
worker_identity: str
claimed_revision: int
receipt_revision: int
attempt: int
operation_digest: str
lease_until: float
proof_digest: str
def to_dict(self) -> dict[str, object]:
return asdict(self)
def _text(value: object, name: str) -> str:
if not isinstance(value, str) or not value:
raise SchedulabilityChanged(f"invalid v3 receipt {name}")
return value
def _int(value: object, name: str, *, minimum: int = 0) -> int:
if isinstance(value, bool) or not isinstance(value, int) or value < minimum:
raise SchedulabilityChanged(f"invalid v3 receipt {name}")
return value
def _float(value: object, name: str) -> float:
if (
isinstance(value, bool)
or not isinstance(value, (int, float))
or not math.isfinite(float(value))
or float(value) <= 0
):
raise SchedulabilityChanged(f"invalid v3 receipt {name}")
return float(value)
def _parse_receipt(result: Mapping[str, object]) -> TaskPlaneClaimReceiptV3:
if not isinstance(result, Mapping) or set(result) != _RECEIPT_FIELDS:
raise SchedulabilityChanged("invalid v3 claim receipt fields")
if result["schema"] != _RECEIPT_SCHEMA or result["authority_namespace"] != "task-plane":
raise SchedulabilityChanged("invalid v3 claim receipt namespace")
return TaskPlaneClaimReceiptV3(
schema=_RECEIPT_SCHEMA,
authority_namespace="task-plane",
task_id=_text(result["task_id"], "task_id"),
claim_id=_text(result["claim_id"], "claim_id"),
worker_id=_text(result["worker_id"], "worker_id"),
worker_identity=_text(result["worker_identity"], "worker_identity"),
claimed_revision=_int(result["claimed_revision"], "claimed_revision"),
receipt_revision=_int(result["receipt_revision"], "receipt_revision"),
attempt=_int(result["attempt"], "attempt", minimum=1),
operation_digest=_text(result["operation_digest"], "operation_digest"),
lease_until=_float(result["lease_until"], "lease_until"),
proof_digest=_text(result["proof_digest"], "proof_digest"),
)
def validate_task_plane_receipt(
result: Mapping[str, object], *, proof: SchedulabilityProof
) -> TaskPlaneClaimReceiptV3:
"""Validate the C6 receipt without treating it as a WriterLease fence."""
if proof.schema != "aios.schedulability-proof.v3":
raise SchedulabilityChanged("v3 receipt requires v3 proof")
receipt = _parse_receipt(result)
if (
receipt.task_id != proof.task_id
or receipt.worker_id != proof.worker_id
or receipt.worker_identity != proof.observation.worker_identity
or receipt.proof_digest != proof.proof_digest
or receipt.operation_digest != proof.observation.operation_digest
or receipt.receipt_revision <= receipt.claimed_revision
or receipt.lease_until <= proof.created_at
):
raise SchedulabilityChanged("v3 claim receipt does not bind proof")
return receipt
class TaskPlaneSchedulabilityAuthority:
"""Use the existing TaskPlane store as the only claim/lease authority."""
requires_v3_proof = True
def __init__(
self,
store: TaskPlaneStore,
binding: ClaimAuthorityBinding,
capability_matcher: Callable[[Iterable[str], Iterable[str]], bool] = has_capabilities,
) -> None:
_require_binding(store, binding)
if not callable(capability_matcher):
raise TypeError("capability_matcher must be callable")
self.store = store
self.binding = binding
self.capability_matcher = capability_matcher
@staticmethod
def _task_view(task: Any) -> dict[str, Any]:
return {
"task_id": task.task_id,
"revision": task.revision,
"generation": task.task_plan.metadata.get("generation", 0),
"status": task.status,
"required_capabilities": list(task.task_plan.required_capabilities),
"operation_digest": task.task_plan.operation_digest(),
"priority": task.task_plan.metadata.get("priority", 0),
"created_at": task.created_at,
"metadata": dict(task.task_plan.metadata),
}
@staticmethod
def _worker_view(worker: Any) -> dict[str, Any]:
return {
"worker_id": worker.worker_id,
"identity": worker.identity,
"capabilities": list(worker.capabilities),
"status": worker.status,
"disabled": worker.disabled,
"registration_revision": worker.registration_revision,
"max_concurrency": worker.max_concurrency,
"heartbeat_interval_seconds": worker.heartbeat_interval_seconds,
"last_heartbeat": worker.last_heartbeat,
}
def read_task(self, task_id: str) -> Mapping[str, Any]:
return self._task_view(self.store.get_task(task_id))
def list_candidate_tasks(self) -> list[Mapping[str, Any]]:
return [self._task_view(task) for task in self.store.list_tasks()]
def read_worker(self, worker_id: str) -> Mapping[str, Any]:
return self._worker_view(self.store.get_worker(worker_id))
def read_authorization(
self, *, task: Mapping[str, Any], worker: Mapping[str, Any], actor_identity: str
) -> Mapping[str, Any]:
value = self.binding.read_authorization(
task=task, worker=worker, actor_identity=actor_identity
)
if (
not isinstance(value, Mapping)
or not isinstance(value.get("allowed"), bool)
or not isinstance(value.get("authorization_ref"), str)
or not value["authorization_ref"]
or not isinstance(value.get("revision"), str)
or not value["revision"]
):
raise AuthorityAtomicBindingUnavailable()
return value
def active_claim_count(self, worker_id: str) -> int:
with self.store._lock:
return int(
self.store.conn.execute(
"SELECT COUNT(1) FROM claims WHERE worker_id=? AND status='ACTIVE' AND lease_until>?",
(worker_id, time.time()),
).fetchone()[0]
)
def dependencies_satisfied(self, task: Mapping[str, Any]) -> bool:
record = self.store.get_task(str(task["task_id"]))
return all(
self.store.get_task(dependency_id).status == "COMPLETED"
for dependency_id in record.task_plan.dependencies
)
def freeze_blocked(self, task: Mapping[str, Any]) -> bool:
# Only the exact boolean False proves the absence of a freeze. An
# unavailable or malformed authority value must deny an automatic claim.
return self.binding.freeze_blocked(task) is not False
def conflicting_lease(self, task_id: str) -> bool:
return self.store.active_claim(task_id) is not None
def exact_scope_digest(
self, *, task: Mapping[str, Any], worker: Mapping[str, Any], actor_identity: str
) -> tuple[bool, str]:
result = self.binding.exact_scope_digest(
task=task, worker=worker, actor_identity=actor_identity
)
if (
not isinstance(result, tuple)
or len(result) != 2
or not isinstance(result[0], bool)
or not isinstance(result[1], str)
or not result[1]
):
raise AuthorityAtomicBindingUnavailable()
return result
def claim_atomically(
self,
*,
task_id: str,
worker_id: str,
actor_identity: str,
expected: ClaimConditions,
lease_seconds: int,
schedulability_proof_digest: str,
) -> Mapping[str, Any]:
if not isinstance(lease_seconds, int) or isinstance(lease_seconds, bool) or lease_seconds <= 0:
raise SchedulabilityChanged("invalid claim lease duration")
with self.binding.claim_context(self.store) as transaction:
if transaction.store is not self.store or not getattr(transaction, "authority_id", ""):
raise AuthorityAtomicBindingUnavailable()
transaction.assert_conditions(task_id, worker_id, actor_identity, expected)
task = self.store.get_task(task_id)
worker = self.store.get_worker(worker_id)
operation_digest = task.task_plan.operation_digest()
generation = task.task_plan.metadata.get("generation", 0)
task_view = self._task_view(task)
worker_view = self._worker_view(worker)
authorization = self.read_authorization(
task=task_view, worker=worker_view, actor_identity=actor_identity
)
scope = self.exact_scope_digest(
task=task_view, worker=worker_view, actor_identity=actor_identity
)
if (
task.revision != expected.task_revision
or task.status != expected.task_status
or generation != expected.task_generation
or worker.registration_revision != expected.worker_generation
or worker.identity != expected.worker_identity
or worker.max_concurrency != expected.capacity_limit
or (expected.operation_digest is not None and operation_digest != expected.operation_digest)
or not self.capability_matcher(worker.capabilities, task.task_plan.required_capabilities)
or worker.disabled
or worker.status != "active"
or authorization["allowed"] is not True
or authorization["authorization_ref"] != expected.authorization_ref
or authorization["revision"] != expected.authorization_revision
or scope != (True, expected.scope_digest)
or self.freeze_blocked(task_view)
or not all(
self.store.get_task(dependency_id).status == "COMPLETED"
for dependency_id in task.task_plan.dependencies
)
or self.store.active_claim(task_id) is not None
or self.active_claim_count(worker_id) >= worker.max_concurrency
):
raise ConflictError("atomic claim condition changed")
claim_internal = getattr(self.store, "_claim_task_in_transaction", None)
if not callable(claim_internal):
raise AuthorityAtomicBindingUnavailable()
claim = claim_internal(
task_id=task_id,
worker_id=worker_id,
worker_identity=actor_identity,
lease_seconds=lease_seconds,
expected_revision=expected.task_revision,
operation_digest=operation_digest,
proof_digest=schedulability_proof_digest,
expected_binding=expected.to_dict(),
)
post_claim = self.store.get_task(task_id)
receipt = TaskPlaneClaimReceiptV3(
schema=_RECEIPT_SCHEMA,
authority_namespace="task-plane",
task_id=claim.task_id,
claim_id=claim.claim_id,
worker_id=claim.worker_id,
worker_identity=claim.worker_identity,
claimed_revision=claim.task_revision,
receipt_revision=post_claim.revision,
attempt=claim.attempt,
operation_digest=claim.operation_digest,
lease_until=claim.lease_until,
proof_digest=schedulability_proof_digest,
)
return receipt.to_dict()
class ScopedLeaseController:
"""Renew an existing TaskPlane claim; it never creates a second lease."""
def __init__(self, store: TaskPlaneStore, binding: ClaimAuthorityBinding) -> None:
_require_binding(store, binding)
self.store = store
self.binding = binding
def _validate_persisted_claim_evidence(
self,
current: TaskPlaneClaimReceiptV3,
*,
active: Any,
task: Any,
worker: Any,
) -> Mapping[str, object]:
"""Bind a C6 receipt to the durable claim decision, not its caller input."""
# The claim row is created before the task's CLAIMED transition. The
# transition is the only source for a v3 receipt revision; accepting a
# larger caller-provided value would let a legacy claim masquerade as
# a C6 claim.
if current.receipt_revision != active.task_revision + 1:
raise ConflictError("receipt revision does not bind active claim")
expected_task_revision = (
current.receipt_revision
if task.status == "CLAIMED"
else current.receipt_revision + 1
)
if task.revision != expected_task_revision:
raise ConflictError("receipt revision does not bind task state")
# A C6 proof digest is durable only when it appears in the exact
# TASK_CLAIMED event for this claim. The legacy claim path records no
# proof digest, so it cannot be upgraded by assembling v3-shaped JSON.
if not self.store.verify_event_chain(current.task_id):
raise ConflictError("claim event chain verification failed")
claim_events = [
event
for event in self.store.list_events(current.task_id)
if (
event.get("type") == "TASK_CLAIMED"
and isinstance(event.get("data"), Mapping)
and event["data"].get("claim_id") == current.claim_id
and event["data"].get("worker_id") == current.worker_id
and event["data"].get("proof_digest") == current.proof_digest
)
]
if len(claim_events) != 1:
raise ConflictError("receipt proof is not bound to claimed event")
expected_binding = claim_events[0]["data"].get("expected_binding")
required_binding_fields = {
"task_revision",
"task_generation",
"task_status",
"worker_generation",
"worker_identity",
"authorization_ref",
"authorization_revision",
"scope_digest",
"capacity_limit",
"operation_digest",
}
if not isinstance(expected_binding, Mapping) or set(expected_binding) != required_binding_fields:
raise ConflictError("claim lacks durable C6 authority binding")
task_generation = task.task_plan.metadata.get("generation", 0)
if (
isinstance(expected_binding["task_revision"], bool)
or not isinstance(expected_binding["task_revision"], int)
or expected_binding["task_revision"] < 0
or expected_binding["task_revision"] != current.claimed_revision
or expected_binding["task_revision"] != active.task_revision
or isinstance(expected_binding["task_generation"], bool)
or not isinstance(expected_binding["task_generation"], int)
or expected_binding["task_generation"] != task_generation
or expected_binding["task_status"] not in {"CREATED", "QUEUED", "FAILED_RECOVERABLE"}
or isinstance(expected_binding["worker_generation"], bool)
or not isinstance(expected_binding["worker_generation"], int)
or expected_binding["worker_generation"] < 1
or expected_binding["worker_identity"] != current.worker_identity
or isinstance(expected_binding["capacity_limit"], bool)
or not isinstance(expected_binding["capacity_limit"], int)
or expected_binding["capacity_limit"] < 1
or expected_binding["capacity_limit"] != worker.max_concurrency
or not isinstance(expected_binding["operation_digest"], str)
or expected_binding["operation_digest"] != current.operation_digest
or not isinstance(expected_binding["authorization_ref"], str)
or not expected_binding["authorization_ref"]
or not isinstance(expected_binding["authorization_revision"], str)
or not expected_binding["authorization_revision"]
or not isinstance(expected_binding["scope_digest"], str)
or not expected_binding["scope_digest"]
):
raise ConflictError("durable C6 claim binding changed")
# The scheduler decision retains the remaining immutable claim facts.
# Requiring exactly one matching decision also rejects a proof digest
# copied between attempts for the same task.
decision_payload = {
"worker_id": current.worker_id,
"worker_identity": current.worker_identity,
"operation_digest": current.operation_digest,
"attempt": current.attempt,
"proof_digest": current.proof_digest,
}
decision_events = [
event
for event in self.store.list_events(current.task_id)
if event.get("type") == "SCHEDULE_DECISION" and event.get("data") == decision_payload
]
if len(decision_events) != 1:
raise ConflictError("receipt proof is not bound to schedule decision")
self._validate_worker_heartbeat_chain(
bound_revision=expected_binding["worker_generation"],
worker=worker,
claim_actor_identity=current.worker_identity,
)
return expected_binding
def _validate_worker_heartbeat_chain(
self,
*,
bound_revision: object,
worker: Any,
claim_actor_identity: str,
) -> None:
"""Allow only a complete run of ordinary worker heartbeats after claim.
Worker registration revisions advance on every worker heartbeat. The
worker audit stream is therefore the authoritative distinction between
liveness refresh and re-registration or privilege change.
"""
current_revision = worker.registration_revision
if (
isinstance(bound_revision, bool)
or not isinstance(bound_revision, int)
or isinstance(current_revision, bool)
or not isinstance(current_revision, int)
or current_revision < bound_revision
):
raise ConflictError("worker registration revision changed")
if current_revision == bound_revision:
return
audit_task_id = "task-worker-registry"
if not self.store.verify_event_chain(audit_task_id):
raise ConflictError("worker registry event chain verification failed")
expected_revisions = list(range(bound_revision + 1, current_revision + 1))
events = []
for event in self.store.list_events(audit_task_id):
data = event.get("data")
if not isinstance(data, Mapping) or data.get("worker_id") != worker.worker_id:
continue
revision = data.get("registration_revision")
if isinstance(revision, bool) or not isinstance(revision, int) or revision <= bound_revision:
continue
events.append((event, data, revision))
if len(events) != len(expected_revisions):
raise ConflictError("worker registration audit chain incomplete")
for expected_revision, (event, data, revision) in zip(expected_revisions, events):
if (
event.get("type") != "WORKER_HEARTBEAT"
or set(data) != {"worker_id", "actor_identity", "registration_revision"}
or data.get("actor_identity") != claim_actor_identity
or revision != expected_revision
):
raise ConflictError("worker registration changed after C6 claim")
def _validate_active_receipt(
self,
transaction: ClaimTransaction,
current: TaskPlaneClaimReceiptV3,
*,
worker_id: str,
actor_identity: str,
) -> None:
"""Validate a receipt without extending its lease or granting a new one."""
if transaction.store is not self.store or not getattr(transaction, "authority_id", ""):
raise AuthorityAtomicBindingUnavailable()
holder_checker = getattr(transaction, "assert_lease_holder", None)
authorization_checker = getattr(transaction, "assert_lease_authorized", None)
if not callable(holder_checker) or not callable(authorization_checker):
raise AuthorityAtomicBindingUnavailable()
holder_checker(current.task_id, current.claim_id, worker_id, actor_identity)
# ``active_claim`` uses the store's authoritative clock and status
# filter. Do not trust a receipt's former lease expiry by itself.
active = self.store.active_claim(current.task_id)
if active is None:
raise LockedError("lease expired or inactive")
if (
active.claim_id != current.claim_id
or active.task_id != current.task_id
or active.worker_id != worker_id
or active.worker_identity != actor_identity
or active.task_revision != current.claimed_revision
or active.operation_digest != current.operation_digest
or active.attempt != current.attempt
or active.lease_until <= time.time()
):
raise ConflictError("active claim does not match receipt")
task = self.store.get_task(current.task_id)
worker = self.store.get_worker(worker_id)
expected_binding = self._validate_persisted_claim_evidence(
current, active=active, task=task, worker=worker
)
task_view = TaskPlaneSchedulabilityAuthority._task_view(task)
worker_view = TaskPlaneSchedulabilityAuthority._worker_view(worker)
authorization = self.binding.read_authorization(
task=task_view, worker=worker_view, actor_identity=actor_identity
)
scope = self.binding.exact_scope_digest(
task=task_view, worker=worker_view, actor_identity=actor_identity
)
if (
task.status not in {"CLAIMED", "RUNNING"}
or task.task_plan.operation_digest() != current.operation_digest
or not isinstance(authorization, Mapping)
or authorization.get("allowed") is not True
or authorization.get("authorization_ref") != expected_binding["authorization_ref"]
or authorization.get("revision") != expected_binding["authorization_revision"]
or not isinstance(scope, tuple)
or len(scope) != 2
or scope[0] is not True
or not isinstance(scope[1], str)
or not scope[1]
or scope[1] != expected_binding["scope_digest"]
or self.binding.freeze_blocked(task_view) is not False
or worker.disabled
or worker.status != "active"
or worker.identity != current.worker_identity
or not has_capabilities(worker.capabilities, task.task_plan.required_capabilities)
):
raise ConflictError("lease authority changed")
authorization_checker(
current.task_id, current.claim_id, worker_id, actor_identity, current
)
@contextmanager
def execution_guard(
self,
receipt: Mapping[str, object],
*,
worker_id: str,
actor_identity: str,
):
"""Hold the authoritative claim binding through one pilot critical section.
This guard never calls ``heartbeat_claim`` and never makes a new claim.
Its context remains open for the caller's critical section, so another
same-store state change cannot interleave between validation and use.
"""
current = _parse_receipt(receipt)
if current.worker_id != worker_id or current.worker_identity != actor_identity:
raise SchedulabilityChanged("lease holder mismatch")
with self.binding.claim_context(self.store) as transaction:
self._validate_active_receipt(
transaction, current, worker_id=worker_id, actor_identity=actor_identity
)
yield current
def renew(
self,
receipt: Mapping[str, object],
*,
worker_id: str,
actor_identity: str,
lease_seconds: int,
) -> Mapping[str, object]:
current = _parse_receipt(receipt)
if current.worker_id != worker_id or current.worker_identity != actor_identity:
raise SchedulabilityChanged("lease holder mismatch")
with self.binding.claim_context(self.store) as transaction:
self._validate_active_receipt(
transaction, current, worker_id=worker_id, actor_identity=actor_identity
)
renewed = self.store.heartbeat_claim(
current.task_id,
current.claim_id,
lease_seconds,
expected_task_revision=current.claimed_revision,
operation_digest=current.operation_digest,
)
return TaskPlaneClaimReceiptV3(
schema=_RECEIPT_SCHEMA,
authority_namespace="task-plane",
task_id=renewed.task_id,
claim_id=renewed.claim_id,
worker_id=renewed.worker_id,
worker_identity=renewed.worker_identity,
claimed_revision=renewed.task_revision,
receipt_revision=current.receipt_revision,
attempt=renewed.attempt,
operation_digest=renewed.operation_digest,
lease_until=renewed.lease_until,
proof_digest=current.proof_digest,
).to_dict()
+439 -128
View File
@@ -10,7 +10,7 @@ import threading
import time
import uuid
from contextlib import contextmanager
from typing import Any
from typing import Any, Mapping
from .capabilities import has_capabilities
from .errors import (
@@ -26,6 +26,7 @@ from .models import (
ProbeOutcome,
TaskPlan,
TaskRecord,
TaskStateTransitionError,
WorkerClaim,
WorkerRegistration,
canonical_json,
@@ -65,11 +66,38 @@ class TaskPlaneStore:
self.high_assurance = high_assurance
self.fault_injector = None
self.probe_attestation_verifier = None
self.evidence_attestation_verifier = None
self._lock = threading.RLock()
self._tx_state = threading.local()
self._closed = False
self._init_db(db_path)
@staticmethod
def _verifier_identity(verifier: Any) -> str | None:
identity = getattr(verifier, "verifier_identity", None)
return str(identity) if identity else None
def configure_evidence_attestation_verifier(self, verifier: Any) -> Any:
"""Install one evidence verifier without weakening an existing binding."""
current = self.evidence_attestation_verifier
if verifier is None:
return current
if current is None:
self.evidence_attestation_verifier = verifier
return verifier
if current is verifier:
return current
current_identity = self._verifier_identity(current)
candidate_identity = self._verifier_identity(verifier)
if (
current_identity is not None
and candidate_identity is not None
and current_identity == candidate_identity
):
return current
raise ValueError("conflicting evidence attestation verifier identity")
def _init_db(self, path: str) -> None:
if path != ":memory:":
parent = os.path.dirname(os.path.abspath(path))
@@ -410,6 +438,18 @@ class TaskPlaneStore:
# an external request. They must never migrate to a phase
# that permits a new lease or commit.
"effect_phase": "TEXT NOT NULL DEFAULT 'LEGACY_UNKNOWN'",
# C6 exact-owner integration extends this existing effect
# ledger. The versioned binding is immutable and its
# separately indexed effect key permits exact readback;
# it is deliberately not a second action ledger.
"host_effect_key": "TEXT",
"host_binding_json": "TEXT",
"host_binding_digest": "TEXT",
"writer_lease_json": "TEXT",
"writer_lease_digest": "TEXT",
"host_closure_json": "TEXT",
"host_closure_digest": "TEXT",
"host_closure_observed_at": "REAL",
},
)
self.conn.execute(
@@ -436,6 +476,11 @@ class TaskPlaneStore:
"CREATE UNIQUE INDEX IF NOT EXISTS ux_root_review_trigger "
"ON task_root_reviews(trigger_child_task_id, trigger_child_revision)"
)
self.conn.execute(
"CREATE UNIQUE INDEX IF NOT EXISTS ux_root_action_host_effect "
"ON task_root_action_effects(host_effect_key) "
"WHERE host_effect_key IS NOT NULL"
)
self.conn.execute(
"""
CREATE TABLE IF NOT EXISTS task_missions (
@@ -754,22 +799,60 @@ class TaskPlaneStore:
status: str,
failure_reason: str | None = None,
) -> TaskRecord:
with self._transaction():
current = self.get_task(task_id)
if current.revision != expected_revision:
raise ConflictError("task revision mismatch")
current.with_status(status)
outer_depth = getattr(self._tx_state, "depth", 0)
observed: TaskRecord | None = None
try:
with self._transaction():
current = self.get_task(task_id)
observed = current
if current.revision != expected_revision:
raise ConflictError("task revision mismatch")
current.with_status(status)
now = _now()
self.conn.execute(
"""
UPDATE tasks
SET status=?, revision=revision+1, updated_at=?, failure_reason=?
WHERE task_id=?
""",
(status, now, failure_reason, task_id),
now = _now()
self.conn.execute(
"""
UPDATE tasks
SET status=?, revision=revision+1, updated_at=?, failure_reason=?
WHERE task_id=?
""",
(status, now, failure_reason, task_id),
)
return self.get_task(task_id)
except TaskStateTransitionError as exc:
if observed is None:
raise
if outer_depth:
raise
self._append_transition_denial(
task_id, observed, attempted_status=status, error=exc
)
return self.get_task(task_id)
raise
def _append_transition_denial(
self,
task_id: str,
observed: TaskRecord,
*,
attempted_status: str,
error: TaskStateTransitionError,
) -> None:
try:
self.append_event(
task_id,
"TASK_TRANSITION_DENIED",
{
"from_status": observed.status,
"attempted_status": attempted_status,
"observed_revision": observed.revision,
"reason": str(error),
},
subject=f"task/{task_id}",
)
except Exception as audit_error:
raise RuntimeError(
"task transition denied but denial audit persistence failed"
) from audit_error
def register_worker(
self,
@@ -1048,112 +1131,203 @@ class TaskPlaneStore:
lease_seconds: int,
expected_revision: int,
operation_digest: str,
*,
proof_digest: str | None = None,
expected_binding: Mapping[str, object] | None = None,
) -> WorkerClaim:
with self._transaction():
return self._claim_task_in_transaction(
task_id,
worker_id,
worker_identity,
lease_seconds,
expected_revision,
operation_digest,
proof_digest=proof_digest,
expected_binding=expected_binding,
)
def _claim_task_in_transaction(
self,
task_id: str,
worker_id: str,
worker_identity: str,
lease_seconds: int,
expected_revision: int,
operation_digest: str,
*,
proof_digest: str | None = None,
expected_binding: Mapping[str, object] | None = None,
) -> WorkerClaim:
"""Claim after revalidating the supplied binding in the active transaction."""
now = _now()
lease_until = now + max(1, int(lease_seconds))
with self._transaction():
worker = self.get_worker(worker_id)
if worker.disabled:
raise LockedError("worker disabled")
if worker.identity != worker_identity:
raise ConflictError("worker identity mismatch")
worker = self.get_worker(worker_id)
if worker.disabled:
raise LockedError("worker disabled")
if worker.identity != worker_identity:
raise ConflictError("worker identity mismatch")
task = self.get_task(task_id)
if task.revision != expected_revision:
raise ConflictError("task revision mismatch")
if task.status not in {"CREATED", "QUEUED", "FAILED_RECOVERABLE"}:
raise ConflictError(f"task in invalid state for claim: {task.status}")
if task.status == "FAILED_RECOVERABLE" and not self.task_has_retryable_probe(
task_id
task = self.get_task(task_id)
if task.revision != expected_revision:
raise ConflictError("task revision mismatch")
if expected_binding is not None:
task_generation = task.task_plan.metadata.get("generation", 0)
if (
not isinstance(task_generation, int)
or isinstance(task_generation, bool)
or task_generation < 0
):
raise LockedError("recoverable task lacks NOT_APPLIED probe evidence")
if self.active_claim(task_id):
raise ConflictError("task already claimed")
if task.task_plan.data_classification == "credential-secret":
if (
worker.worker_type != "secure_file_transformer"
and "secure_file_transformer" not in worker.capabilities
):
from .errors import ForbiddenError
raise ForbiddenError(
"credential-secret task requires secure file transformer"
)
for dependency_id in task.task_plan.dependencies:
if self.get_task(dependency_id).status != "COMPLETED":
raise LockedError("task dependency incomplete")
required = task.task_plan.required_capabilities
if required and not has_capabilities(worker.capabilities, required):
raise ConflictError("worker lacks required capabilities")
attempts = self.conn.execute(
"SELECT COUNT(1) FROM claims WHERE task_id=? AND status='ACTIVE' AND lease_until>?",
(task_id, now),
).fetchone()[0]
if attempts > 0:
raise ConflictError("existing active claim")
claim_id = f"claim-{uuid.uuid4()}"
active_for_worker = self.conn.execute(
"SELECT COUNT(1) FROM claims WHERE worker_id=? AND status='ACTIVE' AND lease_until>?",
(worker_id, now),
).fetchone()[0]
if active_for_worker >= worker.max_concurrency:
raise LockedError("worker concurrency exceeded")
attempt = self.conn.execute(
"SELECT COUNT(1) FROM claims WHERE task_id=?",
(task_id,),
).fetchone()[0] + 1
self.conn.execute(
"INSERT INTO claims(claim_id, task_id, worker_id, worker_identity, claimed_at, lease_until, attempt, task_revision, operation_digest, status) "
"VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, 'ACTIVE')",
(
claim_id,
task_id,
worker_id,
worker_identity,
now,
lease_until,
attempt,
expected_revision,
operation_digest,
),
)
schedule_payload = {
"worker_id": worker_id,
"worker_identity": worker_identity,
"operation_digest": operation_digest,
"attempt": attempt,
raise ConflictError("task generation invalid")
checks = {
"task_revision": task.revision,
"task_generation": task_generation,
"task_status": task.status,
"worker_generation": worker.registration_revision,
"worker_identity": worker.identity,
"capacity_limit": worker.max_concurrency,
"operation_digest": task.task_plan.operation_digest(),
}
existing_decision = self.conn.execute(
"""
SELECT 1 FROM events
WHERE task_id=? AND type='SCHEDULE_DECISION' AND event_data=?
""",
(
task_id,
json.dumps(redact_value(schedule_payload), ensure_ascii=False),
),
).fetchone()
if existing_decision is None:
self.append_event(
task_id,
"SCHEDULE_DECISION",
schedule_payload,
subject=f"task/{task_id}",
for field, actual in checks.items():
if expected_binding.get(field) != actual:
raise ConflictError(f"claim binding {field} mismatch")
if expected_binding.get("operation_digest") != operation_digest:
raise ConflictError("claim binding operation_digest mismatch")
if task.status not in {"CREATED", "QUEUED", "FAILED_RECOVERABLE"}:
raise ConflictError(f"task in invalid state for claim: {task.status}")
if task.status == "FAILED_RECOVERABLE" and not self.task_has_retryable_probe(
task_id
):
raise LockedError("recoverable task lacks NOT_APPLIED probe evidence")
if self.active_claim(task_id):
raise ConflictError("task already claimed")
if task.task_plan.data_classification == "credential-secret":
if (
worker.worker_type != "secure_file_transformer"
and "secure_file_transformer" not in worker.capabilities
):
from .errors import ForbiddenError
raise ForbiddenError(
"credential-secret task requires secure file transformer"
)
self.conn.execute(
"UPDATE tasks SET status='CLAIMED', revision=revision+1, updated_at=? WHERE task_id=?",
(now, task_id),
for dependency_id in task.task_plan.dependencies:
if self.get_task(dependency_id).status != "COMPLETED":
raise LockedError("task dependency incomplete")
required = task.task_plan.required_capabilities
if required and not has_capabilities(worker.capabilities, required):
raise ConflictError("worker lacks required capabilities")
attempts = self.conn.execute(
"SELECT COUNT(1) FROM claims WHERE task_id=? AND status='ACTIVE' AND lease_until>?",
(task_id, now),
).fetchone()[0]
if attempts > 0:
raise ConflictError("existing active claim")
claim_id = f"claim-{uuid.uuid4()}"
active_for_worker = self.conn.execute(
"SELECT COUNT(1) FROM claims WHERE worker_id=? AND status='ACTIVE' AND lease_until>?",
(worker_id, now),
).fetchone()[0]
if active_for_worker >= worker.max_concurrency:
raise LockedError("worker concurrency exceeded")
attempt = self.conn.execute(
"SELECT COUNT(1) FROM claims WHERE task_id=?",
(task_id,),
).fetchone()[0] + 1
self.conn.execute(
"INSERT INTO claims(claim_id, task_id, worker_id, worker_identity, claimed_at, lease_until, attempt, task_revision, operation_digest, status) "
"VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, 'ACTIVE')",
(
claim_id,
task_id,
worker_id,
worker_identity,
now,
lease_until,
attempt,
expected_revision,
operation_digest,
),
)
schedule_payload = {
"worker_id": worker_id,
"worker_identity": worker_identity,
"operation_digest": operation_digest,
"attempt": attempt,
}
if proof_digest is not None:
schedule_payload["proof_digest"] = proof_digest
existing_decision = self.conn.execute(
"""
SELECT 1 FROM events
WHERE task_id=? AND type='SCHEDULE_DECISION' AND event_data=?
""",
(
task_id,
json.dumps(redact_value(schedule_payload), ensure_ascii=False),
),
).fetchone()
if existing_decision is None:
self.append_event(
task_id,
"SCHEDULE_DECISION",
schedule_payload,
subject=f"task/{task_id}",
)
self.append_event(task_id, "TASK_CLAIMED", {"claim_id": claim_id, "worker_id": worker_id}, subject=f"task/{task_id}")
return self._row_to_claim(self.conn.execute(
self.conn.execute(
"UPDATE tasks SET status='CLAIMED', revision=revision+1, updated_at=? WHERE task_id=?",
(now, task_id),
)
claimed_payload = {"claim_id": claim_id, "worker_id": worker_id}
if proof_digest is not None:
claimed_payload["proof_digest"] = proof_digest
if expected_binding is not None:
# Preserve only the C6 conditions rechecked above plus the host
# authorization/scope version fields. A later renew/execute must
# bind to this durable decision, rather than caller-shaped receipt
# JSON. Do not serialize arbitrary values accepted by this
# internal compatibility seam.
claimed_payload["expected_binding"] = {
field: expected_binding.get(field)
for field in (
"task_revision",
"task_generation",
"task_status",
"worker_generation",
"worker_identity",
"authorization_ref",
"authorization_revision",
"scope_digest",
"capacity_limit",
"operation_digest",
)
}
self.append_event(
task_id,
"TASK_CLAIMED",
claimed_payload,
subject=f"task/{task_id}",
)
return self._row_to_claim(
self.conn.execute(
"SELECT * FROM claims WHERE claim_id=?",
(claim_id,),
).fetchone())
).fetchone()
)
def heartbeat_claim(self, task_id: str, claim_id: str, lease_seconds: int) -> WorkerClaim:
def heartbeat_claim(
self,
task_id: str,
claim_id: str,
lease_seconds: int,
*,
expected_task_revision: int | None = None,
operation_digest: str | None = None,
) -> WorkerClaim:
now = _now()
lease_until = now + max(1, lease_seconds)
with self._transaction():
@@ -1168,14 +1342,32 @@ class TaskPlaneStore:
raise ConflictError("claim not active")
if claim.lease_until <= now:
raise LockedError("lease expired")
if (
expected_task_revision is not None
and claim.task_revision != expected_task_revision
):
raise ConflictError("claim task revision mismatch")
if (
operation_digest is not None
and claim.operation_digest != operation_digest
):
raise ConflictError("claim operation digest mismatch")
if self.get_task(task_id).status in {"CANCELLED", "COMPLETED", "FAILED_FINAL"}:
raise ConflictError("task is terminal")
updated = self.conn.execute(
"""
UPDATE claims SET lease_until=?
WHERE claim_id=? AND task_id=? AND status='ACTIVE' AND lease_until>?
AND task_revision=? AND operation_digest=?
""",
(lease_until, claim_id, task_id, now),
(
lease_until,
claim_id,
task_id,
now,
claim.task_revision,
claim.operation_digest,
),
)
if updated.rowcount != 1:
raise LockedError("claim changed during heartbeat")
@@ -1408,6 +1600,7 @@ class TaskPlaneStore:
expected_task_id=task_id,
expected_claim_id=claim_id,
expected_previous_hash=previous,
attestation_verifier=self.evidence_attestation_verifier,
):
raise ConflictError("evidence envelope invalid")
self.add_evidence_envelope(envelope)
@@ -1455,16 +1648,26 @@ class TaskPlaneStore:
def cancel_task_with_event(
self, task_id: str, expected_revision: int, reason: str
) -> TaskRecord:
with self._transaction():
updated = self.cancel_task(task_id, expected_revision, reason)
self._fault("cancel.after_state_before_event")
self.append_event(
task_id,
"TASK_CANCELLED",
{"reason": reason},
subject=f"task/{task_id}",
observed: TaskRecord | None = None
try:
with self._transaction():
observed = self.get_task(task_id)
updated = self.cancel_task(task_id, expected_revision, reason)
self._fault("cancel.after_state_before_event")
self.append_event(
task_id,
"TASK_CANCELLED",
{"reason": reason},
subject=f"task/{task_id}",
)
return updated
except TaskStateTransitionError as exc:
if observed is None:
raise
self._append_transition_denial(
task_id, observed, attempted_status="CANCELLED", error=exc
)
return updated
raise
def delegate_task_with_budget_and_events(
self,
@@ -1699,38 +1902,120 @@ class TaskPlaneStore:
prev = event["hash"]
return True
def add_evidence(self, task_id: str, kind: str, payload: dict[str, Any], worker_identity: str) -> str:
def add_evidence(
self,
task_id: str,
kind: str,
payload: dict[str, Any],
worker_identity: str,
*,
envelope: EvidenceEnvelope | None = None,
claim_id: str | None = None,
evidence_id: str | None = None,
created_at: float | None = None,
expires_at: float | None = None,
signature: str | None = None,
attestation_ref: str | None = None,
signature_algorithm: str | None = None,
signing_key_id: str | None = None,
verifier_identity: str | None = None,
authority_domain: str | None = None,
sequence: int | None = None,
) -> str:
"""Add authenticated evidence while retaining the legacy call shape.
Old unsigned calls now fail with a migration error before any write.
Producers may submit a complete envelope or all explicit proof fields.
"""
if envelope is not None:
if (
envelope.task_id != task_id
or envelope.kind != kind
or envelope.worker_identity != worker_identity
or envelope.payload != redact_value(payload)
):
raise ValidationError("evidence envelope arguments mismatch")
self.add_evidence_envelope(envelope)
return envelope.evidence_id
if not (signature or attestation_ref):
raise ValidationError(
"evidence authentication required; submit a complete verified "
"envelope or explicit signature/provider proof"
)
claim = self.active_claim(task_id)
claim_id = claim.claim_id if claim else "claim-unbound-shadow"
envelope = EvidenceEnvelope.create(
effective_claim_id = claim_id or (
claim.claim_id if claim else "claim-unbound-shadow"
)
if claim is not None and effective_claim_id != claim.claim_id:
raise ValidationError("evidence claim does not match active claim")
built = EvidenceEnvelope.create(
task_id=task_id,
claim_id=claim_id,
claim_id=effective_claim_id,
kind=kind,
worker_identity=worker_identity,
payload=payload,
previous_evidence_hash=self._last_evidence_hash(task_id),
evidence_id=evidence_id,
created_at=created_at,
expires_at=expires_at,
signature=signature,
attestation_ref=attestation_ref,
signature_algorithm=signature_algorithm,
signing_key_id=signing_key_id,
verifier_identity=verifier_identity,
authority_domain=authority_domain,
sequence=sequence,
)
self.add_evidence_envelope(envelope)
return envelope.evidence_id
self.add_evidence_envelope(built)
return built.evidence_id
def _last_evidence_hash(self, task_id: str) -> str:
row = self.conn.execute(
"""
SELECT digest FROM evidence WHERE task_id=?
ORDER BY created_at DESC,evidence_id DESC LIMIT 1
ORDER BY rowid DESC LIMIT 1
""",
(task_id,),
).fetchone()
return str(row["digest"]) if row else ""
def _last_evidence_sequence(self, authority_domain: str) -> int | None:
row = self.conn.execute(
"""
SELECT MAX(CAST(json_extract(envelope_json, '$.sequence') AS INTEGER))
FROM evidence
WHERE json_extract(envelope_json, '$.authority_domain')=?
AND json_type(envelope_json, '$.sequence')='integer'
""",
(authority_domain,),
).fetchone()
return None if row is None or row[0] is None else int(row[0])
def add_evidence_envelope(self, envelope: EvidenceEnvelope) -> str:
with self._transaction():
self.get_task(envelope.task_id)
if (envelope.authority_domain is None) != (envelope.sequence is None):
raise ConflictError("evidence authority domain and sequence must be paired")
if envelope.authority_domain is not None:
if (
not envelope.authority_domain
or not isinstance(envelope.sequence, int)
or isinstance(envelope.sequence, bool)
or envelope.sequence < 1
):
raise ConflictError("evidence authority sequence invalid")
last_sequence = self._last_evidence_sequence(
envelope.authority_domain
)
if last_sequence is not None and envelope.sequence <= last_sequence:
raise ConflictError("evidence authority sequence not monotonic")
expected_previous = self._last_evidence_hash(envelope.task_id)
if not envelope.verify(
expected_task_id=envelope.task_id,
expected_claim_id=envelope.claim_id,
expected_previous_hash=expected_previous,
attestation_verifier=self.evidence_attestation_verifier,
):
raise ConflictError("evidence envelope invalid")
self.conn.execute(
@@ -1759,13 +2044,14 @@ class TaskPlaneStore:
task_id = _validated_id(task_id, "task_id")
with self._lock:
rows = self.conn.execute(
"SELECT * FROM evidence WHERE task_id=? ORDER BY created_at ASC",
"SELECT * FROM evidence WHERE task_id=? ORDER BY rowid ASC",
(task_id,),
).fetchall()
return [dict(r) for r in rows]
def verify_evidence_integrity(self, task_id: str) -> bool:
previous = ""
domain_sequences: dict[str, int] = {}
for evidence in self.list_evidence(task_id):
try:
raw = evidence.get("envelope_json")
@@ -1781,10 +2067,24 @@ class TaskPlaneStore:
worker_identity=payload["worker_identity"],
payload=payload["payload"],
created_at=float(payload["created_at"]),
expires_at=float(
payload.get(
"expires_at", float(payload["created_at"]) + 300.0
)
),
previous_evidence_hash=payload["previous_evidence_hash"],
evidence_digest=payload["evidence_digest"],
signature=payload.get("signature"),
attestation_ref=payload.get("attestation_ref"),
signature_algorithm=payload.get("signature_algorithm"),
signing_key_id=payload.get("signing_key_id"),
verifier_identity=payload.get("verifier_identity"),
authority_domain=payload.get("authority_domain"),
sequence=(
None
if payload.get("sequence") is None
else int(payload["sequence"])
),
)
except (KeyError, TypeError, ValueError, json.JSONDecodeError):
return False
@@ -1798,9 +2098,20 @@ class TaskPlaneStore:
expected_task_id=task_id,
expected_claim_id=envelope.claim_id,
expected_previous_hash=previous,
attestation_verifier=self.evidence_attestation_verifier,
)
):
return False
if envelope.authority_domain is not None:
if (
not isinstance(envelope.sequence, int)
or isinstance(envelope.sequence, bool)
or envelope.sequence <= domain_sequences.get(
envelope.authority_domain, 0
)
):
return False
domain_sequences[envelope.authority_domain] = envelope.sequence
previous = envelope.evidence_digest
return True
@@ -26,6 +26,8 @@ import re
import stat
import subprocess
import sys
import tarfile
import tempfile
import time
from datetime import datetime, timezone
from pathlib import Path
@@ -405,51 +407,85 @@ def run_secret_scan_provider(
tree_oid: str,
store_id: str = "secret_scan",
provider_identity: str = "aios.secret-scanner",
artifact_path: Path | None = None,
) -> tuple[Mapping[str, Any], str]:
"""Execute the real secret scanner (gitleaks) and produce a root receipt."""
if not Path(gitleaks_bin).is_file():
raise TrustedProviderExecutionError("SECRET_SCANNER_UNAVAILABLE")
artifact_sha = None
if artifact_path is not None:
artifact_sha = _validate_artifact_scan_target(artifact_path, target)
# --no-git scans the working tree as a filesystem (does not require a .git
# dir), so an exported release checkout can be scanned faithfully.
scanned = _run(
[
gitleaks_bin, "detect",
"--source", str(target),
"--no-git",
"--no-banner",
"--exit-code", "0",
],
cwd=target,
timeout=1800,
)
warnings = 0
# gitleaks exits 1 when findings are present; with --exit-code 0 it returns 0
# but prints findings. Count lines that indicate a finding.
finding_lines = [
ln for ln in scanned.stdout.splitlines()
if ("Finding:" in ln and ":" in ln and "RuleID" not in ln)
] + [
ln for ln in scanned.stderr.splitlines()
if ("Finding:" in ln and ":" in ln and "RuleID" not in ln)
]
artifact_sha = None
if target.is_file():
artifact_sha = hashlib.sha256(target.read_bytes()).hexdigest()
with tempfile.TemporaryDirectory(prefix="aios-secret-report-") as report_root:
report = Path(report_root) / "findings.json"
scanned = _run(
[
gitleaks_bin, "detect",
"--source", str(target),
"--no-git",
"--no-banner",
"--exit-code", "1",
"--report-format", "json", "--report-path", str(report),
],
cwd=target,
timeout=1800,
)
try:
findings = json.loads(report.read_text())
except (OSError, ValueError) as exc:
raise TrustedProviderExecutionError("SECRET_SCANNER_REPORT_INVALID") from exc
if not isinstance(findings, list) or any(not isinstance(v, dict) for v in findings):
raise TrustedProviderExecutionError("SECRET_SCANNER_REPORT_INVALID")
report_sha = hashlib.sha256(report.read_bytes()).hexdigest()
if artifact_path is not None and _validate_artifact_scan_target(artifact_path, target) != artifact_sha:
raise TrustedProviderExecutionError("SECRET_SCAN_ARTIFACT_CHANGED")
# Passed only when the scanner actually ran cleanly (exit 0) AND found zero
# leaks. A scanner crash (non-zero exit) is never a pass.
clean_run = scanned.returncode == 0
result = {
"passed": clean_run and len(finding_lines) == 0,
"passed": clean_run and len(findings) == 0,
"scanner": "gitleaks",
"exit_code": scanned.returncode,
"target": str(target),
"findings": len(finding_lines),
"findings": len(findings),
"report_sha256": report_sha,
"artifact_sha256": artifact_sha,
"clean_run": clean_run,
}
return result, _receipt_ref_for(result, "secret_scan")
def _validate_artifact_scan_target(artifact: Path, target: Path) -> str:
"""Bind a real filesystem scan to every byte of its exact archive payload."""
if artifact.is_symlink() or not artifact.is_file() or target.is_symlink() or not target.is_dir():
raise TrustedProviderExecutionError("SECRET_SCAN_ARTIFACT_TARGET_INVALID")
raw = artifact.read_bytes()
import io
expected: set[str] = set()
try:
with tarfile.open(fileobj=io.BytesIO(raw), mode="r:*") as archive:
members = archive.getmembers()
if not members or len(members) > 20_000 or sum(m.size for m in members) > 2 * 1024**3:
raise TrustedProviderExecutionError("SECRET_SCAN_ARCHIVE_LIMIT")
for member in members:
parts = Path(member.name).parts
if not member.isfile() or member.name.startswith("/") or "\\" in member.name or not parts or any(p in {".", ".."} for p in parts) or member.name in expected:
raise TrustedProviderExecutionError("SECRET_SCAN_ARCHIVE_INVALID")
path = target.joinpath(*parts)
if path.is_symlink() or not path.is_file() or not path.resolve().is_relative_to(target.resolve()):
raise TrustedProviderExecutionError("SECRET_SCAN_PAYLOAD_MISMATCH")
if archive.extractfile(member).read() != path.read_bytes():
raise TrustedProviderExecutionError("SECRET_SCAN_PAYLOAD_MISMATCH")
expected.add(member.name)
except (tarfile.TarError, OSError) as exc:
raise TrustedProviderExecutionError("SECRET_SCAN_ARCHIVE_INVALID") from exc
actual = {p.relative_to(target).as_posix() for p in target.rglob("*") if p.is_file()}
if actual != expected or any(p.is_symlink() for p in target.rglob("*")):
raise TrustedProviderExecutionError("SECRET_SCAN_PAYLOAD_MISMATCH")
return hashlib.sha256(raw).hexdigest()
def _receipt_ref_for(result: Mapping[str, Any], kind: str) -> str:
run_id = hashlib.sha256(canonical_json_bytes(result)).hexdigest()[:16]
return f"run-{run_id}/receipt.json"
@@ -531,6 +567,7 @@ def _run_provider_executor(
target=repo_root,
source_oid=source_oid,
tree_oid=tree_oid,
artifact_path=(Path(os.environ["AIOS_RELEASE_ARTIFACT"]) if os.environ.get("AIOS_RELEASE_ARTIFACT") else None),
)
if kind == "gitea":
gitea_api_url = os.environ.get("AIOS_GITEA_API_URL", "").strip()
@@ -601,9 +638,14 @@ if __name__ == "__main__":
parser.add_argument("--store-root", required=True)
parser.add_argument("--receipt-ref", required=True)
parser.add_argument("--repo-root", default=None, help="Real source checkout to execute against")
parser.add_argument("--artifact-path", type=Path, default=None)
args = parser.parse_args()
if args.repo_root:
os.environ["AIOS_REPO_ROOT"] = args.repo_root
if args.artifact_path:
if args.kind != "secret_scan":
raise SystemExit("ARTIFACT_BINDING_REQUIRES_SECRET_SCAN")
os.environ["AIOS_RELEASE_ARTIFACT"] = str(args.artifact_path)
try:
digest = produce(
args.kind,
@@ -46,12 +46,23 @@ def _paths(binding: Mapping[str, Any]) -> dict[str, Path]:
slug=version.lower()
return {"release_dir":root/(version+"-"+str(binding["target_oid"])[:12]+"-"+str(binding["artifact_sha256"])[:12]),"current_link":Path("/opt/aios")/slug,"env_file":Path("/etc/aios")/(slug+".env"),"state_file":Path("/var/lib/aios")/slug/"deployment-state.json","unit_file":Path("/etc/systemd/system")/(service+".service" if not service.endswith(".service") else service),"health_url":f"http://127.0.0.1:{int(binding['production_port'])}/health"}
def _health(url: str, binding: Mapping[str, Any]) -> dict[str, Any]:
def _health(url: str, binding: Mapping[str, Any], *, runtime_verifier=None) -> dict[str, Any]:
try:
with urllib.request.urlopen(url, timeout=5) as response: body=json.loads(response.read().decode()); status=response.status
except (OSError, ValueError, urllib.error.URLError): return {"status":"UNAVAILABLE"}
exact=(status==200 and body.get("version")==binding["version"] and body.get("commit_oid")==binding["target_oid"] and body.get("artifact_sha256")==binding["artifact_sha256"] and body.get("deployment_state")=="ACTIVE" and body.get("production_deployed") is True)
return {"status":"ACTIVE_VERIFIED" if exact else "CONFLICT", "http_status":status, **body}
if str(binding["version"]).startswith("V2R2C6"):
# Health bodies are untrusted declarations; even a body containing PASS
# cannot certify itself. A host-owned independent verifier must bind
# the response to the exact candidate and fresh observed runtime.
try:
independently_verified = callable(runtime_verifier) and runtime_verifier(binding, body) is True
except Exception:
independently_verified = False
result_status = "ACTIVE_VERIFIED" if exact and independently_verified else "UNKNOWN"
else:
result_status = "ACTIVE_VERIFIED" if exact else "CONFLICT"
return {**body, "status": result_status, "http_status": status}
def _extract(artifact: Path, destination: Path) -> None:
import tarfile
@@ -70,7 +81,13 @@ def probe(value: Mapping[str, Any]) -> dict[str, Any]:
return _health(str(p["health_url"]),binding) if active else {"status":"NOT_APPLIED","version":binding["version"],"target_oid":binding["target_oid"],"artifact_sha256":binding["artifact_sha256"],"production_deployed":False,"deployment_state":"ABSENT"}
def apply(value: Mapping[str, Any]) -> dict[str, Any]:
binding=_binding(value); p=_paths(binding); p["release_dir"].parent.mkdir(parents=True, exist_ok=True); artifact=Path(str(binding["artifact_path"]))
binding=_binding(value)
if str(binding["version"]).startswith("V2R2C6"):
# This adapter installs exactly one legacy health service. A C6
# multi-component payload requires the governed component host binding;
# do not report a health-only installation as a C6 deployment.
raise AdapterBlocked("component_host_binding_required")
p=_paths(binding); p["release_dir"].parent.mkdir(parents=True, exist_ok=True); artifact=Path(str(binding["artifact_path"]))
if artifact.stat().st_size != int(binding["artifact_size"]) or _sha(artifact)!=binding["artifact_sha256"]: raise AdapterBlocked("artifact_binding_mismatch")
marker={"schema":"aios.trusted-runtime-release-binding.v1",**binding}
marker_path=p["release_dir"]/"DEPLOYMENT-BINDING.json"
+6 -1
View File
@@ -78,7 +78,12 @@ def serve(host: str, port: int) -> None:
def main() -> int:
parser=argparse.ArgumentParser(); parser.add_argument("--root",type=Path,default=Path(".")); parser.add_argument("--manifest",type=Path); parser.add_argument("--serve",action="store_true"); parser.add_argument("--host",default="127.0.0.1"); parser.add_argument("--port",type=int,default=18927); args=parser.parse_args()
if args.serve: serve(args.host,args.port); return 0
if args.serve:
# A health listener may report liveness only after its real artifact
# manifest has been verified. This is intentionally not runtime
# identity proof; that needs a separate trusted observation.
verify_manifest(args.root,args.manifest)
serve(args.host,args.port); return 0
print(json.dumps(verify_manifest(args.root,args.manifest),sort_keys=True)); return 0
if __name__ == "__main__": raise SystemExit(main())
+39 -5
View File
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
from __future__ import annotations
import json, os, re, sqlite3, subprocess, time, uuid, hashlib, urllib.parse, html
import json, os, re, sqlite3, subprocess, time, uuid, hashlib, urllib.parse, html, ipaddress
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
from pathlib import Path
from typing import Any
@@ -14,6 +14,25 @@ HOST = os.getenv('AIOS_GITOPS_HOST', '127.0.0.1')
PORT = int(os.getenv('AIOS_GITOPS_PORT', '18890'))
ENTRY = os.getenv('AIOS_ENTRY', 'https://cnnetmkt.top')
DEV_ALLOW = os.getenv('AIOS_GITOPS_DEV_ALLOW', '0') == '1'
def _loopback(value):
try: return ipaddress.ip_address(value).is_loopback
except (ValueError, TypeError): return False
def validate_dev_profile():
if DEV_ALLOW and not (os.getenv('AIOS_RUNTIME_PROFILE') == 'isolated-staging' and _loopback(HOST)):
raise RuntimeError('DEV_ALLOW requires isolated-staging and loopback')
def runtime_selection(principal, data):
if not principal.get('token_ok'): raise PermissionError('authenticated principal required')
if any(key in data for key in ('username', 'user_id', 'role', 'tier')):
raise ValueError('runtime identity must come from authenticated principal')
username=principal['username']; role=principal['role']
if role not in _ROLE_WHITELIST or re.fullmatch(r'[A-Za-z0-9_@.-]{1,120}', username) is None or username in ('.','..'):
raise ValueError('invalid authenticated principal')
tier='ultra' if role=='owner' else 'regular'
return username, role, tier
BASE_MODELS = [
{'id':'base.deepseekv4-flash','provider':'one-api','model':'deepseekv4-flash','display':'DeepSeek V4 Flash','tier':'regular','reasoning':1,'vision':0,'tools':1,'context':128000},
{'id':'base.qwen3.6-35b','provider':'one-api','model':'qwen3.6-35b','display':'Qwen 3.6 35B','tier':'pro','reasoning':1,'vision':0,'tools':1,'context':262144},
@@ -66,6 +85,7 @@ def token_identity(token: str):
DEV_ALLOW flag maps a fixed dev principal (must be explicitly enabled).
"""
if DEV_ALLOW:
validate_dev_profile()
return {'username': 'dev-owner', 'role': 'owner'}
if not token or not TOKEN_FILE.exists():
return None
@@ -83,7 +103,9 @@ def token_identity(token: str):
return None
def valid_token(token: str) -> bool:
if DEV_ALLOW: return True
if DEV_ALLOW:
validate_dev_profile()
return True
return token_identity(token) is not None
def git_cmd(args, check=False):
WORKSPACE.mkdir(parents=True, exist_ok=True); p = subprocess.run(['git'] + args, cwd=str(WORKSPACE), text=True, capture_output=True)
@@ -159,7 +181,19 @@ class Handler(BaseHTTPRequestHandler):
self.send_json({'error':'not_found','path':path},404)
except Exception as e: self.send_json({'error':str(e)},500)
def do_POST(self):
init_db(); path = urllib.parse.urlparse(self.path).path; data = parse_body(self)
path = urllib.parse.urlparse(self.path).path
if path == '/api/runtime/select':
if not (_loopback(HOST) and _loopback(self.client_address[0])):
self.send_json({'error':'loopback ingress required'},403); return
a = self.require_write()
if not a: return
data = parse_body(self)
try: runtime_selection(a, data)
except (ValueError, PermissionError):
self.send_json({'error':'invalid runtime selection identity'},403); return
else:
data = parse_body(self)
init_db()
try:
if path == '/api/model/custom/add':
a = self.require_write();
@@ -213,7 +247,7 @@ class Handler(BaseHTTPRequestHandler):
if not m: self.send_json({'error':'model not found'},404); return
self.send_json({'status':'ok' if m['base_url'] and m['model_name'] else 'failed','live_test':os.getenv('AIOS_ENABLE_MODEL_LIVE_TEST','0')=='1','model':m['model_name'],'base_url':m['base_url'],'note':'Network live call is disabled unless AIOS_ENABLE_MODEL_LIVE_TEST=1'}); return
if path == '/api/runtime/select':
a=self.actor(); username=safe_name(data.get('username') or a['username']); role=data.get('role') or a['role']; tier=data.get('tier') or ('ultra' if role=='owner' else 'regular'); risk=data.get('risk','normal')
username,role,tier=runtime_selection(a, data); risk=data.get('risk','normal')
if risk in ('high_tool','shell','docker','write_prod'): runtime,backend='ephemeral_sandbox','openclaw_executor'
elif role in ('owner','admin') or tier=='ultra': runtime,backend='dedicated','hermes'
elif tier=='pro': runtime,backend='dedicated_or_shared','hermes'
@@ -268,5 +302,5 @@ class Handler(BaseHTTPRequestHandler):
except Exception as e: self.send_json({'error':str(e)},500)
def api_spec(): return {'version':'V1R1C0','short_links':['/base','/ver','/diff','/patch','/run','/model','/api'],'gitops_get':['/api/status','/api/base','/api/ver','/api/diff','/api/patch'],'gitops_post':['/api/branch','/api/file','/api/commit','/api/tag','/api/rollback','/api/nl'],'model_get':['/api/model/base','/api/model/custom','/api/model/priority'],'model_post':['/api/model/custom/add','/api/model/custom/update','/api/model/delete','/api/model/priority/set','/api/model/route/resolve','/api/model/test'],'runtime':['/api/runtime/status','/api/runtime/select','/api/runtime/register','/api/runtime/heartbeat'],'auth':'write APIs require Bearer token unless AIOS_GITOPS_DEV_ALLOW=1 for local acceptance tests'}
def main(): init_db(); print(f'AIOS V1R1C0 GitOps Model API listening on {HOST}:{PORT}'); ThreadingHTTPServer((HOST, PORT), Handler).serve_forever()
def main(): validate_dev_profile(); init_db(); print(f'AIOS V1R1C0 GitOps Model API listening on {HOST}:{PORT}'); ThreadingHTTPServer((HOST, PORT), Handler).serve_forever()
if __name__ == '__main__': main()
@@ -0,0 +1,30 @@
# C4 capability contract provenance
The C4 checkout has no `aios_capability_contract.py`, although
`active-runtime/app/aios_agent_task_protocol.py` imports it at module scope.
The read-only QA capture used for the missing source closure is:
- source: `C:\\Users\\ImRuY\\.codex-restored\\work\\v2r2c5-release-20260926\\qa\\capability-contract.txt`
- SHA256: `c97c9f8a3cf199a9c0348da1212aa26d161aad8ddaed84db534e46f490f2422f`
- 140 lines
The contract preserves separate task and worker alias maps, maps unknown task
names to `exact:<token>`, ignores unknown worker names, and returns false for
unknown or mismatched eligibility. It is used by formal AgentTask poll and
claim paths only. Generic `task_plane` matching remains outside this lane.
The authoritative runtime overlay is captured separately by QA:
- base SHA256: `953de1d491b4f1bc47c71f502027fd488e6c48ecc49da8a91dc9d8754c320d17`
- overlay SHA256: `7555aa067fc01c6a25276a834dda4924b404f3b0c529ed3f99e9f1d83d18798e`
The consolidated module preserves the base mappings and adds only the seven
explicit lifecycle labels: `agent-task:control`, `governed.task.create`,
`governed.write.files`, `task.principal.issue`, `testing`, `verification`, and
`writer.lease`. It does not depend on an absolute deployed path.
Validation logs:
- `PYTHONPATH=active-runtime/app python -m pytest -q tests/test_v2r2c5_capability_contract.py` -> 6 passed.
- `PYTHONPATH=active-runtime/app;active-runtime python -m pytest -q tests/test_task_admission_bootstrap.py tests/test_task_admission_bootstrap_chain_contract.py` -> 45 passed.
- `tests/test_task_admission_bootstrap_http.py` could not collect on Windows because the existing `gitea_tag_publication.py` imports unavailable `fcntl`; no capability assertion ran in that file.
@@ -0,0 +1,357 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://schemas.aios.invalid/release/v1/authorization-bundle.schema.json",
"type": "object",
"properties": {
"schema_version": {
"const": "1.0.0"
},
"kind": {
"const": "ReleaseAuthorizationBundle"
},
"data_class": {
"$ref": "common.schema.json#/$defs/fixture"
},
"bundle_id": {
"$ref": "common.schema.json#/$defs/id"
},
"bundle_revision": {
"type": "integer",
"minimum": 1
},
"mission_id": {
"$ref": "common.schema.json#/$defs/id"
},
"release_run_id": {
"$ref": "common.schema.json#/$defs/id"
},
"version": {
"type": "string",
"minLength": 1
},
"status": {
"enum": [
"REQUESTED",
"ACTIVE",
"REVOKED",
"COMPLETED"
]
},
"owner_intent_ref": {
"$ref": "common.schema.json#/$defs/digest_ref"
},
"issuer_ref": {
"type": "string",
"minLength": 1
},
"root_grant_ref": {
"anyOf": [
{
"$ref": "common.schema.json#/$defs/digest_ref"
},
{
"type": "null"
}
]
},
"authority_receipt_ref": {
"anyOf": [
{
"$ref": "common.schema.json#/$defs/digest_ref"
},
{
"type": "null"
}
]
},
"scope": {
"type": "object",
"properties": {
"repositories": {
"type": "array",
"items": {
"$ref": "common.schema.json#/$defs/id"
},
"minItems": 1,
"uniqueItems": true
},
"environments": {
"type": "array",
"items": {
"$ref": "common.schema.json#/$defs/id"
},
"minItems": 1,
"uniqueItems": true
},
"component_ids": {
"type": "array",
"items": {
"$ref": "common.schema.json#/$defs/id"
},
"minItems": 1,
"uniqueItems": true
},
"actions": {
"type": "array",
"items": {
"$ref": "common.schema.json#/$defs/action"
},
"minItems": 1,
"uniqueItems": true
}
},
"required": [
"repositories",
"environments",
"component_ids",
"actions"
],
"additionalProperties": false
},
"delegation": {
"type": "object",
"properties": {
"child_tasks": {
"const": true
},
"successor_tasks": {
"const": true
},
"agent_transfer": {
"const": true
},
"scope_must_not_expand": {
"const": true
}
},
"required": [
"child_tasks",
"successor_tasks",
"agent_transfer",
"scope_must_not_expand"
],
"additionalProperties": false
},
"validity": {
"type": "object",
"properties": {
"not_before": {
"$ref": "common.schema.json#/$defs/time"
},
"until": {
"type": "array",
"items": {
"enum": [
"RELEASE_CLOSED",
"REVOKED",
"CANCELLED"
]
},
"uniqueItems": true,
"minItems": 3,
"maxItems": 3
},
"revocation_ref": {
"type": "string",
"minLength": 1
},
"revocation_generation": {
"type": "integer",
"minimum": 1
}
},
"required": [
"not_before",
"until",
"revocation_ref",
"revocation_generation"
],
"additionalProperties": false
},
"credentials": {
"type": "object",
"properties": {
"maximum_ttl_seconds": {
"type": "integer",
"minimum": 1,
"maximum": 3600
},
"renewable_within_goal": {
"const": true
},
"issuer_operation_ref": {
"type": "string",
"minLength": 1
}
},
"required": [
"maximum_ttl_seconds",
"renewable_within_goal",
"issuer_operation_ref"
],
"additionalProperties": false
},
"risk_budget": {
"type": "object",
"properties": {
"max_cumulative_unavailability_seconds": {
"type": "integer",
"minimum": 0
},
"max_error_rate_delta": {
"type": "number",
"minimum": 0,
"maximum": 1
},
"observation_seconds": {
"type": "integer",
"minimum": 1
},
"policy_source_ref": {
"type": "string",
"minLength": 1
}
},
"required": [
"max_cumulative_unavailability_seconds",
"max_error_rate_delta",
"observation_seconds",
"policy_source_ref"
],
"additionalProperties": false
},
"forbidden": {
"type": "array",
"items": {
"enum": [
"delete_database",
"format_storage",
"destroy_unique_backup",
"change_root_trust",
"disable_audit"
]
},
"uniqueItems": true,
"minItems": 5,
"maxItems": 5
},
"owner_required_reasons": {
"type": "array",
"items": {
"$ref": "common.schema.json#/$defs/owner_reason"
},
"uniqueItems": true,
"minItems": 4,
"maxItems": 4
},
"execution_binding": {
"anyOf": [
{
"type": "object",
"properties": {
"contract_id": {
"$ref": "common.schema.json#/$defs/id"
},
"contract_revision": {
"type": "integer",
"minimum": 1
},
"contract_sha256": {
"$ref": "common.schema.json#/$defs/sha256"
},
"content_key": {
"$ref": "common.schema.json#/$defs/sha256"
},
"target_environment": {
"$ref": "common.schema.json#/$defs/id"
},
"task_id": {
"$ref": "common.schema.json#/$defs/id"
},
"attempt_id": {
"$ref": "common.schema.json#/$defs/id"
},
"lease_generation": {
"type": "integer",
"minimum": 1
},
"capability_receipt_ref": {
"$ref": "common.schema.json#/$defs/digest_ref"
},
"expires_at": {
"$ref": "common.schema.json#/$defs/time"
}
},
"required": [
"contract_id",
"contract_revision",
"contract_sha256",
"content_key",
"target_environment",
"task_id",
"attempt_id",
"lease_generation",
"capability_receipt_ref",
"expires_at"
],
"additionalProperties": false
},
{
"type": "null"
}
]
},
"extensions": {
"$ref": "common.schema.json#/$defs/extensions"
}
},
"required": [
"schema_version",
"kind",
"data_class",
"bundle_id",
"bundle_revision",
"mission_id",
"release_run_id",
"version",
"status",
"owner_intent_ref",
"issuer_ref",
"root_grant_ref",
"authority_receipt_ref",
"scope",
"delegation",
"validity",
"credentials",
"risk_budget",
"forbidden",
"owner_required_reasons",
"execution_binding",
"extensions"
],
"additionalProperties": false,
"allOf": [
{
"if": {
"properties": {
"status": {
"const": "ACTIVE"
}
},
"required": [
"status"
]
},
"then": {
"properties": {
"root_grant_ref": {
"$ref": "common.schema.json#/$defs/digest_ref"
},
"authority_receipt_ref": {
"$ref": "common.schema.json#/$defs/digest_ref"
}
}
}
}
]
}
+160
View File
@@ -0,0 +1,160 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://schemas.aios.invalid/release/v1/common.schema.json",
"$comment": "Local identifiers only; no network retrieval. LIVE_CONFIG is a declaration, never proof of authorization.",
"$defs": {
"id": {
"type": "string",
"pattern": "^[A-Za-z0-9][A-Za-z0-9._:/-]{0,199}$"
},
"sha256": {
"type": "string",
"pattern": "^[a-f0-9]{64}$"
},
"oid": {
"type": "string",
"pattern": "^(?:[a-f0-9]{40}|[a-f0-9]{64})$"
},
"time": {
"type": "string",
"format": "date-time"
},
"ref": {
"type": "string",
"minLength": 3
},
"digest_ref": {
"type": "object",
"properties": {
"ref": {
"type": "string",
"minLength": 1
},
"sha256": {
"type": "string",
"pattern": "^[a-f0-9]{64}$"
}
},
"required": [
"ref",
"sha256"
],
"additionalProperties": false
},
"stage": {
"enum": [
"resolve_contract",
"preflight",
"build_test",
"evidence_closure",
"candidate_freeze",
"authorization_check",
"runtime_switch",
"verify",
"tag",
"baseline_registry",
"acceptance",
"close"
]
},
"action": {
"enum": [
"source_update",
"build",
"test",
"staging",
"backup",
"governed_merge",
"production_switch",
"rollback",
"verify",
"tag",
"baseline_update",
"acceptance",
"task_create",
"agent_transfer",
"credential_derive",
"lease_renew",
"evidence_append"
]
},
"actor": {
"type": "object",
"properties": {
"kind": {
"enum": [
"OWNER",
"AGENT",
"SYSTEM"
]
},
"ref": {
"type": "string",
"minLength": 1
}
},
"required": [
"kind",
"ref"
],
"additionalProperties": false
},
"fixture": {
"enum": [
"FIXTURE",
"LIVE_CONFIG"
]
},
"state": {
"enum": [
"CREATED",
"PREPARING",
"RUNNING",
"WAITING_ENVIRONMENT",
"WAITING_CAPACITY",
"WAITING_AUTHORITY",
"WAITING_AGENT",
"WAITING_OWNER",
"RECONCILING",
"RECOVERING",
"VERIFYING",
"FINALIZING",
"ACCEPTED",
"FAILED",
"ROLLED_BACK",
"CLOSED",
"CANCELLED"
]
},
"activity": {
"enum": [
"EXECUTING",
"WAIT_AUTHORITY",
"WAIT_ENVIRONMENT",
"WAIT_AGENT",
"WAIT_REVIEW",
"WAIT_OWNER",
"RECOVERY",
"QUEUED",
"UNKNOWN"
]
},
"owner_reason": {
"enum": [
"GOAL_CHANGE",
"TRUST_BOUNDARY_CHANGE",
"IRREVERSIBLE_OPERATION",
"BUSINESS_RISK_BUDGET_EXCEEDED"
]
},
"extensions": {
"type": "object",
"propertyNames": {
"pattern": "^[a-z][a-z0-9]*(?:[._-][a-z0-9]+)+$"
},
"additionalProperties": {
"type": "object"
}
}
}
}
@@ -0,0 +1,683 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://schemas.aios.invalid/release/v1/release-contract.schema.json",
"type": "object",
"properties": {
"schema_version": {
"const": "1.0.0"
},
"kind": {
"const": "ReleaseContract"
},
"data_class": {
"$ref": "common.schema.json#/$defs/fixture"
},
"contract_id": {
"$ref": "common.schema.json#/$defs/id"
},
"contract_revision": {
"type": "integer",
"minimum": 1
},
"mission_id": {
"$ref": "common.schema.json#/$defs/id"
},
"release_run_id": {
"$ref": "common.schema.json#/$defs/id"
},
"version": {
"type": "string",
"minLength": 1
},
"state": {
"enum": [
"DRAFT",
"RESOLVED",
"FROZEN"
]
},
"target_environment": {
"$ref": "common.schema.json#/$defs/id"
},
"policy_ref": {
"$ref": "common.schema.json#/$defs/digest_ref"
},
"catalog_ref": {
"$ref": "common.schema.json#/$defs/digest_ref"
},
"source": {
"type": "object",
"properties": {
"repository_id": {
"$ref": "common.schema.json#/$defs/id"
},
"authority_ref": {
"type": "string",
"minLength": 1
},
"branch": {
"type": "string",
"minLength": 1
},
"commit_oid": {
"anyOf": [
{
"$ref": "common.schema.json#/$defs/oid"
},
{
"type": "null"
}
]
},
"tree_oid": {
"anyOf": [
{
"$ref": "common.schema.json#/$defs/oid"
},
{
"type": "null"
}
]
},
"governed_commit_oid": {
"anyOf": [
{
"$ref": "common.schema.json#/$defs/oid"
},
{
"type": "null"
}
]
}
},
"required": [
"repository_id",
"authority_ref",
"branch",
"commit_oid",
"tree_oid",
"governed_commit_oid"
],
"additionalProperties": false
},
"build": {
"type": "object",
"properties": {
"definition_ref": {
"$ref": "common.schema.json#/$defs/digest_ref"
},
"output_kind": {
"const": "runtime"
}
},
"required": [
"definition_ref",
"output_kind"
],
"additionalProperties": false
},
"artifact": {
"anyOf": [
{
"type": "object",
"properties": {
"kind": {
"enum": [
"runtime",
"source_archive"
]
},
"ref": {
"type": "string",
"minLength": 1
},
"sha256": {
"$ref": "common.schema.json#/$defs/sha256"
},
"size_bytes": {
"type": "integer",
"minimum": 1
},
"provenance_ref": {
"$ref": "common.schema.json#/$defs/digest_ref"
}
},
"required": [
"kind",
"ref",
"sha256",
"size_bytes",
"provenance_ref"
],
"additionalProperties": false
},
{
"type": "null"
}
]
},
"components": {
"type": "array",
"items": {
"type": "object",
"properties": {
"component_id": {
"$ref": "common.schema.json#/$defs/id"
},
"host_ref": {
"type": "string",
"minLength": 1
},
"service_unit": {
"type": "string",
"minLength": 1
},
"runtime_root": {
"type": "string",
"minLength": 1
},
"bind_address": {
"type": "string",
"minLength": 1
},
"port": {
"type": "integer",
"minimum": 1,
"maximum": 65535
},
"config_ref": {
"$ref": "common.schema.json#/$defs/digest_ref"
},
"credential_refs": {
"type": "array",
"items": {
"type": "string",
"minLength": 1
},
"minItems": 0,
"uniqueItems": true
},
"owner_ref": {
"type": "string",
"minLength": 1
},
"next_owner_ref": {
"anyOf": [
{
"type": "string",
"minLength": 1
},
{
"type": "null"
}
]
},
"health_probe_ref": {
"type": "string",
"minLength": 1
}
},
"required": [
"component_id",
"host_ref",
"service_unit",
"runtime_root",
"bind_address",
"port",
"config_ref",
"credential_refs",
"owner_ref",
"next_owner_ref",
"health_probe_ref"
],
"additionalProperties": false
},
"minItems": 1,
"uniqueItems": true
},
"rollback": {
"type": "object",
"properties": {
"baseline_ref": {
"type": "string",
"minLength": 1
},
"version": {
"type": "string",
"minLength": 1
},
"artifact_sha256": {
"$ref": "common.schema.json#/$defs/sha256"
},
"restore_operation_ref": {
"type": "string",
"minLength": 1
},
"restore_evidence_ref": {
"anyOf": [
{
"$ref": "common.schema.json#/$defs/digest_ref"
},
{
"type": "null"
}
]
},
"data_compatibility": {
"enum": [
"VERIFIED",
"UNKNOWN",
"INCOMPATIBLE"
]
}
},
"required": [
"baseline_ref",
"version",
"artifact_sha256",
"restore_operation_ref",
"restore_evidence_ref",
"data_compatibility"
],
"additionalProperties": false
},
"operations": {
"type": "object",
"properties": {
"resolve_contract": {
"type": "string",
"minLength": 1
},
"preflight": {
"type": "string",
"minLength": 1
},
"build_test": {
"type": "string",
"minLength": 1
},
"evidence_closure": {
"type": "string",
"minLength": 1
},
"candidate_freeze": {
"type": "string",
"minLength": 1
},
"authorization_check": {
"type": "string",
"minLength": 1
},
"runtime_switch": {
"type": "string",
"minLength": 1
},
"verify": {
"type": "string",
"minLength": 1
},
"tag": {
"type": "string",
"minLength": 1
},
"baseline_registry": {
"type": "string",
"minLength": 1
},
"acceptance": {
"type": "string",
"minLength": 1
},
"close": {
"type": "string",
"minLength": 1
}
},
"required": [
"resolve_contract",
"preflight",
"build_test",
"evidence_closure",
"candidate_freeze",
"authorization_check",
"runtime_switch",
"verify",
"tag",
"baseline_registry",
"acceptance",
"close"
],
"additionalProperties": false
},
"risk_budget": {
"type": "object",
"properties": {
"max_cumulative_unavailability_seconds": {
"type": "integer",
"minimum": 0
},
"max_error_rate_delta": {
"type": "number",
"minimum": 0,
"maximum": 1
},
"observation_seconds": {
"type": "integer",
"minimum": 1
},
"policy_source_ref": {
"type": "string",
"minLength": 1
}
},
"required": [
"max_cumulative_unavailability_seconds",
"max_error_rate_delta",
"observation_seconds",
"policy_source_ref"
],
"additionalProperties": false
},
"acceptance": {
"type": "object",
"properties": {
"probe_refs": {
"type": "array",
"items": {
"type": "string",
"minLength": 1
},
"minItems": 1,
"uniqueItems": true
},
"required_receipt_kinds": {
"type": "array",
"items": {
"enum": [
"runtime_identity",
"business_acceptance",
"authorization",
"backup",
"rollback_readiness",
"tag",
"baseline",
"source_lineage"
]
},
"minItems": 8,
"uniqueItems": true
}
},
"required": [
"probe_refs",
"required_receipt_kinds"
],
"additionalProperties": false
},
"evidence": {
"type": "object",
"properties": {
"content_key": {
"anyOf": [
{
"$ref": "common.schema.json#/$defs/sha256"
},
{
"type": "null"
}
]
},
"test_report_ref": {
"anyOf": [
{
"$ref": "common.schema.json#/$defs/digest_ref"
},
{
"type": "null"
}
]
},
"review_report_refs": {
"type": "array",
"items": {
"$ref": "common.schema.json#/$defs/digest_ref"
},
"minItems": 0,
"uniqueItems": true
},
"closure_ref": {
"anyOf": [
{
"$ref": "common.schema.json#/$defs/digest_ref"
},
{
"type": "null"
}
]
}
},
"required": [
"content_key",
"test_report_ref",
"review_report_refs",
"closure_ref"
],
"additionalProperties": false
},
"workload": {
"type": "object",
"properties": {
"base_tree_oid": {
"anyOf": [
{
"$ref": "common.schema.json#/$defs/oid"
},
{
"type": "null"
}
]
},
"files_added": {
"anyOf": [
{
"type": "integer",
"minimum": 0
},
{
"type": "null"
}
]
},
"files_modified": {
"anyOf": [
{
"type": "integer",
"minimum": 0
},
{
"type": "null"
}
]
},
"files_deleted": {
"anyOf": [
{
"type": "integer",
"minimum": 0
},
{
"type": "null"
}
]
},
"lines_added": {
"anyOf": [
{
"type": "integer",
"minimum": 0
},
{
"type": "null"
}
]
},
"lines_deleted": {
"anyOf": [
{
"type": "integer",
"minimum": 0
},
{
"type": "null"
}
]
}
},
"required": [
"base_tree_oid",
"files_added",
"files_modified",
"files_deleted",
"lines_added",
"lines_deleted"
],
"additionalProperties": false
},
"extensions": {
"$ref": "common.schema.json#/$defs/extensions"
}
},
"required": [
"schema_version",
"kind",
"data_class",
"contract_id",
"contract_revision",
"mission_id",
"release_run_id",
"version",
"state",
"target_environment",
"policy_ref",
"catalog_ref",
"source",
"build",
"artifact",
"components",
"rollback",
"operations",
"risk_budget",
"acceptance",
"evidence",
"workload",
"extensions"
],
"additionalProperties": false,
"allOf": [
{
"if": {
"properties": {
"state": {
"enum": [
"RESOLVED",
"FROZEN"
]
}
},
"required": [
"state"
]
},
"then": {
"properties": {
"source": {
"properties": {
"commit_oid": {
"$ref": "common.schema.json#/$defs/oid"
},
"tree_oid": {
"$ref": "common.schema.json#/$defs/oid"
}
}
}
}
}
},
{
"if": {
"properties": {
"state": {
"const": "FROZEN"
}
},
"required": [
"state"
]
},
"then": {
"properties": {
"source": {
"properties": {
"governed_commit_oid": {
"$ref": "common.schema.json#/$defs/oid"
}
}
},
"artifact": {
"type": "object",
"properties": {
"kind": {
"const": "runtime"
},
"ref": {
"type": "string",
"minLength": 1
},
"sha256": {
"$ref": "common.schema.json#/$defs/sha256"
},
"size_bytes": {
"type": "integer",
"minimum": 1
},
"provenance_ref": {
"$ref": "common.schema.json#/$defs/digest_ref"
}
},
"required": [
"kind",
"ref",
"sha256",
"size_bytes",
"provenance_ref"
],
"additionalProperties": false
},
"evidence": {
"properties": {
"content_key": {
"$ref": "common.schema.json#/$defs/sha256"
},
"test_report_ref": {
"$ref": "common.schema.json#/$defs/digest_ref"
},
"closure_ref": {
"$ref": "common.schema.json#/$defs/digest_ref"
}
}
},
"rollback": {
"properties": {
"restore_evidence_ref": {
"$ref": "common.schema.json#/$defs/digest_ref"
},
"data_compatibility": {
"const": "VERIFIED"
}
}
}
}
}
}
]
}
File diff suppressed because it is too large Load Diff
+125
View File
@@ -0,0 +1,125 @@
{
"schema": "aios.component-build.v1",
"components": [
{
"component_id": "formal-agent-task",
"roots": [
"active-runtime/app/aios_agent_task_protocol.py",
"active-runtime/app/aios_agent_task_api_lineage_supersession.py",
"active-runtime/app/aios_task_parameterized_recovery.py"
],
"extra_files": [
"active-runtime/migrations/005_task_lineage_supersession.sql",
"active-runtime/migrations/006_task_lineage_supersession_initial_projection.sql"
],
"entrypoint": "aios_agent_task_protocol:TaskStore",
"dependency_contract_version": "aios.agent-task.capability-contract.v1",
"config": {
"listen_host": "127.0.0.1",
"listen_port": 18940,
"writes_enabled": false,
"credential_refs": [],
"feature_flags": {
"auto_claim": false
}
}
},
{
"component_id": "bridge",
"roots": [
"active-runtime/app/aios_chatgpt_bridge_v2_server.py",
"active-runtime/app/agent_context_api.py",
"active-runtime/app/agent_observability_api.py",
"active-runtime/app/agent_event_api.py",
"active-runtime/app/aios_chatgpt_actions_api.py",
"active-runtime/app/aios_gitops_model_api.py",
"active-runtime/app/aios_model_api_staging.py",
"active-runtime/app/aios_control_plane_runtime_status.py",
"active-runtime/app/governed_unattended_release.py",
"active-runtime/app/silent_release_execution.py",
"active-runtime/app/release_standing_authority.py",
"active-runtime/app/release_host_authority.py",
"active-runtime/app/release_campaign_authorization.py",
"active-runtime/app/owner_authorization_broker.py",
"active-runtime/app/task_center_api.py"
],
"extra_files": [
"deploy/systemd/aios-chatgpt-bridge-v2-staging.service",
"adapters/gitops/aios_gitops_model_api.py",
"contracts/release/v1/common.schema.json",
"contracts/release/v1/release-contract.schema.json",
"contracts/release/v1/authorization-bundle.schema.json",
"contracts/release/v1/release-run-event.schema.json",
"deploy/aios-v2/release-policy.v1.json",
"overlays/user-control-center/portal/gitops-dashboard/app.js",
"overlays/user-control-center/portal/gitops-dashboard/index.html",
"overlays/user-control-center/portal/gitops-dashboard/event_recovery.js",
"overlays/user-control-center/portal/gitops-dashboard/styles.css"
],
"entrypoint": "aios_chatgpt_bridge_v2_server:main",
"dependency_contract_version": "aios.chatgpt-gitops-bridge.v2",
"config": {
"listen_host": "127.0.0.1",
"listen_port": 18892,
"writes_enabled": false,
"credential_refs": [
"gitea-governance-executor-token"
],
"feature_flags": {
"auto_claim": false
}
}
},
{
"component_id": "task-plane",
"roots": [
"active-runtime/app/task_plane/api.py",
"active-runtime/app/task_plane/c6_composition.py",
"active-runtime/app/task_plane/host_patch_executor.py",
"active-runtime/app/task_plane/host_receipt_readers.py",
"active-runtime/app/task_plane/authorization_composition.py",
"active-runtime/app/task_plane/patch_contract.py",
"active-runtime/app/task_plane/evidence_verifier.py",
"active-runtime/app/task_plane/decision_policy.py"
],
"extra_files": [
"scripts/autonomous-release/c6_host_lab.py",
"deploy/aios-v2/c6-host-lab.requirements.lock",
"deploy/aios-v2/c6-host-lab.config.json",
"deploy/aios-v2/c6-host-lab.service"
],
"entrypoint": "app.task_plane.api:run_server",
"dependency_contract_version": "aios.task-plane.v3",
"config": {
"listen_host": "127.0.0.1",
"listen_port": 18940,
"writes_enabled": false,
"credential_refs": [],
"feature_flags": {
"auto_claim": false,
"aro": false
}
}
},
{
"component_id": "health",
"roots": [
"active-runtime/app/v2r2c2_health_service.py"
],
"extra_files": [
"deploy/aios-v2/systemd/aios-v2r2c2-production.service"
],
"entrypoint": "v2r1c6_privacy_tools:main",
"dependency_contract_version": "aios.component-manifest.v1",
"config": {
"listen_host": "127.0.0.1",
"listen_port": 18928,
"writes_enabled": false,
"credential_refs": [],
"feature_flags": {
"runtime_verification": true
}
}
}
]
}
+17
View File
@@ -0,0 +1,17 @@
{
"schema": "aios.c6-host-lab-config.v1",
"activation": "DIAGNOSTIC_ONLY",
"deployable": false,
"production_deployed": false,
"service_type": "simple",
"network_listeners": true,
"listen_mode": "DYNAMIC_LOOPBACK_PORT",
"allowed_lab_root_regex": "^/var/tmp/aios-v2r2c6-lab-[0-9]{8}([-A-Za-z0-9_.]+)?$",
"required_safety_properties": [
"NoNewPrivileges",
"ProtectSystem",
"ProtectHome",
"ReadOnlyPaths",
"ReadWritePaths"
]
}
@@ -0,0 +1,28 @@
# Linux x86_64 CPython 3.12 isolated C6 validation environment; no production readiness claim.
annotated-doc==0.0.5 --hash=sha256:117bac03a25ede5df5440e855b32d556049ca169ead221505badf432fed4b101
annotated-types==0.8.0 --hash=sha256:f072f4d804ea359e4eaf198b1af7a8b0943881a87f31bb764f8bf219bb9419e0
anyio==4.15.1 --hash=sha256:6152fdbbf9a77fdec97731721bebf7c4c44f7c29b424b0065826173efc7ed101
attrs==26.1.0 --hash=sha256:c647aa4a12dfbad9333ca4e71fe62ddc36f4e63b2d260a37a8b83d2f043ac309
cffi==2.1.1 --hash=sha256:c1453022f490d2459a11819d83ad1d586e9ff65a12ac3e705ffebd46d3685dcf
click==8.5.0 --hash=sha256:255bc9599cf7748b4b1a446ccc735421bd08a2ae529a8b88597d3de5664ee360
cryptography==50.0.1 --hash=sha256:51afcfceb15597cf2635068e4ac9a56b2abde622edde17f37d85fd7b5306497a
fastapi==0.141.1 --hash=sha256:bfb91aa2d334c61cb35ba9a116fc123b3d3df31640b801cf57a7a78ec3f603b3
h11==0.16.0 --hash=sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86
idna==3.20 --hash=sha256:ab7ae7122974553370f0bdb919e1a960b2cd1bc1ef0276416d896db81c14582c
iniconfig==2.3.0 --hash=sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12
jsonschema==4.26.0 --hash=sha256:d489f15263b8d200f8387e64b4c3a75f06629559fb73deb8fdfb525f2dab50ce
jsonschema-specifications==2025.9.1 --hash=sha256:98802fee3a11ee76ecaca44429fda8a41bff98b00a0f2838151b113f210cc6fe
packaging==26.3 --hash=sha256:d7193f7c8e4e93f444fde0262bf90af30e16fa0ad0ad44cb553c87339b23cd1c
pluggy==1.6.0 --hash=sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746
pycparser==3.0 --hash=sha256:b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992
pydantic==2.13.5 --hash=sha256:346a034f080da3755d8e9cb5e00e8b07de1d39e4f6e2c87d8ab7cafa0b269a73
pydantic_core==2.46.5 --hash=sha256:0fc5be0abd4a407e200d844b404e33639a554e7bd0d448e7b9ae181be4789ac2
Pygments==2.21.0 --hash=sha256:2363c69b61c4a97c838da3b130dcd6468f4848992b21a82f2a63ec34377137d9
pytest==9.1.1 --hash=sha256:37a86b45efb9a47a61a36449063e8e18d0cab3161329fc099eb21783169c4f0c
PyYAML==6.0.3 --hash=sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc
referencing==0.37.0 --hash=sha256:381329a9f99628c9069361716891d34ad94af76e461dcb0335825aecc7692231
rpds-py==2026.6.3 --hash=sha256:ecabd69db66de867690f9797f2f8fa27ba501bbc24540cbdbdc649cd15888ba6
starlette==1.7.0 --hash=sha256:67f8e99895493dd2911a03f11314af6ceebeae4e704bb9f43dfc6a9db151c93e
typing-inspection==0.4.4 --hash=sha256:65b8397ba37ccbce054456aaccddfc91e6e3083c92824df348d96ca832f3f147
typing_extensions==4.16.0 --hash=sha256:481caa481374e813c1b176ada14e97f1f67a4539ce9cfeb3f350d78d6370c2e8
uvicorn==0.54.0 --hash=sha256:505bdb0f318731d45f1f712071fc781a8981f6847a31c902c9f5e652d4f67faf
+18
View File
@@ -0,0 +1,18 @@
[Unit]
Description=AIOS C6 isolated provisional artifact diagnostic
After=network-online.target
[Service]
Type=simple
# Render this reference with scripts/autonomous-release/c6_host_lab.py. The
# generated unit contains the pinned artifact, receipt, runner and contract paths.
NoNewPrivileges=true
# The authorized isolated root is under /var/tmp, so systemd PrivateTmp would
# hide the only permitted runtime path from this service.
PrivateTmp=false
ProtectSystem=strict
ProtectHome=true
RestrictAddressFamilies=AF_UNIX AF_INET
IPAddressDeny=any
IPAddressAllow=127.0.0.1/32
UMask=0077
+111
View File
@@ -0,0 +1,111 @@
{
"schema_version": "1.0.0",
"kind": "ReleasePolicy",
"policy_id": "AIOS-RELEASE-POLICY-V1",
"installation_state": "NOT_INSTALLED",
"entrypoint": "release promote VERSION",
"read_only_prep_alias": "release prep VERSION",
"stage_order": [
"resolve_contract",
"preflight",
"build_test",
"evidence_closure",
"candidate_freeze",
"authorization_check",
"runtime_switch",
"verify",
"tag",
"baseline_registry",
"acceptance",
"close"
],
"root_identity": {
"reuse_existing_mission": true,
"reuse_existing_release_run": true,
"recovery_creates_attempt_not_new_goal": true
},
"human_policy": {
"additional_interactions_target": 0,
"owner_required_reasons": [
"GOAL_CHANGE",
"TRUST_BOUNDARY_CHANGE",
"IRREVERSIBLE_OPERATION",
"BUSINESS_RISK_BUDGET_EXCEEDED"
],
"signer_unavailable_state": "WAITING_AUTHORITY",
"reviewer_unavailable_state": "WAITING_CAPACITY",
"initial_request_target": 1,
"selection_submission_target": 0,
"receipt_or_notification_is_human_interaction": false
},
"timing_targets_seconds": {
"preflight": 300,
"ready_candidate_to_acceptance_ordinary": 3600,
"ready_candidate_to_acceptance_hotfix": 900
},
"timing_measurement": {
"always_report_request_to_acceptance": true,
"hide_preparation_time": false,
"exclude_failures_from_reporting": false,
"initial_calibration_runs": 5,
"calibration_does_not_establish_p95": true
},
"retry": {
"fast_attempts": 3,
"backoff_seconds": [
5,
15
],
"maximum_wait_interval_seconds": 300,
"unknown_effect_requires_reconcile": true,
"no_identical_blocker_without_new_fact": true,
"fast_attempts_includes_initial": true,
"dependency_probe_initial_seconds": 60,
"exhaustion_action": "ROUTE_TO_ENGINEERING_PRODUCER_WITHOUT_NEW_OWNER_PROMPT"
},
"infrastructure": {
"reuse_existing_signer": true,
"reuse_existing_outbox": true,
"new_mandatory_daemons": [],
"new_mandatory_databases": [],
"new_mandatory_message_brokers": []
},
"runtime_budget_source": "existing_authoritative_environment_policy",
"missing_budget_behavior": "BLOCKED_CONFIGURATION_GAP",
"trial_mode": "SHADOW_THEN_STAGING_THEN_SMALL_REVERSIBLE_PRODUCTION",
"live_fixture_policy": "DENY_FIXTURE_AND_UNRESOLVED_REFERENCES",
"rollback": {
"reconcile_before_retry": true,
"retain_previous_artifact": true,
"bind_current_accepted_baseline": true,
"close_only_after_compensation_or_failure_receipt": true
},
"revision": "1.1.0",
"stage_timing_targets_seconds": {
"request_to_start": 30,
"preflight_aspirational": 120,
"authorization_check": 10,
"runtime_switch": 300,
"verify": 300
},
"stage_target_status": "PROPOSED_NOT_MEASURED_DO_NOT_SHORTEN_REQUIRED_OBSERVATION",
"failure_routing": {
"F0": "EXISTING_RETRY_RENEW_RECONCILE",
"F1": "ENGINEERING_OR_AUTHORITY_PRODUCER",
"F2": "VERIFIED_OWNER_BOUNDARY_ONLY",
"unmapped": "F1_STOP_RELATED_EFFECTS_AND_CLASSIFY",
"mixed_boundary_precedence": true
},
"continuation": {
"reuse_existing_checkpoint_and_saga": true,
"checkpoint_is_authority": false,
"revalidate_authority_before_effect": true,
"preserve_root_clock": true,
"unknown_effect_before_retry": "RECONCILE",
"goal_valid_until": [
"RELEASE_CLOSED",
"REVOKED",
"CANCELLED"
]
}
}
@@ -0,0 +1,73 @@
# C5 控制面闭合报告
状态:本地工程闭合候选已形成;live control plane 尚未闭合。本文是交付准备记录,不是生产发布、activation、Task admission 或 RC 成功证明。
## 已闭合的工程边界
架构决定和实施计划将 unattended runner 定义为编排客户端。它不拥有 Owner authority、epoch issuer、Task DB、Git writer、approval verifier 或 production executor。现有组件仍是唯一权威:`TaskAdmissionBootstrapService`/`AgentTaskAdmissionPort` 负责 envelope、grant 和 Task admission;既有 lease/principal/writer chain 负责写入;trusted builder 和 `SilentReleaseActionService` 负责 candidate/provider evidence;`ReleaseSagaStore`、`ReleaseOrchestrator` 和 `ReleaseRecoveryWorker` 负责部署、回滚与未知副作用恢复。旧的 `release_state_machine.py` 不应再作为第二状态权威。
本地 runner `active-runtime/app/governed_unattended_release.py` 已提供以下受限组合接口:
```text
GovernedUnattendedReleaseRunner.intake(intent)
GovernedUnattendedReleaseRunner.admit(intent, authorization)
GovernedUnattendedReleaseRunner.publish(intent, admission_receipt)
GovernedUnattendedReleaseRunner.postpublication_rc(event_id, source_commit, version, source_readback)
GovernedUnattendedReleaseRunner.deploy(candidate_id)
```
`SourcePublicationIntent` 对 repository、source/tree/base OID、精确 target branch、generation、execution digest、file allowlist、capability set 和 risk 做不可变规范化,并派生稳定 `task_id` 与 idempotency key。`admit` 在 envelope 已创建后遇到超时、连接断开或 5xx 时先 reconcile,不把未知结果当作未应用,也不重复提交;禁止错误不会重试。`publish` 要求同一 Task 的 `task_lease`、`task_principal`、`writer_lease`、authenticated commit receipt、base/branch/generation/execution digest/source/tree 全部绑定。缺一即阻断。
本地模拟器 `scripts/simulate_c5_unattended_release.py` 从 Git object 读取 source/base/tree/diff/archive,运行候选 C5 contract tests,使用临时 Owner key 和真实 admission/bootstrap/TaskStore 实现完成隔离签名与 admission,再写入现有模型的 candidate/manifest/RC JSON。输出明确标记 `SIMULATION`、`official_release=false`、`authority_source_commit=null`、`acceptance=PENDING_AUTHENTICATED_PRODUCTION_PROVIDER_EVIDENCE`、`approval=NOT_REQUESTED`、`deployment=NOT_ATTEMPTED`。这条链路可证明结构、签名验证、隔离 TaskStore、幂等和非权威 artifact 写出;不能证明 live authority、live publication、trusted production provider、release approval、runtime health 或 deployment。
`governed_release_evidence_producer.py` 的 `produce_evidence_closure(...)` 可生成现有 RC evidence closure,但要求 tests、canary、Gitea、rollback、secret-scan 每一类均有 trusted-provider receipt,并校验 artifact/manifest/package closure/source/tree。相同 closure 幂等,内容变化为冲突。该闭合器不得被调用来伪造 provider pass。
现有 release service 的正式接口仍应按以下顺序复用:
```text
SilentReleaseActionService.create_release_intent(payload)
-> get_release_candidate(candidate_id)
-> get_release_plan(candidate_id)
-> get_acceptance(candidate_id)
-> verify_release_acceptance(candidate_id)
-> get_approval_envelope(candidate_id)
-> submit_owner_approval(signed_payload)
-> start_deployment(candidate_id)
```
`create_release_intent` 只能接收 event/source/version,并从 authenticated source admission resolver 得到版本与 admission ID;不能接收 caller evidence。`submit_owner_approval` 必须经既有 signed release-plan verifier 和 durable approval store。`start_deployment` 会重新加载 durable approval,调用既有 saga/orchestrator;caller 传入的 approval 不作为授权来源。
## 当前 live 未闭合
以下条件仍未证实,不能写成 PASS 或 COMPLETE:
- `authority_source_commit` 仍为 `null`;没有正式 live source publication receipt、authority Git readback、TaskLease/TaskPrincipal/WriterLease/commit closure。
- 没有 live C5 exact source-only policy 的 Owner-signed activation、policy digest、issuer、revocation source 和 fresh epoch。不能用本地 policy/request hash、旧 activation binding、simulation epoch 或借用 credential 替代。
- 没有 live registered machine caller 的可用 admission route;当前 runner 不能自行成为 authority issuer 或把 `engineering` 风险提升为全局允许值。
- 没有真实 trusted builder/provider resolver 的完整绑定,尤其是 authenticated tests、canary、Gitea、rollback、secret-scan receipts;本地 candidate artifact 不是生产 evidence closure。
- 没有 pre-authorized Owner approval broker 为精确 candidate/release plan 签名。source-only authorization 不等于 release approval。
- 没有完成 live `SilentReleaseActionService` 的 candidate、acceptance、release plan、saga readback,也没有 deployment composition、runtime health、rollback 和 post-tag/finalization readback。
- C6 已冻结;C5 自动化支持若进入后续发布,必须重新计算 source/tree/file binding、execution plan 和 signed authority,不能把支持代码自动并入既有 candidate。
## 最终本地验证
2026-09-26,在 Linux/Python 3.12.3 的隔离目录中完成 **184 passed** 定向回归(5.53 秒)。范围包含 epoch v1/v2、source policy、runner、真实 release service/Owner signature 集成、既有 admission/bootstrap/recovery、production contract 和 provider evidence producer。没有重跑历史全量业务测试,也没有把旧的 598 passed 计为本次新增代码的结果。
新增集成覆盖实际 signed builder、signed Owner approval、source substitution 拒绝、无 verifier 拒绝及 durable attestation 篡改拒绝。公开 acceptance 接口包含脱敏字段;`verify_release_acceptance` 在现有 service 内加载原始 durable 数据重新验签,保持公开脱敏规则,向 runner 返回验证摘要。
最终 `demo-run-4` 从不可变候选 `536a2af0f07962495c88526061ce43fb27994c15` 的 archive 执行 C5 capability N01/N02/N03,经过真实 SSHSIG 验签、真实隔离 Task admission、幂等重放和 scope/revocation 负例,成功写出 `V2R2C5_RC.simulation.json`。该 candidate 保持 `BUILDING`;正式 source OID 为空,provider acceptance 待补、approval 未请求、deployment 未执行。
可复核证据位于集成工作树相邻的 `../evidence/final-targeted-tests.xml`、`../evidence/demo-run-4/V2R2C5_RC.simulation.json` 和 `../evidence/demo-run-4/SHA256_MANIFEST.json`。这些结果证明隔离工程闭环,不证明 live 发布闭环。
## 可恢复下一步
1. 在现有 governed Task → TaskLease → TaskPrincipal → WriterLease → Governed Commit 链上发布 runner/epoch compatibility/exact policy guard 支持,并 read back commit/tree/receipt。
2. 由已有 Owner authority 通过控制面激活精确 C5 source-only policy,记录 signed activation、policy digest、issuer、expiry、revocation source;随后请求 fresh epoch。禁止直接写受信任目录或复用旧 epoch。
3. 用真实 registered caller 执行 admission;对 envelope、admission receipt、Task 状态和 authority OID 做 readback。任何 unknown effect 先 reconcile。
4. 仅在 publication receipt、lease chain、commit receipt 和 authority readback 完整后,调用 `create_release_intent`,再由真实 provider 生成并验证 candidate、acceptance、plan、saga 和 RC evidence。
5. 由预授权 Owner approval broker 对精确 release plan 签名,经 `submit_owner_approval` 验证并 durable 存储。缺 broker 时保持阻断,不降低 risk、不 mint client signature。
6. 若另有明确生产授权,再调用 `start_deployment`;依赖既有 pre-switch/canary、health、rollback、tag/finalization reconciliation。否则将结果保留在已签名、可重放的 RC/approval-ready evidence,而不触碰生产。
## 运行时假设
真实组合依赖 Linux/POSIX `fcntl` locks、`fsync`/atomic rename、受限 symlink/open、Git authoritative bare repo、Gitea/ref readback、systemd service identity/restart、health endpoint、Ed25519 verification,以及 KMS/HSM/secret-manager reference。`execution_enabled=false`/shadow mode 是默认安全状态;生产执行缺少 approval verifier、trusted builder、builder attestation verifier、orchestrator、pre-switch validator 或 trusted policy 时必须 fail closed。
@@ -0,0 +1,52 @@
# C5 release automation implementation and rollout
The engineering task implements a repeatable orchestration path and isolated executable proof. It does not treat local artifacts as an activated production capability. V2R2C6 is frozen.
## Parallel implementation ownership
1. Epoch compatibility: admission bootstrap and signed-epoch provider; v1 regression, original-byte v2 verification, tamper/schema/revocation tests.
2. Exact policy lifecycle: existing structured Owner issuer plus an exact source-publication guard; TTL, current trust, revocation, scope and configured-risk checks.
3. Thin runner: generated immutable intake, admission/reconciliation, publication proof gate, existing trusted candidate service and signed approval/deployment entry points.
4. Integration: isolated executable demo, targeted contract tests, architecture/gap reports and immutable local commit/archive evidence.
Each implementation lane uses a separate Git branch/worktree. Integration uses ordinary local commits without force-push, main mutation, TaskDB mutation or production writes.
## Minimum executable closure
Run the simulation CLI with a Git repository, committed source/base and a new output directory. It must derive the file list, tree and source archive from Git objects; run the selected checks; issue a fresh test-only Owner policy/epoch using ephemeral keys; verify them with the actual registry verifier and dual-schema provider; submit an envelope through the actual admission service against an isolated Task transport; and write an existing-model RC artifact with all evidence hashes.
The output must say `SIMULATION`, `official_release=false`, and retain `authority_source_commit=null`. It must not invent live provider receipts or set `READY_FOR_APPROVAL` without the existing acceptance path. Ephemeral private keys are confined to a temporary directory and excluded from exported evidence. A signed test receipt authenticates only its stated isolated environment.
From this implementation checkout on Linux/POSIX, with Git, OpenSSH, pytest and cryptography available:
```bash
python3 scripts/simulate_c5_unattended_release.py \
--repository /path/to/local/candidate/repository \
--source 536a2af0f07962495c88526061ce43fb27994c15 \
--base e5f8209f57b43fc39e55c4555776e6362ce326eb \
--output /path/to/new/isolated/evidence-directory
```
The command creates its own isolated Task store and ephemeral signing domain. It has no live transport or deployment adapter. No manually assembled JSON, existing Owner credential, service restart or production endpoint is needed.
## Required checks
- v1/v2 consumer compatibility, original bytes, issuer/key/namespace/digest, unknown schema, invalid fields, expiry and revocation.
- Existing medium policy still denies engineering; exact source policy denies any changed branch/base/source/tree/files/caps/execution binding.
- Missing verifier and missing/failing revocation lookup deny; admission replay rechecks authorization.
- Stable intake identity, bounded retry, no retry for policy rejection, unknown side-effect reconciliation and conflicting receipt rejection.
- Missing TaskLease/principal/WriterLease/commit/readback cannot produce a live RC.
- Existing signed approval, provider evidence, saga integrity, deployment verification, rollback and post-tag reconciliation contracts.
- C5 capability negatives N01/N02/N03 and affected regression tests only; historical full-suite counts are not reused as evidence for new changes.
## Governed rollout after local verification
1. Publish the support commit through the existing Task -> TaskLease -> TaskPrincipal -> WriterLease -> Governed Commit chain. Read back its OID/tree/receipt.
2. Governed-install the dual-schema consumer and exact source-only policy guard on the existing control-plane service, preserving the old artifact for rollback.
3. Activate the exact policy and dedicated machine caller using the existing Owner authority. Record signed activation, policy digest, issuer and revocation source. This is a provisioning step, not an implicit authority granted by the runner.
4. Request a fresh epoch and execute C5 admission/publication. Rebind evidence to the authority-readback OID; it may differ from the local candidate.
5. Run actual build/test/security/canary/rollback providers and their authenticated resolver. Invoke `create_release_intent` against the authority source; read back candidate, acceptance, plan and saga.
6. Have the pre-authorized Owner broker sign the exact release plan, and submit it through `submit_owner_approval`. Source-only authorization is insufficient here.
7. Call the existing deployment controller with approved durable state. Canary/pre-switch, health, promotion, registry/baseline finalization and rollback follow the existing saga. Verify runtime identity and final consistency.
Missing live authority or provider composition is a durable blocker, never a reason to lower risk, bypass G0, mint signatures in the client or call production restart directly.
@@ -0,0 +1,60 @@
# C5 governed unattended release: architecture decision
Status: selected for isolated implementation; live activation is not implied.
Task: TASK-AIOS-C5-GOVERNED-UNATTENDED-RELEASE-PLATFORM-DESIGN-AND-IMPLEMENTATION-20260926-R1
## Decision
Select **B, additive v1/v2 epoch consumption**. Verify the producer's original canonical signed bytes and retain the actual schema, issuer, key, policy digest and revocation semantics. Project common typed fields only after verification. A v2 payload is never relabelled v1 and its signature is never copied onto another payload.
The producer's `policy_digest` hashes the Owner policy. It is not interchangeable with the default Task Admission policy digest. The existing policy factory propagates this digest; the compatibility change preserves that binding. A v2 `canonical_payload_digest` covers the payload before that field is added, while the signature authenticates the complete original canonical payload.
Option A would either be the same dual-schema verification hidden behind a misleading v1 label, or require a new signature. Option C would change more producers and consumers without resolving the immediate authority/composition gap. B preserves v1 operation and has the smallest rollback surface.
## One control plane
The unattended runner is an orchestration client. It does not become an authority issuer, Task database writer, Git writer, approval verifier, release state machine, or production executor.
| Responsibility | Existing authority/component |
|---|---|
| Owner signatures and narrowing | `LocalOwnerAuthority`, registered Owner key and configured Owner policy |
| Epoch verification | `SignedEpochDirectoryProvider`, `RegistryOwnerApprovalVerifier` |
| Exact source-only policy | Structured Owner-signed policy, verified request guard |
| Envelope, grant and Task admission | `TaskAdmissionBootstrapService`, `AgentTaskAdmissionPort` |
| Claim, principal and WriterLease | Existing Task/lease/governed writer chain |
| Source commit proof | Governed commit receipt plus authority Git readback |
| Build, provider evidence and candidate | Trusted builder, provider resolver, `SilentReleaseActionService` |
| Approval | Existing signed release-plan approval verifier and durable approval store |
| Deployment and recovery | `ReleaseSagaStore`, `ReleaseOrchestrator`, `ReleaseRecoveryWorker` |
The older linear `release_state_machine.py` is not added as another release state authority. The runner consumes durable state from the existing stores. Recovery of an unknown mutation reconciles that state before another write.
## Least privilege and unattended authority
The source-only policy binds repository, source commit/tree, base commit, exact target branch, generation, execution digest, file set, capability set, engineering risk, expiry and fresh identity. It cannot authorize main writes, tags, deployment, baseline mutation or historical Task mutation. The global admission ceiling stays `medium`; an exact verified request may use its separately signed engineering policy. A different request must be denied before envelope creation.
Recognizing the `engineering` rank in the issuer does not raise any configured ceiling. An existing `medium` Owner policy still rejects engineering issuance. The structured policy issuer must narrow its configured Owner policy; it cannot install or activate a policy for itself.
Each authorization rechecks signature, current key trust, expiry, revocation and scope. No automatic extension of expired epochs is allowed. A controller requests a fresh epoch under an already activated Owner policy. Source-only authority cannot be exchanged for release approval. Automatic release approval requires a separately delegated Owner broker to sign the exact candidate and release plan; missing or revoked delegation stops the workflow.
“No interaction” is an execution property after governed provisioning. It is not permission to create a trust root or grant the controller new powers. Current production has not been provisioned for this new composition.
## Retry and recovery
Task and event identities derive from immutable input. Retries retain them. Policy rejection is terminal until policy/input changes. A timeout is not proof that a write failed: use the admission receipt/official Task readback, publication receipt and authority OID, or existing release saga/operation journal to reconcile. Unknown or conflicting effects remain blocked. Transport calls require bounded timeouts; a controller deadline alone cannot cancel a remote mutation.
Build and acceptance evidence are obtained through trusted providers, not caller-supplied pass flags. A local simulation artifact is explicitly marked non-authoritative. Its source OID, archive hash and cryptographic checks can be real without proving live publication, runtime acceptance or deployment.
## Risks and rollout
1. Schema acceptance alone does not activate a usable policy. Roll out the consumer and exact policy guard together through governed control-plane publication.
2. The existing deployed Owner policy does not authorize the C5 scope, and the current caller has no usable authenticated activation/epoch route. Provision the narrowly scoped service identity and policy through the existing governance plane. Do not use direct root file writes as a substitute.
3. Signed source policy and epoch must agree on the Owner policy digest. Mixing epochs, digests or request identities fails closed.
4. The local source candidate `536a2af0f07962495c88526061ce43fb27994c15` is preserved. Automation support changes are separate commits. Any decision to include them in the release must regenerate the governed execution plan and exact file/source bindings.
5. The existing deployment engine contains canary/pre-switch, runtime verification, rollback and post-tag reconciliation logic. Tests establish adapter contracts; they do not prove current production composition or health.
## Rollback
Disable the new runner, revoke its source-only policy/epoch and stop new admissions. Preserve journals, receipts, immutable Tasks and candidate records. Revert the consumer/support module deployment to the prior governed control-plane artifact; v1 remains supported throughout migration. Never rewrite already issued signed payloads or historical Task state.
Production deployment rollback remains the existing signed plan's rollback operation and release saga. A post-tag/finalization uncertainty requires reconciliation; it must not be hidden by an automatic tag rewrite. C4 stays the production baseline until a separately approved C5 deployment completes runtime verification.
@@ -0,0 +1,57 @@
# C6 主机接入设计与交接边界
Agent-B 在 `e87c70778aa0911436cac5b59f1aa11080c5ba77` 集成基线上补主机适配。C5 生产冻结与发布由 Agent-A 负责;本补丁不反向合入 C5。持续业务授权和短期执行凭证分离,原 Saga、Mission/Run、动作身份和验签合同保留。
```mermaid
flowchart TD
A[版本级业务授权与实时撤销状态] --> B[签名 current-record reader]
S[Task / scope / lineage 权威记录] --> B
B --> C[既有 StandingReleaseAuthorizationBroker]
K[目的受限的已登记 release signer] --> C
C --> D[短期精确执行凭证]
D --> E[既有 Controller / Saga]
E --> F[既有 TaskPlane 自动领取与租约]
F --> G[精确 RootAction 持久 intent / effect]
G --> H[正式 Bridge / WriterLease / 外部 CAS fence]
H --> I[真实效果读回]
V[独立 verifier 签名回执] --> J[既有三层 closure reducer]
P[独立业务 acceptor 签名回执] --> J
I --> J
J --> A
```
## 信任和执行接口
1. `release_host_authority.VerifiedCurrentRecordReader` 每次生成随机 challenge,校验响应目的、查询摘要、父授权摘要、时间窗口、已登记密钥与原始 Ed25519 签名。主机配置固定 purpose→key;响应不能带入新信任根。权威端必须每次读真实当前状态后签 challenge,不能给旧 ACTIVE snapshot 重新加时间。
2. `HostReleaseAuthorityReaders` 将验签后的精确记录转换为既有 StandingReleaseAuthorization / ReleaseExecutionScope。执行范围绑定同轮父授权记录。broker 在签发前后和 effect 前重读,不用旧签名掩盖撤销、完成、关闭、根或范围变更。
3. `RegisteredReleaseSignerCommand` 只向已登记 release approval signer 提交既有 `SignedReleasePlanApproval.canonical_bytes()`;校验 key、算法、消息摘要和 raw Ed25519 签名后返回原合同。SSHSIG armor 不是 raw Ed25519 签名,禁止解码冒充。发布 broker 不代替 TaskPrincipal、WriterLease、promotion/source authority。
4. `task_plane.host_patch_executor` 连接现有 C6 pilot seam 与外部持久事务所有者。解析真实 ExecutablePatchV1;claim 不是 WriterLease。实际提交必须在外部事务线性化点检查当前授权代次、租约/fence 和 expected OID CAS。内存 callback、本地进程锁或测试事务不能证明生产跨主机原子性。
5. `HostOperationReceiptReaders` 读取独立 verifier 与 business acceptor 的签名挑战响应。主机固定角色身份、不同 key 与 acceptance revision;执行者不能验证/验收自己。输入不得携带 `authenticated` 字段,只有验签和角色绑定成功才在内部观测中派生该值。原 closure reducer 再核对 effect receipt、结果摘要、verification receipt、时效和 acceptance revision。
RootAction 的 host binding 固定授权引用、授权代次、公钥指纹、Task/Job/claim/fence、补丁、计划与实际提交内容。短期观测的 challenge 和有效窗口单独验证,不成为新动作身份。普通旧式 `run_once` 不得领取这些已绑定 C6 动作;C6 的恢复先枚举既有账本未闭合 intent,再做领取发现,避免任务退出候选列表后遗留未知效果。恢复入口仅允许精确对账,不能重新 acquire/commit。
动作提交成功与三层验收闭合分别记录:尚缺独立验证或业务验收的已提交动作仍属于重启后的待闭合集合。闭合记录必须从受信回读重新核算精确 receipt 与摘要链,并在原 effect 行保存验收证据摘要;调用方自报成功不能移除待闭合动作。该记录用于恢复调度,不替代原始独立服务回执。
## 验收回读协议
`compose_host_release_controller` 提供实际组合入口,分别注入 current-record 与 approval 的目的受限 keyring,并配置 key_id→raw 公钥 SHA256;不同别名不能绕过目的隔离。组合冷启动不读取短期服务,实际签发/执行时才读并核验。内置 `BoundedCommandTransport` 只支持 POSIX 主机,以进程组、总时间和总输出上限管理提供方及子进程;Windows 在启动子进程前拒绝此 transport。字节协议 reader、签名适配及控制器仍可在 Windows 做隔离验证。
purpose 分别为 `operation-verification`、`operation-acceptance`。query 包含完整 OperationBinding、binding_digest、expected_identity、expected_key_id、expected_key_fingerprint;acceptance 另含 expected_acceptance_revision。响应使用 current-record v1 envelope;record 是原 operation-verification/acceptance-observation v1 的 schema 与对应 observation,省略 authenticated。acceptance 的 expected 身份和 revision 来自主机配置,不能由外部 receipt 自报。两角色的已登记身份、key_id 和 raw Ed25519 公钥 SHA256 均须不同,不能用同一公钥的不同别名绕过独立性检查;keyring 轮换必须显式更新对应主机策略,不能静默换钥。
公开业务检查、部署/恢复检查由实际独立服务生成。适配器不生成成功回执、不写平台 registry,也不把聊天角色当正式 worker 身份。缺服务、密钥、授权或当前记录时停止相关 effect,仍可继续诊断与其他独立工程。
## 安装和证据
`c6_host_lab.py` 验证 immutable artifact、sidecar、组件配置与 unit 字节,拒绝危险归档路径、链接、占位符及摘要漂移。实验服务使用独立目录、非 root UID 和 loopback 端点;必须分别记录进程启动/重启、真实组件加载和权限拒绝,不能把静态 systemd verify 或自定义 health JSON 当生产验收。
隔离测试使用临时密钥、仓库和事务,证据仅证明实现行为。正式主机仍需已登记 authority/scope/signer、patch worker 与 transaction owner、独立 verifier/acceptor。最终版本必须重新绑定 Agent-A 真实验收的 C5 source/tree/artifact/config/rollback。所有 provisional 制品维持 deployable=false;签名、candidate 或 source 变更必须重建摘要,不能复用旧批准。
存储迁移只在既有 root-action effect 账本增加可空字段和唯一索引,保留旧状态与未知副作用语义;旧 DB 重开回归独立验证。生产恢复到 C5 还必须关闭 C6 执行调度并使旧消费者不能领取遗留 C6 动作,未知效果保持隔离对账。不能仅切旧代码后继续消费新账本,也不能删除新增列、回灌旧 DB 或丢弃新任务来伪造恢复成功;这属于最终 C5 恢复合同尚需实测的门槛。
当前 Public Bridge 的 WriterLease 返回合同若没有真实 lease ID 和 fence,精确执行入口拒绝提交;不能由客户端自行生成这些字段。外部授权代次、租约与 OID 的原子校验仍必须由正式写入端兑现。崩溃后的纯对账路径不会因为本地记录为 LEASE_ACQUIRED 就自动重发提交;恢复调用方和正式写入端必须证明可安全续行后才能接通对应恢复能力。
`deploy/aios-v2/c6-host-lab.requirements.lock` 固定 Linux x86_64 / CPython 3.12 的 27 个 wheel 版本和 SHA256。这是隔离验证环境锁,不声明其他平台兼容。交接 wheelhouse 可用 `python -m pip install --no-index --find-links wheelhouse --require-hashes -r c6-host-lab.requirements.lock` 离线安装到新 venv;本次已实际重建并通过 `pip check` 和 cryptography/FastAPI/Uvicorn/pytest/jsonschema/yaml 导入。不要安装到系统 Python 或修改生产环境。
## 给 Agent-A 的串行配置边界
交接包给出固定源码、配置模板、协议、测试和摘要后,由 A 协调正式共享变更窗口。需要核对现有服务身份和 exact signer 协议,注册/配置只能走已有治理能力;不让 Owner 手工拼接签名。B 在正式写权交接前只操作自己的隔离 lab。未观测到可用服务时,缺口按 authority/signer/worker/fence/verifier 分项登记,不简写成“只等 C5”。
@@ -0,0 +1,80 @@
# V2R2C5 C5 mainline consolidation source record
## Scope and authority boundary
This maintenance candidate is reconciled onto current main
`e33664f03302fd9de7635afdd8aa9339a50e5f0a` (tree
`92ece1e9646ac2d12b4b467c82b1279ac1e11608`) and preserves its V2R2C6 source
and security changes. The original reviewed selection was made against
`b915386ccbed46a7ae77220887da8ae666649a79`. Current main already contains the
product source commit; the remaining five reviewed source commits and isolated
fixtures are carried by this candidate. This is a source and test consolidation
only. It does not create a production baseline, update the runtime registry,
activate a Task or Agent, publish a tag, or authorize a protected merge. The
production source, tag, artifact, and existing baseline records remain immutable
inputs to their validators.
| Layer | Commit | Current-main relationship |
| --- | --- | --- |
| installer / G1 | `7e14abeb6b91978ce16f3d903b6c0714af302991` | integrated by this candidate |
| builder | `8fc61bd11213bf22cc795ba933a5f367f8831f82` | integrated by this candidate |
| completion | `66530975f52a4d6f1449f18d23af808e0c9339bb` | integrated by this candidate |
| deployment | `5443c93f00ce879e1025a53258557788da4dbcec` | integrated by this candidate |
| boundary / provider | `74c0eb75348b752c82f45e53759edfff5a47e3ef` | integrated by this candidate |
| product | `5f65e458957dd51c9973cfa956a624f3cbf54990` | already an ancestor of current main |
The reviewed selection manifest contained 41 paths and eight non-identical
overlap records. No receipt, journal, live baseline, runtime registry record,
or other generated production state was used as source.
## Per-file decisions
| Classification | Paths | Decision |
| --- | --- | --- |
| source | `active-runtime/app/aios_baseline/service.py`; `aios_capability_contract.py`; `aios_task_principal_v3_receipt_provider.py`; `approved_release_executor.py`; `c5_exact_deployment_adapter.py`; `c5_registered_installer_executor.py`; `c5_staging_installation.py`; `governed_publication_client.py`; `release_source_admission.py`; `task_plane/live_governed_writer_adapters.py` | Integrated G1/product behavior and fail-closed validators. |
| builder source | `active-runtime/app/aios_trusted_candidate_builder.py` | Kept the later builder contract and integration validation. Replaced historical live policy identity constants with values read from the governed policy. |
| completion source | `active-runtime/app/c5_completion_api.py`; `c5_completion_phase.py`; `c5_completion_renewal.py`; `c5_completion_signer.py`; `silent_release_hardening.py` | Retained completion-only successor validation. Authority root, state root, Owner key, predecessor fingerprint, and historical Saga head now require a root-owned digest-bound bindings file. |
| shared completion/boundary source | `active-runtime/app/c5_release_execution_host.py` | Retained the completion host and added the boundary integration-policy checks. Source-admission Task identity is now supplied by the digest-bound host configuration. |
| deployment source | `active-runtime/app/c5_production_deployment_adapter.py` | Applied the later strict symlink-resolution check to the integrated installer version. |
| provider source | `active-runtime/app/trusted_evidence_providers.py` | Retained artifact-to-extracted-tree binding and structured gitleaks JSON handling. |
| helper | `scripts/build_c5_live_principal_overlay.py`; `scripts/build_c5_staging_artifact.py`; `scripts/probe_issuer_v3_r2_readonly.py` | Kept deterministic/read-only helper behavior. Overlay creation now requires explicit Task and Agent bindings; the checked-in example uses synthetic fixture identities. The staging artifact producer emits the final consumer's fixed 128-tilde `c5-r14-source.conf` drop-in path. |
| fixture | `tests/fixtures/c5-r14-execution-plan.json`; `tests/fixtures/issuer-v3-base/*`; `tests/fixtures/trusted-candidate-builder-policy-c5.json` | Kept only isolated synthetic Task/Agent identities. Moved the C5 builder policy from runtime config to test fixtures. |
| tests | `tests/baseline_v1_1/test_claim_heartbeat.py`; `tests/baseline_v1_1/test_loop.py`; `tests/task_plane/test_live_governed_writer_adapters.py`; `tests/test_c5_builder_contract.py`; `tests/test_c5_completion_renewal.py`; `tests/test_c5_exact_deployment_adapter.py`; `tests/test_c5_production_deployment_adapter.py`; `tests/test_c5_registered_installer_executor.py`; `tests/test_c5_release_execution_host.py`; `tests/test_c5_staging_artifact.py`; `tests/test_c5_staging_installation.py`; `tests/test_governed_publication_client.py`; `tests/test_issuer_v3_overlay_builder.py`; `tests/test_issuer_v3_receipt_provider.py`; `tests/test_signer_contract_compatibility.py`; `tests/test_trusted_candidate_builder.py`; `tests/test_trusted_evidence_providers_harness.py`; `tests/test_v2r2c5_capability_contract.py` | Integrated the selected regression coverage and resolved overlap tests in favor of the combined security contract. Execution-enabled heartbeat fixtures explicitly install trusted identity and evidence verifiers. The staging-artifact fixture builds a disposable Git source repository and never reads a live release tree. |
## Overlap resolution
1. Builder source and tests use the later builder contract while preserving G1
admission and rollback validation.
2. Deployment source and tests retain the integrated adapter and add the later
strict symlink target checks.
3. The release host retains completion-only support and the boundary/provider
integration checks; the boundary host was not accepted wholesale because it
would remove completion behavior.
4. `silent_release_hardening.py` retains the completion dependency and derives
its state-root check from the registered completion authority while keeping
the C6 standing-authorization guard and credential fencing.
5. The six current-main overlaps were reconciled without whole-file selection:
the baseline evidence changes were identical, the capability vocabulary kept
the C6 digest additions, staging tests kept C6 file-mode fixtures, and the C5
completion dependency was combined with C6 authorization guards.
6. The unsigned staging producer and the final consumer now share the governed
fixed drop-in identity. The consumer's path, mode, archive, hash, and signature
validators remain unchanged and fail closed on any other producer path.
## Excluded ephemeral material
The candidate contains no live one-time Task or Agent identity, TTL, nonce,
approval fingerprint, activation file, or default historical Saga-head binding.
It also contains no copied receipt, live journal, baseline, registry projection,
signer credential, or production service state. Immutable source/tree/artifact,
lineage, hash, expiry, authentication, and compare-and-swap checks remain in the
code. Test-only identities are explicitly named `fixture-*` or `FIXTURE-*`.
## Verification record
Verification is recorded against the committed candidate and its exported
snapshot. Windows results are treated as development evidence only where POSIX
ownership, mode, or symlink semantics are unavailable. The authoritative full
regression is run from the exported tree in a disposable Linux unit with empty
capabilities, private network and temporary directories, a read-only system,
and no signer, live configuration, or Task database access.
+99
View File
@@ -0,0 +1,99 @@
# V2R2C6 acceptance record
Current review: see [V2R2C6_MAINLINE_VERIFICATION_20261001.md](V2R2C6_MAINLINE_VERIFICATION_20261001.md).
The 2026-09-26 facts below are historical. C6 is now based on the accepted
`V2R2C5` tag (`5f65e458957dd51c9973cfa956a624f3cbf54990`); source integration,
component tests, and isolated Linux proofs have advanced. Production C6
acceptance remains unestablished. Do not use the historical base or gate rows
below as current deployment facts.
Status: **DEVELOPMENT CANDIDATE — NOT RELEASE ACCEPTED**.
This record separates source verification from package, host, governed runtime,
and business acceptance. Final commit/tree and artifact identities are recorded
in the delivery `SOURCE_MANIFEST.json`; validation commands/logs accompany it.
## Identity and provenance
| Item | Recorded fact | Acceptance effect |
|---|---|---|
| C5 base | local candidate `536a2af0f07962495c88526061ce43fb27994c15` | Pinned development ancestry only. It is not evidence that C5 was authority-published. |
| C6 branch | `agent/v2r2c6-autonomy-20260926` | Local development branch; no protected-ref or publication claim. |
| Final C6 HEAD | delivery `SOURCE_MANIFEST.json` | Exact committed source is required before merge/release review. |
| Remote publication | not performed in this work | No C6 branch, tag, mainline, artifact, or deployment publication is claimed. |
## Acceptance gates
| Gate | State | Evidence and remaining requirement |
|---|---|---|
| A. Requirement/file traceability | LOCAL SOURCE MAPPED | F1-F7, the architecture input and implementation mappings are recorded in the C6 documents. Delivery inventory binds changed files to committed hashes; runtime gaps remain listed below. |
| B. Local source behavior | PASS IN RECORDED SCOPE | 734 passed, 3 POSIX skips, 17 subtests passed. Includes the full TaskPlane test directory and selected related C5/C6 suites; it does not prove production behavior. |
| C. Repository-wide supported-platform regression | NOT ESTABLISHED | Windows cannot collect all Linux-only modules and absent external dependencies. The successful related-suite command is recorded separately and is not a repository-wide result. |
| D. Component artifact/config | NOT ACCEPTED | Component/build-spec code and tests exist. Final immutable archive identity, extracted-package import, service-UID load, actual config digest, governed install receipt, readback and rollback evidence are required. |
| E. Host composition | NOT TESTED | C6 is default-off. A real Root reader, pinned registry, registered worker authority, verifier, effect/readback owner, durable-intent owner, governed pilot and WriterLease/CAS adapter have not been bound and observed together. |
| F. Governed end-to-end | NOT TESTED | No live admission → claim/heartbeat → TaskPrincipal → WriterLease → durable intent/effect → exact readback → independent verification → authorized acceptance run is recorded. |
| G. Linux/restart/rollback | NOT TESTED | Clean Linux install, effective systemd config, service UID, listener owner, restart, stale observation, rollback and post-rollback readback remain required. Windows POSIX no-follow tests were skipped and the implementation fails closed there. |
| H. Release/business acceptance | NOT ACCEPTED | C5 publication ancestry, C6 governed publication, independent release review, deployed runtime identity and authorized business acceptance are absent. |
## Functional acceptance statements
- Authorization planning is read-only. It may find an existing exact Root path,
but cannot manufacture or extend Root authority. Any write still requires the
existing governed WriterLease/CAS path.
- Automatic claim is authoritative only when the store adapter performs all
schedulability and authorization rechecks in the claim transaction. Its claim
receipt is not a WriterLease.
- Parallelism is accepted only for disjoint local resource scopes. Same-effect
and same-branch writes remain serialized by the authoritative effect owner and
WriterLease/CAS, including across processes and hosts.
- Missing or ambiguous effect evidence stays `UNKNOWN`; it does not authorize a
retry. ACK-loss recovery uses exact effect-key readback.
- P6 is intentionally narrow: it accepts an exact, unexpired Owner-signed
terminal decision and emits an idempotent `NOOP` receipt. It provides no
resume, terminal reopen, effect execution, or successor creation path.
- ARO Phase A is default-off and read-only across eight dimensions. Its six-value
decision output and audit report do not grant authority or mutate task,
policy, Git, release, or business state.
- Execution recorded, independently verified, and business accepted remain
separate gates. A component test, self-report, Shadow result, health endpoint,
archive checksum, or verifier `UNKNOWN` cannot close business acceptance.
## Evidence accepted at this stage
The following evidence is accepted only as local component evidence:
- operation/readback/patch focused suite: 24 passed;
- ARO/decision focused suite: 28 passed;
- auto-claim, schedulability and capability contract workstream suite: 57 passed;
- signed terminal-NOOP recovery plus lineage/API workstream suite: 38 passed;
- external API security and related legacy tests: 25 passed, 2 Windows skips.
The skips cover POSIX directory-descriptor/no-follow behavior and require a Linux
run. Package, runtime identity, evidence composition, full regression and live
host totals are intentionally not inferred or filled from the presence of test
files. Final commands, environment, exit codes/counts and artifact hashes
accompany the delivery manifest. Source regression ran on Windows 11 / Python
3.14.7: 734 passed, 3 skipped, 17 subtests passed in 112.85 seconds (runner wall
time 115.27 seconds). Six Windows socket shutdown thread warnings remain in the
log. The first aggregate failed six fixture time assertions; observation-time
clocks fixed them, then the same complete related suite passed. No production
behavior was relaxed to satisfy the fixture.
## Required release evidence still absent
1. Authority-published C5 identity and ancestry reconciliation against the local
C5 candidate used for development.
2. Independent release review and complete CI/test record on supported Linux
environments; local HEAD/tree and related-suite inventory are delivered.
3. Immutable component archives, build spec/config digests, dependency and
rollback identities, governed installation receipt and readback.
4. Trusted Linux host observation of loaded module/config/process identities,
restart behavior, rollback, and drift detection.
5. A real default-off-to-enabled composition with Root/registry/worker/evidence/
intent/readback/WriterLease adapters and the registered governed source-patch
pilot.
6. The complete governed vertical flow, independent verifier result, authorized
business acceptance and exact OID/effect reconciliation.
Until those records exist, the release verdict remains **NOT ACCEPTED**. This
does not invalidate the focused source tests; it limits what they prove.
+164
View File
@@ -0,0 +1,164 @@
# V2R2C6 architecture and compatibility
2026-09-29 update: `V2R2C6_TWO_LAYER_AUTHORIZATION.md` specifies the concrete
standing release broker, signed parent/actor binding, short credential renewal,
stable action identity and actual effect guards. It builds on C6 `faf6eb2` and
retains the existing Saga, approval store and capability ledger. Local host
composition is provided; registered production readers/signer and external
write fencing still require host integration and runtime acceptance.
2026-09-28 update: the branch incorporates reviewed C5 source
`2af1dfbc40277ba1d89c8fae8b18098d40e51672` through merge `d54f832`.
The release controller, standing-authorization routing and task/event projection
changes are described in `V2R2C6_RELEASE_CONTROL_INTEGRATION.md`. These are local
source changes; host composition and production acceptance remain separate.
Original development base: C5 local candidate `536a2af0f07962495c88526061ce43fb27994c15`
(tree `9f27f76e7fa5561f90ff8bfe26fd18865b4a5ebb`). This is a source ancestry
statement, not proof that C5 has been released. C6 is developed on
`agent/v2r2c6-autonomy-20260926`; protected refs and deployed state are outside
this development change.
## Authorization and automation
The Owner's existing authorization remains the trust boundary. A read-only
planner checks the pinned Git capability declaration, exact task/actor/mission,
repository/branch/source/file scope, time, budget and revocation. It never signs
or extends a Root. `acquire_authorized_writer` re-reads those inputs and delegates
only to the existing `GovernedWriterAutomation.acquire_verified` path. A valid
Root can support the existing bounded Epoch issuance; an expired or widened
Root requires the established Owner authorization flow.
```mermaid
flowchart LR
G[Immutable Git capability registry] --> P[Read-only authorization planner]
R[Existing verified Root authority] --> P
P --> W[Existing governed writer automation]
I[Authenticated registered workers] --> C[C6 host tick / Task API]
C --> S[Schedulability proof and atomic recheck]
S --> T[Existing TaskPlane SQLite authority]
T --> X[Registered source patch pilot]
X --> W
W --> F[TaskPrincipal / WriterLease / CAS]
F --> O[Authoritative effect readback]
O --> V[Independent verification]
V --> A[Authorized business acceptance]
```
`bind_c6` explicitly binds the existing TaskPlane service to its existing store,
registered worker provider and operation-bound worker receipt verifier, pinned
capability map, authoritative observation readers, durable intent reader and
governed source patch pilot. The default configuration is disabled.
The API host owns the optional tick; no second scheduler daemon, authorization
database, signer, Task database or ExecutionLease is introduced. An installed
module, enabled flag, or healthy HTTP response does not grant execution rights.
The only execution pilot is `governed_source_patch_apply`. A task must carry its
immutable `c6_operation_binding_digest` and generation; that digest binds actor,
tenant, task/job, source, branch, capability namespace, input/patch/plan/policy,
scope and effect identity. Runtime preflight is read-only and repeated before
effect. A pilot must persist an exact execution intent and the host must read
that receipt back before execution. The current claim, authorization, freeze
and exact effect state are checked again under the host authority guard before
the pilot's short write critical section. Planning, model calls and general
agent work must occur outside that section. The registered executor remains
responsible for write-time Root/revocation/budget/WriterLease/CAS checks and
effect-key idempotency at its own linearization point. No executor adapter
is synthesized from an agent-provided `live=true` assertion.
## Concurrency, claims and recovery
Formal AgentTask capability tokens retain their C5 vocabulary; negotiated
clients additionally bind its deterministic schema digest on registration,
admission, polling and claim. Legacy clients retain C5 behavior. Generic
TaskPlane wildcard capabilities stay in a different namespace.
The TaskPlane adapter rechecks authorization, scope, worker registration revision,
capabilities, heartbeat, task revision/generation, dependencies, freeze and
capacity inside the same authority/SQLite transaction that creates the claim.
Its v3 receipt contains actual claim/revision/attempt/digest fields. An attempt
number is never relabelled as a WriterLease fencing generation. Existing claim
renewal is fenced by holder, claim identity and task operation digest. Enabled
C6 HTTP heartbeats require the exact v3 claim receipt and use this same scoped
controller; missing authority assertions deny renewal.
`MutationGuard` uses ticket identity and thread ownership. Replayed, forged,
foreign or already-finished tickets cannot release another holder's lock.
Optional resource scopes permit independent local targets to proceed in parallel;
the existing WriterLease/CAS remains authoritative across processes and hosts.
Unknown effect outcomes are reconciled by the same immutable effect key. A
missing reader, 404 or lost ACK does not authorize re-execution. The host keeps a
pending unknown operation on readback only. Existing durable intents also block
execution after a host restart; readback or formal recovery must resolve them.
Durable effect knowledge belongs to
the existing Task/Job authorities, not the host's in-memory observation cache.
Terminal recovery validates exact signed identity/CAS/nonce before returning a
NOOP, and does not rewrite historical tasks.
## Evidence and closure
Execution requires explicit finite trusted identities and an injected evidence
verifier. Service construction preserves a store's configured verifier and
rejects conflicting verifier identities. Legacy unsigned evidence producers
receive a migration error before writing; complete signed envelopes bind the
task, claim, producer, domain sequence, digest chain and expiry. The included
HMAC adapter consumes an externally supplied key resolver; it creates no trust
root or persisted key. A deployment must bind its approved verification provider.
Task/Job execution, independent verification and authorized business acceptance
are separate facts. Shadow completion, self-verification, untrusted signatures,
stale/cross-scope receipts or absent acceptance cannot become business success.
Rejected terminal transitions are audited after the failed mutation transaction
rolls back. Each evidence domain rejects duplicate/regressing sequence numbers;
there is no invented global sequence across authorities.
ARO Phase A reads eight dimensions (function, performance, exception, usage,
defect, history, deadlock and long-running work). It writes only immutable audit
reports and exposes the six decision values. Missing facts remain UNKNOWN. It
does not acquire authority, sign, execute, or automatically expand task scope.
## Components, runtime identity and configuration
`deploy/aios-v2/c6-components.json` declares formal AgentTask, Bridge, TaskPlane
and health component roots. The builder reads dependency bytes from the exact
Git commit with replacement objects disabled, resolves only selected local
modules, and records every packaged file's digest. Tests import extracted
components without a worktree PYTHONPATH. Dynamic/data dependencies must be
explicit; production host bindings must be checked separately.
`BUILD-SPEC.json` freezes nonsecret effective config as a separate hashed build
input. It permits credential reference names, not credential values or arbitrary
environment/drop-in dictionaries. `COMPONENT-MANIFEST.json` records module,
config, explicit per-component data/unit files, source/tree, entrypoint,
dependency contract and rollback identities.
The detached receipt records the final archive digest, avoiding self-reference.
Exclusive publication prevents accidental overwrite; it is not a claim of
filesystem immutability. Consumers must verify receipt and file hashes whenever
opening an artifact. A source rollback reference is not a tested service rollback.
The legacy health-only installer explicitly rejects C6 component deployment.
These source packages require an approved component host installation binding.
Runtime status separates declared identity from trusted observed identity and
uses PASS/FAIL/UNKNOWN. A 40-character Git OID is not an artifact SHA-256. Empty
inventory, stale/missing observations and self-reported environment/health values
cannot produce readiness or independent runtime verification. Liveness remains
available. Clean Linux service installation/restart/rollback and independently
observed module/config hashes are release gates, not inferred from package tests.
## Compatibility and later integration
Explicit-trust fixture/API migration is intentional: callers enabling execution
must configure trusted identities and a verifier; unsigned evidence is rejected.
C5 formal protocol exports and default legacy capability negotiation remain.
Single-segment filenames and nested relative file paths have separate validators;
protected path writes use POSIX directory descriptors/no-follow and fail closed
on unsupported platforms. Runtime selection and event/context/observability
ingress use server-derived identity and tenant scope. Development authorization
bypass requires an explicitly isolated staging profile on loopback.
After C5 publication, verify the released C5 commit against this recorded base.
If C5 changed, merge that released ancestry into the C6 branch and rerun affected
checks. C6 review, governed repository publication, mainline merge, deployment
and runtime/business acceptance remain separate gates. Do not infer any of them
from a local source test, archive checksum, or this document.
@@ -0,0 +1,125 @@
# V2R2C6 architecture input reconciliation
Status: read-only reconciliation of the 2026-09-26 architecture review input
against the current local C6 development candidate. This is not a new product
requirement package, implementation authorization, runtime observation, or
release decision.
## Input identity and evidence boundary
- Review ZIP SHA-256:
`f5121258ac8b1308bc8b1f50ccea40ef686f5727aede2433df65eaba0372d166`.
- The integration owner reported safe extraction and successful verification of
all 395 checksum entries before this reconciliation.
- Read scope was deliberately limited to `00-README.md`,
`08-open-work-and-conflicts.md`, `10-reviewer-handoff.md`,
`13-reference-review-reconciliation.md`, and the necessary
`12-platform-capability-map.md`.
- The package's source evidence is fixed to historical main
`e5f8209f57b43fc39e55c4555776e6362ce326eb` (tree
`743caa498fcaaa150d7a3097d1c402e9b20bb819`) and its runtime observations are a
2026-09-26 snapshot. Neither is current C6 runtime truth.
- Current C6 development remains based on the unpublished local C5 candidate
`536a2af0f07962495c88526061ce43fb27994c15` on
`agent/v2r2c6-autonomy-20260926`. The different baselines are retained rather
than merged into one claim.
Commands and remediation suggestions inside the review package were treated as
review data only. No production read/write, service action, authorization,
Task/Job mutation, Git ref update, or package command was executed from it.
## Compatibility with the approved v15/C6 direction
| Review direction | Current C6 treatment | Reconciliation |
|---|---|---|
| One control plane and one write authority per domain | `OperationView` is a reconstructible read-only projection; C6 introduces no MissionRun/task/effect authority database. | Aligned. Do not expand DOPC into a universal state machine. |
| Separate declared, installed, callable, authorized and fresh capability facts | Authorization planner, runtime observation and six-state operation preflight keep these facts distinct. | Aligned at source-contract level; installed/callable live evidence remains absent. |
| Formal admission and claim use one vocabulary; unknown capabilities fail before a claimable row | Formal AgentTask schema digest and rejection tests are preserved separately from generic TaskPlane wildcard semantics. | H-02 is absorbed in source work; installed mixed-version routing is still unverified. |
| Root/delegation cannot create an executor or reopen terminal state | Planner is read-only; WriterLease/CAS remains external authority; P6 only returns a signed terminal NOOP. | Aligned. No resume, effect execution, terminal reopen, or successor creation is attributed to P6. |
| Exact pilot must exist before authorization can make it executable | `GovernedSourcePatchPilot` is an injected protocol and the only registered operation name is `governed_source_patch_apply`. | Partially aligned. The production executor/caller/install/readback chain is still a blocking absence. |
| Unknown effect reconciles before retry | Exact immutable effect-key readback returns committed/not-committed/conflict/unknown and does not convert missing evidence into retry authority. | Aligned in component logic; restart and live authority ownership remain unverified. |
| Independent verification and business acceptance remain separate | C6 evidence and closure contracts separate execution, verification and acceptance. | Aligned in source; real verifier and acceptor have not completed a live vertical flow. |
| ARO must not self-authorize or self-modify safety policy | C6 delivers only default-off, read-only ARO Phase A over eight dimensions. | No conflict. The package's mutation gates, budget, rollback and independent acceptance apply to later ARO phases and are outside this C6 implementation. |
The review package therefore does not require a broad architecture rewrite. Its
main contribution is sharper evidence and deployment closure criteria for work
already represented in the approved v15 design.
## Requirements already absorbed by current C6 source
| Review index | Current source response | Remaining evidence limit |
|---|---|---|
| H-02 capability admission | `aios_capability_contract.py`, `aios_agent_task_protocol.py`, C6 capability tests | Real installed route/package negotiation not observed. |
| H-03 selected security findings | Model/actions hardening plus `agent_context_api.py`, `agent_observability_api.py`, `agent_event_api.py`, `agent_event_bus.py` address M-1/M-2/M-4/M-5/M-6/M-7/M-12 source paths. | All 13 gates still require final production-module evidence; source fixes do not prove effective mounting or deployment. |
| H-06 read-only preflight | `task_plane/operation_view.py` implements exact binding and six mutually exclusive outcomes with bounded evidence references. | Real runtime observations and pre-effect host revalidation are not bound. |
| H-08 projection/readback | `operation_readback.py` preserves authority-over-projection and treats stale/conflicting facts as unknown. | No live lag/divergence/restart observation exists. |
| H-11 event tenant/target isolation | Event ingress requires trusted-principal `tenant_id` and exact `allowed_event_targets`; publish persists tenant binding and list/get/transition/stats enforce it before mutation. Missing host scope fails closed. | Effective host principal projection and externally mounted routes remain unverified. |
| H-14 ARO Phase A | `aro_readonly.py` and `decision_policy.py` provide eight dimensions, immutable reports and six decisions without business mutation. | Later autonomous remediation gates are deliberately absent. |
| H-23 reference-review security threads | Context identity, observability scope, event path handling, runtime selection and unknown-action negative paths have C6 tests. | Historical V1.1 test/deployment claims were not reused as current PASS. |
## Current C6 implementation blockers confirmed by the new input
These are concrete composition gaps, not requests for another framework:
1. **Exact governed pilot (H-05).** C6 defines the input contract and host
protocol, but no production `governed_source_patch_apply` executor has been
identified and bound with its real caller, artifact, install path and exact
authorization consumer. Until then preflight must return
`EXECUTION_ENTRYPOINT_ABSENT` or another precise non-ready result.
2. **Durable intent and effect owner (H-07).** The host requires an injected
durable-intent reader and authoritative effect reader. A fixture or in-memory
cache cannot satisfy this. The real Task/Job authority must own persistence,
ACK-loss readback and restart reconciliation without creating a second DB.
3. **Authority composition (H-04).** Default-off C6 has not been bound to the
deployed verified Root reader, pinned Git registry, registered-worker
authority, revocation/budget checks, or actual WriterLease/CAS path.
4. **Independent closure (H-09).** The source supports explicit verifier
identity and acceptance separation, but there is no current independent
verifier plus authorized business acceptor result for the C6 candidate.
5. **Runtime/component identity (H-01/H-20).** Component packaging contracts are
present, but no clean Linux install, service-UID import, effective config,
loaded byte digest, rollback or on-site C6 readback exists.
6. **Test truth (H-19).** Focused Windows component suites are evidence only for
their tested units. Linux-only no-follow, systemd, packaging, restart and full
production imports remain required.
These blockers already fit the default-off host contract documented in
`V2R2C6_CHANGE_MATRIX.md`; they do not justify enabling a generic shell writer,
minting new authorization, or treating health/HTTP 200 as readiness.
## Newly explicit follow-up work outside the current C6 boundary
The review package also covers broader AIOS platform work. The following should
remain separately owned follow-up routes unless a later requirement explicitly
brings them into a release:
- **H-12 durable message architecture:** C6 now enforces event tenant and exact
target scope from the trusted principal, but does not merge the file event
bus, TaskPlane outbox and Agent-OS message/memory prototypes into one durable
fabric. Delivery receipts, ordering, deduplication and DLQ ownership need a
separately approved route. A new universal message service is not implied.
- **H-13 model/tool governance:** registry freshness, provider probes, fallback,
cost and audit are separate capability domains; C6 runtime-selection hardening
does not prove upstream model/tool availability.
- **H-15 retirement:** old Task/Bridge/model entrypoints require caller and
in-flight-task inventories, single-writer proof and controlled drain before
sunset.
- **H-17 disaster recovery:** Git/DB/artifact/receipt backup and isolated restore
must preserve nonce, lease and idempotency protections; no exercise occurred.
- **H-18 API convergence:** source routes, OpenAPI, effective overlays and live
methods need a per-operation compatibility table. Current source-level tests
cannot establish effective routing.
- **H-21/H-22 performance and non-core products:** real SLO/cost/manual-event
measurements and WebOps/search/channel E2E belong to their product owners.
- **ARO later phases:** capability allowlists, deny-lists, budgets, rollback and
independent acceptance are prerequisites for any future mutation phase. They
are not silently added to Phase A.
## Acceptance effect
The new architecture input strengthens the existing `NOT RELEASE ACCEPTED`
verdict but does not change it to a new failure or a completed review. Current
C6 source has absorbed several design and security contracts; package integrity,
historical main inspection and focused tests remain separate from live component
identity, governed execution, independent verification, rollback and business
acceptance. Closing the six blockers above is the minimum C6 integration route.
+91
View File
@@ -0,0 +1,91 @@
# V2R2C6 change matrix
Current implementation and unresolved requirements are recorded in
[V2R2C6_MAINLINE_VERIFICATION_20261001.md](V2R2C6_MAINLINE_VERIFICATION_20261001.md).
The development identities and runtime observations below describe the original
2026-09-26 milestone. The current C5 parent is accepted tag `V2R2C5`, source
`5f65e458957dd51c9973cfa956a624f3cbf54990`.
Status: local development candidate tracking, 2026-09-26. This document maps
implemented source and focused tests; it is not a release, deployment, runtime,
or business-acceptance receipt.
- Development base: local C5 candidate `536a2af0f07962495c88526061ce43fb27994c15`.
The candidate is not authority-published C5.
- Development branch: `agent/v2r2c6-autonomy-20260926`.
- Final C6 HEAD and the final whole-suite result are intentionally left for the
integration owner after concurrent edits stop.
## Architecture review input
The later architecture review ZIP
`f5121258ac8b1308bc8b1f50ccea40ef686f5727aede2433df65eaba0372d166`
was reconciled in
[`V2R2C6_ARCHITECTURE_INPUT_RECONCILIATION.md`](V2R2C6_ARCHITECTURE_INPUT_RECONCILIATION.md).
Its fixed main (`e5f8209...`) and runtime observations are historical evidence,
not current C6 truth. It confirms the present default-off blockers: no proven
production exact pilot, durable-intent/effect owner, live Root/registry/worker/
WriterLease composition, independent verifier/acceptor flow, Linux component
readback, or complete production-module test evidence.
Its broader platform items do not expand this C6 into a universal rewrite.
Event tenant/target authorization was absorbed into the M12 source patch;
durable message delivery, model/tool registry freshness, legacy entrypoint
retirement, disaster recovery, route/OpenAPI/overlay convergence, and later ARO
mutation phases remain explicit follow-up work.
## F1-F7 traceability
| Finding | Implemented source | Focused verification surface | Current boundary |
|---|---|---|---|
| F1 component artifact coverage | `scripts/autonomous-release/component_release.py`, `deploy/aios-v2/c6-components.json`, `scripts/autonomous-release/build_trusted_release_artifact.py`, `active-runtime/app/trusted_production_adapter.py` | `tests/test_c6_component_package.py` | Source-level component manifests and package checks exist. Service-UID import, governed installation, component readback, rollback, and vertical live E2E are not verified. |
| F2 runtime identity truth | `active-runtime/app/component_runtime_identity.py`, `active-runtime/app/aios_chatgpt_bridge_v2_server.py`, `active-runtime/app/deployment_state.py`, `active-runtime/app/v2r1c6_privacy_tools.py` | `tests/test_c6_runtime_identity.py` | Declared and observed identities, Git OID and artifact SHA-256, and PASS/FAIL/UNKNOWN are separated in source. No independently observed live module/config/process identity has been collected. |
| F3 verifier composition and producer migration | `active-runtime/app/task_plane/evidence_verifier.py`, `active-runtime/app/task_plane/{api,models,policy,store,evidence}.py`, `active-runtime/app/task_plane/c6_composition.py` | `tests/test_c6_evidence_composition.py`, `tests/task_plane/test_c6_composition.py`, `tests/task_plane/test_c6_terminal_audit.py` | Execution-mode trust is explicit and conflicting/missing verifier bindings fail closed. The included HMAC verifier needs an external resolver and is not a production trust root. A real provider, migrated live producers, restart readback, and fault-atomic live flow remain unverified. |
| F4 reproducible configuration | `scripts/autonomous-release/component_release.py`, `deploy/aios-v2/c6-components.json` (`BUILD-SPEC.json`/component manifest contracts) | `tests/test_c6_component_package.py` | Nonsecret effective configuration and credential references have build-time contracts. Clean Linux installation, real systemd effective configuration, listener owner, restart, and rollback equivalence remain unverified. |
| F5 exact preflight | `active-runtime/app/task_plane/operation_view.py`, `operation_readback.py`, `patch_contract.py`, `c6_composition.py` | `tests/task_plane/test_operation_view.py`, `test_operation_readback.py`, `test_patch_contract_c6.py`, `test_c6_governed_pilot_integration.py` | Operation binding, six preflight outcomes, stale/unknown handling, exact effect readback, and source-patch contract are implemented. READY is read-only and must be rechecked before effect. Live Root, registry, durable intent, governed pilot, and WriterLease/CAS adapters are not proven. |
| F6 capability domain contract | `active-runtime/app/aios_capability_contract.py`, `aios_agent_task_protocol.py`, `active-runtime/app/task_plane/c6_composition.py` | `tests/test_c6_capability_contract.py`, `tests/test_capability_registry_c6.py`, existing AgentTask protocol regressions | Formal AgentTask vocabulary/schema digest remains distinct from generic TaskPlane capabilities; unknown formal tokens fail closed. Installed route/package interoperability and live mixed-version negotiation remain unverified. |
| F7 trust and acceptance semantics | `active-runtime/app/task_plane/{policy,models,evidence,evidence_verifier,c6_composition}.py` | `tests/test_c6_evidence_composition.py`, `tests/task_plane/test_c6_composition.py`, `test_c6_terminal_audit.py` | Execution recorded, independently verified, and business accepted are separate facts. Shadow, missing/untrusted verifier, and absent acceptance cannot close business success. A real acceptor and production consumer have not been exercised. |
## Original C6 capability goals
| Goal | Implemented source and behavior | Verification and explicit limit |
|---|---|---|
| Authorization automation | `task_plane/authorization_planner.py` reads a pinned Git declaration and verified Root snapshot; `authorization_composition.py` delegates only through existing governed writer automation. | `tests/task_plane/test_authorization_planner_c6.py`. The planner does not sign, create a Root, widen scope, or authorize a write by itself. |
| Automatic claim and renewal | `task_plane/schedulability_store_adapter.py`, `auto_claim.py`, and existing store/schedulability modules recheck worker, task, authorization, scope, dependency, freeze, capacity, revision/generation, and active claim in the authority transaction. | `tests/task_plane/test_auto_claim_store_c6.py` plus existing schedulability tests cover contention, stale proof, expiry, holder and renewal denial. A TaskPlane claim generation is not a WriterLease fencing generation. |
| Parallel work | `task_plane/governance_control_kernel.py` uses ticket identity/thread ownership and optional resource scope; disjoint work can proceed while same resource serializes. Cross-process writes remain governed by WriterLease/CAS. | `tests/task_plane/test_mutation_guard_ownership_c6.py`, `test_c6_governed_pilot_integration.py`. No multi-host live concurrency run has been performed. |
| Unknown-effect recovery | `operation_readback.py` reconciles exact immutable effect keys and keeps missing/conflicting outcomes UNKNOWN; `aios_task_parameterized_recovery.py` adds the bounded terminal recovery adapter. | `test_operation_readback.py`, `tests/test_task_parameterized_recovery_c6.py`. **P6 supports only an exact Owner-signed terminal `NOOP`. It does not resume an effect, reopen a terminal task, or create/authorize a successor.** |
| ARO Phase A | `task_plane/aro_readonly.py` evaluates function, performance, exception, usage, defect, history, deadlock, and long-running dimensions; `decision_policy.py` exposes six deterministic decisions. | `test_aro_phase_a_c6.py`, `test_decision_policy_c6.py`. Default off; read-only inputs; immutable audit output; no authority, signer, policy mutation, task mutation, or `AUTO_FIX` for Phase A. |
| External API residual security | `agent_context_api.py`, `agent_observability_api.py`, `agent_event_api.py`, `agent_event_bus.py`, shared `security_path_guards.py`, plus model/actions hardening owned by integration. | `tests/test_c6_security_residuals.py`, `test_c6_model_api_security.py`, and adjusted legacy negative fixtures. Server principal and loopback checks precede mutation; event tenant and exact targets come from trusted principal attributes and missing host scope fails closed; protected file operations fail closed without supported no-follow primitives. |
## Host and authority boundary
`task_plane/c6_composition.py` is default-off. Enabling it safely requires the
real host to bind all of the following, with mutually consistent identities:
1. an existing trusted `TaskPlaneService` with explicit identity/evidence trust;
2. a pinned Git capability registry OID and manifest digest;
3. registered worker and authorization readers;
4. authoritative operation/effect readers and a durable-intent reader;
5. the single registered `governed_source_patch_apply` pilot whose write path
revalidates Root, revocation, scope, budget, generation, WriterLease and CAS.
No test fixture, `enabled=true`, health response, installed module, or in-memory
intent is a substitute for those bindings. Clean Linux package installation and
rollback, service-UID imports, actual systemd configuration, live governed E2E,
independent runtime observation, and on-site release have not been verified.
## Focused test evidence available before final integration
| Command/scope | Observed result | Meaning |
|---|---|---|
| `pytest tests/task_plane/test_operation_view.py tests/task_plane/test_operation_readback.py tests/task_plane/test_patch_contract_c6.py -q` | 24 passed | Operation/readback/patch component behavior only. |
| `pytest tests/task_plane/test_aro_phase_a_c6.py tests/task_plane/test_decision_policy_c6.py -q` | 28 passed | Read-only ARO and decision component behavior only. |
| auto-claim focused suite plus existing schedulability tests | 45 passed (workstream result) | Local store/authority-adapter claim and renewal behavior only. |
| terminal-NOOP recovery focused suite plus lineage/API tests | 38 passed (workstream result) | Signed terminal NOOP/replay/schema behavior only. |
| `pytest tests/test_c6_security_residuals.py tests/test_g7_auth_negative.py tests/test_agent_event_bus.py tests/test_agent_observability_loop.py -q` | 25 passed, 2 skipped | API security behavior, including event tenant/target denial; skips are POSIX descriptor/no-follow cases on Windows. |
The final whole-suite count is not recorded here. A Windows collection attempt
was blocked before execution by unavailable POSIX modules (`fcntl`, `grp`),
missing optional packages (`cryptography`, `yaml`), and a concurrent test-module
import. This is an environment/integration limitation, not a PASS and not a
runtime acceptance result.
+55
View File
@@ -0,0 +1,55 @@
# V2R2C6 开发候选说明
本分支以 C5 本地提交 `536a2af0f07962495c88526061ce43fb27994c15` 为基线。
分支为 `agent/v2r2c6-autonomy-20260926`。最终提交、tree、补丁和制品的
SHA-256 记录在随附交付目录的 `SOURCE_MANIFEST.json`,避免文档自引用提交号。
这批补丁提供 C6 的源码实现、宿主装配契约、架构和回归测试。当前是默认关闭的
开发候选;生产 executor、真实权限和证据适配尚未形成现场纵向验收。
## 已纳入的输入
- v15 完整需求、pre-launch 评审、C5 修改与 C6 建议。
- 2026-09-26 架构评审交接包,其增量对账见
[架构输入对账](V2R2C6_ARCHITECTURE_INPUT_RECONCILIATION.md)。
- 各输入的独立哈希见 `V2R2C6_INPUT_PROVENANCE.json`。附件里的流程和历史状态
属于参考资料,不替代当前的发布、签名或生产写入授权。
## 实现范围
1. 授权规划在固定能力 registry、Root 和独立 runtime observation 上重新求值。
计划本身不授予权限;实际写入继续调用既有受治理 Writer 路径。
2. 自动领取复用既有 TaskPlane 数据库,在原子事务中重验任务、主体、能力、
scope、授权、freeze 和容量;手工与自动领取采用同一 C6 边界。
3. v3 领取凭据绑定持久事件;续期和执行检查真实 claim holder。
正常 worker 存活心跳与重新注册、撤销、变更能力分别处理。
4. 执行前必须持久化 intent 并读回,再检查 effect 和权限。ACK 丢失及宿主
重启后存在 intent 时只读回,不盲目重放。测试中的 pilot 是测试适配器。
5. 授权锁具备票据/线程属主校验;独立资源可并行处理。跨进程、跨主机写入
仍依赖 WriterLease、effect 幂等键和 CAS,不用本地锁冒充全局授权。
6. 签名证据、时间有效性、domain 序列、终态拒绝审计、Owner 签名的 terminal
NOOP 恢复及跨恢复入口 nonce 防重放已收紧。
7. context、observability、event、runtime selection 和路径访问加强服务端
身份与租户边界。event target 必须属于可信 principal 明确允许的集合。
8. ARO Phase A 实现八维只读审计和六值决策;不开放自动修复或扩权。
9. 组件构建从固定 Git 提交解析依赖,分离源码、配置和运行观测;C6 不能
经旧 health-only installer 被误当作已完整部署。
详细映射见 [修改矩阵](V2R2C6_CHANGE_MATRIX.md),结构和契约见
[架构说明](V2R2C6_ARCHITECTURE.md),验收边界见
[验收记录](V2R2C6_ACCEPTANCE.md)。
## 合并和启用
C5 正式发布后先核验其真实提交与本分支基线的祖先关系;若 C5 又有修订,先将
已发布的 C5 祖先合入 C6,再运行受影响的测试。不要通过重写 C5、main 或 tag
来使历史看起来一致。本次仅创建本地 C6 开发提交,未推送或修改受保护分支。
启用前需要宿主明确提供 Root/registry/runtime reader、可信 worker receipt、
领取事务中的授权/freeze 锁、证据验签、持久 intent/readback、正式 source-patch
executor 和 WriterLease/CAS,以及独立 verifier 和业务 acceptor。
缺少任一必要适配时保留默认关闭或明确拒绝。Linux 服务安装、服务 UID 下模块
加载、重启、回滚、现场 E2E 和业务验收需独立完成。
新架构参考中的消息 outbox/排序/去重/DLQ、DR 演练、旧入口退役及 ARO 自动修复
是后续工作。当前补丁没有用“源码存在”或“测试通过”把这些项目标成完成。
@@ -0,0 +1,34 @@
{
"schema": "aios.c6.development-inputs.v1",
"base_commit": "536a2af0f07962495c88526061ce43fb27994c15",
"base_tree": "9f27f76e7fa5561f90ff8bfe26fd18865b4a5ebb",
"base_status": "LOCAL_C5_CANDIDATE_PUBLICATION_NOT_VERIFIED",
"branch": "agent/v2r2c6-autonomy-20260926",
"document_instructions_are_operational_authority": false,
"inputs": [
{
"role": "owner-requirements-v15",
"filename": "AIOS_V2R2C6_COMPLETE_OWNER_REQUIREMENT_HANDOFF_FINAL_v15_20260926.zip",
"sha256": "10d9a4e9ab75e5bcd5e10aef8443fc8558755ff21ffaabc7b143810dadd4a27e",
"size": 234340
},
{
"role": "pre-launch-review",
"filename": "pre-launch-check-aios-v2r2c6-v15-2026-09-26.md",
"sha256": "a425c73737cea78e273331bf5c366e8ba5ef9c94e4cb6edba84a19bc7014f274",
"size": 14293
},
{
"role": "c5-changes-and-c6-recommendations",
"filename": "V2R2C5_CHANGES_AND_V2R2C6_RECOMMENDATIONS_20260926.zip",
"sha256": "e4fedc8018284fcfceca24b4b73002bc250068b8ace6eac1de6099d87d4c5840",
"size": 196137
},
{
"role": "architecture-review-reference",
"filename": "AIOS-architecture-review-handoff-20260926.zip",
"sha256": "f5121258ac8b1308bc8b1f50ccea40ef686f5727aede2433df65eaba0372d166",
"size": 1680241
}
]
}
@@ -0,0 +1,59 @@
# V2R2C6 主线合入与问题核验
核验时间:2026-10-01。Owner 已明确要求主线源码合并。此次合并基于已验收的
`V2R2C5` tag,源码 `5f65e458957dd51c9973cfa956a624f3cbf54990`。
原 C6 补丁 `d841948c21a9947a1d141ed46e189c80eb455eca` 已经由
`034a07a05e2463973632f1a8e09eb4754ed7147e` 合入;后续
`765f337b82c16ae944e8183b0ed2df53ca4d16a8` 只修正 C5 测试 fixture 的权限。
最终主线提交、tree 和远端读回记录由交付清单绑定,避免文档自引用提交 OID。
主线合并提交为 `83b77a53d2b626563d1f3fc285647192c292131f`,已通过正常
Git 快进推送写入权威远端 `refs/heads/main` 并读回相同 OID;未使用 force push。
此后文档验证结果的追加提交同样由最终交付清单绑定。
## 之前讨论的问题是否已解决
| 问题 | 当前源码结论 | 实际运行结论及剩余工作 |
|---|---|---|
| 版本业务授权被机器短 TTL 耗尽 | 已实现两层授权:根授权支持验收完成、撤销、取消、重大范围变更终止;短凭证受根授权与 verifier 上限约束。 | C6 正式 authority/scope reader 和 signer 组合尚未在生产观察。不能宣称最近发布中的授权空窗已消除。 |
| 临用签发、提前或到期后自动续发 | broker 核验谱系、当前根授权和已知 effect;原审批存储通过锁和旧指纹 CAS 更新,保留 Saga 和动作身份。到期瞬间现在严格拒绝。 | 发布审批机制有源码验证;TaskPrincipal、WriterLease、source/promotion authority 仍需各自正式 issuer/续发 caller 接线,不能由审批 broker 代替。 |
| 普通技术故障反复要求 Owner | 控制器区分 `WAITING_AUTHORITY` 与真实业务边界;撤销/关闭和风险边界先阻止相关副作用。常规路径目标为追加交互 0。 | 生产零追加交互尚无 C6 端到端证据。实时授权控制器 service inactive,Result=exit-code,ExecMainStatus=243;本次未观察到对应 timer。 |
| 任务自动领取、续租 | claim/renew 在同一 TaskPlane 权威事务中重新核对能力、范围、worker、依赖、冻结、容量、proof、claim 和 CAS。 | 长时间阻塞执行期间缺少自动 claim heartbeat 或已强制的执行时限;当前只有显式 renew API及阶段前后检查。该活性缺口仍存在。 |
| 多 Agent 并行 | 不同资源可并行;同一任务、动作和资源通过 claim、root/claim fence、动作账本和 CAS 串行。 | 生产跨主机 WriterLease/fence/expected-OID CAS 组合尚未验证。进程锁和本地测试不等于分布式写互斥。 |
| ACK 丢失、重启重复执行 | durable intent、同一账本与 exact readback 让 UNKNOWN 进入只读对账;已持久化 `LEASE_ACQUIRED` 恢复不重复 acquire。 | 外部 lease 成功但 phase 尚未持久化的窗口仍依赖 Bridge 幂等和 Job/readback;缺少当前 C6 实际故障注入证据。 |
| 重复失败和历史遗留可见性 | 原任务事件与 Mission/Run 起点保留,交互/决策/选择分别计数。 | dashboard 尚无独立 failure/retry/attempt 聚合;不能声称所有历史重复问题已解决。 |
| SSE 断线与游标接续 | 已实现退避重连、polling fallback、410 snapshot 重置、watermark 和旧 stream generation 防护。 | 客户端去重缓存上限 1024;更早的 event ID 重放仍依赖 revision 和服务端冲突处置。 |
| 执行、独立验证与业务验收混淆 | 三层证据和独立 receipt 验证接口已实现;缺失/冲突保持 UNKNOWN。 | 实际注册 verifier/acceptor、完整 governed 任务链与 C6 Production Acceptance 未建立。 |
## 本次核验发现并修正
执行凭证有效期改为 `not_before <= now < expires_at`。原实现的闭区间会在恰好
到期时放行。修正 verifier、实际 effect guard 和服务入口 guard,并补入受控时钟
回归;到期凭证可在当前根授权允许且 effect 已知时恢复续发,但不能据此执行旧凭证。
Linux 对照还确认原 C5 fixture 用 `write_bytes()` 受 umask 影响产生 0600 文件,
契约却记录 0644。测试 fixture 现在显式写入记录中的 mode;生产权限校验未放宽。
## 验证范围
- 先前精确 C5/C6 集成:重点授权/能力 82 passed;TaskPlane/C6/host 858 passed,
6 平台 skips。首次 7 个组件 fixture 错误来自未提交 HEAD,提交后组件组
15 passed、1 Linux-only skip。
- C5 跨平台相关测试:52 passed、1 POSIX skip。
- 隔离 Linux:baseline loop 20 passed、C6 capability 5 passed、修正后的
C5 staging installation 33 passed。staging artifact 缺独立 `live-release`
fixture,仍未完成有效验证。
- 本次独立授权审查后的修正:renewal/controller 34 passed。
- 扩展授权/发布加固回归:Windows 157 passed,2 项因当前账户没有 symlink
创建权限而在 fixture 阶段失败;同一源码 tree 的隔离 Linux 完整组
158 passed、1 Windows-only skip,覆盖两项 symlink 用例。
- 本次独立自动化审查:decision/preflight 26 passed;SSE recovery JavaScript
测试通过。历史测试组存在重叠,不相加为总数。
实时只读证据:生产 baseline 为 C5/`5f65e458…`;Task API WorkingDirectory 仍为
`396ea981…-formal-lineage-canonical-v17`;C6 隔离 service inactive。C5 主线 tag
与源码合并不代表 C6 已部署。历史交付中的真实 TaskPlane systemd 启动/重启与
401 证据只证明隔离安装,不证明生产授权或业务完成。
结论:可以确认表中已有源码与测试覆盖的问题已在对应范围内修正;完整无人值守
发布仍需正式主机接线、自动租约活性、当前权限链和独立生产验收。主线源码合并
应保留 C6 的显式启用门槛,并继续按这些未完成项验收。
@@ -0,0 +1,232 @@
{
"schema": "aios.c6.release-control-input.v1",
"input_revision": "1.1.0",
"input_zip_sha256": "bc6f98cf3e1e4fdcef5dd04600a8f3f8b331eabe3818226f6469ff4914787820",
"reviewed_c5_source_commit": "2af1dfbc40277ba1d89c8fae8b18098d40e51672",
"previous_c6_source_commit": "64eed3b11982305812b819647f63b79faff51bcf",
"integration_commit": "d54f832",
"files": [
{
"input_path": "contracts/common.schema.json",
"repository_path": "contracts/release/v1/common.schema.json",
"sha256": "cd8d7924c9636f50deec951c38d70514d552ffade578743f05caf3278f833831"
},
{
"input_path": "contracts/release-contract.schema.json",
"repository_path": "contracts/release/v1/release-contract.schema.json",
"sha256": "ecec73c89c2803245954b984a8394df78326897ab00fecb8c502966d8cfeb4f7"
},
{
"input_path": "contracts/authorization-bundle.schema.json",
"repository_path": "contracts/release/v1/authorization-bundle.schema.json",
"sha256": "8639a5525fabc3167fec16b33d528d975ba7a29e3d886292663669a306d158e8"
},
{
"input_path": "contracts/release-run-event.schema.json",
"repository_path": "contracts/release/v1/release-run-event.schema.json",
"sha256": "922f8dc89c2e21bc5999cb5e88ec7c06bcffe99bfba3b8d6133480ebdb0b1f34"
},
{
"input_path": "policy/release-policy.v1.json",
"repository_path": "deploy/aios-v2/release-policy.v1.json",
"sha256": "dc9728741b188a1199207cfb34a3881598ecfb8d5178d9719da59ffd92e9774c"
}
],
"source_mapping": {
"schema_version": "1.0.0",
"kind": "SourceReviewMap",
"status": "SOURCE_VERIFIED_RUNTIME_UNVERIFIED",
"observed_at": "2026-09-27T17:54:27.787347+00:00",
"source": {
"root_observed": "C:\\Users\\ImRuY\\.codex-restored\\work\\v2r2c5-release-automation-20260926\\source",
"head": "2af1dfbc40277ba1d89c8fae8b18098d40e51672",
"tree_oid": "537482cf7e9e123aa867aea15c6ffc33b6accac2",
"branch": "c5/unattended-release-platform-20260926",
"dirty": false
},
"runtime_verified": false,
"authority_write_allowed": false,
"entries": [
{
"id": "runner",
"path": "active-runtime/app/governed_unattended_release.py",
"sha256": "8a3a28d07cd967a65008c7216f58e0dd5b4461a3f47d754b52d83203daac66b5",
"anchor": "class GovernedUnattendedReleaseRunner:",
"line": 171,
"owner_task": "T04",
"change": "复用阶段分离、正式准入/发布与精确回读;runner 不拥有状态机。"
},
{
"id": "controller",
"path": "active-runtime/app/silent_release_execution.py",
"sha256": "52da4632984d48407eab39cb8ddc68943c1ca5a9c831103fd27ed076e4c4fade",
"anchor": "class PersistentSilentReleaseController:",
"line": 294,
"owner_task": "T04",
"change": "复用单 leader/startup;改造默认通知调用为先机器判定。"
},
{
"id": "saga",
"path": "active-runtime/app/silent_release_hardening.py",
"sha256": "ff2a64b64996d163fc4734bee9eaa3efd7a25290e9f4e8532967a7d32cb8b2aa",
"anchor": "class ReleaseSagaStore:",
"line": 2265,
"owner_task": "T03",
"change": "复用 hash-chain journal 和可重建快照;ReleaseRun 关联,不新建第二个部署真相源。"
},
{
"id": "release_service",
"path": "active-runtime/app/silent_release_hardening.py",
"sha256": "ff2a64b64996d163fc4734bee9eaa3efd7a25290e9f4e8532967a7d32cb8b2aa",
"anchor": "class SilentReleaseActionService:",
"line": 3570,
"owner_task": "T04",
"change": "复用 candidate/plan/acceptance;通知不等于人已参与。"
},
{
"id": "checkpoint",
"path": "active-runtime/app/task_plane/governance_checkpoint.py",
"sha256": "ad895e9ac7ab092e6af59cb42180ada6085a4ada483ba67b3f8c4b6e26000af8",
"anchor": "class RecoveryCheckpoint:",
"line": 28,
"owner_task": "T03",
"change": "复用观察缓存和 RetryBudget;不充当授权或 effect 成功证据。"
},
{
"id": "decision",
"path": "active-runtime/app/task_plane/governance_control_kernel.py",
"sha256": "8d053cc1cc2b0242989636b6f2b99fc16308ec4cf67f5bf9332b62705135f5f5",
"anchor": "class AutonomousDecisionEngine:",
"line": 719,
"owner_task": "T02",
"change": "复用决策、RuntimeManifest、ActionLedger、MutationGuard;补授权上下文和人工边界映射。"
},
{
"id": "recovery",
"path": "active-runtime/app/task_plane/governance_recovery_controller.py",
"sha256": "770189f9cd8e1e70130651a01b8ab689c892669e39bf6bfb9548fc10ba47643f",
"anchor": "class GovernanceRecoveryController:",
"line": 93,
"owner_task": "T02",
"change": "复用 exact child recovery 和未知结果对账;不得泛化成任意失败重放。"
},
{
"id": "mission",
"path": "active-runtime/app/task_plane/mission.py",
"sha256": "892eb2bd3a35569eb09db7b16fb41b1ae80c6c0d8f5079dc1eed7c602b4c4ffd",
"anchor": "class MissionAuthority:",
"line": 143,
"owner_task": "T02",
"change": "复用 Mission/child delegation 与 principal lineage。"
},
{
"id": "task",
"path": "active-runtime/app/task_plane/api.py",
"sha256": "8609f8aac3538ed057e9c586c1228aa5b66fd05b269bc74011fb5b03df7d222c",
"anchor": "class TaskPlaneService:",
"line": 63,
"owner_task": "T05",
"change": "复用任务读取/写入接口;投影不直接改任务表。"
},
{
"id": "campaign",
"path": "active-runtime/app/release_campaign_authorization.py",
"sha256": "017829710fe3965b66604b4eb0a81516b4e73d6b4f4886f245015716c90bdcff",
"anchor": "class CampaignAuthorization:",
"line": 176,
"owner_task": "T02",
"change": "已有长期授权及操作集;固定 repository/branch/generation,不能直接作为通用 Bundle。"
},
{
"id": "broker",
"path": "active-runtime/app/owner_authorization_broker.py",
"sha256": "4ee0cfc97d425e7b0fab946a0c1b7cb228b1d23bfed7045d460ed5157aef4cd3",
"anchor": "class OwnerAuthorizationBroker:",
"line": 406,
"owner_task": "T02",
"change": "复用正式 broker;与 runner 可调用接口须做真实签名/撤销接线验证。"
},
{
"id": "deployment",
"path": "active-runtime/app/silent_release_controller.py",
"sha256": "fd5b34cabeeec15cd9ed51449dc50b3863a366e8fc15b871f56cfd0dcd74a7f5",
"anchor": "class SystemdDeploymentAdapter:",
"line": 1091,
"owner_task": "T01",
"change": "配置型 systemd adapter;核实 service/dropin/cutover 和 previous baseline,不照抄默认值。"
},
{
"id": "trusted_plan",
"path": "active-runtime/app/governed_production_release.py",
"sha256": "3b63e568b837c76536e85223054303274ac89e229bc954f64b4dce67ea42566e",
"anchor": "class TrustedPlanReleaseExecutor:",
"line": 563,
"owner_task": "T04",
"change": "复用 trusted-plan 边界;Bridge 不能代替 privileged approved executor 执行生产副作用。"
},
{
"id": "staging_installer",
"path": "active-runtime/app/c5_staging_installation.py",
"sha256": "1191b602d25820b6960c483e7109556a4af4b6c2fda68e3c55a6f91a23943017",
"anchor": "class InstallationError",
"line": 116,
"owner_task": "T01",
"change": "固定 staging unit/path/operation,不当作 production installer。"
},
{
"id": "principal_scope",
"path": "active-runtime/app/c5_governed_principal_scope.py",
"sha256": "462344bf08f30181cebf6864f2b9c22170771cb3cc2604921de085aa3ceb4036",
"anchor": "class C5ScopeEnrollmentService:",
"line": 633,
"owner_task": "T02",
"change": "已有 scope enrollment,须与实时 runtime composition/epoch 对应。"
},
{
"id": "http",
"path": "active-runtime/app/aios_chatgpt_bridge_v2_server.py",
"sha256": "e1dcaebd792ef49015137990c9f3b14012635b7956f3a9ba59f37965a72af4a7",
"anchor": "\"/v2/release/execute\"",
"line": 63,
"owner_task": "T05",
"change": "已有 /v2 release/admission routes;提案 /api/releases 并非现成路由。"
},
{
"id": "mission_ingress",
"path": "active-runtime/app/aios_task_mission_authority_server.py",
"sha256": "8e205c6585647a35875172a03bb324278f7a418a554dcaf677d14e8fd848dad1",
"anchor": "class SignedMissionIngress:",
"line": 45,
"owner_task": "T00",
"change": "签名 ingress audience 为 staging;需要验证实际部署环境,不能假定 production。"
},
{
"id": "task_view",
"path": "active-runtime/app/task_dashboard_view.py",
"sha256": "6def60463f58db8c049c5cfe18f706780f48c00cf04620e6a75bd447e0bd0fca",
"anchor": "class TaskDashboardView:",
"line": 8,
"owner_task": "T06",
"change": "已有视图模型;补任务树、授权、next owner 和 unknown 时长,不能把返回 route 当页面上线。"
},
{
"id": "web",
"path": "overlays/user-control-center/portal/gitops-dashboard/app.js",
"sha256": "19ee89d871cee6b8ca204ee2b946e01f01f2360fe5bc333b395a1c67826d1a04",
"anchor": "function connectEvents()",
"line": 373,
"owner_task": "T06",
"change": "已有 EventSource;onerror close 后无显式重连/增量接续,需补恢复和状态时效。"
}
]
},
"input_tests": {
"passed": 36,
"source_entries_matched": 19,
"archive_manifest_entries_matched": 28
},
"authority_granted_by_document": false,
"runtime_verified": false,
"policy_installation_status": "NOT_INSTALLED",
"scope": "C6 local code integration; attachments do not authorize production operations"
}
@@ -0,0 +1,129 @@
# C6 发布控制修改:v1.1.0 输入整合
2026-09-29 补充:下文保留 9 月 28 日输入整合时的实现与验证范围。持续授权
已进一步补齐具体 broker、JIT 签发/续发、签名父授权绑定、稳定动作身份和
实际执行 guard;当前设计及接口以 `V2R2C6_TWO_LAYER_AUTHORIZATION.md` 为准。
主机正式 reader/signer、分布式写入 fence 和线上验收仍是独立条件。
本轮在既有 C6 分支 `agent/v2r2c6-autonomy-20260926` 上整合评审意见。
原始 C6 提交为 `64eed3b11982305812b819647f63b79faff51bcf`,新 C5 源码为
`2af1dfbc40277ba1d89c8fae8b18098d40e51672`,通过合并提交
`d54f83266803a4df349385ea92dd7e6e4d040b91` 纳入,保留两侧提交历史。
原始 C5/C6 工作成果和 9 月 26 日交付包保留。
## 输入与实现边界
v1.1.0 ZIP 的 SHA256 为
`bc6f98cf3e1e4fdcef5dd04600a8f3f8b331eabe3818226f6469ff4914787820`。
本轮重新验证 28 项包内摘要、19 项 C5 源码锚点和摘要、36 项离线测试。
逐文件来源见 `V2R2C6_RELEASE_CONTROL_INPUT.json`。
附件里的 T00–T07 是目标实施及验收要求,不是已发生的运行事实,也不授予
生产写入权限。本轮交付范围是本地源码、测试、架构与可复现候选制品。
没有从附件复制生产 Task、授权、端口或服务状态作为当前执行依据。
四份 JSON Schema 位于 `contracts/release/v1/`,唯一参考策略位于
`deploy/aios-v2/release-policy.v1.json`;它保留 `NOT_INSTALLED` 状态。
Schema 描述接口结构,不能证明有效授权,也不会自动注册签名权或部署入口。
## 复用的运行结构
| 职责 | 现有实现 | 本轮约束 |
|---|---|---|
| 发布入口与阶段衔接 | `governed_unattended_release.GovernedUnattendedReleaseRunner` | 接通原 broker 和正式回读;不持有第二套状态机 |
| 常驻接续 | `silent_release_execution.PersistentSilentReleaseController` | 根授权覆盖范围内先自动处理;人工通知由真实边界决定 |
| 发布副作用与恢复真相 | `silent_release_hardening.ReleaseSagaStore` / `ReleaseOrchestrator` | journal 为真相;快照可重建;UNKNOWN 先对账 |
| 短期签名与持续授权 | `release_campaign_authorization.CampaignAuthorization` / 原 `SignedApprovalVerifier` | 新控制器注入当前授权读取与发布签名适配;不能将 Task writer broker 当成发布签名方 |
| 决策与写入互斥 | `task_plane.governance_control_kernel` | 复用 DecisionEngine、ActionLedger、MutationGuard;真实边界优先 |
| 任务展示与事件 | `task_dashboard_view` / `task_center_api` / 现有 Portal | 只读投影,不直写 Task 表;未知时长和交互数显式未知 |
| 候选制品 | `scripts/autonomous-release/component_release.py` | 精确 Git 提交打包上述模块、契约和前端;不从未提交工作区取字节 |
原 C6 的认证 worker、原子领取、执行意图回读、WriterLease/CAS、证据签名和
独立 runtime identity 机制保持在同一分支。发布控制接续不代替这些写入检查。
## 本轮行为变化
控制器通过 `StandingAuthorizationBroker(envelope) -> StandingAuthorizationResult`
读取当前授权上下文。配置该入口后,每次准备副作用和恢复 effectful worker 前
都会重新读取生命周期与精确范围,并调用已有 `AutonomousDecisionEngine`。
缓存中的短签名不能跳过根授权关闭、撤销或 scope 变化。审批仍须通过原
`SignedApprovalVerifier` 的签名、时效、撤销和精确候选/计划检查。
技术错误、缺配置、未知授权进入 `WAITING_AUTHORITY`;撤销、关闭或取消禁止
新 effect;只有四类明确 Owner 边界产生人工通知。UNKNOWN effect 优先走原
Saga 的恢复/对账路径;未查明之前不为新 main 事件生成下一候选。
决策器增加类型化 `DecisionEffectIntent`、`AuthorityDecisionContext` 和
生命周期/scope 枚举。显式风险边界优先于机制修复,字符串和布尔值不能伪装
成类型化授权上下文。`FILE_ALLOWLIST_MISSING`、`ACCEPTANCE_REVISION_MISSING`
和 `TASK_EXPIRED_OWNER_RECOVERY_REQUIRED` 依据实时 scope 分流,不仅凭错误名
决定问 Owner 或放行。决策输出仅用于路由,不具有签发凭据的能力。
授权规划器沿用 governed writer 的同一份 `RISK_ORDER`,修复新 C5 的
`engineering` 风险等级被 C6 旧字典误拒绝的问题;未知等级仍拒绝。
Task Center 在既有 router 中提供受同一身份解析器保护的 overview、SSE 和
poll 读取接口。投影提供任务谱系、当前责任与授权引用字段,人工统计分别记录
初始请求、追加交互、决策、提供选项和选择提交次数。自动任务事件和系统通知
不计成人工输入;缺历史覆盖证据时为 `null`,不能从空事件列表推导为 0。
当前 TaskPlane 列表源只含简要任务记录,不含完整 executor/issuer/授权责任链;
缺失字段保持未知,完整链路仍需通过正式领域读取补齐,不从浏览器角色反推。
前端使用实体 revision 和续传 cursor,断线显示 STALE,按有界退避重连并
降级轮询;游标失效后重新读取快照。旧响应和重复事件不能倒退新任务状态。
原有 GitOps 事件流保留,Task Center 事件仅更新相关任务投影。
任务卡片保留展开状态及阅读滚动位置。游标按每个任务的持久事件序号续传,
不使用可能迟到的时间戳作为分页位置;传输容量超限明确报错,不循环重置。
## A26–A31 覆盖与限制
以下是源码与本地验证覆盖,不是完整运行验收 PASS。
| 验收项 | 已实现或复用的本地覆盖 | 仍需的完整验收证据 |
|---|---|---|
| A26 恢复缓存 | 原 Saga journal 链校验、截断/篡改拒绝、journal 后快照前崩溃恢复;控制器重启幂等、UNKNOWN 阻断新候选、恢复执行前当前授权检查 | Linux flock、多 Run、旧 worker/fence 以及真实多域 effect 故障注入 |
| A27 持续授权 | 类型化当前授权入口、每次重新读取、根授权关闭/撤销拒绝、scope 改变、短签名到期/篡改拒绝、原验签保留 | 当前根授权读取器与已登记发布签名方的正式组合;子孙任务/successor、真实撤销竞态与 0 追加交互 |
| A28 错误分流 | 三种历史错误分别覆盖 in-scope/out-of-scope/unknown;四类明确 Owner 原因;只读诊断可单独路由 | 实际 caller、Task/capability、修复操作及回读绑定 |
| A29 交互与耗时 | 五类计数、稳定身份去重、冲突/缺历史未知、任务谱系投影;未知耗时不填 0 | 实际输入端生成持久交互证据、完整跨 Mission/Run/attempt 原始起点、计时及预算观测 |
| A30 混合故障 | 明确 Owner 边界优先于机制故障;重启/缓存不能掩盖关闭或 scope 变化;原执行验签仍强制 | 写边界检查后撤销、进程指纹变化的真实 runtime 故障注入 |
| A31 事件恢复 | 受权 overview/SSE/poll、cursor 恢复、重复/旧 revision 拒绝、旧连接代次保护、实体更新与 STALE 展示 | 实际服务挂载、鉴权中间件、浏览器与反向代理环境下的续传/切换/历史阅读验收 |
没有新增全局 `current-release.json`、第二套决策器、动作账本或后台服务。
T00–T07 的主机接线和在线验收不因本地覆盖而自动完成。
## 主机接入契约
1. 发布控制器注入 `StandingAuthorizationBroker`。适配器每次调用读取当前
Campaign/Bundle 生命周期、撤销和精确 ReleasePlan scope,并返回带持久
evidence refs 的 `AuthorityDecisionContext`。只有 ACTIVE + IN_SCOPE 可调用
已登记的发布审批签名方,产生原 ApprovalKeyRing 接受的完整
`SignedReleasePlanApproval`。缺少适配器时等待 authority,不创建伪签名。
2. `OwnerAuthorizationBroker` 是 Task writer 准入,明确拒绝 production/tag/
baseline/main 能力;TaskPrincipal 签名属于另一契约,不能替代上述发布审批。
`CampaignAuthorization` 读取 campaign 事实但不签发发布审批;Bundle 的
epoch 摘要也不具有候选/ReleasePlan 的完整签名语义。本轮没有改变这些边界。
3. 实际 FastAPI host 挂载 `task_center_api.router` 并调用
`configure_task_plane_projection(service, principal_resolver)`。resolver 必须从
服务端认证上下文得到 `task_plane_identity`;adapter 通过已有
`TaskPlaneService.list_tasks/get_events` 执行领域授权读取。缺失 reader 或
resolver 返回 503,领域未认证/无权限分别返回 401/403,不采信浏览器传入的 role/scope。
4. 当前 repository 没有上述 host 的完整注册调用链。本地 TestClient 可验证
router 行为,不能据此声称线上 endpoint、页面或 signer 已部署。
## 验证记录与部署边界
完整 TaskPlane 回归通过 633 项及 17 个子测试;风险等级兼容修复后另补跑
20 项通过。独立授权/合并范围评审通过 138 项、跳过 1 项 POSIX 检查;其计数
与其他测试有交集,不能累加为独立用例总数。控制器专项通过 13 项。
最终发布/视图回归、JavaScript 恢复测试和精确提交制品验证以交付包
`evidence/` 下各自 JSON、原始日志为准。
Windows 上 Linux `fcntl`、`fork` 与 symlink 权限相关检查,以及依赖外部
C5 `live-release/staging-source-snapshot` 的测试,应明确记录为未验证。
不能使用空实现、跳过验签或模拟 POSIX 锁来声称这些检查已经通过。
控制器逻辑专项的 `PortableLeaderLock` 仅隔离 POSIX 依赖以验证路由,不是锁
互斥、跨进程并发或生产运行证据;相关实际 POSIX 用例保留为未运行。
真实 authority caller、正式 broker 注册、撤销竞态、Linux 安装/重启/回滚、
持续后台执行、生产业务验收和上线页面仍需部署环境证据。分钟级发布目标和
追加交互 0 次是验收目标,本地测试不产生历史耗时或真实交互次数。
@@ -0,0 +1,92 @@
# V2R2C6 两层发布授权补丁
## 目标与范围
在 C6 `faf6eb22898ad5d361824f21dc649527489f16a4` 上补齐持续版本授权与短期执行凭证的组合。沿用 C5/C6 的发布控制器、ReleasePlan、Saga、审批存储、CapabilityLedger 和恢复机制。版本目标不因机器凭证到期而要求重复 Owner 决策;过期执行凭证仍不得执行受保护动作。
本次交付是本地源码补丁与验证证据,不注册生产签名方、不修改生产信任根、不发布受保护分支。此前材料中的历史耗时和空窗为需求背景,本轮不据此声称重新核验过线上状态。
## 设计决策
1. 业务授权由可信 authority reader 读取并验证,绑定版本、仓库、生产范围、授权基线 OID、信任根、能力上限、回滚目标、Mission/Run 和授权代次。生命周期由可信验收关闭、撤销、取消或重大范围变更终止。可配置绝对截止期;3–7 天不是强制重批周期。
2. 已登记发布签名方派生短期执行凭证,保留原 Ed25519 验签与精确 ReleasePlan 绑定。子凭证签名覆盖父授权和执行者身份。旧直接审批格式保持兼容,不能据此伪装成持续授权子凭证。
3. 在现有控制器调用链内临用签发、提前刷新;没有第二个定时控制器或动作账本。短期默认值维持现有 verifier 上限内;不批量拉长 promotion epoch、TaskPrincipal 或 WriterLease。
4. 续发保持同一候选/ReleasePlan/Saga 与动作键,变更 nonce、签发时间、到期时间和签名。存储替换有并发控制和预期旧指纹约束,旧 worker 不能覆盖新凭证。已执行动作不可因续发再次执行;未知副作用先对账。
5. 新候选摘要可以变化,但须通过可信 successor/lineage 校验,并获得新的精确绑定凭证。新范围、信任根、授权基线不允许静默迁移。
6. 每次签发与实际副作用前检查当前根授权。单进程检查不能替代远端写入 CAS/fence;主机接入必须在写边界兑现撤销代次和执行 fencing。
7. 凭证过期或 authority 暂不可用仅阻断对应写入。只读诊断、状态查询和安全恢复入口不应依赖有效执行 token 才能启动。读取器、签名方在调用时工作,不在模块导入时读取短期凭证。
## 实施分工与顺序
- 持续授权模块:严格类型化当前授权及执行范围,可信谱系校验,fresh 子凭证派生,刷新窗口,当前授权检查。
- 原审批/执行模块:签名绑定的兼容扩展,安全续发存储,稳定动作身份,执行前授权检查。
- 控制器组合:注入正式 broker,状态和恢复沿用原 Saga;完成包组件清单与接入说明。
- 独立验证:多次到期、重启、撤销竞态、越界、未知结果、并行领取/刷新、签名篡改和兼容回归;生成固定 Git 提交对应的补丁、bundle、摘要和验证记录。
## 验收与证据边界
验收必须区分:本地模型/契约测试、现有服务集成测试、打包导入验证、主机正式接入与在线生产验收。局部测试不代表线上持续授权已生效。最终运行结果随补丁包的 `evidence/final-regression.json`、日志和独立审查记录交付。
## 已实现的组合接口
`silent_release_execution.compose_standing_release_controller` 接受现有 `SilentReleaseActionService`,注入两个可信 reader 和一个已登记 signer,安装实际执行入口的 guard,返回原 `PersistentSilentReleaseController`。
| 参数/接口 | 责任与边界 |
|---|---|
| `authority_reader() -> StandingReleaseAuthorization` | 验证并读取当前业务授权。返回真实 Owner 身份、允许的 actor 集合、授权代次/摘要、版本/仓库、稳定生产范围、基线/信任根、谱系根、Mission/Run、生命周期及可信验收状态。必须来自正式 authority,不接受请求体构造的授权。 |
| `execution_scope_reader(envelope) -> ReleaseExecutionScope` | 验证实际 actor、Mission/Run、当前候选、ReleasePlan、根候选和 predecessor/successor 证据,以及已对账的 effect 状态。正式任务谱系读取器须验证继承关系;仅填写摘要不是证明。 |
| `signer.key_id`、`signer.sign(unsigned_approval)` | 使用已登记发布签名身份,仅给完整待签载荷添加签名;服务继续用原 `ApprovalKeyRing` 和验签器核对。生产模式拒绝显式 test-only signer。 |
| `execution_ttl_seconds=1200` | 发布执行凭证默认 20 分钟,允许范围不超过现有一小时验签上限,实际还受 verifier 配置和父授权剩余有效期约束。 |
| `renewal_margin_seconds=120` | 剩余两分钟时可提前签发;已经到期时,在核验旧凭证真实性、当前根授权和动作状态后也可恢复续发。 |
| `broker.check_current(approval, capability, operation)` | 重新读取当前根授权与精确范围,验签并检查当前有效期。实际执行还叠加存储中最新凭证指纹检查,不能用“续发”操作名绕过。 |
调用链:
```mermaid
flowchart TD
A[可信版本授权 reader] --> B[StandingReleaseAuthorizationBroker]
L[可信 Task / lineage / effect reader] --> B
P[原 Candidate 与 ReleasePlan 存储] --> B
B --> S[已登记发布 signer]
S --> V[原 SignedApprovalVerifier]
V --> C[原审批存储 CAS 轮换]
C --> R[原控制器与 Saga 恢复]
R --> G[当前根授权 + 最新凭证 + 原动作 claim]
G --> E[切换 / 回滚 / tag / registry / baseline]
```
Owner 与 actor 分开记录。一个业务授权可以预先列出多个允许的 agent,每份子凭证仍绑定具体 actor;对同一动作续发不能更换 actor 或放宽能力。不同任务可以并行,同一动作由原 ledger/claim 互斥。跨 actor 的接管需要正式 Task/谱系授权,不能靠更换 nonce 隐式接管。
新字段 `SignedReleasePlanApproval.authorization_binding` 存在时纳入签名载荷。旧直接审批不包含该字段,旧 canonical payload 保持原样。持续授权模式的存储续发只允许时效字段、nonce 和签名更新;签名身份、Owner、actor、父授权、候选和 ReleasePlan 等执行绑定必须一致。正常续发不重置 Saga、不清空 ledger、不重记 Mission/Run 起点。
组合接口启用持续授权模式后,所有实际执行入口均要求父授权绑定,缓存的旧式直接审批不能绕过 broker 经服务入口执行。未启用该模式的既有直接审批流程继续兼容。底层控制器分别要求当前根授权和当前子凭证检查;仅注入其中一个不能放行绑定凭证。
回滚也通过原 CapabilityLedger 领取稳定动作执行权。多个进程恢复同一个 `ROLLBACK_PENDING` 时不会直接重复调用回滚 adapter;结果未知时必须先通过可信只读 probe 对账,没有 probe 时保持阻断。回滚权限、当前根授权与最新子凭证检查仍然适用。
## 失败分流与恢复
| 观察到的状态 | 自动处理 |
|---|---|
| 当前根授权有效,子凭证即将/已经过期,effect 已知 | 在原授权范围内 fresh 签发、原存储 CAS 接替;不生成追加 Owner 通知。 |
| reader/signer 暂不可用或授权状态未知 | `WAITING_AUTHORITY`,保留现有任务与恢复状态;不把技术故障包装为 Owner 决策。 |
| 根授权撤销、取消、关闭或可信验收已完成 | 拒绝后续副作用,仍可读取状态;旧子凭证即使未到期也不覆盖根授权终止。 |
| 版本、仓库、信任根、生产边界或能力确实越界 | 拒绝相关副作用,交给已有 Owner 边界分流。不能自动扩大父授权。 |
| 新候选摘要变化但谱系及稳定目标范围符合授权 | 派生新的精确候选凭证;不沿用旧候选签名。 |
| effect 为 UNKNOWN | 禁止靠续发重放动作;先通过正式只读对账获得可信结果。 |
| 新旧 agent/进程同时续发或领取同一动作 | OS 文件锁与 expected-fingerprint CAS 序列化更新;动作键跨凭证轮换保持稳定,已执行结果仍幂等。 |
凭证过期不应成为 API 进程能否启动的条件。本补丁修复的是本地发布服务 `serve_shadow` 初次轮询异常处理:使用与后续轮询相同的 fail-closed 状态记录,保持 GET 诊断可用。这不等于已修复或验证历史服务器 `/v2/` 冷启动故障;该服务器的实际启动接线仍需单独核查。
## 凭证命名空间与上线条件
本补丁提供可运行的发布审批派生与续发组合,不把所有凭证合并到一个签名器。TaskPrincipal/WriterLease 沿用其已有短租约、续租和 claim 机制;promotion epoch、source admission/source-publication policy 的正式签发入口及撤销规则由其 authority 负责。不会把发布审批签名当作这些命名空间的凭据,也没有修改它们的全局 TTL。后续主机组合应把同一已验证业务授权传给各自正式 issuer,并分别验收。
主机接入必须提供:
1. 具有签名验证、当前撤销/关闭读取能力的 authority reader,以及可验证谱系、actor 与未知 effect 对账状态的 scope reader。数据类类型校验不替代密码学验证。
2. 与现有发布 `ApprovalKeyRing` 一致的已登记签名方;这里不创建私钥、不自动登记信任根、不使用 Task writer 签名冒充发布审批。
3. 执行者与任务/租约身份的正式绑定,以及外部写入端的授权代次、CAS/fence。进程内“检查后再调用”仍存在最后一次检查后的撤销竞态,只有实际写入端可以原子拒绝旧代次。
4. 可信 Production Acceptance 事件写入原 authority 生命周期并使 reader 返回关闭;关闭后回滚需要仍有效且明确覆盖回滚的授权,不能复用已结束的业务授权。
5. 本地文件锁只覆盖共享同一锁文件的进程;多主机需原正式资源锁/CAS,不能将 Windows/POSIX 本地并发测试等同于分布式互斥证据。
上述主机条件缺失时保持 fail closed。本地补丁交付完成与生产无人值守验收是两个不同状态。
@@ -15,7 +15,13 @@ const Platform = (() => {
approvals: [],
audit: [],
policy: null,
eventSource: null
gitopsEventSource: null,
taskEventTransport: null,
taskProjection: null,
taskCards: new Map(),
taskCardNodes: new Map(),
refreshGeneration: 0,
taskProjectionGeneration: 0
};
const ENDPOINTS = {
@@ -36,7 +42,10 @@ const Platform = (() => {
runtime: 'run',
rollback: 'rollback-request',
tag: 'tag-request',
events: 'events/gitops'
events: 'events/gitops',
taskOverview: 'admin/ai-evolution/tasks/overview',
taskEvents: 'admin/ai-evolution/tasks/events',
taskEventPoll: 'admin/ai-evolution/tasks/events/poll'
};
const DEFAULT_POLICY = {
@@ -160,9 +169,11 @@ const Platform = (() => {
await loadPolicy();
applyRbac();
await refreshAll();
await refreshTaskProjection();
await refreshApprovals();
await refreshAudit();
connectEvents();
connectTaskEvents();
}
async function loadIdentity() {
@@ -191,6 +202,7 @@ const Platform = (() => {
}
async function refreshAll() {
const refreshGeneration = ++state.refreshGeneration;
const tasks = [
['baseline', ENDPOINTS.base],
['versions', ENDPOINTS.versions],
@@ -198,10 +210,14 @@ const Platform = (() => {
['patches', ENDPOINTS.patch],
['models', ENDPOINTS.model]
];
const next = {};
for (const [key, endpoint] of tasks) {
try { state[key] = await api(endpoint, 'GET', undefined, { allowError: true }); }
catch (e) { state[key] = { error: e.message, response: e.response }; }
try { next[key] = await api(endpoint, 'GET', undefined, { allowError: true }); }
catch (e) { next[key] = { error: e.message, response: e.response }; }
}
// A reply from a superseded refresh may arrive after a newer event.
if (refreshGeneration !== state.refreshGeneration) return;
Object.assign(state, next);
put('baseline-output', state.baseline);
put('versions-output', state.versions);
put('runtime-output', state.runtime);
@@ -212,6 +228,94 @@ const Platform = (() => {
audit('dashboard.refresh', 'all', 'ok');
}
async function refreshTaskProjection() {
const generation = ++state.taskProjectionGeneration;
let projection;
try { projection = await api(ENDPOINTS.taskOverview, 'GET', undefined, { allowError: true }); }
catch (error) { projection = { error: error.message, response: error.response }; }
if (generation !== state.taskProjectionGeneration) return null;
if (!projection || !Array.isArray(projection.tasks)) {
setText('task-events-chip', '任务事件:STALE');
put('task-projection-output', { status: 'STALE', detail: projection });
return null;
}
state.taskProjection = projection;
const visible = new Set(projection.tasks.map(task => task.task_id));
for (const [id, nodes] of state.taskCardNodes) {
if (!visible.has(id)) {
nodes.container.remove();
state.taskCardNodes.delete(id);
state.taskCards.delete(id);
}
}
projection.tasks.forEach(updateTaskCard);
renderTaskSummary();
return projection;
}
function updateTaskCard(task) {
if (!task || typeof task.task_id !== 'string' || !task.task_id) return false;
const previous = state.taskCards.get(task.task_id);
const revision = task.revision;
if (previous && Number.isInteger(previous.revision) &&
(!Number.isInteger(revision) || revision < previous.revision)) return false;
state.taskCards.set(task.task_id, task);
const target = el('task-projection-cards');
if (!target) return true;
let nodes = state.taskCardNodes.get(task.task_id);
if (!nodes) {
const container = document.createElement('details');
const summary = document.createElement('summary');
const body = document.createElement('pre');
container.appendChild(summary);
container.appendChild(body);
target.appendChild(container);
nodes = { container, summary, body };
state.taskCardNodes.set(task.task_id, nodes);
}
nodes.summary.textContent = `${task.title || task.task_id} · ${task.status || 'unknown'}`;
// Only display explicitly public authority metadata. Keep the shared
// credential sanitizer intact instead of exempting arbitrary authorization.
const authority = task.authorization || {};
const display = { ...task };
delete display.authorization;
display.authority = {
owner_gate_required: authority.owner_gate_required,
owner_gate_status: authority.owner_gate_status,
capabilities: authority.capabilities,
decision: authority.decision
};
const text = safeStringify(display);
if (nodes.body.textContent !== text) {
const scroll = nodes.body.scrollTop;
nodes.body.textContent = text;
nodes.body.scrollTop = scroll;
}
return true;
}
function renderTaskSummary() {
const tasks = Array.from(state.taskCards.values());
const statuses = {};
tasks.forEach(task => {
const status = task.status || 'unknown';
statuses[status] = (statuses[status] || 0) + 1;
});
if (state.taskProjection) state.taskProjection.tasks = tasks;
put('task-projection-output', { total: tasks.length, statuses });
}
function applyTaskEvent(payload) {
if (!payload || !payload.task || payload.task.task_id !== payload.entity_id) return;
if (!updateTaskCard(payload.task)) return;
// Invalidate an older whole-snapshot request. Event cards come from the
// authorized domain snapshot, so no full dashboard HTTP refresh is needed.
state.taskProjectionGeneration += 1;
if (Array.isArray(payload.related_tasks)) payload.related_tasks.forEach(updateTaskCard);
if (state.taskProjection && payload.cursor) state.taskProjection.snapshot_cursor = payload.cursor;
renderTaskSummary();
}
function updateMetrics() {
const runtimeStatus = state.runtime && state.runtime.status || 'unknown';
const version = state.baseline && (state.baseline.active_version || state.baseline.version) || '-';
@@ -372,15 +476,45 @@ const Platform = (() => {
function connectEvents() {
if (!window.EventSource) return;
if (state.gitopsEventSource) state.gitopsEventSource.close();
try {
state.eventSource = new EventSource(`/api/${ENDPOINTS.events}?tenant=${encodeURIComponent(state.tenant)}`);
state.eventSource.onmessage = (event) => {
const payload = JSON.parse(event.data);
state.gitopsEventSource = new EventSource(`/api/${ENDPOINTS.events}?tenant=${encodeURIComponent(state.tenant)}`);
state.gitopsEventSource.onmessage = event => {
let payload;
try { payload = JSON.parse(event.data); } catch (_) { return; }
audit('event.gitops', payload.type || 'event', 'ok', payload);
if (['approval', 'patch', 'runtime', 'model'].includes(payload.type)) refreshAll();
};
state.eventSource.onerror = () => { if (state.eventSource) state.eventSource.close(); };
} catch (_) { /* server-sent events optional */ }
state.gitopsEventSource.onerror = () => { if (state.gitopsEventSource) state.gitopsEventSource.close(); };
} catch (_) { /* GitOps server-sent events remain optional */ }
}
function connectTaskEvents() {
if (!window.AiosEventRecovery) return;
if (state.taskEventTransport) state.taskEventTransport.stop();
const snapshotCursor = state.taskProjection && state.taskProjection.snapshot_cursor;
state.taskEventTransport = new window.AiosEventRecovery.EventRecovery({
eventsUrl: `/api/${ENDPOINTS.taskEvents}`,
pollUrl: `/api/${ENDPOINTS.taskEventPoll}`,
cursor: typeof snapshotCursor === 'string' ? snapshotCursor : null,
EventSource: window.EventSource,
fetch: window.fetch.bind(window),
onState: status => {
setText('task-events-chip', `task events: ${status.toLowerCase()}`);
audit('event.task-center.transport', 'task-center', status);
},
onCursorReset: async () => {
const snapshot = await refreshTaskProjection();
if (!snapshot) throw new Error('task snapshot unavailable');
const cursor = snapshot.snapshot_cursor;
return typeof cursor === 'string' ? cursor : null;
},
onEvent: payload => {
audit('event.task-center', payload.type || 'event', 'ok', payload);
applyTaskEvent(payload);
}
});
state.taskEventTransport.start();
}
function wireTabs() {
@@ -396,7 +530,7 @@ const Platform = (() => {
}
function wireForms() {
const refreshBtn = el('refresh-all'); if (refreshBtn) refreshBtn.onclick = refreshAll;
const refreshBtn = el('refresh-all'); if (refreshBtn) refreshBtn.onclick = async () => { await refreshAll(); await refreshTaskProjection(); };
const refreshModel = el('refresh-model'); if (refreshModel) refreshModel.onclick = refreshAll;
const refreshApprovalsBtn = el('refresh-approvals'); if (refreshApprovalsBtn) refreshApprovalsBtn.onclick = refreshApprovals;
const clearApprovals = el('clear-local-approvals'); if (clearApprovals) clearApprovals.onclick = () => { state.approvals = []; saveApprovals(); renderApprovals(); audit('approval.local.clear', 'localStorage', 'ok'); };
@@ -0,0 +1,129 @@
/* Resumable SSE client. The same Task Center handler also offers /events/poll. */
(function (root, factory) {
const api = factory();
if (typeof module === 'object' && module.exports) module.exports = api;
root.AiosEventRecovery = api;
})(typeof globalThis !== 'undefined' ? globalThis : this, function () {
class EventRecovery {
constructor(options) {
this.eventsUrl = options.eventsUrl;
this.pollUrl = options.pollUrl;
this.EventSource = options.EventSource;
this.fetch = options.fetch;
this.setTimeout = options.setTimeout || setTimeout;
this.clearTimeout = options.clearTimeout || clearTimeout;
this.onEvent = options.onEvent || (() => {});
this.onState = options.onState || (() => {});
this.onCursorReset = options.onCursorReset || (async () => {});
this.maxRetries = options.maxRetries || 3;
this.pollDelayMs = options.pollDelayMs || 5000;
this.backoffMs = options.backoffMs || 250;
this.cursor = options.cursor || null;
this.retries = 0;
this.stream = null;
this.timer = null;
this.seenEvents = new Set();
this.entityRevisions = new Map();
this.active = false;
this.generation = 0;
}
start() {
this.active = true;
this.generation += 1;
this._connect(this.generation);
}
stop() {
this.active = false;
this.generation += 1;
if (this.timer) this.clearTimeout(this.timer);
this.timer = null;
if (this.stream) this.stream.close();
this.stream = null;
}
_url(base) {
const cursor = this.cursor ? `?cursor=${encodeURIComponent(this.cursor)}` : '';
return `${base}${cursor}`;
}
_connect(generation = this.generation) {
if (!this.active || generation !== this.generation) return;
if (!this.EventSource) return this._poll(generation);
this.onState('CONNECTING');
try {
const stream = new this.EventSource(this._url(this.eventsUrl));
this.stream = stream;
stream.onopen = () => { if (this.active && generation === this.generation && this.stream === stream) this.onState('LIVE'); };
stream.onmessage = (event) => {
if (!this.active || generation !== this.generation || this.stream !== stream) return;
let payload;
try { payload = JSON.parse(event.data); } catch (_) { return; }
if (event.lastEventId && !payload.cursor) payload.cursor = event.lastEventId;
this._accept(payload);
};
stream.onerror = () => {
if (this.stream !== stream) return;
stream.close();
this.stream = null;
this._reconnectOrPoll(generation);
};
} catch (_) { this._reconnectOrPoll(generation); }
}
_reconnectOrPoll(generation = this.generation) {
if (!this.active || generation !== this.generation) return;
this.onState('STALE');
if (this.retries >= this.maxRetries) return this._poll(generation);
const wait = Math.min(this.backoffMs * (2 ** this.retries), 4000);
this.retries += 1;
this.timer = this.setTimeout(() => this._connect(generation), wait);
}
async _poll(generation = this.generation) {
if (!this.active || generation !== this.generation) return;
if (!this.fetch) return;
this.onState('POLLING');
try {
const response = await this.fetch(this._url(this.pollUrl), { credentials: 'same-origin' });
if (!this.active || generation !== this.generation) return;
if (response.status === 410) {
this.cursor = null;
this.seenEvents.clear();
this.entityRevisions.clear();
this.retries = 0;
const snapshotCursor = await this.onCursorReset();
if (!this.active || generation !== this.generation) return;
if (typeof snapshotCursor === 'string' && snapshotCursor) this.cursor = snapshotCursor;
return this._connect(generation);
}
if (!response.ok) throw new Error(`poll failed: ${response.status}`);
const page = await response.json();
if (!this.active || generation !== this.generation) return;
(Array.isArray(page.events) ? page.events : []).forEach(event => this._accept(event));
if (page.next_cursor) this.cursor = page.next_cursor;
} catch (_) {
if (this.active && generation === this.generation) this.onState('STALE');
}
if (this.active && generation === this.generation) this.timer = this.setTimeout(() => this._poll(generation), this.pollDelayMs);
}
_accept(event) {
if (!event || typeof event.event_id !== 'string') return false;
const entity = event.entity_id;
const revision = Number(event.revision);
const previous = this.entityRevisions.get(entity);
if (this.seenEvents.has(event.event_id) || (Number.isFinite(revision) && Number.isFinite(previous) && revision < previous)) return false;
this.seenEvents.add(event.event_id);
if (this.seenEvents.size > 1024) this.seenEvents.delete(this.seenEvents.values().next().value);
if (Number.isFinite(revision)) this.entityRevisions.set(entity, revision);
if (event.cursor) this.cursor = event.cursor;
this.retries = 0;
this.onEvent(event);
return true;
}
}
return { EventRecovery };
});
@@ -17,6 +17,7 @@
<div class="statusbar">
<span id="role-chip" class="chip">role: loading</span>
<span id="runtime-chip" class="chip muted">runtime: loading</span>
<span id="task-events-chip" class="chip muted">task events: loading</span>
<button id="refresh-all" class="primary">刷新全部</button>
</div>
</header>
@@ -45,6 +46,7 @@
<article class="card"><h2>Versions</h2><pre id="versions-output">loading...</pre></article>
<article class="card"><h2>Runtime</h2><pre id="runtime-output">loading...</pre></article>
<article class="card"><h2>Patch Summary</h2><pre id="patch-summary-output">loading...</pre></article>
<article class="card"><h2>任务与执行进度</h2><pre id="task-projection-output">loading...</pre><div id="task-projection-cards"></div></article>
</div>
</section>
@@ -294,6 +296,7 @@
window.__gitopsSurface = { csrfToken: csrfToken, validateBranch: validateBranch, validateWorktree: validateWorktree, stateHeaders: stateHeaders };
})();
</script>
<script src="./event_recovery.js"></script>
<script src="./app.js"></script>
</body>
</html>

Some files were not shown because too many files have changed in this diff Show More