autonomy-smoke-tests autonomy smoke tests PASS (A18-A23 staged-run authorized by Owner; G7 staging checks green)
autonomy-unit-tests R2 290/0 + G7 66/0 unit suites PASS
campaign-approval-gate OWNER APPROVAL — campaign approval gate PASS (Owner-authorized agent)
git-diff-check git diff --check clean (only patch-content artifact)
py-compile py_compile 71/71 PASS
reconciliation-contract reconciliation contract consistent (no drops, ChangeEnvelope verified)
release-state-machine release state machine advances to STAGING (authorized by Owner)
secret-scan secret scan 0 secrets PASS
signer-validator-contract signer-validator contract PASS (Owner-authorized)
R2+R2.1: task_plane lineage/auto-claim/autonomous-next-child/mission/legacy-attachment, task_lineage + hooks/integration/taskplane-adapter, terminal proof, mission authority server, job controller/admission live adapters (53 new + 7 modified). G5 dependency/import closure: tests/conftest.py inject active-runtime root (app namespace) alongside active-runtime/app (legacy aios_* imports); fix 3 real NameErrors in test_autonomous_next_child.py (mod-qualified refs). Working-tree subset 290 passed / 0 failed in supported run; overlay/stub + S3 live-binding pending env.
891 lines
37 KiB
Python
891 lines
37 KiB
Python
"""Read-only four-authority evidence provider for one canonical C04 attachment.
|
|
|
|
This module never creates, claims, resumes, reconciles, or mutates a Task or
|
|
Job. The expired Owner epoch is verified as historical provenance only.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import hashlib
|
|
import json
|
|
import os
|
|
import sqlite3
|
|
import stat
|
|
import time
|
|
import urllib.error
|
|
import urllib.parse
|
|
import urllib.request
|
|
from dataclasses import dataclass
|
|
from datetime import datetime, timezone
|
|
from pathlib import Path
|
|
from typing import Any, Callable, Mapping
|
|
|
|
from app.aios_gitops_job_identity import legacy_task_storage_key, task_storage_key
|
|
from app.aios_single_host_authority import (
|
|
DEFAULT_OWNER_SIGNER_ID,
|
|
OWNER_NAMESPACE,
|
|
RegistryOwnerApprovalVerifier,
|
|
SignerRegistry,
|
|
)
|
|
from app.task_plane.legacy_attachment import (
|
|
ATTACH_EVIDENCE_SCHEMA,
|
|
AuthorityReadbackRef,
|
|
SOURCE_KIND,
|
|
VerifiedLegacyAttachmentEvidence,
|
|
)
|
|
|
|
|
|
class LegacyAttachmentRuntimeError(RuntimeError):
|
|
"""Stable fail-closed readback error."""
|
|
|
|
|
|
def _fail(code: str) -> None:
|
|
raise LegacyAttachmentRuntimeError(code)
|
|
|
|
|
|
def _strict_object(pairs: list[tuple[str, Any]]) -> dict[str, Any]:
|
|
value: dict[str, Any] = {}
|
|
for key, item in pairs:
|
|
if key in value:
|
|
_fail("DUPLICATE_JSON_FIELD")
|
|
value[key] = item
|
|
return value
|
|
|
|
|
|
def _json_object(raw: bytes | str, code: str) -> dict[str, Any]:
|
|
try:
|
|
if isinstance(raw, bytes):
|
|
raw = raw.decode("utf-8")
|
|
value = json.loads(raw, object_pairs_hook=_strict_object)
|
|
except (UnicodeDecodeError, json.JSONDecodeError) as exc:
|
|
raise LegacyAttachmentRuntimeError(code) from exc
|
|
if not isinstance(value, dict):
|
|
_fail(code)
|
|
return value
|
|
|
|
|
|
def _canonical(value: object) -> bytes:
|
|
try:
|
|
return json.dumps(
|
|
value,
|
|
ensure_ascii=False,
|
|
sort_keys=True,
|
|
separators=(",", ":"),
|
|
allow_nan=False,
|
|
).encode("utf-8")
|
|
except (TypeError, ValueError) as exc:
|
|
raise LegacyAttachmentRuntimeError("AUTHORITY_JSON_INVALID") from exc
|
|
|
|
|
|
def _sha(value: object) -> str:
|
|
return hashlib.sha256(_canonical(value)).hexdigest()
|
|
|
|
|
|
def _hex(value: object, code: str) -> str:
|
|
if (
|
|
not isinstance(value, str)
|
|
or len(value) != 64
|
|
or any(ch not in "0123456789abcdef" for ch in value)
|
|
):
|
|
_fail(code)
|
|
return value
|
|
|
|
|
|
def _text(value: object, code: str) -> str:
|
|
if not isinstance(value, str) or not value or value != value.strip():
|
|
_fail(code)
|
|
return value
|
|
|
|
|
|
def _integer(value: object, code: str) -> int:
|
|
if type(value) is not int or value < 0:
|
|
_fail(code)
|
|
return value
|
|
|
|
|
|
def _iso(value: object, code: str) -> str:
|
|
text = _text(value, code)
|
|
try:
|
|
parsed = datetime.fromisoformat(text.replace("Z", "+00:00"))
|
|
except ValueError as exc:
|
|
raise LegacyAttachmentRuntimeError(code) from exc
|
|
if parsed.tzinfo is None or parsed.utcoffset() is None:
|
|
_fail(code)
|
|
return parsed.astimezone(timezone.utc).isoformat().replace("+00:00", "Z")
|
|
|
|
|
|
def _observed(clock: Callable[[], float]) -> str:
|
|
return datetime.fromtimestamp(float(clock()), timezone.utc).isoformat().replace(
|
|
"+00:00", "Z"
|
|
)
|
|
|
|
|
|
def _mapping(value: object, code: str) -> dict[str, Any]:
|
|
if not isinstance(value, Mapping):
|
|
_fail(code)
|
|
return dict(value)
|
|
|
|
|
|
def _secure_file(
|
|
path: Path, *, require_root_owner: bool, writable_authority: bool = False
|
|
) -> os.stat_result:
|
|
try:
|
|
info = path.lstat()
|
|
except OSError as exc:
|
|
raise LegacyAttachmentRuntimeError("AUTHORITY_FILE_UNAVAILABLE") from exc
|
|
if path.is_symlink() or not stat.S_ISREG(info.st_mode):
|
|
_fail("AUTHORITY_FILE_INVALID")
|
|
if os.name == "posix":
|
|
if not writable_authority and stat.S_IMODE(info.st_mode) & 0o022:
|
|
_fail("AUTHORITY_FILE_WRITABLE")
|
|
if require_root_owner and info.st_uid != 0:
|
|
_fail("AUTHORITY_FILE_OWNER_INVALID")
|
|
return info
|
|
|
|
|
|
def _secure_directory(path: Path) -> os.stat_result:
|
|
try:
|
|
info = path.lstat()
|
|
except OSError as exc:
|
|
raise LegacyAttachmentRuntimeError("AUTHORITY_DIRECTORY_UNAVAILABLE") from exc
|
|
if path.is_symlink() or not stat.S_ISDIR(info.st_mode):
|
|
_fail("AUTHORITY_DIRECTORY_INVALID")
|
|
return info
|
|
|
|
|
|
def _stable_read(path: Path, *, require_root_owner: bool, maximum: int = 2_000_000) -> bytes:
|
|
before = _secure_file(path, require_root_owner=require_root_owner)
|
|
if before.st_size > maximum:
|
|
_fail("AUTHORITY_FILE_TOO_LARGE")
|
|
try:
|
|
raw = path.read_bytes()
|
|
except OSError as exc:
|
|
raise LegacyAttachmentRuntimeError("AUTHORITY_FILE_UNAVAILABLE") from exc
|
|
after = _secure_file(path, require_root_owner=require_root_owner)
|
|
if (before.st_dev, before.st_ino, before.st_size, before.st_mtime_ns) != (
|
|
after.st_dev,
|
|
after.st_ino,
|
|
after.st_size,
|
|
after.st_mtime_ns,
|
|
):
|
|
_fail("AUTHORITY_FILE_CHANGED")
|
|
return raw
|
|
|
|
|
|
def _ro_connection(path: Path, *, require_root_owner: bool) -> tuple[sqlite3.Connection, os.stat_result]:
|
|
before = _secure_file(
|
|
path, require_root_owner=require_root_owner, writable_authority=True
|
|
)
|
|
try:
|
|
resolved = path.resolve(strict=True)
|
|
connection = sqlite3.connect(
|
|
resolved.as_uri() + "?mode=ro", uri=True, timeout=5.0
|
|
)
|
|
connection.row_factory = sqlite3.Row
|
|
connection.execute("PRAGMA query_only=ON")
|
|
connection.execute("PRAGMA foreign_keys=ON")
|
|
connection.execute("BEGIN")
|
|
return connection, before
|
|
except (OSError, sqlite3.Error) as exc:
|
|
raise LegacyAttachmentRuntimeError("AUTHORITY_DB_UNAVAILABLE") from exc
|
|
|
|
|
|
def _finish_ro(connection: sqlite3.Connection, path: Path, before: os.stat_result) -> None:
|
|
connection.close()
|
|
after = _secure_file(
|
|
path, require_root_owner=False, writable_authority=True
|
|
)
|
|
if (before.st_dev, before.st_ino) != (after.st_dev, after.st_ino):
|
|
_fail("AUTHORITY_DB_CHANGED")
|
|
|
|
|
|
def _columns(connection: sqlite3.Connection, table: str) -> tuple[str, ...]:
|
|
return tuple(str(row[1]) for row in connection.execute(f"PRAGMA table_info({table})"))
|
|
|
|
|
|
@dataclass(frozen=True, slots=True)
|
|
class LegacyC04Binding:
|
|
mission_id: str
|
|
root_task_id: str
|
|
anchor_task_id: str
|
|
owner_principal_id: str
|
|
original_acceptance_digest: str
|
|
source_task_id: str
|
|
external_job_id: str
|
|
|
|
def validate(self) -> None:
|
|
for value in (
|
|
self.mission_id,
|
|
self.root_task_id,
|
|
self.anchor_task_id,
|
|
self.owner_principal_id,
|
|
self.source_task_id,
|
|
self.external_job_id,
|
|
):
|
|
_text(value, "C04_BINDING_INVALID")
|
|
_hex(self.original_acceptance_digest, "C04_ACCEPTANCE_DIGEST_INVALID")
|
|
|
|
|
|
@dataclass(frozen=True, slots=True)
|
|
class CredentialFileBearerSupplier:
|
|
"""Read a systemd credential for one call without caching or displaying it."""
|
|
|
|
path: Path
|
|
|
|
def __post_init__(self) -> None:
|
|
if not self.path.is_absolute():
|
|
_fail("AGENT_TASK_CREDENTIAL_PATH_INVALID")
|
|
if os.name == "posix":
|
|
try:
|
|
self.path.resolve(strict=False).relative_to(Path("/run/credentials"))
|
|
except ValueError as exc:
|
|
raise LegacyAttachmentRuntimeError(
|
|
"AGENT_TASK_CREDENTIAL_PATH_INVALID"
|
|
) from exc
|
|
|
|
def __call__(self) -> str:
|
|
raw = _stable_read(self.path, require_root_owner=False, maximum=8192)
|
|
try:
|
|
value = raw.decode("utf-8").strip()
|
|
except UnicodeDecodeError as exc:
|
|
raise LegacyAttachmentRuntimeError(
|
|
"AGENT_TASK_CREDENTIAL_INVALID"
|
|
) from exc
|
|
if len(value) < 32 or len(value) > 4096:
|
|
_fail("AGENT_TASK_CREDENTIAL_INVALID")
|
|
return value
|
|
|
|
|
|
class _NoRedirect(urllib.request.HTTPRedirectHandler):
|
|
def redirect_request(self, req, fp, code, msg, headers, newurl): # noqa: ANN001
|
|
return None
|
|
|
|
|
|
|
|
_AGENT_TASK_COLUMNS = (
|
|
"task_id",
|
|
"goal",
|
|
"instructions",
|
|
"success_criteria_json",
|
|
"required_capabilities_json",
|
|
"constraints_json",
|
|
"metadata_json",
|
|
"deadline",
|
|
"priority",
|
|
"max_attempts",
|
|
"attempt",
|
|
"status",
|
|
"claimed_by",
|
|
"lease_token_hash",
|
|
"lease_expires_at",
|
|
"result_json",
|
|
"result_digest",
|
|
"result_idempotency_key",
|
|
"verification_json",
|
|
"revision",
|
|
"created_at",
|
|
"updated_at",
|
|
)
|
|
|
|
|
|
@dataclass(slots=True)
|
|
class AuthenticatedAgentTaskReader:
|
|
"""Authenticated non-mutating proof + read-only authority-row snapshot.
|
|
|
|
The authenticated HTTP request is intentionally sent to the task events
|
|
endpoint because the historical GET /v1/tasks/{id} path calls the global
|
|
lease-expiry mutator. The task row itself is read from the authoritative
|
|
SQLite database using query_only mode and an exact schema allowlist.
|
|
"""
|
|
|
|
base_url: str
|
|
bearer_supplier: Callable[[], str]
|
|
task_db_path: Path
|
|
timeout_seconds: float = 10.0
|
|
clock: Callable[[], float] = time.time
|
|
require_db_root_owner: bool = False
|
|
|
|
def __post_init__(self) -> None:
|
|
parsed = urllib.parse.urlsplit(self.base_url)
|
|
if (
|
|
parsed.scheme != "http"
|
|
or parsed.hostname not in {"127.0.0.1", "localhost", "::1"}
|
|
or parsed.username is not None
|
|
or parsed.password is not None
|
|
or parsed.query
|
|
or parsed.fragment
|
|
):
|
|
_fail("AGENT_TASK_URL_INVALID")
|
|
if not self.task_db_path.is_absolute():
|
|
_fail("AGENT_TASK_DB_PATH_INVALID")
|
|
self.base_url = self.base_url.rstrip("/")
|
|
|
|
def _authenticated_events(self, task_id: str) -> tuple[list[dict[str, Any]], str]:
|
|
token = self.bearer_supplier()
|
|
if not isinstance(token, str) or len(token.strip()) < 32:
|
|
_fail("AGENT_TASK_CREDENTIAL_INVALID")
|
|
url = (
|
|
self.base_url
|
|
+ "/v1/tasks/"
|
|
+ urllib.parse.quote(task_id, safe="._:-")
|
|
+ "/events"
|
|
)
|
|
request = urllib.request.Request(
|
|
url,
|
|
method="GET",
|
|
headers={
|
|
"Authorization": "Bearer " + token.strip(),
|
|
"Accept": "application/json",
|
|
},
|
|
)
|
|
try:
|
|
with urllib.request.build_opener(_NoRedirect).open(
|
|
request, timeout=self.timeout_seconds
|
|
) as response:
|
|
if response.status != 200 or response.geturl() != url:
|
|
_fail("AGENT_TASK_EVENTS_REJECTED")
|
|
raw = response.read(2_000_001)
|
|
except (urllib.error.URLError, TimeoutError, OSError) as exc:
|
|
raise LegacyAttachmentRuntimeError(
|
|
"AGENT_TASK_EVENTS_UNAVAILABLE"
|
|
) from exc
|
|
finally:
|
|
token = ""
|
|
if len(raw) > 2_000_000:
|
|
_fail("AGENT_TASK_EVENTS_TOO_LARGE")
|
|
try:
|
|
value = json.loads(raw.decode("utf-8"), object_pairs_hook=_strict_object)
|
|
except (UnicodeDecodeError, json.JSONDecodeError, ValueError) as exc:
|
|
raise LegacyAttachmentRuntimeError("AGENT_TASK_EVENTS_INVALID") from exc
|
|
if not isinstance(value, list):
|
|
_fail("AGENT_TASK_EVENTS_INVALID")
|
|
events: list[dict[str, Any]] = []
|
|
for event in value:
|
|
if not isinstance(event, Mapping) or event.get("task_id") != task_id:
|
|
_fail("AGENT_TASK_EVENTS_INVALID")
|
|
events.append(dict(event))
|
|
return events, url
|
|
|
|
@staticmethod
|
|
def _task_projection(row: sqlite3.Row) -> dict[str, Any]:
|
|
try:
|
|
body = {
|
|
"task_id": row["task_id"],
|
|
"goal": row["goal"],
|
|
"instructions": row["instructions"],
|
|
"success_criteria": json.loads(row["success_criteria_json"]),
|
|
"required_capabilities": json.loads(row["required_capabilities_json"]),
|
|
"constraints": json.loads(row["constraints_json"]),
|
|
"metadata": json.loads(row["metadata_json"]),
|
|
"deadline": row["deadline"],
|
|
"priority": row["priority"],
|
|
"max_attempts": row["max_attempts"],
|
|
"attempt": row["attempt"],
|
|
"status": row["status"],
|
|
"claimed_by": row["claimed_by"],
|
|
"lease_expires_at": row["lease_expires_at"],
|
|
"revision": row["revision"],
|
|
"created_at": row["created_at"],
|
|
"updated_at": row["updated_at"],
|
|
"result": json.loads(row["result_json"]) if row["result_json"] else None,
|
|
"verification": (
|
|
json.loads(row["verification_json"])
|
|
if row["verification_json"]
|
|
else None
|
|
),
|
|
}
|
|
except (KeyError, TypeError, json.JSONDecodeError) as exc:
|
|
raise LegacyAttachmentRuntimeError(
|
|
"AGENT_TASK_ROW_INVALID"
|
|
) from exc
|
|
_mapping(body["constraints"], "AGENT_TASK_CONSTRAINTS_INVALID")
|
|
_mapping(body["metadata"], "AGENT_TASK_METADATA_INVALID")
|
|
_integer(body["revision"], "AGENT_TASK_REVISION_INVALID")
|
|
if body["status"] not in {"SUCCEEDED", "FAILED", "EXPIRED", "CANCELLED"}:
|
|
_fail("AGENT_TASK_NOT_TERMINAL")
|
|
return body
|
|
|
|
def read(self, task_id: str) -> dict[str, Any]:
|
|
events, events_url = self._authenticated_events(task_id)
|
|
connection, before = _ro_connection(
|
|
self.task_db_path, require_root_owner=self.require_db_root_owner
|
|
)
|
|
try:
|
|
if _columns(connection, "tasks") != _AGENT_TASK_COLUMNS:
|
|
_fail("AGENT_TASK_DB_SCHEMA_INVALID")
|
|
row = connection.execute(
|
|
"SELECT * FROM tasks WHERE task_id=? LIMIT 1", (task_id,)
|
|
).fetchone()
|
|
if row is None:
|
|
_fail("AGENT_TASK_NOT_FOUND")
|
|
body = self._task_projection(row)
|
|
finally:
|
|
_finish_ro(connection, self.task_db_path, before)
|
|
event_digest = _sha(events)
|
|
return {
|
|
"body": body,
|
|
"observed_at": _observed(self.clock),
|
|
"readback_ref": (
|
|
events_url
|
|
+ "#authenticated-events-sha256="
|
|
+ event_digest
|
|
+ ";task-db-revision="
|
|
+ str(body["revision"])
|
|
),
|
|
"canonical_sha256": _sha(body),
|
|
"authenticated_events_sha256": event_digest,
|
|
}
|
|
|
|
|
|
_ADMISSION_COLUMNS = {
|
|
"task_admission_envelopes": (
|
|
"idempotency_key", "envelope_id", "request_digest", "envelope_json", "status", "created_at"
|
|
),
|
|
"task_admission_grants": (
|
|
"envelope_id", "grant_id", "grant_digest", "grant_json", "authorization_digest", "created_at"
|
|
),
|
|
"task_admission_receipts": ("envelope_id", "task_id", "receipt_json", "created_at"),
|
|
}
|
|
|
|
|
|
@dataclass(slots=True)
|
|
class AdmissionEpochRegistryReader:
|
|
db_path: Path
|
|
epoch_root: Path
|
|
revocation_file: Path
|
|
registry_path: Path
|
|
require_db_root_owner: bool = False
|
|
require_static_root_owner: bool = True
|
|
clock: Callable[[], float] = time.time
|
|
|
|
def __post_init__(self) -> None:
|
|
for path in (self.db_path, self.epoch_root, self.revocation_file, self.registry_path):
|
|
if not path.is_absolute():
|
|
_fail("AUTHORITY_PATH_NOT_ABSOLUTE")
|
|
|
|
def read(self, task_id: str) -> dict[str, Any]:
|
|
connection, before = _ro_connection(
|
|
self.db_path, require_root_owner=self.require_db_root_owner
|
|
)
|
|
try:
|
|
if any(_columns(connection, table) != columns for table, columns in _ADMISSION_COLUMNS.items()):
|
|
_fail("ADMISSION_SCHEMA_INVALID")
|
|
rows = connection.execute(
|
|
"SELECT e.*,g.grant_id,g.grant_digest,g.grant_json,g.authorization_digest,"
|
|
"g.created_at AS grant_created_at,r.task_id,r.receipt_json,"
|
|
"r.created_at AS receipt_created_at FROM task_admission_receipts r "
|
|
"JOIN task_admission_envelopes e ON e.envelope_id=r.envelope_id "
|
|
"JOIN task_admission_grants g ON g.envelope_id=r.envelope_id WHERE r.task_id=?",
|
|
(task_id,),
|
|
).fetchall()
|
|
if len(rows) != 1:
|
|
_fail("ADMISSION_READBACK_AMBIGUOUS")
|
|
row = dict(rows[0])
|
|
except sqlite3.Error as exc:
|
|
raise LegacyAttachmentRuntimeError("ADMISSION_READBACK_FAILED") from exc
|
|
finally:
|
|
_finish_ro(connection, self.db_path, before)
|
|
|
|
envelope = _json_object(row["envelope_json"], "ADMISSION_ENVELOPE_INVALID")
|
|
grant = _json_object(row["grant_json"], "ADMISSION_GRANT_INVALID")
|
|
receipt = _json_object(row["receipt_json"], "ADMISSION_RECEIPT_INVALID")
|
|
envelope_id = _text(row["envelope_id"], "ADMISSION_ENVELOPE_INVALID")
|
|
grant_id = _text(row["grant_id"], "ADMISSION_GRANT_INVALID")
|
|
epoch_id = _text(grant.get("parent_epoch_id"), "ADMISSION_EPOCH_ID_INVALID")
|
|
if (
|
|
row.get("status") != "ADMITTED"
|
|
or envelope.get("schema") != "aios.task-admission-envelope.v1"
|
|
or grant.get("schema") != "aios.task-admission-grant.v1"
|
|
or receipt.get("schema") != "aios.task-admission-receipt.v1"
|
|
or envelope.get("envelope_id") != envelope_id
|
|
or grant.get("envelope_id") != envelope_id
|
|
or receipt.get("envelope_id") != envelope_id
|
|
or envelope.get("task_id") != task_id
|
|
or grant.get("task_id") != task_id
|
|
or receipt.get("task_id") != task_id
|
|
or grant.get("grant_id") != grant_id
|
|
or receipt.get("grant_id") != grant_id
|
|
or grant.get("authorization_kind") != "delegation_epoch"
|
|
or receipt.get("authorization_kind") != "delegation_epoch"
|
|
or receipt.get("parent_epoch_id") != epoch_id
|
|
):
|
|
_fail("ADMISSION_BINDING_INVALID")
|
|
request_value = {
|
|
key: envelope.get(key)
|
|
for key in (
|
|
"task_id", "repository", "authority_base_oid", "target_branch",
|
|
"generation", "execution_digest", "requested_capabilities",
|
|
"file_allowlist", "risk", "trust_domain", "policy_digest",
|
|
)
|
|
}
|
|
if envelope.get("request_digest") != _sha(request_value) or row.get("request_digest") != _sha(request_value):
|
|
_fail("ADMISSION_REQUEST_DIGEST_INVALID")
|
|
grant_digest = _sha(grant)
|
|
if row.get("grant_digest") != grant_digest or receipt.get("grant_digest_sha256") != grant_digest:
|
|
_fail("ADMISSION_GRANT_DIGEST_INVALID")
|
|
auth_value = {
|
|
"envelope_id": envelope_id,
|
|
"authorization_kind": "delegation_epoch",
|
|
"delegation_epoch_id": epoch_id,
|
|
}
|
|
if row.get("authorization_digest") != _sha(auth_value):
|
|
_fail("ADMISSION_AUTHORIZATION_DIGEST_INVALID")
|
|
linked = (
|
|
"repository", "authority_base_oid", "target_branch", "generation",
|
|
"execution_digest", "policy_digest", "trust_domain", "risk",
|
|
)
|
|
if any(grant.get(key) != envelope.get(key) for key in linked):
|
|
_fail("ADMISSION_GRANT_SCOPE_INVALID")
|
|
if grant.get("capabilities") != envelope.get("requested_capabilities") or grant.get("file_allowlist") != envelope.get("file_allowlist"):
|
|
_fail("ADMISSION_GRANT_SCOPE_INVALID")
|
|
if receipt.get("execution_digest") != grant.get("execution_digest"):
|
|
_fail("ADMISSION_RECEIPT_SCOPE_INVALID")
|
|
|
|
_secure_directory(self.epoch_root)
|
|
canonical_path = self.epoch_root / "owner-delegation-epoch.canonical.json"
|
|
signature_path = self.epoch_root / "owner-delegation-epoch.sig"
|
|
key_path = self.epoch_root / "owner-delegation-epoch.key-id"
|
|
canonical = _stable_read(canonical_path, require_root_owner=self.require_static_root_owner)
|
|
epoch = _json_object(canonical, "OWNER_EPOCH_INVALID")
|
|
if _canonical(epoch) != canonical:
|
|
_fail("OWNER_EPOCH_NOT_CANONICAL")
|
|
signature = _stable_read(signature_path, require_root_owner=self.require_static_root_owner).decode("utf-8")
|
|
key_id = _stable_read(key_path, require_root_owner=self.require_static_root_owner).decode("utf-8").strip()
|
|
registry_raw = _stable_read(self.registry_path, require_root_owner=self.require_static_root_owner)
|
|
registry_json = _json_object(registry_raw, "OWNER_REGISTRY_INVALID")
|
|
revocations = _json_object(
|
|
_stable_read(self.revocation_file, require_root_owner=self.require_static_root_owner),
|
|
"OWNER_REVOCATIONS_INVALID",
|
|
)
|
|
if (
|
|
set(revocations) != {"schema", "revoked_epoch_ids"}
|
|
or revocations.get("schema") != "aios.owner-delegation-revocations.v1"
|
|
or not isinstance(revocations.get("revoked_epoch_ids"), list)
|
|
or epoch_id in revocations["revoked_epoch_ids"]
|
|
):
|
|
_fail("OWNER_EPOCH_REVOKED")
|
|
try:
|
|
registry = SignerRegistry(
|
|
self.registry_path, require_root_owner=self.require_static_root_owner
|
|
)
|
|
record = registry.require(DEFAULT_OWNER_SIGNER_ID, role="OWNER_AUTHORITY")
|
|
verified = RegistryOwnerApprovalVerifier(
|
|
self.registry_path,
|
|
require_root_owner=self.require_static_root_owner,
|
|
signer_id=DEFAULT_OWNER_SIGNER_ID,
|
|
).verify(
|
|
namespace=OWNER_NAMESPACE,
|
|
payload=canonical,
|
|
signature=signature,
|
|
key_id=key_id,
|
|
)
|
|
except Exception as exc:
|
|
raise LegacyAttachmentRuntimeError("OWNER_REGISTRY_VERIFICATION_FAILED") from exc
|
|
now = int(self.clock())
|
|
provenance_checks = {
|
|
"signature": verified is True,
|
|
"fingerprint": record.fingerprint == key_id,
|
|
"schema": epoch.get("schema") == "aios.owner-delegation-epoch.v1",
|
|
"epoch_id": epoch.get("epoch_id") == epoch_id,
|
|
"owner": epoch.get("owner_identity") == grant.get("owner_identity"),
|
|
"repository": epoch.get("repository") == grant.get("repository"),
|
|
"trust_domain": epoch.get("trust_domain") == grant.get("trust_domain"),
|
|
"policy": epoch.get("policy_digest") == grant.get("policy_digest"),
|
|
"revocation_epoch": epoch.get("revocation_epoch") == 0,
|
|
"expires_type": type(epoch.get("expires_at")) is int,
|
|
"not_before_type": type(epoch.get("not_before")) is int,
|
|
}
|
|
failed = [name for name, passed in provenance_checks.items() if not passed]
|
|
if failed:
|
|
_fail("OWNER_EPOCH_PROVENANCE_INVALID:" + ",".join(sorted(failed)))
|
|
if (
|
|
now < epoch["expires_at"]
|
|
or type(epoch.get("issued_at")) is not int
|
|
or epoch["issued_at"] > grant.get("issued_at", 0)
|
|
or grant.get("issued_at", 0) < epoch["not_before"]
|
|
or grant.get("expires_at", 0) > epoch["expires_at"]
|
|
):
|
|
_fail("OWNER_EPOCH_PROVENANCE_INVALID:temporal")
|
|
if not any(str(grant["target_branch"]).startswith(str(prefix)) for prefix in epoch.get("allowed_branch_prefixes", [])):
|
|
_fail("OWNER_EPOCH_SCOPE_INVALID")
|
|
if not set(grant.get("capabilities", [])).issubset(set(epoch.get("allowed_capabilities", []))):
|
|
_fail("OWNER_EPOCH_SCOPE_INVALID")
|
|
risk_rank = {"low": 0, "medium": 1, "high": 2}
|
|
if (
|
|
grant.get("risk") not in risk_rank
|
|
or epoch.get("risk_ceiling") not in risk_rank
|
|
or risk_rank[grant["risk"]] > risk_rank[epoch["risk_ceiling"]]
|
|
):
|
|
_fail("OWNER_EPOCH_SCOPE_INVALID")
|
|
if any(
|
|
not any(path == prefix.rstrip("/") or path.startswith(prefix.rstrip("/") + "/") for prefix in epoch.get("allowed_file_prefixes", []))
|
|
for path in grant.get("file_allowlist", [])
|
|
):
|
|
_fail("OWNER_EPOCH_SCOPE_INVALID")
|
|
|
|
admission_projection = {
|
|
"envelope": envelope,
|
|
"grant": grant,
|
|
"receipt": receipt,
|
|
"request_digest": row["request_digest"],
|
|
"grant_digest": row["grant_digest"],
|
|
"authorization_digest": row["authorization_digest"],
|
|
"envelope_status": row["status"],
|
|
}
|
|
owner_projection = {
|
|
"epoch": epoch,
|
|
"epoch_sha256": hashlib.sha256(canonical).hexdigest(),
|
|
"epoch_key_id": key_id,
|
|
"signature_verified": True,
|
|
"epoch_currently_valid": False,
|
|
"revocations": revocations,
|
|
"registry_schema": registry_json.get("schema"),
|
|
"registry_generation": registry.authority_generation,
|
|
"signer": record.to_dict(),
|
|
}
|
|
observed = _observed(self.clock)
|
|
return {
|
|
"envelope": envelope,
|
|
"grant": grant,
|
|
"receipt": receipt,
|
|
"epoch": epoch,
|
|
"epoch_digest": hashlib.sha256(canonical).hexdigest(),
|
|
"signer_id": record.signer_id,
|
|
"signer_fingerprint": record.fingerprint,
|
|
"registry_generation": registry.authority_generation,
|
|
"observed_at": observed,
|
|
"admission_ref": f"sqlite:{self.db_path}#task={task_id};envelope={envelope_id}",
|
|
"admission_sha256": _sha(admission_projection),
|
|
"registry_ref": f"file:{self.registry_path}#generation={registry.authority_generation};epoch={epoch_id}",
|
|
"registry_sha256": _sha(owner_projection),
|
|
}
|
|
|
|
|
|
_JOB_COLUMNS = {
|
|
"job_aliases": (
|
|
"external_job_id", "internal_job_id", "task_id", "repository", "target_branch",
|
|
"branch_mode", "generation", "commit_idempotency_key", "expected_branch_oid",
|
|
"request_digest", "adopted_legacy", "created_at",
|
|
),
|
|
"jobs": (
|
|
"job_id", "idempotency_key", "campaign_id", "repository", "operation", "branch",
|
|
"expected_branch_oid", "payload_digest", "payload_ref", "payload_size", "state",
|
|
"side_effect_state", "revision", "request_json", "receipt_json", "created_at", "updated_at",
|
|
),
|
|
"idempotency_keys": ("key", "job_id", "binding_json", "created_at"),
|
|
}
|
|
|
|
|
|
@dataclass(slots=True)
|
|
class BridgeJobSQLiteReader:
|
|
jobs_root: Path
|
|
require_root_owner: bool = False
|
|
clock: Callable[[], float] = time.time
|
|
|
|
def __post_init__(self) -> None:
|
|
if not self.jobs_root.is_absolute():
|
|
_fail("BRIDGE_JOB_ROOT_NOT_ABSOLUTE")
|
|
|
|
def read(self, task_id: str, external_job_id: str) -> dict[str, Any]:
|
|
exact_root = self.jobs_root / task_storage_key(task_id)
|
|
legacy_root = self.jobs_root / legacy_task_storage_key(task_id)
|
|
db_path = exact_root / "state.sqlite3"
|
|
if legacy_root != exact_root and legacy_root.exists():
|
|
_fail("BRIDGE_JOB_STORE_AMBIGUOUS")
|
|
_secure_directory(self.jobs_root)
|
|
_secure_directory(exact_root)
|
|
try:
|
|
if db_path.resolve(strict=True).parent != exact_root.resolve(strict=True):
|
|
_fail("BRIDGE_JOB_STORE_INVALID")
|
|
except OSError as exc:
|
|
raise LegacyAttachmentRuntimeError("BRIDGE_JOB_STORE_INVALID") from exc
|
|
connection, before = _ro_connection(
|
|
db_path, require_root_owner=self.require_root_owner
|
|
)
|
|
try:
|
|
if any(_columns(connection, table) != columns for table, columns in _JOB_COLUMNS.items()):
|
|
_fail("BRIDGE_JOB_SCHEMA_INVALID")
|
|
aliases = connection.execute(
|
|
"SELECT * FROM job_aliases WHERE external_job_id=? AND task_id=?",
|
|
(external_job_id, task_id),
|
|
).fetchall()
|
|
if len(aliases) != 1:
|
|
_fail("BRIDGE_JOB_ALIAS_AMBIGUOUS")
|
|
alias = dict(aliases[0])
|
|
job_rows = connection.execute(
|
|
"SELECT * FROM jobs WHERE job_id=?", (alias["internal_job_id"],)
|
|
).fetchall()
|
|
key_rows = connection.execute(
|
|
"SELECT * FROM idempotency_keys WHERE job_id=?", (alias["internal_job_id"],)
|
|
).fetchall()
|
|
if len(job_rows) != 1 or len(key_rows) != 1:
|
|
_fail("BRIDGE_JOB_BINDING_MISSING")
|
|
job, key = dict(job_rows[0]), dict(key_rows[0])
|
|
except sqlite3.Error as exc:
|
|
raise LegacyAttachmentRuntimeError("BRIDGE_JOB_READBACK_FAILED") from exc
|
|
finally:
|
|
_finish_ro(connection, db_path, before)
|
|
request = _json_object(job.get("request_json") or "{}", "BRIDGE_JOB_REQUEST_INVALID")
|
|
binding = _mapping(request.get("binding", request), "BRIDGE_JOB_REQUEST_INVALID")
|
|
key_binding = _json_object(key.get("binding_json") or "{}", "BRIDGE_JOB_KEY_BINDING_INVALID")
|
|
external_commit_key = _text(alias.get("commit_idempotency_key"), "BRIDGE_EXTERNAL_IDEMPOTENCY_INVALID")
|
|
internal_key = _text(job.get("idempotency_key"), "BRIDGE_INTERNAL_IDEMPOTENCY_INVALID")
|
|
if (
|
|
alias.get("external_job_id") != external_job_id
|
|
or alias.get("task_id") != task_id
|
|
or job.get("job_id") != alias.get("internal_job_id")
|
|
or job.get("campaign_id") != task_id
|
|
or key.get("job_id") != job.get("job_id")
|
|
or key.get("key") != internal_key
|
|
or binding.get("task_id", binding.get("campaign_id")) != task_id
|
|
or binding.get("external_job_id") != external_job_id
|
|
or binding.get("idempotency_key") != external_commit_key
|
|
or key_binding.get("task_id", key_binding.get("campaign_id")) != task_id
|
|
or key_binding.get("external_job_id") != external_job_id
|
|
or key_binding.get("idempotency_key") != external_commit_key
|
|
or alias.get("repository") != job.get("repository")
|
|
or alias.get("repository") != binding.get("repository")
|
|
or alias.get("target_branch") != job.get("branch")
|
|
or alias.get("target_branch") != binding.get("branch", binding.get("target_branch"))
|
|
):
|
|
_fail("BRIDGE_JOB_BINDING_INVALID")
|
|
try:
|
|
if int(alias.get("generation")) != int(binding.get("generation")):
|
|
_fail("BRIDGE_JOB_BINDING_INVALID")
|
|
except (TypeError, ValueError) as exc:
|
|
raise LegacyAttachmentRuntimeError("BRIDGE_JOB_BINDING_INVALID") from exc
|
|
projection = {
|
|
"alias": alias,
|
|
"job": job,
|
|
"idempotency": key,
|
|
"external_commit_idempotency_key": external_commit_key,
|
|
"internal_idempotency_key": internal_key,
|
|
}
|
|
return {
|
|
**projection,
|
|
"observed_at": _observed(self.clock),
|
|
"readback_ref": f"sqlite:{db_path}#external={external_job_id};internal={job['job_id']}",
|
|
"canonical_sha256": _sha(projection),
|
|
}
|
|
|
|
|
|
@dataclass(slots=True)
|
|
class LegacyAttachmentRuntimeProvider:
|
|
binding: LegacyC04Binding
|
|
agent_task: AuthenticatedAgentTaskReader
|
|
admission: AdmissionEpochRegistryReader
|
|
bridge_job: BridgeJobSQLiteReader
|
|
|
|
def __post_init__(self) -> None:
|
|
self.binding.validate()
|
|
|
|
def read_verified(
|
|
self,
|
|
*,
|
|
mission_id: str,
|
|
root_task_id: str,
|
|
anchor_task_id: str,
|
|
source_kind: str,
|
|
source_task_id: str,
|
|
external_job_id: str,
|
|
original_acceptance_digest: str,
|
|
) -> VerifiedLegacyAttachmentEvidence:
|
|
expected = self.binding
|
|
if (
|
|
mission_id != expected.mission_id
|
|
or root_task_id != expected.root_task_id
|
|
or anchor_task_id != expected.anchor_task_id
|
|
or source_kind != SOURCE_KIND
|
|
or source_task_id != expected.source_task_id
|
|
or external_job_id != expected.external_job_id
|
|
or original_acceptance_digest != expected.original_acceptance_digest
|
|
):
|
|
_fail("C04_AUTHORITY_BINDING_MISMATCH")
|
|
|
|
task_read = self.agent_task.read(source_task_id)
|
|
admission_read = self.admission.read(source_task_id)
|
|
job_read = self.bridge_job.read(source_task_id, external_job_id)
|
|
task = task_read["body"]
|
|
metadata = _mapping(task["metadata"], "AGENT_TASK_METADATA_INVALID")
|
|
constraints = _mapping(task["constraints"], "AGENT_TASK_CONSTRAINTS_INVALID")
|
|
execution = _mapping(metadata.get("execution"), "AGENT_TASK_EXECUTION_INVALID")
|
|
grant = admission_read["grant"]
|
|
envelope = admission_read["envelope"]
|
|
alias, job = job_read["alias"], job_read["job"]
|
|
try:
|
|
immutable_metadata = _json_object(
|
|
constraints.get("immutable_metadata", ""), "AGENT_TASK_IMMUTABLE_METADATA_INVALID"
|
|
)
|
|
except LegacyAttachmentRuntimeError:
|
|
raise
|
|
if (
|
|
immutable_metadata != metadata
|
|
or metadata.get("grant_id") != grant.get("grant_id")
|
|
or metadata.get("grant_digest_sha256") != admission_read["receipt"].get("grant_digest_sha256")
|
|
or metadata.get("execution_digest") != grant.get("execution_digest")
|
|
or execution.get("execution_digest") != grant.get("execution_digest")
|
|
or execution.get("external_job_id") != external_job_id
|
|
or execution.get("task_id") != source_task_id
|
|
or metadata.get("repository") != grant.get("repository")
|
|
or metadata.get("target_branch") != grant.get("target_branch")
|
|
or metadata.get("generation") != grant.get("generation")
|
|
or constraints.get("repository") != grant.get("repository")
|
|
or constraints.get("target_branch") != grant.get("target_branch")
|
|
or constraints.get("generation") != grant.get("generation")
|
|
or envelope.get("task_id") != source_task_id
|
|
or alias.get("repository") != grant.get("repository")
|
|
or alias.get("target_branch") != grant.get("target_branch")
|
|
or alias.get("generation") != grant.get("generation")
|
|
or admission_read["signer_id"] != expected.owner_principal_id
|
|
):
|
|
_fail("FOUR_AUTHORITY_BINDING_INVALID")
|
|
|
|
return VerifiedLegacyAttachmentEvidence(
|
|
schema=ATTACH_EVIDENCE_SCHEMA,
|
|
source_kind=SOURCE_KIND,
|
|
source_task_id=source_task_id,
|
|
source_task_created_at=_iso(task.get("created_at"), "AGENT_TASK_CREATED_AT_INVALID"),
|
|
source_task_status=_text(task.get("status"), "AGENT_TASK_STATUS_INVALID"),
|
|
source_task_revision=_integer(task.get("revision"), "AGENT_TASK_REVISION_INVALID"),
|
|
source_task_attempt=_integer(task.get("attempt"), "AGENT_TASK_ATTEMPT_INVALID"),
|
|
source_task_max_attempts=_integer(task.get("max_attempts"), "AGENT_TASK_MAX_ATTEMPTS_INVALID"),
|
|
external_job_id=external_job_id,
|
|
internal_job_id=_text(job.get("job_id"), "BRIDGE_INTERNAL_JOB_INVALID"),
|
|
internal_idempotency_key=_text(job.get("idempotency_key"), "BRIDGE_INTERNAL_IDEMPOTENCY_INVALID"),
|
|
job_branch=_text(job.get("branch"), "BRIDGE_JOB_BRANCH_INVALID"),
|
|
job_state=_text(job.get("state"), "BRIDGE_JOB_STATE_INVALID"),
|
|
job_revision=_integer(job.get("revision"), "BRIDGE_JOB_REVISION_INVALID"),
|
|
side_effect_state=_text(job.get("side_effect_state"), "BRIDGE_SIDE_EFFECT_INVALID"),
|
|
grant_id=_text(grant.get("grant_id"), "ADMISSION_GRANT_INVALID"),
|
|
grant_digest=_hex(admission_read["receipt"].get("grant_digest_sha256"), "ADMISSION_GRANT_DIGEST_INVALID"),
|
|
execution_digest=_hex(grant.get("execution_digest"), "ADMISSION_EXECUTION_DIGEST_INVALID"),
|
|
epoch_id=_text(admission_read["epoch"].get("epoch_id"), "OWNER_EPOCH_ID_INVALID"),
|
|
epoch_digest=_hex(admission_read["epoch_digest"], "OWNER_EPOCH_DIGEST_INVALID"),
|
|
historical_signer_id=admission_read["signer_id"],
|
|
historical_signer_fingerprint=admission_read["signer_fingerprint"],
|
|
registry_generation=_integer(admission_read["registry_generation"], "OWNER_REGISTRY_GENERATION_INVALID"),
|
|
signature_verified=True,
|
|
epoch_currently_valid=False,
|
|
readbacks=(
|
|
AuthorityReadbackRef(
|
|
role="agent_task", authority="aios-agent-task-api",
|
|
readback_ref=task_read["readback_ref"], canonical_sha256=task_read["canonical_sha256"],
|
|
observed_at=task_read["observed_at"], revision=task["revision"],
|
|
),
|
|
AuthorityReadbackRef(
|
|
role="admission", authority="aios-task-admission-store",
|
|
readback_ref=admission_read["admission_ref"], canonical_sha256=admission_read["admission_sha256"],
|
|
observed_at=admission_read["observed_at"], revision=None,
|
|
),
|
|
AuthorityReadbackRef(
|
|
role="owner_registry", authority="aios-single-host-owner-authority",
|
|
readback_ref=admission_read["registry_ref"], canonical_sha256=admission_read["registry_sha256"],
|
|
observed_at=admission_read["observed_at"], revision=admission_read["registry_generation"],
|
|
),
|
|
AuthorityReadbackRef(
|
|
role="bridge_job", authority="aios-gitops-job-controller",
|
|
readback_ref=job_read["readback_ref"], canonical_sha256=job_read["canonical_sha256"],
|
|
observed_at=job_read["observed_at"], revision=job["revision"],
|
|
),
|
|
),
|
|
)
|