Compare commits

..
24 changed files with 3633 additions and 1131 deletions
@@ -94,6 +94,91 @@ class AuthorityGitExecutor:
self._git(["worktree", "add", "--detach", str(path), base_oid])
return path
def recover_failed_detached_worktree_exact(
self,
*,
path: Path,
expected_oid: str,
stale_oid: str,
target_ref: str,
) -> str:
"""Recreate one exact clean, detached, single-attempt worktree at its durable head."""
_oid(expected_oid, "expected_oid")
_oid(stale_oid, "stale_oid")
if not TASK_BRANCH_RE.fullmatch(target_ref):
raise AuthorityGitError("TASK_BRANCH_SCOPE_DENIED")
if self.read_ref(target_ref) is not None:
raise AuthorityGitError("AUTHORITY_TARGET_BRANCH_ALREADY_EXISTS")
if (
not path.is_absolute()
or path.parent != self.worktree_root
or path.name in {"", ".", ".."}
or path.is_symlink()
or not path.is_dir()
):
raise AuthorityGitError("WORKTREE_RECOVERY_PATH_INVALID")
head = subprocess.run(
["git", "-C", str(path), "rev-parse", "HEAD"],
text=True, capture_output=True, check=False,
)
if head.returncode != 0 or head.stdout.strip() != stale_oid or stale_oid == expected_oid:
raise AuthorityGitError("WORKTREE_RECOVERY_HEAD_INVALID")
symbolic = subprocess.run(
["git", "-C", str(path), "symbolic-ref", "-q", "HEAD"],
text=True, capture_output=True, check=False,
)
if symbolic.returncode != 1:
raise AuthorityGitError("WORKTREE_RECOVERY_NOT_DETACHED")
status = subprocess.run(
["git", "-C", str(path), "status", "--porcelain=v1", "--untracked-files=all"],
text=True, capture_output=True, check=False,
)
if status.returncode != 0 or status.stdout:
raise AuthorityGitError("WORKTREE_RECOVERY_NOT_CLEAN")
parents = self._git(["rev-list", "--parents", "-n", "1", stale_oid]).split()
if parents != [stale_oid, expected_oid]:
raise AuthorityGitError("WORKTREE_RECOVERY_LINEAGE_INVALID")
registered = [
line.removeprefix("worktree ")
for line in self._git(["worktree", "list", "--porcelain"]).splitlines()
if line.startswith("worktree ")
]
if registered.count(str(path)) != 1:
raise AuthorityGitError("WORKTREE_RECOVERY_REGISTRATION_INVALID")
self._git(["worktree", "remove", "--force", str(path)])
if path.exists() or path.is_symlink():
raise AuthorityGitError("WORKTREE_RECOVERY_REMOVE_FAILED")
recreated = self.create_detached_worktree(expected_oid, path.name, path.parent)
readback = subprocess.run(
["git", "-C", str(recreated), "rev-parse", "HEAD"],
text=True, capture_output=True, check=False,
)
detached = subprocess.run(
["git", "-C", str(recreated), "symbolic-ref", "-q", "HEAD"],
text=True, capture_output=True, check=False,
)
if readback.returncode != 0 or readback.stdout.strip() != expected_oid or detached.returncode != 1:
raise AuthorityGitError("WORKTREE_RECOVERY_READBACK_MISMATCH")
return stale_oid
def create_task_branch_CAS(self, branch: str, expected_base_oid: str, new_oid: str) -> str:
if not TASK_BRANCH_RE.fullmatch(branch):
raise AuthorityGitError("TASK_BRANCH_SCOPE_DENIED")
_oid(expected_base_oid, "expected_base_oid")
_oid(new_oid, "new_oid")
current = self.read_ref(branch)
if current is not None:
if current == new_oid:
return new_oid
raise AuthorityGitError("AUTHORITY_TARGET_BRANCH_ALREADY_EXISTS")
if not self.ancestry_check(expected_base_oid, new_oid):
raise AuthorityGitError("NEW_OID_NOT_DESCENDANT")
self._git(["update-ref", branch, new_oid, "0" * 40])
readback = self.read_ref(branch)
if readback != new_oid:
raise AuthorityGitError("AUTHORITY_CREATE_READBACK_MISMATCH")
return readback
def advance_task_branch_CAS(self, branch: str, expected_oid: str, new_oid: str) -> str:
if not TASK_BRANCH_RE.fullmatch(branch):
raise AuthorityGitError("TASK_BRANCH_SCOPE_DENIED")
@@ -13,6 +13,8 @@ BRIDGE_OPERATIONS = (
"getGovernedWriteCapabilities",
"createGovernedTask",
"acquireWriterLease",
"bootstrapWriterLease",
"renewWriterLease",
"commitFileGoverned",
"commitFilesGoverned",
"getGitOpsJob",
@@ -31,6 +33,8 @@ BRIDGE_OPERATIONS = (
OPERATION_REQUIRED_FIELDS = {
"createGovernedTask": ("task_id", "repository", "base_oid", "target_branch", "risk", "description", "idempotency_key"),
"acquireWriterLease": ("schema", "task_id", "repository", "branch_mode", "base_oid", "expected_branch_oid", "target_branch", "generation", "lease_seconds"),
"bootstrapWriterLease": ("schema", "task_id", "repository", "branch_mode", "base_oid", "expected_branch_oid", "target_branch", "generation", "lease_seconds"),
"renewWriterLease": ("schema", "task_id", "repository", "branch_mode", "base_oid", "expected_branch_oid", "target_branch", "generation", "lease_seconds"),
"commitFileGoverned": ("task_id", "repository", "expected_branch_oid", "target_branch", "generation", "idempotency_key", "path", "content_base64", "message"),
"commitFilesGoverned": ("task_id", "repository", "expected_branch_oid", "target_branch", "generation", "idempotency_key", "files", "message"),
"getGitOpsJob": ("job_id",),
@@ -46,6 +50,49 @@ OPERATION_REQUIRED_FIELDS = {
"getControlPlaneSnapshot": (),
}
BRIDGE_OPERATION_CONTRACTS = {
"acquireWriterLease": {
"method": "POST",
"path": "/v2/writer-lease",
"required_fields": OPERATION_REQUIRED_FIELDS["acquireWriterLease"],
"response_fields": (
"task_id", "job_id", "generation", "lease_seconds", "lease_expires_at",
"create_target_branch", "worktree_mode", "cas_ref_update_required",
),
},
"bootstrapWriterLease": {
"method": "POST",
"path": "/v2/writer-lease/bootstrap",
"required_fields": OPERATION_REQUIRED_FIELDS["bootstrapWriterLease"],
"response_fields": (
"task_id", "job_id", "generation", "lease_seconds", "lease_expires_at",
"create_target_branch", "worktree_mode", "cas_ref_update_required",
"recovered", "bootstrapped", "runtime_rehydrated",
"durable_branch_lease_present", "idempotent",
),
},
"renewWriterLease": {
"method": "POST",
"path": "/v2/writer-lease/renew",
"required_fields": OPERATION_REQUIRED_FIELDS["renewWriterLease"],
"response_fields": (
"task_id", "job_id", "generation", "lease_seconds", "lease_expires_at",
"recovered", "bootstrapped", "runtime_rehydrated",
"durable_branch_lease_present", "idempotent",
),
},
"commitFileGoverned": {
"method": "POST", "path": "/v2/commit-file",
"required_fields": OPERATION_REQUIRED_FIELDS["commitFileGoverned"],
"response_fields": ("job_id", "commit_oid", "commit_visible", "branch_mode", "target_branch", "generation"),
},
"commitFilesGoverned": {
"method": "POST", "path": "/v2/commit-files",
"required_fields": OPERATION_REQUIRED_FIELDS["commitFilesGoverned"],
"response_fields": ("job_id", "commit_oid", "commit_visible", "branch_mode", "target_branch", "generation"),
},
}
class RuntimeDriftError(RuntimeError):
pass
@@ -122,6 +169,14 @@ class BridgeV2Adapter:
"legacy_writes_enabled": False,
"governed_writes_enabled": True,
"operations": list(BRIDGE_OPERATIONS),
"operation_contracts": {
operation: {
**contract,
"required_fields": list(contract["required_fields"]),
"response_fields": list(contract["response_fields"]),
}
for operation, contract in BRIDGE_OPERATION_CONTRACTS.items()
},
"force_push": False,
"main_write": False,
"tag_write": False,
@@ -151,6 +206,8 @@ class BridgeV2Adapter:
def createGovernedTask(self, request: Mapping[str, Any]) -> Any: return self._call("createGovernedTask", request)
def acquireWriterLease(self, request: Mapping[str, Any]) -> Any: return self._call("acquireWriterLease", request)
def bootstrapWriterLease(self, request: Mapping[str, Any]) -> Any: return self._call("bootstrapWriterLease", request)
def renewWriterLease(self, request: Mapping[str, Any]) -> Any: return self._call("renewWriterLease", request)
def commitFileGoverned(self, request: Mapping[str, Any]) -> Any: return self._call("commitFileGoverned", request)
def commitFilesGoverned(self, request: Mapping[str, Any]) -> Any: return self._call("commitFilesGoverned", request)
def getGitOpsJob(self, request: Mapping[str, Any]) -> Any: return self._call("getGitOpsJob", request)
File diff suppressed because it is too large Load Diff
+125 -27
View File
@@ -51,11 +51,13 @@ class AuthorityPort(Protocol):
def branch_exists(self, branch: str) -> bool: ...
def branch_head(self, branch: str) -> str: ...
def is_ancestor(self, older: str, newer: str) -> bool: ...
def create_task_branch_cas(self, branch: str, expected_base: str, new: str) -> str: ...
def compare_and_swap_branch(self, branch: str, expected: str, new: str) -> str: ...
class RemotePort(Protocol):
def repository_identity(self) -> str: ...
def push_new_exact(self, branch: str, new: str) -> None: ...
def push_exact(self, branch: str, expected: str, new: str) -> None: ...
def read_git_ref(self, branch: str) -> str | None: ...
def read_api_branch(self, branch: str) -> str | None: ...
@@ -103,6 +105,9 @@ class AuthorityGitAdapter:
def is_ancestor(self, older: str, newer: str) -> bool:
return self.executor.ancestry_check(older, newer)
def create_task_branch_cas(self, branch: str, expected_base: str, new: str) -> str:
return self.executor.create_task_branch_CAS(self._ref(branch), expected_base, new)
def compare_and_swap_branch(self, branch: str, expected: str, new: str) -> str:
return self.executor.advance_task_branch_CAS(self._ref(branch), expected, new)
@@ -258,6 +263,15 @@ class GiteaContinuationRemote:
raise ContinuationExecutionError("GITEA_GIT_READBACK_INVALID")
return oid
def push_new_exact(self, branch: str, new: str) -> None:
self._require_projected()
self.repository_identity()
if self.read_git_ref(branch) is not None:
raise ContinuationExecutionError("GITEA_TARGET_BRANCH_ALREADY_EXISTS")
_run(["git", "push", self.remote_name, f"{new}:refs/heads/{branch}"], cwd=self.workspace)
if self.read_git_ref(branch) != new:
raise ContinuationExecutionError("GITEA_CREATE_READBACK_MISMATCH")
def push_exact(self, branch: str, expected: str, new: str) -> None:
self._require_projected()
self.repository_identity()
@@ -329,6 +343,7 @@ class ContinuationPermissions:
@dataclass(frozen=True, slots=True)
class ContinuationRuntimeBinding:
request: WriterLeaseRequestV2
job_id: str
workspace_path: Path
lease_not_before: datetime
lease_expires_at: datetime
@@ -389,13 +404,20 @@ class ContinuationExecutor:
def observe_generation(self, branch: str, generation: int) -> None:
self._observed_generations[branch] = max(generation, self._observed_generations.get(branch, 0))
def _max_generation(self, branch: str) -> int:
def _max_generation(self, branch: str, *, exclude_job_id: str | None = None) -> int:
observed = self._observed_generations.get(branch, 0)
with self.controller.store._connect() as connection:
if exclude_job_id is None:
row = connection.execute(
"SELECT MAX(generation) AS generation FROM branch_leases WHERE branch=?",
(branch,),
).fetchone()
else:
row = connection.execute(
"SELECT MAX(generation) AS generation FROM branch_leases "
"WHERE branch=? AND job_id!=?",
(branch, exclude_job_id),
).fetchone()
durable = int(row["generation"] or 0)
return max(observed, durable)
@@ -404,6 +426,7 @@ class ContinuationExecutor:
return {
"schema": WriterLeaseRequestV2.schema,
"task_id": request.task_id,
"job_id": request.job_id,
"repository": request.repository,
"branch_mode": request.branch_mode,
"base_oid": request.base_oid,
@@ -413,10 +436,21 @@ class ContinuationExecutor:
"lease_seconds": request.lease_seconds,
}
def _validate_preflight(self, binding: ContinuationRuntimeBinding) -> None:
def _validate_preflight(self, binding: ContinuationRuntimeBinding, *, job_id: str | None = None) -> None:
request = binding.request
normalized = validate_writer_lease_request_v2(self._request_mapping(request), self.authority)
if normalized != {**self._request_mapping(request), "create_target_branch": False, "worktree_mode": "detached_at_expected_head", "cas_ref_update_required": True}:
request_mapping = self._request_mapping(request)
normalized = validate_writer_lease_request_v2(request_mapping, self.authority)
expected_normalized = {
**request_mapping,
"create_target_branch": request.branch_mode == "CREATE_NEW",
"worktree_mode": (
"detached_at_base_oid"
if request.branch_mode == "CREATE_NEW"
else "detached_at_expected_head"
),
"cas_ref_update_required": True,
}
if normalized != expected_normalized:
raise ContinuationExecutionError("CONTINUATION_NORMALIZATION_INVALID")
if not binding.original_receipt_valid or not binding.original_grant_consumed:
raise ContinuationExecutionError("ORIGINAL_GRANT_LINEAGE_INVALID")
@@ -426,7 +460,7 @@ class ContinuationExecutor:
raise ContinuationExecutionError("LEASE_NOT_ACTIVE")
if now >= binding.lease_expires_at:
raise ContinuationExecutionError("LEASE_EXPIRED")
if request.generation <= self._max_generation(request.target_branch):
if request.generation <= self._max_generation(request.target_branch, exclude_job_id=job_id):
raise ContinuationExecutionError("GENERATION_NOT_MONOTONIC")
if not self.workspace.is_detached():
raise ContinuationExecutionError("WORKTREE_NOT_DETACHED")
@@ -452,57 +486,102 @@ class ContinuationExecutor:
"generation": request.generation,
}
def execute(self, binding: ContinuationRuntimeBinding, commit: ContinuationCommit) -> dict[str, object]:
payload = commit.payload()
submitted = self.controller.submit(self._job_request(binding, commit, payload), payload=payload)
current = self.controller.store.read(submitted.job_id)
if JobState(current["state"]) == JobState.COMPLETED:
def execute(
self,
binding: ContinuationRuntimeBinding,
commit: ContinuationCommit,
*,
before_complete: Callable[[str], None] | None = None,
) -> dict[str, object]:
commit.payload()
if not binding.job_id or binding.request.job_id != binding.job_id:
raise ContinuationExecutionError("RUNTIME_LINEAGE_MISSING_JOB_ID")
current = self.controller.store.read(binding.job_id)
current_state = JobState(current["state"])
if current_state == JobState.COMPLETED:
return self.controller.store._receipt(current, duplicate=True).as_dict()
self._validate_preflight(binding)
if current_state == JobState.REMOTE_VERIFIED:
receipt = json.loads(str(current["receipt_json"] or "{}"))
commit_oid = str(receipt.get("authority_oid") or "")
if not OID_RE.fullmatch(commit_oid):
raise ContinuationExecutionError("GOVERNED_COMMIT_OID_INVALID")
if before_complete is not None:
before_complete(commit_oid)
return self.controller.advance(
binding.job_id,
JobState.COMPLETED,
side_effect_state="REMOTE_VERIFIED",
receipt=receipt,
).as_dict()
self._validate_preflight(binding, job_id=binding.job_id)
request = binding.request
projected = False
try:
self.credential.project()
projected = True
row = self.controller.store.read(submitted.job_id)
if JobState(row["state"]) == JobState.REQUESTED:
row = self.controller.store.read(binding.job_id)
state = JobState(row["state"])
if state == JobState.REQUESTED:
self.controller.advance(
submitted.job_id,
binding.job_id,
JobState.AUTHORIZED,
side_effect_state="AUTHORIZED",
receipt={"task_id": request.task_id, "original_grant_consumed": True},
expected_revision=int(row["revision"]),
)
row = self.controller.store.read(submitted.job_id)
if JobState(row["state"]) == JobState.AUTHORIZED:
row = self.controller.store.read(binding.job_id)
state = JobState(row["state"])
if state == JobState.AUTHORIZED:
self.controller.lease_job(
submitted.job_id,
lease_id=f"lease-{submitted.job_id}",
binding.job_id,
lease_id=f"lease-{binding.job_id}",
lineage_anchor_oid=request.base_oid,
current_head_oid=request.expected_branch_oid,
generation=request.generation,
lease_expires_at=binding.lease_expires_at.astimezone(timezone.utc).isoformat(),
expected_revision=int(row["revision"]),
)
self.controller.advance(submitted.job_id, JobState.WORKTREE_READY, side_effect_state="WORKTREE_READY")
self.controller.advance(submitted.job_id, JobState.EXECUTING, side_effect_state="IN_PROGRESS")
row = self.controller.store.read(binding.job_id)
state = JobState(row["state"])
if state == JobState.LEASED:
self.controller.advance(binding.job_id, JobState.WORKTREE_READY,
side_effect_state="WORKTREE_READY", expected_revision=int(row["revision"]))
row = self.controller.store.read(binding.job_id)
state = JobState(row["state"])
if state == JobState.WORKTREE_READY:
self.controller.advance(binding.job_id, JobState.EXECUTING,
side_effect_state="IN_PROGRESS", expected_revision=int(row["revision"]))
elif state != JobState.EXECUTING:
raise ContinuationExecutionError("JOB_EXECUTION_STATE_INVALID")
new_oid = self.workspace.commit_files(commit.files, commit.message)
if not OID_RE.fullmatch(new_oid) or not self.authority.is_ancestor(request.expected_branch_oid, new_oid):
raise ContinuationExecutionError("NON_FAST_FORWARD_COMMIT")
if request.branch_mode == "CREATE_NEW":
authority_oid = self.authority.create_task_branch_cas(request.target_branch, request.base_oid, new_oid)
else:
authority_oid = self.authority.compare_and_swap_branch(request.target_branch, request.expected_branch_oid, new_oid)
if authority_oid != new_oid:
raise ContinuationExecutionError("AUTHORITY_CAS_READBACK_MISMATCH")
self.controller.advance(
submitted.job_id,
binding.job_id,
JobState.LOCAL_VERIFIED,
side_effect_state="LOCAL_VERIFIED",
receipt={"authority_oid": authority_oid, "force_push": False},
)
try:
self.controller.advance(submitted.job_id, JobState.REMOTE_SYNCING, side_effect_state="IN_PROGRESS")
self.controller.advance(binding.job_id, JobState.REMOTE_SYNCING, side_effect_state="IN_PROGRESS")
if request.branch_mode == "CREATE_NEW":
self.remote.push_new_exact(request.target_branch, new_oid)
else:
self.remote.push_exact(request.target_branch, request.expected_branch_oid, new_oid)
except Exception:
return self._remote_pending(submitted.job_id, authority_oid, new_oid)
return self._finish_remote_verification(submitted.job_id, authority_oid, new_oid)
return self._remote_pending(binding.job_id, authority_oid, new_oid)
return self._finish_remote_verification(
binding.job_id,
authority_oid,
new_oid,
before_complete=before_complete,
)
finally:
if projected:
self.credential.revoke()
@@ -525,7 +604,14 @@ class ContinuationExecutor:
)
return result.as_dict()
def _finish_remote_verification(self, job_id: str, authority_oid: str, new_oid: str) -> dict[str, object]:
def _finish_remote_verification(
self,
job_id: str,
authority_oid: str,
new_oid: str,
*,
before_complete: Callable[[str], None] | None = None,
) -> dict[str, object]:
row = self.controller.store.read(job_id)
branch = row["branch"]
request = json.loads(row["request_json"])["binding"]
@@ -542,8 +628,20 @@ class ContinuationExecutor:
"reconciliation_decision": decision.action.value,
"force_push": False,
}
self.controller.advance(job_id, JobState.REMOTE_VERIFIED, side_effect_state="REMOTE_VERIFIED", receipt=receipt)
return self.controller.advance(job_id, JobState.COMPLETED, side_effect_state="REMOTE_VERIFIED", receipt=receipt).as_dict()
self.controller.advance(
job_id,
JobState.REMOTE_VERIFIED,
side_effect_state="REMOTE_VERIFIED",
receipt=receipt,
)
if before_complete is not None:
before_complete(new_oid)
return self.controller.advance(
job_id,
JobState.COMPLETED,
side_effect_state="REMOTE_VERIFIED",
receipt=receipt,
).as_dict()
def reconcile(self, job_id: str) -> dict[str, object]:
row = self.controller.store.read(job_id)
@@ -49,6 +49,7 @@ class WriterLeaseRequestV2:
target_branch: str
generation: int
lease_seconds: int = 900
job_id: str = ""
schema = "aios.task-writer-lease.v2"
accepted_schemas = frozenset({"aios.task-writer-lease.v2", "aios.task-writer-lease-request.v2"})
@@ -56,6 +57,8 @@ class WriterLeaseRequestV2:
def validate(self, probe: BranchProbe) -> None:
if not self.task_id or self.repository != "aios/aios":
raise ContinuationError("request_identity_invalid")
if not self.job_id or not self.job_id.startswith("job-"):
raise ContinuationError("job_id_invalid")
if self.branch_mode not in {"CREATE_NEW", "CONTINUE_EXISTING"}:
raise ContinuationError("branch_mode_invalid")
_oid(self.base_oid, "base_oid")
@@ -1,352 +0,0 @@
"""Cryptographic Owner-approval boundary for governed existing-branch publication.
Only a complete signed envelope from an authoritative trust-root adapter is
accepted. References, chat assertions, and locally supplied keys are not an
approval. Missing public-key or revocation evidence fails closed.
"""
from __future__ import annotations
import base64
from dataclasses import dataclass
from datetime import datetime, timezone
import hashlib
import json
import os
from pathlib import Path
import re
from typing import Any, Mapping, Protocol
from cryptography.exceptions import InvalidSignature
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PublicKey
APPROVAL_SCHEMA = "aios.existing-branch-owner-approval.v1"
PUBLISH_OPERATION = "publishExistingBranchGoverned"
OID_RE = re.compile(r"^[0-9a-f]{40}$")
DIGEST_RE = re.compile(r"^[0-9a-f]{64}$")
IDENTIFIER_RE = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._@:+/-]{0,254}$")
BRANCH_RE = re.compile(r"^agent/[A-Za-z0-9][A-Za-z0-9._/-]{2,179}$")
class OwnerApprovalRejected(RuntimeError):
"""Stable, audit-safe error for a rejected Owner approval."""
def __init__(self, code: str) -> None:
super().__init__(code)
self.code = code
def canonical_json(value: Any) -> bytes:
"""Encode a signed payload without implementation-dependent spacing."""
try:
encoded = json.dumps(
value,
sort_keys=True,
separators=(",", ":"),
ensure_ascii=False,
allow_nan=False,
)
except (TypeError, ValueError) as exc:
raise OwnerApprovalRejected("OWNER_APPROVAL_CANONICALIZATION_REJECTED") from exc
return (encoded + "\n").encode("utf-8")
def digest_json(value: Any) -> str:
return hashlib.sha256(canonical_json(value)).hexdigest()
def _require_identifier(field: str, value: object) -> str:
if not isinstance(value, str) or not IDENTIFIER_RE.fullmatch(value):
raise OwnerApprovalRejected(f"OWNER_APPROVAL_{field.upper()}_REJECTED")
return value
def _require_digest(field: str, value: object) -> str:
if not isinstance(value, str) or not DIGEST_RE.fullmatch(value):
raise OwnerApprovalRejected(f"OWNER_APPROVAL_{field.upper()}_REJECTED")
return value
def _require_oid(field: str, value: object) -> str:
if not isinstance(value, str) or not OID_RE.fullmatch(value):
raise OwnerApprovalRejected(f"OWNER_APPROVAL_{field.upper()}_REJECTED")
return value
def _parse_utc(field: str, value: object) -> datetime:
if not isinstance(value, str):
raise OwnerApprovalRejected(f"OWNER_APPROVAL_{field.upper()}_REJECTED")
try:
instant = datetime.fromisoformat(value.replace("Z", "+00:00"))
except ValueError as exc:
raise OwnerApprovalRejected(f"OWNER_APPROVAL_{field.upper()}_REJECTED") from exc
if instant.tzinfo is None:
raise OwnerApprovalRejected(f"OWNER_APPROVAL_{field.upper()}_REJECTED")
return instant.astimezone(timezone.utc)
@dataclass(frozen=True, slots=True)
class PublicationApprovalBinding:
repository: str
branch: str
expected_branch_oid: str
operation: str
request_digest: str
idempotency_digest: str
def validate(self) -> "PublicationApprovalBinding":
_require_identifier("repository", self.repository)
if not BRANCH_RE.fullmatch(self.branch):
raise OwnerApprovalRejected("OWNER_APPROVAL_BRANCH_REJECTED")
_require_oid("expected_branch_oid", self.expected_branch_oid)
if self.operation != PUBLISH_OPERATION:
raise OwnerApprovalRejected("OWNER_APPROVAL_OPERATION_REJECTED")
_require_digest("request_digest", self.request_digest)
_require_digest("idempotency_digest", self.idempotency_digest)
return self
@property
def as_dict(self) -> dict[str, str]:
return {
"repository": self.repository,
"branch": self.branch,
"expected_branch_oid": self.expected_branch_oid,
"operation": self.operation,
"request_digest": self.request_digest,
"idempotency_digest": self.idempotency_digest,
}
@dataclass(frozen=True, slots=True)
class OwnerApprovalEnvelope:
approval_id: str
repository: str
branch: str
expected_branch_oid: str
operation: str
issued_at: str
not_before: str
expires_at: str
nonce: str
owner_identity: str
trust_domain: str
key_id: str
request_digest: str
idempotency_digest: str
signature: str
@property
def canonical_payload(self) -> dict[str, str]:
return {
"schema": APPROVAL_SCHEMA,
"approval_id": self.approval_id,
"repository": self.repository,
"branch": self.branch,
"expected_branch_oid": self.expected_branch_oid,
"operation": self.operation,
"issued_at": self.issued_at,
"not_before": self.not_before,
"expires_at": self.expires_at,
"nonce": self.nonce,
"owner_identity": self.owner_identity,
"trust_domain": self.trust_domain,
"key_id": self.key_id,
"request_digest": self.request_digest,
"idempotency_digest": self.idempotency_digest,
}
def canonical_bytes(self) -> bytes:
return canonical_json(self.canonical_payload)
@property
def fingerprint(self) -> str:
return hashlib.sha256(self.canonical_bytes()).hexdigest()
@property
def binding(self) -> PublicationApprovalBinding:
return PublicationApprovalBinding(
repository=self.repository,
branch=self.branch,
expected_branch_oid=self.expected_branch_oid,
operation=self.operation,
request_digest=self.request_digest,
idempotency_digest=self.idempotency_digest,
)
@classmethod
def from_mapping(cls, value: object) -> "OwnerApprovalEnvelope":
if not isinstance(value, Mapping):
raise OwnerApprovalRejected("OWNER_APPROVAL_ENVELOPE_REQUIRED")
expected_fields = {
"schema", "approval_id", "repository", "branch", "expected_branch_oid",
"operation", "issued_at", "not_before", "expires_at", "nonce",
"owner_identity", "trust_domain", "key_id", "request_digest",
"idempotency_digest", "signature",
}
if set(value) != expected_fields or value.get("schema") != APPROVAL_SCHEMA:
raise OwnerApprovalRejected("OWNER_APPROVAL_ENVELOPE_REJECTED")
envelope = cls(
approval_id=_require_identifier("approval_id", value["approval_id"]),
repository=_require_identifier("repository", value["repository"]),
branch=str(value["branch"]),
expected_branch_oid=_require_oid("expected_branch_oid", value["expected_branch_oid"]),
operation=str(value["operation"]),
issued_at=str(value["issued_at"]),
not_before=str(value["not_before"]),
expires_at=str(value["expires_at"]),
nonce=_require_identifier("nonce", value["nonce"]),
owner_identity=_require_identifier("owner_identity", value["owner_identity"]),
trust_domain=_require_identifier("trust_domain", value["trust_domain"]),
key_id=_require_identifier("key_id", value["key_id"]),
request_digest=_require_digest("request_digest", value["request_digest"]),
idempotency_digest=_require_digest("idempotency_digest", value["idempotency_digest"]),
signature=str(value["signature"]),
)
envelope.binding.validate()
_parse_utc("issued_at", envelope.issued_at)
_parse_utc("not_before", envelope.not_before)
_parse_utc("expires_at", envelope.expires_at)
if not envelope.signature:
raise OwnerApprovalRejected("OWNER_APPROVAL_SIGNATURE_REJECTED")
return envelope
@dataclass(frozen=True, slots=True)
class TrustRootEvidence:
"""Public-key and revocation evidence from the authoritative trust root."""
key_id: str
owner_identity: str
trust_domain: str
public_key: bytes | None
authority_confirmed: bool
revocation_state: str
class AuthoritativeTrustRootAdapter(Protocol):
def resolve(
self, *, key_id: str, owner_identity: str, trust_domain: str
) -> TrustRootEvidence | None: ...
class ReplayLedger(Protocol):
def consume(
self, approval: OwnerApprovalEnvelope, binding: PublicationApprovalBinding
) -> None: ...
class DurableNonceReplayLedger:
"""Single-use nonce storage that remains conservative across restarts."""
def __init__(self, root: Path) -> None:
self.root = Path(root)
def consume(
self, approval: OwnerApprovalEnvelope, binding: PublicationApprovalBinding
) -> None:
binding.validate()
if self.root.is_symlink():
raise OwnerApprovalRejected("OWNER_APPROVAL_REPLAY_LEDGER_REJECTED")
try:
self.root.mkdir(parents=True, exist_ok=True)
if not self.root.is_dir() or self.root.is_symlink():
raise OwnerApprovalRejected("OWNER_APPROVAL_REPLAY_LEDGER_REJECTED")
name = hashlib.sha256(approval.nonce.encode("utf-8")).hexdigest()
record_path = self.root / name
flags = os.O_WRONLY | os.O_CREAT | os.O_EXCL
if hasattr(os, "O_NOFOLLOW"):
flags |= os.O_NOFOLLOW
descriptor = os.open(record_path, flags, 0o600)
except FileExistsError as exc:
raise OwnerApprovalRejected("OWNER_APPROVAL_REPLAY_REJECTED") from exc
except OwnerApprovalRejected:
raise
except OSError as exc:
raise OwnerApprovalRejected("OWNER_APPROVAL_REPLAY_LEDGER_REJECTED") from exc
record = {
"schema": "aios.existing-branch-owner-approval-replay.v1",
"nonce_sha256": name,
"approval_fingerprint": approval.fingerprint,
"binding_digest": digest_json(binding.as_dict),
}
try:
with os.fdopen(descriptor, "wb", closefd=True) as stream:
stream.write(canonical_json(record))
stream.flush()
os.fsync(stream.fileno())
directory = os.open(self.root, os.O_RDONLY | getattr(os, "O_DIRECTORY", 0))
try:
os.fsync(directory)
finally:
os.close(directory)
except OSError as exc:
raise OwnerApprovalRejected("OWNER_APPROVAL_REPLAY_LEDGER_REJECTED") from exc
class OwnerApprovalVerifier:
def __init__(
self,
trust_root: AuthoritativeTrustRootAdapter,
*,
allowed_trust_domains: set[str] | frozenset[str],
maximum_lifetime_seconds: int = 900,
) -> None:
if not allowed_trust_domains or maximum_lifetime_seconds < 1:
raise ValueError("OWNER_APPROVAL_VERIFIER_CONFIGURATION_REJECTED")
self.trust_root = trust_root
self.allowed_trust_domains = frozenset(allowed_trust_domains)
self.maximum_lifetime_seconds = maximum_lifetime_seconds
def verify(
self,
value: object,
binding: PublicationApprovalBinding,
*,
now: datetime | None = None,
) -> OwnerApprovalEnvelope:
binding.validate()
approval = OwnerApprovalEnvelope.from_mapping(value)
if approval.binding != binding:
raise OwnerApprovalRejected("OWNER_APPROVAL_BINDING_REJECTED")
instant = datetime.now(timezone.utc) if now is None else now
if instant.tzinfo is None:
raise OwnerApprovalRejected("OWNER_APPROVAL_TIME_REJECTED")
instant = instant.astimezone(timezone.utc)
issued_at = _parse_utc("issued_at", approval.issued_at)
not_before = _parse_utc("not_before", approval.not_before)
expires_at = _parse_utc("expires_at", approval.expires_at)
if not issued_at <= not_before <= expires_at:
raise OwnerApprovalRejected("OWNER_APPROVAL_TIME_REJECTED")
if not not_before <= instant <= expires_at:
raise OwnerApprovalRejected("OWNER_APPROVAL_TIME_REJECTED")
if (expires_at - issued_at).total_seconds() > self.maximum_lifetime_seconds:
raise OwnerApprovalRejected("OWNER_APPROVAL_TIME_REJECTED")
if approval.trust_domain not in self.allowed_trust_domains:
raise OwnerApprovalRejected("OWNER_APPROVAL_TRUST_DOMAIN_REJECTED")
evidence = self.trust_root.resolve(
key_id=approval.key_id,
owner_identity=approval.owner_identity,
trust_domain=approval.trust_domain,
)
if evidence is None or not evidence.authority_confirmed:
raise OwnerApprovalRejected("OWNER_APPROVAL_TRUST_ROOT_EVIDENCE_MISSING")
if (
evidence.key_id != approval.key_id
or evidence.owner_identity != approval.owner_identity
or evidence.trust_domain != approval.trust_domain
):
raise OwnerApprovalRejected("OWNER_APPROVAL_TRUST_ROOT_BINDING_REJECTED")
if evidence.revocation_state == "REVOKED":
raise OwnerApprovalRejected("OWNER_APPROVAL_REVOKED")
if evidence.revocation_state != "ACTIVE":
raise OwnerApprovalRejected("OWNER_APPROVAL_REVOCATION_EVIDENCE_MISSING")
if evidence.public_key is None or len(evidence.public_key) != 32:
raise OwnerApprovalRejected("OWNER_APPROVAL_TRUST_ROOT_EVIDENCE_MISSING")
try:
signature = base64.b64decode(approval.signature, validate=True)
Ed25519PublicKey.from_public_bytes(evidence.public_key).verify(
signature, approval.canonical_bytes()
)
except (ValueError, TypeError, InvalidSignature) as exc:
raise OwnerApprovalRejected("OWNER_APPROVAL_SIGNATURE_REJECTED") from exc
return approval
@@ -1,331 +0,0 @@
"""Narrow governed publisher for an already-existing task branch.
Its only writing port is a compare-and-swap publication after a complete
signed Owner approval, a stable remote observation, and a single-use nonce
claim. It neither owns trust material nor accepts direct reference strings.
"""
from __future__ import annotations
from dataclasses import dataclass
import re
from typing import Any, Mapping, Protocol
from aios_existing_branch_approval import (
OwnerApprovalEnvelope,
OwnerApprovalRejected,
OwnerApprovalVerifier,
PublicationApprovalBinding,
ReplayLedger,
canonical_json,
digest_json,
)
REQUEST_SCHEMA = "aios.existing-branch-publication.v1"
PUBLISH_OPERATION = "publishExistingBranchGoverned"
OID_RE = re.compile(r"^[0-9a-f]{40}$")
BRANCH_RE = re.compile(r"^agent/[A-Za-z0-9][A-Za-z0-9._/-]{2,179}$")
class ExistingBranchPublicationRejected(RuntimeError):
def __init__(self, code: str) -> None:
super().__init__(code)
self.code = code
class RemoteOidConflict(RuntimeError):
pass
class RemoteObservationAmbiguous(RuntimeError):
pass
@dataclass(frozen=True, slots=True)
class ExistingBranchPublicationRequest:
repository: str
branch: str
expected_branch_oid: str
idempotency_key: str
payload: Mapping[str, Any]
@classmethod
def from_mapping(cls, value: object) -> "ExistingBranchPublicationRequest":
if not isinstance(value, Mapping):
raise ExistingBranchPublicationRejected("PUBLICATION_REQUEST_REJECTED")
expected_fields = {
"schema",
"repository",
"branch",
"expected_branch_oid",
"operation",
"idempotency_key",
"payload",
}
if set(value) != expected_fields or value.get("schema") != REQUEST_SCHEMA:
raise ExistingBranchPublicationRejected("PUBLICATION_REQUEST_REJECTED")
repository = value["repository"]
branch = value["branch"]
expected_oid = value["expected_branch_oid"]
key = value["idempotency_key"]
payload = value["payload"]
if (
not isinstance(repository, str)
or repository != "aios/aios"
or not BRANCH_RE.fullmatch(str(branch))
or not isinstance(expected_oid, str)
or not OID_RE.fullmatch(expected_oid)
or value.get("operation") != PUBLISH_OPERATION
or not isinstance(key, str)
or not key.strip()
or not isinstance(payload, Mapping)
):
raise ExistingBranchPublicationRejected("PUBLICATION_REQUEST_REJECTED")
try:
canonical_json(payload)
except OwnerApprovalRejected as exc:
raise ExistingBranchPublicationRejected("PUBLICATION_PAYLOAD_REJECTED") from exc
return cls(repository, str(branch), expected_oid, key, dict(payload))
@property
def request_digest(self) -> str:
return digest_json(
{
"schema": REQUEST_SCHEMA,
"repository": self.repository,
"branch": self.branch,
"expected_branch_oid": self.expected_branch_oid,
"operation": PUBLISH_OPERATION,
"payload": self.payload,
}
)
@property
def idempotency_digest(self) -> str:
return digest_json(
{
"idempotency_key": self.idempotency_key,
"request_digest": self.request_digest,
}
)
@property
def approval_binding(self) -> PublicationApprovalBinding:
return PublicationApprovalBinding(
repository=self.repository,
branch=self.branch,
expected_branch_oid=self.expected_branch_oid,
operation=PUBLISH_OPERATION,
request_digest=self.request_digest,
idempotency_digest=self.idempotency_digest,
)
@dataclass(frozen=True, slots=True)
class RemoteBranchObservation:
state: str
git_oid: str | None
api_oid: str | None
expected_commit_visible: bool | None
@dataclass(frozen=True, slots=True)
class RemotePublicationEvidence:
authority_oid: str | None
gitea_git_oid: str | None
gitea_api_oid: str | None
commit_visible: bool | None
class ExistingBranchRemotePort(Protocol):
def observe(self, branch: str) -> RemoteBranchObservation: ...
def publish_compare_and_swap(
self, branch: str, expected_branch_oid: str, commit_oid: str
) -> RemotePublicationEvidence: ...
class AuthorityBranchPort(Protocol):
def read_branch_oid(self, branch: str) -> str | None: ...
class CredentialProjection(Protocol):
def project(self) -> None: ...
def revoke(self) -> None: ...
class PublicationWorkspace(Protocol):
def head_oid(self) -> str: ...
def commit_payload(self, payload: Mapping[str, Any]) -> str: ...
class DetachedWorkspaceFactory(Protocol):
def open_detached(self, expected_branch_oid: str) -> PublicationWorkspace: ...
class AuditSink(Protocol):
def record(self, event: Mapping[str, Any]) -> None: ...
class ExistingBranchPublisher:
"""Validate once, observe once, then make one bounded publication attempt."""
def __init__(
self,
*,
verifier: OwnerApprovalVerifier,
replay_ledger: ReplayLedger,
authority: AuthorityBranchPort,
remote: ExistingBranchRemotePort,
credentials: CredentialProjection,
workspaces: DetachedWorkspaceFactory,
audit: AuditSink,
) -> None:
self.verifier = verifier
self.replay_ledger = replay_ledger
self.authority = authority
self.remote = remote
self.credentials = credentials
self.workspaces = workspaces
self.audit = audit
def _audit(
self,
*,
status: str,
request: ExistingBranchPublicationRequest,
approval: OwnerApprovalEnvelope | None = None,
commit_oid: str | None = None,
) -> None:
event: dict[str, Any] = {
"event": "governed_existing_branch_publication",
"status": status,
"repository": request.repository,
"branch": request.branch,
"expected_branch_oid": request.expected_branch_oid,
"operation": PUBLISH_OPERATION,
"request_digest": request.request_digest,
"idempotency_digest": request.idempotency_digest,
}
if approval is not None:
event.update(
{
"approval_id": approval.approval_id,
"owner_identity": approval.owner_identity,
"trust_domain": approval.trust_domain,
"approval_fingerprint": approval.fingerprint,
}
)
if commit_oid is not None:
event["commit_oid"] = commit_oid
self.audit.record(event)
def _preflight_remote(
self, request: ExistingBranchPublicationRequest
) -> RemoteBranchObservation | dict[str, str]:
try:
authority_oid = self.authority.read_branch_oid(request.branch)
except Exception:
return {"status": "RECONCILIATION_REQUIRED"}
if authority_oid != request.expected_branch_oid:
return {"status": "MANUAL_OWNER_REVIEW_REQUIRED"}
try:
observation = self.remote.observe(request.branch)
except RemoteObservationAmbiguous:
return {"status": "RECONCILIATION_REQUIRED"}
if observation.state == "AMBIGUOUS":
return {"status": "RECONCILIATION_REQUIRED"}
if observation.state != "PRESENT":
return {"status": "MANUAL_OWNER_REVIEW_REQUIRED"}
if (
observation.git_oid is not None
and observation.api_oid is not None
and observation.git_oid != observation.api_oid
):
return {"status": "RECONCILIATION_REQUIRED"}
if (
observation.git_oid != request.expected_branch_oid
or observation.api_oid != request.expected_branch_oid
):
return {"status": "MANUAL_OWNER_REVIEW_REQUIRED"}
if observation.expected_commit_visible is not True:
return {"status": "RECONCILIATION_REQUIRED"}
return observation
def publish(
self, request_value: object, approval_value: object
) -> dict[str, str | bool]:
request = ExistingBranchPublicationRequest.from_mapping(request_value)
try:
approval = self.verifier.verify(approval_value, request.approval_binding)
except OwnerApprovalRejected:
self._audit(status="REJECTED", request=request)
raise
preflight = self._preflight_remote(request)
if isinstance(preflight, dict):
self._audit(status=preflight["status"], request=request, approval=approval)
return preflight
self.replay_ledger.consume(approval, request.approval_binding)
workspace = self.workspaces.open_detached(request.expected_branch_oid)
if workspace.head_oid() != request.expected_branch_oid:
self._audit(status="REJECTED", request=request, approval=approval)
raise ExistingBranchPublicationRejected("DETACHED_WORKSPACE_HEAD_REJECTED")
projected = False
try:
self.credentials.project()
projected = True
commit_oid = workspace.commit_payload(request.payload)
if not isinstance(commit_oid, str) or not OID_RE.fullmatch(commit_oid):
raise ExistingBranchPublicationRejected("PUBLICATION_COMMIT_REJECTED")
try:
evidence = self.remote.publish_compare_and_swap(
request.branch, request.expected_branch_oid, commit_oid
)
except RemoteOidConflict:
self._audit(
status="MANUAL_OWNER_REVIEW_REQUIRED",
request=request,
approval=approval,
commit_oid=commit_oid,
)
return {"status": "MANUAL_OWNER_REVIEW_REQUIRED"}
except RemoteObservationAmbiguous:
self._audit(
status="RECONCILIATION_REQUIRED",
request=request,
approval=approval,
commit_oid=commit_oid,
)
return {"status": "RECONCILIATION_REQUIRED"}
if (
evidence.authority_oid != commit_oid
or evidence.gitea_git_oid != commit_oid
or evidence.gitea_api_oid != commit_oid
or evidence.commit_visible is not True
):
self._audit(
status="RECONCILIATION_REQUIRED",
request=request,
approval=approval,
commit_oid=commit_oid,
)
return {"status": "RECONCILIATION_REQUIRED"}
self._audit(
status="PUBLISHED",
request=request,
approval=approval,
commit_oid=commit_oid,
)
return {
"status": "PUBLISHED",
"commit_oid": commit_oid,
"authority_oid": evidence.authority_oid,
"gitea_git_oid": evidence.gitea_git_oid,
"gitea_api_oid": evidence.gitea_api_oid,
"commit_visible": evidence.commit_visible,
}
finally:
if projected:
self.credentials.revoke()
@@ -27,6 +27,19 @@ class ServiceUnavailable(JobError):
pass
@dataclass(frozen=True, slots=True)
class LeaseRenewalResult:
"""Durable outcome of one writer-lease recovery attempt."""
renewed: bool
idempotent: bool
durable: bool
lease_expires_at: str | None
def __bool__(self) -> bool:
return self.renewed or self.idempotent
class BindingConflict(JobError):
pass
@@ -66,6 +79,14 @@ def utc_now() -> str:
return datetime.now(timezone.utc).isoformat().replace("+00:00", "Z")
def parse_utc(value: str) -> datetime:
normalized = value[:-1] + "+00:00" if value.endswith("Z") else value
parsed = datetime.fromisoformat(normalized)
if parsed.tzinfo is None:
raise ValueError("LEASE_EXPIRY_INVALID")
return parsed.astimezone(timezone.utc)
def canonical_json(value: Any) -> bytes:
return (
json.dumps(value, sort_keys=True, separators=(",", ":"), ensure_ascii=False) + "\n"
@@ -404,13 +425,16 @@ class GitOpsJobController:
self.store._mirror(result)
return self.store._receipt(result, duplicate=True)
def update_lease(self, lease_id: str, *, expected_revision: int, current_head_oid: str, generation: int, state: str = "ACTIVE") -> None:
def update_lease(self, lease_id: str, *, expected_revision: int, current_head_oid: str, generation: int, state: str = "ACTIVE", lease_expires_at: str | None = None) -> None:
if len(current_head_oid) != 40 or generation < 1:
raise JobError("LEASE_BINDING_INVALID")
try:
with self.store._connect() as connection:
connection.execute("BEGIN IMMEDIATE")
if lease_expires_at is None:
updated = connection.execute("UPDATE branch_leases SET current_head_oid=?,generation=?,state=?,revision=revision+1 WHERE lease_id=? AND revision=?", (current_head_oid, generation, state, lease_id, expected_revision))
else:
updated = connection.execute("UPDATE branch_leases SET current_head_oid=?,generation=?,lease_expires_at=?,state=?,revision=revision+1 WHERE lease_id=? AND revision=?", (current_head_oid, generation, lease_expires_at, state, lease_id, expected_revision))
if updated.rowcount != 1:
raise JobError("LEASE_REVISION_CAS_CONFLICT")
connection.commit()
@@ -419,6 +443,94 @@ class GitOpsJobController:
except sqlite3.Error as exc:
raise ServiceUnavailable("JOB_DB_UNAVAILABLE") from exc
def renew_lease(
self,
*,
branch: str,
lineage_anchor_oid: str,
current_head_oid: str,
generation: int,
lease_expires_at: str,
) -> LeaseRenewalResult:
if (
not branch
or len(lineage_anchor_oid) != 40
or len(current_head_oid) != 40
or generation < 1
or not lease_expires_at.strip()
):
raise JobError("LEASE_BINDING_INVALID")
try:
with self.store._connect() as connection:
connection.execute("BEGIN IMMEDIATE")
rows = connection.execute(
"SELECT lease_id, revision, generation, state, lease_expires_at "
"FROM branch_leases "
"WHERE branch=? AND lineage_anchor_oid=? AND current_head_oid=? "
"AND generation=? AND state='ACTIVE' ORDER BY revision DESC",
(branch, lineage_anchor_oid, current_head_oid, generation),
).fetchall()
if not rows:
connection.commit()
return LeaseRenewalResult(False, False, False, None)
if len(rows) != 1:
raise JobError("LEASE_BINDING_CONFLICT")
row = rows[0]
try:
current_expires_at = parse_utc(str(row["lease_expires_at"]))
requested_expires_at = parse_utc(lease_expires_at)
except (TypeError, ValueError) as exc:
raise JobError("LEASE_EXPIRY_INVALID") from exc
now = datetime.now(timezone.utc)
if requested_expires_at <= now:
raise JobError("LEASE_EXPIRY_INVALID")
if current_expires_at > now:
connection.commit()
return LeaseRenewalResult(
False,
True,
True,
str(row["lease_expires_at"]),
)
updated = connection.execute(
"UPDATE branch_leases SET lease_expires_at=?,revision=revision+1 "
"WHERE lease_id=? AND revision=? AND generation=? "
"AND state='ACTIVE' AND lease_expires_at=?",
(
lease_expires_at,
row["lease_id"],
int(row["revision"]),
generation,
row["lease_expires_at"],
),
)
if updated.rowcount != 1:
current = connection.execute(
"SELECT lease_expires_at, generation, state FROM branch_leases "
"WHERE lease_id=?",
(row["lease_id"],),
).fetchone()
if (
current is not None
and int(current["generation"]) == generation
and current["state"] == "ACTIVE"
and parse_utc(str(current["lease_expires_at"])) > now
):
connection.commit()
return LeaseRenewalResult(
False,
True,
True,
str(current["lease_expires_at"]),
)
raise JobError("LEASE_REVISION_CAS_CONFLICT")
connection.commit()
return LeaseRenewalResult(True, False, True, lease_expires_at)
except JobError:
raise
except sqlite3.Error as exc:
raise ServiceUnavailable("JOB_DB_UNAVAILABLE") from exc
def advance(self, job_id: str, state: JobState, *, side_effect_state: str, receipt: Mapping[str, Any] | None = None, expected_revision: int | None = None) -> JobReceipt:
return self._advance(
job_id,
@@ -964,6 +964,7 @@ def validate_writer_lease_request_v2(raw: Mapping[str, Any], probe: Any) -> dict
required = {
"schema",
"task_id",
"job_id",
"repository",
"branch_mode",
"base_oid",
@@ -976,6 +977,7 @@ def validate_writer_lease_request_v2(raw: Mapping[str, Any], probe: Any) -> dict
raise ProvisioningError("LEASE_REQUEST_V2_FIELDS_REJECTED", 409)
request = WriterLeaseRequestV2(
task_id=str(raw["task_id"]),
job_id=str(raw["job_id"]),
repository=str(raw["repository"]),
branch_mode=str(raw["branch_mode"]),
base_oid=str(raw["base_oid"]),
@@ -988,6 +990,7 @@ def validate_writer_lease_request_v2(raw: Mapping[str, Any], probe: Any) -> dict
return {
"schema": WriterLeaseRequestV2.schema,
"task_id": request.task_id,
"job_id": request.job_id,
"repository": request.repository,
"branch_mode": request.branch_mode,
"base_oid": request.base_oid,
+53 -23
View File
@@ -34,6 +34,7 @@ from .models import (
)
from .policy import PolicyEngine
from .store import TaskPlaneStore
from .scope import bind_task_scope
from .worker_registry import WorkerRegistry
@@ -68,28 +69,54 @@ class TaskPlaneService:
self.delegation = DelegationEngine(store)
self.store.probe_attestation_verifier = self.policy.probe_attestation_verifier
def create_task(self, body: dict[str, Any], idempotency_key: str | None = None) -> dict[str, Any]:
def _authorized_task(self, task_id: str, actor: str, action: str) -> TaskRecord:
task = self.store.get_task(task_id, self.policy.actor_context(actor))
self.policy.assert_allowed(action, actor, task=task)
return task
def create_task(
self,
body: dict[str, Any],
idempotency_key: str | None = None,
*,
actor_identity: str | None = None,
) -> dict[str, Any]:
if body.get("parent_task_id"):
raise ForbiddenError("child tasks must use the delegate endpoint")
issuer = body.get("issuer", {})
actor = issuer.get("id", "anon") if isinstance(issuer, dict) else "anon"
declared_actor = issuer.get("id", "anon") if isinstance(issuer, dict) else "anon"
actor = actor_identity or declared_actor
self.policy.assert_allowed("create", actor)
try:
plan = TaskPlan.from_dict(body)
except (TypeError, ValueError) as exc:
raise ValidationError("invalid task plan") from exc
if actor_identity is not None:
plan = bind_task_scope(
plan, self.policy.actor_context(actor_identity), public_create=True
)
task = self.store.create_task_with_event(plan, idempotency_key=idempotency_key)
return task.to_dict()
def list_tasks(self, actor: str | None = None) -> list[dict[str, Any]]:
context = None
if actor is not None:
self.policy.assert_allowed("read", actor)
return [task.to_dict(include_task=False) for task in self.store.list_tasks()]
context = self.policy.actor_context(actor)
return [
task.to_dict(include_task=False)
for task in self.store.list_tasks(context)
]
def get_task(self, task_id: str, actor: str | None = None) -> dict[str, Any]:
context = None
if actor is not None:
self.policy.assert_allowed("read", actor, task=self.store.get_task(task_id))
return self.store.get_task(task_id).to_dict()
self.policy.assert_allowed("read", actor)
context = self.policy.actor_context(actor)
task = self.store.get_task(task_id, context)
if actor is not None:
self.policy.assert_allowed("read", actor, task=task)
return task.to_dict()
def get_worker(self, worker_id: str, actor: str | None = None) -> dict[str, Any]:
if actor is not None:
@@ -163,9 +190,8 @@ class TaskPlaneService:
if not worker_id:
raise ValidationError("worker_id required")
task = self.store.get_task(task_id)
task = self._authorized_task(task_id, actor, "claim")
self.policy.assert_readonly_worker_binding(actor, str(worker_id))
self.policy.assert_allowed("claim", actor, task=task)
lease_seconds = _bounded_int(
body.get("lease_seconds", 30), "lease_seconds", 1, 3600
)
@@ -184,10 +210,9 @@ class TaskPlaneService:
if not claim_id:
raise ValidationError("claim_id required")
actor = body.get("actor_identity") or body.get("worker_identity") or "worker"
task = self.store.get_task(task_id)
task = self._authorized_task(task_id, actor, "heartbeat")
claim = self.store.get_claim(claim_id)
self.policy.assert_readonly_worker_binding(actor, claim.worker_id)
self.policy.assert_allowed("heartbeat", actor, task=task)
if claim.worker_identity != actor:
raise ForbiddenError("claim identity mismatch")
lease_seconds = _bounded_int(
@@ -206,10 +231,9 @@ class TaskPlaneService:
message = body.get("message")
if progress is None:
raise ValidationError("progress required")
task = self.store.get_task(task_id)
task = self._authorized_task(task_id, actor, "progress")
claim = self.store.get_claim(claim_id)
self.policy.assert_readonly_worker_binding(actor, claim.worker_id)
self.policy.assert_allowed("progress", actor, task=task)
if task.task_plan.data_classification == "credential-secret":
if message not in {None, ""} or contains_secret_text(stage):
raise ValidationError(
@@ -234,7 +258,7 @@ class TaskPlaneService:
if not claim_id:
raise ValidationError("claim_id required")
actor = body.get("actor_identity") or body.get("worker_identity") or "worker"
task = self.store.get_task(task_id)
task = self._authorized_task(task_id, actor, "result")
status = body.get("status", "COMPLETED")
outputs = body.get("outputs", [])
@@ -247,7 +271,6 @@ class TaskPlaneService:
raise ValidationError("evidence items must be objects")
claim = self.store.get_claim(claim_id)
self.policy.assert_readonly_worker_binding(actor, claim.worker_id)
self.policy.assert_allowed("result", actor, task=task)
if claim.worker_identity != actor:
raise ForbiddenError("claim identity mismatch")
if task.task_plan.data_classification == "credential-secret":
@@ -326,8 +349,7 @@ class TaskPlaneService:
reason = body.get("reason", "failed")
actor = body.get("actor_identity") or body.get("worker_identity") or "worker"
self.policy.assert_allowed("fail", actor)
task = self.store.get_task(task_id)
self.policy.assert_allowed("fail", actor, task=task)
task = self._authorized_task(task_id, actor, "fail")
claim = self.store.get_claim(claim_id)
if claim.worker_identity != actor:
raise ForbiddenError("claim identity mismatch")
@@ -339,8 +361,7 @@ class TaskPlaneService:
actor = body.get("actor_identity") or body.get("worker_identity") or "owner"
self.policy.assert_allowed("cancel", actor)
reason = body.get("reason", "cancelled")
task = self.store.get_task(task_id)
self.policy.assert_allowed("cancel", actor, task=task)
task = self._authorized_task(task_id, actor, "cancel")
updated = self.store.cancel_task_with_event(
task_id, task.revision, reason=reason
)
@@ -349,8 +370,7 @@ class TaskPlaneService:
def delegate(self, task_id: str, body: dict[str, Any]) -> dict[str, Any]:
actor = body.get("actor_identity") or "owner"
self.policy.assert_allowed("delegate", actor)
parent = self.store.get_task(task_id)
self.policy.assert_allowed("delegate", actor, task=parent)
parent = self._authorized_task(task_id, actor, "delegate")
if "task" in body:
try:
child_plan = TaskPlan.from_dict(body["task"])
@@ -364,24 +384,30 @@ class TaskPlaneService:
except (TypeError, ValueError) as exc:
raise ValidationError("invalid child task plan") from exc
child_plan.parent_task_id = task_id
child_plan = bind_task_scope(
child_plan, self.policy.actor_context(actor), public_create=False
)
child = self.delegation.delegate(task_id, child_plan, caller=actor)
return child.to_dict()
def get_events(self, task_id: str, actor: str | None = None) -> list[dict[str, Any]]:
if actor is not None:
self.policy.assert_allowed("audit", actor, task=self.store.get_task(task_id))
self._authorized_task(task_id, actor, "audit")
else:
self.store.get_task(task_id)
return self.store.list_events(task_id)
def get_evidence(self, task_id: str, actor: str | None = None) -> list[dict[str, Any]]:
if actor is not None:
self.policy.assert_allowed("audit", actor, task=self.store.get_task(task_id))
self._authorized_task(task_id, actor, "audit")
else:
self.store.get_task(task_id)
return self.evidence.list(task_id)
def get_children(self, task_id: str, actor: str | None = None) -> list[str]:
if actor is not None:
self.policy.assert_allowed("read", actor, task=self.store.get_task(task_id))
self._authorized_task(task_id, actor, "read")
else:
self.store.get_task(task_id)
return self.store.list_children(task_id)
@@ -506,7 +532,11 @@ class TaskPlaneRequestHandler(BaseHTTPRequestHandler):
):
raise ForbiddenError("issuer identity mismatch")
body["issuer"] = {"type": "authenticated", "id": identity}
return 201, self.service.create_task(body or {}, headers.get("idempotency-key"))
return 201, self.service.create_task(
body or {},
headers.get("idempotency-key"),
actor_identity=identity,
)
if body.get("actor_identity") not in {None, identity}:
raise ForbiddenError("actor identity mismatch")
body["actor_identity"] = identity
+27
View File
@@ -13,6 +13,8 @@ from dataclasses import dataclass, field
from datetime import datetime, timezone
from typing import Any
from .scope import normalize_task_scope
SENSITIVE_KEYS = {
"secret",
@@ -66,6 +68,12 @@ TASK_PLAN_KEYS = {
"allowed_input_paths",
"allowed_output_paths",
"output_policy",
"scope_type",
"tenant_id",
"user_id",
"project_id",
"conversation_id",
"run_id",
}
@@ -237,6 +245,12 @@ class TaskPlan:
schema_version: int = 1
task_id: str | None = None
task_type: str = "code.change"
scope_type: str = "legacy"
tenant_id: str | None = None
user_id: str | None = None
project_id: str | None = None
conversation_id: str | None = None
run_id: str | None = None
issuer: dict[str, Any] = field(default_factory=dict)
intent: str | None = None
required_capabilities: list[str] = field(default_factory=list)
@@ -311,10 +325,17 @@ class TaskPlan:
raise ValueError("credential-secret requires metadata-only logging")
if output_policy != "hash-count-status-only":
raise ValueError("credential-secret requires restricted output")
scope = normalize_task_scope(data)
return cls(
schema_version=schema_version,
task_id=validate_identifier(data["task_id"], "task_id"),
task_type=validate_type(data.get("task_type", "code.change"), "task_type"),
scope_type=scope.scope_type,
tenant_id=scope.tenant_id,
user_id=scope.user_id,
project_id=scope.project_id,
conversation_id=scope.conversation_id,
run_id=scope.run_id,
issuer=issuer,
intent=data.get("intent"),
required_capabilities=_normalize_list(data.get("required_capabilities", [])),
@@ -348,6 +369,12 @@ class TaskPlan:
"schema_version": self.schema_version,
"task_id": self.task_id,
"task_type": self.task_type,
"scope_type": self.scope_type,
"tenant_id": self.tenant_id,
"user_id": self.user_id,
"project_id": self.project_id,
"conversation_id": self.conversation_id,
"run_id": self.run_id,
"issuer": self.issuer,
"intent": self.intent,
"required_capabilities": list(self.required_capabilities),
+20 -1
View File
@@ -3,10 +3,11 @@
from __future__ import annotations
from dataclasses import dataclass
from typing import Callable
from typing import Any, Callable, Mapping
from .errors import ForbiddenError, LockedError
from .models import ProbeEvidence, ResultEnvelope, TaskRecord
from .scope import TaskActorContext, actor_context as build_actor_context
RISK_RANK = {"low": 1, "medium": 2, "high": 3}
@@ -93,6 +94,10 @@ class PolicyEngine:
result_signature_verifier: Callable[[ResultEnvelope], bool] | None = None,
probe_attestation_verifier: Callable[[ProbeEvidence], bool] | None = None,
identity_capabilities: dict[str, set[str]] | None = None,
identity_actor_contexts: Mapping[
str, TaskActorContext | Mapping[str, Any]
]
| None = None,
admin_identities: set[str] | None = None,
readonly_worker_execution_enabled: bool = False,
readonly_worker_profiles: list[ReadonlyWorkerProfile] | None = None,
@@ -109,12 +114,26 @@ class PolicyEngine:
identity: set(capabilities)
for identity, capabilities in (identity_capabilities or {}).items()
}
self.identity_actor_contexts = dict(identity_actor_contexts or {})
self.admin_identities = set(admin_identities or ())
self.readonly_worker_execution_enabled = readonly_worker_execution_enabled
self.readonly_worker_profiles = {
profile.identity: profile for profile in (readonly_worker_profiles or [])
}
def actor_context(self, identity: str) -> TaskActorContext:
configured = self.identity_actor_contexts.get(
identity, self.identity_actor_contexts.get("*")
)
capabilities = set(
self.identity_capabilities.get(
identity, self.identity_capabilities.get("*", set())
)
)
if identity in self.admin_identities:
capabilities.add("task.admin")
return build_actor_context(identity, configured, capabilities)
def _readonly_worker_allows(
self, action: str, actor: str, task: TaskRecord | None
) -> bool:
+181
View File
@@ -0,0 +1,181 @@
"""Trusted TaskPlane scope model and SQL authorization helpers."""
from __future__ import annotations
from dataclasses import dataclass, replace
from typing import Any, Mapping
from .errors import ForbiddenError, ValidationError
SCOPE_FIELDS = ("tenant_id", "user_id", "project_id", "conversation_id", "run_id")
PRIVILEGED_CAPABILITIES = frozenset(
{"*", "admin", "system.admin", "task.admin", "task.read.all"}
)
def _value(value: Any, field: str) -> Any:
if hasattr(value, "task_plan"):
value = value.task_plan
if isinstance(value, Mapping):
return value.get(field)
return getattr(value, field, None)
def _text(value: Any) -> str | None:
if value is None:
return None
result = str(value).strip()
return result or None
@dataclass(frozen=True)
class TaskScope:
scope_type: str
tenant_id: str | None = None
user_id: str | None = None
project_id: str | None = None
conversation_id: str | None = None
run_id: str | None = None
@dataclass(frozen=True)
class TaskActorContext:
identity: str
scope_type: str = "legacy"
tenant_id: str | None = None
user_id: str | None = None
project_id: str | None = None
conversation_id: str | None = None
run_id: str | None = None
capabilities: frozenset[str] = frozenset()
def normalize_task_scope(value: Any) -> TaskScope:
fields = {field: _text(_value(value, field)) for field in SCOPE_FIELDS}
declared = _text(_value(value, "scope_type"))
if declared is None:
declared = "tenant" if fields["tenant_id"] else "legacy"
if declared == "tenant":
if fields["tenant_id"] is None:
raise ValidationError(
"tenant scope requires tenant_id", code="TASK_SCOPE_TENANT_ID_REQUIRED"
)
elif declared == "legacy":
if any(fields.values()):
raise ValidationError(
"legacy scope cannot contain scoped ownership",
code="TASK_SCOPE_LEGACY_WITH_SCOPED_FIELDS",
)
elif declared == "system":
if any(fields[field] for field in ("tenant_id", "user_id", "project_id", "conversation_id")):
raise ValidationError(
"system scope cannot contain tenant ownership",
code="TASK_SCOPE_SYSTEM_WITH_TENANT_FIELDS",
)
else:
raise ValidationError("invalid task scope type", code="TASK_SCOPE_TYPE_INVALID")
return TaskScope(declared, **fields)
def actor_is_privileged(actor: TaskActorContext) -> bool:
return bool(actor.capabilities & PRIVILEGED_CAPABILITIES)
def actor_context(
identity: str,
configured: TaskActorContext | Mapping[str, Any] | None,
capabilities: set[str] | frozenset[str],
) -> TaskActorContext:
if isinstance(configured, TaskActorContext):
context = configured
else:
data = dict(configured or {})
data.pop("identity", None)
data.pop("capabilities", None)
context = TaskActorContext(identity=str(identity), **data)
scope = normalize_task_scope(context)
return replace(
context,
identity=str(identity),
scope_type=scope.scope_type,
tenant_id=scope.tenant_id,
user_id=scope.user_id,
project_id=scope.project_id,
conversation_id=scope.conversation_id,
run_id=scope.run_id,
capabilities=frozenset(str(item) for item in capabilities),
)
def bind_task_scope(plan: Any, actor: TaskActorContext, *, public_create: bool) -> Any:
requested = normalize_task_scope(plan)
if actor_is_privileged(actor):
trusted = normalize_task_scope(actor)
if requested.scope_type == "legacy" and trusted.scope_type != "legacy":
plan.scope_type = trusted.scope_type
for field in SCOPE_FIELDS:
setattr(plan, field, getattr(actor, field))
return plan
trusted = normalize_task_scope(actor)
if public_create and trusted.scope_type == "legacy":
raise ForbiddenError(
"authenticated identity has no tenant scope",
code="TASK_ACTOR_SCOPE_UNAVAILABLE",
)
if trusted.scope_type != "tenant":
if requested.scope_type == "legacy":
plan.scope_type = trusted.scope_type
for field in SCOPE_FIELDS:
setattr(plan, field, getattr(actor, field))
return plan
if requested.scope_type != trusted.scope_type:
raise ForbiddenError("task scope mismatch", code="TASK_SCOPE_MISMATCH_DENY")
return plan
if requested.scope_type not in {"legacy", "tenant"}:
raise ForbiddenError("task scope mismatch", code="TASK_SCOPE_MISMATCH_DENY")
updates: dict[str, Any] = {"scope_type": "tenant"}
for field in SCOPE_FIELDS:
declared = getattr(requested, field)
authoritative = getattr(actor, field)
if declared is not None and declared != authoritative:
raise ForbiddenError(
"task scope mismatch",
code="TASK_SCOPE_MISMATCH_DENY",
details={"field": field},
)
updates[field] = authoritative
for field, item in updates.items():
setattr(plan, field, item)
return plan
def actor_scope_sql(actor: TaskActorContext | None) -> tuple[str, tuple[Any, ...]]:
if actor is None or actor_is_privileged(actor):
return "", ()
scope = normalize_task_scope(actor)
if scope.scope_type == "legacy":
return "scope_type = 'legacy'", ()
if scope.scope_type == "system":
return "scope_type = 'system'", ()
clauses = ["scope_type = 'tenant'", "tenant_id = ?"]
params: list[Any] = [scope.tenant_id]
for field in ("user_id", "project_id", "conversation_id", "run_id"):
value = getattr(scope, field)
if value is None:
clauses.append(f"{field} IS NULL")
else:
clauses.append(f"({field} IS NULL OR {field} = ?)")
params.append(value)
return " AND ".join(clauses), tuple(params)
def task_scope_values(value: Any) -> tuple[str | None, ...]:
scope = normalize_task_scope(value)
return (
scope.scope_type,
scope.tenant_id,
scope.user_id,
scope.project_id,
scope.conversation_id,
scope.run_id,
)
+95 -11
View File
@@ -32,6 +32,7 @@ from .models import (
redact_value,
validate_identifier,
)
from .scope import TaskActorContext, actor_scope_sql, task_scope_values
EventEnvelope = tuple[str, str, str, str, str, float]
@@ -134,6 +135,12 @@ class TaskPlaneStore:
status TEXT NOT NULL,
task_json TEXT NOT NULL,
task_type TEXT NOT NULL,
scope_type TEXT NOT NULL DEFAULT 'legacy',
tenant_id TEXT,
user_id TEXT,
project_id TEXT,
conversation_id TEXT,
run_id TEXT,
root_task_id TEXT NOT NULL,
parent_task_id TEXT,
correlation_id TEXT,
@@ -272,6 +279,21 @@ class TaskPlaneStore:
)
"""
)
self._ensure_columns(
"tasks",
{
"scope_type": "TEXT NOT NULL DEFAULT 'legacy'",
"tenant_id": "TEXT",
"user_id": "TEXT",
"project_id": "TEXT",
"conversation_id": "TEXT",
"run_id": "TEXT",
},
)
self.conn.execute(
"UPDATE tasks SET scope_type='legacy' "
"WHERE scope_type IS NULL OR TRIM(scope_type)=''"
)
self._ensure_columns(
"workers",
{
@@ -299,6 +321,14 @@ class TaskPlaneStore:
)
self.conn.execute("CREATE INDEX IF NOT EXISTS ix_tasks_status ON tasks(status)")
self.conn.execute("CREATE INDEX IF NOT EXISTS ix_tasks_parent ON tasks(parent_task_id)")
self.conn.execute(
"CREATE INDEX IF NOT EXISTS ix_tasks_scope_tenant_project "
"ON tasks(scope_type, tenant_id, project_id)"
)
self.conn.execute(
"CREATE INDEX IF NOT EXISTS ix_tasks_scope_owner "
"ON tasks(scope_type, tenant_id, user_id, conversation_id, run_id)"
)
self.conn.execute("CREATE INDEX IF NOT EXISTS ix_workers_status ON workers(status)")
self.conn.execute("CREATE INDEX IF NOT EXISTS ix_claims_task ON claims(task_id)")
self.conn.execute(
@@ -347,6 +377,42 @@ class TaskPlaneStore:
raise NotFoundError("task")
task_json = json.loads(row["task_json"])
task_plan = TaskPlan.from_dict(task_json)
stored_scope = tuple(
row[field]
for field in (
"scope_type",
"tenant_id",
"user_id",
"project_id",
"conversation_id",
"run_id",
)
)
json_has_scope = any(
field in task_json
for field in (
"scope_type",
"tenant_id",
"user_id",
"project_id",
"conversation_id",
"run_id",
)
)
if json_has_scope and stored_scope != task_scope_values(task_plan):
raise ConflictError(
"task scope persistence mismatch",
code="TASK_SCOPE_PERSISTENCE_MISMATCH",
)
if not json_has_scope:
(
task_plan.scope_type,
task_plan.tenant_id,
task_plan.user_id,
task_plan.project_id,
task_plan.conversation_id,
task_plan.run_id,
) = stored_scope
task_plan.parent_task_id = row["parent_task_id"]
task_plan.correlation_id = row["correlation_id"]
return TaskRecord(
@@ -389,6 +455,7 @@ class TaskPlaneStore:
task_id = task_plan.task_id or f"task-{uuid.uuid4()}"
task_plan.task_id = task_id
task_json = redact_value(task_plan.to_dict(redact=False))
scope_values = task_scope_values(task_plan)
now = _now()
with self._transaction():
if idempotency_key:
@@ -397,7 +464,11 @@ class TaskPlaneStore:
(idempotency_key,),
).fetchone()
if existing:
if _canonical(json.loads(existing["task_json"])) != _canonical(task_json):
existing_plan = TaskPlan.from_dict(
json.loads(existing["task_json"])
)
existing_json = redact_value(existing_plan.to_dict(redact=False))
if _canonical(existing_json) != _canonical(task_json):
raise ConflictError("idempotency key payload mismatch")
return self.get_task(existing["task_id"])
root_task_id = task_id
@@ -412,14 +483,18 @@ class TaskPlaneStore:
self.conn.execute(
"""
INSERT INTO tasks
(task_id, revision, status, task_json, task_type, root_task_id, parent_task_id, correlation_id, idempotency_key, created_at, updated_at, failure_reason)
VALUES (?, 1, ?, ?, ?, ?, ?, ?, ?, ?, ?, NULL)
(task_id, revision, status, task_json, task_type,
scope_type, tenant_id, user_id, project_id, conversation_id, run_id,
root_task_id, parent_task_id, correlation_id, idempotency_key,
created_at, updated_at, failure_reason)
VALUES (?, 1, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, NULL)
""",
(
task_id,
"CREATED",
json.dumps(task_json, ensure_ascii=False),
task_plan.task_type,
*scope_values,
root_task_id,
task_plan.parent_task_id,
task_plan.correlation_id,
@@ -451,21 +526,30 @@ class TaskPlaneStore:
)
return self.get_task(task.task_id)
def get_task(self, task_id: str) -> TaskRecord:
def get_task(
self, task_id: str, actor_context: TaskActorContext | None = None
) -> TaskRecord:
task_id = _validated_id(task_id, "task_id")
predicate, scope_params = actor_scope_sql(actor_context)
sql = "SELECT * FROM tasks WHERE task_id=?"
if predicate:
sql += f" AND ({predicate})"
with self._lock:
row = self.conn.execute(
"SELECT * FROM tasks WHERE task_id=?", (task_id,)
).fetchone()
row = self.conn.execute(sql, (task_id, *scope_params)).fetchone()
if row is None:
raise NotFoundError(f"task {task_id} not found")
return self._row_to_task(row)
def list_tasks(self) -> list[TaskRecord]:
def list_tasks(
self, actor_context: TaskActorContext | None = None
) -> list[TaskRecord]:
predicate, scope_params = actor_scope_sql(actor_context)
sql = "SELECT * FROM tasks"
if predicate:
sql += f" WHERE ({predicate})"
sql += " ORDER BY created_at DESC"
with self._lock:
rows = self.conn.execute(
"SELECT * FROM tasks ORDER BY created_at DESC"
).fetchall()
rows = self.conn.execute(sql, scope_params).fetchall()
return [self._row_to_task(row) for row in rows]
def get_task_budget(self, task_id: str) -> Budget:
+223
View File
@@ -240,6 +240,114 @@ components:
additionalProperties: false
properties:
candidate_id: {type: string}
BridgeWriterLeaseRecoveryRequest:
type: object
additionalProperties: false
required: [schema, task_id, repository, branch_mode, base_oid, expected_branch_oid, target_branch, generation, lease_seconds]
properties:
schema:
type: string
enum: [aios.task-writer-lease.v2, aios.task-writer-lease-request.v2]
task_id: {type: string, minLength: 1, maxLength: 256}
job_id: {type: string, pattern: '^job-[A-Za-z0-9][A-Za-z0-9._-]{7,255}$'}
repository: {type: string, const: aios/aios}
branch_mode: {type: string, enum: [CREATE_NEW, CONTINUE_EXISTING]}
base_oid: {type: string, pattern: '^[0-9a-f]{40}$'}
expected_branch_oid: {type: string, pattern: '^[0-9a-f]{40}$'}
target_branch: {type: string, pattern: '^agent/[A-Za-z0-9][A-Za-z0-9._/-]{2,179}$'}
generation: {type: integer, minimum: 1}
lease_seconds: {type: integer, minimum: 1, maximum: 86400}
BridgeWriterLeaseResponse:
type: object
additionalProperties: false
required: [schema, task_id, job_id, repository, branch_mode, base_oid, expected_branch_oid, target_branch, generation, lease_seconds, create_target_branch, worktree_mode, cas_ref_update_required]
properties:
schema: {type: string, enum: [aios.task-writer-lease.v2]}
task_id: {type: string}
job_id: {type: string, pattern: '^job-[A-Za-z0-9][A-Za-z0-9._-]{7,255}$'}
repository: {type: string, const: aios/aios}
branch_mode: {type: string, enum: [CREATE_NEW, CONTINUE_EXISTING]}
base_oid: {type: string, pattern: '^[0-9a-f]{40}$'}
expected_branch_oid: {type: string, pattern: '^[0-9a-f]{40}$'}
target_branch: {type: string, pattern: '^agent/[A-Za-z0-9][A-Za-z0-9._/-]{2,179}$'}
generation: {type: integer, minimum: 1}
lease_seconds: {type: integer, minimum: 1, maximum: 86400}
create_target_branch: {type: boolean}
worktree_mode: {type: string, enum: [detached_at_base_oid, detached_at_expected_head]}
cas_ref_update_required: {type: boolean, const: true}
lease_expires_at: {type: string, format: date-time}
BridgeWriterLeaseRecoveryResponse:
type: object
additionalProperties: false
required: [schema, task_id, job_id, repository, branch_mode, base_oid, expected_branch_oid, target_branch, generation, lease_seconds, lease_expires_at, create_target_branch, worktree_mode, cas_ref_update_required, recovered, bootstrapped, idempotent, runtime_rehydrated, durable_branch_lease_present]
properties:
schema: {type: string, enum: [aios.task-writer-lease.v2]}
task_id: {type: string}
job_id: {type: string, pattern: '^job-[A-Za-z0-9][A-Za-z0-9._-]{7,255}$'}
repository: {type: string, const: aios/aios}
branch_mode: {type: string, enum: [CREATE_NEW, CONTINUE_EXISTING]}
base_oid: {type: string, pattern: '^[0-9a-f]{40}$'}
expected_branch_oid: {type: string, pattern: '^[0-9a-f]{40}$'}
target_branch: {type: string, pattern: '^agent/[A-Za-z0-9][A-Za-z0-9._/-]{2,179}$'}
generation: {type: integer, minimum: 1}
lease_seconds: {type: integer, minimum: 1, maximum: 86400}
lease_expires_at: {type: string, format: date-time}
create_target_branch: {type: boolean, const: false}
worktree_mode: {type: string, enum: [detached_at_base_oid, detached_at_expected_head]}
cas_ref_update_required: {type: boolean, const: true}
recovered: {type: boolean}
bootstrapped: {type: boolean}
idempotent: {type: boolean}
runtime_rehydrated: {type: boolean}
durable_branch_lease_present: {type: boolean}
BridgeCommitFileRequest:
type: object
additionalProperties: false
required: [task_id, repository, expected_branch_oid, target_branch, generation, idempotency_key, path, content_base64, message]
properties:
task_id: {type: string, minLength: 1}
job_id: {type: string, pattern: '^job-[A-Za-z0-9][A-Za-z0-9._-]{7,255}$'}
repository: {type: string, const: aios/aios}
expected_branch_oid: {type: string, pattern: '^[0-9a-f]{40}$'}
target_branch: {type: string, pattern: '^agent/[A-Za-z0-9][A-Za-z0-9._/-]{2,179}$'}
generation: {type: integer, minimum: 1}
idempotency_key: {type: string, minLength: 1, maxLength: 256}
path: {type: string, minLength: 1, maxLength: 512}
content_base64: {type: string, format: byte}
message: {type: string, minLength: 1, maxLength: 200}
BridgeCommitFilesRequest:
type: object
additionalProperties: false
required: [task_id, repository, expected_branch_oid, target_branch, generation, idempotency_key, files, message]
properties:
task_id: {type: string, minLength: 1}
job_id: {type: string, pattern: '^job-[A-Za-z0-9][A-Za-z0-9._-]{7,255}$'}
repository: {type: string, const: aios/aios}
expected_branch_oid: {type: string, pattern: '^[0-9a-f]{40}$'}
target_branch: {type: string, pattern: '^agent/[A-Za-z0-9][A-Za-z0-9._/-]{2,179}$'}
generation: {type: integer, minimum: 1}
idempotency_key: {type: string, minLength: 1, maxLength: 256}
files: {type: object, additionalProperties: {type: string, format: byte}, minProperties: 1}
message: {type: string, minLength: 1, maxLength: 200}
BridgeCommitResponse:
type: object
additionalProperties: true
required: [job_id, commit_oid, commit_visible, branch_mode, target_branch, generation]
properties:
job_id: {type: string, pattern: '^job-[A-Za-z0-9][A-Za-z0-9._-]{7,255}$'}
commit_oid: {type: string, pattern: '^[0-9a-f]{40}$'}
commit_visible: {type: boolean}
branch_mode: {type: string, enum: [CONTINUE_EXISTING]}
target_branch: {type: string, pattern: '^agent/[A-Za-z0-9][A-Za-z0-9._/-]{2,179}$'}
generation: {type: integer, minimum: 1}
BridgeGovernedCapabilities:
type: object
required: [bridge_schema_version, governed_writes_enabled, operations, operation_contracts]
properties:
bridge_schema_version: {type: string, const: aios.chatgpt-gitops-bridge.v2}
governed_writes_enabled: {type: boolean, const: true}
operations: {type: array, items: {type: string}}
operation_contracts: {type: object, additionalProperties: true}
paths:
/createReleaseIntent:
post:
@@ -567,3 +675,118 @@ paths:
'200': {description: Safe GitOps plan}
'401': {description: Unauthorized}
x-openai-isConsequential: true
/v2/capabilities:
get:
operationId: getGovernedWriteCapabilities
summary: Read the governed Bridge v2 capability contract
responses:
'200':
description: Governed operations and their request/response contracts
content:
application/json:
schema: {$ref: '#/components/schemas/BridgeGovernedCapabilities'}
/v2/writer-lease:
post:
operationId: acquireWriterLease
summary: Acquire the exact governed writer lease for a task generation
security: [{bearerAuth: []}]
requestBody:
required: true
content:
application/json:
schema: {$ref: '#/components/schemas/BridgeWriterLeaseRecoveryRequest'}
responses:
'200':
description: Exact detached worktree lease binding
content:
application/json:
schema: {$ref: '#/components/schemas/BridgeWriterLeaseResponse'}
'400': {description: Invalid lease binding}
'409': {description: Branch, base, or generation conflict}
x-openai-isConsequential: true
/v2/writer-lease/bootstrap:
post:
operationId: bootstrapWriterLease
summary: Bootstrap a governed writer runtime without creating a durable lease
description: >
Staging-only runtime recovery. It validates the exact task, repository,
base OID, expected branch OID, target branch, and generation. It must
not create a branch, insert a branch lease, or consume a generation.
security: [{bearerAuth: []}]
requestBody:
required: true
content:
application/json:
schema: {$ref: '#/components/schemas/BridgeWriterLeaseRecoveryRequest'}
responses:
'200':
description: Runtime binding bootstrapped or returned idempotently
content:
application/json:
schema: {$ref: '#/components/schemas/BridgeWriterLeaseRecoveryResponse'}
'400': {description: Invalid lease binding}
'409': {description: A matching durable lease already exists or binding conflicts}
'503': {description: Runtime or durable state unavailable}
x-openai-isConsequential: true
/v2/writer-lease/renew:
post:
operationId: renewWriterLease
summary: Recover an expired governed writer lease without changing its binding
description: >
Renews only the current task generation and exact repository, base OID,
expected branch OID, and target branch. Durable renewal is revision-CAS
protected; runtime rehydration is reported separately.
security: [{bearerAuth: []}]
requestBody:
required: true
content:
application/json:
schema: {$ref: '#/components/schemas/BridgeWriterLeaseRecoveryRequest'}
responses:
'200':
description: Lease recovered or idempotently observed as already active
content:
application/json:
schema: {$ref: '#/components/schemas/BridgeWriterLeaseRecoveryResponse'}
'400': {description: Invalid schema, generation, or lease duration}
'409': {description: Task, base, repository, branch, generation, or durable lease conflict}
'503': {description: Durable job store or runtime unavailable}
x-openai-isConsequential: true
/v2/commit-file:
post:
operationId: commitFileGoverned
summary: Commit one file through the governed writer lease
security: [{bearerAuth: []}]
requestBody:
required: true
content:
application/json:
schema: {$ref: '#/components/schemas/BridgeCommitFileRequest'}
responses:
'200':
description: Authority and remote visibility receipt
content:
application/json:
schema: {$ref: '#/components/schemas/BridgeCommitResponse'}
'409': {description: Lease, expected-head, idempotency, or repository conflict}
'503': {description: Reconciliation required}
x-openai-isConsequential: true
/v2/commit-files:
post:
operationId: commitFilesGoverned
summary: Atomically commit a bounded set of files through the governed writer lease
security: [{bearerAuth: []}]
requestBody:
required: true
content:
application/json:
schema: {$ref: '#/components/schemas/BridgeCommitFilesRequest'}
responses:
'200':
description: Authority and remote visibility receipt
content:
application/json:
schema: {$ref: '#/components/schemas/BridgeCommitResponse'}
'409': {description: Lease, expected-head, idempotency, or repository conflict}
'503': {description: Reconciliation required}
x-openai-isConsequential: true
@@ -0,0 +1,54 @@
# R4.2 P0-A TaskPlane Scope Isolation Design
Status: approved candidate design. This document is not evidence of a protected-main merge or production deployment.
## Boundary
- Task: `r42-p0a-taskplane-scope-isolation-20260816`
- Governed Job: `job-3fd743b51971c6007923699c1233f84c`
- Repository: `aios/aios`
- Base: `0eb6e1a864edfdaeef37b438991707a110e21558`
- Target branch: `agent/r42-p0a-taskplane-scope-isolation-20260816`
- Generation: `1`
The implementation reuses this exact boundary. It does not create a replacement Task, Job, branch, generation, or idempotency key.
## Model and invariants
`TaskPlan` gains first-class `scope_type`, `tenant_id`, `user_id`, `project_id`, `conversation_id`, and `run_id` fields. The fields participate in accepted-key validation, parsing, serialization, persistence, and authorization.
Historical rows without scope data normalize to `scope_type=legacy`; no tenant is inferred. Tenant-scoped tasks require a non-empty `tenant_id`. System/global resources use an explicit `scope_type=system` invariant and do not receive a fabricated tenant. A task may add user, project, conversation, and run ownership within its tenant.
## Trusted ActorContext
TaskPlane defines a small immutable `TaskActorContext` used only for authorization. The HTTP authenticator returns an identity. `PolicyEngine` resolves that identity through server-owned policy configuration into the effective context and capabilities. Request fields such as `tenant_id`, `user_id`, `group_id`, and `role` are declarations only and never expand authority.
Public create, list, and get paths always use the resolved context. Internal compatibility calls may omit a context and retain explicit legacy behavior, but the HTTP adapter never uses that path.
## Persistence
SQLite remains the current runtime. `_create_schema()` performs an idempotent forward migration that adds the six scope columns and indexes, then backfills only missing `scope_type` values to `legacy`.
New writes persist scope in both the canonical `task_json` representation and first-class columns. Reads accept historical JSON without scope, using legacy columns, but reject a new-row mismatch between JSON and columns. SQL list/get predicates are parameterized and applied before row hydration.
PostgreSQL remains a separate migration target and is not claimed by this change.
## Authorization flow
1. HTTP bearer authentication yields an identity.
2. `PolicyEngine` verifies capability and resolves the identity to `TaskActorContext`.
3. Create binds the requested scope to the trusted context and rejects mismatches.
4. List/get pass the trusted context to `TaskPlaneStore`.
5. Store applies tenant and ownership predicates in SQL; privileged capabilities may explicitly cross scope.
Tenant actors cannot read legacy tasks, another tenant, or mismatched owned resources. Legacy actors can only read legacy tasks. System actors require explicit privileged capability for cross-scope access.
## Failure behavior
Missing tenant scope, forged scope, unknown scope types, JSON/column divergence, and unauthorized list/get operations fail closed with canonical TaskPlane errors. Public get does not reveal whether an out-of-scope task exists.
## Verification
Tests exercise real model round trips, SQLite migration/restart, Service/Policy/Store wiring, tenant A versus tenant B list/get denial, ownership filtering, forged create payload rejection, legacy isolation, privileged access, and SQL parameterization. Existing TaskPlane API, RBAC, recovery/security, and security-contract suites remain mandatory.
The candidate is complete only after real `TASK_CLAIMED`, `TASK_HEARTBEAT`, exact WriterLease, governed commit, OID convergence, result submission, and `TASK_VERIFIED`. Protected main and production remain unchanged.
@@ -0,0 +1,9 @@
# R4.2 P0-A TaskPlane Scope Isolation
Status: candidate implementation. This is not proof of protected-main promotion or production deployment.
Task scope is now represented by `scope_type`, `tenant_id`, `user_id`, `project_id`, `conversation_id`, and `run_id` in the model and SQLite persistence. Historical rows become `legacy`; their tenant is never guessed. Tenant tasks require `tenant_id`. System resources use `scope_type=system` without a fabricated tenant.
The HTTP adapter uses the authenticated identity. `PolicyEngine` resolves that identity through server-owned `identity_actor_contexts`; request payload fields do not grant scope. Public create binds the trusted scope, and public list/get apply parameterized scope predicates in `TaskPlaneStore`.
SQLite is the current runtime database. PostgreSQL remains a separate migration and runtime-acceptance gate.
@@ -45,6 +45,9 @@ class ShadowClientRBACPublicInterfaceTests(unittest.TestCase):
identity_verifier=self.IdentityVerifier({"shadow-client"}),
execution_enabled=False,
identity_capabilities={"shadow-client": set(capabilities)},
identity_actor_contexts={
"shadow-client": {"scope_type": "system"}
},
)
service = self.TaskPlaneService(store, policy=policy)
@@ -0,0 +1,298 @@
from __future__ import annotations
import json
import os
import sqlite3
import sys
import threading
import urllib.error
import urllib.request
from http.server import ThreadingHTTPServer
import pytest
ROOT = os.getcwd()
if os.path.join(ROOT, "active-runtime") not in sys.path:
sys.path.append(os.path.join(ROOT, "active-runtime"))
from app.task_plane.api import TaskPlaneRequestHandler, TaskPlaneService
from app.task_plane.errors import ForbiddenError, NotFoundError, ValidationError
from app.task_plane.models import TaskPlan
from app.task_plane.policy import PolicyEngine
from app.task_plane.store import TaskPlaneStore
def _policy() -> PolicyEngine:
return PolicyEngine(
identity_capabilities={
"actor-a": {"task.creator", "task.read"},
"actor-b": {"task.creator", "task.read"},
"reader-a": {"task.read"},
"reader-b": {"task.read"},
"owner": {"task.creator", "task.read", "task.read.all"},
"unbound": {"task.creator", "task.read"},
},
identity_actor_contexts={
"actor-a": {
"scope_type": "tenant",
"tenant_id": "tenant-a",
"user_id": "user-a",
"project_id": "project-a",
"conversation_id": "conversation-a",
"run_id": "run-a",
},
"reader-a": {
"scope_type": "tenant",
"tenant_id": "tenant-a",
"user_id": "user-a",
"project_id": "project-a",
"conversation_id": "conversation-a",
"run_id": "run-a",
},
"actor-b": {
"scope_type": "tenant",
"tenant_id": "tenant-b",
"user_id": "user-b",
"project_id": "project-b",
},
"reader-b": {
"scope_type": "tenant",
"tenant_id": "tenant-b",
"user_id": "user-b",
"project_id": "project-b",
},
"owner": {"scope_type": "system"},
},
)
def _service() -> TaskPlaneService:
return TaskPlaneService(TaskPlaneStore(":memory:"), policy=_policy())
def test_taskplan_scope_fields_round_trip_as_first_class_values() -> None:
plan = TaskPlan.from_dict(
{
"task_id": "task-scope-roundtrip",
"scope_type": "tenant",
"tenant_id": "tenant-a",
"user_id": "user-a",
"project_id": "project-a",
"conversation_id": "conversation-a",
"run_id": "run-a",
}
)
payload = plan.to_dict()
assert TaskPlan.from_dict(payload).to_dict() == payload
def test_tenant_scope_requires_tenant_id_and_system_scope_does_not() -> None:
with pytest.raises(ValidationError) as exc:
TaskPlan.from_dict({"task_id": "task-missing-tenant", "scope_type": "tenant"})
assert exc.value.error_code == "TASK_SCOPE_TENANT_ID_REQUIRED"
system = TaskPlan.from_dict(
{"task_id": "task-system", "scope_type": "system", "run_id": "system-run"}
)
assert system.tenant_id is None
def test_ACTOR_BINDING_AUTHZ_MERGE_uses_server_scope() -> None:
service = _service()
created = service.create_task(
{"task_id": "task-a", "task_type": "code.change"},
actor_identity="actor-a",
)
assert created["plan"]["tenant_id"] == "tenant-a"
assert created["plan"]["user_id"] == "user-a"
assert created["plan"]["project_id"] == "project-a"
declared = service.create_task(
{
"task_id": "task-a-declared",
"tenant_id": "tenant-a",
"user_id": "user-a",
"project_id": "project-a",
"conversation_id": "conversation-a",
"run_id": "run-a",
},
actor_identity="actor-a",
)
assert declared["plan"]["scope_type"] == "tenant"
def test_TASK_SCOPE_MISMATCH_DENY_rejects_forged_client_scope() -> None:
service = _service()
with pytest.raises(ForbiddenError) as exc:
service.create_task(
{
"task_id": "task-forged",
"scope_type": "tenant",
"tenant_id": "tenant-b",
"user_id": "user-b",
"project_id": "project-b",
},
actor_identity="actor-a",
)
assert exc.value.error_code == "TASK_SCOPE_MISMATCH_DENY"
def test_public_create_fails_closed_without_server_actor_scope() -> None:
service = _service()
with pytest.raises(ForbiddenError) as exc:
service.create_task(
{"task_id": "task-unbound"}, actor_identity="unbound"
)
assert exc.value.error_code == "TASK_ACTOR_SCOPE_UNAVAILABLE"
def test_TASKPLANE_LIST_AUTHZ_DENY_and_TASK_OWNERSHIP_FILTER() -> None:
service = _service()
service.create_task({"task_id": "task-a"}, actor_identity="actor-a")
service.create_task({"task_id": "task-b"}, actor_identity="actor-b")
assert [item["task_id"] for item in service.list_tasks("reader-a")] == ["task-a"]
assert [item["task_id"] for item in service.list_tasks("reader-b")] == ["task-b"]
def test_TASKPLANE_GET_AUTHZ_DENY_does_not_disclose_cross_tenant_task() -> None:
service = _service()
service.create_task({"task_id": "task-a"}, actor_identity="actor-a")
with pytest.raises(NotFoundError):
service.get_task("task-a", "reader-b")
def test_http_authentication_drives_server_scope_and_cross_tenant_404() -> None:
service = _service()
class Handler(TaskPlaneRequestHandler):
pass
Handler.service = service
Handler.authenticator = lambda token: {
"token-a": "actor-a",
"token-b": "reader-b",
}.get(token)
server = ThreadingHTTPServer(("127.0.0.1", 0), Handler)
thread = threading.Thread(target=server.serve_forever, daemon=True)
thread.start()
base = f"http://127.0.0.1:{server.server_port}"
try:
request = urllib.request.Request(
base + "/tasks",
data=json.dumps({"task_id": "task-http-a"}).encode(),
headers={
"Authorization": "Bearer token-a",
"Content-Type": "application/json",
},
method="POST",
)
with urllib.request.urlopen(request, timeout=3) as response:
created = json.loads(response.read())
assert created["plan"]["tenant_id"] == "tenant-a"
denied = urllib.request.Request(
base + "/tasks/task-http-a",
headers={"Authorization": "Bearer token-b"},
)
with pytest.raises(urllib.error.HTTPError) as exc:
urllib.request.urlopen(denied, timeout=3)
assert exc.value.code == 404
finally:
server.shutdown()
server.server_close()
thread.join(timeout=3)
def test_mutating_endpoint_is_scoped_before_cancel() -> None:
policy = PolicyEngine(
execution_enabled=True,
identity_capabilities={
"actor-a": {"task.creator", "task.read"},
"actor-b": {"task.cancel"},
},
identity_actor_contexts={
"actor-a": {"scope_type": "tenant", "tenant_id": "tenant-a"},
"actor-b": {"scope_type": "tenant", "tenant_id": "tenant-b"},
},
)
service = TaskPlaneService(TaskPlaneStore(":memory:"), policy=policy)
service.create_task({"task_id": "task-a"}, actor_identity="actor-a")
with pytest.raises(NotFoundError):
service.cancel("task-a", {"actor_identity": "actor-b"})
def test_privileged_server_actor_can_cross_scope() -> None:
service = _service()
service.create_task({"task_id": "task-a"}, actor_identity="actor-a")
service.create_task({"task_id": "task-b"}, actor_identity="actor-b")
assert {item["task_id"] for item in service.list_tasks("owner")} == {
"task-a",
"task-b",
}
def test_scope_sql_parameters_do_not_interpolate_tenant_values() -> None:
service = _service()
service.create_task({"task_id": "task-a"}, actor_identity="actor-a")
policy = PolicyEngine(
identity_capabilities={"attacker": {"task.read"}},
identity_actor_contexts={
"attacker": {
"scope_type": "tenant",
"tenant_id": "tenant-a' OR 1=1 --",
}
},
)
attacker_service = TaskPlaneService(service.store, policy=policy)
assert attacker_service.list_tasks("attacker") == []
def test_existing_sqlite_rows_migrate_to_legacy_without_tenant_guess(tmp_path) -> None:
path = tmp_path / "legacy.sqlite3"
connection = sqlite3.connect(path)
connection.execute(
"""
CREATE TABLE tasks (
task_id TEXT PRIMARY KEY, revision INTEGER NOT NULL, status TEXT NOT NULL,
task_json TEXT NOT NULL, task_type TEXT NOT NULL, root_task_id TEXT NOT NULL,
parent_task_id TEXT, correlation_id TEXT, idempotency_key TEXT UNIQUE,
created_at REAL NOT NULL, updated_at REAL NOT NULL, failure_reason TEXT
)
"""
)
connection.execute(
"INSERT INTO tasks VALUES (?,?,?,?,?,?,?,?,?,?,?,?)",
(
"task-legacy",
1,
"CREATED",
json.dumps({"task_id": "task-legacy"}),
"code.change",
"task-legacy",
None,
None,
"legacy-key",
1.0,
1.0,
None,
),
)
connection.commit()
connection.close()
store = TaskPlaneStore(str(path))
task = store.get_task("task-legacy")
assert task.task_plan.scope_type == "legacy"
assert task.task_plan.tenant_id is None
replay = store.create_task(
TaskPlan.from_dict({"task_id": "task-legacy"}),
idempotency_key="legacy-key",
)
assert replay.task_id == "task-legacy"
assert tuple(
store.conn.execute(
"SELECT scope_type,tenant_id FROM tasks WHERE task_id='task-legacy'"
).fetchone()
) == ("legacy", None)
tenant_context = _policy().actor_context("reader-a")
with pytest.raises(NotFoundError):
store.get_task("task-legacy", tenant_context)
store.close()
+122 -1
View File
@@ -2,6 +2,7 @@ import base64
from pathlib import Path
from aios_authority_git_executor import AuthorityGitExecutor
from aios_chatgpt_bridge_v2 import BridgeV2Adapter, OPERATION_REQUIRED_FIELDS
from aios_chatgpt_bridge_v2_server import BridgeHandler, RuntimeConfig
from aios_continuation_executor import GiteaContinuationRemote, ServerSideCredentialProjection
from aios_continuation_lease import WriterLeaseRequestV2
@@ -26,6 +27,7 @@ class Probe:
def test_create_new_lease_is_exact_base_and_absent():
request = WriterLeaseRequestV2(
job_id="job-bridge-runtime-lineage-test",
task_id="TASK-P0-BRIDGE-V21-DURABLE-RECONCILIATION-20260809",
repository="aios/aios",
branch_mode="CREATE_NEW",
@@ -39,6 +41,7 @@ def test_create_new_lease_is_exact_base_and_absent():
def test_create_new_lease_rejects_existing_target():
request = WriterLeaseRequestV2(
job_id="job-bridge-runtime-lineage-test",
task_id="task",
repository="aios/aios",
branch_mode="CREATE_NEW",
@@ -75,7 +78,7 @@ def test_slash_branch_uses_paginated_collection():
def test_http_route_contract_is_complete():
expected = {
"/v2/runtime", "/v2/capabilities", "/v2/governed-task",
"/v2/writer-lease", "/v2/commit-file", "/v2/commit-files",
"/v2/writer-lease", "/v2/writer-lease/bootstrap", "/v2/writer-lease/renew", "/v2/commit-file", "/v2/commit-files",
"/v2/job", "/v2/job/resume", "/v2/job/reconcile",
"/v2/tag/approval-request",
"/v2/tag/publish",
@@ -88,3 +91,121 @@ def test_http_route_contract_is_complete():
from aios_chatgpt_bridge_v2_server import ROUTES
assert set(ROUTES) == expected
def test_renew_writer_lease_is_exposed_in_action_and_capability_contracts():
adapter = BridgeV2Adapter(runtime_status={})
capabilities = adapter.getGovernedWriteCapabilities()
contract = capabilities["operation_contracts"]["renewWriterLease"]
assert contract["method"] == "POST"
assert contract["path"] == "/v2/writer-lease/renew"
assert tuple(contract["required_fields"]) == OPERATION_REQUIRED_FIELDS["renewWriterLease"]
assert "runtime_rehydrated" in contract["response_fields"]
action_schema = Path("docs/gitops/openapi-chatgpt-actions.yaml").read_text(encoding="utf-8")
assert "/v2/writer-lease/renew:" in action_schema
assert "operationId: renewWriterLease" in action_schema
assert "BridgeWriterLeaseRecoveryRequest" in action_schema
assert "BridgeWriterLeaseRecoveryResponse" in action_schema
assert {
capabilities["operation_contracts"][operation]["path"]
for operation in (
"acquireWriterLease",
"renewWriterLease",
"commitFileGoverned",
"commitFilesGoverned",
)
} == {
"/v2/writer-lease",
"/v2/writer-lease/renew",
"/v2/commit-file",
"/v2/commit-files",
}
def test_bootstrap_writer_lease_is_exposed_in_action_and_capability_contracts():
adapter = BridgeV2Adapter(runtime_status={})
capabilities = adapter.getGovernedWriteCapabilities()
contract = capabilities["operation_contracts"]["bootstrapWriterLease"]
assert contract["method"] == "POST"
assert contract["path"] == "/v2/writer-lease/bootstrap"
assert tuple(contract["required_fields"]) == OPERATION_REQUIRED_FIELDS["bootstrapWriterLease"]
assert "bootstrapped" in contract["response_fields"]
assert "durable_branch_lease_present" in contract["response_fields"]
action_schema = Path("docs/gitops/openapi-chatgpt-actions.yaml").read_text(encoding="utf-8")
assert "/v2/writer-lease/bootstrap:" in action_schema
assert "operationId: bootstrapWriterLease" in action_schema
assert "bootstrapped: {type: boolean}" in action_schema
assert "durable_branch_lease_present: {type: boolean}" in action_schema
def test_http_allows_explicit_job_id_on_lease_and_commit_requests():
handler = BridgeHandler.__new__(BridgeHandler)
lease_body = {
"schema": WriterLeaseRequestV2.schema,
"task_id": "task",
"job_id": "job-explicit-runtime-lineage",
"repository": "aios/aios",
"branch_mode": "CONTINUE_EXISTING",
"base_oid": "a" * 40,
"expected_branch_oid": "a" * 40,
"target_branch": "agent/explicit-runtime-lineage",
"generation": 2,
"lease_seconds": 120,
}
handler._allowed("acquireWriterLease", lease_body)
handler._allowed(
"commitFilesGoverned",
{
"task_id": "task",
"job_id": "job-explicit-runtime-lineage",
"repository": "aios/aios",
"expected_branch_oid": "a" * 40,
"target_branch": "agent/explicit-runtime-lineage",
"generation": 2,
"idempotency_key": "commit-explicit-runtime-lineage",
"files": {"docs/x.txt": "eA=="},
"message": "test",
},
)
def test_http_allows_omitted_job_id_for_durable_server_resolution():
handler = BridgeHandler.__new__(BridgeHandler)
lease_body = {
"schema": WriterLeaseRequestV2.schema,
"task_id": "task",
"repository": "aios/aios",
"branch_mode": "CONTINUE_EXISTING",
"base_oid": "a" * 40,
"expected_branch_oid": "a" * 40,
"target_branch": "agent/implicit-runtime-lineage",
"generation": 2,
"lease_seconds": 120,
}
handler._allowed("acquireWriterLease", lease_body)
handler._allowed(
"commitFilesGoverned",
{
"task_id": "task",
"repository": "aios/aios",
"expected_branch_oid": "a" * 40,
"target_branch": "agent/implicit-runtime-lineage",
"generation": 2,
"idempotency_key": "commit-implicit-runtime-lineage",
"files": {"docs/x.txt": "eA=="},
"message": "test",
},
)
def test_capability_contracts_return_exact_job_id_lineage():
contracts = BridgeV2Adapter(runtime_status={}).getGovernedWriteCapabilities()["operation_contracts"]
for operation in ("acquireWriterLease", "bootstrapWriterLease", "renewWriterLease"):
assert "job_id" in contracts[operation]["response_fields"]
for operation in ("commitFileGoverned", "commitFilesGoverned"):
assert "job_id" in contracts[operation]["response_fields"]
@@ -0,0 +1,304 @@
from __future__ import annotations
from pathlib import Path
import re
import json
from types import SimpleNamespace
import pytest
import yaml
from aios_chatgpt_bridge_v2_server import GovernedRuntime
from aios_continuation_executor import ContinuationExecutor
from aios_continuation_lease import WriterLeaseRequestV2
from aios_gitops_job_controller import JobState
TASK = "continuation-normalization-test"
JOB = "job-continuation-normalization-test"
REPO = "aios/aios"
BASE = "a" * 40
BRANCH = "agent/continuation-normalization-test"
class Authority:
def __init__(self, head: str | None) -> None:
self.head = head
def read_ref(self, ref: str) -> str | None:
assert ref == "refs/heads/" + BRANCH
return self.head
def commit_exists(self, oid: str) -> bool:
return oid == BASE
def branch_exists(self, branch: str) -> bool:
assert branch == BRANCH
return self.head is not None
def branch_head(self, branch: str) -> str:
assert branch == BRANCH and self.head is not None
return self.head
def is_ancestor(self, older: str, newer: str) -> bool:
return older == BASE and newer == BASE
def lease(**overrides: object) -> WriterLeaseRequestV2:
values = {
"task_id": TASK,
"job_id": JOB,
"repository": REPO,
"branch_mode": "CREATE_NEW",
"base_oid": BASE,
"expected_branch_oid": BASE,
"target_branch": BRANCH,
"generation": 1,
"lease_seconds": 120,
}
values.update(overrides)
return WriterLeaseRequestV2(**values)
def runtime(*, head: str | None = BASE, **binding_overrides: object) -> GovernedRuntime:
value = GovernedRuntime.__new__(GovernedRuntime)
value.authority = Authority(head)
binding = {
"task_id": TASK,
"job_id": JOB,
"repository": REPO,
"base_oid": BASE,
"expected_branch_oid": BASE,
"target_branch": BRANCH,
"generation": 1,
"branch_mode": "CREATE_NEW",
}
binding.update(binding_overrides)
value.bindings = {TASK: binding}
return value
def test_same_lineage_create_new_normalizes_to_continue_existing() -> None:
normalized = runtime()._normalize_runtime_lease(lease())
assert normalized.branch_mode == "CONTINUE_EXISTING"
assert normalized.task_id == TASK
assert normalized.job_id == JOB
assert normalized.repository == REPO
assert normalized.target_branch == BRANCH
assert normalized.expected_branch_oid == BASE
assert normalized.generation == 1
def test_unprovisioned_create_new_stays_create_new() -> None:
assert runtime(head=None)._normalize_runtime_lease(lease()).branch_mode == "CREATE_NEW"
@pytest.mark.parametrize(
("field", "value"),
(
("repository", "other/repo"),
("base_oid", "b" * 40),
("expected_branch_oid", "b" * 40),
("target_branch", "agent/foreign"),
("generation", 2),
),
)
def test_binding_mismatch_fails_closed(field: str, value: object) -> None:
with pytest.raises(RuntimeError, match="LEASE_BINDING_CONFLICT"):
runtime(**{field: value})._normalize_runtime_lease(lease())
def test_drifted_existing_head_fails_closed() -> None:
with pytest.raises(RuntimeError, match="EXPECTED_BRANCH_OID_CONFLICT"):
runtime(head="b" * 40)._normalize_runtime_lease(lease())
def test_openapi_contract_parses_and_requires_exact_commit_lineage() -> None:
document = yaml.safe_load(
Path("docs/gitops/openapi-chatgpt-actions.yaml").read_text(encoding="utf-8")
)
schema = document["components"]["schemas"]["BridgeCommitResponse"]
assert {
"job_id",
"commit_oid",
"commit_visible",
"branch_mode",
"target_branch",
"generation",
} <= set(schema["required"])
def test_changed_production_files_pass_secret_scan() -> None:
paths = [
Path("active-runtime/app/aios_authority_git_executor.py"),
Path("active-runtime/app/aios_chatgpt_bridge_v2_server.py"),
Path("active-runtime/app/aios_continuation_executor.py"),
Path("active-runtime/app/aios_continuation_lease.py"),
Path("active-runtime/app/authorized_task_writer_provisioning.py"),
Path("active-runtime/app/aios_chatgpt_bridge_v2.py"),
Path("docs/gitops/openapi-chatgpt-actions.yaml"),
]
expression = re.compile(
r"(?i)(BEGIN (RSA|OPENSSH|EC) PRIVATE KEY|"
r"authorization:\\s*bearer\\s+[A-Za-z0-9._-]{12,}|"
r"api[_-]?key\\s*[:=]\\s*['\\\"][^'\\\"]{12,})"
)
hits = []
for path in paths:
for number, line in enumerate(
path.read_text(encoding="utf-8", errors="replace").splitlines(), 1
):
if expression.search(line):
hits.append(f"{path}:{number}")
assert hits == []
def test_remote_verified_job_is_not_completed_when_head_cas_callback_fails() -> None:
class Store:
def read(self, _job_id):
return {
"branch": BRANCH,
"request_json": json.dumps(
{"binding": {"expected_branch_oid": BASE}}
),
}
class Controller:
def __init__(self):
self.store = Store()
self.states = []
def advance(self, _job_id, state, **_kwargs):
self.states.append(state)
return SimpleNamespace(as_dict=lambda: {"state": state.value})
executor = ContinuationExecutor.__new__(ContinuationExecutor)
executor.controller = Controller()
executor._observe_remote = lambda _branch, oid: (oid, oid, True)
def fail_cas(_oid):
raise RuntimeError("LEASE_CAS_CONFLICT")
with pytest.raises(RuntimeError, match="LEASE_CAS_CONFLICT"):
executor._finish_remote_verification(
JOB,
"b" * 40,
"b" * 40,
before_complete=fail_cas,
)
assert executor.controller.states == [JobState.REMOTE_VERIFIED]
@pytest.mark.parametrize("branch_mode", ("CREATE_NEW", "CONTINUE_EXISTING"))
def test_executor_preflight_accepts_the_exact_mode_dependent_normalized_shape(
branch_mode: str,
) -> None:
from datetime import datetime, timedelta, timezone
from aios_continuation_executor import (
ContinuationPermissions,
ContinuationRuntimeBinding,
)
request = lease(branch_mode=branch_mode)
now = datetime.now(timezone.utc)
executor = ContinuationExecutor.__new__(ContinuationExecutor)
executor.authority = Authority(None if branch_mode == "CREATE_NEW" else BASE)
executor.now = lambda: now
executor._max_generation = lambda _branch, *, exclude_job_id=None: 0
executor.workspace = SimpleNamespace(
is_detached=lambda: True,
head_oid=lambda: BASE,
)
executor.remote = SimpleNamespace(repository_identity=lambda: REPO)
binding = ContinuationRuntimeBinding(
request=request,
job_id=JOB,
workspace_path=Path("/tmp/continuation-normalization-test"),
lease_not_before=now - timedelta(seconds=1),
lease_expires_at=now + timedelta(seconds=60),
original_receipt_valid=True,
original_grant_consumed=True,
permissions=ContinuationPermissions(),
)
executor._validate_preflight(binding, job_id=JOB)
def test_create_new_executor_resumes_executing_and_uses_absence_bound_writers() -> None:
from datetime import datetime, timedelta, timezone
from aios_continuation_executor import (
ContinuationCommit,
ContinuationPermissions,
ContinuationRuntimeBinding,
)
new_oid = "b" * 40
class Store:
def read(self, _job_id: str) -> dict[str, object]:
return {"state": "EXECUTING", "revision": 4}
class Controller:
def __init__(self) -> None:
self.store = Store()
self.advanced: list[str] = []
def advance(self, _job_id: str, state: JobState, **_kwargs: object) -> object:
self.advanced.append(state.value)
return SimpleNamespace(as_dict=lambda: {"state": state.value})
class Workspace:
def commit_files(self, _files: object, _message: str) -> str:
return new_oid
class CreateAuthority:
def __init__(self) -> None:
self.created: list[tuple[str, str, str]] = []
def is_ancestor(self, older: str, newer: str) -> bool:
return older == BASE and newer == new_oid
def create_task_branch_cas(self, branch: str, base: str, new: str) -> str:
self.created.append((branch, base, new))
return new
def compare_and_swap_branch(self, *_args: object) -> str:
raise AssertionError("existing-branch CAS must not serve CREATE_NEW")
class CreateRemote:
def __init__(self) -> None:
self.created: list[tuple[str, str]] = []
def push_new_exact(self, branch: str, new: str) -> None:
self.created.append((branch, new))
def push_exact(self, *_args: object) -> None:
raise AssertionError("existing-branch push must not serve CREATE_NEW")
class Credential:
def project(self) -> None: pass
def revoke(self) -> None: pass
controller=Controller(); authority=CreateAuthority(); remote=CreateRemote()
executor=ContinuationExecutor(controller,authority,Workspace(),remote,Credential())
executor._validate_preflight=lambda *_args,**_kwargs: None
executor._finish_remote_verification=lambda *_args,**_kwargs: {
"job_id":JOB,"state":"COMPLETED","receipt":{"authority_oid":new_oid,"commit_visible":True}
}
now=datetime.now(timezone.utc)
binding=ContinuationRuntimeBinding(
request=lease(branch_mode="CREATE_NEW"),job_id=JOB,
workspace_path=Path("/tmp/create-new-executor-resume"),
lease_not_before=now-timedelta(seconds=1),lease_expires_at=now+timedelta(seconds=60),
original_receipt_valid=True,original_grant_consumed=True,
permissions=ContinuationPermissions(),
)
result=executor.execute(
binding,ContinuationCommit("create-new-test",{"docs/test.txt":b"ok"},"test")
)
assert result["state"]=="COMPLETED"
assert authority.created==[(BRANCH,BASE,new_oid)]
assert remote.created==[(BRANCH,new_oid)]
assert controller.advanced==["LOCAL_VERIFIED","REMOTE_SYNCING"]
@@ -1,285 +0,0 @@
from __future__ import annotations
import base64
from datetime import datetime, timedelta, timezone
import json
from cryptography.hazmat.primitives import serialization
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey
import pytest
from aios_existing_branch_approval import (
DurableNonceReplayLedger,
OwnerApprovalEnvelope,
OwnerApprovalRejected,
OwnerApprovalVerifier,
TrustRootEvidence,
)
from aios_existing_branch_publisher import (
ExistingBranchPublicationRejected,
ExistingBranchPublisher,
RemoteBranchObservation,
RemotePublicationEvidence,
)
BASE_OID = "a" * 40
COMMIT_OID = "b" * 40
BRANCH = "agent/owner-approval-contract-20260810"
class TrustRoot:
def __init__(self, public_key: bytes, *, revocation: str = "ACTIVE") -> None:
self.public_key = public_key
self.revocation = revocation
self.calls: list[str] = []
def resolve(self, *, key_id: str, owner_identity: str, trust_domain: str):
self.calls.append("trust-root")
return TrustRootEvidence(
key_id=key_id,
owner_identity=owner_identity,
trust_domain=trust_domain,
public_key=self.public_key,
authority_confirmed=True,
revocation_state=self.revocation,
)
class Credentials:
def __init__(self, events: list[str]) -> None:
self.events = events
def project(self) -> None:
self.events.append("credential-project")
def revoke(self) -> None:
self.events.append("credential-revoke")
class Workspace:
def __init__(self, events: list[str]) -> None:
self.events = events
def head_oid(self) -> str:
self.events.append("workspace-head")
return BASE_OID
def commit_payload(self, payload):
self.events.append("workspace-commit")
return COMMIT_OID
class Workspaces:
def __init__(self, events: list[str]) -> None:
self.events = events
def open_detached(self, expected_branch_oid: str) -> Workspace:
assert expected_branch_oid == BASE_OID
self.events.append("workspace-open")
return Workspace(self.events)
class Remote:
def __init__(self, events: list[str]) -> None:
self.events = events
self.observation = RemoteBranchObservation("PRESENT", BASE_OID, BASE_OID, True)
self.authority_oid = BASE_OID
self.pushes: list[tuple[str, str, str]] = []
def read_branch_oid(self, branch: str) -> str:
self.events.append("authority-observe")
assert branch == BRANCH
return self.authority_oid
def observe(self, branch: str) -> RemoteBranchObservation:
self.events.append("remote-observe")
assert branch == BRANCH
return self.observation
def publish_compare_and_swap(self, branch: str, expected: str, commit: str):
self.events.append("remote-publish")
self.pushes.append((branch, expected, commit))
return RemotePublicationEvidence(COMMIT_OID, COMMIT_OID, COMMIT_OID, True)
class Audit:
def __init__(self) -> None:
self.rows: list[dict] = []
def record(self, event) -> None:
self.rows.append(dict(event))
def request(*, payload=None, branch=BRANCH, expected_oid=BASE_OID):
return {
"schema": "aios.existing-branch-publication.v1",
"repository": "aios/aios",
"branch": branch,
"expected_branch_oid": expected_oid,
"operation": "publishExistingBranchGoverned",
"idempotency_key": "owner-approval-binding-20260810",
"payload": {"files": ["active-runtime/app/example.py"]} if payload is None else payload,
}
def signed_approval(private: Ed25519PrivateKey, request_value, **replacements):
from aios_existing_branch_publisher import ExistingBranchPublicationRequest
parsed = ExistingBranchPublicationRequest.from_mapping(request_value)
now = datetime.now(timezone.utc)
raw = {
"schema": "aios.existing-branch-owner-approval.v1",
"approval_id": "approval-owner-binding-20260810",
"repository": parsed.repository,
"branch": parsed.branch,
"expected_branch_oid": parsed.expected_branch_oid,
"operation": "publishExistingBranchGoverned",
"issued_at": (now - timedelta(seconds=30)).isoformat(),
"not_before": (now - timedelta(seconds=1)).isoformat(),
"expires_at": (now + timedelta(minutes=5)).isoformat(),
"nonce": "nonce-owner-binding-20260810",
"owner_identity": "owner.aios",
"trust_domain": "aios-owner-root",
"key_id": "owner-key-20260810",
"request_digest": parsed.request_digest,
"idempotency_digest": parsed.idempotency_digest,
"signature": "AA==",
}
raw.update(replacements)
envelope = OwnerApprovalEnvelope.from_mapping(raw)
raw["signature"] = base64.b64encode(private.sign(envelope.canonical_bytes())).decode("ascii")
return raw
def service(tmp_path, *, revocation="ACTIVE"):
private = Ed25519PrivateKey.generate()
public_key = private.public_key().public_bytes(
serialization.Encoding.Raw, serialization.PublicFormat.Raw
)
root = TrustRoot(public_key, revocation=revocation)
events: list[str] = []
remote = Remote(events)
audit = Audit()
publisher = ExistingBranchPublisher(
verifier=OwnerApprovalVerifier(root, allowed_trust_domains={"aios-owner-root"}),
replay_ledger=DurableNonceReplayLedger(tmp_path / "replay"),
authority=remote,
remote=remote,
credentials=Credentials(events),
workspaces=Workspaces(events),
audit=audit,
)
return private, root, events, remote, audit, publisher
def test_valid_signed_owner_approval_is_verified_before_capability_projection(tmp_path):
private, root, events, remote, audit, publisher = service(tmp_path)
value = request()
approval = signed_approval(private, value)
result = publisher.publish(value, approval)
assert result["status"] == "PUBLISHED"
assert result["commit_oid"] == COMMIT_OID
assert remote.pushes == [(BRANCH, BASE_OID, COMMIT_OID)]
assert events == [
"authority-observe",
"remote-observe",
"workspace-open",
"workspace-head",
"credential-project",
"workspace-commit",
"remote-publish",
"credential-revoke",
]
assert root.calls == ["trust-root"]
assert approval["signature"] not in json.dumps(audit.rows)
assert "signature" not in audit.rows[-1]
@pytest.mark.parametrize(
"case",
[
"invalid_signature",
"expired",
"revoked",
"missing_key_evidence",
"missing_revocation_evidence",
"wrong_repository",
"wrong_branch",
"wrong_oid",
"wrong_operation",
"arbitrary_reference",
],
)
def test_invalid_owner_approval_inputs_are_rejected_without_publication(tmp_path, case):
private, root, events, remote, _, publisher = service(
tmp_path, revocation="REVOKED" if case == "revoked" else "ACTIVE"
)
value = request()
approval = signed_approval(private, value)
if case == "invalid_signature":
approval["signature"] = "AAAA"
elif case == "expired":
approval = signed_approval(
private,
value,
issued_at="2000-01-01T00:00:00+00:00",
not_before="2000-01-01T00:00:01+00:00",
expires_at="2000-01-01T00:01:00+00:00",
)
elif case == "wrong_repository":
approval = signed_approval(private, value, repository="other/repository")
elif case == "wrong_branch":
approval = signed_approval(private, value, branch="agent/another-approved-branch")
elif case == "wrong_oid":
approval = signed_approval(private, value, expected_branch_oid="c" * 40)
elif case == "wrong_operation":
approval["operation"] = "differentOperation"
elif case == "arbitrary_reference":
approval = "owner-approval-reference-123"
elif case == "missing_key_evidence":
root.public_key = None
elif case == "missing_revocation_evidence":
root.revocation = "UNKNOWN"
with pytest.raises((OwnerApprovalRejected, ExistingBranchPublicationRejected)):
publisher.publish(value, approval)
assert remote.pushes == []
assert "credential-project" not in events
assert "workspace-open" not in events
@pytest.mark.parametrize(
"changed",
[
lambda value: {**value, "branch": "agent/approval-replay-other-branch"},
lambda value: {**value, "expected_branch_oid": "c" * 40},
lambda value: {**value, "payload": {"files": ["other.py"]}},
],
)
def test_signed_approval_cannot_be_replayed_for_another_branch_oid_or_payload(tmp_path, changed):
private, _, events, remote, _, publisher = service(tmp_path)
original = request()
approval = signed_approval(private, original)
with pytest.raises(OwnerApprovalRejected, match="OWNER_APPROVAL_BINDING_REJECTED"):
publisher.publish(changed(original), approval)
assert remote.pushes == []
assert events == []
def test_signed_approval_nonce_is_single_use(tmp_path):
private, _, events, remote, _, publisher = service(tmp_path)
value = request()
approval = signed_approval(private, value)
assert publisher.publish(value, approval)["status"] == "PUBLISHED"
with pytest.raises(OwnerApprovalRejected, match="OWNER_APPROVAL_REPLAY_REJECTED"):
publisher.publish(value, approval)
assert len(remote.pushes) == 1
assert events.count("credential-project") == 1
@@ -1,72 +0,0 @@
from __future__ import annotations
import inspect
from aios_existing_branch_publisher import (
ExistingBranchPublisher,
RemoteBranchObservation,
)
from test_existing_branch_owner_approval import (
BASE_OID,
COMMIT_OID,
request,
service,
signed_approval,
)
def test_remote_oid_conflict_requires_manual_owner_review_before_capability_projection(tmp_path):
private, _, events, remote, _, publisher = service(tmp_path)
remote.observation = RemoteBranchObservation("PRESENT", "c" * 40, "c" * 40, True)
value = request()
result = publisher.publish(value, signed_approval(private, value))
assert result == {"status": "MANUAL_OWNER_REVIEW_REQUIRED"}
assert events == ["authority-observe", "remote-observe"]
assert remote.pushes == []
def test_ambiguous_remote_requires_reconciliation_after_one_observation(tmp_path):
private, _, events, remote, _, publisher = service(tmp_path)
remote.observation = RemoteBranchObservation("AMBIGUOUS", None, None, None)
value = request()
result = publisher.publish(value, signed_approval(private, value))
assert result == {"status": "RECONCILIATION_REQUIRED"}
assert events == ["authority-observe", "remote-observe"]
assert remote.pushes == []
def test_authority_oid_conflict_requires_manual_owner_review(tmp_path):
private, _, events, remote, _, publisher = service(tmp_path)
remote.authority_oid = "c" * 40
value = request()
result = publisher.publish(value, signed_approval(private, value))
assert result == {"status": "MANUAL_OWNER_REVIEW_REQUIRED"}
assert events == ["authority-observe"]
assert remote.pushes == []
def test_success_reports_all_remote_agreement_fields(tmp_path):
private, _, _, _, _, publisher = service(tmp_path)
value = request()
result = publisher.publish(value, signed_approval(private, value))
assert result == {
"status": "PUBLISHED",
"commit_oid": COMMIT_OID,
"authority_oid": COMMIT_OID,
"gitea_git_oid": COMMIT_OID,
"gitea_api_oid": COMMIT_OID,
"commit_visible": True,
}
def test_publisher_exposes_only_compare_and_swap_publication_path():
source = inspect.getsource(ExistingBranchPublisher).lower()
assert "force" not in source
+763
View File
@@ -0,0 +1,763 @@
from __future__ import annotations
from concurrent.futures import ThreadPoolExecutor
from datetime import datetime, timedelta, timezone
import hashlib
from dataclasses import asdict
from pathlib import Path
import sqlite3
import json
from types import SimpleNamespace
import threading
import pytest
from aios_chatgpt_bridge_v2_server import GovernedRuntime
from aios_continuation_executor import ContinuationPermissions, ContinuationRuntimeBinding
from aios_continuation_lease import WriterLeaseRequestV2
from aios_gitops_job_controller import GitOpsJobController, JobError, JobStore
from aios_governed_task_bootstrap import GovernedTaskBootstrap
TASK_ID = "gov-writer-lease-recovery-test"
BRANCH = "agent/writer-lease-recovery-test"
BASE = "a" * 40
JOB_ID_PREFIX = "writer-lease-runtime-lineage"
class LocalAuthority:
def __init__(self, head: str | None = None) -> None:
self.head = head
def read_ref(self, _ref: str) -> str | None:
return self.head
def commit_exists(self, _oid: str) -> bool:
return True
def ancestry_check(self, _older: str, _newer: str) -> bool:
return True
def writer_request(task_id: str = TASK_ID, **overrides: object) -> dict[str, object]:
request: dict[str, object] = {
"schema": WriterLeaseRequestV2.schema,
"task_id": task_id,
"repository": "aios/aios",
"branch_mode": "CREATE_NEW",
"base_oid": BASE,
"expected_branch_oid": BASE,
"target_branch": BRANCH,
"generation": 1,
"lease_seconds": 120,
}
request.update(overrides)
return request
def make_runtime(tmp_path: Path, task_id: str = TASK_ID, *, authority_head: str | None = None) -> GovernedRuntime:
runtime = GovernedRuntime.__new__(GovernedRuntime)
runtime.config = SimpleNamespace(repository="aios/aios", state_root=tmp_path)
runtime.authority = LocalAuthority(authority_head)
runtime.bindings = {
task_id: {
"task_id": task_id,
"repository": "aios/aios",
"base_oid": BASE,
"expected_branch_oid": BASE,
"target_branch": BRANCH,
"branch_mode": "CREATE_NEW",
"generation": 1,
}
}
runtime.runtimes = {}
runtime.controllers = {}
runtime.bootstrap = GovernedTaskBootstrap(
runtime.authority,
tmp_path,
remote_name="test",
token_path=tmp_path / "token",
ssh_key_path=tmp_path / "key",
)
runtime.bindings = {}
runtime.create_task(
{
"task_id": task_id,
"repository": "aios/aios",
"base_oid": BASE,
"expected_branch_oid": BASE,
"target_branch": BRANCH,
"risk": "engineering",
"description": "governed runtime lineage test",
"idempotency_key": JOB_ID_PREFIX + "-" + task_id,
}
)
job_root = runtime._job_root(task_id)
runtime.controllers[task_id] = GitOpsJobController(
JobStore(job_root, payload_root=job_root / "payloads")
)
def build(
lease: WriterLeaseRequestV2,
*,
job_id: str,
lease_not_before: datetime | None = None,
lease_expires_at: datetime | None = None,
) -> tuple[ContinuationRuntimeBinding, GitOpsJobController, object]:
controller = runtime.controllers.get(lease.task_id)
if controller is None:
root = runtime._job_root(lease.task_id)
controller = GitOpsJobController(JobStore(root, payload_root=root / "payloads"))
now = datetime.now(timezone.utc)
return (
ContinuationRuntimeBinding(
request=lease,
job_id=job_id,
workspace_path=tmp_path / "worktrees" / lease.task_id,
lease_not_before=lease_not_before or now - timedelta(seconds=1),
lease_expires_at=lease_expires_at or now + timedelta(seconds=lease.lease_seconds),
original_receipt_valid=True,
original_grant_consumed=True,
permissions=ContinuationPermissions(),
),
controller,
object(),
)
runtime._build_runtime = build
return runtime
def create_durable_lease(
runtime: GovernedRuntime,
*,
expires_at: str = "2020-01-01T00:00:00+00:00",
) -> GitOpsJobController:
root = runtime._job_root(TASK_ID)
controller = GitOpsJobController(JobStore(root, payload_root=root / "payloads"))
receipt = SimpleNamespace(job_id=runtime.bindings[TASK_ID]["job_id"])
controller.lease_job(
receipt.job_id,
lease_id="writer-lease-recovery",
lineage_anchor_oid=BASE,
current_head_oid=BASE,
generation=1,
lease_expires_at=expires_at,
expected_revision=0,
)
runtime.controllers[TASK_ID] = controller
return controller
def durable_row(runtime: GovernedRuntime) -> tuple[str, int]:
database = runtime._job_root(TASK_ID) / "state.sqlite3"
with sqlite3.connect(f"file:{database}?mode=ro", uri=True) as connection:
row = connection.execute(
"SELECT lease_expires_at, revision FROM branch_leases WHERE lease_id=?",
("writer-lease-recovery",),
).fetchone()
assert row is not None
return row
def test_first_bootstrap_refreshes_only_runtime_without_durable_lease(tmp_path: Path) -> None:
runtime = make_runtime(tmp_path)
result = runtime.bootstrap_writer_lease(writer_request())
assert result["bootstrapped"] is True
assert result["runtime_rehydrated"] is False
assert result["durable_branch_lease_present"] is False
assert result["idempotent"] is False
database = runtime._job_root(TASK_ID) / "state.sqlite3"
with sqlite3.connect(f"file:{database}?mode=ro", uri=True) as connection:
assert connection.execute("SELECT COUNT(*) FROM branch_leases").fetchone()[0] == 0
def test_duplicate_bootstrap_is_idempotent_without_consuming_generation(tmp_path: Path) -> None:
runtime = make_runtime(tmp_path)
first = runtime.bootstrap_writer_lease(writer_request())
second = runtime.bootstrap_writer_lease(writer_request())
assert first["bootstrapped"] is True
assert first["generation"] == 1
assert second["bootstrapped"] is False
assert second["idempotent"] is True
assert second["generation"] == 1
database = runtime._job_root(TASK_ID) / "state.sqlite3"
with sqlite3.connect(f"file:{database}?mode=ro", uri=True) as connection:
assert connection.execute("SELECT COUNT(*) FROM branch_leases").fetchone()[0] == 0
def test_commit_after_bootstrap_reuses_the_exact_runtime_binding(tmp_path: Path) -> None:
runtime = make_runtime(tmp_path)
class RecordingWorkspace:
def __init__(self) -> None:
self.files: dict[str, bytes] = {}
def materialize_files(self, files: dict[str, bytes]) -> None:
self.files = dict(files)
class RecordingExecutor:
def __init__(self) -> None:
self.workspace = RecordingWorkspace()
self.binding = None
self.commit = None
def execute(self, binding: object, commit: object) -> dict[str, object]:
self.binding = binding
self.commit = commit
return {'task_id': binding.request.task_id, 'generation': binding.request.generation, 'files': sorted(self.workspace.files)}
executor = RecordingExecutor()
original_build = runtime._build_runtime
def build(lease: WriterLeaseRequestV2, *, job_id: str, lease_not_before: datetime | None=None, lease_expires_at: datetime | None=None) -> tuple[ContinuationRuntimeBinding, GitOpsJobController, object]:
binding, controller, _ = original_build(lease, job_id=job_id, lease_not_before=lease_not_before, lease_expires_at=lease_expires_at)
return (binding, controller, executor)
runtime._build_runtime = build
runtime.bootstrap_writer_lease(writer_request())
lease_response = runtime.acquire_lease(writer_request())
binding, controller, executor = runtime.runtimes[TASK_ID, 1]
assert lease_response['job_id'] == binding.job_id
assert lease_response['durable_branch_lease_present'] is True
_original_execute = executor.execute
def _execute_with_governed_commit_contract(*args, **kwargs):
_before_complete = kwargs.pop('before_complete', None)
_legacy = dict(_original_execute(*args, **kwargs))
_result = {**_legacy, 'job_id': binding.job_id, 'state': 'COMPLETED', 'side_effect_state': 'REMOTE_VERIFIED', 'receipt': {'authority_oid': 'b' * 40, 'git_remote_oid': 'b' * 40, 'api_branch_oid': 'b' * 40, 'commit_visible': True, 'remote_verified': True, 'force_push': False}}
if _before_complete is not None:
_before_complete('b' * 40)
return _result
executor.execute = _execute_with_governed_commit_contract
_bound_job_id = binding.job_id
result = runtime.commit_files({'task_id': TASK_ID, 'repository': 'aios/aios', 'generation': 1, 'target_branch': BRANCH, 'expected_branch_oid': BASE, 'idempotency_key': 'commit-after-bootstrap-v1', 'path': 'docs/recovery.txt', 'content_base64': 'YQ==', 'message': 'test commit after bootstrap'})
assert result['task_id'] == TASK_ID
assert result['generation'] == 1
assert result['files'] == ['docs/recovery.txt']
assert executor.binding.request.task_id == TASK_ID
assert executor.binding.request.generation == 1
database = runtime._job_root(TASK_ID) / 'state.sqlite3'
with sqlite3.connect(f'file:{database}?mode=ro', uri=True) as connection:
assert connection.execute('SELECT COUNT(*) FROM branch_leases').fetchone()[0] == 1
assert connection.execute('PRAGMA foreign_key_check(branch_leases)').fetchall() == []
assert result['commit_oid'] == 'b' * 40
assert result['commit_visible'] is True
assert result['branch_mode'] == 'CONTINUE_EXISTING'
assert result['target_branch'] == BRANCH
assert int(result['generation']) == 1
with controller.store._connect() as connection:
_persisted = connection.execute('SELECT current_head_oid,revision FROM branch_leases WHERE lease_id=?', ('lease-' + str(_bound_job_id),)).fetchone()
assert _persisted is not None
assert _persisted[0] == 'b' * 40
assert int(_persisted[1]) == 1
fresh_runtime = make_runtime(tmp_path, authority_head='b' * 40)
fresh_response = fresh_runtime.bootstrap_writer_lease(writer_request(branch_mode='CONTINUE_EXISTING', expected_branch_oid='b' * 40))
fresh_binding, _, _ = fresh_runtime.runtimes[TASK_ID, 1]
assert fresh_response['job_id'] == str(_bound_job_id)
assert fresh_response['runtime_rehydrated'] is True
assert fresh_binding.job_id == str(_bound_job_id)
assert fresh_binding.request.expected_branch_oid == 'b' * 40
with fresh_runtime.controllers[TASK_ID].store._connect() as connection:
_fresh_lease = connection.execute('SELECT job_id,current_head_oid,revision FROM branch_leases WHERE lease_id=?', ('lease-' + str(_bound_job_id),)).fetchone()
assert tuple(_fresh_lease) == (str(_bound_job_id), 'b' * 40, 1)
@pytest.mark.parametrize(
("field", "value", "error"),
(
("repository", "other/repository", "LEASE_BINDING_CONFLICT"),
("base_oid", "b" * 40, "LEASE_BINDING_CONFLICT"),
("expected_branch_oid", "b" * 40, "LEASE_BINDING_CONFLICT"),
("target_branch", "agent/wrong-target", "LEASE_BINDING_CONFLICT"),
("generation", 2, "GENERATION_BINDING_CONFLICT"),
),
)
def test_bootstrap_rejects_any_nonmatching_binding_field(
tmp_path: Path, field: str, value: object, error: str
) -> None:
runtime = make_runtime(tmp_path)
with pytest.raises(RuntimeError, match=error):
runtime.bootstrap_writer_lease(writer_request(**{field: value}))
def test_bootstrap_rejects_unknown_task_ownership(tmp_path: Path) -> None:
runtime = make_runtime(tmp_path)
with pytest.raises(RuntimeError, match="TASK_OWNERSHIP_INVALID"):
runtime.bootstrap_writer_lease(writer_request(task_id="unknown-task"))
def test_renew_missing_durable_lease_is_not_bootstrap(tmp_path: Path) -> None:
runtime = make_runtime(tmp_path)
with pytest.raises(RuntimeError, match="LEASE_NOT_FOUND"):
runtime.renew_writer_lease(writer_request())
def test_renew_is_generation_scoped_before_durable_lookup(tmp_path: Path) -> None:
runtime = make_runtime(tmp_path)
create_durable_lease(runtime)
with pytest.raises(RuntimeError, match="GENERATION_BINDING_CONFLICT"):
runtime.renew_writer_lease(writer_request(generation=2))
assert durable_row(runtime) == ("2020-01-01T00:00:00+00:00", 0)
def test_restart_rehydrates_only_from_matching_durable_lease(tmp_path: Path) -> None:
runtime = make_runtime(tmp_path)
create_durable_lease(runtime)
runtime.runtimes.clear()
result = runtime.renew_writer_lease(writer_request())
expires_at, revision = durable_row(runtime)
assert result["recovered"] is True
assert result["runtime_rehydrated"] is True
assert result["durable_branch_lease_present"] is True
assert revision == 1
assert datetime.fromisoformat(expires_at) > datetime.now(timezone.utc)
def test_duplicate_renewal_is_idempotent_after_first_durable_recovery(tmp_path: Path) -> None:
runtime = make_runtime(tmp_path)
create_durable_lease(runtime)
first = runtime.renew_writer_lease(writer_request())
second = runtime.renew_writer_lease(writer_request())
assert first["recovered"] is True
assert first["idempotent"] is False
assert second["recovered"] is False
assert second["idempotent"] is True
assert durable_row(runtime)[1] == 1
def test_update_lease_keeps_expiry_without_optional_argument_and_keeps_cas(tmp_path: Path) -> None:
runtime = make_runtime(tmp_path)
controller = create_durable_lease(runtime)
controller.update_lease(
"writer-lease-recovery",
expected_revision=0,
current_head_oid=BASE,
generation=1,
)
assert durable_row(runtime) == ("2020-01-01T00:00:00+00:00", 1)
with pytest.raises(JobError, match="LEASE_REVISION_CAS_CONFLICT"):
controller.update_lease(
"writer-lease-recovery",
expected_revision=0,
current_head_oid=BASE,
generation=1,
)
def test_two_runtime_connections_resolve_renewal_cas_race_idempotently(tmp_path: Path) -> None:
first = make_runtime(tmp_path)
first_controller = create_durable_lease(first)
second = make_runtime(tmp_path)
second_controller = GitOpsJobController(
JobStore(second._job_root(TASK_ID), payload_root=second._job_root(TASK_ID) / "payloads")
)
second.controllers[TASK_ID] = second_controller
gate = threading.Barrier(2)
def gate_update(controller: GitOpsJobController) -> None:
original = controller.update_lease
def update(*args: object, **kwargs: object) -> None:
gate.wait(timeout=5)
original(*args, **kwargs)
controller.update_lease = update
gate_update(first_controller)
gate_update(second_controller)
with ThreadPoolExecutor(max_workers=2) as pool:
results = list(pool.map(lambda runtime: runtime.renew_writer_lease(writer_request()), (first, second)))
assert sorted((item["recovered"], item["idempotent"]) for item in results) == [
(False, True),
(True, False),
]
assert durable_row(first)[1] == 1
def test_bootstrap_preserves_exact_governed_job_lineage(tmp_path: Path) -> None:
runtime = make_runtime(tmp_path)
expected_job_id = runtime.bindings[TASK_ID]["job_id"]
response = runtime.bootstrap_writer_lease(writer_request())
binding, _, _ = runtime.runtimes[(TASK_ID, 1)]
assert response["job_id"] == expected_job_id
assert binding.job_id == expected_job_id
assert binding.request.job_id == expected_job_id
def test_restart_rehydrate_preserves_exact_governed_job_lineage(tmp_path: Path) -> None:
first = make_runtime(tmp_path)
expected_job_id = first.bindings[TASK_ID]["job_id"]
first.acquire_lease(writer_request())
runtime = make_runtime(tmp_path)
assert runtime is not first
assert runtime.runtimes == {}
response = runtime.bootstrap_writer_lease(writer_request())
binding, _, _ = runtime.runtimes[(TASK_ID, 1)]
assert response["job_id"] == expected_job_id
assert binding.job_id == expected_job_id
assert binding.request.job_id == expected_job_id
def test_wrong_governed_job_id_is_denied_before_runtime_or_lease_side_effect(tmp_path: Path) -> None:
runtime = make_runtime(tmp_path)
with pytest.raises(RuntimeError, match="LEASE_BINDING_CONFLICT"):
runtime.bootstrap_writer_lease(writer_request(job_id="job-wrong-lineage"))
assert runtime.runtimes == {}
database = runtime._job_root(TASK_ID) / "state.sqlite3"
with sqlite3.connect(f"file:{database}?mode=ro", uri=True) as connection:
assert connection.execute("SELECT COUNT(*) FROM branch_leases").fetchone()[0] == 0
assert connection.execute("PRAGMA foreign_key_check(branch_leases)").fetchall() == []
def test_acquire_creates_exact_governed_writer_lease_with_fk_and_cas(tmp_path: Path) -> None:
runtime = make_runtime(tmp_path)
expected_job_id = runtime.bindings[TASK_ID]["job_id"]
response = runtime.acquire_lease(writer_request())
assert response["job_id"] == expected_job_id
assert response["durable_branch_lease_present"] is True
assert response["idempotent"] is False
database = runtime._job_root(TASK_ID) / "state.sqlite3"
with sqlite3.connect(f"file:{database}?mode=ro", uri=True) as connection:
foreign_keys = connection.execute(
"PRAGMA foreign_key_list(branch_leases)"
).fetchall()
row = connection.execute(
"SELECT job_id,branch,lineage_anchor_oid,current_head_oid,generation,state,revision "
"FROM branch_leases"
).fetchone()
job = connection.execute(
"SELECT state,side_effect_state,revision FROM jobs WHERE job_id=?",
(expected_job_id,),
).fetchone()
assert connection.execute("PRAGMA foreign_key_check(branch_leases)").fetchall() == []
assert any(
fk[2] == "jobs" and fk[3] == "job_id" and fk[4] == "job_id"
for fk in foreign_keys
)
assert row == (expected_job_id, BRANCH, BASE, BASE, 1, "ACTIVE", 0)
assert job == ("LEASED", "LEASED", 2)
def test_duplicate_acquire_observes_same_durable_writer_lease(tmp_path: Path) -> None:
runtime = make_runtime(tmp_path)
expected_job_id = runtime.bindings[TASK_ID]["job_id"]
first = runtime.acquire_lease(writer_request())
second = runtime.acquire_lease(writer_request())
assert first["job_id"] == expected_job_id
assert second["job_id"] == expected_job_id
assert second["idempotent"] is True
database = runtime._job_root(TASK_ID) / "state.sqlite3"
with sqlite3.connect(f"file:{database}?mode=ro", uri=True) as connection:
leases = connection.execute(
"SELECT lease_id,job_id,revision FROM branch_leases"
).fetchall()
job = connection.execute(
"SELECT state,revision FROM jobs WHERE job_id=?",
(expected_job_id,),
).fetchone()
assert leases == [("lease-" + expected_job_id, expected_job_id, 0)]
assert job == ("LEASED", 2)
def test_wrong_job_acquire_is_denied_before_authorize_or_lease(tmp_path: Path) -> None:
runtime = make_runtime(tmp_path)
expected_job_id = runtime.bindings[TASK_ID]["job_id"]
with pytest.raises(RuntimeError, match="LEASE_BINDING_CONFLICT"):
runtime.acquire_lease(writer_request(job_id="job-wrong-lineage"))
database = runtime._job_root(TASK_ID) / "state.sqlite3"
with sqlite3.connect(f"file:{database}?mode=ro", uri=True) as connection:
job = connection.execute(
"SELECT state,side_effect_state,revision FROM jobs WHERE job_id=?",
(expected_job_id,),
).fetchone()
lease_count = connection.execute("SELECT COUNT(*) FROM branch_leases").fetchone()[0]
assert job == ("REQUESTED", "NONE", 0)
assert lease_count == 0
def test_omitted_request_job_id_resolves_the_exact_durable_parent(tmp_path: Path) -> None:
runtime = make_runtime(tmp_path)
expected_job_id = runtime.bindings[TASK_ID]["job_id"]
raw = dict(writer_request())
response = runtime.acquire_lease(raw)
binding, _, _ = runtime.runtimes[(TASK_ID, 1)]
assert response["job_id"] == expected_job_id
assert binding.job_id == expected_job_id
assert binding.request.job_id == expected_job_id
def test_durable_job_expected_head_mismatch_is_denied(tmp_path: Path) -> None:
runtime = make_runtime(tmp_path)
expected_job_id = runtime.bindings[TASK_ID]["job_id"]
controller = runtime.controllers[TASK_ID]
with controller.store._connect() as connection:
row = connection.execute(
"SELECT request_json FROM jobs WHERE job_id=?", (expected_job_id,)
).fetchone()
request = json.loads(row[0])
request["expected_branch_oid"] = "c" * 40
request["binding"]["expected_branch_oid"] = "c" * 40
connection.execute(
"UPDATE jobs SET request_json=? WHERE job_id=?",
(json.dumps(request, sort_keys=True, separators=(",", ":")), expected_job_id),
)
connection.commit()
with pytest.raises(RuntimeError, match="RUNTIME_GOVERNED_JOB_UNRESOLVED"):
runtime.bootstrap_writer_lease(writer_request())
assert runtime.runtimes == {}
def _commit_ready_runtime(tmp_path: Path):
runtime = make_runtime(tmp_path)
runtime.bootstrap_writer_lease(writer_request())
binding, controller, _ = runtime.runtimes[(TASK_ID, 1)]
class Workspace:
def materialize_files(self, _files):
return None
class Executor:
def __init__(self):
self.workspace = Workspace()
self.calls = 0
def execute(self, _binding, _commit, *, before_complete=None):
self.calls += 1
result = {
"job_id": binding.job_id,
"state": "COMPLETED",
"side_effect_state": "REMOTE_VERIFIED",
"receipt": {
"authority_oid": "b" * 40,
"git_remote_oid": "b" * 40,
"api_branch_oid": "b" * 40,
"commit_visible": True,
"remote_verified": True,
"force_push": False,
},
}
if before_complete is not None:
before_complete("b" * 40)
return result
executor = Executor()
runtime.runtimes[(TASK_ID, 1)] = (binding, controller, executor)
runtime.acquire_lease(writer_request())
return runtime, binding, controller, executor
def _commit_request():
return {
"task_id": TASK_ID,
"repository": "aios/aios",
"generation": 1,
"target_branch": BRANCH,
"expected_branch_oid": BASE,
"idempotency_key": "commit-cas-test-v1",
"path": "docs/cas.txt",
"content_base64": "YQ==",
"message": "test commit cas",
}
def test_commit_lease_revision_cas_conflict_is_preserved(tmp_path: Path) -> None:
runtime, _binding, controller, _executor = _commit_ready_runtime(tmp_path)
def conflict(*_args, **_kwargs):
raise JobError("LEASE_REVISION_CAS_CONFLICT")
controller.update_lease = conflict
with pytest.raises(RuntimeError, match="LEASE_CAS_CONFLICT"):
runtime.commit_files(_commit_request())
def test_commit_divergent_durable_head_fails_before_executor(tmp_path: Path) -> None:
runtime, binding, controller, executor = _commit_ready_runtime(tmp_path)
controller.update_lease(
"lease-" + binding.job_id,
expected_revision=0,
current_head_oid="c" * 40,
generation=1,
)
with pytest.raises(RuntimeError, match="LEASE_HEAD_RECONCILIATION_REQUIRED"):
runtime.commit_files(_commit_request())
assert executor.calls == 0
def test_remote_pending_never_advances_durable_head(tmp_path: Path) -> None:
runtime, binding, controller, executor = _commit_ready_runtime(tmp_path)
def pending(_binding, _commit, **_kwargs):
executor.calls += 1
return {
"job_id": binding.job_id,
"state": "RECONCILIATION_REQUIRED",
"side_effect_state": "REMOTE_PUSH_PENDING",
"receipt": {
"authority_oid": "b" * 40,
"desired_remote_oid": "b" * 40,
"remote_verified": False,
},
}
executor.execute = pending
with pytest.raises(RuntimeError, match="RECONCILIATION_REQUIRED"):
runtime.commit_files(_commit_request())
with controller.store._connect() as connection:
row = connection.execute(
"SELECT current_head_oid,revision FROM branch_leases WHERE lease_id=?",
("lease-" + binding.job_id,),
).fetchone()
assert tuple(row) == (BASE, 0)
def test_retry_accepts_exact_head_already_advanced_at_remote_verified_boundary(tmp_path: Path) -> None:
runtime, binding, controller, executor = _commit_ready_runtime(tmp_path)
commit_oid = "b" * 40
controller.update_lease(
"lease-" + binding.job_id,
expected_revision=0,
current_head_oid=commit_oid,
generation=1,
)
receipt = {
"authority_oid": commit_oid,
"git_remote_oid": commit_oid,
"api_branch_oid": commit_oid,
"commit_visible": True,
"remote_verified": True,
"force_push": False,
}
with controller.store._connect() as connection:
connection.execute(
"UPDATE jobs SET state='REMOTE_VERIFIED',side_effect_state='REMOTE_VERIFIED',"
"receipt_json=? WHERE job_id=?",
(json.dumps(receipt, sort_keys=True, separators=(",", ":")), binding.job_id),
)
connection.commit()
result = runtime.commit_files(_commit_request())
assert result["commit_oid"] == commit_oid
assert executor.calls == 1
with controller.store._connect() as connection:
row = connection.execute(
"SELECT current_head_oid,revision FROM branch_leases WHERE lease_id=?",
("lease-" + binding.job_id,),
).fetchone()
assert tuple(row) == (commit_oid, 1)
def test_completed_response_with_invalid_oid_uses_canonical_oid_error(tmp_path: Path) -> None:
runtime, binding, controller, executor = _commit_ready_runtime(tmp_path)
def invalid_oid(_binding, _commit, **_kwargs):
executor.calls += 1
return {
"job_id": binding.job_id,
"state": "COMPLETED",
"side_effect_state": "REMOTE_VERIFIED",
"receipt": {
"authority_oid": "not-an-oid",
"commit_visible": True,
"remote_verified": True,
},
}
executor.execute = invalid_oid
with pytest.raises(RuntimeError, match="GOVERNED_COMMIT_OID_INVALID"):
runtime.commit_files(_commit_request())
with controller.store._connect() as connection:
row = connection.execute(
"SELECT current_head_oid,revision FROM branch_leases WHERE lease_id=?",
("lease-" + binding.job_id,),
).fetchone()
assert tuple(row) == (BASE, 0)
def test_acquire_renews_an_expired_matching_durable_lease_with_revision_cas(tmp_path: Path) -> None:
runtime = make_runtime(tmp_path)
create_durable_lease(runtime)
runtime.runtimes.clear()
result = runtime.acquire_lease(writer_request())
expires_at, revision = durable_row(runtime)
assert result["recovered"] is True
assert result["runtime_rehydrated"] is False
assert result["durable_branch_lease_present"] is True
assert revision == 1
assert datetime.fromisoformat(expires_at) > datetime.now(timezone.utc)
def test_authority_recovers_only_clean_detached_single_attempt_worktree(tmp_path: Path) -> None:
import subprocess
from aios_authority_git_executor import AuthorityGitExecutor
bare = tmp_path / "authority.git"
seed = tmp_path / "seed"
subprocess.run(["git", "init", "--bare", str(bare)], check=True, capture_output=True)
subprocess.run(["git", "init", str(seed)], check=True, capture_output=True)
(seed / "base.txt").write_text("base\n", encoding="utf-8")
subprocess.run(["git", "-C", str(seed), "add", "base.txt"], check=True)
subprocess.run(
["git", "-C", str(seed), "-c", "user.name=test", "-c", "user.email=test@example.invalid",
"commit", "-m", "base"], check=True, capture_output=True,
)
base = subprocess.check_output(["git", "-C", str(seed), "rev-parse", "HEAD"], text=True).strip()
subprocess.run(["git", "-C", str(seed), "push", str(bare), "HEAD:refs/heads/main"], check=True, capture_output=True)
root = tmp_path / "worktrees"
authority = AuthorityGitExecutor(bare, root)
worktree = authority.create_detached_worktree(base, "task")
(worktree / "attempt.txt").write_text("attempt\n", encoding="utf-8")
subprocess.run(["git", "-C", str(worktree), "add", "attempt.txt"], check=True)
subprocess.run(
["git", "-C", str(worktree), "-c", "user.name=test", "-c", "user.email=test@example.invalid",
"commit", "-m", "failed attempt"], check=True, capture_output=True,
)
stale = subprocess.check_output(["git", "-C", str(worktree), "rev-parse", "HEAD"], text=True).strip()
recovered = authority.recover_failed_detached_worktree_exact(
path=worktree,
expected_oid=base,
stale_oid=stale,
target_ref="refs/heads/agent/recovery-test",
)
assert recovered == stale
assert subprocess.check_output(["git", "-C", str(worktree), "rev-parse", "HEAD"], text=True).strip() == base
assert subprocess.run(["git", "-C", str(worktree), "symbolic-ref", "-q", "HEAD"]).returncode == 1
assert subprocess.check_output(["git", "-C", str(worktree), "status", "--porcelain"], text=True) == ""