Compare commits

...
35 changed files with 7834 additions and 80 deletions
+13 -7
View File
@@ -13,6 +13,7 @@ from hashlib import sha256
from pathlib import Path
import json
from typing import Any
from message_handoff import stable_event_id
def utc_now() -> str:
@@ -55,13 +56,18 @@ class AgentEventBus:
payload: dict[str, Any],
correlation_id: str | None = None,
) -> dict[str, Any]:
event_id = "evt_" + sha256(
json.dumps(
{"kind": kind, "source": source, "summary": summary, "payload": payload},
sort_keys=True,
ensure_ascii=False,
).encode("utf-8")
).hexdigest()[:16]
event_id = stable_event_id(
source=source, target=target, message_type=kind, payload=payload,
tenant_id=(payload.get('tenant_id') if isinstance(payload, dict) else None),
user_id=(payload.get('user_id') if isinstance(payload, dict) else None),
project_id=(payload.get('project_id') if isinstance(payload, dict) else None),
conversation_id=(payload.get('conversation_id') if isinstance(payload, dict) else None),
task_id=(payload.get('task_id') if isinstance(payload, dict) else None),
run_id=(payload.get('run_id') if isinstance(payload, dict) else None),
causation_id=(payload.get('causation_id') if isinstance(payload, dict) else None),
sequence=(payload.get('sequence') if isinstance(payload, dict) else None),
idempotency_key=(payload.get('idempotency_key') if isinstance(payload, dict) else None),
)
event = AgentEvent(event_id, kind, source, target, summary, payload, correlation_id=correlation_id)
self._write("pending", event.to_dict())
self._audit("created", event.to_dict())
@@ -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")
@@ -12,7 +12,11 @@ BRIDGE_OPERATIONS = (
"getRuntimeStatus",
"getGovernedWriteCapabilities",
"createGovernedTask",
"projectOnlineAgentExecution",
"registerOnlineAgentExecutionPlan",
"acquireWriterLease",
"bootstrapWriterLease",
"renewWriterLease",
"commitFileGoverned",
"commitFilesGoverned",
"getGitOpsJob",
@@ -30,7 +34,14 @@ BRIDGE_OPERATIONS = (
OPERATION_REQUIRED_FIELDS = {
"createGovernedTask": ("task_id", "repository", "base_oid", "target_branch", "risk", "description", "idempotency_key"),
"projectOnlineAgentExecution": ("task_id",),
"registerOnlineAgentExecutionPlan": (
"schema", "task_id", "job_id", "source_head_oid", "generation",
"idempotency_key", "execution",
),
"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 +57,68 @@ OPERATION_REQUIRED_FIELDS = {
"getControlPlaneSnapshot": (),
}
BRIDGE_OPERATION_CONTRACTS = {
"projectOnlineAgentExecution": {
"method": "POST",
"path": "/v2/execution-envelope",
"required_fields": OPERATION_REQUIRED_FIELDS["projectOnlineAgentExecution"],
"response_fields": (
"schema", "task_id", "job_id", "repository", "base_oid",
"expected_branch_oid", "target_branch", "branch_mode", "generation",
"idempotency_key", "action", "execution_digest",
),
},
"registerOnlineAgentExecutionPlan": {
"method": "POST",
"path": "/v2/execution-plan",
"required_fields": OPERATION_REQUIRED_FIELDS["registerOnlineAgentExecutionPlan"],
"response_fields": (
"schema", "task_id", "job_id", "source_head_oid", "generation",
"idempotency_key", "plan_digest", "execution", "execution_digest",
),
},
"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 +195,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,
@@ -150,7 +231,11 @@ class BridgeV2Adapter:
return handler(dict(request))
def createGovernedTask(self, request: Mapping[str, Any]) -> Any: return self._call("createGovernedTask", request)
def projectOnlineAgentExecution(self, request: Mapping[str, Any]) -> Any: return self._call("projectOnlineAgentExecution", request)
def registerOnlineAgentExecutionPlan(self, request: Mapping[str, Any]) -> Any: return self._call("registerOnlineAgentExecutionPlan", 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
+152 -29
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)
@@ -152,7 +157,7 @@ class DetachedGitWorkspace:
def materialize_files(self, files: Mapping[str, bytes]) -> None:
dirty = self._dirty_paths()
expected_paths = set(files)
if dirty and dirty != expected_paths:
if dirty and not dirty.issubset(expected_paths):
raise ContinuationExecutionError("WORKTREE_DIRTY_SCOPE_MISMATCH")
for relative, content in files.items():
target = (self.path / relative).resolve()
@@ -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,127 @@ 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:
return self.controller.store._receipt(current, duplicate=True).as_dict()
self._validate_preflight(binding)
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:
cycle = self.controller.begin_continuation_cycle(
binding.job_id,
idempotency_key=commit.idempotency_key,
expected_head_oid=binding.request.expected_branch_oid,
generation=binding.request.generation,
expected_revision=int(current["revision"]),
)
if cycle.get("completed"):
return self.controller.store._receipt(
self.controller.store.read(binding.job_id), duplicate=True
).as_dict()
current = self.controller.store.read(binding.job_id)
current_state = JobState(current["state"])
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)
result = self.controller.advance(
binding.job_id,
JobState.COMPLETED,
side_effect_state="REMOTE_VERIFIED",
receipt=receipt,
).as_dict()
self.controller.complete_continuation_cycle(
binding.job_id,
idempotency_key=commit.idempotency_key,
commit_oid=commit_oid,
)
return result
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)
result = self._finish_remote_verification(
binding.job_id,
authority_oid,
new_oid,
before_complete=before_complete,
)
if str(result.get("state") or "") == JobState.COMPLETED.value:
self.controller.complete_continuation_cycle(
binding.job_id,
idempotency_key=commit.idempotency_key,
commit_oid=new_oid,
)
return result
finally:
if projected:
self.credential.revoke()
@@ -525,7 +629,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 +653,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")
@@ -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"
@@ -209,7 +230,15 @@ class JobStore:
kind TEXT NOT NULL, digest TEXT NOT NULL, payload_json TEXT NOT NULL,
created_at TEXT NOT NULL, FOREIGN KEY(job_id) REFERENCES jobs(job_id)
);
INSERT OR REPLACE INTO metadata(key, value) VALUES('schema_version', '1');
CREATE TABLE IF NOT EXISTS continuation_cycles (
job_id TEXT NOT NULL, idempotency_key TEXT NOT NULL,
sequence INTEGER NOT NULL, expected_head_oid TEXT NOT NULL,
generation INTEGER NOT NULL, state TEXT NOT NULL,
commit_oid TEXT, created_at TEXT NOT NULL, updated_at TEXT NOT NULL,
PRIMARY KEY(job_id,idempotency_key), UNIQUE(job_id,sequence),
FOREIGN KEY(job_id) REFERENCES jobs(job_id)
);
INSERT OR REPLACE INTO metadata(key, value) VALUES('schema_version', '2');
"""
)
except sqlite3.Error as exc:
@@ -404,13 +433,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 +451,271 @@ 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 begin_continuation_cycle(
self,
job_id: str,
*,
idempotency_key: str,
expected_head_oid: str,
generation: int,
expected_revision: int,
) -> dict[str, Any]:
if (
not idempotency_key
or len(idempotency_key) > 128
or not all(character.isalnum() or character in "._:-" for character in idempotency_key)
or len(expected_head_oid) != 40
or any(character not in "0123456789abcdef" for character in expected_head_oid)
or generation < 1
):
raise JobError("CONTINUATION_CYCLE_BINDING_INVALID")
try:
with self.store._connect() as connection:
connection.execute("BEGIN IMMEDIATE")
row = connection.execute("SELECT * FROM jobs WHERE job_id=?", (job_id,)).fetchone()
if row is None:
raise JobError("JOB_NOT_FOUND")
existing = connection.execute(
"SELECT * FROM continuation_cycles WHERE job_id=? AND idempotency_key=?",
(job_id, idempotency_key),
).fetchone()
if existing is not None:
if str(existing["expected_head_oid"]) != expected_head_oid or int(existing["generation"]) != generation:
raise JobError("CONTINUATION_CYCLE_BINDING_CONFLICT")
if str(existing["state"]) == "COMPLETED":
connection.commit()
return {"completed": True, "commit_oid": str(existing["commit_oid"] or ""), "idempotent": True}
if str(row["state"]) == JobState.COMPLETED.value:
receipt = json.loads(str(row["receipt_json"] or "{}"))
commit_oid = str(receipt.get("authority_oid") or "")
lease = connection.execute(
"SELECT current_head_oid FROM branch_leases WHERE job_id=? AND generation=? AND state='ACTIVE'",
(job_id, generation),
).fetchall()
if (
len(lease) == 1
and len(commit_oid) == 40
and str(lease[0]["current_head_oid"] or "") == commit_oid
and receipt.get("remote_verified") is True
):
now = utc_now()
connection.execute(
"UPDATE continuation_cycles SET state='COMPLETED',commit_oid=?,updated_at=? "
"WHERE job_id=? AND idempotency_key=? AND state='ACTIVE'",
(commit_oid, now, job_id, idempotency_key),
)
connection.commit()
return {"completed": True, "commit_oid": commit_oid, "idempotent": True}
raise JobError("CONTINUATION_CYCLE_RECOVERY_REQUIRED")
connection.commit()
return {"completed": False, "commit_oid": None, "idempotent": True}
if int(row["revision"]) != expected_revision:
raise JobError("REVISION_CAS_CONFLICT")
if str(row["state"]) != JobState.COMPLETED.value:
raise JobError("CONTINUATION_CYCLE_JOB_STATE_INVALID")
if str(row["branch"]) in {"main", "master"} or str(row["branch"]).startswith(("refs/", "tags/")):
raise JobError("PROTECTED_BRANCH_REJECTED")
receipt = json.loads(str(row["receipt_json"] or "{}"))
if receipt.get("remote_verified") is not True or any(
str(receipt.get(field) or "") != expected_head_oid
for field in ("authority_oid", "api_branch_oid", "git_remote_oid")
):
raise JobError("CONTINUATION_CYCLE_RECEIPT_INVALID")
leases = connection.execute(
"SELECT * FROM branch_leases WHERE job_id=? AND branch=? AND generation=? AND state='ACTIVE'",
(job_id, str(row["branch"]), generation),
).fetchall()
if len(leases) != 1 or str(leases[0]["current_head_oid"] or "") != expected_head_oid:
raise JobError("CONTINUATION_CYCLE_LEASE_INVALID")
if connection.execute(
"SELECT 1 FROM continuation_cycles WHERE job_id=? AND state='ACTIVE' LIMIT 1",
(job_id,),
).fetchone() is not None:
raise JobError("CONTINUATION_CYCLE_ACTIVE_CONFLICT")
sequence = int(connection.execute(
"SELECT COALESCE(MAX(sequence),0)+1 FROM continuation_cycles WHERE job_id=?",
(job_id,),
).fetchone()[0])
now = utc_now()
connection.execute(
"INSERT INTO continuation_cycles(job_id,idempotency_key,sequence,expected_head_oid,generation,state,commit_oid,created_at,updated_at) "
"VALUES(?,?,?,?,?,'ACTIVE',NULL,?,?)",
(job_id, idempotency_key, sequence, expected_head_oid, generation, now, now),
)
next_revision = expected_revision + 1
updated = connection.execute(
"UPDATE jobs SET state=?,side_effect_state=?,revision=?,updated_at=? WHERE job_id=? AND revision=?",
(JobState.LEASED.value, "CONTINUATION_LEASED", next_revision, now, job_id, expected_revision),
)
if updated.rowcount != 1:
raise JobError("REVISION_CAS_CONFLICT")
connection.execute(
"INSERT INTO state_transitions(job_id,from_state,to_state,revision,side_effect_state,created_at) VALUES(?,?,?,?,?,?)",
(job_id, JobState.COMPLETED.value, JobState.LEASED.value, next_revision, "CONTINUATION_LEASED", now),
)
cycle_receipt = {
"idempotency_key": idempotency_key,
"expected_head_oid": expected_head_oid,
"generation": generation,
"sequence": sequence,
}
connection.execute(
"INSERT INTO receipts(receipt_id,job_id,kind,digest,payload_json,created_at) VALUES(?,?,?,?,?,?)",
(
f"receipt-{job_id}-{next_revision}",
job_id,
"CONTINUATION_LEASED",
hashlib.sha256(canonical_json(cycle_receipt)).hexdigest(),
json.dumps(cycle_receipt, sort_keys=True),
now,
),
)
result = dict(connection.execute("SELECT * FROM jobs WHERE job_id=?", (job_id,)).fetchone())
connection.commit()
except (JobError, BindingConflict):
raise
except sqlite3.Error as exc:
raise ServiceUnavailable("JOB_DB_UNAVAILABLE") from exc
result["request"] = json.loads(result["request_json"])
self.store._mirror(result)
return {"completed": False, "commit_oid": None, "idempotent": False}
def complete_continuation_cycle(self, job_id: str, *, idempotency_key: str, commit_oid: str) -> bool:
if len(commit_oid) != 40 or any(character not in "0123456789abcdef" for character in commit_oid):
raise JobError("CONTINUATION_CYCLE_COMMIT_OID_INVALID")
try:
with self.store._connect() as connection:
connection.execute("BEGIN IMMEDIATE")
cycle = connection.execute(
"SELECT * FROM continuation_cycles WHERE job_id=? AND idempotency_key=?",
(job_id, idempotency_key),
).fetchone()
if cycle is None:
connection.commit()
return False
if str(cycle["state"]) == "COMPLETED":
if str(cycle["commit_oid"] or "") != commit_oid:
raise JobError("CONTINUATION_CYCLE_COMMIT_CONFLICT")
connection.commit()
return True
row = connection.execute("SELECT * FROM jobs WHERE job_id=?", (job_id,)).fetchone()
leases = connection.execute(
"SELECT current_head_oid FROM branch_leases WHERE job_id=? AND generation=? AND state='ACTIVE'",
(job_id, int(cycle["generation"])),
).fetchall()
receipt = json.loads(str(row["receipt_json"] or "{}")) if row is not None else {}
if (
row is None
or str(row["state"]) != JobState.COMPLETED.value
or receipt.get("remote_verified") is not True
or str(receipt.get("authority_oid") or "") != commit_oid
or len(leases) != 1
or str(leases[0]["current_head_oid"] or "") != commit_oid
):
raise JobError("CONTINUATION_CYCLE_COMPLETION_EVIDENCE_INVALID")
now = utc_now()
updated = connection.execute(
"UPDATE continuation_cycles SET state='COMPLETED',commit_oid=?,updated_at=? "
"WHERE job_id=? AND idempotency_key=? AND state='ACTIVE'",
(commit_oid, now, job_id, idempotency_key),
)
if updated.rowcount != 1:
raise JobError("CONTINUATION_CYCLE_CAS_CONFLICT")
connection.commit()
return True
except (JobError, BindingConflict):
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,
@@ -2,6 +2,8 @@
from __future__ import annotations
from dataclasses import dataclass, asdict
from contextlib import contextmanager
import fcntl
import hashlib
import json
from pathlib import Path
@@ -9,6 +11,11 @@ import re
from typing import Any, Mapping
from aios_gitops_job_controller import GitOpsJobController, JobStore
from aios_online_agent_execution import (
GovernedExecutionBinding,
project_execution_envelope,
project_online_task,
)
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}$")
@@ -36,35 +43,161 @@ class GovernedTaskBinding:
return {"task_id": self.task_id, "repository": self.repository, "base_oid": self.base_oid, "target_branch": self.target_branch, "expected_branch_oid": self.expected_branch_oid, "generation": self.generation, "branch_mode": self.branch_mode, "create_target_branch": self.branch_mode == "CREATE_NEW", "worktree_mode": "detached_at_base_oid" if self.branch_mode == "CREATE_NEW" else "detached_at_expected_head", "cas_ref_update_required": True, "idempotent": idempotent, "state": "BOUND", "job_id": self.job_id}
def _safe(value: str) -> str:
def _legacy_safe(value: str) -> str:
return re.sub(r"[^A-Za-z0-9_.-]+", "-", value).strip("-")[:120] or "task"
def _task_key(value: str) -> str:
readable = _legacy_safe(value)[:72]
return f"{readable}-{hashlib.sha256(value.encode('utf-8')).hexdigest()[:24]}"
class GovernedTaskBootstrap:
def __init__(self, authority: Any, root: Path, *, remote_name: str, token_path: Path, ssh_key_path: Path, api_base: str = "https://g.cnnetmkt.top/git/api/v1") -> None:
self.authority = authority; self.root = Path(root); self.remote_name = remote_name; self.token_path = Path(token_path); self.ssh_key_path = Path(ssh_key_path); self.api_base = api_base; self.task_meta_root = self.root / "task-bindings"; self.job_root = self.root / "jobs"; self.task_meta_root.mkdir(parents=True, exist_ok=True); self.job_root.mkdir(parents=True, exist_ok=True)
self.authority = authority; self.root = Path(root); self.remote_name = remote_name; self.token_path = Path(token_path); self.ssh_key_path = Path(ssh_key_path); self.api_base = api_base; self.task_meta_root = self.root / "task-bindings"; self.job_root = self.root / "jobs"; self.lock_root = self.root / "task-locks"; self.task_meta_root.mkdir(parents=True, exist_ok=True); self.job_root.mkdir(parents=True, exist_ok=True); self.lock_root.mkdir(parents=True, exist_ok=True)
def _store(self, task_id: str) -> tuple[JobStore, GitOpsJobController]:
root = self.job_root / _safe(task_id); store = JobStore(root, payload_root=root / "payloads"); return store, GitOpsJobController(store)
legacy = self.job_root / _legacy_safe(task_id)
keyed = self.job_root / _task_key(task_id)
root = legacy if legacy.is_dir() and not keyed.exists() else keyed
store = JobStore(root, payload_root=root / "payloads"); return store, GitOpsJobController(store)
def _metadata(self, task_id: str) -> Path:
return self.task_meta_root / (_safe(task_id) + ".json")
return self.task_meta_root / (_task_key(task_id) + ".json")
def _legacy_metadata(self, task_id: str) -> Path:
return self.task_meta_root / (_legacy_safe(task_id) + ".json")
@contextmanager
def _task_lock(self, task_id: str):
path = self.lock_root / (_task_key(task_id) + ".lock")
with path.open("a+b") as handle:
fcntl.flock(handle.fileno(), fcntl.LOCK_EX)
try:
yield
finally:
fcntl.flock(handle.fileno(), fcntl.LOCK_UN)
@contextmanager
def _branch_lock(self, repository: str, target_branch: str):
key = hashlib.sha256((repository + "\0" + target_branch).encode("utf-8")).hexdigest()
path = self.lock_root / ("branch-" + key + ".lock")
with path.open("a+b") as handle:
fcntl.flock(handle.fileno(), fcntl.LOCK_EX)
try:
yield
finally:
fcntl.flock(handle.fileno(), fcntl.LOCK_UN)
def _read_existing(self, task_id: str) -> GovernedTaskBinding | None:
path = self._metadata(task_id)
if not path.exists():
path = self._legacy_metadata(task_id)
if not path.is_file() or path.is_symlink(): return None
return GovernedTaskBinding(**json.loads(path.read_text(encoding="utf-8")))
binding = GovernedTaskBinding(**json.loads(path.read_text(encoding="utf-8")))
if binding.task_id != task_id:
raise BootstrapError("TASK_BINDING_KEY_COLLISION")
return binding
def get_binding(self, task_id: str) -> GovernedTaskBinding:
"""Return the exact durable binding; never synthesize missing lineage."""
existing = self._read_existing(str(task_id))
if existing is None or not existing.job_id:
raise BootstrapError("TASK_EXECUTION_PROJECTION_MISSING")
return existing
def execution_binding(self, binding: GovernedTaskBinding) -> GovernedExecutionBinding:
expected = binding.expected_branch_oid
mode = binding.branch_mode
if mode == "CREATE_NEW":
current = self.authority.read_ref("refs/heads/" + binding.target_branch)
if current is not None:
store, _ = self._store(binding.task_id)
with store._connect() as connection:
rows = connection.execute(
"SELECT current_head_oid,generation,state,lineage_anchor_oid "
"FROM branch_leases WHERE job_id=? AND branch=? AND state='ACTIVE' "
"ORDER BY revision DESC",
(binding.job_id, binding.target_branch),
).fetchall()
if len(rows) != 1:
raise BootstrapError("SAME_JOB_CONTINUATION_LEASE_INVALID")
row = rows[0]
if (
str(row["current_head_oid"] or "") != current
or int(row["generation"] or 0) != binding.generation
or str(row["lineage_anchor_oid"] or "") != binding.base_oid
):
raise BootstrapError("SAME_JOB_CONTINUATION_BINDING_INVALID")
expected = current
mode = "CONTINUE_EXISTING"
return GovernedExecutionBinding.from_mapping(
{
"task_id": binding.task_id,
"job_id": binding.job_id,
"repository": binding.repository,
"base_oid": binding.base_oid,
"expected_branch_oid": expected,
"target_branch": binding.target_branch,
"branch_mode": mode,
"generation": binding.generation,
"idempotency_key": binding.idempotency_key,
}
)
def execution_envelope(
self,
task_id: str,
execution_request: Mapping[str, Any] | None = None,
*,
required_capabilities: tuple[str, ...] = (),
expected_job_id: str | None = None,
) -> dict[str, Any]:
binding = self.get_binding(task_id)
if expected_job_id and binding.job_id != expected_job_id:
raise BootstrapError("JOB_BINDING_CONFLICT")
return project_execution_envelope(
self.execution_binding(binding),
execution_request,
required_capabilities=required_capabilities,
)
def online_task_projection(
self,
task_id: str,
task_request: Mapping[str, Any],
execution_request: Mapping[str, Any] | None = None,
*,
expected_job_id: str | None = None,
) -> dict[str, Any]:
binding = self.get_binding(task_id)
if expected_job_id and binding.job_id != expected_job_id:
raise BootstrapError("JOB_BINDING_CONFLICT")
return project_online_task(self.execution_binding(binding), task_request, execution_request)
def _write(self, binding: GovernedTaskBinding) -> None:
path = self._metadata(binding.task_id); temporary = path.with_suffix(".tmp"); temporary.write_text(json.dumps(asdict(binding), sort_keys=True), encoding="utf-8"); temporary.replace(path)
def create(self, request: Mapping[str, Any]) -> dict[str, Any]:
task_id = str(request.get("task_id") or "")
if not task_id:
raise BootstrapError("TASK_BINDING_INVALID")
repository = str(request.get("repository") or "")
target_branch = str(request.get("target_branch") or "")
with self._task_lock(task_id):
with self._branch_lock(repository, target_branch):
return self._create_locked(request)
def _create_locked(self, request: Mapping[str, Any]) -> dict[str, Any]:
values = {key: str(request[key]) for key in ("task_id", "repository", "base_oid", "target_branch", "risk", "description", "idempotency_key")}
if values["repository"] != "aios/aios" or not OID_RE.fullmatch(values["base_oid"]): raise BootstrapError("TASK_BINDING_INVALID")
if not BRANCH_RE.fullmatch(values["target_branch"]): raise BootstrapError("TARGET_BRANCH_INVALID")
existing = self._read_existing(values["task_id"])
if existing is not None:
if any(getattr(existing, key) != values[key] for key in ("repository", "base_oid", "target_branch", "risk", "description")): raise BootstrapError("TASK_BINDING_CONFLICT")
if any(getattr(existing, key) != values[key] for key in ("repository", "base_oid", "target_branch", "risk", "description", "idempotency_key")): raise BootstrapError("TASK_BINDING_CONFLICT")
optional = {"expected_branch_oid": existing.expected_branch_oid, "branch_mode": existing.branch_mode, "generation": existing.generation}
for key, durable in optional.items():
if key in request and str(request[key]) != str(durable): raise BootstrapError("TASK_BINDING_CONFLICT")
return existing.response(idempotent=True)
if not self.authority.commit_exists(values["base_oid"]): raise BootstrapError("BASE_COMMIT_NOT_FOUND")
current = self.authority.read_ref("refs/heads/" + values["target_branch"])
@@ -74,8 +207,24 @@ class GovernedTaskBootstrap:
if expected != values["base_oid"]: raise BootstrapError("EXPECTED_BASE_CONFLICT")
else:
if expected != current: raise BootstrapError("EXISTING_BRANCH_OID_CONFLICT")
mode, generation = "CONTINUE_EXISTING", int(request.get("generation", 2))
if generation < 1: raise BootstrapError("GENERATION_INVALID")
parent_task_id = str(request.get("parent_task_id") or "")
parent_job_id = str(request.get("parent_job_id") or "")
if not parent_task_id or not parent_job_id:
raise BootstrapError("EXISTING_BRANCH_OWNERSHIP_REQUIRED")
parent = self._read_existing(parent_task_id)
if parent is None or parent.job_id != parent_job_id or parent.repository != values["repository"] or parent.target_branch != values["target_branch"]:
raise BootstrapError("EXISTING_BRANCH_OWNERSHIP_CONFLICT")
store, _ = self._store(parent_task_id)
with store._connect() as connection:
rows = connection.execute(
"SELECT current_head_oid,generation,state FROM branch_leases WHERE job_id=? AND branch=? AND state='ACTIVE' ORDER BY revision DESC",
(parent_job_id, values["target_branch"]),
).fetchall()
if len(rows) != 1 or str(rows[0]["current_head_oid"] or "") != current:
raise BootstrapError("EXISTING_BRANCH_OWNERSHIP_CONFLICT")
mode, generation = "CONTINUE_EXISTING", int(rows[0]["generation"]) + 1
if "generation" in request and int(request["generation"]) != generation:
raise BootstrapError("GENERATION_BINDING_CONFLICT")
payload = json.dumps(values, sort_keys=True, separators=(",", ":")).encode()
_, controller = self._store(values["task_id"])
receipt = controller.submit({"idempotency_key": values["idempotency_key"], "campaign_id": values["task_id"], "task_id": values["task_id"], "repository": values["repository"], "operation": "GOVERNED_TASK_CREATE", "branch": values["target_branch"], "expected_branch_oid": expected, "payload_digest": hashlib.sha256(payload).hexdigest(), "lineage_anchor_oid": values["base_oid"]}, payload=payload)
@@ -0,0 +1,306 @@
"""Uncredentialed deterministic executor for governed Online Agent changes.
This process never receives Agent/Owner/Bridge credentials and never writes Git refs.
It operates only on an exchange bundle and returns hash-bound changed files + test evidence.
"""
from __future__ import annotations
import base64
from dataclasses import dataclass
import errno
import hashlib
import io
import json
import os
from pathlib import Path
import re
import shutil
import socket
import subprocess
import tarfile
import tempfile
import time
from typing import Any, Mapping, Sequence
ALLOWED_PROGRAMS = {"/usr/bin/python3", "/usr/bin/git", "/bin/true", "/usr/bin/true"}
MAX_ARCHIVE_BYTES = 64 * 1024 * 1024
MAX_FILE_BYTES = 4 * 1024 * 1024
SECRET_ENV_TOKENS = ("TOKEN", "SECRET", "PASSWORD", "CREDENTIAL", "AUTHORIZATION", "PRIVATE_KEY")
REQUEST_ID_RE = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._:-]{15,254}$")
SENSITIVE_VALUE_RE = re.compile(
r"(?i)(authorization\s*[:=]\s*bearer\s+\S+|authorization\s+bearer\s+\S+|"
r"(?:password|passwd|secret|token|api[_-]?key)\s*[:=]\s*\S+)"
)
class IsolatedExecutorError(RuntimeError):
pass
def sha256(data: bytes) -> str:
return hashlib.sha256(data).hexdigest()
def canonical(value: object) -> bytes:
return json.dumps(value, sort_keys=True, separators=(",", ":"), ensure_ascii=False).encode("utf-8")
def safe_relative(value: object) -> str:
if not isinstance(value, str) or not value or value.startswith("/"):
raise IsolatedExecutorError("PATH_INVALID")
path = Path(value)
if ".." in path.parts or "." in path.parts or any(part in {"", ".git"} for part in path.parts):
raise IsolatedExecutorError("PATH_INVALID")
return path.as_posix()
def resolve_under(root: Path, relative: str) -> Path:
root = root.resolve()
path = (root / safe_relative(relative)).resolve(strict=False)
if path == root or root not in path.parents:
raise IsolatedExecutorError("PATH_ESCAPE")
return path
def _credential_exposure(directory: Path) -> str | None:
"""Return an exposure code; EACCES is the intended credential isolation proof."""
try:
if not directory.exists():
return None
if directory.is_symlink():
return "CREDENTIAL_MOUNT_SYMLINK"
if os.access(directory, os.R_OK):
return "CREDENTIAL_MOUNT_ACCESSIBLE"
if directory.is_dir():
for child in directory.iterdir():
if child.is_file() and os.access(child, os.R_OK):
return "CREDENTIAL_FILE_ACCESSIBLE"
except PermissionError:
return None
return None
def assert_uncredentialed() -> None:
credentials = Path("/run/credentials")
candidates = {credentials}
credentials_directory = Path(os.environ.get("CREDENTIALS_DIRECTORY", "/run/credentials"))
candidates.add(credentials_directory)
leaked = sorted(
key for key, value in os.environ.items()
if value
and (any(token in key.upper() for token in SECRET_ENV_TOKENS) or SENSITIVE_VALUE_RE.search(str(value)))
)
if leaked:
raise IsolatedExecutorError("CREDENTIAL_ENV_EXPOSED:" + ",".join(leaked))
for directory in candidates:
exposure = _credential_exposure(directory)
if exposure is not None:
raise IsolatedExecutorError(exposure)
# In the intended PrivateNetwork namespace host loopback services are unreachable.
try:
probe = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
except OSError as exc:
if exc.errno in {errno.EACCES, errno.EPERM, errno.EAFNOSUPPORT}:
return
raise IsolatedExecutorError("NETWORK_ISOLATION_PROBE_FAILED") from exc
probe.settimeout(0.15)
try:
if probe.connect_ex(("127.0.0.1", 18892)) == 0:
raise IsolatedExecutorError("GOVERNED_BRIDGE_REACHABLE")
finally:
probe.close()
def extract_source(archive: Path, root: Path) -> None:
if archive.is_symlink() or not archive.is_file() or archive.stat().st_size > MAX_ARCHIVE_BYTES:
raise IsolatedExecutorError("SOURCE_ARCHIVE_INVALID")
with tarfile.open(archive, "r:gz") as tf:
for member in tf.getmembers():
path = Path(member.name)
if path.is_absolute() or ".." in path.parts or ".git" in path.parts:
raise IsolatedExecutorError("SOURCE_ARCHIVE_PATH_INVALID")
if member.issym() or member.islnk():
raise IsolatedExecutorError("SOURCE_ARCHIVE_LINK_DENY")
tf.extractall(root, filter="data")
def run_command(item: Mapping[str, Any], root: Path) -> dict[str, Any]:
argv = item.get("argv")
if not isinstance(argv, Sequence) or isinstance(argv, (str, bytes)) or not argv:
raise IsolatedExecutorError("COMMAND_INVALID")
argv = [str(part) for part in argv]
if argv[0] not in ALLOWED_PROGRAMS:
raise IsolatedExecutorError("COMMAND_PROGRAM_DENY")
if argv[0] == "/usr/bin/python3" and "-m" in argv:
index = argv.index("-m")
if index + 1 >= len(argv) or argv[index + 1] not in {"pytest", "py_compile"}:
raise IsolatedExecutorError("PYTHON_MODULE_DENY")
if argv[0] == "/usr/bin/git" and any(
token in {"push", "update-ref", "commit", "merge", "rebase", "reset", "clean", "branch"}
for token in argv[1:]
):
raise IsolatedExecutorError("GIT_MUTATION_DENY")
timeout = int(item.get("timeout_seconds") or 300)
if not 1 <= timeout <= 1800:
raise IsolatedExecutorError("COMMAND_TIMEOUT_INVALID")
cwd = root if item.get("cwd", ".") == "." else resolve_under(root, str(item.get("cwd")))
started = time.monotonic()
completed = subprocess.run(
argv,
cwd=cwd,
capture_output=True,
text=True,
check=False,
timeout=timeout,
env={"PATH": "/usr/bin:/bin", "LC_ALL": "C.UTF-8"},
)
result = {
"label": str(item.get("label") or "command"),
"exit_code": completed.returncode,
"stdout_sha256": sha256(completed.stdout.encode("utf-8", "replace")),
"stderr_sha256": sha256(completed.stderr.encode("utf-8", "replace")),
"duration_ms": int((time.monotonic() - started) * 1000),
}
if completed.returncode != 0:
raise IsolatedExecutorError(
"COMMAND_FAILED:" + result["label"] + ":" + str(completed.returncode)
)
return result
def apply_changes(request: Mapping[str, Any], root: Path) -> list[str]:
changes = request.get("change_set")
if not isinstance(changes, list) or not changes:
raise IsolatedExecutorError("CHANGE_SET_INVALID")
changed: list[str] = []
for change in changes:
if not isinstance(change, Mapping):
raise IsolatedExecutorError("CHANGE_INVALID")
relative = safe_relative(change.get("path"))
path = resolve_under(root, relative)
current = path.read_bytes() if path.is_file() else None
expected = change.get("expected_sha256")
if expected is not None and (current is None or sha256(current) != expected):
raise IsolatedExecutorError("SOURCE_DRIFT:" + relative)
operation = change.get("operation")
if operation == "write_file":
content = change.get("content")
if not isinstance(content, str):
raise IsolatedExecutorError("WRITE_CONTENT_INVALID")
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(content, encoding="utf-8")
elif operation == "replace_text":
if current is None:
raise IsolatedExecutorError("REPLACE_SOURCE_MISSING")
old = str(change.get("old"))
new = str(change.get("new"))
count = int(change.get("count", 1))
text = current.decode("utf-8")
if text.count(old) != count:
raise IsolatedExecutorError("REPLACE_COUNT_INVALID:" + relative)
path.write_text(text.replace(old, new), encoding="utf-8")
elif operation == "delete_file":
if path.exists():
path.unlink()
else:
raise IsolatedExecutorError("CHANGE_OPERATION_INVALID")
changed.append(relative)
return sorted(set(changed))
def changed_bundle(root: Path, changed: list[str]) -> list[dict[str, Any]]:
files = []
for relative in changed:
path = resolve_under(root, relative)
if path.exists():
data = path.read_bytes()
if len(data) > MAX_FILE_BYTES:
raise IsolatedExecutorError("CHANGED_FILE_TOO_LARGE:" + relative)
files.append({
"path": relative,
"deleted": False,
"sha256": sha256(data),
"content_b64": base64.b64encode(data).decode("ascii"),
})
else:
files.append({"path": relative, "deleted": True, "sha256": None, "content_b64": None})
return files
def execute_request(request_path: Path, result_path: Path) -> None:
assert_uncredentialed()
request = json.loads(request_path.read_text(encoding="utf-8"))
if request.get("schema") != "aios.isolated-execution.request.v1":
raise IsolatedExecutorError("REQUEST_SCHEMA_INVALID")
request_id = request.get("request_id")
if not isinstance(request_id, str) or not REQUEST_ID_RE.fullmatch(request_id):
raise IsolatedExecutorError("REQUEST_ID_INVALID")
source_archive = Path(str(request.get("source_archive") or ""))
if not source_archive.is_absolute():
raise IsolatedExecutorError("SOURCE_ARCHIVE_PATH_INVALID")
expected_archive_sha = str(request.get("source_archive_sha256") or "")
if sha256(source_archive.read_bytes()) != expected_archive_sha:
raise IsolatedExecutorError("SOURCE_ARCHIVE_SHA_MISMATCH")
with tempfile.TemporaryDirectory(prefix="aios-isolated-executor-") as temporary:
root = Path(temporary) / "workspace"
root.mkdir()
extract_source(source_archive, root)
changed = apply_changes(request, root)
commands = [
run_command(item, root)
for item in request.get("verification", [])
if isinstance(item, Mapping)
]
result = {
"schema": "aios.isolated-execution.result.v1",
"request_id": request_id,
"request_sha256": sha256(canonical(request)),
"changed_files": changed_bundle(root, changed),
"verification": commands,
"credential_isolation": {
"credential_env_absent": True,
"credential_mount_inaccessible": True,
"governed_bridge_unreachable": True,
},
}
result["result_sha256"] = sha256(canonical(result))
temporary_result = result_path.with_suffix(".tmp")
temporary_result.write_text(json.dumps(result, sort_keys=True, separators=(",", ":")), encoding="utf-8")
os.replace(temporary_result, result_path)
def serve(root: Path) -> None:
assert_uncredentialed()
requests = root / "requests"
results = root / "results"
failed = root / "failed"
for path in (requests, results, failed):
path.mkdir(parents=True, exist_ok=True)
while True:
for request in sorted(requests.glob("*.json")):
if request.is_symlink():
continue
result = results / request.name
try:
execute_request(request, result)
request.unlink()
except Exception as exc:
(failed / request.name).write_text(
json.dumps({"error": str(exc) or type(exc).__name__}, sort_keys=True),
encoding="utf-8",
)
request.unlink(missing_ok=True)
time.sleep(0.5)
if __name__ == "__main__":
import argparse
parser = argparse.ArgumentParser()
parser.add_argument("--root", default="/var/lib/aios-online-agent-executor")
parser.add_argument("--request")
parser.add_argument("--result")
args = parser.parse_args()
if args.request and args.result:
execute_request(Path(args.request), Path(args.result))
else:
serve(Path(args.root))
@@ -0,0 +1,397 @@
"""Canonical governed execution projection for Online Agent Tasks.
The durable governed binding is authoritative. Natural-language task fields and
worker configuration are never allowed to invent repository, ref, generation, or
idempotency coordinates.
"""
from __future__ import annotations
from dataclasses import asdict, dataclass
import hashlib
import json
import re
from pathlib import Path
from typing import Any, Mapping, Sequence
import urllib.error
import urllib.request
EXECUTION_SCHEMA = "aios.online-agent.execution.v2"
PROJECTION_SCHEMA = "aios.online-agent.task-projection.v1"
OID_RE = re.compile(r"^[0-9a-f]{40}$")
TASK_RE = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._:-]{2,179}$")
BRANCH_RE = re.compile(r"^agent/[A-Za-z0-9][A-Za-z0-9._/-]{2,179}$")
KEY_RE = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._:-]{7,159}$")
PROTECTED_PREFIXES = ("main", "master", "refs/", "tags/", "tag/", "release/", "baseline/", "production/")
MUTATION_ACTIONS = frozenset({"deterministic"})
SUPPORTED_ACTIONS = frozenset({"deterministic", "source_capture"})
class ExecutionProjectionError(RuntimeError):
"""A canonical fail-closed projection or validation error."""
class OnlineTaskProjectionClient:
"""Official HTTP projection client using only a control credential reference."""
def __init__(self, api_base: str, control_token_path: Path) -> None:
self.api_base = api_base.rstrip("/")
self.control_token_path = Path(control_token_path)
def _token(self) -> str:
path = self.control_token_path
if path.is_symlink() or not path.is_file():
_error("ONLINE_TASK_PROJECTION_CONTROL_UNAVAILABLE")
value = path.read_text(encoding="utf-8").strip()
if len(value) < 32:
_error("ONLINE_TASK_PROJECTION_CONTROL_INVALID")
return value
def create(
self,
projected_task: Mapping[str, Any],
*,
binding: "GovernedExecutionBinding",
) -> dict[str, Any]:
body = dict(projected_task)
payload = _canonical(body).encode("utf-8")
request = urllib.request.Request(
self.api_base + "/v1/tasks",
method="POST",
data=payload,
headers={
"Authorization": "Bearer " + self._token(),
"Content-Type": "application/json",
"Accept": "application/json",
},
)
try:
with urllib.request.urlopen(request, timeout=15) as response:
result = json.loads(response.read().decode("utf-8"))
status = response.status
except urllib.error.HTTPError as exc:
raw = exc.read(8192).decode("utf-8", "replace")
try:
error = json.loads(raw).get("error")
except (AttributeError, json.JSONDecodeError):
error = "HTTP_ERROR"
raise ExecutionProjectionError(f"ONLINE_TASK_PROJECTION_FAILED:{exc.code}:{error}") from exc
except (OSError, urllib.error.URLError, TimeoutError) as exc:
raise ExecutionProjectionError("ONLINE_TASK_PROJECTION_UNAVAILABLE") from exc
if status not in {200, 201} or not isinstance(result, dict):
_error("ONLINE_TASK_PROJECTION_RESPONSE_INVALID")
if result.get("task_id") != binding.task_id or result.get("status") not in {"QUEUED", "VERIFYING", "SUCCEEDED"}:
_error("ONLINE_TASK_PROJECTION_RESPONSE_INVALID")
metadata = result.get("metadata")
if not isinstance(metadata, Mapping):
_error("ONLINE_TASK_PROJECTION_RESPONSE_INVALID")
returned_execution = validate_execution_envelope(
_object(metadata.get("execution"), "ONLINE_TASK_PROJECTION_RESPONSE_INVALID"),
binding=binding,
)
submitted_metadata = _object(body.get("metadata"), "ONLINE_TASK_PROJECTION_REQUEST_INVALID")
submitted_execution = validate_execution_envelope(
_object(submitted_metadata.get("execution"), "ONLINE_TASK_PROJECTION_REQUEST_INVALID"),
binding=binding,
)
if _canonical(returned_execution) != _canonical(submitted_execution):
_error("ONLINE_TASK_PROJECTION_RESPONSE_BINDING_INVALID")
return result
def _error(code: str) -> None:
raise ExecutionProjectionError(code)
def _object(value: object, code: str) -> dict[str, Any]:
if not isinstance(value, Mapping):
_error(code)
return dict(value)
def _string(value: object, code: str) -> str:
if not isinstance(value, str) or not value.strip():
_error(code)
return value.strip()
def _task_id(value: object) -> str:
value = _string(value, "TASK_ID_INVALID")
if TASK_RE.fullmatch(value) is None:
_error("TASK_ID_INVALID")
return value
def _oid(value: object, code: str) -> str:
value = _string(value, code)
if OID_RE.fullmatch(value) is None:
_error(code)
return value
def _branch(value: object) -> str:
value = _string(value, "TARGET_BRANCH_INVALID")
if BRANCH_RE.fullmatch(value) is None or value.startswith(PROTECTED_PREFIXES):
_error("TARGET_BRANCH_INVALID")
return value
def _generation(value: object) -> int:
if not isinstance(value, int) or isinstance(value, bool) or value < 1:
_error("GENERATION_INVALID")
return value
def _canonical(value: object) -> str:
return json.dumps(value, ensure_ascii=False, sort_keys=True, separators=(",", ":"))
def _digest(value: object) -> str:
return hashlib.sha256(_canonical(value).encode("utf-8")).hexdigest()
def _source_signals(value: object) -> tuple[str, ...]:
"""Normalize untrusted natural-language hints before they enter a signed envelope."""
if value is None:
return ()
if not isinstance(value, Sequence) or isinstance(value, (str, bytes)):
_error("SOURCE_CAPTURE_SIGNALS_INVALID")
result: list[str] = []
for item in value:
if not isinstance(item, str):
_error("SOURCE_CAPTURE_SIGNALS_INVALID")
text = " ".join(item.split()).strip()
if not text:
continue
# Projection may accept long task prose, but execution envelopes stay bounded.
text = text[:512].rstrip()
if text not in result:
result.append(text)
if len(result) == 16:
break
return tuple(result)
def _capabilities(value: object) -> tuple[str, ...]:
if value is None:
return ()
if not isinstance(value, Sequence) or isinstance(value, (str, bytes)):
_error("CAPABILITY_SCOPE_INVALID")
result = tuple(sorted({_string(item, "CAPABILITY_SCOPE_INVALID") for item in value}))
if any(len(item) > 120 for item in result):
_error("CAPABILITY_SCOPE_INVALID")
return result
@dataclass(frozen=True, slots=True)
class GovernedExecutionBinding:
task_id: str
job_id: str
repository: str
base_oid: str
expected_branch_oid: str
target_branch: str
branch_mode: str
generation: int
idempotency_key: str
@classmethod
def from_mapping(cls, value: Mapping[str, Any]) -> "GovernedExecutionBinding":
data = dict(value)
task_id = _task_id(data.get("task_id"))
job_id = _string(data.get("job_id"), "JOB_ID_REQUIRED")
if TASK_RE.fullmatch(job_id) is None:
_error("JOB_ID_INVALID")
repository = _string(data.get("repository"), "REPOSITORY_INVALID")
if repository != "aios/aios":
_error("WRONG_REPOSITORY_ENVELOPE_DENY")
base_oid = _oid(data.get("base_oid"), "BASE_OID_INVALID")
expected = _oid(data.get("expected_branch_oid"), "EXPECTED_BRANCH_OID_INVALID")
target = _branch(data.get("target_branch"))
mode = _string(data.get("branch_mode"), "BRANCH_MODE_MISSING")
if mode not in {"CREATE_NEW", "CONTINUE_EXISTING"}:
_error("BRANCH_MODE_INVALID")
generation = _generation(data.get("generation"))
if mode == "CREATE_NEW" and expected != base_oid:
_error("EXPECTED_BASE_CONFLICT")
key = _string(data.get("idempotency_key"), "IDEMPOTENCY_KEY_INVALID")
if KEY_RE.fullmatch(key) is None:
_error("IDEMPOTENCY_KEY_INVALID")
return cls(task_id, job_id, repository, base_oid, expected, target, mode, generation, key)
def public(self) -> dict[str, Any]:
return asdict(self)
def binding_from_bootstrap(value: Mapping[str, Any]) -> GovernedExecutionBinding:
return GovernedExecutionBinding.from_mapping(value)
def validate_execution_envelope(
value: Mapping[str, Any],
*,
binding: GovernedExecutionBinding | None = None,
worker_capabilities: Sequence[str] | None = None,
) -> dict[str, Any]:
envelope = dict(value)
if envelope.get("schema") != EXECUTION_SCHEMA:
_error("EXECUTION_ENVELOPE_SCHEMA_VALIDATION")
candidate = GovernedExecutionBinding.from_mapping(envelope)
if binding is not None and candidate != binding:
comparisons = (
(candidate.task_id, binding.task_id, "TASK_BINDING_CONFLICT"),
(candidate.job_id, binding.job_id, "JOB_BINDING_CONFLICT"),
(candidate.repository, binding.repository, "WRONG_REPOSITORY_ENVELOPE_DENY"),
(candidate.base_oid, binding.base_oid, "WRONG_BASE_ENVELOPE_DENY"),
(candidate.expected_branch_oid, binding.expected_branch_oid, "WRONG_EXPECTED_HEAD_ENVELOPE_DENY"),
(candidate.target_branch, binding.target_branch, "WRONG_BRANCH_ENVELOPE_DENY"),
(candidate.generation, binding.generation, "WRONG_GENERATION_ENVELOPE_DENY"),
(candidate.branch_mode, binding.branch_mode, "BRANCH_MODE_BINDING_CONFLICT"),
(candidate.idempotency_key, binding.idempotency_key, "IDEMPOTENCY_BINDING_CONFLICT"),
)
for actual, expected, code in comparisons:
if actual != expected:
_error(code)
action = _string(envelope.get("action"), "EXECUTION_ACTION_MISSING")
if action not in SUPPORTED_ACTIONS:
_error("EXECUTION_ACTION_UNSUPPORTED")
capability_scope = _capabilities(envelope.get("capability_scope"))
if worker_capabilities is not None and not set(capability_scope).issubset(set(worker_capabilities)):
_error("CAPABILITY_MISMATCH_DENY")
workspace = _object(envelope.get("workspace"), "WORKSPACE_POLICY_REQUIRED")
if workspace.get("authority_git_dir") != "/data/aios-gitops/repo.git" or workspace.get("read_only_repository") is not True:
_error("WORKSPACE_POLICY_DENY")
governance = _object(envelope.get("governance"), "GOVERNANCE_POLICY_REQUIRED")
governed_coordinates = (
(governance.get("task_id"), candidate.task_id, "TASK_BINDING_CONFLICT"),
(governance.get("job_id"), candidate.job_id, "JOB_BINDING_CONFLICT"),
(governance.get("idempotency_key"), candidate.idempotency_key, "IDEMPOTENCY_BINDING_CONFLICT"),
)
for actual_coordinate, expected_coordinate, code in governed_coordinates:
if actual_coordinate != expected_coordinate:
_error(code)
commit_key = _string(governance.get("commit_idempotency_key"), "COMMIT_IDEMPOTENCY_KEY_INVALID")
if KEY_RE.fullmatch(commit_key) is None:
_error("COMMIT_IDEMPOTENCY_KEY_INVALID")
if governance.get("writer_lease") is not (action in MUTATION_ACTIONS):
_error("WRITER_LEASE_POLICY_DENY")
for field in ("main_write", "production_write", "tag_write", "baseline_write", "force_push", "protected_ref_bypass"):
if governance.get(field, False) is not False:
_error("POLICY_DENY")
changes = envelope.get("change_set")
verification = envelope.get("verification")
if not isinstance(changes, list) or not isinstance(verification, list):
_error("EXECUTION_OPERATIONS_INVALID")
if action == "source_capture":
if changes or verification:
_error("SOURCE_CAPTURE_MUTATION_DENY")
signals = list(_source_signals(envelope.get("source_signals") or []))
if envelope.get("source_signals") != signals:
_error("SOURCE_CAPTURE_SIGNALS_INVALID")
elif not changes:
_error("CHANGE_SET_INVALID")
declared = str(envelope.get("execution_digest") or "")
material = {key: item for key, item in envelope.items() if key != "execution_digest"}
actual = _digest(material)
if declared != actual:
_error("EXECUTION_DIGEST_INVALID")
return {**material, "execution_digest": actual}
def project_execution_envelope(
binding: GovernedExecutionBinding,
execution_request: Mapping[str, Any] | None = None,
*,
required_capabilities: Sequence[str] = (),
) -> dict[str, Any]:
request = dict(execution_request or {})
action = str(request.get("action") or "source_capture")
if action not in SUPPORTED_ACTIONS:
_error("EXECUTION_ACTION_UNSUPPORTED")
capabilities = _capabilities(request.get("capability_scope", required_capabilities))
governance = {
"task_id": binding.task_id,
"job_id": binding.job_id,
"idempotency_key": binding.idempotency_key,
"commit_idempotency_key": str(request.get("commit_idempotency_key") or f"{binding.task_id}-commit-g{binding.generation}"),
"writer_lease": action in MUTATION_ACTIONS,
"main_write": False,
"production_write": False,
"tag_write": False,
"baseline_write": False,
"force_push": False,
"protected_ref_bypass": False,
}
supplied_governance = request.get("governance")
if supplied_governance is not None:
governance.update(_object(supplied_governance, "GOVERNANCE_POLICY_REQUIRED"))
material: dict[str, Any] = {
"schema": EXECUTION_SCHEMA,
**binding.public(),
"action": action,
"adapter": str(request.get("adapter") or ("deterministic" if action == "deterministic" else "source_capture")),
"capability_scope": list(capabilities),
"change_set": list(request.get("change_set") or []),
"verification": list(request.get("verification") or []),
"source_signals": list(_source_signals(request.get("source_signals") or [])) if action == "source_capture" else [],
"governance": governance,
"workspace": {
"authority_git_dir": "/data/aios-gitops/repo.git",
"read_only_repository": True,
"cleanup_policy": "always",
},
}
envelope = {**material, "execution_digest": _digest(material)}
return validate_execution_envelope(envelope, binding=binding)
def project_online_task(
binding: GovernedExecutionBinding,
task_request: Mapping[str, Any],
execution_request: Mapping[str, Any] | None = None,
) -> dict[str, Any]:
request = dict(task_request)
required = _capabilities(request.get("required_capabilities"))
execution = project_execution_envelope(binding, execution_request, required_capabilities=required)
metadata = dict(request.get("metadata") or {})
if "execution" in metadata:
validate_execution_envelope(_object(metadata["execution"], "EXECUTION_ENVELOPE_INVALID"), binding=binding)
if metadata["execution"] != execution:
_error("EXECUTION_PROJECTION_CONFLICT")
metadata["execution"] = execution
metadata["execution_projection"] = {
"schema": PROJECTION_SCHEMA,
"task_id": binding.task_id,
"job_id": binding.job_id,
"execution_digest": execution["execution_digest"],
}
return {
**request,
"task_id": binding.task_id,
"metadata": metadata,
"required_capabilities": list(required),
"idempotency_key": str(request.get("idempotency_key") or f"online-task:{binding.idempotency_key}"),
}
def resolve_task_execution(
task: Mapping[str, Any],
binding: GovernedExecutionBinding,
*,
worker_capabilities: Sequence[str] | None = None,
) -> tuple[dict[str, Any], str]:
metadata = task.get("metadata")
if isinstance(metadata, Mapping) and "execution" in metadata:
value = validate_execution_envelope(
_object(metadata["execution"], "EXECUTION_ENVELOPE_INVALID"),
binding=binding,
worker_capabilities=worker_capabilities,
)
return value, "persisted"
projected = project_execution_envelope(
binding,
{"action": "source_capture", "capability_scope": task.get("required_capabilities") or []},
required_capabilities=task.get("required_capabilities") or [],
)
validate_execution_envelope(projected, binding=binding, worker_capabilities=worker_capabilities)
return projected, "recovered_projection"
@@ -0,0 +1,187 @@
"""Durable, authority-bound deterministic execution plans for Online Agent Tasks."""
from __future__ import annotations
from contextlib import contextmanager
from dataclasses import dataclass
import fcntl
import hashlib
import json
from pathlib import Path
from typing import Any, Mapping
from aios_online_agent_execution import (
GovernedExecutionBinding,
project_execution_envelope,
)
class ExecutionPlanError(RuntimeError):
pass
def _canonical(value: object) -> bytes:
return json.dumps(value, sort_keys=True, separators=(",", ":"), ensure_ascii=False).encode("utf-8")
def _digest(value: object) -> str:
return hashlib.sha256(_canonical(value)).hexdigest()
def _task_key(task_id: str) -> str:
readable = "".join(ch if ch.isalnum() or ch in "._-" else "-" for ch in task_id).strip("-")[:72] or "task"
return readable + "-" + hashlib.sha256(task_id.encode("utf-8")).hexdigest()[:24]
@dataclass(frozen=True)
class ExecutionPlanRecord:
task_id: str
job_id: str
source_head_oid: str
generation: int
idempotency_key: str
plan_digest: str
execution: dict[str, Any]
def public(self) -> dict[str, Any]:
return {
"schema": "aios.online-agent.plan-record.v1",
"task_id": self.task_id,
"job_id": self.job_id,
"source_head_oid": self.source_head_oid,
"generation": self.generation,
"idempotency_key": self.idempotency_key,
"plan_digest": self.plan_digest,
"execution": dict(self.execution),
}
class ExecutionPlanStore:
def __init__(self, root: Path) -> None:
self.root = Path(root)
self.root.mkdir(parents=True, exist_ok=True)
def _path(self, task_id: str) -> Path:
return self.root / (_task_key(task_id) + ".json")
@contextmanager
def _lock(self, task_id: str):
path = self.root / (_task_key(task_id) + ".lock")
with path.open("a+b") as handle:
fcntl.flock(handle.fileno(), fcntl.LOCK_EX)
try:
yield
finally:
fcntl.flock(handle.fileno(), fcntl.LOCK_UN)
@staticmethod
def _source_head(binding: GovernedExecutionBinding) -> str:
return binding.expected_branch_oid if binding.branch_mode == "CONTINUE_EXISTING" else binding.base_oid
def _read(self, task_id: str) -> ExecutionPlanRecord | None:
path = self._path(task_id)
if not path.is_file() or path.is_symlink():
return None
value = json.loads(path.read_text(encoding="utf-8"))
if not isinstance(value, Mapping) or value.get("schema") != "aios.online-agent.plan-record.v1":
raise ExecutionPlanError("EXECUTION_PLAN_RECORD_INVALID")
execution = value.get("execution")
if not isinstance(execution, Mapping):
raise ExecutionPlanError("EXECUTION_PLAN_RECORD_INVALID")
return ExecutionPlanRecord(
task_id=str(value.get("task_id") or ""),
job_id=str(value.get("job_id") or ""),
source_head_oid=str(value.get("source_head_oid") or ""),
generation=int(value.get("generation") or 0),
idempotency_key=str(value.get("idempotency_key") or ""),
plan_digest=str(value.get("plan_digest") or ""),
execution=dict(execution),
)
def register(
self,
binding: GovernedExecutionBinding,
request: Mapping[str, Any],
) -> dict[str, Any]:
with self._lock(binding.task_id):
return self._register_locked(binding, request)
def _register_locked(
self,
binding: GovernedExecutionBinding,
request: Mapping[str, Any],
) -> dict[str, Any]:
if request.get("schema") != "aios.online-agent.plan.v1":
raise ExecutionPlanError("EXECUTION_PLAN_SCHEMA_INVALID")
if request.get("task_id") != binding.task_id or request.get("job_id") != binding.job_id:
raise ExecutionPlanError("EXECUTION_PLAN_BINDING_CONFLICT")
if int(request.get("generation") or 0) != binding.generation:
raise ExecutionPlanError("EXECUTION_PLAN_GENERATION_CONFLICT")
source_head = str(request.get("source_head_oid") or "")
if source_head != self._source_head(binding):
raise ExecutionPlanError("EXECUTION_PLAN_SOURCE_HEAD_CONFLICT")
key = str(request.get("idempotency_key") or "")
if len(key) < 8:
raise ExecutionPlanError("EXECUTION_PLAN_IDEMPOTENCY_INVALID")
execution_request = request.get("execution")
if not isinstance(execution_request, Mapping) or execution_request.get("action") != "deterministic":
raise ExecutionPlanError("EXECUTION_PLAN_DETERMINISTIC_REQUIRED")
envelope = project_execution_envelope(binding, execution_request)
material = {
"task_id": binding.task_id,
"job_id": binding.job_id,
"source_head_oid": source_head,
"generation": binding.generation,
"idempotency_key": key,
"execution": dict(execution_request),
}
plan_digest = _digest(material)
existing = self._read(binding.task_id)
expected_previous = request.get("expected_previous_plan_digest")
if existing is not None:
if existing.idempotency_key == key:
if existing.plan_digest != plan_digest:
raise ExecutionPlanError("EXECUTION_PLAN_IDEMPOTENCY_CONFLICT")
return {**existing.public(), "idempotent": True, "execution_digest": envelope["execution_digest"]}
if expected_previous != existing.plan_digest:
raise ExecutionPlanError("EXECUTION_PLAN_REPLACEMENT_CAS_MISMATCH")
if existing.source_head_oid != source_head:
raise ExecutionPlanError("EXECUTION_PLAN_STALE_REPLACEMENT_DENY")
elif expected_previous not in (None, ""):
raise ExecutionPlanError("EXECUTION_PLAN_REPLACEMENT_CAS_MISMATCH")
record = ExecutionPlanRecord(
task_id=binding.task_id,
job_id=binding.job_id,
source_head_oid=source_head,
generation=binding.generation,
idempotency_key=key,
plan_digest=plan_digest,
execution=dict(execution_request),
)
path = self._path(binding.task_id)
temporary = path.with_suffix(".tmp")
temporary.write_text(
json.dumps(record.public(), sort_keys=True, separators=(",", ":")),
encoding="utf-8",
)
temporary.replace(path)
return {**record.public(), "idempotent": False, "execution_digest": envelope["execution_digest"]}
def resolve(self, binding: GovernedExecutionBinding) -> dict[str, Any] | None:
record = self._read(binding.task_id)
if record is None:
return None
if (
record.task_id != binding.task_id
or record.job_id != binding.job_id
or record.generation != binding.generation
):
raise ExecutionPlanError("EXECUTION_PLAN_BINDING_CONFLICT")
if record.source_head_oid != self._source_head(binding):
raise ExecutionPlanError("EXECUTION_PLAN_SOURCE_HEAD_DRIFT")
envelope = project_execution_envelope(binding, record.execution)
return {
"execution": dict(record.execution),
"plan_digest": record.plan_digest,
"execution_digest": envelope["execution_digest"],
}
File diff suppressed because it is too large Load Diff
@@ -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,
@@ -0,0 +1,5 @@
[Service]
LoadCredential=agent_task_control_token:/etc/aios-gitops/agent-task-control.token
Environment=AIOS_AGENT_TASK_API_URL=http://127.0.0.1:18940
Environment=AIOS_ONLINE_TASK_PROJECTION_ENABLED=1
Environment=AIOS_AGENT_TASK_CONTROL_TOKEN_FILE=%d/agent_task_control_token
@@ -0,0 +1,33 @@
[Unit]
Description=AIOS uncredentialed deterministic Planner/Executor
After=local-fs.target
[Service]
Type=simple
User=aios-engineering-executor
Group=aios-executor-exchange
ExecStart=/usr/bin/python3 -B /opt/aios/current/active-runtime/app/aios_isolated_executor.py --root /var/lib/aios-online-agent-executor
Restart=always
RestartSec=2
NoNewPrivileges=true
PrivateTmp=true
PrivateNetwork=true
ProtectSystem=strict
ProtectHome=true
ProtectKernelTunables=true
ProtectKernelModules=true
ProtectControlGroups=true
LockPersonality=true
RestrictSUIDSGID=true
RestrictRealtime=true
RestrictNamespaces=true
MemoryDenyWriteExecute=true
RestrictAddressFamilies=AF_UNIX AF_INET
IPAddressDeny=any
IPAddressAllow=localhost
InaccessiblePaths=/run/credentials
ReadWritePaths=/var/lib/aios-online-agent-executor
UMask=0027
[Install]
WantedBy=multi-user.target
@@ -0,0 +1,45 @@
[Unit]
Description=AIOS governed Online Agent Worker
After=aios-agent-task-api.service aios-chatgpt-bridge-v2-staging.service
Wants=aios-agent-task-api.service aios-chatgpt-bridge-v2-staging.service
[Service]
Type=simple
User=aios-engineering-worker
Group=aios-engineering-worker
SupplementaryGroups=gitops aios-executor-exchange
LoadCredentialEncrypted=agent_token:/etc/credstore.encrypted/aios-online-agent-worker-20260815/agent_token
Environment=PYTHONUNBUFFERED=1
Environment=AIOS_AGENT_TASK_API_URL=http://127.0.0.1:18940
Environment=AIOS_GOVERNED_WRITE_API_URL=http://127.0.0.1:18892
Environment=AIOS_AGENT_ID=online-engineering-worker
Environment=AIOS_AGENT_TYPE=trusted-online-engineering-worker
Environment=AIOS_AGENT_VERSION=2026-08-17.1-dynamic
Environment=AIOS_WORKER_CAPABILITIES=git.read,git.test,governed.write.branch,governed.write.file
Environment=AIOS_AUTHORITY_GIT_DIR=/data/aios-gitops/repo.git
Environment=AIOS_ISOLATED_EXECUTOR_ROOT=/var/lib/aios-online-agent-executor
Environment=AIOS_WORKER_STATE_ROOT=/var/lib/aios-online-agent-worker
Environment=AIOS_WORKER_WORKSPACE_ROOT=/var/lib/aios-online-agent-worker/workspaces
ExecStart=/usr/bin/python3 -B /opt/aios/current/active-runtime/app/aios_online_agent_worker.py
Restart=always
RestartSec=5
TimeoutStopSec=20
NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=strict
ProtectHome=true
ProtectKernelTunables=true
ProtectKernelModules=true
ProtectControlGroups=true
LockPersonality=true
RestrictSUIDSGID=true
RestrictRealtime=true
RestrictNamespaces=true
MemoryDenyWriteExecute=true
RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6
ReadOnlyPaths=/data/aios-gitops/repo.git
ReadWritePaths=/var/lib/aios-online-agent-worker
UMask=0027
[Install]
WantedBy=multi-user.target
@@ -0,0 +1,45 @@
[Unit]
Description=AIOS trusted dynamic engineering consumer
After=aios-agent-task-api.service aios-chatgpt-bridge-v2-staging.service
Wants=aios-agent-task-api.service aios-chatgpt-bridge-v2-staging.service
[Service]
Type=simple
User=aios-engineering-worker
Group=aios-engineering-worker
SupplementaryGroups=gitops aios-executor-exchange
LoadCredentialEncrypted=agent_token:/etc/credstore.encrypted/aios-online-engineering-20260815/agent_token
Environment=PYTHONUNBUFFERED=1
Environment=AIOS_AGENT_TASK_API_URL=http://127.0.0.1:18940
Environment=AIOS_GOVERNED_WRITE_API_URL=http://127.0.0.1:18892
Environment=AIOS_AGENT_ID=online-engineering-consumer
Environment=AIOS_AGENT_TYPE=trusted-online-engineering-worker
Environment=AIOS_AGENT_VERSION=2026-08-17.1-dynamic
Environment=AIOS_WORKER_CAPABILITIES=git.read,git.test,governed.write.branch,governed.write.file
Environment=AIOS_AUTHORITY_GIT_DIR=/data/aios-gitops/repo.git
Environment=AIOS_ISOLATED_EXECUTOR_ROOT=/var/lib/aios-online-agent-executor
Environment=AIOS_WORKER_STATE_ROOT=/var/lib/aios-online-engineering-consumer
Environment=AIOS_WORKER_WORKSPACE_ROOT=/var/lib/aios-online-engineering-consumer/workspaces
ExecStart=/usr/bin/python3 -B /opt/aios/current/active-runtime/app/aios_online_agent_worker.py
Restart=always
RestartSec=5
TimeoutStopSec=20
NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=strict
ProtectHome=true
ProtectKernelTunables=true
ProtectKernelModules=true
ProtectControlGroups=true
LockPersonality=true
RestrictSUIDSGID=true
RestrictRealtime=true
RestrictNamespaces=true
MemoryDenyWriteExecute=true
RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6
ReadOnlyPaths=/data/aios-gitops/repo.git
ReadWritePaths=/var/lib/aios-online-engineering-consumer
UMask=0027
[Install]
WantedBy=multi-user.target
@@ -0,0 +1,4 @@
g aios-executor-exchange -
u aios-engineering-executor - "AIOS isolated engineering executor" /var/lib/aios-online-agent-executor /usr/sbin/nologin
m aios-engineering-executor aios-executor-exchange
m aios-engineering-worker aios-executor-exchange
@@ -0,0 +1,5 @@
d /var/lib/aios-online-agent-executor 2770 aios-engineering-executor aios-executor-exchange -
d /var/lib/aios-online-agent-executor/requests 2770 aios-engineering-worker aios-executor-exchange -
d /var/lib/aios-online-agent-executor/results 2770 aios-engineering-executor aios-executor-exchange -
d /var/lib/aios-online-agent-executor/failed 2770 aios-engineering-executor aios-executor-exchange -
d /var/lib/aios-online-agent-executor/sources 2770 aios-engineering-worker aios-executor-exchange -
+311
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,206 @@ 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
/v2/execution-envelope:
post:
operationId: projectOnlineAgentExecution
summary: Project the canonical execution envelope for an existing governed task
description: Returns the durable task binding and canonical source_capture or deterministic projection.
security: [{bearerAuth: []}]
requestBody:
required: true
content:
application/json:
schema:
type: object
additionalProperties: false
required: [task_id]
properties:
task_id: {type: string, minLength: 3}
responses:
'200':
description: Canonical execution envelope
content:
application/json:
schema:
type: object
required: [schema, task_id, job_id, action, execution_digest]
properties:
schema: {type: string, enum: [aios.online-agent.execution.v2]}
task_id: {type: string}
job_id: {type: string}
action: {type: string, enum: [source_capture, deterministic]}
execution_digest: {type: string, pattern: '^[0-9a-f]{64}$'}
execution: {type: object, additionalProperties: true}
'400': {description: Missing or invalid task binding}
'409': {description: Canonical projection conflict}
x-openai-isConsequential: true
/v2/execution-plan:
post:
operationId: registerOnlineAgentExecutionPlan
summary: Register a deterministic plan for an existing governed task
description: Registers a non-empty deterministic change set and bounded verification plan against the exact source head.
security: [{bearerAuth: []}]
requestBody:
required: true
content:
application/json:
schema:
type: object
additionalProperties: false
required: [schema, task_id, job_id, source_head_oid, generation, idempotency_key, execution]
properties:
schema: {type: string, enum: [aios.online-agent.plan.v1]}
task_id: {type: string, minLength: 3}
job_id: {type: string, minLength: 8}
source_head_oid: {type: string, pattern: '^[0-9a-f]{40}$'}
generation: {type: integer, minimum: 1}
idempotency_key: {type: string, minLength: 8}
expected_previous_plan_digest: {type: string, pattern: '^[0-9a-f]{64}$'}
execution:
type: object
additionalProperties: false
required: [action, change_set, verification]
properties:
action: {type: string, const: deterministic}
adapter: {type: string}
capability_scope: {type: array, items: {type: string}}
change_set: {type: array, minItems: 1, maxItems: 128, items: {type: object, additionalProperties: true}}
verification: {type: array, maxItems: 128, items: {type: object, additionalProperties: true}}
responses:
'200':
description: Deterministic plan registered or returned idempotently
content:
application/json:
schema:
type: object
required: [schema, task_id, job_id, source_head_oid, generation, idempotency_key, plan_digest, execution]
properties:
schema: {type: string, enum: [aios-online-agent.plan-record.v1]}
task_id: {type: string}
job_id: {type: string}
source_head_oid: {type: string, pattern: '^[0-9a-f]{40}$'}
generation: {type: integer}
idempotency_key: {type: string}
plan_digest: {type: string, pattern: '^[0-9a-f]{64}$'}
execution: {type: object, additionalProperties: true}
execution_digest: {type: string, pattern: '^[0-9a-f]{64}$'}
'400': {description: Invalid deterministic plan or source head}
'409': {description: Binding, idempotency, or replacement CAS conflict}
x-openai-isConsequential: true
@@ -0,0 +1,134 @@
# Online Agent Automatic Claim Infrastructure
## Status and fixed boundary
This candidate targets `p0-claim-online-agent-auto-execution-20260817` on governed Job
`job-2031a9e5682a4238f98e3ac7aee8a593`, base
`a642b045775b9154a00ae78543037fbef4fb447b`, target branch
`agent/p0-claim-online-agent-auto-execution-20260817`, generation 1.
It does not authorize protected-main, Production, tag, baseline, raw Git, Task DB, or
credential mutations.
## Considered approaches
1. Require every caller to write `metadata.execution`. This is simple but preserves the
current operational gap: a valid governed Job can still project an unexecutable Online
Task.
2. Let workers infer repository and WriterLease coordinates from natural-language Task
fields. This removes the skip but violates fail-closed lineage and can bind the wrong Job,
branch, base, or generation.
3. Use a canonical server-side projector plus a task-agnostic worker resolver. This is the
selected approach. The governed binding remains the only authority for repository, Job,
branch, base, expected head, generation, and idempotency. Producers can persist the
projected envelope when creating the Online Task. A worker may recover an omitted envelope
only from the same canonical projector and durable binding; it never guesses coordinates.
## Components
### Canonical execution contract
`aios_online_agent_execution.py` owns schema `aios.online-agent.execution.v2`. It validates
the complete governed lineage and supports two explicit actions:
- `deterministic`: a hash-bound change set, verification commands, and governed commit;
- `source_capture`: a read-only first attempt that returns `NEEDS_REWORK_SOURCE_CAPTURE` and
cannot acquire WriterLease or mutate Git.
Wrong task, Job, repository, base, expected head, branch, generation, capability scope, or
governance policy fails closed with a canonical diagnostic.
### Governed Job to Online Task projection
`GovernedTaskBootstrap` exposes an exact durable binding lookup and builds Online Task
projection data through the canonical contract. When the Bridge is configured with the
official Agent Task control credential reference, governed Job creation submits that complete
projection through the official Task HTTP API. The Bridge also exposes a loopback envelope
projection operation used for recovery. The persisted projection includes the versioned
envelope rather than relying on manual metadata edits.
Projection delivery is a durable outbox, not a best-effort tail call. The outbox record is
written before HTTP delivery and a Bridge reconciler retries it after transient API failure or
process restart. A response is accepted only when its complete execution envelope equals the
submitted projection. Requests that explicitly require projection are rejected before governed
bootstrap when the projection principal is unavailable.
Loopback is not treated as an identity boundary. Every POST route requires an exact
runtime-principal bearer credential supplied by systemd credential projection; unauthenticated
local callers are denied before request dispatch. The control-plane credential used to create
Online Tasks remains a separate Bridge-only reference and is never projected into the worker.
For legacy Online Tasks with an omitted envelope, the generic worker asks the same Bridge
operation for a read-only `source_capture` envelope. Projection succeeds only when the exact
durable binding exists. An omitted envelope is therefore recoverable; an invalid envelope is
never replaced or weakened.
Persisted envelopes are also reprojected through the Bridge before claim and compared by exact
digest and Job lineage. A self-consistent client-generated envelope is therefore not authority.
### Dynamic worker and trusted consumer
The repository gains a deployable generic worker and systemd unit definitions as governed
source files. The worker validates or
resolves an envelope before claim, revalidates after atomic claim, starts heartbeat, and then
executes the selected action. The trusted engineering consumer uses this same task-agnostic
entrypoint. No compiled worker contains a Task ID, base OID, branch, generation, or plan path.
Skip observability distinguishes:
- `TASK_SKIPPED_PROJECTION_MISSING`;
- `TASK_SKIPPED_ENVELOPE_INVALID`;
- `TASK_SKIPPED_CAPABILITY_MISMATCH`;
- `TASK_SKIPPED_POLICY_DENY`;
- `TASK_POLL_FAILED` / worker unavailable.
## State flow
```text
Governed Job BOUND
-> canonical durable binding
-> Online Task projection with execution.v2
-> capability-filtered poll
-> envelope and lineage validation
-> atomic claim
-> initial heartbeat/start
-> refetch Task and re-resolve envelope
-> compare execution digest and Job
-> source capture OR deterministic governed execution
-> result construction
-> result submission
-> external server-side verification gate
```
Legacy omission uses `binding -> source_capture projection` between poll and validation. It
does not write Task metadata and does not convert a malformed envelope into a valid one.
## Recovery and safety
- Projection is deterministic and digest-bound.
- Governed Task creation is serialized by a cross-process, Task-ID-digest lock. Durable paths
use collision-resistant exact Task keys while retaining read compatibility with verified
legacy bindings.
- A new Task cannot adopt an existing branch from caller-selected generation data. It must name
an exact durable parent Task/Job whose single ACTIVE lease owns the branch head; generation is
then derived from that lease.
- Claim is attempted only after validation; claimed payload drift is denied.
- Source capture has no WriterLease or commit path.
- Deterministic execution retains detached worktree, exact WriterLease, governed commit,
durable current-head CAS, and receipt convergence.
- Local deterministic recovery is keyed by Task and execution digest; live acceptance must
independently reconverge Job, lease, receipt, and branch OIDs before verification.
- An ambiguous commit transport or server outcome becomes `RECONCILIATION_REQUIRED`; the
worker does not submit a canonical failure until exact Job receipt and head evidence is
observed.
- A completed read-only `source_capture` state is ignored when a later attempt has a changed
digest and no governed state exists; all other digest drift is denied.
- Protected refs are never worker targets.
## Acceptance
The implementation must pass the named projection, schema, claimability, dynamic-binding,
wrong-lineage, capability, P0-A, and P0-B tests. Runtime acceptance additionally requires a
real Online Task to be claimed and heartbeated without per-task worker or systemd plan
injection, plus governed receipt/OID/CAS convergence for this implementation Task.
Unit tests and deployable unit definitions do not by themselves satisfy runtime acceptance.
@@ -0,0 +1,56 @@
# P0-CLAIM Scheme A follow-up
This follow-up continues the existing candidate `71523aef6e48b1e2fa516c456ca189b7a039104b`.
It does not create a replacement Task, Job, branch or generation.
## Retry extension
A versioned Owner-controlled loopback API provides
`POST /v1/tasks/{task_id}/retry-extension`.
The request binds FAILED status, revision, exhausted attempt/max_attempts, next max attempts,
previous verification SHA256, Owner authorization ID and idempotency key. The service performs one
SQLite `BEGIN IMMEDIATE` transaction containing the CAS update, durable retry-extension record and
Task event. Each authorization extends the boundary by exactly one attempt.
## Same-Job continuation
The durable GovernedTask binding remains the lineage anchor. After the first CREATE_NEW commit,
`execution_binding()` may normalize that SAME Job to `CONTINUE_EXISTING` only when exactly one
ACTIVE WriterLease for the Job owns the current authority branch head, generation and lineage
anchor. The lineage `base_oid` stays unchanged; only `expected_branch_oid` becomes the current head.
GovernedTask creation is also serialized by a branch-digest lock, preventing sibling continuation
tasks from racing generation admission.
## Projection and recovery
Natural-language source signals are normalized server-side to at most 16 unique 512-byte values.
Malformed persisted envelopes still fail closed.
Fresh commits and recovery require a COMPLETED Job, `remote_verified=true`, exact
authority/API/remote receipt OIDs, and exactly one ACTIVE WriterLease whose durable current head
matches the commit. Delayed local ref visibility becomes `RECONCILIATION_REQUIRED`, not a terminal
canonical failure.
## Credential isolation
The credentialed Coordinator performs only Task/Bridge coordination and governed commit. All
deterministic repository modification and test commands run in
`aios-online-agent-isolated-executor` under a separate system user with:
- no Agent/Owner credential projection;
- no gitops supplementary group;
- `PrivateNetwork=true`;
- `RestrictAddressFamilies=AF_UNIX AF_INET`, with `IPAddressDeny=any` and
`IPAddressAllow=localhost`, so regression tests may use an isolated loopback
HTTP server without gaining host or external network reachability;
- `InaccessiblePaths=/run/credentials`.
The executor receives a hash-bound source archive and deterministic change set through a filesystem
exchange and returns only changed file bytes/hashes plus command evidence. The Coordinator validates
that response before materializing exact allowed files and calling the governed Bridge.
Codex/Hermes mutation adapters remain disabled until a separately audited uncredentialed planner
backend exists. Under the current operating model, implementation code is authored before Online
Agent execution and is carried as a deterministic plan.
+9
View File
@@ -20,3 +20,12 @@ def test_unified_event_flow():
dashboard = AgentEventDashboard(bus).overview()
assert dashboard["audit"]["enabled"] is True
def test_target_changes_event_identity(tmp_path):
bus = AgentEventBus(tmp_path)
first = bus.publish('test', 'same', 'user-a', 'same summary', {'value': 1})
second = bus.publish('test', 'same', 'user-b', 'same summary', {'value': 1})
first_id = first.get('id') if isinstance(first, dict) else getattr(first, 'id', first)
second_id = second.get('id') if isinstance(second, dict) else getattr(second, 'id', second)
assert first_id != second_id
+152 -2
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",
@@ -74,8 +77,8 @@ def test_slash_branch_uses_paginated_collection():
def test_http_route_contract_is_complete():
expected = {
"/v2/runtime", "/v2/capabilities", "/v2/governed-task",
"/v2/writer-lease", "/v2/commit-file", "/v2/commit-files",
"/v2/runtime", "/v2/capabilities", "/v2/governed-task", "/v2/execution-envelope", "/v2/execution-plan",
"/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,150 @@ 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_http_mutation_routes_require_exact_runtime_principal():
handler = BridgeHandler.__new__(BridgeHandler)
runtime_principal = "runtime-" "agent-principal-token-1234"
handler.control_token = runtime_principal
handler.agent_task_api_url = "http://127.0.0.1:1"
handler.headers = {"Authorization": f"Bearer {runtime_principal}"}
assert handler._authorized("createGovernedTask", {}) is True
handler.headers = {"Authorization": "Bearer wrong-runtime-principal-token"}
assert handler._authorized("createGovernedTask", {}) is False
handler.headers = {}
assert handler._authorized("createGovernedTask", {}) is False
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"]
def test_online_agent_execution_routes_and_action_projection_are_exposed():
from aios_chatgpt_bridge_v2 import BridgeV2Adapter, OPERATION_REQUIRED_FIELDS
from aios_chatgpt_bridge_v2_server import ROUTES
assert ROUTES["/v2/execution-envelope"] == "projectOnlineAgentExecution"
assert ROUTES["/v2/execution-plan"] == "registerOnlineAgentExecutionPlan"
adapter = BridgeV2Adapter(runtime_status={})
contracts = adapter.getGovernedWriteCapabilities()["operation_contracts"]
assert tuple(contracts["projectOnlineAgentExecution"]["required_fields"]) == OPERATION_REQUIRED_FIELDS["projectOnlineAgentExecution"]
assert tuple(contracts["registerOnlineAgentExecutionPlan"]["required_fields"]) == OPERATION_REQUIRED_FIELDS["registerOnlineAgentExecutionPlan"]
schema = Path("docs/gitops/openapi-chatgpt-actions.yaml").read_text(encoding="utf-8")
assert "/v2/execution-envelope:" in schema
assert "operationId: projectOnlineAgentExecution" in schema
assert "/v2/execution-plan:" in schema
assert "operationId: registerOnlineAgentExecutionPlan" in schema
@@ -0,0 +1,307 @@
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})
def complete_continuation_cycle(self, *_args: object, **_kwargs: object) -> None:
return None
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"]
+67
View File
@@ -0,0 +1,67 @@
from pathlib import Path
import hashlib,json,os,tarfile,sys,tempfile
import pytest
ROOT=Path(__file__).resolve().parents[1]
sys.path.insert(0,str(ROOT/"active-runtime/app"))
import aios_isolated_executor as mod
def make_archive(tmp_path):
root=tmp_path/"src"; root.mkdir()
(root/"hello.py").write_text("VALUE = 1\n")
archive=tmp_path/"source.tgz"
with tarfile.open(archive,"w:gz") as tf: tf.add(root/"hello.py",arcname="hello.py")
return archive
def test_isolated_executor_applies_hash_bound_change(tmp_path,monkeypatch):
archive=make_archive(tmp_path)
monkeypatch.delenv("CREDENTIALS_DIRECTORY",raising=False)
monkeypatch.setattr(mod,"assert_uncredentialed",lambda:None)
request={
"schema":"aios.isolated-execution.request.v1",
"request_id":"task-req-0000000000000000",
"source_archive":str(archive),
"source_archive_sha256":mod.sha256(archive.read_bytes()),
"change_set":[{
"path":"hello.py","operation":"replace_text","old":"VALUE = 1","new":"VALUE = 2","count":1,
"expected_sha256":mod.sha256(b"VALUE = 1\n")
}],
"verification":[{"label":"compile","argv":["/usr/bin/python3","-m","py_compile","hello.py"],"cwd":".","timeout_seconds":30}]
}
req=tmp_path/"req.json"; res=tmp_path/"res.json"
req.write_text(json.dumps(request))
mod.execute_request(req,res)
out=json.loads(res.read_text())
assert out["request_id"]=="task-req-0000000000000000"
assert out["credential_isolation"]["credential_env_absent"] is True
changed=out["changed_files"][0]
assert changed["path"]=="hello.py" and changed["deleted"] is False
def test_isolated_executor_source_drift_denied(tmp_path,monkeypatch):
archive=make_archive(tmp_path); monkeypatch.setattr(mod,"assert_uncredentialed",lambda:None)
request={
"schema":"aios.isolated-execution.request.v1","request_id":"task-req-0000000000000001",
"source_archive":str(archive),
"source_archive_sha256":mod.sha256(archive.read_bytes()),
"change_set":[{"path":"hello.py","operation":"write_file","content":"x\n","expected_sha256":"0"*64}],
"verification":[]
}
req=tmp_path/"req.json"; res=tmp_path/"res.json"; req.write_text(json.dumps(request))
with pytest.raises(mod.IsolatedExecutorError,match="SOURCE_DRIFT"):
mod.execute_request(req,res)
def test_credential_boundary_eacces_is_accepted(monkeypatch):
class DeniedDirectory:
def exists(self): return True
def is_symlink(self): return False
def is_dir(self): return True
def iterdir(self): raise PermissionError(13,"permission denied")
monkeypatch.setattr(mod.os,"access",lambda *_args,**_kwargs:False)
assert mod._credential_exposure(DeniedDirectory()) is None
def test_systemd_unit_has_no_credentials_and_private_network():
unit=(ROOT/"deploy/systemd/aios-online-agent-isolated-executor.service").read_text()
assert "PrivateNetwork=true" in unit
assert "InaccessiblePaths=/run/credentials" in unit
assert "LoadCredential" not in unit
assert "SupplementaryGroups=gitops" not in unit
+524
View File
@@ -0,0 +1,524 @@
from __future__ import annotations
import hashlib
import json
from pathlib import Path
import sys
from types import SimpleNamespace
import pytest
ROOT = Path(__file__).resolve().parents[1]
APP = ROOT / "active-runtime" / "app"
sys.path.insert(0, str(APP))
from aios_chatgpt_bridge_v2 import ( # noqa: E402
BRIDGE_OPERATION_CONTRACTS,
BridgeV2Adapter,
)
from aios_chatgpt_bridge_v2_server import GovernedRuntime # noqa: E402
from aios_governed_task_bootstrap import BootstrapError, GovernedTaskBinding, GovernedTaskBootstrap, _task_key # noqa: E402
from aios_online_agent_execution import ( # noqa: E402
ExecutionProjectionError,
GovernedExecutionBinding,
OnlineTaskProjectionClient,
project_execution_envelope,
project_online_task,
validate_execution_envelope,
)
import aios_online_agent_worker as worker_module # noqa: E402
from aios_online_agent_worker import ExecutionEnvelope, WorkerError, WorkerSupervisor, governed_commit, load_task_state # noqa: E402
BASE = "a642b045775b9154a00ae78543037fbef4fb447b"
TASK = "p0-claim-online-agent-auto-execution-20260817"
JOB = "job-2031a9e5682a4238f98e3ac7aee8a593"
BRANCH = "agent/p0-claim-online-agent-auto-execution-20260817"
def binding() -> GovernedExecutionBinding:
return GovernedExecutionBinding.from_mapping(
{
"task_id": TASK,
"job_id": JOB,
"repository": "aios/aios",
"base_oid": BASE,
"expected_branch_oid": BASE,
"target_branch": BRANCH,
"branch_mode": "CREATE_NEW",
"generation": 1,
"idempotency_key": "p0-claim-online-agent-auto-execution-v1",
}
)
def task_without_envelope() -> dict:
return {
"task_id": TASK,
"goal": "make governed Online Tasks automatically executable",
"instructions": "project, claim, heartbeat, and preserve exact lineage",
"required_capabilities": ["git.read", "git.test"],
"metadata": {},
}
def test_project_online_task_builds_execution_metadata_from_binding() -> None:
projected = project_online_task(binding(), task_without_envelope())
execution = projected["metadata"]["execution"]
assert execution["schema"] == "aios.online-agent.execution.v2"
assert execution["job_id"] == JOB
assert execution["task_id"] == TASK
assert execution["repository"] == "aios/aios"
assert execution["base_oid"] == BASE
assert execution["expected_branch_oid"] == BASE
assert execution["target_branch"] == BRANCH
assert execution["generation"] == 1
assert projected["metadata"]["execution_projection"]["execution_digest"] == execution["execution_digest"]
def test_job_to_task_execution_projection(tmp_path: Path) -> None:
exact = binding()
durable = GovernedTaskBinding(
task_id=exact.task_id,
job_id=exact.job_id,
repository=exact.repository,
base_oid=exact.base_oid,
expected_branch_oid=exact.expected_branch_oid,
target_branch=exact.target_branch,
branch_mode=exact.branch_mode,
generation=exact.generation,
idempotency_key=exact.idempotency_key,
risk="engineering",
description="automatic claim infrastructure",
)
class Bootstrap:
def create(self, _request: dict) -> dict:
return durable.response()
def online_task_projection(self, _task_id: str, online: dict, execution: dict | None, **_kwargs: object) -> dict:
return project_online_task(exact, online, execution)
def get_binding(self, _task_id: str) -> GovernedTaskBinding:
return durable
def execution_binding(self, _binding: GovernedTaskBinding) -> GovernedExecutionBinding:
return exact
class Projector:
def __init__(self) -> None:
self.calls = 0
def create(self, projected: dict, *, binding: GovernedExecutionBinding) -> dict:
self.calls += 1
assert binding == exact
return {**projected, "status": "QUEUED"}
runtime = GovernedRuntime.__new__(GovernedRuntime)
runtime.bootstrap = Bootstrap()
runtime.bindings = {}
runtime.online_task_projector = Projector()
runtime.projection_outbox_root = tmp_path / "projection-outbox"
runtime.projection_outbox_root.mkdir()
response = GovernedRuntime.create_task(
runtime,
{
"task_id": TASK,
"repository": "aios/aios",
"base_oid": BASE,
"target_branch": BRANCH,
"risk": "engineering",
"description": "automatic claim infrastructure",
"idempotency_key": exact.idempotency_key,
"online_task": task_without_envelope(),
},
)
assert response["online_task_projection"]["status"] == "PROJECTED"
assert response["online_task_projection"]["task_status"] == "QUEUED"
assert runtime.online_task_projector.calls == 1
def test_execution_envelope_schema_validation() -> None:
envelope = project_execution_envelope(binding())
validate_execution_envelope(envelope, binding=binding())
malformed = dict(envelope)
malformed["schema"] = "aios.online-agent.execution.v1"
with pytest.raises(ExecutionProjectionError, match="EXECUTION_ENVELOPE_SCHEMA_VALIDATION"):
validate_execution_envelope(malformed, binding=binding())
def test_projection_response_must_equal_submitted_execution(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None:
submitted = project_online_task(binding(), task_without_envelope(), {"action": "source_capture", "source_signals": ["submitted"]})
returned = project_online_task(binding(), task_without_envelope(), {"action": "source_capture", "source_signals": ["different"]})
returned.update({"status": "QUEUED"})
class Response:
status = 201
def __enter__(self):
return self
def __exit__(self, *_args: object) -> None:
return None
def read(self) -> bytes:
return json.dumps(returned).encode("utf-8")
monkeypatch.setattr("aios_online_agent_execution.urllib.request.urlopen", lambda *_args, **_kwargs: Response())
token = tmp_path / "control.token"
token.write_text("x" * 40, encoding="utf-8")
with pytest.raises(ExecutionProjectionError, match="ONLINE_TASK_PROJECTION_RESPONSE_BINDING_INVALID"):
OnlineTaskProjectionClient("http://127.0.0.1:1", token).create(submitted, binding=binding())
class _ProjectedBridge:
def __init__(self, value: dict) -> None:
self.value = value
self.calls = 0
def project_execution(self, _task: dict, _execution: dict | None = None) -> tuple[int, dict]:
self.calls += 1
return 200, self.value
class _FakeAgent:
def __init__(self, tasks: list[dict]) -> None:
self.tasks = tasks
self.claimed: list[str] = []
def poll(self) -> list[dict]:
return self.tasks
def claim(self, task_id: str) -> tuple[int, dict]:
self.claimed.append(task_id)
return 200, {"task_id": task_id, "lease_token": "x" * 32, "attempt": 1, "revision": 2}
def test_task_visible_to_worker_claimable(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None:
monkeypatch.setattr(worker_module, "STATE_ROOT", tmp_path)
supervisor = WorkerSupervisor("runtime-token-not-secret")
supervisor.bridge = _ProjectedBridge(project_execution_envelope(binding()))
envelope, source = supervisor.resolve_envelope(task_without_envelope())
assert source == "recovered_projection"
assert envelope.task_id == TASK
assert envelope.job_id == JOB
assert envelope.action == "source_capture"
def test_task_claim_after_projection(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None:
monkeypatch.setattr(worker_module, "STATE_ROOT", tmp_path)
supervisor = WorkerSupervisor("runtime-token-not-secret")
fake_agent = _FakeAgent([task_without_envelope()])
supervisor.agent = fake_agent
supervisor.bridge = _ProjectedBridge(project_execution_envelope(binding()))
processed: list[tuple[str, str]] = []
supervisor.process_claim = lambda task, claim, envelope: processed.append((task["task_id"], envelope.execution_digest if hasattr(envelope, "execution_digest") else envelope.digest)) # type: ignore[method-assign]
assert supervisor.once() == 0
assert fake_agent.claimed == [TASK]
assert processed and processed[0][0] == TASK
def test_claim_heartbeat_source_capture_and_result_lifecycle(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None:
execution = project_execution_envelope(binding(), {"action": "source_capture", "source_signals": ["agent"]})
task = task_without_envelope()
task["metadata"] = {"execution": execution}
class Agent:
def __init__(self) -> None:
self.heartbeats: list[str] = []
self.results: list[dict] = []
def heartbeat(self, _task_id: str, _lease_token: str, stage: str) -> tuple[int, dict]:
self.heartbeats.append(stage)
return 200, {}
def get(self, _task_id: str) -> tuple[int, dict]:
return 200, task
def result(self, _task_id: str, _lease_token: str, _key: str, result: dict) -> tuple[int, dict]:
self.results.append(result)
return 200, {}
worktree = tmp_path / "worktree"
source = worktree / "active-runtime" / "app" / "agent_auto_fixture.py"
source.parent.mkdir(parents=True)
source.write_text("VALUE = 1\n", encoding="utf-8")
monkeypatch.setattr(worker_module, "STATE_ROOT", tmp_path / "state")
monkeypatch.setattr(worker_module, "new_run_root", lambda: tmp_path / "run")
monkeypatch.setattr(worker_module, "prepare_worktree", lambda *_args, **_kwargs: worktree)
monkeypatch.setattr(worker_module, "cleanup_run_root", lambda *_args, **_kwargs: None)
agent = Agent()
supervisor = WorkerSupervisor("runtime-token-not-secret")
supervisor.agent = agent
supervisor.bridge = _ProjectedBridge(execution)
envelope, _ = supervisor.resolve_envelope(task)
supervisor.process_claim(task, {"task_id": TASK, "lease_token": "l" * 32, "attempt": 2, "revision": 8}, envelope)
assert agent.heartbeats and agent.heartbeats[0] == "claimed"
assert len(agent.results) == 1
assert agent.results[0]["status"] == "SUCCEEDED"
assert agent.results[0]["classification"] == "NEEDS_REWORK_SOURCE_CAPTURE"
def test_dynamic_task_binding_no_hardcoded_task_id() -> None:
worker = (APP / "aios_online_agent_worker.py").read_text(encoding="utf-8")
assert TASK not in worker
assert BASE not in worker
for name in ("aios-online-agent-worker.service", "aios-online-engineering-consumer.service"):
unit = (ROOT / "deploy" / "systemd" / name).read_text(encoding="utf-8")
assert TASK not in unit
assert JOB not in unit
assert BASE not in unit
assert BRANCH not in unit
assert "AIOS_ENGINEERING_PLAN_FILE" not in unit
assert "aios_online_agent_worker.py" in unit
projection = (ROOT / "deploy" / "systemd" / "aios-chatgpt-bridge-v2-agent-task-projection.conf").read_text(encoding="utf-8")
assert "AIOS_ONLINE_TASK_PROJECTION_ENABLED=1" in projection
assert "LoadCredential=agent_task_control_token:" in projection
assert "LoadCredentialEncrypted=" not in projection
assert "Bearer " not in projection
def test_durable_task_key_has_collision_resistant_exact_identity() -> None:
assert _task_key("task:a") != _task_key("task-a")
assert _task_key("x" * 140 + "a") != _task_key("x" * 140 + "b")
def test_existing_binding_rejects_idempotency_head_and_generation_drift(tmp_path: Path) -> None:
class Authority:
@staticmethod
def commit_exists(_oid: str) -> bool:
return True
@staticmethod
def read_ref(_ref: str) -> None:
return None
bootstrap = GovernedTaskBootstrap(Authority(), tmp_path, remote_name="origin", token_path=tmp_path / "token", ssh_key_path=tmp_path / "key")
request = {
"task_id": TASK,
"repository": "aios/aios",
"base_oid": BASE,
"target_branch": BRANCH,
"risk": "engineering",
"description": "automatic claim infrastructure",
"idempotency_key": "p0-claim-online-agent-auto-execution-v1",
"expected_branch_oid": BASE,
"generation": 1,
}
bootstrap.create(request)
for drift in (
{"idempotency_key": "p0-claim-online-agent-auto-execution-v2"},
{"expected_branch_oid": "b" * 40},
{"generation": 2},
):
with pytest.raises(BootstrapError, match="TASK_BINDING_CONFLICT"):
bootstrap.create({**request, **drift})
def test_existing_branch_without_durable_parent_ownership_is_denied(tmp_path: Path) -> None:
class Authority:
@staticmethod
def commit_exists(_oid: str) -> bool:
return True
@staticmethod
def read_ref(_ref: str) -> str:
return BASE
bootstrap = GovernedTaskBootstrap(Authority(), tmp_path, remote_name="origin", token_path=tmp_path / "token", ssh_key_path=tmp_path / "key")
with pytest.raises(BootstrapError, match="EXISTING_BRANCH_OWNERSHIP_REQUIRED"):
bootstrap.create({
"task_id": TASK,
"repository": "aios/aios",
"base_oid": BASE,
"target_branch": BRANCH,
"risk": "engineering",
"description": "automatic claim infrastructure",
"idempotency_key": "p0-claim-online-agent-auto-execution-v1",
"expected_branch_oid": BASE,
"generation": 2,
})
@pytest.mark.parametrize(
("field", "value", "error"),
[
("repository", "other/repo", "WRONG_REPOSITORY_ENVELOPE_DENY"),
("target_branch", "agent/wrong-branch", "WRONG_BRANCH_ENVELOPE_DENY"),
("generation", 2, "WRONG_GENERATION_ENVELOPE_DENY"),
],
)
def test_wrong_lineage_envelope_deny(field: str, value: object, error: str) -> None:
envelope = project_execution_envelope(binding())
envelope[field] = value
with pytest.raises(ExecutionProjectionError, match=error):
validate_execution_envelope(envelope, binding=binding())
def test_capability_mismatch_deny() -> None:
envelope = project_execution_envelope(
binding(),
{"action": "source_capture", "capability_scope": ["git.read", "governed.write.file"]},
)
with pytest.raises(ExecutionProjectionError, match="CAPABILITY_MISMATCH_DENY"):
validate_execution_envelope(envelope, binding=binding(), worker_capabilities=["git.read"])
def test_governance_lineage_override_deny() -> None:
envelope = project_execution_envelope(binding())
envelope["governance"] = {**envelope["governance"], "job_id": "job-wrong-lineage"}
with pytest.raises(ExecutionProjectionError, match="JOB_BINDING_CONFLICT"):
validate_execution_envelope(envelope, binding=binding())
def test_malformed_persisted_envelope_does_not_fallback() -> None:
task = task_without_envelope()
task["metadata"] = {"execution": {"schema": "broken"}}
bridge = _ProjectedBridge(project_execution_envelope(binding()))
supervisor = WorkerSupervisor("runtime-token-not-secret")
supervisor.bridge = bridge
with pytest.raises(WorkerError, match="EXECUTION_ENVELOPE_SCHEMA_VALIDATION"):
supervisor.resolve_envelope(task)
assert bridge.calls == 0
def test_self_consistent_persisted_wrong_lineage_is_denied_by_authority() -> None:
task = task_without_envelope()
wrong = binding()
wrong = GovernedExecutionBinding.from_mapping({**wrong.public(), "job_id": "job-00000000000000000000000000000000"})
task["metadata"] = {"execution": project_execution_envelope(wrong)}
supervisor = WorkerSupervisor("runtime-token-not-secret")
supervisor.bridge = _ProjectedBridge(project_execution_envelope(binding()))
with pytest.raises(WorkerError, match="TASK_PERSISTED_BINDING_CONFLICT"):
supervisor.resolve_envelope(task)
def test_projection_control_missing_denies_before_governed_side_effect() -> None:
class Bootstrap:
called = False
def create(self, _request: dict) -> dict:
self.called = True
raise AssertionError("must not create governed state")
runtime = GovernedRuntime.__new__(GovernedRuntime)
runtime.bootstrap = Bootstrap()
runtime.online_task_projector = None
with pytest.raises(RuntimeError, match="ONLINE_TASK_PROJECTION_CONTROL_UNAVAILABLE"):
GovernedRuntime.create_task(runtime, {"task_id": TASK, "require_online_task_projection": True})
assert runtime.bootstrap.called is False
def test_source_capture_projection_cannot_write() -> None:
envelope = project_execution_envelope(binding())
assert envelope["action"] == "source_capture"
assert envelope["change_set"] == []
assert envelope["verification"] == []
assert envelope["governance"]["writer_lease"] is False
assert all(envelope["governance"][field] is False for field in ("main_write", "production_write", "tag_write", "baseline_write", "force_push", "protected_ref_bypass"))
def test_source_capture_state_allows_next_attempt_deterministic_digest(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None:
monkeypatch.setattr(worker_module, "STATE_ROOT", tmp_path)
state_path = tmp_path / "tasks" / f"{TASK}.json"
state_path.parent.mkdir(parents=True)
state_path.write_text(json.dumps({"task_id": TASK, "execution_digest": "a" * 64, "phase": "source_capture_done"}), encoding="utf-8")
assert load_task_state(TASK, "b" * 64) is None
def test_bridge_exposes_controlled_projection_operation() -> None:
seen: list[dict] = []
adapter = BridgeV2Adapter(
runtime_status={},
handlers={"projectOnlineAgentExecution": lambda request: seen.append(dict(request)) or project_execution_envelope(binding())},
)
response = adapter.projectOnlineAgentExecution({"task_id": TASK})
assert response["job_id"] == JOB
assert seen == [{"task_id": TASK}]
assert BRIDGE_OPERATION_CONTRACTS["projectOnlineAgentExecution"]["path"] == "/v2/execution-envelope"
def test_execution_digest_is_canonical() -> None:
envelope = project_execution_envelope(binding())
material = {key: value for key, value in envelope.items() if key != "execution_digest"}
expected = hashlib.sha256(json.dumps(material, ensure_ascii=False, sort_keys=True, separators=(",", ":")).encode()).hexdigest()
assert envelope["execution_digest"] == expected
def test_governed_commit_uses_existing_exact_job_without_recreating(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None:
execution = project_execution_envelope(
binding(),
{
"action": "deterministic",
"change_set": [{"operation": "write_file", "path": "result.txt", "content": "ok\n"}],
"governance": {"allowed_files": ["result.txt"]},
},
)
envelope = ExecutionEnvelope.from_task({"task_id": TASK, "metadata": {"execution": execution}})
(tmp_path / "result.txt").write_text("ok\n", encoding="utf-8")
commit_oid = "b" * 40
class Bridge:
def __init__(self) -> None:
self.routes: list[str] = []
def call(self, route: str, body: dict) -> tuple[int, dict]:
self.routes.append(route)
if route == "/v2/job":
assert body == {"job_id": JOB}
else:
assert body["task_id"] == TASK
assert body["job_id"] == JOB
if route == "/v2/writer-lease":
return 200, {
"task_id": TASK,
"job_id": JOB,
"repository": "aios/aios",
"base_oid": BASE,
"expected_branch_oid": BASE,
"branch_mode": "CREATE_NEW",
"generation": 1,
"target_branch": BRANCH,
}
if route == "/v2/commit-files":
return 200, {
"job_id": JOB,
"target_branch": BRANCH,
"generation": 1,
"branch_mode": "CONTINUE_EXISTING",
"commit_oid": commit_oid,
"commit_visible": True,
"receipt": {
"remote_verified": True,
"authority_oid": commit_oid,
"api_branch_oid": commit_oid,
"git_remote_oid": commit_oid,
},
}
if route == "/v2/job":
return 200, {
"state": "COMPLETED",
"receipt": {
"remote_verified": True,
"authority_oid": commit_oid,
"api_branch_oid": commit_oid,
"git_remote_oid": commit_oid,
},
"branch_leases": [{
"job_id": JOB,
"branch": BRANCH,
"state": "ACTIVE",
"generation": 1,
"current_head_oid": commit_oid,
}],
}
raise AssertionError(route)
bridge = Bridge()
monkeypatch.setattr(worker_module.subprocess, "run", lambda *args, **kwargs: SimpleNamespace(returncode=0, stdout=commit_oid + "\n", stderr=""))
heartbeat = SimpleNamespace(ensure=lambda: None)
result = governed_commit(envelope, tmp_path, bridge, heartbeat)
assert bridge.routes == ["/v2/writer-lease", "/v2/commit-files", "/v2/job"]
assert result["job_id"] == JOB
assert result["commit_oid"] == commit_oid
+60
View File
@@ -0,0 +1,60 @@
from pathlib import Path
import sys
import pytest
ROOT=Path(__file__).resolve().parents[1]
sys.path.insert(0,str(ROOT/"active-runtime/app"))
from aios_online_agent_execution import GovernedExecutionBinding
from aios_online_agent_plan_store import ExecutionPlanStore, ExecutionPlanError
BASE="a642b045775b9154a00ae78543037fbef4fb447b"
HEAD="71523aef6e48b1e2fa516c456ca189b7a039104b"
def binding(head=HEAD):
return GovernedExecutionBinding.from_mapping({
"task_id":"task-plan-a","job_id":"job-plan-a","repository":"aios/aios","base_oid":BASE,
"expected_branch_oid":head,"target_branch":"agent/task-plan-a","branch_mode":"CONTINUE_EXISTING",
"generation":1,"idempotency_key":"governed-plan-a",
})
def request():
return {
"schema":"aios.online-agent.plan.v1","task_id":"task-plan-a","job_id":"job-plan-a",
"source_head_oid":HEAD,"generation":1,"idempotency_key":"plan-task-a-v1",
"execution":{
"action":"deterministic",
"change_set":[{"path":"x.py","operation":"write_file","content":"x=1\n"}],
"verification":[{"label":"compile","argv":["/usr/bin/python3","-m","py_compile","x.py"],"cwd":".","timeout_seconds":30}],
"capability_scope":["git.read","git.test"],
}
}
def test_plan_register_and_resolve(tmp_path):
store=ExecutionPlanStore(tmp_path)
created=store.register(binding(),request())
assert created["idempotent"] is False
resolved=store.resolve(binding())
assert resolved["plan_digest"]==created["plan_digest"]
assert resolved["execution"]["action"]=="deterministic"
def test_plan_is_idempotent(tmp_path):
store=ExecutionPlanStore(tmp_path); req=request()
first=store.register(binding(),req); second=store.register(binding(),req)
assert second["idempotent"] is True
assert first["plan_digest"]==second["plan_digest"]
def test_plan_source_head_drift_denied(tmp_path):
store=ExecutionPlanStore(tmp_path); store.register(binding(),request())
with pytest.raises(ExecutionPlanError,match="SOURCE_HEAD_DRIFT"):
store.resolve(binding("b"*40))
def test_plan_replacement_requires_cas(tmp_path):
store=ExecutionPlanStore(tmp_path); first=store.register(binding(),request())
changed=request(); changed["idempotency_key"]="plan-task-a-v2"
changed["execution"]["change_set"][0]["content"]="x=2\n"
with pytest.raises(ExecutionPlanError,match="REPLACEMENT_CAS_MISMATCH"):
store.register(binding(),changed)
changed["expected_previous_plan_digest"]=first["plan_digest"]
second=store.register(binding(),changed)
assert second["plan_digest"]!=first["plan_digest"]
+245
View File
@@ -0,0 +1,245 @@
from __future__ import annotations
from pathlib import Path
from types import SimpleNamespace
import json
import sys
import pytest
ROOT=Path(__file__).resolve().parents[1]
APP=ROOT/"active-runtime/app"
sys.path.insert(0,str(APP))
from aios_online_agent_execution import GovernedExecutionBinding, project_execution_envelope
from aios_governed_task_bootstrap import GovernedTaskBinding, GovernedTaskBootstrap
from aios_gitops_job_controller import GitOpsJobController, JobState, JobStore
from aios_continuation_executor import ContinuationExecutionError, DetachedGitWorkspace
from aios_online_agent_worker import (
BridgeClient,
IsolatedDeterministicExecutor,
load_task_state,
_claim_order,
_isolated_executor_wait_seconds,
_verify_converged_job,
WorkerError,
)
BASE="a642b045775b9154a00ae78543037fbef4fb447b"
HEAD="71523aef6e48b1e2fa516c456ca189b7a039104b"
TASK="p0-claim-online-agent-auto-execution-20260817"
JOB="job-2031a9e5682a4238f98e3ac7aee8a593"
BRANCH="agent/p0-claim-online-agent-auto-execution-20260817"
def test_source_signals_are_bounded_by_projection():
binding=GovernedExecutionBinding.from_mapping({
"task_id":TASK,"job_id":JOB,"repository":"aios/aios","base_oid":BASE,
"expected_branch_oid":BASE,"target_branch":BRANCH,"branch_mode":"CREATE_NEW",
"generation":1,"idempotency_key":"p0-claim-online-agent-auto-execution-a642b04-v1",
})
execution=project_execution_envelope(binding,{
"action":"source_capture",
"source_signals":["x"*5000,"x"*5000,"trace credential tool "*100],
})
assert len(execution["source_signals"]) <= 16
assert all(0 < len(item) <= 512 for item in execution["source_signals"])
def test_same_job_create_new_binding_normalizes_to_current_head():
binding=GovernedTaskBinding(
task_id=TASK,job_id=JOB,repository="aios/aios",base_oid=BASE,
expected_branch_oid=BASE,target_branch=BRANCH,branch_mode="CREATE_NEW",
generation=1,idempotency_key="p0-claim-online-agent-auto-execution-a642b04-v1",
risk="engineering",description="automatic claim",
)
bootstrap=GovernedTaskBootstrap.__new__(GovernedTaskBootstrap)
bootstrap.authority=SimpleNamespace(read_ref=lambda ref: HEAD)
class Connection:
def __enter__(self): return self
def __exit__(self,*a): return False
def execute(self,*a,**k):
return SimpleNamespace(fetchall=lambda:[{
"current_head_oid":HEAD,"generation":1,"state":"ACTIVE","lineage_anchor_oid":BASE
}])
bootstrap._store=lambda _task:(SimpleNamespace(_connect=lambda:Connection()),None)
normalized=bootstrap.execution_binding(binding)
assert normalized.base_oid == BASE
assert normalized.expected_branch_oid == HEAD
assert normalized.branch_mode == "CONTINUE_EXISTING"
assert normalized.generation == 1
def test_recovery_receipt_requires_exact_oid_and_active_lease():
envelope=SimpleNamespace(job_id=JOB,target_branch=BRANCH,generation=1)
response={
"state":"COMPLETED",
"receipt":{"remote_verified":True,"authority_oid":HEAD,"api_branch_oid":HEAD,"git_remote_oid":HEAD},
"branch_leases":[{
"job_id":JOB,"branch":BRANCH,"state":"ACTIVE","generation":1,"current_head_oid":HEAD
}],
}
_verify_converged_job(response,envelope,HEAD)
bad={**response,"receipt":{**response["receipt"],"git_remote_oid":BASE}}
with pytest.raises(WorkerError,match="GOVERNED_RECEIPT_OID_MISMATCH"):
_verify_converged_job(bad,envelope,HEAD)
def test_executor_unit_is_uncredentialed():
unit=(ROOT/"deploy/systemd/aios-online-agent-isolated-executor.service").read_text()
assert "PrivateNetwork=true" in unit
assert "RestrictAddressFamilies=AF_UNIX AF_INET" in unit
assert "IPAddressDeny=any" in unit
assert "IPAddressAllow=localhost" in unit
assert "InaccessiblePaths=/run/credentials" in unit
assert "LoadCredential" not in unit
assert "SupplementaryGroups=gitops" not in unit
def test_isolated_executor_wait_budget_includes_safety_margin():
assert _isolated_executor_wait_seconds(({"timeout_seconds":1200},)) == 1230
def test_prepared_deterministic_plan_precedes_source_capture_at_same_priority():
deterministic=SimpleNamespace(action="deterministic",task_id="task-deterministic")
source_capture=SimpleNamespace(action="source_capture",task_id="task-source-capture")
task={"priority":100,"created_at":"2026-08-17T00:00:00Z"}
ordered=sorted(((task,source_capture),(task,deterministic)),key=_claim_order)
assert ordered[0][1] is deterministic
def test_plan_registry_gets_precedence_when_task_has_no_execution(monkeypatch):
client=BridgeClient("agent-token-placeholder")
seen=[]
monkeypatch.setattr(client,"call",lambda route,body:(seen.append((route,body)) or (200,{})))
task={"task_id":TASK,"required_capabilities":["git.read"]}
client.project_execution(task)
assert seen == [("/v2/execution-envelope",{"task_id":TASK,"required_capabilities":["git.read"]})]
client.project_execution(task,{"action":"source_capture","source_signals":["trace"]})
assert seen[-1][1]["execution"]["action"] == "source_capture"
def test_executor_exchange_files_are_group_readable_and_directories_setgid(tmp_path):
request=tmp_path/"request.json"
IsolatedDeterministicExecutor._atomic(request,b"{}")
assert request.stat().st_mode & 0o777 == 0o640
tmpfiles=(ROOT/"deploy/tmpfiles.d/aios-online-agent-executor.conf").read_text()
assert tmpfiles.count(" 2770 ") == 5
def test_precommit_state_recovery_requires_a_later_official_attempt(monkeypatch,tmp_path):
import aios_online_agent_worker as worker_module
monkeypatch.setattr(worker_module,"STATE_ROOT",tmp_path)
state_path=tmp_path/"tasks"/f"{TASK}.json"
state_path.parent.mkdir(parents=True)
state_path.write_text(json.dumps({"task_id":TASK,"execution_digest":"a"*64,"phase":"workspace_prepared"}))
assert load_task_state(TASK,"b"*64) is None
state_path.write_text(json.dumps({"task_id":TASK,"execution_digest":"a"*64,"phase":"verification_passed","attempt":16}))
with pytest.raises(WorkerError,match="TASK_PAYLOAD_DRIFT"):
load_task_state(TASK,"b"*64,attempt=16)
assert load_task_state(TASK,"b"*64,attempt=17) is None
def test_reconciliation_state_recovery_requires_later_attempt_and_same_job(monkeypatch,tmp_path):
import aios_online_agent_worker as worker_module
monkeypatch.setattr(worker_module,"STATE_ROOT",tmp_path)
state_path=tmp_path/"tasks"/f"{TASK}.json"
state_path.parent.mkdir(parents=True)
state_path.write_text(json.dumps({
"task_id":TASK,
"execution_digest":"a"*64,
"phase":"reconciliation_required",
"attempt":18,
"job_id":JOB,
"details":{"api_status":503,"error":"WORKTREE_DIRTY_SCOPE_MISMATCH"},
}))
with pytest.raises(WorkerError,match="TASK_PAYLOAD_DRIFT"):
load_task_state(TASK,"b"*64,attempt=18,continuation_job_id=JOB,continuation_head_oid=HEAD)
assert load_task_state(TASK,"b"*64,attempt=19,continuation_job_id=JOB,continuation_head_oid=HEAD) is None
with pytest.raises(WorkerError,match="TASK_PAYLOAD_DRIFT"):
load_task_state(TASK,"b"*64,attempt=19,continuation_job_id="job-other",continuation_head_oid=HEAD)
def test_legacy_dirty_scope_reconciliation_state_is_exactly_bounded(monkeypatch,tmp_path):
import aios_online_agent_worker as worker_module
monkeypatch.setattr(worker_module,"STATE_ROOT",tmp_path)
state_path=tmp_path/"tasks"/f"{TASK}.json"
state_path.parent.mkdir(parents=True)
state_path.write_text(json.dumps({
"task_id":TASK,
"execution_digest":"a"*64,
"phase":"reconciliation_required",
"job_id":JOB,
"details":{"api_status":503,"error":"WORKTREE_DIRTY_SCOPE_MISMATCH"},
}))
assert load_task_state(TASK,"b"*64,attempt=19,continuation_job_id=JOB,continuation_head_oid=HEAD) is None
with pytest.raises(WorkerError,match="TASK_PAYLOAD_DRIFT"):
load_task_state(TASK,"b"*64,attempt=19,continuation_job_id=JOB,continuation_head_oid="not-an-oid")
def test_completed_same_job_begins_one_idempotent_continuation_cycle(tmp_path):
controller=GitOpsJobController(JobStore(tmp_path/"jobs",payload_root=tmp_path/"payloads"))
submitted=controller.submit({
"idempotency_key":"same-job-continuation-test",
"campaign_id":TASK,
"task_id":TASK,
"repository":"aios/aios",
"operation":"GOVERNED_TASK_CREATE",
"branch":BRANCH,
"expected_branch_oid":HEAD,
"payload_digest":"a"*64,
})
job_id=submitted.job_id
controller.advance(job_id,JobState.AUTHORIZED,side_effect_state="AUTHORIZED",expected_revision=0)
controller.lease_job(
job_id,
lease_id="lease-"+job_id,
lineage_anchor_oid=BASE,
current_head_oid=HEAD,
generation=1,
lease_expires_at="2099-01-01T00:00:00+00:00",
expected_revision=1,
)
controller.advance(job_id,JobState.WORKTREE_READY,side_effect_state="WORKTREE_READY",expected_revision=2)
controller.advance(job_id,JobState.EXECUTING,side_effect_state="IN_PROGRESS",expected_revision=3)
controller.advance(job_id,JobState.LOCAL_VERIFIED,side_effect_state="LOCAL_VERIFIED",receipt={"authority_oid":HEAD},expected_revision=4)
controller.advance(job_id,JobState.REMOTE_SYNCING,side_effect_state="IN_PROGRESS",expected_revision=5)
receipt={
"remote_verified":True,
"commit_visible":True,
"authority_oid":HEAD,
"api_branch_oid":HEAD,
"git_remote_oid":HEAD,
}
controller.advance(job_id,JobState.REMOTE_VERIFIED,side_effect_state="REMOTE_VERIFIED",receipt=receipt,expected_revision=6)
controller.advance(job_id,JobState.COMPLETED,side_effect_state="REMOTE_VERIFIED",receipt=receipt,expected_revision=7)
first=controller.begin_continuation_cycle(
job_id,
idempotency_key="same-job-continuation-commit-v1",
expected_head_oid=HEAD,
generation=1,
expected_revision=8,
)
assert first == {"completed":False,"commit_oid":None,"idempotent":False}
assert controller.store.read(job_id)["state"] == "LEASED"
replay=controller.begin_continuation_cycle(
job_id,
idempotency_key="same-job-continuation-commit-v1",
expected_head_oid=HEAD,
generation=1,
expected_revision=8,
)
assert replay == {"completed":False,"commit_oid":None,"idempotent":True}
def test_deterministic_materialization_recovers_only_an_in_scope_dirty_subset(monkeypatch,tmp_path):
workspace=object.__new__(DetachedGitWorkspace)
workspace.path=tmp_path
monkeypatch.setattr(DetachedGitWorkspace,"_dirty_paths",lambda _self:{"old.py"})
workspace.materialize_files({"old.py":b"new-old\n","new.py":b"new\n"})
assert (tmp_path/"old.py").read_bytes() == b"new-old\n"
assert (tmp_path/"new.py").read_bytes() == b"new\n"
monkeypatch.setattr(DetachedGitWorkspace,"_dirty_paths",lambda _self:{"outside.py"})
with pytest.raises(ContinuationExecutionError,match="WORKTREE_DIRTY_SCOPE_MISMATCH"):
workspace.materialize_files({"old.py":b"again\n"})
+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) == ""
+73
View File
@@ -0,0 +1,73 @@
#!/usr/bin/env python3
from __future__ import annotations
import argparse
import ast
import pathlib
import shutil
import sys
from datetime import datetime, timezone
UTC = timezone.utc
def die(msg: str) -> None:
print(f"ERROR: {msg}", file=sys.stderr)
raise SystemExit(2)
def backup(path: pathlib.Path) -> pathlib.Path:
stamp = datetime.now(UTC).strftime("%Y%m%dT%H%M%SZ")
out = path.with_name(path.name + f".bak.{stamp}")
shutil.copy2(path, out)
return out
def main() -> int:
ap = argparse.ArgumentParser(
description="Repair misplaced subprocess import in execution-rebind runtime."
)
ap.add_argument("--rebind", required=True)
args = ap.parse_args()
path = pathlib.Path(args.rebind).resolve()
if not path.is_file():
die(f"missing rebind source: {path}")
text = path.read_text(encoding="utf-8")
marker = "from __future__ import annotations\n"
if marker not in text:
die("future import marker missing")
lines = text.splitlines(keepends=True)
filtered = [line for line in lines if line.strip() != "import subprocess"]
text2 = "".join(filtered)
future_index = text2.find(marker)
if future_index < 0:
die("future import marker missing after normalization")
insert_at = future_index + len(marker)
normalized = text2[:insert_at] + "\nimport subprocess\n" + text2[insert_at:]
if normalized == text:
print("OK import order already normalized")
else:
b = backup(path)
path.write_text(normalized, encoding="utf-8")
print(f"PATCHED import order; backup={b}")
ast.parse(path.read_text(encoding="utf-8"), filename=str(path))
print(f"OK syntax: {path}")
current = path.read_text(encoding="utf-8")
required_markers = (
"EXECUTION_REBIND_NON_FAST_FORWARD_DENY",
"old_expected_branch_oid",
"new_expected_branch_oid",
)
missing = [m for m in required_markers if m not in current]
if missing:
die("re-anchor patch markers missing: " + ",".join(missing))
print("RESULT: IMPORT_ORDER_REPAIRED")
return 0
if __name__ == "__main__":
raise SystemExit(main())
+172
View File
@@ -0,0 +1,172 @@
#!/usr/bin/env python3
from __future__ import annotations
import argparse
import ast
from datetime import datetime, timezone
import pathlib
import shutil
import sys
UTC = timezone.utc
def die(msg: str) -> None:
print(f"ERROR: {msg}", file=sys.stderr)
raise SystemExit(2)
def backup(path: pathlib.Path) -> pathlib.Path:
stamp = datetime.now(UTC).strftime("%Y%m%dT%H%M%SZ")
out = path.with_name(path.name + f".bak.{stamp}")
shutil.copy2(path, out)
return out
def ensure_import(path: pathlib.Path, import_line: str) -> None:
text = path.read_text(encoding="utf-8")
if import_line + "\n" in text:
return
lines = text.splitlines(keepends=True)
at = 0
if lines and lines[0].startswith("#!"):
at = 1
if at < len(lines) and lines[at].startswith("from __future__ import"):
at += 1
lines.insert(at, import_line + "\n")
path.write_text("".join(lines), encoding="utf-8")
def replace_once(path: pathlib.Path, old: str, new: str, label: str) -> None:
text = path.read_text(encoding="utf-8")
if new in text:
print(f"OK already patched: {label}")
return
count = text.count(old)
if count != 1:
die(f"{label}: expected exactly one match, found {count}")
b = backup(path)
path.write_text(text.replace(old, new, 1), encoding="utf-8")
print(f"PATCHED {label}; backup={b}")
def syntax_check(path: pathlib.Path) -> None:
ast.parse(path.read_text(encoding="utf-8"), filename=str(path))
print(f"OK syntax: {path}")
def main() -> int:
ap = argparse.ArgumentParser(description="Add guarded same-job execution re-anchor support.")
ap.add_argument("--rebind", required=True)
args = ap.parse_args()
path = pathlib.Path(args.rebind).resolve()
if not path.is_file():
die(f"missing rebind source: {path}")
ensure_import(path, "import subprocess")
old = ''' binding = GovernedExecutionBinding.from_mapping(old_execution)
resolved = dict(self.resolver(task_id, capabilities))
new_execution = validate_execution_envelope(resolved, binding=binding)
if new_execution.get("action") != "deterministic":
raise ExecutionRebindError("EXECUTION_REBIND_DETERMINISTIC_PLAN_REQUIRED", 409)
if new_execution["execution_digest"] != request.expected_new_execution_digest:
raise ExecutionRebindError("EXECUTION_REBIND_NEW_DIGEST_DRIFT", 409)
if new_execution["execution_digest"] == old_execution["execution_digest"]:
raise ExecutionRebindError("EXECUTION_REBIND_DIGEST_UNCHANGED", 409)
'''
new = ''' binding = GovernedExecutionBinding.from_mapping(old_execution)
resolved = dict(self.resolver(task_id, capabilities))
candidate_binding = GovernedExecutionBinding.from_mapping(resolved)
# Same-job continuation may advance only the branch head (and may
# transition CREATE_NEW -> CONTINUE_EXISTING after the branch exists).
immutable_fields = (
"task_id",
"job_id",
"repository",
"base_oid",
"target_branch",
"generation",
"idempotency_key",
)
for field in immutable_fields:
if getattr(candidate_binding, field) != getattr(binding, field):
raise ExecutionRebindError("EXECUTION_REBIND_BINDING_CONFLICT", 409)
head_advanced = candidate_binding.expected_branch_oid != binding.expected_branch_oid
if head_advanced:
allowed_modes = {
(binding.branch_mode, binding.branch_mode),
("CREATE_NEW", "CONTINUE_EXISTING"),
}
if (binding.branch_mode, candidate_binding.branch_mode) not in allowed_modes:
raise ExecutionRebindError("EXECUTION_REBIND_BRANCH_MODE_CONFLICT", 409)
authority_git_dir = "/data/aios-gitops/repo.git"
authority_ref = f"refs/heads/{binding.target_branch}"
try:
authority_head = subprocess.check_output(
[
"/usr/bin/git",
f"--git-dir={authority_git_dir}",
"rev-parse",
authority_ref,
],
text=True,
timeout=5,
).strip()
ancestry = subprocess.run(
[
"/usr/bin/git",
f"--git-dir={authority_git_dir}",
"merge-base",
"--is-ancestor",
binding.expected_branch_oid,
candidate_binding.expected_branch_oid,
],
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
timeout=5,
check=False,
)
except (OSError, subprocess.SubprocessError) as exc:
raise ExecutionRebindError("EXECUTION_REBIND_AUTHORITY_GIT_UNAVAILABLE", 503) from exc
if authority_head != candidate_binding.expected_branch_oid:
raise ExecutionRebindError("EXECUTION_REBIND_CURRENT_HEAD_MISMATCH", 409)
if ancestry.returncode != 0:
raise ExecutionRebindError("EXECUTION_REBIND_NON_FAST_FORWARD_DENY", 409)
elif candidate_binding.branch_mode != binding.branch_mode:
raise ExecutionRebindError("EXECUTION_REBIND_BRANCH_MODE_CONFLICT", 409)
# Validate the canonical envelope on its own after the continuation
# binding proof above. Worker-side strict equality remains unchanged.
new_execution = validate_execution_envelope(resolved)
# Re-anchor may legitimately remain source_capture when no deterministic
# plan is registered yet. A non-head-changing rebind must still advance
# source_capture -> deterministic.
if new_execution.get("action") not in {"source_capture", "deterministic"}:
raise ExecutionRebindError("EXECUTION_REBIND_ACTION_INVALID", 409)
if not head_advanced and new_execution.get("action") != "deterministic":
raise ExecutionRebindError("EXECUTION_REBIND_DETERMINISTIC_PLAN_REQUIRED", 409)
if new_execution["execution_digest"] != request.expected_new_execution_digest:
raise ExecutionRebindError("EXECUTION_REBIND_NEW_DIGEST_DRIFT", 409)
if new_execution["execution_digest"] == old_execution["execution_digest"]:
raise ExecutionRebindError("EXECUTION_REBIND_DIGEST_UNCHANGED", 409)
'''
replace_once(path, old, new, "same-job fast-forward execution re-anchor")
old_event = ''' "old_action": old_execution.get("action"),
"new_action": new_execution.get("action"),
"previous_revision": request.expected_revision,
'''
new_event = ''' "old_action": old_execution.get("action"),
"new_action": new_execution.get("action"),
"old_expected_branch_oid": binding.expected_branch_oid,
"new_expected_branch_oid": candidate_binding.expected_branch_oid,
"head_advanced": head_advanced,
"previous_revision": request.expected_revision,
'''
replace_once(path, old_event, new_event, "re-anchor audit evidence")
syntax_check(path)
print("RESULT: PATCH_OK")
return 0
if __name__ == "__main__":
raise SystemExit(main())
+141
View File
@@ -0,0 +1,141 @@
#!/usr/bin/env python3
from __future__ import annotations
import argparse
import ast
from datetime import datetime, timezone
import pathlib
import shutil
import sys
UTC = timezone.utc
def die(msg: str) -> None:
print(f"ERROR: {msg}", file=sys.stderr)
raise SystemExit(2)
def backup(path: pathlib.Path) -> pathlib.Path:
stamp = datetime.now(UTC).strftime("%Y%m%dT%H%M%SZ")
out = path.with_name(path.name + f".bak.{stamp}")
shutil.copy2(path, out)
return out
def ensure_datetime_import(path: pathlib.Path) -> bool:
text = path.read_text(encoding="utf-8")
target = "from datetime import datetime, timezone\n"
if target in text:
return False
lines = text.splitlines(keepends=True)
at = 0
if lines and lines[0].startswith("#!"):
at = 1
if at < len(lines) and lines[at].startswith("from __future__ import"):
at += 1
b = backup(path)
lines.insert(at, target)
path.write_text("".join(lines), encoding="utf-8")
print(f"PATCHED datetime import: {path}; backup={b}")
return True
def replace_once(path: pathlib.Path, old: str, new: str, label: str) -> bool:
text = path.read_text(encoding="utf-8")
if new in text:
print(f"OK already patched: {label}: {path}")
return False
n = text.count(old)
if n != 1:
die(f"{label}: expected exactly 1 match, found {n}: {path}")
b = backup(path)
path.write_text(text.replace(old, new, 1), encoding="utf-8")
print(f"PATCHED {label}: {path}; backup={b}")
return True
def syntax_check(path: pathlib.Path) -> None:
ast.parse(path.read_text(encoding="utf-8"), filename=str(path))
print(f"OK syntax: {path}")
def main() -> int:
ap = argparse.ArgumentParser()
ap.add_argument("--rebind", required=True)
ap.add_argument("--worker", required=True)
ap.add_argument("--bridge", required=True)
args = ap.parse_args()
rebind = pathlib.Path(args.rebind).resolve()
worker = pathlib.Path(args.worker).resolve()
bridge = pathlib.Path(args.bridge).resolve()
for p in (rebind, worker, bridge):
if not p.is_file():
die(f"missing file: {p}")
old_rebind = ''' if task["lease_token_hash"] not in (None, "") or task["lease_expires_at"] not in (None, ""):
raise ExecutionRebindError("EXECUTION_REBIND_ACTIVE_LEASE_DENY", 409)
'''
new_rebind = ''' # A persisted lease token is not by itself proof of an active lease.
lease_expires_at = task["lease_expires_at"]
if lease_expires_at not in (None, ""):
try:
parsed_lease_expires_at = datetime.fromisoformat(
str(lease_expires_at).replace("Z", "+00:00")
).astimezone(timezone.utc)
except (TypeError, ValueError) as exc:
raise ExecutionRebindError("EXECUTION_REBIND_LEASE_EXPIRY_INVALID", 409) from exc
if parsed_lease_expires_at > datetime.now(timezone.utc):
raise ExecutionRebindError("EXECUTION_REBIND_ACTIVE_LEASE_DENY", 409)
'''
old_worker = ''' active = [
item for item in leases
if isinstance(item, Mapping)
and item.get("state") == "ACTIVE"
and item.get("job_id") == envelope.job_id
and item.get("branch") == envelope.target_branch
]
'''
new_worker = ''' now = datetime.now(timezone.utc)
active = []
for item in leases:
if (
not isinstance(item, Mapping)
or item.get("state") != "ACTIVE"
or item.get("job_id") != envelope.job_id
or item.get("branch") != envelope.target_branch
):
continue
try:
expires_at = datetime.fromisoformat(
str(item.get("lease_expires_at") or "").replace("Z", "+00:00")
).astimezone(timezone.utc)
except (TypeError, ValueError):
continue
if expires_at > now:
active.append(item)
'''
old_bridge = ''' if durable_expires_at <= datetime.now(timezone.utc):
return self.renew_writer_lease(request)
'''
new_bridge = ''' # Expired durable lease is recoverable on the same task/job/generation.
if durable_expires_at <= datetime.now(timezone.utc):
return self.renew_writer_lease(request)
'''
changed = []
for p in (rebind, worker):
if ensure_datetime_import(p):
changed.append(p)
if replace_once(rebind, old_rebind, new_rebind, "execution-rebind effective lease expiry"):
changed.append(rebind)
if replace_once(worker, old_worker, new_worker, "worker effective lease expiry"):
changed.append(worker)
if replace_once(bridge, old_bridge, new_bridge, "bridge actual-runtime marker"):
changed.append(bridge)
for p in (rebind, worker, bridge):
syntax_check(p)
print("RESULT: PATCH_OK")
print("CHANGED:", ",".join(dict.fromkeys(str(p) for p in changed)) if changed else "none")
return 0
if __name__ == "__main__":
raise SystemExit(main())
+63
View File
@@ -0,0 +1,63 @@
#!/usr/bin/env python3
from __future__ import annotations
import argparse
import ast
from datetime import datetime, timezone
import pathlib
import shutil
import sys
UTC = timezone.utc
def die(msg: str) -> None:
print(f"ERROR: {msg}", file=sys.stderr)
raise SystemExit(2)
def backup(path: pathlib.Path) -> pathlib.Path:
stamp = datetime.now(UTC).strftime("%Y%m%dT%H%M%SZ")
out = path.with_name(path.name + f".bak.{stamp}")
shutil.copy2(path, out)
return out
def main() -> int:
ap = argparse.ArgumentParser(description="Allow pre-claim execution rebind at attempt=0.")
ap.add_argument("--rebind", required=True)
args = ap.parse_args()
path = pathlib.Path(args.rebind).resolve()
if not path.is_file():
die(f"missing rebind source: {path}")
text = path.read_text(encoding="utf-8")
old = " or attempt < 1\n"
new = " or attempt < 0\n"
if new in text:
print("OK already patched: pre-claim attempt=0")
else:
count = text.count(old)
if count != 1:
die(f"expected exactly one attempt<1 guard, found {count}")
b = backup(path)
path.write_text(text.replace(old, new, 1), encoding="utf-8")
print(f"PATCHED pre-claim attempt=0; backup={b}")
ast.parse(path.read_text(encoding="utf-8"), filename=str(path))
current = path.read_text(encoding="utf-8")
for marker in (
"or attempt < 0",
"EXECUTION_REBIND_NON_FAST_FORWARD_DENY",
"old_expected_branch_oid",
"new_expected_branch_oid",
):
if marker not in current:
die(f"required marker missing: {marker}")
print(f"OK syntax: {path}")
print("RESULT: PRECLAIM_REBIND_PATCH_OK")
return 0
if __name__ == "__main__":
raise SystemExit(main())
+145
View File
@@ -0,0 +1,145 @@
#!/usr/bin/env python3
from __future__ import annotations
import argparse
import ast
import datetime as dt
import pathlib
import shutil
import sys
UTC = dt.timezone.utc
def die(msg: str) -> None:
print(f"ERROR: {msg}", file=sys.stderr)
raise SystemExit(2)
def backup(path: pathlib.Path) -> pathlib.Path:
stamp = dt.datetime.now(UTC).strftime("%Y%m%dT%H%M%SZ")
out = path.with_suffix(path.suffix + f".bak.{stamp}")
shutil.copy2(path, out)
return out
def replace_once(path: pathlib.Path, old: str, new: str, label: str) -> bool:
text = path.read_text(encoding="utf-8")
if new in text:
print(f"OK already patched: {label}")
return False
count = text.count(old)
if count != 1:
die(f"{label}: expected exactly 1 match, found {count} in {path}")
backup_path = backup(path)
path.write_text(text.replace(old, new, 1), encoding="utf-8")
print(f"PATCHED {label}; backup={backup_path}")
return True
def ensure_datetime_import(path: pathlib.Path) -> bool:
text = path.read_text(encoding="utf-8")
target = "from datetime import datetime, timezone\n"
if target in text:
return False
lines = text.splitlines(keepends=True)
insert_at = 0
if lines and lines[0].startswith("#!"):
insert_at = 1
if insert_at < len(lines) and lines[insert_at].startswith("from __future__ import"):
insert_at += 1
lines.insert(insert_at, target)
backup_path = backup(path)
path.write_text("".join(lines), encoding="utf-8")
print(f"PATCHED datetime import: {path}; backup={backup_path}")
return True
def syntax_check(path: pathlib.Path) -> None:
ast.parse(path.read_text(encoding="utf-8"), filename=str(path))
print(f"OK syntax: {path}")
def main() -> int:
ap = argparse.ArgumentParser(description="Repair stale governed writer lease handling.")
ap.add_argument("--app-dir", default="/opt/aios/current/active-runtime/app")
args = ap.parse_args()
app = pathlib.Path(args.app_dir)
rebind = app / "aios_agent_task_execution_rebind.py"
worker = app / "aios_online_agent_worker.py"
bridge = app / "aios_chatgpt_bridge_v2_server.py"
for p in (rebind, worker, bridge):
if not p.is_file():
die(f"missing file: {p}")
old_rebind = ''' if task["lease_token_hash"] not in (None, "") or task["lease_expires_at"] not in (None, ""):
raise ExecutionRebindError("EXECUTION_REBIND_ACTIVE_LEASE_DENY", 409)
'''
new_rebind = ''' # A persisted lease token is not by itself proof of an active lease.
# Treat an unexpired lease as active; stale lease projection is recoverable.
lease_expires_at = task["lease_expires_at"]
if lease_expires_at not in (None, ""):
try:
parsed_lease_expires_at = datetime.fromisoformat(
str(lease_expires_at).replace("Z", "+00:00")
).astimezone(timezone.utc)
except (TypeError, ValueError) as exc:
raise ExecutionRebindError("EXECUTION_REBIND_LEASE_EXPIRY_INVALID", 409) from exc
if parsed_lease_expires_at > datetime.now(timezone.utc):
raise ExecutionRebindError("EXECUTION_REBIND_ACTIVE_LEASE_DENY", 409)
'''
old_worker = ''' active = [
item for item in leases
if isinstance(item, Mapping)
and item.get("state") == "ACTIVE"
and item.get("job_id") == envelope.job_id
and item.get("branch") == envelope.target_branch
]
'''
new_worker = ''' now = datetime.now(timezone.utc)
active = []
for item in leases:
if (
not isinstance(item, Mapping)
or item.get("state") != "ACTIVE"
or item.get("job_id") != envelope.job_id
or item.get("branch") != envelope.target_branch
):
continue
try:
expires_at = datetime.fromisoformat(
str(item.get("lease_expires_at") or "").replace("Z", "+00:00")
).astimezone(timezone.utc)
except (TypeError, ValueError):
continue
if expires_at > now:
active.append(item)
'''
old_bridge = ''' if durable_expires_at <= datetime.now(timezone.utc):
return self.renew_writer_lease(request)
'''
new_bridge = ''' # An expired durable lease is recoverable on the same governed
# task/job/generation. Renew it instead of treating stale ACTIVE state
# as a durable-binding failure.
if durable_expires_at <= datetime.now(timezone.utc):
return self.renew_writer_lease(request)
'''
changed = []
if ensure_datetime_import(rebind):
changed.append(rebind)
if ensure_datetime_import(worker):
changed.append(worker)
if replace_once(rebind, old_rebind, new_rebind, "execution-rebind effective lease expiry"):
changed.append(rebind)
if replace_once(worker, old_worker, new_worker, "worker converged-job effective lease expiry"):
changed.append(worker)
if replace_once(bridge, old_bridge, new_bridge, "bridge expired-lease recovery contract"):
changed.append(bridge)
for p in (rebind, worker, bridge):
syntax_check(p)
print("RESULT: PATCH_OK")
print("CHANGED:", ",".join(dict.fromkeys(str(p) for p in changed)) if changed else "none")
return 0
if __name__ == "__main__":
raise SystemExit(main())