Compare commits

..
Author SHA1 Message Date
AIOS Governed Continuation 447163fe82 test(security): allow exact historical idempotency fixture
autonomy-smoke-tests phase1-p0 PASS evidence_sha256=fdb08fc46698d7d38b3981df0e27bc5b5bb1da1c57f63fc756a4f08b649a132c
autonomy-unit-tests phase1-p0 PASS evidence_sha256=a7d74280ec71babbb7ff2824c0a8f1ec29db647478f395457979e02164360f9d
campaign-approval-gate phase1-p0 PASS evidence_sha256=d0d77e65dee6f2cf1303a1030473331754ab91982b830684284a562fe5320f0d
git-diff-check phase1-p0 PASS evidence_sha256=5e9628231a64e6faa04570e4817ff4109d791cea2838541994698847987983fb
py-compile phase1-p0 PASS evidence_sha256=47df225f475589b8d9c843ba6d49908c39ce7920890e8b2f355d24818d0558e0
reconciliation-contract phase1-p0 PASS evidence_sha256=24840ca69508034e002e4b12a5525822042bb01b5a6f73a697ca5fdbea30f8ec
release-state-machine phase1-p0 PASS evidence_sha256=805e5a17e6cfb8a2070a34c36e52c89808b89fd4055800dc1fa9b3d58c6c656b
secret-scan phase1-p0 PASS evidence_sha256=eb3dc75806b3af6c1963f1d284cb8b90971694356fdac63d2a09cd08972d8c74
signer-validator-contract phase1-p0 PASS evidence_sha256=da317f9fc7c8ce90b99417c4ec129ed5565c058ac96bb89e56a1a72ce5ab97b3
2026-09-07 18:00:05 +08:00
AIOS Governed Continuation 35f8d189e6 test(governed): normalize bootstrap contract line endings
autonomy-smoke-tests phase1-p0 PASS evidence_sha256=e174976180ecd717a6d24749c91cc53e35a573273de650004fb4a5b7983ccc23
autonomy-unit-tests phase1-p0 PASS evidence_sha256=45c6e4bd0d53ee3f0c64186f16cadbe13adf07b8d4fa24d4624ed90fc08468b2
campaign-approval-gate phase1-p0 PASS evidence_sha256=b182a5af7b5605da5bda1856d6f69774ec19305666fb563b6b4254b85eea167f
git-diff-check phase1-p0 PASS evidence_sha256=6b50f3b275db390e111297b3780a66a97269aa102317dae7584f9987fbd72981
py-compile phase1-p0 PASS evidence_sha256=c82d4aedfb4c752b67467a25ea9f8b9f75f0bea008553abcbcaa78f03751f164
reconciliation-contract phase1-p0 PASS evidence_sha256=cd0071eb74bc0606654eea7159ab835a6ea087a24ce594657a0d7b286206cb59
release-state-machine phase1-p0 PASS evidence_sha256=d4b8b0d8838fc523eaab64dbb4618971b341148f068c1da2395c3955d6fd6e23
secret-scan phase1-p0 FAIL evidence_sha256=47d13c1b7d3c967277c56ce2c62c12bd2b9f221e1e42cbb38461c44574f0919f
signer-validator-contract phase1-p0 PASS evidence_sha256=cdb8009765558b5acd2c3de15bdf5681814e64b2c1587676a5528be5bbca1e95
2026-09-07 17:40:07 +08:00
AIOS Governed Continuation 6d4f79a8c2 fix(governed): make P0 task admission self-contained 2026-09-07 15:22:20 +08:00
AIOS Governed Continuation a7e835b9d3 feat(governed): close P0 task admission bootstrap 2026-09-03 21:40:32 +08:00
AIOS Governed Continuation 91f104ef62 fix(gitops): repair trusted-builder receipt replay framing
autonomy-smoke-tests focused receipt/replay and Task lifecycle smoke: PASS
autonomy-unit-tests full Linux regression: 834 passed, 3 skipped, 154 subtests passed
campaign-approval-gate standing Owner V15 exact Repair Task authorization and formal Task verification: PASS
git-diff-check exact two-file governed diff and whitespace check: PASS
py-compile active-runtime and tests compileall: PASS
reconciliation-contract WriterLease completed replay and governed branch convergence: PASS
release-state-machine full Linux regression includes release state-machine contract: PASS; no release mutation
secret-scan exact changed-file high-signal credential/private-key scan: PASS
signer-validator-contract receipt framing/replay/adoption contract: 18 focused tests PASS; private key non-exportable
2026-08-30 00:05:00 +08:00
owner f8ad8889a4 Merge pull request 'fix(gitops): bind trusted-builder provisioning to final common main' (#71) from agent/v15-post-pr70-builder-release-readiness-20260829 into main
Reviewed-on: #71
Reviewed-by: owner <owner@cnnetmkt.top>
2026-08-29 21:39:20 +08:00
18 changed files with 5285 additions and 54 deletions
+10
View File
@@ -0,0 +1,10 @@
title = "AIOS repository secret scan policy"
[extend]
useDefault = true
[allowlist]
description = "Exact non-secret Task Admission test idempotency fixture in the historical PR commit"
condition = "AND"
paths = ['''tests/test_task_admission_bootstrap_http\.py$''']
regexes = ['''idempotency-1''']
+80 -15
View File
@@ -6,6 +6,12 @@ import hashlib
from pathlib import Path
from typing import Any, Callable, Mapping
from aios_task_admission_bridge_contract import (
TASK_ADMISSION_OPERATION_OPTIONAL_FIELDS,
TASK_ADMISSION_OPERATION_REQUIRED_FIELDS,
TASK_ADMISSION_ROUTES,
)
BRIDGE_SCHEMA_VERSION = "aios.chatgpt-gitops-bridge.v2"
BRIDGE_OPERATIONS = (
@@ -29,15 +35,17 @@ BRIDGE_OPERATIONS = (
"getControlPlaneSnapshot",
)
TASK_ADMISSION_OPERATIONS = tuple(TASK_ADMISSION_OPERATION_REQUIRED_FIELDS)
OPERATION_REQUIRED_FIELDS = {
"createGovernedTask": ("task_id", "repository", "base_oid", "target_branch", "risk", "description", "idempotency_key"),
"projectOnlineAgentExecution": ("task_id", "required_capabilities"),
"acquireWriterLease": ("schema", "task_id", "repository", "branch_mode", "base_oid", "expected_branch_oid", "target_branch", "generation", "lease_seconds"),
"commitFileGoverned": ("task_id", "repository", "expected_branch_oid", "target_branch", "generation", "idempotency_key", "path", "content_base64", "message"),
"commitFilesGoverned": ("task_id", "repository", "expected_branch_oid", "target_branch", "generation", "idempotency_key", "files", "message"),
"getGitOpsJob": ("job_id",),
"resumeGitOpsJob": ("job_id", "expected_revision"),
"reconcileGitOpsJob": ("job_id", "expected_revision"),
"acquireWriterLease": ("schema", "task_id", "job_id", "repository", "branch_mode", "base_oid", "expected_branch_oid", "target_branch", "generation", "idempotency_key", "lease_seconds"),
"commitFileGoverned": ("task_id", "job_id", "repository", "expected_branch_oid", "target_branch", "generation", "idempotency_key", "path", "content_base64", "message"),
"commitFilesGoverned": ("task_id", "job_id", "repository", "expected_branch_oid", "target_branch", "generation", "idempotency_key", "files", "message"),
"getGitOpsJob": ("task_id", "job_id"),
"resumeGitOpsJob": ("task_id", "job_id", "expected_revision"),
"reconcileGitOpsJob": ("task_id", "job_id", "expected_revision"),
"createTagApprovalRequest": ("version", "target_oid", "evidence_closure_oid", "approval_plan_id", "owner_approval", "execute"),
"requestOrCreateTag": ("version", "target_oid", "evidence_closure_oid", "approval_plan_id", "execute"),
"getTagPublicationStatus": ("version",),
@@ -46,6 +54,7 @@ OPERATION_REQUIRED_FIELDS = {
"getReleaseConsistency": ("version", "target_oid", "evidence_closure_oid", "approval_plan_id"),
"runReleaseAcceptance": ("version", "target_oid", "evidence_closure_oid", "approval_plan_id"),
"getControlPlaneSnapshot": (),
**TASK_ADMISSION_OPERATION_REQUIRED_FIELDS,
}
_OPERATION_PATHS = {
@@ -60,19 +69,48 @@ _OPERATION_PATHS = {
"getTagPublicationStatus": "/v2/tag/status",
"getReleaseConsistency": "/v2/release/consistency",
"runReleaseAcceptance": "/v2/release/acceptance",
**{operation: path for path, operation in TASK_ADMISSION_ROUTES.items()},
}
def operation_contracts() -> dict[str, dict[str, object]]:
def _has_complete_task_admission_handlers(
handlers: Mapping[str, Callable[[Mapping[str, Any]], Any]] | None,
) -> bool:
return handlers is not None and all(
callable(handlers.get(operation)) for operation in TASK_ADMISSION_OPERATIONS
)
def operation_contracts(
handlers: Mapping[str, Callable[[Mapping[str, Any]], Any]] | None = None,
) -> dict[str, dict[str, object]]:
"""Return the runtime contract consumed by adapters and schema tooling."""
return {
contracts = {
operation: {
"method": "POST",
"path": _OPERATION_PATHS.get(operation),
"required_fields": list(fields),
}
for operation, fields in OPERATION_REQUIRED_FIELDS.items()
if operation not in TASK_ADMISSION_OPERATIONS
}
if _has_complete_task_admission_handlers(handlers):
contracts.update(
{
operation: {
"method": "GET" if operation == "getTaskAdmissionStatus" else "POST",
"path": _OPERATION_PATHS[operation],
"required_fields": list(
TASK_ADMISSION_OPERATION_REQUIRED_FIELDS[operation]
),
"optional_fields": list(
TASK_ADMISSION_OPERATION_OPTIONAL_FIELDS[operation]
),
}
for operation in TASK_ADMISSION_OPERATIONS
}
)
return contracts
def render_openapi_contract_fragment() -> str:
@@ -85,6 +123,8 @@ def render_openapi_contract_fragment() -> str:
" post:\n"
" operationId: commitFilesGoverned\n"
" summary: Submit one atomic governed commit containing multiple files\n"
" security: [{taskPrincipalAuth: []}]\n"
" x-aios-task-principal: Bridge verifies a fresh aios.task-principal-assertion.v1 and exact task/job/repository/branch/generation/revision/attempt/execution-digest/capability binding; caller JSON cannot provide a principal and missing verification is denied.\n"
" requestBody:\n"
" required: true\n"
" content:\n"
@@ -116,8 +156,12 @@ def render_openapi_contract_fragment() -> str:
" message: {type: string, minLength: 1, maxLength: 200}\n"
" responses:\n"
" '200': {description: Governed commit job and verified branch result}\n"
" '202': {description: Existing external Job alias requires authoritative reconciliation; no replacement Job is created}\n"
" '401': {description: No verified Task principal is available}\n"
" '403': {description: Verified Task principal lacks exact task, external Job, or capability scope}\n"
" '400': {description: Contract or policy rejection}\n"
" '409': {description: Branch CAS or idempotency conflict}\n"
" '423': {description: Narrow P0 Task-principal mutation gate is frozen; assertion is not consumed}\n"
)
@@ -183,20 +227,38 @@ class ControlPlaneReleaseManifest:
class BridgeV2Adapter:
"""Thin operation adapter; all mutating handlers are injected seams."""
def __init__(self, *, runtime_status: Mapping[str, Any], handlers: Mapping[str, Callable[[Mapping[str, Any]], Any]] | None = None) -> None:
self.runtime_status = dict(runtime_status)
def __init__(self, *, runtime_status: Mapping[str, Any] | Callable[[], Mapping[str, Any]], handlers: Mapping[str, Callable[[Mapping[str, Any]], Any]] | None = None) -> None:
self._runtime_status_source = runtime_status if callable(runtime_status) else dict(runtime_status)
self.handlers = dict(handlers or {})
def _runtime_status(self) -> dict[str, Any]:
value = self._runtime_status_source() if callable(self._runtime_status_source) else self._runtime_status_source
if not isinstance(value, Mapping):
raise BridgeV2Error("RUNTIME_STATUS_INVALID")
return dict(value)
def getRuntimeStatus(self) -> dict[str, Any]:
return {"bridge_schema_version": BRIDGE_SCHEMA_VERSION, **self.runtime_status}
return {"bridge_schema_version": BRIDGE_SCHEMA_VERSION, **self._runtime_status()}
def getGovernedWriteCapabilities(self) -> dict[str, Any]:
runtime_status = self._runtime_status()
governed_writes_enabled = runtime_status.get("governed_writes_enabled") is True
return {
"bridge_schema_version": BRIDGE_SCHEMA_VERSION,
"legacy_writes_enabled": False,
"governed_writes_enabled": True,
"operations": list(BRIDGE_OPERATIONS),
"operation_contracts": operation_contracts(),
"governed_writes_enabled": governed_writes_enabled,
"governed_write_blockers": list(
runtime_status.get("governed_write_blockers") or []
),
"operations": [
*BRIDGE_OPERATIONS,
*(
TASK_ADMISSION_OPERATIONS
if _has_complete_task_admission_handlers(self.handlers)
else ()
),
],
"operation_contracts": operation_contracts(self.handlers),
"force_push": False,
"main_write": False,
"tag_write": False,
@@ -213,7 +275,7 @@ class BridgeV2Adapter:
@staticmethod
def legacy_write_disabled() -> dict[str, object]:
return {"error": "LEGACY_WRITE_DISABLED", "replacement": "createGovernedTask", "governed_writes_available": True}
return {"error": "LEGACY_WRITE_DISABLED", "replacement": "createGovernedTask", "governed_writes_available": False}
def _call(self, operation: str, request: Mapping[str, Any]) -> Any:
required = OPERATION_REQUIRED_FIELDS[operation]
@@ -239,5 +301,8 @@ class BridgeV2Adapter:
def executeApprovedProductionRelease(self, request: Mapping[str, Any]) -> Any: return self._call("executeApprovedProductionRelease", request)
def getReleaseConsistency(self, request: Mapping[str, Any]) -> Any: return self._call("getReleaseConsistency", request)
def runReleaseAcceptance(self, request: Mapping[str, Any]) -> Any: return self._call("runReleaseAcceptance", request)
def getTaskAdmissionEnvelope(self, request: Mapping[str, Any]) -> Any: return self._call("getTaskAdmissionEnvelope", request)
def submitTaskAdmissionAuthorization(self, request: Mapping[str, Any]) -> Any: return self._call("submitTaskAdmissionAuthorization", request)
def getTaskAdmissionStatus(self, request: Mapping[str, Any]) -> Any: return self._call("getTaskAdmissionStatus", request)
def getControlPlaneSnapshot(self, request: Mapping[str, Any] | None = None) -> Any: return self._call("getControlPlaneSnapshot", request or {})
@@ -11,19 +11,36 @@ from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
from pathlib import Path
import re
import sqlite3
from typing import Any, Mapping
from typing import Any, Callable, Mapping
from urllib.parse import parse_qsl, urlsplit
from aios_authority_git_executor import AuthorityGitExecutor
from aios_chatgpt_bridge_v2 import BridgeV2Adapter, BridgeV2Error, OPERATION_REQUIRED_FIELDS
from aios_chatgpt_bridge_v2 import (
BridgeV2Adapter,
BridgeV2Error,
OPERATION_REQUIRED_FIELDS,
TASK_ADMISSION_OPERATIONS,
TASK_ADMISSION_OPERATION_OPTIONAL_FIELDS,
TASK_ADMISSION_ROUTES,
)
from aios_task_admission_bootstrap import TaskAdmissionError
from gitea_tag_publication import validate_gitea_api_url
from aios_continuation_executor import AuthorityGitAdapter, ContinuationCommit, ContinuationExecutor, ContinuationPermissions, ContinuationRuntimeBinding, DetachedGitWorkspace, GiteaContinuationRemote, ServerSideCredentialProjection
from aios_continuation_lease import WriterLeaseRequestV2
from aios_control_plane_snapshot import build_control_plane_snapshot
from aios_gitops_job_controller import GitOpsJobController, JobState, JobStore, _RECOVERABLE, sha256_json
from aios_governed_task_bootstrap import BootstrapError, GovernedTaskBootstrap
from aios_governed_task_bootstrap import (
BootstrapError,
GovernedTaskBinding,
GovernedTaskBootstrap,
)
from governed_production_release import build_executor_config
MAX_BODY = 2 * 1024 * 1024
TaskPrincipalVerifier = Callable[
[Mapping[str, Any], Mapping[str, Any]], Mapping[str, Any] | None
]
_PRIVATE_PRINCIPAL = "_verified_task_principal"
ROUTES = {
"/v2/runtime": "getRuntimeStatus",
"/v2/capabilities": "getGovernedWriteCapabilities",
@@ -42,8 +59,21 @@ ROUTES = {
"/v2/release/execute": "executeApprovedProductionRelease",
"/v2/release/consistency": "getReleaseConsistency",
"/v2/release/acceptance": "runReleaseAcceptance",
**TASK_ADMISSION_ROUTES,
}
# Admission is deliberately separate from a TaskPrincipal. A successful
# admission can create only its durable, Owner-authorized Task record; every
# Git-mutating operation still requires a verified TaskPrincipal and this
# independently evaluated runtime gate.
_TASK_PRINCIPAL_MUTATING_OPERATIONS = frozenset({
"acquireWriterLease",
"commitFileGoverned",
"commitFilesGoverned",
"resumeGitOpsJob",
"reconcileGitOpsJob",
})
@dataclass(frozen=True, slots=True)
class RuntimeConfig:
@@ -95,6 +125,34 @@ class GovernedRuntime:
os.environ.setdefault("GIT_ASKPASS", str(askpass)); os.environ.setdefault("GIT_ASKPASS_REQUIRE", "force"); os.environ.setdefault("GIT_TERMINAL_PROMPT", "0"); os.environ.setdefault("GITEA_MACHINE_ACTOR", "aios-autonomy-agent")
def runtime_status(self) -> dict[str, Any]:
verifier = BridgeHandler.principal_verifier
readiness_reader = getattr(verifier, "readiness", None)
if callable(readiness_reader):
try:
principal_readiness = dict(readiness_reader())
except Exception:
principal_readiness = {
"ready": False,
"error": "TASK_PRINCIPAL_SEALED_READINESS_UNAVAILABLE",
}
else:
principal_readiness = {
"ready": False,
"error": "TASK_PRINCIPAL_SEALED_READINESS_UNAVAILABLE",
}
legacy_global_writes_enabled = os.environ.get(
"AIOS_GITOPS_WRITES_ENABLED", "false"
).strip().lower() in {"1", "true", "yes", "on"}
task_principal_writes_enabled = os.environ.get(
"AIOS_P0_TASK_PRINCIPAL_WRITES_ENABLED", "false"
).strip().lower() in {"1", "true", "yes", "on"}
blockers: list[str] = []
if principal_readiness.get("ready") is not True:
blockers.append("TASK_PRINCIPAL_VERIFIER_NOT_READY")
if legacy_global_writes_enabled:
blockers.append("LEGACY_GLOBAL_WRITES_MUST_REMAIN_DISABLED")
if not task_principal_writes_enabled:
blockers.append("TASK_PRINCIPAL_WRITES_DISABLED")
return {
"status": "active",
"health": 200,
@@ -109,9 +167,100 @@ class GovernedRuntime:
"repository": self.config.repository,
"repository_identity": self.config.repository,
"authoritative_git_dir": str(self.config.authoritative_git_dir),
"legacy_global_writes_enabled": False,
"legacy_global_writes_enabled": legacy_global_writes_enabled,
"task_principal_writes_enabled": task_principal_writes_enabled,
"principal_verifier_configured": verifier is not None,
"task_principal_readiness": principal_readiness,
"governed_writes_enabled": not blockers,
"governed_write_blockers": blockers,
}
def project_admitted_task(
self,
grant: Any,
task_result: Mapping[str, Any],
) -> Mapping[str, Any]:
"""Durably bind one verified admission to its exact local Job input.
This hook is called only after the Agent Task API returns an exact
readback. It writes a local binding, not a Job or a lease; later
TaskPrincipal-gated continuation remains responsible for every Git
side effect.
"""
task_id = str(getattr(grant, "task_id", ""))
repository = str(getattr(grant, "repository", ""))
base_oid = str(getattr(grant, "authority_base_oid", ""))
target_branch = str(getattr(grant, "target_branch", ""))
risk = str(getattr(grant, "risk", ""))
generation = int(getattr(grant, "generation", 0))
external_job_id = str(task_result.get("external_job_id") or "")
if (
not task_id
or repository != self.config.repository
or not base_oid
or not target_branch
or generation < 1
or re.fullmatch(r"job-[0-9a-f]{32}", external_job_id) is None
):
raise RuntimeError("TASK_ADMISSION_PROJECTION_COORDINATES_INVALID")
projection_key = "task-admission:" + task_id
description = (
"P0 governed execution projection for officially admitted Agent Task."
)
try:
existing = self.bootstrap.get_binding(task_id)
except BootstrapError as exc:
if str(exc) != "TASK_EXECUTION_PROJECTION_MISSING":
raise
existing = None
if existing is not None:
if (
existing.repository != repository
or existing.base_oid != base_oid
or existing.target_branch != target_branch
or existing.risk != risk
or existing.description != description
or existing.idempotency_key != projection_key
or existing.generation != generation
or existing.job_id != external_job_id
):
raise RuntimeError("TASK_ADMISSION_PROJECTION_BINDING_CONFLICT")
result = existing.response(idempotent=True)
else:
if not self.authority.commit_exists(base_oid):
raise RuntimeError("TASK_ADMISSION_PROJECTION_BASE_NOT_FOUND")
current_branch_oid = self.authority.read_ref(
"refs/heads/" + target_branch
)
if current_branch_oid is None:
branch_mode = "CREATE_NEW"
expected_branch_oid = base_oid
elif current_branch_oid == base_oid:
branch_mode = "CONTINUE_EXISTING"
expected_branch_oid = current_branch_oid
else:
raise RuntimeError("TASK_ADMISSION_PROJECTION_BRANCH_CONFLICT")
binding = GovernedTaskBinding(
task_id=task_id,
repository=repository,
base_oid=base_oid,
target_branch=target_branch,
risk=risk,
description=description,
idempotency_key=projection_key,
expected_branch_oid=expected_branch_oid,
branch_mode=branch_mode,
generation=generation,
job_id=external_job_id,
)
self.bootstrap._write(binding)
readback = self.bootstrap.get_binding(task_id)
if readback != binding:
raise RuntimeError("TASK_ADMISSION_PROJECTION_READBACK_MISMATCH")
result = readback.response()
self.bindings[task_id] = dict(result)
return result
def _job_root(self, task_id: str) -> Path:
return self.config.state_root / "jobs" / _safe(task_id)
@@ -732,7 +881,8 @@ class GovernedRuntime:
def snapshot(self) -> dict[str, Any]:
task = self._snapshot_task()
return build_control_plane_snapshot(target_version=self.config.target_version, runtime=self.runtime_status(), repository={"identity": self.config.repository, "authority_git_dir": str(self.config.authoritative_git_dir), "authority_main_oid": self.authority.read_ref("refs/heads/main")}, production={"baseline_version": "V2R1C7", "target_oid": "c7adece1cc8beffe7c718d53c775898b45d6c7f8", "deployment_state": "ACTIVE"}, task=task, capabilities={"legacy_writes_enabled": False, "governed_writes_enabled": True, "force_push": False, "main_write": False, "tag_write": False, "baseline_write": False, "production_write": False}, credential_broker={"status": "SERVER_SIDE_ONLY", "provider": "FileCredentialProvider"})
runtime = self.runtime_status()
return build_control_plane_snapshot(target_version=self.config.target_version, runtime=runtime, repository={"identity": self.config.repository, "authority_git_dir": str(self.config.authoritative_git_dir), "authority_main_oid": self.authority.read_ref("refs/heads/main")}, production={"baseline_version": "V2R1C7", "target_oid": "c7adece1cc8beffe7c718d53c775898b45d6c7f8", "deployment_state": "ACTIVE"}, task=task, capabilities={"legacy_writes_enabled": False, "governed_writes_enabled": runtime["governed_writes_enabled"], "force_push": False, "main_write": False, "tag_write": False, "baseline_write": False, "production_write": False}, credential_broker={"status": "SERVER_SIDE_ONLY", "provider": "FileCredentialProvider"})
def handlers(self) -> dict[str, Any]:
return {
@@ -760,47 +910,197 @@ def build_governed_handlers(runtime_config: RuntimeConfig | Mapping[str, Any]) -
return GovernedRuntime(runtime_config).handlers()
def build_bridge_adapter(
runtime: GovernedRuntime,
*,
task_admission_handlers: Mapping[str, Callable[[Mapping[str, Any]], Any]] | None = None,
) -> BridgeV2Adapter:
handlers = dict(runtime.handlers())
if task_admission_handlers is not None:
if (
set(task_admission_handlers) != set(TASK_ADMISSION_OPERATIONS)
or any(
not callable(task_admission_handlers.get(operation))
for operation in TASK_ADMISSION_OPERATIONS
)
):
raise ValueError("TASK_ADMISSION_HANDLERS_INCOMPLETE")
handlers.update(task_admission_handlers)
return BridgeV2Adapter(runtime_status=runtime.runtime_status, handlers=handlers)
class _HTTPError(RuntimeError): pass
class BridgeHandler(BaseHTTPRequestHandler):
adapter: BridgeV2Adapter | None = None
principal_verifier: TaskPrincipalVerifier | None = None
def log_message(self, *_args: Any) -> None: return
def _json(self, value: Any, status: int = 200) -> None:
raw = json.dumps(value, sort_keys=True, separators=(",", ":")).encode(); self.send_response(status); self.send_header("Content-Type", "application/json"); self.send_header("Content-Length", str(len(raw))); self.end_headers(); self.wfile.write(raw)
@staticmethod
def _result_status(value: Any) -> int:
status = value.get("http_status") if isinstance(value, Mapping) else None
return status if isinstance(status, int) and 100 <= status <= 599 else 200
def _body(self) -> dict[str, Any]:
try: length = int(self.headers.get("Content-Length", "-1"))
except ValueError as exc: raise _HTTPError("CONTENT_LENGTH_INVALID") from exc
if length < 0 or length > MAX_BODY: raise _HTTPError("REQUEST_TOO_LARGE")
value = json.loads(self.rfile.read(length).decode("utf-8"))
def strict_object(pairs: list[tuple[str, Any]]) -> dict[str, Any]:
value: dict[str, Any] = {}
for key, item in pairs:
if key in value:
raise _HTTPError("JSON_DUPLICATE_FIELD")
value[key] = item
return value
value = json.loads(
self.rfile.read(length).decode("utf-8"),
object_pairs_hook=strict_object,
)
if not isinstance(value, dict): raise _HTTPError("JSON_OBJECT_REQUIRED")
return value
def _allowed(self, operation: str, body: Mapping[str, Any]) -> None:
allowed = set(OPERATION_REQUIRED_FIELDS[operation]) | {"schema", "lease_seconds", "create_target_branch", "worktree_mode", "cas_ref_update_required", "expected_branch_oid", "generation", "branch_mode", "source_lease_epoch"}
if operation in TASK_ADMISSION_OPERATIONS:
allowed = set(OPERATION_REQUIRED_FIELDS[operation]) | set(
TASK_ADMISSION_OPERATION_OPTIONAL_FIELDS[operation]
)
else:
allowed = set(OPERATION_REQUIRED_FIELDS[operation]) | {"schema", "lease_seconds", "create_target_branch", "worktree_mode", "cas_ref_update_required", "expected_branch_oid", "expected_revision", "generation", "branch_mode", "source_lease_epoch"}
if operation == "projectOnlineAgentExecution": allowed.add("execution")
if operation == "commitFilesGoverned": allowed.add("files")
allowed.update({"owner_approval", "execute", "job_id", "historical_lease_head_oid"})
if operation not in TASK_ADMISSION_OPERATIONS:
allowed.update({"owner_approval", "execute", "job_id", "historical_lease_head_oid"})
if "principal" in body or _PRIVATE_PRINCIPAL in body:
raise _HTTPError("CALLER_PRINCIPAL_FORBIDDEN")
if set(body) - allowed: raise _HTTPError("UNKNOWN_FIELDS")
def _verified_request(self, operation: str, body: Mapping[str, Any]) -> dict[str, Any]:
protected = {
"createGovernedTask", "projectOnlineAgentExecution",
"acquireWriterLease", "commitFileGoverned", "commitFilesGoverned",
"getGitOpsJob", "resumeGitOpsJob", "reconcileGitOpsJob",
}
if operation not in protected:
return dict(body)
if operation in _TASK_PRINCIPAL_MUTATING_OPERATIONS:
adapter = self.adapter
if adapter is None:
raise _HTTPError("TASK_PRINCIPAL_WRITES_DISABLED")
try:
runtime = adapter.getRuntimeStatus()
except Exception as exc:
raise _HTTPError("TASK_PRINCIPAL_WRITES_DISABLED") from exc
if (
not isinstance(runtime, Mapping)
or runtime.get("governed_writes_enabled") is not True
or runtime.get("legacy_global_writes_enabled") is not False
or runtime.get("task_principal_writes_enabled") is not True
):
raise _HTTPError("TASK_PRINCIPAL_WRITES_DISABLED")
verifier = type(self).principal_verifier
if verifier is None:
raise _HTTPError("TASK_PRINCIPAL_VERIFIER_UNAVAILABLE")
get_all = getattr(self.headers, "get_all", None)
if callable(get_all):
authorization_values = get_all("Authorization") or []
else:
authorization = self.headers.get("Authorization")
authorization_values = [] if authorization is None else [authorization]
transport = {
"operation": operation,
"client_address": self.client_address[0] if self.client_address else None,
"headers": {
"authorization": authorization_values[0] if len(authorization_values) == 1 else None,
"x-request-id": self.headers.get("X-Request-Id"),
},
}
principal = verifier(transport, dict(body))
if not isinstance(principal, Mapping):
raise _HTTPError("TASK_PRINCIPAL_REJECTED")
return {**body, _PRIVATE_PRINCIPAL: dict(principal)}
def do_GET(self) -> None:
if self.adapter is None: self._json({"error": "RUNTIME_UNAVAILABLE"}, 503); return
if self.path == "/v2/runtime": self._json(self.adapter.getRuntimeStatus()); return
if self.path == "/v2/capabilities": self._json(self.adapter.getGovernedWriteCapabilities()); return
if self.path == "/v2/control-plane-snapshot": self._json(self.adapter.getControlPlaneSnapshot({})); return
parsed = urlsplit(self.path)
if parsed.path == "/v2/task-admission/status":
if not callable(self.adapter.handlers.get("getTaskAdmissionStatus")):
self._json({"error": "NOT_FOUND"}, 404)
return
try:
parameters = parse_qsl(parsed.query, keep_blank_values=True)
if (
len(parameters) != 1
or parameters[0][0] != "envelope_id"
or not parameters[0][1]
):
raise _HTTPError("TASK_ADMISSION_QUERY_INVALID")
result = self.adapter.getTaskAdmissionStatus(
{"envelope_id": parameters[0][1]}
)
self._json(result, self._result_status(result))
except TaskAdmissionError as exc:
self._json({"error": str(exc)}, exc.status)
except _HTTPError as exc:
self._json({"error": str(exc)}, 400)
except (ValueError, BridgeV2Error) as exc:
self._json({"error": str(exc)}, 400)
except Exception as exc:
self._json({"error": str(exc) or type(exc).__name__}, 503)
return
self._json({"error": "NOT_FOUND"}, 404)
def do_POST(self) -> None:
operation = ROUTES.get(self.path)
if operation is None or operation in ("getRuntimeStatus", "getGovernedWriteCapabilities") or self.adapter is None: self._json({"error": "NOT_FOUND"}, 404); return
if operation in TASK_ADMISSION_OPERATIONS and not callable(self.adapter.handlers.get(operation)):
self._json({"error": "NOT_FOUND"}, 404)
return
try:
body = self._body(); self._allowed(operation, body); self._json(getattr(self.adapter, operation)(body))
except _HTTPError as exc: self._json({"error": str(exc)}, 413 if str(exc) == "REQUEST_TOO_LARGE" else 400)
body = self._body(); self._allowed(operation, body)
result = getattr(self.adapter, operation)(self._verified_request(operation, body))
self._json(result, self._result_status(result))
except _HTTPError as exc:
error = str(exc)
if error == "REQUEST_TOO_LARGE":
status = 413
elif error == "TASK_PRINCIPAL_WRITES_DISABLED":
status = 423
else:
status = 401 if error.startswith("TASK_PRINCIPAL_") else 400
self._json({"error": error}, status)
except TaskAdmissionError as exc: self._json({"error": str(exc)}, exc.status)
except (BootstrapError, ValueError, BridgeV2Error) as exc: self._json({"error": str(exc)}, 409 if "CONFLICT" in str(exc) else 400)
except Exception as exc: self._json({"error": str(exc) or type(exc).__name__}, 409 if any(token in str(exc) for token in ("CONFLICT", "EXPECTATION", "MONOTONIC")) else 503)
except Exception as exc:
error = str(exc) or type(exc).__name__
if error.startswith("TASK_PRINCIPAL_"):
status = 403
elif any(token in error for token in ("CONFLICT", "EXPECTATION", "MONOTONIC", "REVISION_CAS")):
status = 409
else:
status = 503
self._json({"error": error}, status)
def serve(adapter: BridgeV2Adapter, host: str = "127.0.0.1", port: int = 18892) -> None:
BridgeHandler.adapter = adapter; ThreadingHTTPServer((host, port), BridgeHandler).serve_forever()
def main() -> None:
def main(
*,
task_admission_handlers: Mapping[str, Callable[[Mapping[str, Any]], Any]] | None = None,
) -> None:
activation_raw = os.environ.get(
"AIOS_TASK_ADMISSION_ACTIVATION_FILE",
"/var/lib/aios-gitops/bridge-v2-staging/task-admission-activation.json",
).strip()
state_root = Path("/var/lib/aios-gitops/bridge-v2-staging")
token_path = Path(os.environ.get("AIOS_GITEA_TOKEN_FILE", "/run/credentials/aios-chatgpt-bridge-v2-staging.service/gitea_token"))
ssh_key_path = Path(os.environ.get("AIOS_GITEA_SSH_KEY_PATH", "/home/aiman/.ssh/id_ed25519_codex_tmp"))
@@ -822,8 +1122,32 @@ def main() -> None:
)),
)
runtime = GovernedRuntime(config)
# A TaskPrincipal verifier is deliberately supplied only by the hosting
# service. Without that independently authenticated verifier, all Git
# mutation requests fail closed while the admission-only routes remain
# available for a configured Owner-approved epoch.
BridgeHandler.principal_verifier = None
if task_admission_handlers is None:
if os.environ.get("AIOS_TASK_ADMISSION_EPOCH_ROOT", "").strip():
from aios_task_admission_live_adapters import (
build_task_admission_handlers_from_environment,
)
task_admission_handlers = build_task_admission_handlers_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_handlers_from_activation_file,
)
task_admission_handlers = build_task_admission_handlers_from_activation_file(
Path(activation_raw),
post_admission=runtime.project_admitted_task,
)
serve(
BridgeV2Adapter(runtime_status=runtime.runtime_status(), handlers=runtime.handlers()),
build_bridge_adapter(
runtime,
task_admission_handlers=task_admission_handlers,
),
port=int(os.environ.get("AIOS_BRIDGE_V2_PORT", "18892")),
)
@@ -0,0 +1,945 @@
"""Fail-closed bootstrap for narrow Owner-delegated Agent Task admission.
This module solves the authorization bootstrap problem without turning the
runtime anchor into a writer and without weakening TaskPrincipal semantics.
Security model:
* A task-admission envelope is created from a server-normalized request.
* The envelope is authorized either by an already-valid OwnerDelegationEpoch
or by an Owner signature verified by an injected trust backend.
* The derived TaskAdmissionGrant can only narrow the parent authorization.
* The grant is handed to the existing Agent Task control plane through an
injected TaskAdmissionPort. This module never writes the Task database.
* TaskPrincipal issuance remains downstream of a real Task claim/TaskLease.
* Exact idempotent replay returns the same admitted Task; conflicting reuse is
rejected.
"""
from __future__ import annotations
import base64
from contextlib import closing, contextmanager
from dataclasses import asdict, dataclass
from datetime import datetime, timezone
import hashlib
import json
from pathlib import Path, PurePosixPath
import re
import secrets
import sqlite3
import threading
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_RECEIPT = "aios.task-admission-receipt.v1"
OID_RE = re.compile(r"^[0-9a-f]{40}$")
TASK_RE = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._:-]{2,179}$")
BRANCH_RE = re.compile(r"^agent/[A-Za-z0-9][A-Za-z0-9._/-]{2,179}$")
IDEMPOTENCY_RE = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._:@/-]{7,255}$")
CAPABILITY_RE = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._:-]{1,127}$")
KEY_ID_RE = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._:@+-]{2,255}$")
PROHIBITED_CAPABILITIES = frozenset(
{
"git.global_write",
"git.main_write",
"git.force_push",
"tag.publish",
"baseline.finalize",
"production.write",
"production.switch",
"runtime_anchor.write",
"shell.arbitrary",
"credential.export",
}
)
class TaskAdmissionError(RuntimeError):
"""Stable fail-closed task-admission error."""
def __init__(
self,
code: str,
status: int = 400,
*,
details: Mapping[str, Any] | None = None,
) -> None:
super().__init__(code)
self.code = code
self.status = status
self.details = dict(details or {})
@dataclass(frozen=True, slots=True)
class TaskAdmissionPolicy:
repository: str = "aios/aios"
trust_domain: str = "aios.owner"
branch_prefix: str = "agent/"
allowed_capabilities: frozenset[str] = frozenset(
{
"governed.task.create",
"agent-task:control",
"task.principal.issue",
"writer.lease",
"governed.write.files",
"git.read",
"git.test",
"governed.write.branch",
"governed.write.file",
"testing",
"verification",
}
)
max_file_count: int = 64
max_task_ttl_seconds: int = 3600
max_envelope_ttl_seconds: int = 900
max_risk: str = "medium"
policy_digest: str = ""
def normalized_policy_digest(self) -> str:
if self.policy_digest:
return self.policy_digest
return sha256_json(
{
"repository": self.repository,
"trust_domain": self.trust_domain,
"branch_prefix": self.branch_prefix,
"allowed_capabilities": sorted(self.allowed_capabilities),
"max_file_count": self.max_file_count,
"max_task_ttl_seconds": self.max_task_ttl_seconds,
"max_envelope_ttl_seconds": self.max_envelope_ttl_seconds,
"max_risk": self.max_risk,
"prohibited_capabilities": sorted(PROHIBITED_CAPABILITIES),
}
)
@dataclass(frozen=True, slots=True)
class OwnerDelegationEpoch:
"""Already-verified durable Owner authorization.
The verifier/backend is responsible for authenticating the epoch before
returning it to this service.
"""
epoch_id: str
owner_identity: str
trust_domain: str
repository: str
allowed_branch_prefixes: tuple[str, ...]
allowed_capabilities: frozenset[str]
allowed_file_prefixes: tuple[str, ...]
risk_ceiling: str
issued_at: int
not_before: int
expires_at: int
revocation_epoch: int
policy_digest: str
schema: str = SCHEMA_EPOCH
def as_public_dict(self) -> dict[str, Any]:
value = asdict(self)
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)
return value
@dataclass(frozen=True, slots=True)
class TaskAdmissionEnvelope:
envelope_id: str
idempotency_key: str
task_id: str
repository: str
authority_base_oid: str
target_branch: str
generation: int
execution_digest: str
requested_capabilities: tuple[str, ...]
file_allowlist: tuple[str, ...]
risk: str
trust_domain: str
policy_digest: str
issued_at: int
not_before: int
expires_at: int
nonce: str
request_digest: str
schema: str = SCHEMA_ENVELOPE
def as_dict(self) -> dict[str, Any]:
value = asdict(self)
value["requested_capabilities"] = list(self.requested_capabilities)
value["file_allowlist"] = list(self.file_allowlist)
return value
@dataclass(frozen=True, slots=True)
class TaskAdmissionGrant:
grant_id: str
envelope_id: str
task_id: str
repository: str
authority_base_oid: str
target_branch: str
generation: int
execution_digest: str
capabilities: tuple[str, ...]
file_allowlist: tuple[str, ...]
risk: str
trust_domain: str
policy_digest: str
authorization_kind: str
owner_identity: str
parent_epoch_id: str | None
issued_at: int
expires_at: int
one_time_use: bool = True
schema: str = SCHEMA_GRANT
def as_dict(self) -> dict[str, Any]:
value = asdict(self)
value["capabilities"] = list(self.capabilities)
value["file_allowlist"] = list(self.file_allowlist)
return value
class OwnerAuthorizationBackend(Protocol):
"""Bridge to existing trusted Owner authorization.
No permissive default implementation is provided.
"""
def verify_delegation_epoch(self, epoch_id: str) -> OwnerDelegationEpoch:
...
def verify_owner_signature(
self,
*,
key_id: str,
canonical_payload: bytes,
signature_b64: str,
) -> str:
"""Return the verified Owner identity or raise."""
...
class TaskAdmissionPort(Protocol):
"""Existing Agent Task control-plane port."""
def admit(self, grant: TaskAdmissionGrant) -> Mapping[str, Any]:
...
def utc_seconds() -> int:
return int(datetime.now(timezone.utc).timestamp())
def canonical_json_bytes(value: Any) -> bytes:
return json.dumps(
value,
ensure_ascii=False,
sort_keys=True,
separators=(",", ":"),
).encode("utf-8")
def sha256_json(value: Any) -> str:
return hashlib.sha256(canonical_json_bytes(value)).hexdigest()
def _normalize_files(raw: Sequence[object], *, maximum: int) -> tuple[str, ...]:
if not isinstance(raw, (list, tuple)):
raise TaskAdmissionError("TASK_ADMISSION_FILE_ALLOWLIST_REJECTED", 403)
if not raw or len(raw) > maximum:
raise TaskAdmissionError("TASK_ADMISSION_FILE_ALLOWLIST_REJECTED", 403)
normalized: list[str] = []
seen: set[str] = set()
for item in raw:
if (
not isinstance(item, str)
or not item
or "\\" in item
or any(ord(char) < 32 or ord(char) == 127 for char in item)
):
raise TaskAdmissionError("TASK_ADMISSION_FILE_PATH_INVALID", 403)
path = PurePosixPath(item)
if path.is_absolute() or ".." in path.parts or "." in path.parts:
raise TaskAdmissionError("TASK_ADMISSION_FILE_PATH_INVALID", 403)
text = path.as_posix()
# Do not normalize a caller supplied path. A signature must bind the
# exact spelling, not a convenient equivalent such as ``a//b``.
if text != item:
raise TaskAdmissionError("TASK_ADMISSION_FILE_PATH_INVALID", 403)
if text.startswith(".git/") or text == ".git":
raise TaskAdmissionError("TASK_ADMISSION_FILE_PATH_INVALID", 403)
if text not in seen:
normalized.append(text)
seen.add(text)
if not normalized:
raise TaskAdmissionError("TASK_ADMISSION_FILE_ALLOWLIST_REJECTED", 403)
return tuple(sorted(normalized))
def _normalize_capabilities(
raw: Sequence[object],
*,
policy: TaskAdmissionPolicy,
) -> tuple[str, ...]:
if not raw:
raise TaskAdmissionError("TASK_ADMISSION_CAPABILITIES_REQUIRED", 403)
values: set[str] = set()
for item in raw:
if not isinstance(item, str) or not CAPABILITY_RE.fullmatch(item):
raise TaskAdmissionError("TASK_ADMISSION_CAPABILITY_INVALID", 403)
if "*" in item:
raise TaskAdmissionError("TASK_ADMISSION_WILDCARD_CAPABILITY_REJECTED", 403)
values.add(item)
if values & PROHIBITED_CAPABILITIES:
raise TaskAdmissionError("TASK_ADMISSION_PROHIBITED_CAPABILITY", 403)
if not values.issubset(policy.allowed_capabilities):
raise TaskAdmissionError("TASK_ADMISSION_CAPABILITY_OUT_OF_POLICY", 403)
return tuple(sorted(values))
def _risk_rank(value: str) -> int:
ranks = {"low": 0, "medium": 1, "high": 2}
try:
return ranks[value]
except KeyError:
raise TaskAdmissionError("TASK_ADMISSION_RISK_INVALID", 403) from None
def _request_for_digest(
*,
task_id: str,
repository: str,
authority_base_oid: str,
target_branch: str,
generation: int,
execution_digest: str,
requested_capabilities: tuple[str, ...],
file_allowlist: tuple[str, ...],
risk: str,
trust_domain: str,
policy_digest: str,
) -> dict[str, Any]:
return {
"task_id": task_id,
"repository": repository,
"authority_base_oid": authority_base_oid,
"target_branch": target_branch,
"generation": generation,
"execution_digest": execution_digest,
"requested_capabilities": list(requested_capabilities),
"file_allowlist": list(file_allowlist),
"risk": risk,
"trust_domain": trust_domain,
"policy_digest": policy_digest,
}
class TaskAdmissionStore:
"""Portable durable state; all replay decisions are transactionally stored."""
def __init__(self, path: Path) -> None:
self.path = Path(path)
self.path.parent.mkdir(parents=True, exist_ok=True)
self.lock_root = Path(str(self.path) + ".locks")
self.lock_root.mkdir(parents=True, exist_ok=True)
self._lock = threading.RLock()
with self._connection() as con:
con.executescript(
"""
PRAGMA journal_mode=WAL;
CREATE TABLE IF NOT EXISTS task_admission_envelopes(
idempotency_key TEXT PRIMARY KEY,
envelope_id TEXT NOT NULL UNIQUE,
request_digest TEXT NOT NULL,
envelope_json TEXT NOT NULL,
status TEXT NOT NULL,
created_at INTEGER NOT NULL
);
CREATE TABLE IF NOT EXISTS task_admission_grants(
envelope_id TEXT PRIMARY KEY,
grant_id TEXT NOT NULL UNIQUE,
grant_digest TEXT NOT NULL,
grant_json TEXT NOT NULL,
authorization_digest TEXT NOT NULL DEFAULT '',
created_at INTEGER NOT NULL
);
CREATE TABLE IF NOT EXISTS task_admission_receipts(
envelope_id TEXT PRIMARY KEY,
task_id TEXT NOT NULL UNIQUE,
receipt_json TEXT NOT NULL,
created_at INTEGER NOT NULL
);
"""
)
columns = {
row["name"] for row in con.execute("PRAGMA table_info(task_admission_grants)")
}
if "authorization_digest" not in columns:
con.execute(
"ALTER TABLE task_admission_grants ADD COLUMN authorization_digest TEXT NOT NULL DEFAULT ''"
)
def _connect(self) -> sqlite3.Connection:
con = sqlite3.connect(self.path, timeout=30, isolation_level=None)
con.row_factory = sqlite3.Row
return con
@contextmanager
def _connection(self):
"""Yield an owned main-store connection and close it deterministically."""
with closing(self._connect()) as con:
try:
yield con
finally:
if con.in_transaction:
con.rollback()
@contextmanager
def admission_lock(self, envelope_id: str, *, timeout_seconds: float = 5.0):
"""Hold a crash-releasing lock for one envelope, outside the main DB."""
lock_name = hashlib.sha256(envelope_id.encode("utf-8")).hexdigest() + ".sqlite3"
lock_path = self.lock_root / lock_name
with closing(sqlite3.connect(lock_path, timeout=timeout_seconds, isolation_level=None)) as con:
try:
con.execute("BEGIN EXCLUSIVE")
except sqlite3.OperationalError as exc:
raise TaskAdmissionError("TASK_ADMISSION_IN_PROGRESS", 503) from exc
try:
yield
con.execute("COMMIT")
finally:
if con.in_transaction:
con.rollback()
def get_by_idempotency(self, key: str) -> dict[str, Any] | None:
with self._connection() as con:
row = con.execute(
"SELECT * FROM task_admission_envelopes WHERE idempotency_key=?",
(key,),
).fetchone()
return dict(row) if row else None
def get_envelope(self, envelope_id: str) -> dict[str, Any] | None:
with self._connection() as con:
row = con.execute(
"SELECT * FROM task_admission_envelopes WHERE envelope_id=?",
(envelope_id,),
).fetchone()
return dict(row) if row else None
def save_envelope(
self,
*,
idempotency_key: str,
envelope_id: str,
request_digest: str,
envelope_json: str,
created_at: int,
) -> dict[str, Any]:
with self._lock, self._connection() as con:
con.execute("BEGIN IMMEDIATE")
row = con.execute(
"SELECT request_digest,envelope_id FROM task_admission_envelopes WHERE idempotency_key=?",
(idempotency_key,),
).fetchone()
if row is not None:
con.execute("ROLLBACK")
if row["request_digest"] != request_digest:
raise TaskAdmissionError("TASK_ADMISSION_IDEMPOTENCY_CONFLICT", 409)
return json.loads(
con.execute(
"SELECT envelope_json FROM task_admission_envelopes WHERE idempotency_key=?",
(idempotency_key,),
).fetchone()["envelope_json"]
)
con.execute(
"""
INSERT INTO task_admission_envelopes(
idempotency_key,envelope_id,request_digest,envelope_json,status,created_at
) VALUES(?,?,?,?,?,?)
""",
(
idempotency_key,
envelope_id,
request_digest,
envelope_json,
"PENDING_AUTHORIZATION",
created_at,
),
)
con.execute("COMMIT")
return json.loads(envelope_json)
def claim_grant(
self,
*,
envelope_id: str,
grant: TaskAdmissionGrant,
authorization_digest: str,
created_at: int,
) -> tuple[TaskAdmissionGrant, bool]:
"""Persist the immutable grant before the external Task side effect.
``BEGIN IMMEDIATE`` serializes only this SQLite writer transaction;
it does not serialize unrelated envelopes. A second proof cannot
replace the winning proof/grant for the same envelope.
"""
grant_json = canonical_json_bytes(grant.as_dict()).decode("utf-8")
grant_digest = hashlib.sha256(grant_json.encode("utf-8")).hexdigest()
with self._lock, self._connection() as con:
con.execute("BEGIN IMMEDIATE")
row = con.execute(
"SELECT grant_json,authorization_digest FROM task_admission_grants WHERE envelope_id=?",
(envelope_id,),
).fetchone()
if row is not None:
con.execute("ROLLBACK")
if row["authorization_digest"] != authorization_digest:
raise TaskAdmissionError("TASK_ADMISSION_AUTHORIZATION_CONFLICT", 409)
raw = json.loads(row["grant_json"])
return (
TaskAdmissionGrant(
**{
**raw,
"capabilities": tuple(raw["capabilities"]),
"file_allowlist": tuple(raw["file_allowlist"]),
}
),
False,
)
con.execute(
"INSERT INTO task_admission_grants(envelope_id,grant_id,grant_digest,grant_json,authorization_digest,created_at) VALUES(?,?,?,?,?,?)",
(envelope_id, grant.grant_id, grant_digest, grant_json, authorization_digest, created_at),
)
con.execute(
"UPDATE task_admission_envelopes SET status='ADMITTING' WHERE envelope_id=?",
(envelope_id,),
)
con.execute("COMMIT")
return grant, True
def receipt(self, envelope_id: str) -> dict[str, Any] | None:
with self._connection() as con:
row = con.execute(
"SELECT receipt_json FROM task_admission_receipts WHERE envelope_id=?",
(envelope_id,),
).fetchone()
return json.loads(row["receipt_json"]) if row else None
def save_grant_and_receipt(
self,
*,
envelope_id: str,
grant: TaskAdmissionGrant,
receipt: Mapping[str, Any],
created_at: int,
) -> dict[str, Any]:
grant_json = canonical_json_bytes(grant.as_dict()).decode("utf-8")
grant_digest = hashlib.sha256(grant_json.encode("utf-8")).hexdigest()
receipt_json = canonical_json_bytes(dict(receipt)).decode("utf-8")
with self._lock, self._connection() as con:
con.execute("BEGIN IMMEDIATE")
existing = con.execute(
"SELECT receipt_json FROM task_admission_receipts WHERE envelope_id=?",
(envelope_id,),
).fetchone()
if existing is not None:
con.execute("ROLLBACK")
return json.loads(existing["receipt_json"])
stored = con.execute(
"SELECT grant_id,grant_digest FROM task_admission_grants WHERE envelope_id=?",
(envelope_id,),
).fetchone()
if stored is None:
con.execute(
"INSERT INTO task_admission_grants(envelope_id,grant_id,grant_digest,grant_json,authorization_digest,created_at) VALUES(?,?,?,?,?,?)",
(envelope_id, grant.grant_id, grant_digest, grant_json, "", created_at),
)
elif stored["grant_id"] != grant.grant_id or stored["grant_digest"] != grant_digest:
con.execute("ROLLBACK")
raise TaskAdmissionError("TASK_ADMISSION_GRANT_CONFLICT", 409)
con.execute(
"""
INSERT INTO task_admission_receipts(
envelope_id,task_id,receipt_json,created_at
) VALUES(?,?,?,?)
""",
(
envelope_id,
grant.task_id,
receipt_json,
created_at,
),
)
con.execute(
"UPDATE task_admission_envelopes SET status='ADMITTED' WHERE envelope_id=?",
(envelope_id,),
)
con.execute("COMMIT")
return dict(receipt)
def status(self, envelope_id: str) -> dict[str, Any]:
row = self.get_envelope(envelope_id)
if row is None:
raise TaskAdmissionError("TASK_ADMISSION_ENVELOPE_NOT_FOUND", 404)
receipt = self.receipt(envelope_id)
return {
"envelope_id": envelope_id,
"status": row["status"],
"task": receipt,
}
class TaskAdmissionBootstrapService:
def __init__(
self,
*,
store: TaskAdmissionStore,
authorization: OwnerAuthorizationBackend,
task_port: TaskAdmissionPort,
policy: TaskAdmissionPolicy | None = None,
clock=utc_seconds,
post_admission: Callable[[TaskAdmissionGrant, Mapping[str, Any]], Any] | None = None,
) -> None:
self.store = store
self.authorization = authorization
self.task_port = task_port
self.policy = policy or TaskAdmissionPolicy()
self.clock = clock
# This is a narrow server-side projection boundary. The official
# Agent Task port remains the admission authority; a projection may
# only establish local execution state after its exact Task readback.
self.post_admission = post_admission
self._admit_lock = threading.RLock()
def create_envelope(self, raw: Mapping[str, Any]) -> dict[str, Any]:
required = {
"idempotency_key",
"task_id",
"repository",
"authority_base_oid",
"target_branch",
"generation",
"execution_digest",
"requested_capabilities",
"file_allowlist",
"risk",
}
optional = {"expires_in_seconds"}
if set(raw) - (required | optional) or not required.issubset(raw):
raise TaskAdmissionError("TASK_ADMISSION_REQUEST_FIELDS_INVALID")
idempotency_key = str(raw["idempotency_key"])
task_id = str(raw["task_id"])
repository = str(raw["repository"])
authority_base_oid = str(raw["authority_base_oid"])
target_branch = str(raw["target_branch"])
execution_digest = str(raw["execution_digest"])
if not IDEMPOTENCY_RE.fullmatch(idempotency_key):
raise TaskAdmissionError("TASK_ADMISSION_IDEMPOTENCY_KEY_INVALID")
if not TASK_RE.fullmatch(task_id):
raise TaskAdmissionError("TASK_ADMISSION_TASK_ID_INVALID")
if repository != self.policy.repository:
raise TaskAdmissionError("TASK_ADMISSION_REPOSITORY_REJECTED", 403)
if not OID_RE.fullmatch(authority_base_oid):
raise TaskAdmissionError("TASK_ADMISSION_BASE_OID_INVALID")
if (
not BRANCH_RE.fullmatch(target_branch)
or not target_branch.startswith(self.policy.branch_prefix)
or "//" in target_branch
or ".." in target_branch
or "@{" in target_branch
or target_branch.endswith(("/", "."))
or any(
not component
or component.startswith(".")
or component.endswith(".lock")
for component in target_branch.split("/")
)
):
raise TaskAdmissionError("TASK_ADMISSION_BRANCH_REJECTED", 403)
generation = raw["generation"]
if not isinstance(generation, int) or isinstance(generation, bool):
raise TaskAdmissionError("TASK_ADMISSION_GENERATION_INVALID")
if generation < 1:
raise TaskAdmissionError("TASK_ADMISSION_GENERATION_INVALID")
if not re.fullmatch(r"^[0-9a-f]{64}$", execution_digest):
raise TaskAdmissionError("TASK_ADMISSION_EXECUTION_DIGEST_INVALID")
requested_capabilities = _normalize_capabilities(
raw["requested_capabilities"],
policy=self.policy,
)
file_allowlist = _normalize_files(
raw["file_allowlist"],
maximum=self.policy.max_file_count,
)
risk = str(raw["risk"])
if _risk_rank(risk) > _risk_rank(self.policy.max_risk):
raise TaskAdmissionError("TASK_ADMISSION_RISK_OUT_OF_POLICY", 403)
ttl = raw.get("expires_in_seconds", self.policy.max_envelope_ttl_seconds)
if (
not isinstance(ttl, int)
or isinstance(ttl, bool)
or ttl < 60
or ttl > self.policy.max_envelope_ttl_seconds
):
raise TaskAdmissionError("TASK_ADMISSION_ENVELOPE_TTL_INVALID")
now = int(self.clock())
policy_digest = self.policy.normalized_policy_digest()
request_value = _request_for_digest(
task_id=task_id,
repository=repository,
authority_base_oid=authority_base_oid,
target_branch=target_branch,
generation=generation,
execution_digest=execution_digest,
requested_capabilities=requested_capabilities,
file_allowlist=file_allowlist,
risk=risk,
trust_domain=self.policy.trust_domain,
policy_digest=policy_digest,
)
request_digest = sha256_json(request_value)
nonce = secrets.token_urlsafe(24)
envelope_id = "task-admission-" + hashlib.sha256(
(idempotency_key + "\0" + request_digest + "\0" + nonce).encode("utf-8")
).hexdigest()[:32]
envelope = TaskAdmissionEnvelope(
envelope_id=envelope_id,
idempotency_key=idempotency_key,
task_id=task_id,
repository=repository,
authority_base_oid=authority_base_oid,
target_branch=target_branch,
generation=generation,
execution_digest=execution_digest,
requested_capabilities=requested_capabilities,
file_allowlist=file_allowlist,
risk=risk,
trust_domain=self.policy.trust_domain,
policy_digest=policy_digest,
issued_at=now,
not_before=now,
expires_at=now + ttl,
nonce=nonce,
request_digest=request_digest,
)
envelope_dict = envelope.as_dict()
persisted = self.store.save_envelope(
idempotency_key=idempotency_key,
envelope_id=envelope_id,
request_digest=request_digest,
envelope_json=canonical_json_bytes(envelope_dict).decode("utf-8"),
created_at=now,
)
won = persisted["envelope_id"] == envelope_id
return {
"schema": SCHEMA_ENVELOPE,
"envelope": persisted,
"canonical_payload_base64": base64.b64encode(
canonical_json_bytes(persisted)
).decode("ascii"),
"idempotent": not won,
}
def _load_envelope(self, envelope_id: str) -> TaskAdmissionEnvelope:
row = self.store.get_envelope(envelope_id)
if row is None:
raise TaskAdmissionError("TASK_ADMISSION_ENVELOPE_NOT_FOUND", 404)
raw = json.loads(row["envelope_json"])
return TaskAdmissionEnvelope(
**{
**raw,
"requested_capabilities": tuple(raw["requested_capabilities"]),
"file_allowlist": tuple(raw["file_allowlist"]),
}
)
def _verify_epoch(
self,
*,
epoch_id: str,
envelope: TaskAdmissionEnvelope,
now: int,
) -> OwnerDelegationEpoch:
epoch = self.authorization.verify_delegation_epoch(epoch_id)
if epoch.schema != SCHEMA_EPOCH:
raise TaskAdmissionError("TASK_ADMISSION_EPOCH_SCHEMA_INVALID", 403)
if epoch.epoch_id != epoch_id:
raise TaskAdmissionError("TASK_ADMISSION_EPOCH_ID_MISMATCH", 403)
if (
epoch.trust_domain != envelope.trust_domain
or epoch.repository != envelope.repository
or epoch.policy_digest != envelope.policy_digest
):
raise TaskAdmissionError("TASK_ADMISSION_EPOCH_BINDING_MISMATCH", 403)
if epoch.revocation_epoch != 0:
raise TaskAdmissionError("TASK_ADMISSION_EPOCH_REVOKED", 403)
if not epoch.not_before <= now < epoch.expires_at:
raise TaskAdmissionError("TASK_ADMISSION_EPOCH_EXPIRED", 403)
if not any(
envelope.target_branch.startswith(prefix)
for prefix in epoch.allowed_branch_prefixes
):
raise TaskAdmissionError("TASK_ADMISSION_EPOCH_BRANCH_REJECTED", 403)
caps = set(envelope.requested_capabilities)
if not caps.issubset(epoch.allowed_capabilities):
raise TaskAdmissionError("TASK_ADMISSION_EPOCH_CAPABILITY_WIDENING", 403)
if _risk_rank(envelope.risk) > _risk_rank(epoch.risk_ceiling):
raise TaskAdmissionError("TASK_ADMISSION_EPOCH_RISK_WIDENING", 403)
for path in envelope.file_allowlist:
if not any(
path == prefix.rstrip("/")
or path.startswith(prefix.rstrip("/") + "/")
for prefix in epoch.allowed_file_prefixes
):
raise TaskAdmissionError("TASK_ADMISSION_EPOCH_FILE_WIDENING", 403)
return epoch
def authorize_and_admit(self, raw: Mapping[str, Any]) -> dict[str, Any]:
required = {"envelope_id", "authorization_kind"}
allowed = required | {
"delegation_epoch_id",
"owner_key_id",
"signature_base64",
}
if set(raw) - allowed or not required.issubset(raw):
raise TaskAdmissionError("TASK_ADMISSION_AUTH_FIELDS_INVALID")
envelope_id = str(raw["envelope_id"])
with self.store.admission_lock(envelope_id):
# Every replay revalidates policy, temporal bounds, revocation and
# proof after the crash-releasing lock is owned.
envelope = self._load_envelope(envelope_id)
now = int(self.clock())
if envelope.policy_digest != self.policy.normalized_policy_digest():
raise TaskAdmissionError("TASK_ADMISSION_POLICY_DIGEST_DRIFT", 403)
if now < envelope.not_before or now >= envelope.expires_at:
raise TaskAdmissionError("TASK_ADMISSION_ENVELOPE_EXPIRED", 403)
kind = str(raw["authorization_kind"])
parent_epoch: OwnerDelegationEpoch | None = None
if kind == "delegation_epoch":
epoch_id = str(raw.get("delegation_epoch_id") or "")
if not epoch_id:
raise TaskAdmissionError("TASK_ADMISSION_EPOCH_REQUIRED")
parent_epoch = self._verify_epoch(epoch_id=epoch_id, envelope=envelope, now=now)
owner_identity = parent_epoch.owner_identity
parent_epoch_id: str | None = parent_epoch.epoch_id
elif kind == "owner_signature":
key_id = str(raw.get("owner_key_id") or "")
signature = str(raw.get("signature_base64") or "")
if not KEY_ID_RE.fullmatch(key_id) or not signature:
raise TaskAdmissionError("TASK_ADMISSION_OWNER_SIGNATURE_REQUIRED")
owner_identity = self.authorization.verify_owner_signature(
key_id=key_id,
canonical_payload=canonical_json_bytes(envelope.as_dict()),
signature_b64=signature,
)
if not isinstance(owner_identity, str) or not owner_identity:
raise TaskAdmissionError("TASK_ADMISSION_OWNER_SIGNATURE_INVALID", 403)
parent_epoch_id = None
else:
raise TaskAdmissionError("TASK_ADMISSION_AUTHORIZATION_KIND_REJECTED", 403)
authorization_digest = sha256_json(dict(raw))
grant_id = "task-grant-" + hashlib.sha256(
(envelope.envelope_id + "\0" + kind + "\0" + owner_identity + "\0" + (parent_epoch_id or "")).encode("utf-8")
).hexdigest()[:32]
grant = TaskAdmissionGrant(
grant_id=grant_id, envelope_id=envelope.envelope_id, task_id=envelope.task_id,
repository=envelope.repository, authority_base_oid=envelope.authority_base_oid,
target_branch=envelope.target_branch, generation=envelope.generation,
execution_digest=envelope.execution_digest, capabilities=envelope.requested_capabilities,
file_allowlist=envelope.file_allowlist, risk=envelope.risk,
trust_domain=envelope.trust_domain, policy_digest=envelope.policy_digest,
authorization_kind=kind, owner_identity=owner_identity,
parent_epoch_id=parent_epoch_id, issued_at=now,
expires_at=min(
envelope.expires_at,
now + self.policy.max_task_ttl_seconds,
parent_epoch.expires_at if parent_epoch is not None else envelope.expires_at,
),
)
persisted_grant, _ = self.store.claim_grant(
envelope_id=envelope_id, grant=grant,
authorization_digest=authorization_digest, created_at=now,
)
if now >= persisted_grant.expires_at:
raise TaskAdmissionError("TASK_ADMISSION_GRANT_EXPIRED", 403)
if parent_epoch is not None and persisted_grant.expires_at > parent_epoch.expires_at:
raise TaskAdmissionError("TASK_ADMISSION_GRANT_PARENT_EXPIRY_CONFLICT", 409)
existing_receipt = self.store.receipt(envelope_id)
if existing_receipt is not None:
return {**existing_receipt, "idempotent": True}
try:
result = self.task_port.admit(persisted_grant)
except TaskAdmissionError:
raise
except Exception as exc:
raise TaskAdmissionError("TASK_ADMISSION_TASK_STATE_UNKNOWN", 503) from exc
if not isinstance(result, Mapping):
raise TaskAdmissionError("TASK_ADMISSION_PORT_RESPONSE_INVALID", 502)
state = str(result.get("state") or result.get("status") or "")
if str(result.get("task_id") or "") != persisted_grant.task_id:
raise TaskAdmissionError("TASK_ADMISSION_PORT_TASK_MISMATCH", 502)
if state not in {"QUEUED", "CLAIMED", "RUNNING"}:
raise TaskAdmissionError("TASK_ADMISSION_PORT_STATE_INVALID", 502, details={"state": state})
if self.post_admission is not None:
projection_result: Mapping[str, Any] = result
projection_reader = getattr(self.task_port, "projection_result", None)
if callable(projection_reader):
try:
candidate = projection_reader(persisted_grant, result)
except TaskAdmissionError:
raise
except Exception as exc:
raise TaskAdmissionError(
"TASK_ADMISSION_EXECUTION_PROJECTION_FAILED", 503
) from exc
if not isinstance(candidate, Mapping):
raise TaskAdmissionError(
"TASK_ADMISSION_EXECUTION_PROJECTION_INVALID", 503
)
projection_result = candidate
try:
projection = self.post_admission(
persisted_grant, projection_result
)
except TaskAdmissionError:
raise
except Exception as exc:
raise TaskAdmissionError(
"TASK_ADMISSION_EXECUTION_PROJECTION_FAILED", 503
) from exc
if projection is not None and not isinstance(projection, Mapping):
raise TaskAdmissionError(
"TASK_ADMISSION_EXECUTION_PROJECTION_INVALID", 503
)
receipt = {
"schema": SCHEMA_RECEIPT, "envelope_id": persisted_grant.envelope_id,
"grant_id": persisted_grant.grant_id, "task_id": persisted_grant.task_id,
"task_state": state, "authorization_kind": persisted_grant.authorization_kind,
"owner_identity": persisted_grant.owner_identity, "parent_epoch_id": persisted_grant.parent_epoch_id,
"authority_base_oid": persisted_grant.authority_base_oid,
"target_branch": persisted_grant.target_branch, "generation": persisted_grant.generation,
"execution_digest": persisted_grant.execution_digest,
"grant_digest_sha256": sha256_json(persisted_grant.as_dict()),
"admitted_at": now, "idempotent": False,
}
return self.store.save_grant_and_receipt(
envelope_id=envelope_id, grant=persisted_grant, receipt=receipt, created_at=now,
)
def status(self, envelope_id: str) -> dict[str, Any]:
return self.store.status(envelope_id)
@@ -0,0 +1,87 @@
"""Bridge contract surface for Task Admission Bootstrap.
This module is intentionally transport-agnostic. The live Bridge may merge
these routes/operations into its existing authenticated/HTTPS surface, but the
authorization proof for the authorize operation is the verified delegation
epoch or Owner signature carried by the request itself.
No handler here can issue TaskPrincipal assertions or WriterLeases. Those remain
strictly downstream of a real admitted/claimed Agent Task.
"""
from __future__ import annotations
from typing import Any, Mapping
from aios_task_admission_bootstrap import TaskAdmissionBootstrapService
TASK_ADMISSION_ROUTES = {
"/v2/task-admission/envelope": "getTaskAdmissionEnvelope",
"/v2/task-admission/authorize": "submitTaskAdmissionAuthorization",
"/v2/task-admission/status": "getTaskAdmissionStatus",
}
TASK_ADMISSION_OPERATION_REQUIRED_FIELDS = {
"getTaskAdmissionEnvelope": (
"idempotency_key",
"task_id",
"repository",
"authority_base_oid",
"target_branch",
"generation",
"execution_digest",
"requested_capabilities",
"file_allowlist",
"risk",
),
"submitTaskAdmissionAuthorization": (
"envelope_id",
"authorization_kind",
),
"getTaskAdmissionStatus": ("envelope_id",),
}
TASK_ADMISSION_OPERATION_OPTIONAL_FIELDS = {
"getTaskAdmissionEnvelope": ("expires_in_seconds",),
"submitTaskAdmissionAuthorization": (
"delegation_epoch_id",
"owner_key_id",
"signature_base64",
),
"getTaskAdmissionStatus": (),
}
class TaskAdmissionBridgeContract:
def __init__(self, service: TaskAdmissionBootstrapService) -> None:
self.service = service
@staticmethod
def _validate_fields(operation: str, request: Mapping[str, Any]) -> None:
required = set(TASK_ADMISSION_OPERATION_REQUIRED_FIELDS[operation])
allowed = required | set(TASK_ADMISSION_OPERATION_OPTIONAL_FIELDS[operation])
if not required.issubset(request):
raise ValueError("TASK_ADMISSION_BRIDGE_REQUIRED_FIELDS_MISSING")
if set(request) - allowed:
raise ValueError("TASK_ADMISSION_BRIDGE_UNKNOWN_FIELDS")
def dispatch(self, operation: str, request: Mapping[str, Any]) -> dict[str, Any]:
if operation not in TASK_ADMISSION_OPERATION_REQUIRED_FIELDS:
raise ValueError("TASK_ADMISSION_BRIDGE_OPERATION_NOT_FOUND")
self._validate_fields(operation, request)
if operation == "getTaskAdmissionEnvelope":
return self.service.create_envelope(request)
if operation == "submitTaskAdmissionAuthorization":
return self.service.authorize_and_admit(request)
if operation == "getTaskAdmissionStatus":
return self.service.status(str(request["envelope_id"]))
raise ValueError("TASK_ADMISSION_BRIDGE_OPERATION_NOT_FOUND")
def handlers(self) -> dict[str, Any]:
return {
operation: (
lambda request, operation=operation: self.dispatch(operation, request)
)
for operation in TASK_ADMISSION_OPERATION_REQUIRED_FIELDS
}
@@ -0,0 +1,841 @@
"""Narrow adapters for the already-authorized Owner and Agent Task seams.
This file intentionally contains no credential lookup, HTTP client construction,
or fallback identity. The service host injects its authenticated durable-epoch
provider and its service-managed Agent Task transport; a request body can never
provide either authority.
"""
from __future__ import annotations
import base64
from dataclasses import dataclass
from datetime import datetime, timezone
import hashlib
import importlib.util
import json
import os
from pathlib import Path
import stat
import time
from typing import Any, Callable, Mapping, Protocol
import urllib.error
import urllib.request
from aios_task_admission_bootstrap import (
OwnerDelegationEpoch,
TaskAdmissionError,
TaskAdmissionGrant,
canonical_json_bytes,
sha256_json,
)
OWNER_NAMESPACE = "aios-owner-approval-v1"
OWNER_SIGNER_IDENTITY = "aios-task-grant-signer"
class OfficialTaskTransport(Protocol):
"""Service-managed transport; it owns the Task API control credential."""
def request(self, method: str, path: str, body: Mapping[str, Any] | None = None) -> Mapping[str, Any]:
...
class BoundOwnerAuthorizationBackend:
"""Bind epoch lookup to trusted transport identity, never caller JSON."""
def __init__(
self,
*,
authenticated_caller: Mapping[str, Any],
verified_epoch_provider: Callable[..., OwnerDelegationEpoch],
signature_verifier: Any,
) -> None:
if not isinstance(authenticated_caller, Mapping) or not authenticated_caller:
raise ValueError("AUTHENTICATED_CALLER_REQUIRED")
self._caller = dict(authenticated_caller)
self._epoch_provider = verified_epoch_provider
self._signature_verifier = signature_verifier
def verify_delegation_epoch(self, epoch_id: str) -> OwnerDelegationEpoch:
try:
epoch = self._epoch_provider(epoch_id=epoch_id, caller=dict(self._caller))
except TypeError as exc:
raise TaskAdmissionError("TASK_ADMISSION_EPOCH_BACKEND_INVALID", 503) from exc
if not isinstance(epoch, OwnerDelegationEpoch):
raise TaskAdmissionError("TASK_ADMISSION_EPOCH_BACKEND_INVALID", 503)
return epoch
def verify_owner_signature(
self, *, key_id: str, canonical_payload: bytes, signature_b64: str
) -> str:
try:
armored = base64.b64decode(signature_b64, validate=True).decode("utf-8")
except (ValueError, UnicodeDecodeError) as exc:
raise TaskAdmissionError("TASK_ADMISSION_OWNER_SIGNATURE_INVALID", 403) from exc
verify = getattr(self._signature_verifier, "verify", None)
if not callable(verify) or verify(
namespace=OWNER_NAMESPACE,
payload=canonical_payload,
signature=armored,
key_id=key_id,
) is not True:
raise TaskAdmissionError("TASK_ADMISSION_OWNER_SIGNATURE_INVALID", 403)
return OWNER_SIGNER_IDENTITY
class AgentTaskAdmissionPort:
"""Official Task API adapter with exact identity reconciliation.
It never exposes or returns the API control bearer. A timeout or 5xx is
reconciled by exact GET; if that cannot prove the state, the durable caller
receives UNKNOWN instead of a second blind POST.
"""
def __init__(
self,
transport: OfficialTaskTransport,
*,
execution_plan_provider: Callable[[TaskAdmissionGrant], Mapping[str, Any]] | None = None,
) -> None:
if not callable(getattr(transport, "request", None)):
raise ValueError("OFFICIAL_TASK_TRANSPORT_REQUIRED")
self._transport = transport
self._execution_plan_provider = execution_plan_provider
def _execution_plan(self, grant: TaskAdmissionGrant) -> dict[str, Any]:
if self._execution_plan_provider is None:
raise TaskAdmissionError("TASK_ADMISSION_EXECUTION_PLAN_REQUIRED", 503)
try:
raw = self._execution_plan_provider(grant)
except TaskAdmissionError:
raise
except Exception as exc:
raise TaskAdmissionError("TASK_ADMISSION_EXECUTION_PLAN_UNAVAILABLE", 503) from exc
if not isinstance(raw, Mapping):
raise TaskAdmissionError("TASK_ADMISSION_EXECUTION_PLAN_INVALID", 403)
expected = {
"repository": grant.repository,
"target_branch": grant.target_branch,
"generation": grant.generation,
"execution_digest": grant.execution_digest,
}
if any(canonical_json_bytes(raw.get(key)) != canonical_json_bytes(value) for key, value in expected.items()):
raise TaskAdmissionError("TASK_ADMISSION_EXECUTION_PLAN_CONFLICT", 409)
external_job_id = raw.get("external_job_id")
if not isinstance(external_job_id, str) or not external_job_id.strip():
raise TaskAdmissionError("TASK_ADMISSION_EXECUTION_PLAN_INVALID", 403)
# The trusted provider resolves the immutable execution payload by its
# signed digest. Preserve that payload: returning just coordinates
# would discard the executable plan needed by the real worker.
return dict(raw)
@staticmethod
def _metadata(grant: TaskAdmissionGrant, execution: Mapping[str, Any]) -> dict[str, Any]:
return {
"task_admission_schema": "aios.task-admission-grant.v1",
"grant_id": grant.grant_id,
"grant_digest_sha256": sha256_json(grant.as_dict()),
"repository": grant.repository,
"authority_base_oid": grant.authority_base_oid,
"target_branch": grant.target_branch,
"generation": grant.generation,
"execution_digest": grant.execution_digest,
"file_allowlist": list(grant.file_allowlist),
"capabilities": list(grant.capabilities),
"risk": grant.risk,
"execution": dict(execution),
}
@staticmethod
def _deadline(epoch: int) -> str:
# TaskStore.iso preserves the explicit UTC offset; exact reconciliation
# compares against that stored representation rather than a Z variant.
return datetime.fromtimestamp(epoch, timezone.utc).isoformat()
def _body(self, grant: TaskAdmissionGrant, execution: Mapping[str, Any]) -> dict[str, Any]:
metadata = self._metadata(grant, execution)
immutable = canonical_json_bytes(metadata).decode("utf-8")
return {
"task_id": grant.task_id,
"goal": "Execute the immutable governed task admission grant.",
"instructions": "Use only the admitted immutable metadata; execution plan digest: " + grant.execution_digest,
"success_criteria": ["Task processing preserves the admitted immutable identity."],
"required_capabilities": sorted(grant.capabilities),
"constraints": {
"repository": grant.repository,
"authority_base_oid": grant.authority_base_oid,
"target_branch": grant.target_branch,
"generation": grant.generation,
"file_allowlist": list(grant.file_allowlist),
"immutable_metadata": immutable,
},
"metadata": metadata,
"deadline": self._deadline(grant.expires_at),
"priority": 0,
"max_attempts": 1,
"idempotency_key": "task-admission/" + grant.grant_id,
}
def _exact(
self, task: Mapping[str, Any], grant: TaskAdmissionGrant, execution: Mapping[str, Any]
) -> dict[str, Any]:
expected = self._body(grant, execution)
immutable_fields = (
"task_id", "goal", "instructions", "success_criteria",
"required_capabilities", "constraints", "metadata", "deadline",
"priority", "max_attempts",
)
def stable(field: str, value: Any) -> Any:
"""Exclude only re-admission provenance, never execution scope."""
if field == "metadata" and isinstance(value, Mapping):
return {
key: item
for key, item in value.items()
if key not in {"grant_id", "grant_digest_sha256"}
}
if field == "constraints" and isinstance(value, Mapping):
return {
key: item
for key, item in value.items()
if key != "immutable_metadata"
}
return value
if any(
canonical_json_bytes(stable(field, task.get(field)))
!= canonical_json_bytes(stable(field, expected[field]))
for field in immutable_fields
):
raise TaskAdmissionError("TASK_ADMISSION_TASK_IDENTITY_CONFLICT", 409)
state = str(task.get("state") or task.get("status") or "")
if state not in {"QUEUED", "CLAIMED", "RUNNING"}:
raise TaskAdmissionError("TASK_ADMISSION_PORT_STATE_INVALID", 502, details={"state": state})
# The public admission port reports only authoritative Task identity
# and state. The execution-plan identity is an internal capability
# used later by a trusted post-admission projection, never a caller
# visible part of this HTTP read model.
return {"task_id": grant.task_id, "state": state}
def _read(self, grant: TaskAdmissionGrant, execution: Mapping[str, Any]) -> dict[str, Any] | None:
try:
task = self._transport.request("GET", "/v1/tasks/" + grant.task_id)
except Exception as exc:
# A transport is expected to expose a status for normal 404s. A
# generic failure is ambiguous and must not be treated as absent.
statuses = [
getattr(exc, attribute, None)
for attribute in ("status", "http_status", "code")
if getattr(exc, attribute, None) is not None
]
if statuses and all(status == 404 for status in statuses):
return None
raise TaskAdmissionError("TASK_ADMISSION_TASK_STATE_UNKNOWN", 503) from exc
if not isinstance(task, Mapping):
raise TaskAdmissionError("TASK_ADMISSION_PORT_RESPONSE_INVALID", 502)
return self._exact(task, grant, execution)
def admit(self, grant: TaskAdmissionGrant) -> Mapping[str, Any]:
execution = self._execution_plan(grant)
existing = self._read(grant, execution)
if existing is not None:
return existing
try:
created = self._transport.request("POST", "/v1/tasks", self._body(grant, execution))
except Exception as exc:
recovered = self._read(grant, execution)
if recovered is not None:
return recovered
raise TaskAdmissionError("TASK_ADMISSION_TASK_STATE_UNKNOWN", 503) from exc
if not isinstance(created, Mapping):
raise TaskAdmissionError("TASK_ADMISSION_PORT_RESPONSE_INVALID", 502)
# Authoritative GET prevents a POST response from becoming the source
# of task identity/state and handles a task claimed before readback.
recovered = self._read(grant, execution)
if recovered is None:
raise TaskAdmissionError("TASK_ADMISSION_TASK_STATE_UNKNOWN", 503)
return recovered
def projection_result(
self,
grant: TaskAdmissionGrant,
admitted: Mapping[str, Any],
) -> Mapping[str, Any]:
"""Return the server-side execution identity for a validated readback.
`admit()` intentionally keeps this identity out of its public result.
The bootstrap service calls this only for its local post-admission
projection; the value comes from the content-addressed execution-plan
provider, not from the HTTP caller or the Task API response.
"""
if str(admitted.get("task_id") or "") != grant.task_id:
raise TaskAdmissionError("TASK_ADMISSION_PORT_TASK_MISMATCH", 502)
state = str(admitted.get("state") or admitted.get("status") or "")
if state not in {"QUEUED", "CLAIMED", "RUNNING"}:
raise TaskAdmissionError(
"TASK_ADMISSION_PORT_STATE_INVALID", 502, details={"state": state}
)
execution = self._execution_plan(grant)
external_job_id = execution.get("external_job_id")
if not isinstance(external_job_id, str) or not external_job_id.strip():
raise TaskAdmissionError("TASK_ADMISSION_EXECUTION_PLAN_INVALID", 403)
return {
"task_id": grant.task_id,
"state": state,
"external_job_id": external_job_id,
}
class TaskAdmissionHostError(RuntimeError):
"""Stable fail-closed host-composition error."""
class OfficialTaskHTTPError(RuntimeError):
def __init__(self, status: int, body: object | None = None) -> None:
super().__init__(f"AGENT_TASK_HTTP_{status}")
self.status = status
self.http_status = status
self.code = status
self.body = body
def _host_strict_object(pairs: list[tuple[str, Any]]) -> dict[str, Any]:
value: dict[str, Any] = {}
for key, item in pairs:
if key in value:
raise TaskAdmissionHostError("DUPLICATE_JSON_FIELD")
value[key] = item
return value
def _host_read_json_object(path: Path) -> dict[str, Any]:
try:
value = json.loads(
path.read_text(encoding="utf-8"),
object_pairs_hook=_host_strict_object,
)
except (OSError, UnicodeError, json.JSONDecodeError) as exc:
raise TaskAdmissionHostError("JSON_INPUT_INVALID") from exc
if not isinstance(value, dict):
raise TaskAdmissionHostError("JSON_OBJECT_REQUIRED")
return value
def _host_posix_metadata_enforced() -> bool:
"""Return whether POSIX ownership/mode bits are security-significant here.
The live AIOS authority/runtime is Linux, where mode/uid checks are a real
trust boundary. Windows development/test files expose synthesized POSIX
mode bits that do not represent ACL authority, so interpreting ``022``
there would reject every secure temporary directory without improving
live security.
"""
return os.name == "posix"
def _host_require_regular(path: Path, *, root_owned: bool) -> None:
try:
info = path.lstat()
except OSError as exc:
raise TaskAdmissionHostError("SECURE_FILE_MISSING") from exc
if path.is_symlink() or not stat.S_ISREG(info.st_mode):
raise TaskAdmissionHostError("SECURE_FILE_INVALID")
if _host_posix_metadata_enforced():
if stat.S_IMODE(info.st_mode) & 0o022:
raise TaskAdmissionHostError("SECURE_FILE_WRITABLE_BY_GROUP_OR_OTHER")
if root_owned and info.st_uid != 0:
raise TaskAdmissionHostError("SECURE_FILE_NOT_ROOT_OWNED")
def _host_require_directory(path: Path, *, root_owned: bool) -> None:
try:
info = path.lstat()
except OSError as exc:
raise TaskAdmissionHostError("SECURE_DIRECTORY_MISSING") from exc
if path.is_symlink() or not stat.S_ISDIR(info.st_mode):
raise TaskAdmissionHostError("SECURE_DIRECTORY_INVALID")
if _host_posix_metadata_enforced():
if stat.S_IMODE(info.st_mode) & 0o022:
raise TaskAdmissionHostError("SECURE_DIRECTORY_WRITABLE_BY_GROUP_OR_OTHER")
if root_owned and info.st_uid != 0:
raise TaskAdmissionHostError("SECURE_DIRECTORY_NOT_ROOT_OWNED")
def load_owner_approval_verifier(module_path: Path) -> Any:
"""Load the already-installed trusted public Owner verifier."""
_host_require_regular(module_path, root_owned=True)
spec = importlib.util.spec_from_file_location(
"aios_live_owner_approval_verifier",
module_path,
)
if spec is None or spec.loader is None:
raise TaskAdmissionHostError("OWNER_VERIFIER_IMPORT_INVALID")
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
cls = getattr(module, "GrantRootOwnerApprovalVerifier", None)
if cls is None:
raise TaskAdmissionHostError("OWNER_VERIFIER_CLASS_MISSING")
verifier = cls()
if not callable(getattr(verifier, "verify", None)):
raise TaskAdmissionHostError("OWNER_VERIFIER_INVALID")
return verifier
@dataclass(frozen=True, slots=True)
class SignedEpochDirectoryProvider:
"""Re-verify the exact signed Owner epoch on every admission use."""
root: Path
verifier: Any
revocation_file: Path
expected_caller: Mapping[str, Any]
require_root_owner: bool = True
clock: Any = time.time
def _revoked_ids(self) -> set[str]:
_host_require_regular(
self.revocation_file,
root_owned=self.require_root_owner,
)
value = _host_read_json_object(self.revocation_file)
if (
set(value) != {"schema", "revoked_epoch_ids"}
or value.get("schema") != "aios.owner-delegation-revocations.v1"
or not isinstance(value.get("revoked_epoch_ids"), list)
or not all(
isinstance(item, str) and item
for item in value["revoked_epoch_ids"]
)
):
raise TaskAdmissionHostError(
"OWNER_EPOCH_REVOCATION_REGISTRY_INVALID"
)
return set(value["revoked_epoch_ids"])
def __call__(
self,
*,
epoch_id: str,
caller: Mapping[str, Any],
) -> OwnerDelegationEpoch:
if dict(caller) != dict(self.expected_caller):
raise TaskAdmissionError(
"TASK_ADMISSION_CALLER_BINDING_MISMATCH",
403,
)
_host_require_directory(
self.root,
root_owned=self.require_root_owner,
)
canonical = self.root / "owner-delegation-epoch.canonical.json"
signature = self.root / "owner-delegation-epoch.sig"
key_id_file = self.root / "owner-delegation-epoch.key-id"
for path in (canonical, signature, key_id_file):
_host_require_regular(
path,
root_owned=self.require_root_owner,
)
payload = canonical.read_bytes()
value = _host_read_json_object(canonical)
if canonical_json_bytes(value) != payload:
raise TaskAdmissionError(
"TASK_ADMISSION_EPOCH_CANONICAL_MISMATCH",
403,
)
if value.get("schema") != "aios.owner-delegation-epoch.v1":
raise TaskAdmissionError(
"TASK_ADMISSION_EPOCH_SCHEMA_INVALID",
403,
)
if value.get("epoch_id") != epoch_id:
raise TaskAdmissionError(
"TASK_ADMISSION_EPOCH_ID_MISMATCH",
403,
)
if value.get("owner_identity") != OWNER_SIGNER_IDENTITY:
raise TaskAdmissionError(
"TASK_ADMISSION_EPOCH_OWNER_IDENTITY_INVALID",
403,
)
if value.get("revocation_epoch") != 0:
raise TaskAdmissionError("TASK_ADMISSION_EPOCH_REVOKED", 403)
if epoch_id in self._revoked_ids():
raise TaskAdmissionError("TASK_ADMISSION_EPOCH_REVOKED", 403)
key_id = key_id_file.read_text(encoding="utf-8").strip()
armored = signature.read_text(encoding="utf-8")
if self.verifier.verify(
namespace=OWNER_NAMESPACE,
payload=payload,
signature=armored,
key_id=key_id,
) is not True:
raise TaskAdmissionError(
"TASK_ADMISSION_EPOCH_SIGNATURE_INVALID",
403,
)
now = int(self.clock())
not_before = value.get("not_before")
expires_at = value.get("expires_at")
if (
not isinstance(not_before, int)
or isinstance(not_before, bool)
or not isinstance(expires_at, int)
or isinstance(expires_at, bool)
or not not_before <= now < expires_at
):
raise TaskAdmissionError("TASK_ADMISSION_EPOCH_EXPIRED", 403)
branches = value.get("allowed_branch_prefixes")
capabilities = value.get("allowed_capabilities")
files = value.get("allowed_file_prefixes")
if (
not isinstance(branches, list)
or not all(isinstance(item, str) and item for item in branches)
or not isinstance(capabilities, list)
or not all(
isinstance(item, str) and item
for item in capabilities
)
or not isinstance(files, list)
or not all(isinstance(item, str) and item for item in files)
):
raise TaskAdmissionError(
"TASK_ADMISSION_EPOCH_SCOPE_INVALID",
403,
)
return OwnerDelegationEpoch(
epoch_id=str(value["epoch_id"]),
owner_identity=str(value["owner_identity"]),
trust_domain=str(value["trust_domain"]),
repository=str(value["repository"]),
allowed_branch_prefixes=tuple(branches),
allowed_capabilities=frozenset(capabilities),
allowed_file_prefixes=tuple(files),
risk_ceiling=str(value["risk_ceiling"]),
issued_at=int(value["issued_at"]),
not_before=not_before,
expires_at=expires_at,
revocation_epoch=int(value["revocation_epoch"]),
policy_digest=str(value["policy_digest"]),
schema=str(value["schema"]),
)
@dataclass(frozen=True, slots=True)
class ExecutionPlanDirectoryProvider:
"""Resolve a trusted execution plan by its canonical SHA256 digest."""
root: Path
require_root_owner: bool = True
def __call__(self, grant: TaskAdmissionGrant) -> Mapping[str, Any]:
digest = str(grant.execution_digest)
if (
len(digest) != 64
or any(ch not in "0123456789abcdef" for ch in digest)
):
raise TaskAdmissionError(
"TASK_ADMISSION_EXECUTION_PLAN_DIGEST_INVALID",
403,
)
_host_require_directory(
self.root,
root_owned=self.require_root_owner,
)
path = self.root / (digest + ".json")
_host_require_regular(
path,
root_owned=self.require_root_owner,
)
value = _host_read_json_object(path)
if hashlib.sha256(canonical_json_bytes(value)).hexdigest() != digest:
raise TaskAdmissionError(
"TASK_ADMISSION_EXECUTION_PLAN_DIGEST_MISMATCH",
403,
)
expected = {
"task_id": grant.task_id,
"repository": grant.repository,
"target_branch": grant.target_branch,
"generation": grant.generation,
}
if any(value.get(key) != item for key, item in expected.items()):
raise TaskAdmissionError(
"TASK_ADMISSION_EXECUTION_PLAN_CONFLICT",
409,
)
external_job_id = value.get("external_job_id")
if not isinstance(external_job_id, str) or not external_job_id.strip():
raise TaskAdmissionError(
"TASK_ADMISSION_EXECUTION_PLAN_INVALID",
403,
)
return {**value, "execution_digest": digest}
@dataclass(frozen=True, slots=True)
class LocalAgentTaskHTTPTransport:
"""Server-side localhost Task API transport; token never enters caller JSON."""
base_url: str
token_file: Path
timeout_seconds: float = 5.0
require_root_owner: bool = True
def _token(self) -> str:
_host_require_regular(
self.token_file,
root_owned=self.require_root_owner,
)
token = self.token_file.read_text(encoding="utf-8").strip()
if len(token) < 16 or any(ch.isspace() for ch in token):
raise TaskAdmissionHostError("AGENT_TASK_CONTROL_TOKEN_INVALID")
return token
def request(
self,
method: str,
path: str,
body: Mapping[str, Any] | None = None,
) -> Mapping[str, Any]:
if method not in {"GET", "POST"}:
raise TaskAdmissionHostError("AGENT_TASK_HTTP_METHOD_INVALID")
if not path.startswith("/v1/tasks"):
raise TaskAdmissionHostError("AGENT_TASK_HTTP_PATH_INVALID")
raw = None if body is None else canonical_json_bytes(dict(body))
request = urllib.request.Request(
self.base_url.rstrip("/") + path,
data=raw,
method=method,
headers={
"Accept": "application/json",
"Authorization": "Bearer " + self._token(),
**(
{"Content-Type": "application/json"}
if raw is not None
else {}
),
},
)
try:
with urllib.request.urlopen(
request,
timeout=self.timeout_seconds,
) as response:
value = json.loads(
response.read().decode("utf-8"),
object_pairs_hook=_host_strict_object,
)
except urllib.error.HTTPError as exc:
body_value: object | None = None
try:
body_value = json.loads(exc.read().decode("utf-8"))
except Exception:
body_value = None
raise OfficialTaskHTTPError(
exc.code,
body_value,
) from exc
if not isinstance(value, Mapping):
raise TaskAdmissionHostError("AGENT_TASK_HTTP_RESPONSE_INVALID")
return dict(value)
def task_admission_policy_from_epoch(
epoch: OwnerDelegationEpoch,
) -> Any:
"""Build a local ceiling exactly narrowed to the signed epoch."""
from aios_task_admission_bootstrap import TaskAdmissionPolicy
return TaskAdmissionPolicy(
repository=epoch.repository,
trust_domain=epoch.trust_domain,
branch_prefix="agent/",
allowed_capabilities=frozenset(epoch.allowed_capabilities),
max_file_count=min(
64,
max(1, len(epoch.allowed_file_prefixes)),
),
max_task_ttl_seconds=min(
3600,
max(60, epoch.expires_at - epoch.not_before),
),
max_envelope_ttl_seconds=900,
max_risk=epoch.risk_ceiling,
policy_digest=epoch.policy_digest,
)
def _build_task_admission_handlers(
*,
caller_service: str,
epoch_root: Path,
epoch_id: str,
revocation_file: Path,
execution_plan_dir: Path,
state_db: Path,
task_api_url: str,
task_control_token_file: Path,
owner_verifier_module: Path,
post_admission: Callable[[TaskAdmissionGrant, Mapping[str, Any]], Any] | None = None,
) -> Mapping[str, Any]:
"""Compose handlers from already-trusted server-side coordinates."""
from aios_task_admission_bootstrap import (
TaskAdmissionBootstrapService,
TaskAdmissionStore,
)
from aios_task_admission_bridge_contract import TaskAdmissionBridgeContract
verifier = load_owner_approval_verifier(owner_verifier_module)
caller = {
"service": caller_service,
"trust_domain": "aios.owner",
}
provider = SignedEpochDirectoryProvider(
root=epoch_root,
verifier=verifier,
revocation_file=revocation_file,
expected_caller=caller,
require_root_owner=True,
)
epoch = provider(epoch_id=epoch_id, caller=caller)
authorization = BoundOwnerAuthorizationBackend(
authenticated_caller=caller,
verified_epoch_provider=provider,
signature_verifier=verifier,
)
task_port = AgentTaskAdmissionPort(
LocalAgentTaskHTTPTransport(
base_url=task_api_url,
token_file=task_control_token_file,
require_root_owner=True,
),
execution_plan_provider=ExecutionPlanDirectoryProvider(
root=execution_plan_dir,
require_root_owner=True,
),
)
service = TaskAdmissionBootstrapService(
store=TaskAdmissionStore(state_db),
authorization=authorization,
task_port=task_port,
policy=task_admission_policy_from_epoch(epoch),
post_admission=post_admission,
)
return TaskAdmissionBridgeContract(service).handlers()
def build_task_admission_handlers_from_activation_file(
activation_file: Path,
*,
caller_service: str = "aios-chatgpt-bridge-v2-staging.service",
post_admission: Callable[[TaskAdmissionGrant, Mapping[str, Any]], Any] | None = None,
) -> Mapping[str, Any]:
"""Load one root-owned, fail-closed staging activation descriptor."""
_host_require_regular(activation_file, root_owned=True)
value = _host_read_json_object(activation_file)
required = {
"schema",
"epoch_root",
"epoch_id",
"revocation_file",
"execution_plan_dir",
"state_db",
"task_api_url",
"task_control_token_file",
"owner_verifier_module",
}
if set(value) != required or value.get("schema") != "aios.task-admission-host-activation.v1":
raise TaskAdmissionHostError("TASK_ADMISSION_ACTIVATION_FILE_INVALID")
path_fields = (
"epoch_root",
"revocation_file",
"execution_plan_dir",
"state_db",
"task_control_token_file",
"owner_verifier_module",
)
paths: dict[str, Path] = {}
for key in path_fields:
raw = value.get(key)
if not isinstance(raw, str) or not raw.startswith("/"):
raise TaskAdmissionHostError("TASK_ADMISSION_ACTIVATION_PATH_INVALID")
path = Path(raw)
if ".." in path.parts:
raise TaskAdmissionHostError("TASK_ADMISSION_ACTIVATION_PATH_INVALID")
paths[key] = path
epoch_id = value.get("epoch_id")
task_api_url = value.get("task_api_url")
if not isinstance(epoch_id, str) or not epoch_id:
raise TaskAdmissionHostError("TASK_ADMISSION_ACTIVATION_EPOCH_INVALID")
if task_api_url != "http://127.0.0.1:18940":
raise TaskAdmissionHostError("TASK_ADMISSION_ACTIVATION_TASK_API_INVALID")
return _build_task_admission_handlers(
caller_service=caller_service,
epoch_root=paths["epoch_root"],
epoch_id=epoch_id,
revocation_file=paths["revocation_file"],
execution_plan_dir=paths["execution_plan_dir"],
state_db=paths["state_db"],
task_api_url=task_api_url,
task_control_token_file=paths["task_control_token_file"],
owner_verifier_module=paths["owner_verifier_module"],
post_admission=post_admission,
)
def build_task_admission_handlers_from_environment(
*,
caller_service: str = "aios-chatgpt-bridge-v2-staging.service",
post_admission: Callable[[TaskAdmissionGrant, Mapping[str, Any]], Any] | None = None,
) -> Mapping[str, Any]:
"""Compose all three handlers from server-side trusted environment only."""
required = (
"AIOS_TASK_ADMISSION_EPOCH_ROOT",
"AIOS_TASK_ADMISSION_EPOCH_ID",
"AIOS_TASK_ADMISSION_REVOCATION_FILE",
"AIOS_TASK_ADMISSION_EXECUTION_PLAN_DIR",
)
missing = [
key
for key in required
if not os.environ.get(key, "").strip()
]
if missing:
raise TaskAdmissionHostError(
"TASK_ADMISSION_HOST_ENV_MISSING:"
+ ",".join(sorted(missing))
)
return _build_task_admission_handlers(
caller_service=caller_service,
epoch_root=Path(os.environ["AIOS_TASK_ADMISSION_EPOCH_ROOT"]),
epoch_id=os.environ["AIOS_TASK_ADMISSION_EPOCH_ID"],
revocation_file=Path(os.environ["AIOS_TASK_ADMISSION_REVOCATION_FILE"]),
execution_plan_dir=Path(os.environ["AIOS_TASK_ADMISSION_EXECUTION_PLAN_DIR"]),
state_db=Path(os.environ.get(
"AIOS_TASK_ADMISSION_STATE_DB",
"/var/lib/aios-gitops/bridge-v2-staging/task-admission.sqlite3",
)),
task_api_url=os.environ.get(
"AIOS_AGENT_TASK_API_URL",
"http://127.0.0.1:18940",
),
task_control_token_file=Path(os.environ.get(
"AIOS_AGENT_TASK_CONTROL_TOKEN_FILE",
"/etc/aios-gitops/agent-task-control.token",
)),
owner_verifier_module=Path(os.environ.get(
"AIOS_OWNER_APPROVAL_VERIFIER_MODULE",
"/opt/aios/agent-task-api/"
"914eb324f657a695e2635f80cf53202eba636eb350630b12883e2741ce67e535"
"-p0-verification/runtime/app/aios_owner_approval_verifier.py",
)),
post_admission=post_admission,
)
@@ -428,7 +428,7 @@ def _append(path: Path, row: Mapping[str, Any]) -> None:
fd = os.open(path, os.O_WRONLY | os.O_CREAT | os.O_APPEND, 0o600)
try:
fcntl.flock(fd, fcntl.LOCK_EX)
os.write(fd, pretty_json(dict(row)))
os.write(fd, canonical_json(dict(row)) + b"\n")
os.fsync(fd)
fcntl.flock(fd, fcntl.LOCK_UN)
finally:
@@ -437,17 +437,45 @@ def _append(path: Path, row: Mapping[str, Any]) -> None:
def _receipts(path: Path) -> list[dict[str, Any]]:
if not path.is_file() or path.is_symlink():
if not path.exists() and not path.is_symlink():
return []
if path.is_symlink() or not path.is_file():
raise ProvisioningError("RECEIPT_LOG_INVALID")
try:
text = path.read_text(encoding="utf-8")
except (OSError, UnicodeDecodeError) as exc:
raise ProvisioningError("RECEIPT_LOG_INVALID") from exc
if not text:
return []
# New receipts are canonical JSONL: exactly one object per physical line.
# If that framing does not parse, accept exactly one complete legacy object
# so the original pretty-printed receipt remains immutable and replayable.
lines = text.splitlines()
result: list[dict[str, Any]] = []
for line in path.read_text(encoding="utf-8").splitlines():
jsonl_valid = bool(lines)
for line in lines:
if not line:
jsonl_valid = False
break
try:
value = json.loads(line)
except json.JSONDecodeError:
continue
if isinstance(value, dict):
result.append(value)
return result
jsonl_valid = False
break
if not isinstance(value, dict):
jsonl_valid = False
break
result.append(value)
if jsonl_valid:
return result
try:
legacy = json.loads(text)
except json.JSONDecodeError as exc:
raise ProvisioningError("RECEIPT_LOG_INVALID") from exc
if not isinstance(legacy, dict):
raise ProvisioningError("RECEIPT_LOG_INVALID")
return [legacy]
def _run_signature_verify(config: ProvisionerConfig, payload: bytes, signature: Path) -> None:
@@ -465,6 +493,26 @@ def _run_signature_verify(config: ProvisionerConfig, payload: bytes, signature:
raise ProvisioningError("OWNER_SIGNATURE_VERIFY_FAILED")
def _verify_service_policy(config: ProvisionerConfig) -> None:
if config.service_unit.is_symlink() or not config.service_unit.is_file():
raise ProvisioningError("BUILDER_SERVICE_UNIT_MISSING")
text = config.service_unit.read_text(encoding="utf-8")
required = (
"User=root", "LoadCredentialEncrypted=signing_private_key:",
"LoadCredentialEncrypted=builder_hmac:", "NoNewPrivileges=true",
"PrivateNetwork=true", "RestrictAddressFamilies=AF_UNIX",
)
if any(item not in text for item in required):
raise ProvisioningError("BUILDER_SERVICE_POLICY_INVALID")
def _git(config: ProvisionerConfig, *args: str) -> subprocess.CompletedProcess:
return subprocess.run(
["git", "--git-dir", str(config.authority_git_dir), *args],
capture_output=True, check=False, timeout=30,
)
def _verify_source(config: ProvisionerConfig, source: Mapping[str, Any]) -> None:
live_main = subprocess.run(
["git", "--git-dir", str(config.authority_git_dir), "rev-parse", "refs/heads/main"],
@@ -495,16 +543,38 @@ def _verify_source(config: ProvisionerConfig, source: Mapping[str, Any]) -> None
actual = _digest_file(config.source_root / relative)
if actual != expected:
raise ProvisioningError("SOURCE_EVIDENCE_DIGEST_MISMATCH")
if config.service_unit.is_symlink() or not config.service_unit.is_file():
raise ProvisioningError("BUILDER_SERVICE_UNIT_MISSING")
text = config.service_unit.read_text(encoding="utf-8")
required = (
"User=root", "LoadCredentialEncrypted=signing_private_key:",
"LoadCredentialEncrypted=builder_hmac:", "NoNewPrivileges=true",
"PrivateNetwork=true", "RestrictAddressFamilies=AF_UNIX",
)
if any(item not in text for item in required):
raise ProvisioningError("BUILDER_SERVICE_POLICY_INVALID")
_verify_service_policy(config)
def _verify_replay_source(config: ProvisionerConfig, source: Mapping[str, Any]) -> None:
"""Verify the historical f8-bound source after authority main advances."""
commit = source["protected_merge_commit"]
live_main = _git(config, "rev-parse", "refs/heads/main")
tree = _git(config, "rev-parse", f"{commit}^{{tree}}")
parents = _git(config, "rev-list", "--parents", "-n", "1", commit)
source_ancestry = _git(config, "merge-base", "--is-ancestor", commit, live_main.stdout.strip().decode("ascii", "ignore"))
pr_ancestry = _git(config, "merge-base", "--is-ancestor", source["pr_head_oid"], commit)
expected_parents = [commit, *source["merge_parent_oids"]]
if (
live_main.returncode != 0
or tree.returncode != 0
or tree.stdout.strip().decode("ascii", "ignore").lower() != source["gitea_main_tree_oid"]
or parents.returncode != 0
or parents.stdout.decode("utf-8", "replace").strip().split() != expected_parents
or source_ancestry.returncode != 0
or pr_ancestry.returncode != 0
):
raise ProvisioningError("AUTHORITY_HISTORICAL_SOURCE_DRIFT")
if source["merge_parent_oids"][1] != source["pr_head_oid"]:
raise ProvisioningError("SOURCE_PR_HEAD_SECOND_PARENT_INVALID")
for relative, expected in source["source_evidence_sha256"].items():
historical = _git(config, "show", f"{commit}:{relative}")
if historical.returncode != 0 or sha256_bytes(historical.stdout) != expected:
raise ProvisioningError("SOURCE_EVIDENCE_DIGEST_MISMATCH")
# The installed repair package must still satisfy the governed package and
# service policy, but its bytes are expected to differ from historical f8.
verify_package_contract(config.source_root)
_verify_service_policy(config)
def _keyring(identity: Mapping[str, Any], public: bytes, now: dt.datetime) -> dict[str, Any]:
@@ -537,14 +607,158 @@ def _encrypt(config: ProvisionerConfig, plain: Path, target: Path, name: str) ->
os.chmod(target, 0o600)
def _require_private_regular_file(path: Path, code: str) -> None:
if path.is_symlink() or not path.is_file():
raise ProvisioningError(code)
metadata = path.stat()
if stat.S_IMODE(metadata.st_mode) != 0o600 or metadata.st_size <= 0:
raise ProvisioningError(code)
def _read_state_json(path: Path, code: str) -> dict[str, Any]:
_require_private_regular_file(path, code)
try:
value = json.loads(path.read_text(encoding="utf-8"))
except (OSError, UnicodeDecodeError, json.JSONDecodeError) as exc:
raise ProvisioningError(code) from exc
if not isinstance(value, dict):
raise ProvisioningError(code)
return value
def _parse_active_window(record: Mapping[str, Any]) -> None:
try:
not_before = dt.datetime.fromisoformat(str(record["not_before"]).replace("Z", "+00:00"))
expires_at = dt.datetime.fromisoformat(str(record["expires_at"]).replace("Z", "+00:00"))
except (KeyError, ValueError) as exc:
raise ProvisioningError("KEYRING_BINDING_INVALID") from exc
now = utc_now()
if not_before.tzinfo is None or expires_at.tzinfo is None or not (not_before <= now < expires_at):
raise ProvisioningError("KEYRING_BINDING_INVALID")
def _verify_existing_state(
config: ProvisionerConfig,
payload: Mapping[str, Any],
raw: bytes,
signature_bytes: bytes,
row: Mapping[str, Any],
) -> None:
receipt_keys = {
"schema", "status", "created_at", "operation_key", "payload_sha256",
"signature_sha256", "source_commit", "source_tree", "key_id",
"generation", "algorithm", "purpose", "service_principal",
"service_principal_bound", "public_key_sha256",
"private_key_exportable", "credential_projection",
"negative_capability_tests", "production_mutation",
"protected_ref_mutation", "tag_mutation", "baseline_mutation",
}
if set(row) != receipt_keys:
raise ProvisioningError("RECEIPT_CONFLICT")
try:
created_at = dt.datetime.fromisoformat(str(row["created_at"]).replace("Z", "+00:00"))
except ValueError as exc:
raise ProvisioningError("RECEIPT_CONFLICT") from exc
identity = payload["signing_identity"]
source = payload["source"]
expected = {
"schema": "aios.trusted-builder-signer-provisioning-receipt.v1",
"status": "COMPLETED",
"operation_key": "payload:" + sha256_bytes(raw) + ":signature:" + sha256_bytes(signature_bytes),
"payload_sha256": sha256_bytes(raw),
"signature_sha256": sha256_bytes(signature_bytes),
"source_commit": source["gitea_main_oid"],
"source_tree": source["gitea_main_tree_oid"],
"key_id": identity["key_id"],
"generation": identity["key_generation"],
"algorithm": "Ed25519",
"purpose": PURPOSE,
"service_principal": identity["service_principal"],
"service_principal_bound": True,
"private_key_exportable": False,
"credential_projection": "systemd-encrypted-host-credential",
"negative_capability_tests": {name: True for name in sorted(DENIED_CAPABILITIES)},
"production_mutation": False,
"protected_ref_mutation": False,
"tag_mutation": False,
"baseline_mutation": False,
}
if created_at.tzinfo is None or any(row.get(key) != value for key, value in expected.items()):
raise ProvisioningError("RECEIPT_CONFLICT")
public_keyring = _read_state_json(config.public_keyring, "KEYRING_BINDING_INVALID")
if set(public_keyring) != {"algorithm", "purpose", "keys", "keyring_sha256"}:
raise ProvisioningError("KEYRING_BINDING_INVALID")
keys = public_keyring.get("keys")
if not isinstance(keys, list) or len(keys) != 1 or not isinstance(keys[0], Mapping):
raise ProvisioningError("KEYRING_BINDING_INVALID")
record = dict(keys[0])
if set(record) != {
"algorithm", "purpose", "key_id", "generation", "not_before",
"expires_at", "status", "public_key", "public_key_sha256",
}:
raise ProvisioningError("KEYRING_BINDING_INVALID")
try:
public_key = base64.b64decode(str(record["public_key"]), validate=True)
except (ValueError, TypeError) as exc:
raise ProvisioningError("KEYRING_BINDING_INVALID") from exc
public_sha = sha256_bytes(public_key)
keyring_body = {key: public_keyring[key] for key in ("algorithm", "purpose", "keys")}
if (
len(public_key) != 32
or public_keyring["algorithm"] != "Ed25519"
or public_keyring["purpose"] != PURPOSE
or record.get("algorithm") != "Ed25519"
or record.get("purpose") != PURPOSE
or record.get("key_id") != identity["key_id"]
or record.get("generation") != identity["key_generation"]
or record.get("status") != "ACTIVE"
or record.get("public_key_sha256") != public_sha
or row.get("public_key_sha256") != public_sha
or public_keyring.get("keyring_sha256") != sha256_bytes(canonical_json(keyring_body))
):
raise ProvisioningError("KEYRING_BINDING_INVALID")
_parse_active_window(record)
binding = _read_state_json(config.binding_file, "SERVICE_PRINCIPAL_BINDING_INVALID")
expected_binding = {
"schema": "aios.trusted-builder.service-principal-binding.v1",
"purpose": PURPOSE,
"service_principal": identity["service_principal"],
"key_id": identity["key_id"],
"generation": identity["key_generation"],
"algorithm": "Ed25519",
"private_key_export": False,
"public_key_sha256": public_sha,
"source_commit": source["gitea_main_oid"],
"source_tree": source["gitea_main_tree_oid"],
"payload_sha256": sha256_bytes(raw),
}
if binding != expected_binding:
raise ProvisioningError("SERVICE_PRINCIPAL_BINDING_INVALID")
if (
config.credential_store.is_symlink()
or not config.credential_store.is_dir()
or stat.S_IMODE(config.credential_store.stat().st_mode) != 0o700
):
raise ProvisioningError("CREDENTIAL_STORE_INVALID")
_require_private_regular_file(
config.credential_store / "signing_private_key", "RECEIPT_OUTPUT_MISSING"
)
_require_private_regular_file(
config.credential_store / "builder_hmac", "RECEIPT_OUTPUT_MISSING"
)
def provision(config: ProvisionerConfig, payload_path: Path, signature_path: Path) -> dict[str, Any]:
if os.name != "nt" and os.geteuid() != 0:
raise ProvisioningError("ROOT_REQUIRED")
raw, payload = _read_json(payload_path)
_validate_payload(payload)
_run_signature_verify(config, raw, signature_path)
_verify_source(config, payload["source"])
operation_key = "payload:" + sha256_bytes(raw) + ":signature:" + sha256_bytes(signature_path.read_bytes())
signature_bytes = signature_path.read_bytes()
operation_key = "payload:" + sha256_bytes(raw) + ":signature:" + sha256_bytes(signature_bytes)
lock_path = config.receipt_log.with_suffix(".lock")
_safe_parent(lock_path)
with lock_path.open("a+b") as lock:
@@ -553,12 +767,24 @@ def provision(config: ProvisionerConfig, payload_path: Path, signature_path: Pat
previous = [row for row in rows if row.get("operation_key") == operation_key]
if previous:
row = previous[-1]
if row.get("status") != "COMPLETED" or row.get("purpose") != PURPOSE or row.get("key_id") != payload["signing_identity"]["key_id"]:
if any(canonical_json(item) != canonical_json(row) for item in previous):
raise ProvisioningError("RECEIPT_CONFLICT")
for required in (config.public_keyring, config.binding_file, config.credential_store / "signing_private_key", config.credential_store / "builder_hmac"):
if required.is_symlink() or not required.is_file():
raise ProvisioningError("RECEIPT_OUTPUT_MISSING")
return {"status": "IDEMPOTENT_REPLAY_PASS", "idempotent": True, **{key: row[key] for key in ("operation_key", "payload_sha256", "signature_sha256", "key_id", "generation", "public_key_sha256", "purpose", "service_principal_bound", "private_key_exportable")}}
_verify_replay_source(config, payload["source"])
_verify_existing_state(config, payload, raw, signature_bytes, row)
return {
"status": "IDEMPOTENT_REPLAY_PASS",
"idempotent": True,
"mutation_executed": False,
"existing_signer_state_adopted": True,
**{key: row[key] for key in (
"operation_key", "payload_sha256", "signature_sha256",
"source_commit", "source_tree", "key_id", "generation",
"public_key_sha256", "purpose", "service_principal_bound",
"private_key_exportable",
)},
"negative_capability_tests": row["negative_capability_tests"],
}
_verify_source(config, payload["source"])
outputs = [config.public_keyring, config.binding_file, config.credential_store / "signing_private_key", config.credential_store / "builder_hmac"]
if any(path.exists() or path.is_symlink() for path in outputs):
raise ProvisioningError("PREEXISTING_STATE_CONFLICT")
@@ -612,7 +838,7 @@ def provision(config: ProvisionerConfig, payload_path: Path, signature_path: Pat
"created_at": iso(now),
"operation_key": operation_key,
"payload_sha256": sha256_bytes(raw),
"signature_sha256": sha256_bytes(signature_path.read_bytes()),
"signature_sha256": sha256_bytes(signature_bytes),
"source_commit": payload["source"]["gitea_main_oid"],
"source_tree": payload["source"]["gitea_main_tree_oid"],
"key_id": payload["signing_identity"]["key_id"],
@@ -631,7 +857,7 @@ def provision(config: ProvisionerConfig, payload_path: Path, signature_path: Pat
"baseline_mutation": False,
}
_append(config.receipt_log, receipt)
return {"status": "PROVISIONED", "idempotent": False, **{key: receipt[key] for key in ("operation_key", "payload_sha256", "signature_sha256", "source_commit", "source_tree", "key_id", "generation", "purpose", "service_principal_bound", "public_key_sha256", "private_key_exportable")}, "negative_capability_tests": receipt["negative_capability_tests"]}
return {"status": "PROVISIONED", "idempotent": False, "mutation_executed": True, "existing_signer_state_adopted": False, **{key: receipt[key] for key in ("operation_key", "payload_sha256", "signature_sha256", "source_commit", "source_tree", "key_id", "generation", "purpose", "service_principal_bound", "public_key_sha256", "private_key_exportable")}, "negative_capability_tests": receipt["negative_capability_tests"]}
finally:
shutil.rmtree(temp_root, ignore_errors=True)
@@ -0,0 +1,102 @@
# ADR: P0-0 Task Admission Bootstrap without weakening TaskPrincipal
Date: 2026-09-03
Status: Candidate for semantic port
## Context
The live control plane can expose governed writer operations while still being
unable to create the Agent Task/TaskLease required by TaskPrincipal. This is a
bootstrap deadlock, not a reason to enable global repository writes.
The safety invariant is:
```text
Owner / durable delegation
-> Task admission grant
-> real Agent Task
-> claim + heartbeat + TaskLease
-> TaskPrincipal
-> WriterLease
-> governed commit
```
`GovernedTaskBootstrap.create()` may create a GitOps binding/Job. It is not, by
itself, evidence of an Agent Task claim or TaskLease.
## Decision
Introduce a narrow Task Admission Bootstrap substrate with three operations:
- `getTaskAdmissionEnvelope`
- `submitTaskAdmissionAuthorization`
- `getTaskAdmissionStatus`
The envelope normalizes and binds repository, source OID, target branch,
generation, execution digest, capabilities, file allowlist, risk, policy digest,
expiry and nonce.
Authorization is accepted only from:
1. an already-verified `OwnerDelegationEpoch`; or
2. an Owner signature over the exact canonical envelope.
There is deliberately no permissive default authorization backend.
The derived `TaskAdmissionGrant` can only narrow the parent authorization.
Production, tag, baseline, main, global-write, force-push, runtime-anchor and
arbitrary-shell capabilities are explicitly rejected.
The grant is passed to the existing Agent Task control plane via an injected
`TaskAdmissionPort`. This component never writes the Task database.
The production adapter accepts only a service-managed official Task transport;
it never returns its control credential. It first reconciles the exact task
identity, then POSTs the official `/v1/tasks` contract only if absent, and
reconciles an ambiguous response before reporting `UNKNOWN`. Task receipts use
actual `QUEUED`, `CLAIMED`, or `RUNNING` state, never a synthetic ready state.
Durable epochs are resolved through an injected provider that receives the
authenticated caller identity from trusted middleware. A body-supplied epoch
identifier is only a lookup key. The existing durable preparation authorization
does not become file-write or TaskPrincipal authority.
TaskPrincipal is not used to bootstrap the Task. It remains downstream of a
real claim/TaskLease.
## Crash / replay semantics
Envelope creation is idempotent by exact request digest.
Authorization/admission is durable and exact replay returns the same Task
receipt. A SQLite `BEGIN IMMEDIATE` claim persists the immutable grant and its
authorization-proof digest before the Task side effect. Cross-instance callers
observe the durable envelope winner; a different proof cannot replace the
stored grant. The live Task API retains the second exact-identity boundary.
A crash after authorization but before Task API admission must resume the same
envelope/grant identity. It must not create a replacement task or widen scope.
## Integration rule for current live authority
The source snapshot used for this candidate is not proven byte-identical to the
2026-09-03 live authority. Therefore the code is a semantic-port candidate.
The live integration must:
- preserve existing TaskPrincipal verification;
- adapt `TaskAdmissionPort` to the actual Agent Task API;
- adapt `OwnerAuthorizationBackend` to the actual trusted Owner/delegation
verifier;
- expose admission routes outside the TaskPrincipal gate, because the
cryptographic/delegation proof is the bootstrap authority;
- never turn the runtime anchor into a writer;
- never use Gitea/SSH/downstream credentials as Owner authority;
- publish OpenAPI/capability metadata only when handlers are actually wired.
## Consequences
The control plane can become unattended after one durable Owner delegation
epoch, while retaining fail-closed boundaries. If no valid delegation exists,
the only legitimate stop is an already-generated canonical signing envelope
awaiting the Owner's cryptographic signature.
@@ -0,0 +1,109 @@
# AIOS Task Admission Bootstrap OpenAPI fragment.
# Integrate into the canonical Bridge OpenAPI only when the corresponding live
# handlers and authorization backend are configured. This fragment does not
# enable legacy repository mutation or change TaskPrincipal/WriterLease gates.
components:
schemas:
TaskAdmissionEnvelopeRequest:
type: object
additionalProperties: false
required:
- idempotency_key
- task_id
- repository
- authority_base_oid
- target_branch
- generation
- execution_digest
- requested_capabilities
- file_allowlist
- risk
properties:
idempotency_key: {type: string, minLength: 8, maxLength: 256}
task_id: {type: string, minLength: 3, maxLength: 180}
repository: {type: string, enum: [aios/aios]}
authority_base_oid: {type: string, pattern: '^[0-9a-f]{40}$'}
target_branch: {type: string, pattern: '^agent/.+'}
generation: {type: integer, minimum: 1}
execution_digest: {type: string, pattern: '^[0-9a-f]{64}$'}
requested_capabilities:
type: array
minItems: 1
uniqueItems: true
items: {type: string}
file_allowlist:
type: array
minItems: 1
uniqueItems: true
items: {type: string}
risk: {type: string, enum: [low, medium]}
expires_in_seconds:
type: integer
minimum: 60
maximum: 900
TaskAdmissionAuthorizationRequest:
type: object
additionalProperties: false
required: [envelope_id, authorization_kind]
properties:
envelope_id: {type: string}
authorization_kind:
type: string
enum: [delegation_epoch, owner_signature]
delegation_epoch_id: {type: string}
owner_key_id: {type: string}
signature_base64: {type: string}
paths:
/v2/task-admission/envelope:
post:
operationId: getTaskAdmissionEnvelope
summary: Build and persist a canonical narrow Task admission envelope
description: >
No Git/Task mutation occurs. The server normalizes repository, branch,
capabilities, file allowlist, risk, policy digest, expiry and nonce.
requestBody:
required: true
content:
application/json:
schema: {$ref: '#/components/schemas/TaskAdmissionEnvelopeRequest'}
responses:
'200': {description: Canonical envelope and signing payload}
'403': {description: Requested scope exceeds bootstrap policy}
'409': {description: Idempotency key conflicts with a different request}
/v2/task-admission/authorize:
post:
operationId: submitTaskAdmissionAuthorization
summary: Verify durable delegation or Owner signature and admit one exact Task
description: >
This operation does not accept a general-purpose bearer as authority.
Authorization is proven by a verified OwnerDelegationEpoch or by the
Owner signature over the exact canonical envelope. The resulting grant
can only narrow scope and is handed to the existing Agent Task API.
A delegation identifier in caller JSON is insufficient: the backend
binds the verified parent epoch to the authenticated service caller.
requestBody:
required: true
content:
application/json:
schema: {$ref: '#/components/schemas/TaskAdmissionAuthorizationRequest'}
responses:
'200': {description: Exact QUEUED, CLAIMED, or RUNNING Task receipt or idempotent replay}
'403': {description: Invalid, expired, revoked or widened authorization}
'409': {description: Conflicting idempotent admission}
/v2/task-admission/status:
get:
operationId: getTaskAdmissionStatus
summary: Read durable Task-admission state
parameters:
- in: query
name: envelope_id
required: true
schema: {type: string}
responses:
'200': {description: Pending or admitted receipt}
'404': {description: Envelope not found}
@@ -0,0 +1,102 @@
# Task Admission Bootstrap P0 Runbook
## Goal
Repair the Task-admission bootstrap chain without enabling legacy/global writes.
## Preconditions
Re-read live authority, runtime mode, OpenAPI, capabilities and current Task
API. Do not assume the package source paths are current.
Required invariants:
```text
AIOS_GITOPS_WRITES_ENABLED=false
legacy_global_writes_enabled=false
18893=IMMUTABLE_STAGING
main/tag/baseline/production writes=false
```
## Semantic-port sequence
1. Locate the live Agent Task create/admission implementation.
2. Locate the current Owner/delegation signature verifier.
3. Port `aios_task_admission_bootstrap.py` as the common substrate.
4. Implement `TaskAdmissionPort.admit()` with the official Agent Task API.
5. Implement `OwnerAuthorizationBackend` with the current trusted verifier.
6. Wire the three task-admission operations into the existing Bridge.
7. Keep TaskPrincipal issuance downstream of real Task claim/TaskLease.
8. Merge the OpenAPI fragment only after the live handlers are wired.
9. Run impacted Linux/POSIX, TaskPrincipal, Bridge and writer regression.
10. Create the bootstrap-repair governed commit through the existing sanctioned
recovery path.
11. Use the repaired admission path to create the Repository Discovery P0 Task.
12. Claim/heartbeat -> TaskLease -> TaskPrincipal -> WriterLease.
13. Commit only the existing Repository Discovery allowlist through
`commitFilesGoverned`.
14. Reconcile ambiguous outcomes before any retry.
Admission uses a per-envelope, crash-releasing SQLite lock under the
admission state path's `.locks` directory. It persists the immutable
grant before an effect, then the official Task API GET reconciles any
crashed or ambiguous effect before a POST is permitted.
15. Replays revalidate the supplied proof, policy, expiry and revocation before
reading a receipt. A different otherwise-valid proof conflicts with the
persisted authorization digest; it never becomes a harmless receipt replay.
16. The Task API adapter must receive a server-resolved trusted execution plan
for the signed execution digest. It supplies `metadata.execution` with
the exact repository, branch, generation, digest and nonempty external job
identifier. Do not derive a job identity from request JSON or generate one.
17. Require Authority / remote Git / API OID equality and idempotent replay.
18. Publish immutable staging artifact and run Repository Discovery live E2E.
## Never do
- direct Task DB inserts/updates;
- direct bare-repo/ref changes;
- raw Git push as a substitute for governed publication;
- borrow SSH/Gitea/service credentials as Owner authorization;
- disable TaskPrincipal verification;
- issue TaskPrincipal without a live TaskLease;
- set global writes true;
- turn 18893 into a writer;
- include production/tag/baseline/main permissions in a task grant.
## Acceptance
Bootstrap repair:
```text
ENVELOPE_EXACT_REPLAY=PASS
SCOPE_NARROWING=PASS
DELEGATION_REVOKE_EXPIRY=PASS
OWNER_SIGNATURE_VERIFY=PASS
CONCURRENT_ADMISSION_SINGLE_TASK=PASS
TASK_DB_DIRECT_WRITE=false
TASKPRINCIPAL_BYPASS=false
GLOBAL_WRITES=false
CRASH_AFTER_EFFECT_REPLAY=PASS
CRASH_BEFORE_EFFECT_REPLAY=PASS
CHANGED_VALID_PROOF_CONFLICT=PASS
TASK_API_EXACT_IDENTITY=PASS
```
These are implementation and test acceptance conditions only. They are not
evidence of a live claim, governed publication, remote OID equality, or staging
E2E execution; obtain those independently through the sanctioned live runbook.
Repository Discovery publication:
```text
TASK_CREATED=PASS
TASK_CLAIMED=PASS
TASK_HEARTBEAT=PASS
TASK_LEASE=PASS
TASK_PRINCIPAL=PASS
WRITER_LEASE=PASS
COMMIT_FILES_GOVERNED=PASS
AUTHORITY_REMOTE_API_OID_EQUAL=PASS
IDEMPOTENCY_REPLAY=PASS
STAGING_LIVE_E2E=PASS
TASK_VERIFIED=SUCCEEDED
```
+543
View File
@@ -0,0 +1,543 @@
"""Read-only Phase 2 runtime introspection with a bounded SSH transport.
The wrapper is deliberately sent as ``subprocess.run(input=payload)``. No
JSON, ABI data, or base64 representation is placed in the Windows SSH argv;
the default argv is the fixed, short command documented by
``DEFAULT_REMOTE_COMMAND``.
The remote wrapper reads the current issuer unit's MainPID, WorkingDirectory,
and PYTHONPATH, then resolves only the named importable modules. It does not
walk release trees, ``/opt``, historical worktrees, SOUL, or MEMORY, and it
does not call a database, signer, Bridge, Task API, or mutation endpoint.
This module is locally testable, but the module import and systemd/POSIX
behaviour still require external Linux live validation. Phase 2 status is
always explicit about all mutation classes being false.
"""
from __future__ import annotations
import argparse
import ast
import hashlib
import json
import subprocess
import sys
from pathlib import Path
from typing import Any, Callable, Mapping, Sequence
DEFAULT_SSH = "ssh.exe"
DEFAULT_HOST = "hw"
DEFAULT_REMOTE_COMMAND = 'test "$(hostname)" = cn-network-mkt && sudo -n python3 -'
DEFAULT_TIMEOUT_SECONDS = 60
DEFAULT_ISSUER_UNIT = "aios-task-principal-issuer.service"
DEFAULT_ADMISSION_MODULE = "aios_task_principal_admission"
DEFAULT_MIGRATION_MODULE = "aios_task_principal_storage_migration"
DEFAULT_REQUIRED_ADMISSION_FUNCTIONS = (
"offline_migrate_task_principal_schema",
"supersede_p0_scope",
)
DEFAULT_MIGRATION_VERIFIER = "SSHOwnerBootstrapVerifier"
MAX_ARGV_BYTES = 512
MAX_REQUEST_BYTES = 32 * 1024
MUTATION_FLAGS = (
"task_db_mutation",
"scope_mutation",
"git_mutation",
"service_mutation",
)
class RemoteTransportError(RuntimeError):
"""A safe, non-secret description of an SSH transport failure."""
class Phase2StatusError(RuntimeError):
"""The remote result was not a valid fail-closed Phase2 status."""
def build_ssh_argv(
ssh: str = DEFAULT_SSH,
host: str = DEFAULT_HOST,
) -> list[str]:
"""Return the only SSH argv shape permitted by the Phase2 transport."""
argv = [
ssh,
"-T",
"-o",
"BatchMode=yes",
"-o",
"ConnectTimeout=12",
host,
DEFAULT_REMOTE_COMMAND,
]
argv_bytes = sum(len(part.encode("utf-8")) + 1 for part in argv)
if argv_bytes > MAX_ARGV_BYTES:
raise ValueError("SSH argv exceeds the Phase2 hard limit")
return argv
def run_remote_python(
payload: bytes,
*,
ssh: str = DEFAULT_SSH,
host: str = DEFAULT_HOST,
timeout: float = DEFAULT_TIMEOUT_SECONDS,
runner: Callable[..., Any] = subprocess.run,
) -> bytes:
"""Send *payload* as stdin to the fixed remote Python command.
Error messages intentionally contain no stderr text. SSH stderr can
include command output, environment values, or credentials emitted by a
misconfigured remote service.
"""
if not isinstance(payload, bytes):
raise TypeError("payload must be bytes")
if timeout <= 0:
raise ValueError("timeout must be positive")
argv = build_ssh_argv(ssh=ssh, host=host)
try:
completed = runner(
argv,
input=payload,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
timeout=timeout,
check=False,
)
except subprocess.TimeoutExpired as exc:
raise RemoteTransportError(
f"remote SSH timed out after {timeout:g} seconds"
) from exc
except OSError as exc:
raise RemoteTransportError("remote SSH could not be started") from exc
return_code = getattr(completed, "returncode", None)
if return_code != 0:
raise RemoteTransportError(f"remote SSH exited with status {return_code}")
output = getattr(completed, "stdout", b"")
if isinstance(output, str):
output = output.encode("utf-8")
if not isinstance(output, bytes):
raise RemoteTransportError("remote SSH returned a non-byte stdout value")
return output
def module_sha256(path: str | Path) -> str:
"""Hash one resolved module file without searching neighbouring files."""
digest = hashlib.sha256()
with Path(path).open("rb") as source:
for block in iter(lambda: source.read(1024 * 1024), b""):
digest.update(block)
return digest.hexdigest()
def validate_public_function_ast(
source: str,
required_functions: Sequence[str] = DEFAULT_REQUIRED_ADMISSION_FUNCTIONS,
) -> dict[str, Any]:
"""Validate public symbols and the source-level export mechanism.
The live admission overlay intentionally re-exports the legacy public API
through a small ``globals()[name] = value`` loop. Requiring every public
function to be a direct ``def`` would therefore reject the exact deployed
module even though the runtime symbol and signature are present. The AST
check still proves that the module has either a direct/imported binding or
an explicit dynamic re-export mechanism; the caller separately verifies
the runtime callable and its signature.
"""
try:
tree = ast.parse(source)
except SyntaxError as exc:
return {
"ok": False,
"missing": list(required_functions),
"error": f"AST_PARSE_ERROR:{exc.msg}",
}
top_level = {
node.name: node
for node in tree.body
if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef))
and not node.name.startswith("_")
}
imported = {
alias.asname or alias.name.split(".", 1)[0]
for node in tree.body
if isinstance(node, (ast.Import, ast.ImportFrom))
for alias in node.names
if alias.name != "*"
}
dynamic_reexport = any(
isinstance(node, ast.Call)
and isinstance(node.func, ast.Name)
and node.func.id == "globals"
for node in ast.walk(tree)
)
bound = set(top_level) | imported
missing = [
name
for name in required_functions
if name not in bound and not dynamic_reexport
]
return {
"ok": not missing,
"missing": missing,
"functions": sorted(top_level),
"imported": sorted(imported),
"dynamic_reexport": dynamic_reexport,
}
def make_phase2_status(
status: str,
errors: Sequence[str] = (),
**details: Any,
) -> dict[str, Any]:
"""Create a status object whose mutation flags are always false."""
result: dict[str, Any] = {
"kind": "phase2-status",
"version": 1,
"status": status,
"read_only": True,
"fail_closed": status != "PASS",
}
result.update({flag: False for flag in MUTATION_FLAGS})
if errors:
result["errors"] = list(errors)
result.update(details)
return result
def validate_phase2_status(status: Mapping[str, Any]) -> dict[str, Any]:
"""Validate the result contract and reject any mutation claim."""
if status.get("kind") != "phase2-status":
raise Phase2StatusError("remote result is not phase2-status")
for flag in MUTATION_FLAGS:
if status.get(flag) is not False:
raise Phase2StatusError(f"remote result has invalid {flag}")
if status.get("status") not in {"PASS", "FAIL"}:
raise Phase2StatusError("remote result has invalid status")
return dict(status)
def _json_request(request: Mapping[str, Any]) -> str:
"""Serialize only a small request into the stdin-delivered wrapper."""
encoded = json.dumps(dict(request), ensure_ascii=True, separators=(",", ":"))
if len(encoded.encode("utf-8")) > MAX_REQUEST_BYTES:
raise ValueError("Phase2 request is unexpectedly large")
return encoded
REMOTE_SCRIPT_PREFIX = r'''# Phase2 read-only runtime wrapper. Do not add mutations here.
import ast
import hashlib
import importlib
import importlib.util
import inspect
import json
import os
import shlex
import subprocess
import sys
from pathlib import Path
sys.dont_write_bytecode = True
REQUEST = json.loads('''
REMOTE_SCRIPT_SUFFIX = r''')
MUTATION_FLAGS = ("task_db_mutation", "scope_mutation", "git_mutation", "service_mutation")
def status(status_name, errors=(), **details):
result = {
"kind": "phase2-status",
"version": 1,
"status": status_name,
"read_only": True,
"fail_closed": status_name != "PASS",
}
result.update({flag: False for flag in MUTATION_FLAGS})
if errors:
result["errors"] = list(errors)
result.update(details)
return result
def systemd_value(unit, property_name):
try:
completed = subprocess.run(
["systemctl", "show", unit, "--property=" + property_name, "--value", "--no-pager"],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
check=False,
timeout=12,
)
except (OSError, subprocess.TimeoutExpired):
raise RuntimeError("SYSTEMD_QUERY_FAILED")
if completed.returncode != 0:
raise RuntimeError("SYSTEMD_QUERY_FAILED")
return completed.stdout.decode("utf-8", "strict").strip()
def parse_pythonpath(environment_value):
for token in shlex.split(environment_value):
if token.startswith("PYTHONPATH="):
return token[len("PYTHONPATH="):].split(os.pathsep)
return []
def runtime_identity():
unit = REQUEST.get("issuer_unit", "aios-task-principal-issuer.service")
pid = REQUEST.get("issuer_pid")
working_directory = REQUEST.get("working_directory")
pythonpath = REQUEST.get("pythonpath")
# Regression callers may pin one identity component while obtaining the
# remaining components from the live unit. In particular, never replace
# an explicitly supplied PID with a newly queried MainPID.
if pid is None:
pid = int(systemd_value(unit, "MainPID"))
if working_directory is None:
working_directory = systemd_value(unit, "WorkingDirectory")
if pythonpath is None:
pythonpath = parse_pythonpath(systemd_value(unit, "Environment"))
if not pid or not working_directory or working_directory == "-":
raise RuntimeError("ISSUER_RUNTIME_IDENTITY_INCOMPLETE")
if isinstance(pythonpath, str):
pythonpath = pythonpath.split(os.pathsep)
return {
"unit": unit,
"main_pid": int(pid),
"working_directory": working_directory,
"pythonpath": [item for item in pythonpath if item],
}
def setup_import_path(identity):
path = [identity["working_directory"]]
path.extend(identity["pythonpath"])
path.extend(item for item in sys.path if item and item not in path)
sys.path[:] = path
def importable_module(module_name):
try:
spec = importlib.util.find_spec(module_name)
if spec is None or not spec.origin or spec.origin in {"built-in", "frozen"}:
raise RuntimeError("MODULE_NOT_IMPORTABLE:" + module_name)
module = importlib.import_module(module_name)
except Exception as exc:
if isinstance(exc, RuntimeError):
raise
raise RuntimeError("MODULE_IMPORT_FAILED:" + module_name) from exc
module_path = getattr(module, "__file__", None)
if not module_path:
raise RuntimeError("MODULE_HAS_NO_FILE:" + module_name)
return module, Path(module_path).resolve()
def hash_file(path):
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 inspect_admission(module, path, required, expected_hash):
source = path.read_text(encoding="utf-8")
tree = ast.parse(source)
nodes = {
node.name: node for node in tree.body
if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef))
and not node.name.startswith("_")
}
imported = {
alias.asname or alias.name.split(".", 1)[0]
for node in tree.body
if isinstance(node, (ast.Import, ast.ImportFrom))
for alias in node.names
if alias.name != "*"
}
dynamic_reexport = any(
isinstance(node, ast.Call)
and isinstance(node.func, ast.Name)
and node.func.id == "globals"
for node in ast.walk(tree)
)
bound = set(nodes) | imported
missing = [
name
for name in required
if name not in bound and not dynamic_reexport
]
if missing:
raise RuntimeError("PUBLIC_SYMBOL_MISSING:" + ",".join(missing))
signatures = {}
for name in required:
value = getattr(module, name, None)
if value is None:
raise RuntimeError("PUBLIC_SYMBOL_NOT_IMPORTABLE:" + name)
try:
signatures[name] = str(inspect.signature(value))
except (TypeError, ValueError):
raise RuntimeError("SIGNATURE_UNAVAILABLE:" + name)
actual_hash = hash_file(path)
if expected_hash and actual_hash != expected_hash:
raise RuntimeError("MODULE_HASH_MISMATCH:admission")
return {
"name": module.__name__,
"path": str(path),
"sha256": actual_hash,
"signatures": signatures,
"ast_functions": sorted(nodes),
"ast_imported": sorted(imported),
"ast_dynamic_reexport": dynamic_reexport,
}
def inspect_migration(module, path, verifier_name, expected_hash):
verifier = getattr(module, verifier_name, None)
if verifier is None:
raise RuntimeError("MIGRATION_VERIFIER_MISSING:" + verifier_name)
try:
signature = str(inspect.signature(verifier))
except (TypeError, ValueError):
raise RuntimeError("MIGRATION_VERIFIER_SIGNATURE_UNAVAILABLE")
actual_hash = hash_file(path)
if expected_hash and actual_hash != expected_hash:
raise RuntimeError("MODULE_HASH_MISMATCH:migration")
return {
"name": module.__name__,
"path": str(path),
"sha256": actual_hash,
"verifier": verifier_name,
"signature": signature,
}
def main():
try:
identity = runtime_identity()
setup_import_path(identity)
admission_name = REQUEST.get("admission_module", "aios_task_principal_admission")
migration_name = REQUEST.get("migration_module", "aios_task_principal_storage_migration")
if migration_name == admission_name:
raise RuntimeError("MIGRATION_MODULE_NOT_DISTINCT")
required = tuple(REQUEST.get("required_admission_functions", (
"offline_migrate_task_principal_schema", "supersede_p0_scope")))
admission, admission_path = importable_module(admission_name)
migration, migration_path = importable_module(migration_name)
admission_info = inspect_admission(
admission,
admission_path,
required,
REQUEST.get("expected_admission_sha256"),
)
migration_info = inspect_migration(
migration,
migration_path,
REQUEST.get("migration_verifier", "SSHOwnerBootstrapVerifier"),
REQUEST.get("expected_migration_sha256"),
)
print(json.dumps(status(
"PASS",
issuer=identity,
modules={"admission": admission_info, "migration": migration_info},
ast_public_functions=list(required),
), sort_keys=True, separators=(",", ":")))
except Exception as exc:
# Emit only stable error classes and never exception text from imports.
error = str(exc).split(":", 1)[0] or "PHASE2_INTROSPECTION_FAILED"
print(json.dumps(status("FAIL", errors=[error]), sort_keys=True, separators=(",", ":")))
if __name__ == "__main__":
main()
'''
def build_remote_payload(request: Mapping[str, Any] | None = None) -> bytes:
"""Build the stdin wrapper; request data never enters SSH argv."""
request = {} if request is None else request
# repr() makes the embedded JSON a valid Python string for every request
# value, including apostrophes and backslashes; it still remains stdin.
request_literal = repr(_json_request(request))
return (REMOTE_SCRIPT_PREFIX + request_literal + REMOTE_SCRIPT_SUFFIX).encode("utf-8")
def _load_request(args: argparse.Namespace) -> dict[str, Any]:
request: dict[str, Any] = {}
if args.request_json:
loaded = json.loads(args.request_json)
if not isinstance(loaded, dict):
raise ValueError("request JSON must be an object")
request.update(loaded)
if args.request_file:
loaded = json.loads(Path(args.request_file).read_text(encoding="utf-8"))
if not isinstance(loaded, dict):
raise ValueError("request JSON must be an object")
request.update(loaded)
overrides = {
"issuer_unit": args.issuer_unit,
"issuer_pid": args.issuer_pid,
"working_directory": args.working_directory,
"pythonpath": args.pythonpath,
"admission_module": args.admission_module,
"migration_module": args.migration_module,
"expected_admission_sha256": args.expected_admission_sha256,
"expected_migration_sha256": args.expected_migration_sha256,
}
request.update({key: value for key, value in overrides.items() if value is not None})
return request
def _write_status(path: str | Path, status: Mapping[str, Any]) -> None:
destination = Path(path)
destination.write_text(
json.dumps(dict(status), ensure_ascii=True, indent=2, sort_keys=True) + "\n",
encoding="utf-8",
)
def main(argv: Sequence[str] | None = None) -> int:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--request-json")
parser.add_argument("--request-file")
parser.add_argument("--output", default="phase2-status.json")
parser.add_argument("--ssh", default=DEFAULT_SSH)
parser.add_argument("--host", default=DEFAULT_HOST)
parser.add_argument("--timeout", type=float, default=DEFAULT_TIMEOUT_SECONDS)
parser.add_argument("--issuer-unit")
parser.add_argument("--issuer-pid", type=int)
parser.add_argument("--working-directory")
parser.add_argument("--pythonpath", nargs="*")
parser.add_argument("--admission-module")
parser.add_argument("--migration-module")
parser.add_argument("--expected-admission-sha256")
parser.add_argument("--expected-migration-sha256")
args = parser.parse_args(argv)
try:
request = _load_request(args)
payload = build_remote_payload(request)
stdout = run_remote_python(
payload,
ssh=args.ssh,
host=args.host,
timeout=args.timeout,
)
remote_status = json.loads(stdout.decode("utf-8"))
status = validate_phase2_status(remote_status)
except (OSError, ValueError, json.JSONDecodeError, RemoteTransportError, Phase2StatusError) as exc:
status = make_phase2_status("FAIL", errors=[str(exc).split(":", 1)[0]])
_write_status(args.output, status)
return 1
_write_status(args.output, status)
return 0 if status["status"] == "PASS" else 1
if __name__ == "__main__":
raise SystemExit(main())
@@ -102,6 +102,9 @@ def test_http_route_contract_is_complete():
"/v2/release/execute",
"/v2/release/consistency",
"/v2/release/acceptance",
"/v2/task-admission/envelope",
"/v2/task-admission/authorize",
"/v2/task-admission/status",
}
from aios_chatgpt_bridge_v2_server import ROUTES
+131
View File
@@ -0,0 +1,131 @@
"""Local, non-SSH tests for the bounded Phase2 transport and status contract."""
from __future__ import annotations
import importlib.util
import subprocess
from pathlib import Path
from types import SimpleNamespace
import pytest
SCRIPT = Path(__file__).parents[1] / "scripts" / "phase2_introspect_and_freeze.py"
SPEC = importlib.util.spec_from_file_location("phase2_introspect_and_freeze", SCRIPT)
assert SPEC and SPEC.loader
phase2 = importlib.util.module_from_spec(SPEC)
SPEC.loader.exec_module(phase2)
def test_transport_sends_large_payload_without_argv_growth():
payload = b"x" * (256 * 1024)
observed = {}
def runner(argv, **kwargs):
observed["argv"] = argv
observed["kwargs"] = kwargs
return SimpleNamespace(returncode=0, stdout=b'{"ok":true}')
assert phase2.run_remote_python(payload, runner=runner) == b'{"ok":true}'
assert observed["kwargs"]["input"] == payload
argv = observed["argv"]
assert argv == [
"ssh.exe",
"-T",
"-o",
"BatchMode=yes",
"-o",
"ConnectTimeout=12",
"hw",
'test "$(hostname)" = cn-network-mkt && sudo -n python3 -',
]
assert sum(len(part.encode("utf-8")) + 1 for part in argv) <= 512
assert all(payload not in part.encode("utf-8") for part in argv)
assert all("base64" not in part.lower() for part in argv)
def test_transport_reports_timeout_without_stderr():
def runner(argv, **kwargs):
raise subprocess.TimeoutExpired(argv, kwargs["timeout"], stderr=b"secret=do-not-leak")
with pytest.raises(phase2.RemoteTransportError, match="timed out") as error:
phase2.run_remote_python(b"payload", runner=runner, timeout=3)
assert "secret" not in str(error.value)
def test_transport_reports_nonzero_without_stderr():
def runner(argv, **kwargs):
return SimpleNamespace(returncode=23, stdout=b"", stderr=b"token=do-not-leak")
with pytest.raises(phase2.RemoteTransportError, match="status 23") as error:
phase2.run_remote_python(b"payload", runner=runner)
assert "token" not in str(error.value)
def test_status_flags_are_explicit_and_missing_public_symbol_fails_closed():
valid = phase2.validate_public_function_ast(
"def offline_migrate_task_principal_schema():\n pass\n"
"def supersede_p0_scope():\n pass\n"
)
assert valid["ok"] is True
missing = phase2.validate_public_function_ast(
"def offline_migrate_task_principal_schema():\n pass\n"
)
assert missing["ok"] is False
assert missing["missing"] == ["supersede_p0_scope"]
status = phase2.make_phase2_status(
"FAIL", errors=["PUBLIC_SYMBOL_MISSING:supersede_p0_scope"]
)
assert status["status"] == "FAIL"
assert status["fail_closed"] is True
assert all(status[flag] is False for flag in phase2.MUTATION_FLAGS)
def test_dynamic_reexport_is_accepted_only_with_explicit_globals_export_mechanism():
source = (
"for _name, _value in _legacy.__dict__.items():\n"
" globals()[_name] = _value\n"
)
result = phase2.validate_public_function_ast(source)
assert result["ok"] is True
assert result["dynamic_reexport"] is True
no_export = "x = 1\n"
result = phase2.validate_public_function_ast(no_export)
assert result["ok"] is False
assert result["missing"] == list(phase2.DEFAULT_REQUIRED_ADMISSION_FUNCTIONS)
def test_status_validator_rejects_mutation_claims():
status = phase2.make_phase2_status("PASS")
assert phase2.validate_phase2_status(status)["read_only"] is True
status["git_mutation"] = True
with pytest.raises(phase2.Phase2StatusError, match="git_mutation"):
phase2.validate_phase2_status(status)
def test_request_payload_contains_request_but_argv_does_not():
payload = phase2.build_remote_payload(
{
"issuer_pid": 123,
"expected_admission_sha256": "a" * 64,
}
)
assert b'"issuer_pid":123' in payload
assert b'"expected_admission_sha256":"' in payload
argv = phase2.build_ssh_argv()
assert all("123" not in part and "aaaa" not in part for part in argv)
def test_live_defaults_use_current_issuer_and_storage_migration_module():
assert phase2.DEFAULT_ISSUER_UNIT == "aios-task-principal-issuer.service"
assert phase2.DEFAULT_MIGRATION_MODULE == "aios_task_principal_storage_migration"
payload = phase2.build_remote_payload()
assert b"aios-task-principal-issuer.service" in payload
assert b"aios_task_principal_storage_migration" in payload
stale_issuer = "aios-task-principal-" + "admission.service"
stale_migration = "aios_task_principal_" + "migration"
assert stale_issuer.encode() not in payload
assert stale_migration.encode() not in payload
+640
View File
@@ -0,0 +1,640 @@
from __future__ import annotations
import base64
from concurrent.futures import ThreadPoolExecutor
import hashlib
from pathlib import Path
import sys
import threading
import pytest
ROOT = Path(__file__).resolve().parents[1]
APP = ROOT / "active-runtime" / "app"
if str(APP) not in sys.path:
sys.path.insert(0, str(APP))
from aios_task_admission_bootstrap import ( # noqa: E402
OwnerDelegationEpoch,
TaskAdmissionBootstrapService,
TaskAdmissionError,
TaskAdmissionGrant,
TaskAdmissionPolicy,
TaskAdmissionStore,
canonical_json_bytes,
)
from aios_task_admission_live_adapters import ( # noqa: E402
AgentTaskAdmissionPort,
BoundOwnerAuthorizationBackend,
)
BASE_OID = "9" * 40
EXECUTION_DIGEST = "a" * 64
TASK_ID = "repository-discovery-p0-20260903"
BRANCH = "agent/repository-discovery-p0-20260902"
IDEMPOTENCY = "repository-discovery-p0-admission-20260903"
FILES = [
"active-runtime/app/aios_repository_discovery.py",
"active-runtime/app/aios_chatgpt_bridge_v2.py",
"docs/gitops/openapi-chatgpt-actions.yaml",
"tests/test_repository_discovery.py",
]
CAPS = [
"governed.task.create",
"agent-task:control",
"task.principal.issue",
"writer.lease",
"governed.write.files",
"git.read",
"testing",
"verification",
]
class FakeAuthorization:
def __init__(self, *, policy_digest: str, now: int = 1_800_000_000):
self.now = now
self.signature_calls = 0
self.epoch_calls = 0
self.signature_ok = True
self.epoch = OwnerDelegationEpoch(
epoch_id="epoch-repository-discovery-20260903",
owner_identity="owner",
trust_domain="aios.owner",
repository="aios/aios",
allowed_branch_prefixes=("agent/repository-discovery-",),
allowed_capabilities=frozenset(CAPS),
allowed_file_prefixes=("active-runtime", "docs", "tests"),
risk_ceiling="medium",
issued_at=now - 60,
not_before=now - 60,
expires_at=now + 3600,
revocation_epoch=0,
policy_digest=policy_digest,
)
def verify_delegation_epoch(self, epoch_id: str) -> OwnerDelegationEpoch:
self.epoch_calls += 1
if epoch_id != self.epoch.epoch_id:
raise TaskAdmissionError("UNKNOWN_EPOCH", 403)
return self.epoch
def verify_owner_signature(
self,
*,
key_id: str,
canonical_payload: bytes,
signature_b64: str,
) -> str:
self.signature_calls += 1
if key_id != "owner-key-1" or not self.signature_ok:
raise TaskAdmissionError("TASK_ADMISSION_OWNER_SIGNATURE_INVALID", 403)
expected = base64.b64encode(
hashlib.sha256(canonical_payload + b"\0owner-key-1").digest()
).decode("ascii")
if signature_b64 != expected:
raise TaskAdmissionError("TASK_ADMISSION_OWNER_SIGNATURE_INVALID", 403)
return "owner"
class FakeTaskPort:
def __init__(self):
self.calls = 0
self.grants = []
self._lock = threading.Lock()
def admit(self, grant):
with self._lock:
self.calls += 1
self.grants.append(grant)
return {"task_id": grant.task_id, "state": "QUEUED"}
@pytest.fixture
def policy():
return TaskAdmissionPolicy(
allowed_capabilities=frozenset(CAPS),
max_file_count=16,
max_task_ttl_seconds=3600,
max_envelope_ttl_seconds=900,
max_risk="medium",
)
@pytest.fixture
def service(tmp_path: Path, policy: TaskAdmissionPolicy):
now = 1_800_000_000
auth = FakeAuthorization(policy_digest=policy.normalized_policy_digest(), now=now)
port = FakeTaskPort()
svc = TaskAdmissionBootstrapService(
store=TaskAdmissionStore(tmp_path / "admission.sqlite3"),
authorization=auth,
task_port=port,
policy=policy,
clock=lambda: now,
)
return svc, auth, port
def request(**overrides):
value = {
"idempotency_key": IDEMPOTENCY,
"task_id": TASK_ID,
"repository": "aios/aios",
"authority_base_oid": BASE_OID,
"target_branch": BRANCH,
"generation": 1,
"execution_digest": EXECUTION_DIGEST,
"requested_capabilities": list(CAPS),
"file_allowlist": list(FILES),
"risk": "medium",
"expires_in_seconds": 600,
}
value.update(overrides)
return value
def test_envelope_exact_replay_returns_same_nonce_and_payload(service):
svc, _, _ = service
first = svc.create_envelope(request())
second = svc.create_envelope(request())
assert first["idempotent"] is False
assert second["idempotent"] is True
assert first["envelope"] == second["envelope"]
assert first["canonical_payload_base64"] == second["canonical_payload_base64"]
def test_envelope_idempotency_conflict_fails_closed(service):
svc, _, _ = service
svc.create_envelope(request())
with pytest.raises(TaskAdmissionError, match="TASK_ADMISSION_IDEMPOTENCY_CONFLICT"):
svc.create_envelope(request(risk="low"))
@pytest.mark.parametrize(
"field,value,code",
[
("repository", "other/repo", "TASK_ADMISSION_REPOSITORY_REJECTED"),
("target_branch", "main", "TASK_ADMISSION_BRANCH_REJECTED"),
("authority_base_oid", "HEAD", "TASK_ADMISSION_BASE_OID_INVALID"),
("requested_capabilities", ["production.write"], "TASK_ADMISSION_PROHIBITED_CAPABILITY"),
("requested_capabilities", ["git:*"], "TASK_ADMISSION_CAPABILITY_INVALID"),
("file_allowlist", ["../secrets"], "TASK_ADMISSION_FILE_PATH_INVALID"),
("file_allowlist", ["/etc/passwd"], "TASK_ADMISSION_FILE_PATH_INVALID"),
("risk", "high", "TASK_ADMISSION_RISK_OUT_OF_POLICY"),
],
)
def test_envelope_policy_rejects_scope_widening(service, field, value, code):
svc, _, _ = service
with pytest.raises(TaskAdmissionError, match=code):
svc.create_envelope(request(**{field: value}))
def test_valid_delegation_epoch_admits_exact_task_once(service):
svc, auth, port = service
envelope = svc.create_envelope(request())["envelope"]
result = svc.authorize_and_admit(
{
"envelope_id": envelope["envelope_id"],
"authorization_kind": "delegation_epoch",
"delegation_epoch_id": auth.epoch.epoch_id,
}
)
assert result["schema"] == "aios.task-admission-receipt.v1"
assert result["task_id"] == TASK_ID
assert result["task_state"] == "QUEUED"
assert result["parent_epoch_id"] == auth.epoch.epoch_id
assert port.calls == 1
replay = svc.authorize_and_admit(
{
"envelope_id": envelope["envelope_id"],
"authorization_kind": "delegation_epoch",
"delegation_epoch_id": auth.epoch.epoch_id,
}
)
assert replay["task_id"] == TASK_ID
assert replay["idempotent"] is True
assert port.calls == 1
def test_post_admission_projection_runs_after_exact_task_readback_once(
tmp_path: Path, policy: TaskAdmissionPolicy
):
auth = FakeAuthorization(policy_digest=policy.normalized_policy_digest())
port = FakeTaskPort()
observations = []
def project(grant, task):
observations.append((grant.task_id, task["task_id"], task["state"]))
return {"job_id": "projected-job"}
svc = TaskAdmissionBootstrapService(
store=TaskAdmissionStore(tmp_path / "admission.sqlite3"),
authorization=auth,
task_port=port,
policy=policy,
clock=lambda: 1_800_000_000,
post_admission=project,
)
envelope_id = svc.create_envelope(request())["envelope"]["envelope_id"]
proof = {
"envelope_id": envelope_id,
"authorization_kind": "delegation_epoch",
"delegation_epoch_id": auth.epoch.epoch_id,
}
first = svc.authorize_and_admit(proof)
replay = svc.authorize_and_admit(proof)
assert first["task_id"] == TASK_ID
assert replay["idempotent"] is True
assert observations == [(TASK_ID, TASK_ID, "QUEUED")]
def test_epoch_capability_widening_rejected_before_task_port(service):
svc, auth, port = service
# Envelope policy allows the capability, parent epoch deliberately does not.
auth.epoch = OwnerDelegationEpoch(
**{
**auth.epoch.as_public_dict(),
"allowed_capabilities": frozenset(
item for item in CAPS if item != "writer.lease"
),
"allowed_branch_prefixes": auth.epoch.allowed_branch_prefixes,
"allowed_file_prefixes": auth.epoch.allowed_file_prefixes,
}
)
envelope = svc.create_envelope(request())["envelope"]
with pytest.raises(
TaskAdmissionError, match="TASK_ADMISSION_EPOCH_CAPABILITY_WIDENING"
):
svc.authorize_and_admit(
{
"envelope_id": envelope["envelope_id"],
"authorization_kind": "delegation_epoch",
"delegation_epoch_id": auth.epoch.epoch_id,
}
)
assert port.calls == 0
def test_epoch_file_scope_widening_rejected(service):
svc, auth, port = service
auth.epoch = OwnerDelegationEpoch(
**{
**auth.epoch.as_public_dict(),
"allowed_capabilities": auth.epoch.allowed_capabilities,
"allowed_branch_prefixes": auth.epoch.allowed_branch_prefixes,
"allowed_file_prefixes": ("docs",),
}
)
envelope = svc.create_envelope(request())["envelope"]
with pytest.raises(TaskAdmissionError, match="TASK_ADMISSION_EPOCH_FILE_WIDENING"):
svc.authorize_and_admit(
{
"envelope_id": envelope["envelope_id"],
"authorization_kind": "delegation_epoch",
"delegation_epoch_id": auth.epoch.epoch_id,
}
)
assert port.calls == 0
def test_revoked_or_expired_epoch_rejected(service):
svc, auth, port = service
auth.epoch = OwnerDelegationEpoch(
**{
**auth.epoch.as_public_dict(),
"allowed_capabilities": auth.epoch.allowed_capabilities,
"allowed_branch_prefixes": auth.epoch.allowed_branch_prefixes,
"allowed_file_prefixes": auth.epoch.allowed_file_prefixes,
"revocation_epoch": 1,
}
)
envelope = svc.create_envelope(request())["envelope"]
with pytest.raises(TaskAdmissionError, match="TASK_ADMISSION_EPOCH_REVOKED"):
svc.authorize_and_admit(
{
"envelope_id": envelope["envelope_id"],
"authorization_kind": "delegation_epoch",
"delegation_epoch_id": auth.epoch.epoch_id,
}
)
assert port.calls == 0
def test_owner_signature_path_verifies_canonical_payload(service):
svc, auth, port = service
created = svc.create_envelope(request())
payload = canonical_json_bytes(created["envelope"])
signature = base64.b64encode(
hashlib.sha256(payload + b"\0owner-key-1").digest()
).decode("ascii")
result = svc.authorize_and_admit(
{
"envelope_id": created["envelope"]["envelope_id"],
"authorization_kind": "owner_signature",
"owner_key_id": "owner-key-1",
"signature_base64": signature,
}
)
assert result["authorization_kind"] == "owner_signature"
assert result["owner_identity"] == "owner"
assert auth.signature_calls == 1
assert port.calls == 1
def test_bad_owner_signature_is_rejected(service):
svc, _, port = service
envelope = svc.create_envelope(request())["envelope"]
with pytest.raises(TaskAdmissionError, match="TASK_ADMISSION_OWNER_SIGNATURE_INVALID"):
svc.authorize_and_admit(
{
"envelope_id": envelope["envelope_id"],
"authorization_kind": "owner_signature",
"owner_key_id": "owner-key-1",
"signature_base64": base64.b64encode(b"bad").decode("ascii"),
}
)
assert port.calls == 0
def test_expired_envelope_cannot_admit(tmp_path: Path, policy: TaskAdmissionPolicy):
now = [1_800_000_000]
auth = FakeAuthorization(policy_digest=policy.normalized_policy_digest(), now=now[0])
port = FakeTaskPort()
svc = TaskAdmissionBootstrapService(
store=TaskAdmissionStore(tmp_path / "admission.sqlite3"),
authorization=auth,
task_port=port,
policy=policy,
clock=lambda: now[0],
)
envelope = svc.create_envelope(request(expires_in_seconds=60))["envelope"]
now[0] += 61
with pytest.raises(TaskAdmissionError, match="TASK_ADMISSION_ENVELOPE_EXPIRED"):
svc.authorize_and_admit(
{
"envelope_id": envelope["envelope_id"],
"authorization_kind": "delegation_epoch",
"delegation_epoch_id": auth.epoch.epoch_id,
}
)
assert port.calls == 0
def test_concurrent_exact_authorization_calls_admit_once(service):
svc, auth, port = service
envelope_id = svc.create_envelope(request())["envelope"]["envelope_id"]
body = {
"envelope_id": envelope_id,
"authorization_kind": "delegation_epoch",
"delegation_epoch_id": auth.epoch.epoch_id,
}
with ThreadPoolExecutor(max_workers=16) as pool:
results = list(pool.map(lambda _: svc.authorize_and_admit(body), range(16)))
assert {item["task_id"] for item in results} == {TASK_ID}
assert port.calls == 1
def test_cross_instance_envelope_winner_is_the_durable_value(tmp_path: Path, policy: TaskAdmissionPolicy):
auth = FakeAuthorization(policy_digest=policy.normalized_policy_digest())
port = FakeTaskPort()
stores = [TaskAdmissionStore(tmp_path / "shared.sqlite3") for _ in range(16)]
services = [
TaskAdmissionBootstrapService(
store=store, authorization=auth, task_port=port, policy=policy,
clock=lambda: 1_800_000_000,
)
for store in stores
]
with ThreadPoolExecutor(max_workers=16) as pool:
values = list(pool.map(lambda svc: svc.create_envelope(request()), services))
assert len({item["envelope"]["envelope_id"] for item in values}) == 1
assert sum(not item["idempotent"] for item in values) == 1
def test_cross_instance_authorization_calls_create_one_task(tmp_path: Path, policy: TaskAdmissionPolicy):
auth = FakeAuthorization(policy_digest=policy.normalized_policy_digest())
port = FakeTaskPort()
database = tmp_path / "shared.sqlite3"
first = TaskAdmissionBootstrapService(
store=TaskAdmissionStore(database), authorization=auth, task_port=port,
policy=policy, clock=lambda: 1_800_000_000,
)
envelope_id = first.create_envelope(request())["envelope"]["envelope_id"]
services = [
TaskAdmissionBootstrapService(
store=TaskAdmissionStore(database), authorization=auth, task_port=port,
policy=policy, clock=lambda: 1_800_000_000,
)
for _ in range(16)
]
body = {
"envelope_id": envelope_id,
"authorization_kind": "delegation_epoch",
"delegation_epoch_id": auth.epoch.epoch_id,
}
with ThreadPoolExecutor(max_workers=16) as pool:
values = list(pool.map(lambda svc: svc.authorize_and_admit(body), services))
assert {item["task_id"] for item in values} == {TASK_ID}
assert port.calls == 1
@pytest.mark.parametrize("file_name", ["docs//gitops/a.yaml", "docs/./gitops/a.yaml", "docs/a\x00.yaml"])
def test_file_paths_must_already_be_canonical(service, file_name):
svc, _, _ = service
with pytest.raises(TaskAdmissionError, match="TASK_ADMISSION_FILE_PATH_INVALID"):
svc.create_envelope(request(file_allowlist=[file_name]))
@pytest.mark.parametrize("generation", [True, "1", 1.0, 0])
def test_generation_is_a_strict_positive_integer(service, generation):
svc, _, _ = service
with pytest.raises(TaskAdmissionError, match="TASK_ADMISSION_GENERATION_INVALID"):
svc.create_envelope(request(generation=generation))
def test_policy_digest_drift_and_changed_proof_cannot_replay(service):
svc, auth, _ = service
envelope_id = svc.create_envelope(request())["envelope"]["envelope_id"]
body = {
"envelope_id": envelope_id,
"authorization_kind": "delegation_epoch",
"delegation_epoch_id": auth.epoch.epoch_id,
}
svc.authorize_and_admit(body)
svc.policy = TaskAdmissionPolicy(
allowed_capabilities=frozenset(CAPS), policy_digest="policy-has-changed"
)
with pytest.raises(TaskAdmissionError, match="TASK_ADMISSION_POLICY_DIGEST_DRIFT"):
svc.authorize_and_admit(body)
def test_grant_expiry_is_clipped_to_parent_epoch(tmp_path: Path, policy: TaskAdmissionPolicy):
now = 1_800_000_000
auth = FakeAuthorization(policy_digest=policy.normalized_policy_digest(), now=now)
auth.epoch = OwnerDelegationEpoch(
**{**auth.epoch.as_public_dict(), "allowed_capabilities": auth.epoch.allowed_capabilities,
"allowed_branch_prefixes": auth.epoch.allowed_branch_prefixes,
"allowed_file_prefixes": auth.epoch.allowed_file_prefixes, "expires_at": now + 120}
)
port = FakeTaskPort()
svc = TaskAdmissionBootstrapService(
store=TaskAdmissionStore(tmp_path / "admission.sqlite3"), authorization=auth,
task_port=port, policy=policy, clock=lambda: now,
)
result = svc.authorize_and_admit({
"envelope_id": svc.create_envelope(request())["envelope"]["envelope_id"],
"authorization_kind": "delegation_epoch", "delegation_epoch_id": auth.epoch.epoch_id,
})
assert port.grants[0].expires_at == now + 120
assert result["task_id"] == TASK_ID
def test_owner_backend_uses_bound_caller_and_decoded_armored_signature(policy):
captured = {}
class Verifier:
def verify(self, **kwargs):
captured.update(kwargs)
return True
epoch = FakeAuthorization(policy_digest=policy.normalized_policy_digest()).epoch
backend = BoundOwnerAuthorizationBackend(
authenticated_caller={"uid": "bridge-service"},
verified_epoch_provider=lambda **kwargs: captured.update(epoch_lookup=kwargs) or epoch,
signature_verifier=Verifier(),
)
assert backend.verify_delegation_epoch(epoch.epoch_id) == epoch
assert captured["epoch_lookup"]["caller"] == {"uid": "bridge-service"}
assert backend.verify_owner_signature(
key_id="owner-key-1", canonical_payload=b"exact", signature_b64=base64.b64encode(b"-----BEGIN SSH SIGNATURE-----").decode(),
) == "aios-task-grant-signer"
assert captured["namespace"] == "aios-owner-approval-v1"
assert captured["payload"] == b"exact"
def test_official_task_port_reconciles_ambiguous_create(tmp_path: Path, policy):
grant = TaskAdmissionBootstrapService(
store=TaskAdmissionStore(tmp_path / "admission.sqlite3"),
authorization=FakeAuthorization(policy_digest=policy.normalized_policy_digest()),
task_port=FakeTaskPort(), policy=policy, clock=lambda: 1_800_000_000,
)
# Construct via the public flow so the port sees a production-shaped grant.
created = grant.create_envelope(request())["envelope"]
envelope = grant._load_envelope(created["envelope_id"])
task_grant = TaskAdmissionGrant(
grant_id="task-grant-adapter-test", envelope_id=envelope.envelope_id,
task_id=envelope.task_id, repository=envelope.repository,
authority_base_oid=envelope.authority_base_oid, target_branch=envelope.target_branch,
generation=envelope.generation, execution_digest=envelope.execution_digest,
capabilities=envelope.requested_capabilities, file_allowlist=envelope.file_allowlist,
risk=envelope.risk, trust_domain=envelope.trust_domain,
policy_digest=envelope.policy_digest, authorization_kind="owner_signature",
owner_identity="aios-task-grant-signer", parent_epoch_id=None,
issued_at=1_800_000_000, expires_at=1_800_000_600,
)
class NotFound(Exception):
status = 404
class Transport:
def __init__(self): self.calls = []; self.task = None
def request(self, method, path, body=None):
self.calls.append((method, path, body))
if method == "GET" and self.task is None: raise NotFound()
if method == "POST":
self.task = {key: value for key, value in body.items() if key != "idempotency_key"}
self.task["status"] = "QUEUED"
raise TimeoutError("response lost after side effect")
return self.task
transport = Transport()
plan = {
"external_job_id": "fixture-job-adapter-01", "repository": task_grant.repository,
"target_branch": task_grant.target_branch, "generation": task_grant.generation,
"execution_digest": task_grant.execution_digest,
}
assert AgentTaskAdmissionPort(transport, execution_plan_provider=lambda _: plan).admit(task_grant) == {"task_id": TASK_ID, "state": "QUEUED"}
assert [call[:2] for call in transport.calls] == [
("GET", "/v1/tasks/" + TASK_ID), ("POST", "/v1/tasks"), ("GET", "/v1/tasks/" + TASK_ID),
]
def test_execution_identity_is_available_only_to_trusted_post_admission(
tmp_path: Path,
policy: TaskAdmissionPolicy,
):
class NotFound(Exception):
status = 404
class Transport:
def __init__(self):
self.task = None
def request(self, method, path, body=None):
if method == "GET" and self.task is None:
raise NotFound()
if method == "POST":
self.task = {
key: value for key, value in body.items()
if key != "idempotency_key"
}
self.task["status"] = "QUEUED"
return self.task
plan = {
"external_job_id": "job-0123456789abcdef0123456789abcdef",
"repository": "aios/aios",
"target_branch": BRANCH,
"generation": 1,
"execution_digest": EXECUTION_DIGEST,
}
projected: dict[str, object] = {}
auth = FakeAuthorization(policy_digest=policy.normalized_policy_digest())
service = TaskAdmissionBootstrapService(
store=TaskAdmissionStore(tmp_path / "admission.sqlite3"),
authorization=auth,
task_port=AgentTaskAdmissionPort(
Transport(), execution_plan_provider=lambda _: plan
),
policy=policy,
clock=lambda: 1_800_000_000,
post_admission=lambda _grant, result: projected.update(result),
)
envelope_id = service.create_envelope(request())["envelope"]["envelope_id"]
receipt = service.authorize_and_admit(
{
"envelope_id": envelope_id,
"authorization_kind": "delegation_epoch",
"delegation_epoch_id": auth.epoch.epoch_id,
}
)
assert receipt["task_id"] == TASK_ID
assert "external_job_id" not in receipt
assert projected == {
"task_id": TASK_ID,
"state": "QUEUED",
"external_job_id": plan["external_job_id"],
}
def test_status_is_read_only_and_reports_admitted_task(service):
svc, auth, _ = service
envelope_id = svc.create_envelope(request())["envelope"]["envelope_id"]
before = svc.status(envelope_id)
assert before["status"] == "PENDING_AUTHORIZATION"
assert before["task"] is None
svc.authorize_and_admit(
{
"envelope_id": envelope_id,
"authorization_kind": "delegation_epoch",
"delegation_epoch_id": auth.epoch.epoch_id,
}
)
after = svc.status(envelope_id)
assert after["status"] == "ADMITTED"
assert after["task"]["task_id"] == TASK_ID
@@ -0,0 +1,46 @@
from __future__ import annotations
from pathlib import Path
import sys
ROOT = Path(__file__).resolve().parents[1]
APP = ROOT / "active-runtime" / "app"
if str(APP) not in sys.path:
sys.path.insert(0, str(APP))
from aios_task_admission_bridge_contract import ( # noqa: E402
TASK_ADMISSION_OPERATION_REQUIRED_FIELDS,
TASK_ADMISSION_ROUTES,
)
def test_bootstrap_route_operation_mapping_is_exact():
assert TASK_ADMISSION_ROUTES == {
"/v2/task-admission/envelope": "getTaskAdmissionEnvelope",
"/v2/task-admission/authorize": "submitTaskAdmissionAuthorization",
"/v2/task-admission/status": "getTaskAdmissionStatus",
}
def test_bootstrap_operations_do_not_replace_writer_or_principal_gates():
operations = set(TASK_ADMISSION_OPERATION_REQUIRED_FIELDS)
assert "acquireWriterLease" not in operations
assert "commitFilesGoverned" not in operations
assert "issueTaskPrincipal" not in operations
assert operations == {
"getTaskAdmissionEnvelope",
"submitTaskAdmissionAuthorization",
"getTaskAdmissionStatus",
}
def test_openapi_fragment_matches_runtime_contract():
text = (
ROOT / "docs" / "gitops" / "openapi-task-admission-bootstrap-fragment.yaml"
).read_text(encoding="utf-8")
for route, operation in TASK_ADMISSION_ROUTES.items():
assert f" {route}:" in text
assert f"operationId: {operation}" in text
assert "global write" not in text.lower()
assert "runShell" not in text
assert "force push" not in text.lower()
+406
View File
@@ -0,0 +1,406 @@
from __future__ import annotations
import ast
from contextlib import contextmanager
import json
import os
from pathlib import Path
import sys
import threading
from types import SimpleNamespace
from http.server import ThreadingHTTPServer
from urllib.error import HTTPError
from urllib.parse import quote
from urllib.request import Request, urlopen
ROOT = Path(__file__).resolve().parents[1]
APP = ROOT / "active-runtime" / "app"
if str(APP) not in sys.path:
sys.path.insert(0, str(APP))
from aios_chatgpt_bridge_v2 import ( # noqa: E402
BridgeV2Adapter,
TASK_ADMISSION_OPERATION_OPTIONAL_FIELDS,
TASK_ADMISSION_OPERATION_REQUIRED_FIELDS,
TASK_ADMISSION_OPERATIONS,
)
from aios_chatgpt_bridge_v2_server import ( # noqa: E402
BridgeHandler,
GovernedRuntime,
build_bridge_adapter,
)
from aios_governed_task_bootstrap import BootstrapError # noqa: E402
from aios_task_admission_bootstrap import TaskAdmissionError # noqa: E402
class StubRuntime:
def __init__(self, status: dict[str, object], handlers=None) -> None:
self.status = status
self._handlers = dict(handlers or {})
def runtime_status(self) -> dict[str, object]:
return dict(self.status)
def handlers(self) -> dict[str, object]:
return dict(self._handlers)
class FakeOwnerPort:
def __init__(self) -> None:
self.envelopes: list[dict[str, object]] = []
self.authorizations: list[dict[str, object]] = []
def create_envelope(self, request):
self.envelopes.append(dict(request))
return {"envelope_id": "env-1", "status": "PENDING", "http_status": 201}
def authorize(self, request, task_port):
self.authorizations.append(dict(request))
kind = request.get("authorization_kind")
if kind == "unsigned":
raise TaskAdmissionError("TASK_ADMISSION_AUTHORIZATION_REQUIRED", 400)
if kind == "bad_proof":
raise TaskAdmissionError("TASK_ADMISSION_OWNER_SIGNATURE_INVALID", 403)
return task_port.admit(request)
class FakeTaskPort:
def __init__(self) -> None:
self.calls: list[dict[str, object]] = []
def admit(self, request):
self.calls.append(dict(request))
return {"task_id": request.get("task_id", "task-1"), "state": "QUEUED"}
def _status(**overrides: object) -> dict[str, object]:
value: dict[str, object] = {
"status": "active",
"health": 200,
"governed_writes_enabled": False,
"legacy_global_writes_enabled": False,
"task_principal_writes_enabled": False,
}
value.update(overrides)
return value
def _admission_handlers(owner: FakeOwnerPort, task: FakeTaskPort):
return {
"getTaskAdmissionEnvelope": owner.create_envelope,
"submitTaskAdmissionAuthorization": lambda request: owner.authorize(
request, task
),
"getTaskAdmissionStatus": lambda request: {
"envelope_id": request["envelope_id"],
"status": "PENDING",
},
}
def _post(base_url: str, path: str, body: dict[str, object]):
request = Request(
base_url + path,
data=json.dumps(body).encode("utf-8"),
headers={"Content-Type": "application/json"},
method="POST",
)
try:
with urlopen(request, timeout=3) as response:
return response.status, json.loads(response.read())
except HTTPError as exc:
return exc.code, json.loads(exc.read())
def _get(base_url: str, path: str):
try:
with urlopen(base_url + path, timeout=3) as response:
return response.status, json.loads(response.read())
except HTTPError as exc:
return exc.code, json.loads(exc.read())
@contextmanager
def _serve(adapter: BridgeV2Adapter):
class TestBridgeHandler(BridgeHandler):
pass
TestBridgeHandler.adapter = adapter
TestBridgeHandler.principal_verifier = None
server = ThreadingHTTPServer(("127.0.0.1", 0), TestBridgeHandler)
thread = threading.Thread(target=server.serve_forever, daemon=True)
thread.start()
try:
yield f"http://127.0.0.1:{server.server_port}"
finally:
server.shutdown()
server.server_close()
thread.join(timeout=3)
def test_capabilities_expose_admission_only_for_complete_callable_handlers():
runtime = StubRuntime(_status(), {"getRuntimeStatus": lambda request: request})
without_backend = build_bridge_adapter(runtime)
assert not set(TASK_ADMISSION_OPERATIONS) & set(
without_backend.getGovernedWriteCapabilities()["operations"]
)
assert not set(TASK_ADMISSION_OPERATIONS) & set(
without_backend.getGovernedWriteCapabilities()["operation_contracts"]
)
handlers = _admission_handlers(FakeOwnerPort(), FakeTaskPort())
complete = build_bridge_adapter(runtime, task_admission_handlers=handlers)
capabilities = complete.getGovernedWriteCapabilities()
assert set(TASK_ADMISSION_OPERATIONS) <= set(capabilities["operations"])
expected_routes = {
"getTaskAdmissionEnvelope": ("POST", "/v2/task-admission/envelope"),
"submitTaskAdmissionAuthorization": ("POST", "/v2/task-admission/authorize"),
"getTaskAdmissionStatus": ("GET", "/v2/task-admission/status"),
}
for operation in TASK_ADMISSION_OPERATIONS:
contract = capabilities["operation_contracts"][operation]
assert (contract["method"], contract["path"]) == expected_routes[operation]
assert contract["required_fields"] == list(
TASK_ADMISSION_OPERATION_REQUIRED_FIELDS[operation]
)
assert contract["optional_fields"] == list(
TASK_ADMISSION_OPERATION_OPTIONAL_FIELDS[operation]
)
def test_partial_task_admission_handlers_are_rejected():
runtime = StubRuntime(_status())
try:
build_bridge_adapter(
runtime,
task_admission_handlers={
"getTaskAdmissionEnvelope": lambda request: request,
},
)
except ValueError as exc:
assert str(exc) == "TASK_ADMISSION_HANDLERS_INCOMPLETE"
else:
raise AssertionError("partial Task Admission handlers must be rejected")
def test_http_admission_backend_missing_returns_404():
adapter = build_bridge_adapter(StubRuntime(_status()))
with _serve(adapter) as base_url:
status, body = _post(
base_url,
"/v2/task-admission/envelope",
{"unexpected": True},
)
assert status == 404
assert body == {"error": "NOT_FOUND"}
def test_http_admission_envelope_and_status_work_without_task_principal():
owner = FakeOwnerPort()
task = FakeTaskPort()
adapter = build_bridge_adapter(
StubRuntime(_status()),
task_admission_handlers=_admission_handlers(owner, task),
)
envelope = {
"idempotency_key": "test",
"task_id": "task-1",
"repository": "aios/aios",
"authority_base_oid": "a" * 40,
"target_branch": "agent/task-1",
"generation": 1,
"execution_digest": "b" * 64,
"requested_capabilities": ["testing"],
"file_allowlist": ["tests/test_task.py"],
"risk": "low",
"expires_in_seconds": 60,
}
with _serve(adapter) as base_url:
status, body = _post(base_url, "/v2/task-admission/envelope", envelope)
assert status == 201
assert body["envelope_id"] == "env-1"
status, body = _get(
base_url, "/v2/task-admission/status?envelope_id=" + quote("env-1")
)
assert status == 200
assert body == {"envelope_id": "env-1", "status": "PENDING"}
assert len(owner.envelopes) == 1
assert task.calls == []
def test_http_admission_rejects_bad_queries_and_caller_principal():
owner = FakeOwnerPort()
task = FakeTaskPort()
adapter = build_bridge_adapter(
StubRuntime(_status()),
task_admission_handlers=_admission_handlers(owner, task),
)
envelope = {
"idempotency_key": "test",
"task_id": "task-1",
"repository": "aios/aios",
"authority_base_oid": "a" * 40,
"target_branch": "agent/task-1",
"generation": 1,
"execution_digest": "b" * 64,
"requested_capabilities": ["testing"],
"file_allowlist": ["tests/test_task.py"],
"risk": "low",
}
with _serve(adapter) as base_url:
for query in (
"unknown=x",
"envelope_id=env-1&envelope_id=env-2",
"envelope_id=",
):
status, body = _get(base_url, "/v2/task-admission/status?" + query)
assert status == 400
assert body["error"] == "TASK_ADMISSION_QUERY_INVALID"
status, body = _post(
base_url,
"/v2/task-admission/envelope",
{**envelope, "principal": {"task_id": "caller-controlled"}},
)
assert status == 400
assert body == {"error": "CALLER_PRINCIPAL_FORBIDDEN"}
assert owner.envelopes == []
assert task.calls == []
def test_http_admission_proof_failures_use_task_admission_status_and_do_not_mutate():
owner = FakeOwnerPort()
task = FakeTaskPort()
adapter = build_bridge_adapter(
StubRuntime(_status()),
task_admission_handlers=_admission_handlers(owner, task),
)
with _serve(adapter) as base_url:
for kind, expected_status, expected_error in (
("unsigned", 400, "TASK_ADMISSION_AUTHORIZATION_REQUIRED"),
("bad_proof", 403, "TASK_ADMISSION_OWNER_SIGNATURE_INVALID"),
):
status, body = _post(
base_url,
"/v2/task-admission/authorize",
{"envelope_id": "env-1", "authorization_kind": kind},
)
assert status == expected_status
assert body == {"error": expected_error}
assert task.calls == []
def test_existing_governed_mutation_gates_are_unchanged():
request = {
"schema": "aios.writer-lease-request.v2",
"task_id": "task-1",
"job_id": "job-1",
"repository": "aios/aios",
"branch_mode": "CREATE_NEW",
"base_oid": "a" * 40,
"expected_branch_oid": "a" * 40,
"target_branch": "agent/task-1",
"generation": 1,
"idempotency_key": "test",
"lease_seconds": 60,
}
runtime = StubRuntime(_status(), {"acquireWriterLease": lambda body: body})
with _serve(build_bridge_adapter(runtime)) as base_url:
status, body = _post(base_url, "/v2/writer-lease", request)
assert status == 423
assert body == {"error": "TASK_PRINCIPAL_WRITES_DISABLED"}
enabled_runtime = StubRuntime(
_status(
governed_writes_enabled=True,
task_principal_writes_enabled=True,
),
{"acquireWriterLease": lambda body: body},
)
with _serve(build_bridge_adapter(enabled_runtime)) as base_url:
status, body = _post(base_url, "/v2/writer-lease", request)
assert status == 401
assert body == {"error": "TASK_PRINCIPAL_VERIFIER_UNAVAILABLE"}
def test_post_admission_projection_is_exact_and_idempotent():
class Authority:
def __init__(self):
self.branch = None
@staticmethod
def commit_exists(oid):
return oid == "a" * 40
def read_ref(self, ref):
assert ref == "refs/heads/agent/admitted-task"
return self.branch
class Bootstrap:
def __init__(self):
self.binding = None
def get_binding(self, task_id):
if task_id != "admitted-task" or self.binding is None:
raise BootstrapError("TASK_EXECUTION_PROJECTION_MISSING")
return self.binding
def _write(self, binding):
assert self.binding is None
self.binding = binding
runtime = object.__new__(GovernedRuntime)
runtime.config = SimpleNamespace(repository="aios/aios")
runtime.authority = Authority()
runtime.bootstrap = Bootstrap()
runtime.bindings = {}
grant = SimpleNamespace(
task_id="admitted-task",
repository="aios/aios",
authority_base_oid="a" * 40,
target_branch="agent/admitted-task",
risk="low",
generation=1,
)
task_result = {"external_job_id": "job-0123456789abcdef0123456789abcdef"}
first = runtime.project_admitted_task(grant, task_result)
assert first["branch_mode"] == "CREATE_NEW"
assert first["expected_branch_oid"] == "a" * 40
assert first["job_id"] == task_result["external_job_id"]
assert runtime.project_admitted_task(grant, task_result)["idempotent"] is True
runtime.authority.branch = "b" * 40
changed = SimpleNamespace(**{**vars(grant), "task_id": "other-task"})
try:
runtime.project_admitted_task(changed, task_result)
except RuntimeError as exc:
assert str(exc) == "TASK_ADMISSION_PROJECTION_BRANCH_CONFLICT"
else:
raise AssertionError("drifted branch must not create a projection")
def test_live_bridge_preservation_when_source_root_is_supplied():
configured_source = os.environ.get("AIOS_LIVE_BRIDGE_SOURCE")
if not configured_source:
return
source = Path(configured_source)
live_server = source / "aios_chatgpt_bridge_v2_server.py" if source.is_dir() else source
candidate_server = APP / "aios_chatgpt_bridge_v2_server.py"
live_tree = ast.parse(live_server.read_text(encoding="utf-8"))
candidate_tree = ast.parse(candidate_server.read_text(encoding="utf-8"))
def selected(tree):
return [
node
for node in ast.walk(tree)
if (isinstance(node, ast.ClassDef) and node.name == "GovernedRuntime")
or (isinstance(node, ast.FunctionDef) and node.name == "_verified_request")
]
def without_attributes(nodes):
return [ast.dump(node, include_attributes=False) for node in nodes]
assert without_attributes(selected(candidate_tree)) == without_attributes(
selected(live_tree)
)
@@ -0,0 +1,474 @@
from __future__ import annotations
from concurrent.futures import ThreadPoolExecutor
from pathlib import Path
import sys
import threading
import subprocess
from dataclasses import replace
import pytest
ROOT = Path(__file__).resolve().parents[1]
APP = ROOT / "active-runtime" / "app"
if str(APP) not in sys.path:
sys.path.insert(0, str(APP))
from aios_task_admission_bootstrap import ( # noqa: E402
OwnerDelegationEpoch,
TaskAdmissionBootstrapService,
TaskAdmissionError,
TaskAdmissionPolicy,
TaskAdmissionStore,
)
NOW = 1_800_000_000
CAPS = ("governed.task.create", "agent-task:control")
class Authorization:
def __init__(self, digest: str):
self.epoch = OwnerDelegationEpoch(
epoch_id="epoch-recovery", owner_identity="owner", trust_domain="aios.owner",
repository="aios/aios", allowed_branch_prefixes=("agent/recovery-",),
allowed_capabilities=frozenset(CAPS), allowed_file_prefixes=("docs",),
risk_ceiling="medium", issued_at=NOW - 1, not_before=NOW - 1,
expires_at=NOW + 600, revocation_epoch=0, policy_digest=digest,
)
def verify_delegation_epoch(self, epoch_id):
if epoch_id != self.epoch.epoch_id:
raise TaskAdmissionError("UNKNOWN_EPOCH", 403)
return self.epoch
def verify_owner_signature(self, **_kwargs):
return "owner"
class RecoveringPort:
def __init__(self, *, fail_after_effect=False, fail_before_effect=False):
self.fail_after_effect = fail_after_effect
self.fail_before_effect = fail_before_effect
self.posts = 0
self.task_ids = set()
self._lock = threading.Lock()
def admit(self, grant):
with self._lock:
if grant.task_id in self.task_ids:
return {"task_id": grant.task_id, "state": "QUEUED"}
if self.fail_before_effect:
self.fail_before_effect = False
raise RuntimeError("before effect")
self.posts += 1
self.task_ids.add(grant.task_id)
if self.fail_after_effect:
self.fail_after_effect = False
raise RuntimeError("after effect")
return {"task_id": grant.task_id, "state": "QUEUED"}
def request():
return {
"idempotency_key": "recovery-admission-key", "task_id": "recovery-task-20260903",
"repository": "aios/aios", "authority_base_oid": "a" * 40,
"target_branch": "agent/recovery-20260903", "generation": 1,
"execution_digest": "b" * 64, "requested_capabilities": list(CAPS),
"file_allowlist": ["docs/recovery.md"], "risk": "medium", "expires_in_seconds": 300,
}
def make_service(path, port):
policy = TaskAdmissionPolicy(allowed_capabilities=frozenset(CAPS))
auth = Authorization(policy.normalized_policy_digest())
return TaskAdmissionBootstrapService(
store=TaskAdmissionStore(path), authorization=auth, task_port=port,
policy=policy, clock=lambda: NOW,
), auth
@pytest.mark.parametrize("after_effect", [True, False])
def test_replay_recovers_crash_window_with_one_effect(tmp_path, after_effect):
database = tmp_path / "admission.sqlite3"
port = RecoveringPort(
fail_after_effect=after_effect, fail_before_effect=not after_effect,
)
first, auth = make_service(database, port)
envelope_id = first.create_envelope(request())["envelope"]["envelope_id"]
proof = {"envelope_id": envelope_id, "authorization_kind": "delegation_epoch", "delegation_epoch_id": auth.epoch.epoch_id}
with pytest.raises(TaskAdmissionError, match="TASK_ADMISSION_TASK_STATE_UNKNOWN"):
first.authorize_and_admit(proof)
second, _ = make_service(database, port)
replay = second.authorize_and_admit(proof)
assert replay["task_id"] == "recovery-task-20260903"
assert port.posts == 1
def test_cross_instance_recovery_lock_allows_one_effect(tmp_path):
database = tmp_path / "admission.sqlite3"
port = RecoveringPort()
first, auth = make_service(database, port)
envelope_id = first.create_envelope(request())["envelope"]["envelope_id"]
proof = {"envelope_id": envelope_id, "authorization_kind": "delegation_epoch", "delegation_epoch_id": auth.epoch.epoch_id}
services = [make_service(database, port)[0] for _ in range(16)]
with ThreadPoolExecutor(max_workers=16) as pool:
receipts = list(pool.map(lambda svc: svc.authorize_and_admit(proof), services))
assert {value["task_id"] for value in receipts} == {"recovery-task-20260903"}
assert port.posts == 1
def test_changed_valid_epoch_proof_conflicts_after_receipt(tmp_path):
service, auth = make_service(tmp_path / "admission.sqlite3", RecoveringPort())
envelope_id = service.create_envelope(request())["envelope"]["envelope_id"]
first = {"envelope_id": envelope_id, "authorization_kind": "delegation_epoch", "delegation_epoch_id": auth.epoch.epoch_id}
service.authorize_and_admit(first)
auth.epoch = OwnerDelegationEpoch(
**{**auth.epoch.as_public_dict(), "epoch_id": "epoch-recovery-reissued",
"allowed_capabilities": auth.epoch.allowed_capabilities,
"allowed_branch_prefixes": auth.epoch.allowed_branch_prefixes,
"allowed_file_prefixes": auth.epoch.allowed_file_prefixes}
)
changed = {**first, "delegation_epoch_id": auth.epoch.epoch_id}
with pytest.raises(TaskAdmissionError, match="TASK_ADMISSION_AUTHORIZATION_CONFLICT"):
service.authorize_and_admit(changed)
@pytest.mark.parametrize("value", ["docs/a.md", b"docs/a.md", {"docs/a.md": True}, [], ["docs/a\x01.md"], ["docs\\a.md"]])
def test_file_allowlist_requires_canonical_sequence(tmp_path, value):
service, _ = make_service(tmp_path / "admission.sqlite3", RecoveringPort())
with pytest.raises(TaskAdmissionError, match="TASK_ADMISSION_(FILE_ALLOWLIST_REJECTED|FILE_PATH_INVALID)"):
service.create_envelope({**request(), "file_allowlist": value})
@pytest.mark.parametrize("branch", ["agent/.hidden-task", "agent/recovery.lock", "agent/recovery@{x", "agent/recovery..task"])
def test_branch_components_reject_git_ambiguous_spelling(tmp_path, branch):
service, _ = make_service(tmp_path / "admission.sqlite3", RecoveringPort())
with pytest.raises(TaskAdmissionError, match="TASK_ADMISSION_BRANCH_REJECTED"):
service.create_envelope({**request(), "target_branch": branch})
def test_process_death_releases_admission_lock(tmp_path):
database = tmp_path / "death.sqlite3"
script = (
"import sys,os;from pathlib import Path;"
"from aios_task_admission_bootstrap import TaskAdmissionStore;"
"s=TaskAdmissionStore(Path(sys.argv[1]));"
"lock=s.admission_lock('death-envelope');lock.__enter__();os._exit(17)"
)
result = subprocess.run([sys.executable, "-c", script, str(database)], check=False)
assert result.returncode == 17
with TaskAdmissionStore(database).admission_lock("death-envelope", timeout_seconds=1):
pass
def test_persisted_grant_cannot_outlive_its_own_shorter_ttl(tmp_path):
port = RecoveringPort(fail_before_effect=True)
service, auth = make_service(tmp_path / "expiry.sqlite3", port)
service.policy = replace(service.policy, max_task_ttl_seconds=60)
auth.epoch = replace(auth.epoch, policy_digest=service.policy.normalized_policy_digest())
envelope = service.create_envelope(request())["envelope"]
proof = {"envelope_id": envelope["envelope_id"], "authorization_kind": "delegation_epoch", "delegation_epoch_id": auth.epoch.epoch_id}
with pytest.raises(TaskAdmissionError, match="TASK_ADMISSION_TASK_STATE_UNKNOWN"):
service.authorize_and_admit(proof)
service.clock = lambda: NOW + 61
with pytest.raises(TaskAdmissionError, match="TASK_ADMISSION_GRANT_EXPIRED"):
service.authorize_and_admit(proof)
assert port.posts == 0
def _adapter_fixture(tmp_path):
from aios_task_admission_live_adapters import AgentTaskAdmissionPort
service, auth = make_service(tmp_path / "adapter.sqlite3", RecoveringPort())
envelope = service.create_envelope(request())["envelope"]
service.authorize_and_admit({"envelope_id": envelope["envelope_id"], "authorization_kind": "delegation_epoch", "delegation_epoch_id": auth.epoch.epoch_id})
with service.store._connection() as con:
import json
from aios_task_admission_bootstrap import TaskAdmissionGrant
raw = json.loads(con.execute("SELECT grant_json FROM task_admission_grants").fetchone()[0])
grant = TaskAdmissionGrant(**{**raw, "capabilities": tuple(raw["capabilities"]), "file_allowlist": tuple(raw["file_allowlist"])})
plan = {"external_job_id": "fixture-exact-job", "repository": grant.repository,
"target_branch": grant.target_branch, "generation": grant.generation,
"execution_digest": grant.execution_digest, "steps": [{"operation": "fixture-only"}]}
class Transport:
posts = 0
task = None
error = None
def request(self, method, path, body=None):
if method == "POST": self.posts += 1; raise AssertionError("unexpected create")
if self.error: raise self.error
return self.task
transport = Transport()
port = AgentTaskAdmissionPort(transport, execution_plan_provider=lambda _: plan)
transport.task = {**port._body(grant, plan), "status": "QUEUED"}
return port, transport, grant, plan
@pytest.mark.parametrize("field", ["metadata", "instructions", "required_capabilities", "constraints", "deadline", "priority"])
def test_existing_task_conflict_never_becomes_absence(tmp_path, field):
port, transport, grant, _ = _adapter_fixture(tmp_path)
transport.task[field] = None
with pytest.raises(TaskAdmissionError, match="TASK_ADMISSION_TASK_IDENTITY_CONFLICT"):
port.admit(grant)
assert transport.posts == 0
def test_unknown_get_does_not_create_task(tmp_path):
port, transport, grant, _ = _adapter_fixture(tmp_path)
transport.error = TimeoutError("fixture readback unavailable")
with pytest.raises(TaskAdmissionError, match="TASK_ADMISSION_TASK_STATE_UNKNOWN"):
port.admit(grant)
assert transport.posts == 0
def test_execution_provider_keeps_full_payload_and_rejects_type_drift(tmp_path):
port, transport, grant, plan = _adapter_fixture(tmp_path)
assert port._execution_plan(grant) == plan
plan["generation"] = True
with pytest.raises(TaskAdmissionError, match="TASK_ADMISSION_EXECUTION_PLAN_CONFLICT"):
port.admit(grant)
assert transport.posts == 0
def test_signed_epoch_host_provider_reverifies_signature_and_policy(tmp_path):
import hashlib
import json
import time
from aios_task_admission_bootstrap import canonical_json_bytes
from aios_task_admission_live_adapters import (
SignedEpochDirectoryProvider,
task_admission_policy_from_epoch,
)
class Verifier:
def __init__(self):
self.calls = []
def verify(self, **kwargs):
self.calls.append(kwargs)
return True
now = int(time.time())
epoch_value = {
"schema": "aios.owner-delegation-epoch.v1",
"epoch_id": "owner-epoch-host-test-20260903",
"owner_identity": "aios-task-grant-signer",
"trust_domain": "aios.owner",
"repository": "aios/aios",
"allowed_branch_prefixes": ["agent/p0-test"],
"allowed_capabilities": ["git.read", "governed.write.file"],
"allowed_file_prefixes": ["active-runtime/app/example.py"],
"risk_ceiling": "medium",
"issued_at": now - 1,
"not_before": now - 1,
"expires_at": now + 600,
"revocation_epoch": 0,
"policy_digest": "a" * 64,
}
root = tmp_path / "epoch"
root.mkdir(mode=0o700)
(root / "owner-delegation-epoch.canonical.json").write_bytes(
canonical_json_bytes(epoch_value)
)
(root / "owner-delegation-epoch.sig").write_text(
"-----BEGIN SSH SIGNATURE-----\nabc\n-----END SSH SIGNATURE-----\n",
encoding="utf-8",
)
(root / "owner-delegation-epoch.key-id").write_text(
"SHA256:test-key\n",
encoding="utf-8",
)
revocations = tmp_path / "revocations.json"
revocations.write_bytes(
canonical_json_bytes({
"schema": "aios.owner-delegation-revocations.v1",
"revoked_epoch_ids": [],
})
)
for path in (
root / "owner-delegation-epoch.canonical.json",
root / "owner-delegation-epoch.sig",
root / "owner-delegation-epoch.key-id",
revocations,
):
path.chmod(0o600)
verifier = Verifier()
caller = {"service": "test", "trust_domain": "aios.owner"}
provider = SignedEpochDirectoryProvider(
root=root,
verifier=verifier,
revocation_file=revocations,
expected_caller=caller,
require_root_owner=False,
clock=lambda: now,
)
epoch = provider(epoch_id=epoch_value["epoch_id"], caller=caller)
policy = task_admission_policy_from_epoch(epoch)
assert verifier.calls[0]["namespace"] == "aios-owner-approval-v1"
assert verifier.calls[0]["payload"] == canonical_json_bytes(epoch_value)
assert policy.allowed_capabilities == frozenset(
epoch_value["allowed_capabilities"]
)
assert policy.policy_digest == epoch_value["policy_digest"]
def test_signed_epoch_host_provider_rejects_revocation_and_wrong_caller(tmp_path):
import time
from aios_task_admission_bootstrap import canonical_json_bytes
from aios_task_admission_live_adapters import SignedEpochDirectoryProvider
class Verifier:
def verify(self, **_kwargs):
return True
now = int(time.time())
epoch_id = "owner-epoch-host-revoked-20260903"
root = tmp_path / "epoch"
root.mkdir(mode=0o700)
value = {
"schema": "aios.owner-delegation-epoch.v1",
"epoch_id": epoch_id,
"owner_identity": "aios-task-grant-signer",
"trust_domain": "aios.owner",
"repository": "aios/aios",
"allowed_branch_prefixes": ["agent/p0-test"],
"allowed_capabilities": ["git.read"],
"allowed_file_prefixes": ["docs/test.md"],
"risk_ceiling": "medium",
"issued_at": now - 1,
"not_before": now - 1,
"expires_at": now + 600,
"revocation_epoch": 0,
"policy_digest": "b" * 64,
}
(root / "owner-delegation-epoch.canonical.json").write_bytes(
canonical_json_bytes(value)
)
(root / "owner-delegation-epoch.sig").write_text(
"sig", encoding="utf-8"
)
(root / "owner-delegation-epoch.key-id").write_text(
"SHA256:test-key", encoding="utf-8"
)
revocations = tmp_path / "revocations.json"
revocations.write_bytes(
canonical_json_bytes({
"schema": "aios.owner-delegation-revocations.v1",
"revoked_epoch_ids": [epoch_id],
})
)
for path in list(root.iterdir()) + [revocations]:
path.chmod(0o600)
caller = {"service": "test", "trust_domain": "aios.owner"}
provider = SignedEpochDirectoryProvider(
root=root,
verifier=Verifier(),
revocation_file=revocations,
expected_caller=caller,
require_root_owner=False,
clock=lambda: now,
)
with pytest.raises(TaskAdmissionError, match="TASK_ADMISSION_CALLER_BINDING_MISMATCH"):
provider(epoch_id=epoch_id, caller={"service": "wrong"})
with pytest.raises(TaskAdmissionError, match="TASK_ADMISSION_EPOCH_REVOKED"):
provider(epoch_id=epoch_id, caller=caller)
def test_execution_plan_host_provider_is_content_addressed(tmp_path):
import hashlib
from types import SimpleNamespace
from aios_task_admission_bootstrap import canonical_json_bytes
from aios_task_admission_live_adapters import ExecutionPlanDirectoryProvider
plan = {
"task_id": "p0-task-admission-bootstrap-20260903",
"repository": "aios/aios",
"target_branch": "agent/p0-task-admission-bootstrap-repair-20260903",
"generation": 1,
"external_job_id": "job-host-test",
"files": [],
}
digest = hashlib.sha256(canonical_json_bytes(plan)).hexdigest()
root = tmp_path / "plans"
root.mkdir(mode=0o700)
path = root / (digest + ".json")
path.write_bytes(canonical_json_bytes(plan))
path.chmod(0o600)
grant = SimpleNamespace(
execution_digest=digest,
task_id=plan["task_id"],
repository=plan["repository"],
target_branch=plan["target_branch"],
generation=1,
)
provider = ExecutionPlanDirectoryProvider(
root=root,
require_root_owner=False,
)
value = provider(grant)
assert value["execution_digest"] == digest
assert value["external_job_id"] == "job-host-test"
def test_bridge_persistent_consumer_is_opt_in_and_default_fail_closed():
source = (
ROOT / "active-runtime" / "app" / "aios_chatgpt_bridge_v2_server.py"
).read_text(encoding="utf-8")
assert 'os.environ.get("AIOS_TASK_ADMISSION_EPOCH_ROOT", "").strip()' in source
assert "build_task_admission_handlers_from_environment" in source
assert "task_admission_handlers is None" in source
def test_bridge_persistent_host_is_opt_in_and_activation_file_is_fail_closed():
text = (APP / "aios_chatgpt_bridge_v2_server.py").read_text(encoding="utf-8")
assert 'AIOS_TASK_ADMISSION_EPOCH_ROOT' in text
assert 'AIOS_TASK_ADMISSION_ACTIVATION_FILE' in text
assert '/var/lib/aios-gitops/bridge-v2-staging/task-admission-activation.json' in text
assert 'Path(activation_raw).is_file()' in text
assert 'build_task_admission_handlers_from_activation_file' in text
def test_host_default_state_database_stays_inside_existing_bridge_state_root():
text = (APP / "aios_task_admission_live_adapters.py").read_text(encoding="utf-8")
assert '/var/lib/aios-gitops/bridge-v2-staging/task-admission.sqlite3' in text
assert '/var/lib/aios-task-admission/state.sqlite3' not in text
def test_host_posix_permission_gate_is_skipped_when_posix_metadata_is_not_authoritative(
tmp_path,
monkeypatch,
):
import aios_task_admission_live_adapters as live
root = tmp_path / "non-posix-mode"
root.mkdir()
target = root / "value.json"
target.write_text("{}", encoding="utf-8")
# Deliberately make the synthesized mode permissive. On Windows this is
# normal and must not be interpreted as a POSIX trust failure.
root.chmod(0o777)
target.chmod(0o666)
monkeypatch.setattr(live, "_host_posix_metadata_enforced", lambda: False)
live._host_require_directory(root, root_owned=True)
live._host_require_regular(target, root_owned=True)
@pytest.mark.skipif(
__import__("os").name != "posix",
reason="POSIX mode-bit enforcement is a Linux/Unix trust boundary",
)
def test_host_posix_permission_gate_rejects_group_or_other_writable(tmp_path):
import aios_task_admission_live_adapters as live
root = tmp_path / "posix-mode"
root.mkdir(mode=0o700)
root.chmod(0o777)
with pytest.raises(
live.TaskAdmissionHostError,
match="SECURE_DIRECTORY_WRITABLE_BY_GROUP_OR_OTHER",
):
live._host_require_directory(root, root_owned=False)
@@ -112,3 +112,180 @@ def test_package_contract_declares_socket_activation_no_credentials_and_0755_lau
def test_provisioner_self_test_has_no_mutation(capsys):
assert provisioning.main(["--self-test"]) == 0
assert capsys.readouterr().out.strip() == "PASS"
def _replay_fixture(tmp_path: Path):
_source_root, payload_path, payload = _payload(tmp_path)
raw = payload_path.read_bytes()
signature = tmp_path / "owner.sig"
signature.write_bytes(b"owner-signature-test")
os.chmod(signature, 0o600)
config = provisioning.ProvisionerConfig()
config.credential_store = tmp_path / "credstore"
config.public_keyring = tmp_path / "state" / "public-keyring.json"
config.binding_file = tmp_path / "state" / "service-principal-binding.json"
config.receipt_log = tmp_path / "state" / "provisioning-receipts.jsonl"
config.credential_store.mkdir(mode=0o700)
os.chmod(config.credential_store, 0o700)
config.public_keyring.parent.mkdir(mode=0o700)
public = b"p" * 32
now = provisioning.utc_now()
keyring = provisioning._keyring(payload["signing_identity"], public, now)
config.public_keyring.write_bytes(provisioning.pretty_json(keyring))
os.chmod(config.public_keyring, 0o600)
binding = {
"schema": "aios.trusted-builder.service-principal-binding.v1",
"purpose": provisioning.PURPOSE,
"service_principal": payload["signing_identity"]["service_principal"],
"key_id": payload["signing_identity"]["key_id"],
"generation": payload["signing_identity"]["key_generation"],
"algorithm": "Ed25519",
"private_key_export": False,
"public_key_sha256": provisioning.sha256_bytes(public),
"source_commit": payload["source"]["gitea_main_oid"],
"source_tree": payload["source"]["gitea_main_tree_oid"],
"payload_sha256": provisioning.sha256_bytes(raw),
}
config.binding_file.write_bytes(provisioning.pretty_json(binding))
os.chmod(config.binding_file, 0o600)
for name in ("signing_private_key", "builder_hmac"):
target = config.credential_store / name
target.write_bytes(("encrypted-" + name).encode())
os.chmod(target, 0o600)
signature_bytes = signature.read_bytes()
receipt = {
"schema": "aios.trusted-builder-signer-provisioning-receipt.v1",
"status": "COMPLETED",
"created_at": provisioning.iso(now),
"operation_key": "payload:" + provisioning.sha256_bytes(raw) + ":signature:" + provisioning.sha256_bytes(signature_bytes),
"payload_sha256": provisioning.sha256_bytes(raw),
"signature_sha256": provisioning.sha256_bytes(signature_bytes),
"source_commit": payload["source"]["gitea_main_oid"],
"source_tree": payload["source"]["gitea_main_tree_oid"],
"key_id": payload["signing_identity"]["key_id"],
"generation": payload["signing_identity"]["key_generation"],
"algorithm": "Ed25519",
"purpose": provisioning.PURPOSE,
"service_principal": payload["signing_identity"]["service_principal"],
"service_principal_bound": True,
"public_key_sha256": provisioning.sha256_bytes(public),
"private_key_exportable": False,
"credential_projection": "systemd-encrypted-host-credential",
"negative_capability_tests": {name: True for name in sorted(provisioning.DENIED_CAPABILITIES)},
"production_mutation": False,
"protected_ref_mutation": False,
"tag_mutation": False,
"baseline_mutation": False,
}
return config, payload_path, payload, signature, receipt
def test_legacy_multiline_receipt_parse(tmp_path: Path):
path = tmp_path / "receipts.jsonl"
receipt = {"schema": "legacy", "status": "COMPLETED", "nested": {"value": True}}
path.write_bytes(provisioning.pretty_json(receipt))
assert provisioning._receipts(path) == [receipt]
def test_new_canonical_jsonl_receipt_parse(tmp_path: Path):
path = tmp_path / "receipts.jsonl"
rows = [{"id": 1}, {"id": 2}]
for row in rows:
provisioning._append(path, row)
assert path.read_bytes() == b"".join(provisioning.canonical_json(row) + b"\n" for row in rows)
assert provisioning._receipts(path) == rows
@pytest.mark.parametrize(
"content",
[b'{"status":"COMPLETED"}\nnot-json\n', b'{"status":"COMPLETED"'],
ids=["malformed", "truncated"],
)
def test_malformed_or_truncated_receipt_fails_closed(tmp_path: Path, content: bytes):
path = tmp_path / "receipts.jsonl"
path.write_bytes(content)
with pytest.raises(provisioning.ProvisioningError, match="RECEIPT_LOG_INVALID"):
provisioning._receipts(path)
def test_preexisting_valid_state_is_adopted_without_mutation(tmp_path: Path, monkeypatch):
config, payload_path, _payload_value, signature, receipt = _replay_fixture(tmp_path)
config.receipt_log.write_bytes(provisioning.pretty_json(receipt))
os.chmod(config.receipt_log, 0o600)
before = {
path: path.read_bytes()
for path in (
config.receipt_log, config.public_keyring, config.binding_file,
config.credential_store / "signing_private_key",
config.credential_store / "builder_hmac",
)
}
monkeypatch.setattr(provisioning, "_run_signature_verify", lambda *_args: None)
monkeypatch.setattr(provisioning, "_verify_replay_source", lambda *_args: None)
monkeypatch.setattr(provisioning.os, "geteuid", lambda: 0)
result = provisioning.provision(config, payload_path, signature)
assert result["status"] == "IDEMPOTENT_REPLAY_PASS"
assert result["mutation_executed"] is False
assert result["existing_signer_state_adopted"] is True
assert result["private_key_exportable"] is False
assert before == {path: path.read_bytes() for path in before}
def test_duplicate_replay_is_idempotent(tmp_path: Path, monkeypatch):
config, payload_path, _payload_value, signature, receipt = _replay_fixture(tmp_path)
config.receipt_log.write_bytes(provisioning.pretty_json(receipt))
os.chmod(config.receipt_log, 0o600)
monkeypatch.setattr(provisioning, "_run_signature_verify", lambda *_args: None)
monkeypatch.setattr(provisioning, "_verify_replay_source", lambda *_args: None)
monkeypatch.setattr(provisioning.os, "geteuid", lambda: 0)
first = provisioning.provision(config, payload_path, signature)
receipt_bytes = config.receipt_log.read_bytes()
second = provisioning.provision(config, payload_path, signature)
assert first == second
assert config.receipt_log.read_bytes() == receipt_bytes
def test_wrong_payload_is_rejected_against_preexisting_state(tmp_path: Path, monkeypatch):
config, payload_path, payload, signature, receipt = _replay_fixture(tmp_path)
config.receipt_log.write_bytes(provisioning.pretty_json(receipt))
os.chmod(config.receipt_log, 0o600)
payload["nonce"] = "wrong-payload-nonce-0002"
payload_path.write_bytes(provisioning.pretty_json(payload))
monkeypatch.setattr(provisioning, "_run_signature_verify", lambda *_args: None)
monkeypatch.setattr(provisioning, "_verify_source", lambda *_args: None)
monkeypatch.setattr(provisioning.os, "geteuid", lambda: 0)
with pytest.raises(provisioning.ProvisioningError, match="PREEXISTING_STATE_CONFLICT"):
provisioning.provision(config, payload_path, signature)
def test_wrong_signature_is_rejected(tmp_path: Path, monkeypatch):
config, payload_path, _payload_value, signature, receipt = _replay_fixture(tmp_path)
config.receipt_log.write_bytes(provisioning.pretty_json(receipt))
monkeypatch.setattr(
provisioning,
"_run_signature_verify",
lambda *_args: (_ for _ in ()).throw(provisioning.ProvisioningError("OWNER_SIGNATURE_VERIFY_FAILED")),
)
monkeypatch.setattr(provisioning.os, "geteuid", lambda: 0)
with pytest.raises(provisioning.ProvisioningError, match="OWNER_SIGNATURE_VERIFY_FAILED"):
provisioning.provision(config, payload_path, signature)
def test_wrong_purpose_is_rejected(tmp_path: Path):
_source_root, _payload_path, payload = _payload(tmp_path)
payload["signing_identity"]["purpose"] = "WRONG_PURPOSE"
with pytest.raises(provisioning.ProvisioningError, match="SIGNING_IDENTITY_INVALID"):
provisioning._validate_payload(payload)
def test_replay_rejects_state_binding_tampering(tmp_path: Path, monkeypatch):
config, payload_path, _payload_value, signature, receipt = _replay_fixture(tmp_path)
receipt["service_principal"] = "wrong.service"
config.receipt_log.write_bytes(provisioning.pretty_json(receipt))
monkeypatch.setattr(provisioning, "_run_signature_verify", lambda *_args: None)
monkeypatch.setattr(provisioning, "_verify_replay_source", lambda *_args: None)
monkeypatch.setattr(provisioning.os, "geteuid", lambda: 0)
with pytest.raises(provisioning.ProvisioningError, match="RECEIPT_CONFLICT"):
provisioning.provision(config, payload_path, signature)