Compare commits

..
27 changed files with 3633 additions and 1403 deletions
@@ -94,6 +94,91 @@ class AuthorityGitExecutor:
self._git(["worktree", "add", "--detach", str(path), base_oid])
return path
def recover_failed_detached_worktree_exact(
self,
*,
path: Path,
expected_oid: str,
stale_oid: str,
target_ref: str,
) -> str:
"""Recreate one exact clean, detached, single-attempt worktree at its durable head."""
_oid(expected_oid, "expected_oid")
_oid(stale_oid, "stale_oid")
if not TASK_BRANCH_RE.fullmatch(target_ref):
raise AuthorityGitError("TASK_BRANCH_SCOPE_DENIED")
if self.read_ref(target_ref) is not None:
raise AuthorityGitError("AUTHORITY_TARGET_BRANCH_ALREADY_EXISTS")
if (
not path.is_absolute()
or path.parent != self.worktree_root
or path.name in {"", ".", ".."}
or path.is_symlink()
or not path.is_dir()
):
raise AuthorityGitError("WORKTREE_RECOVERY_PATH_INVALID")
head = subprocess.run(
["git", "-C", str(path), "rev-parse", "HEAD"],
text=True, capture_output=True, check=False,
)
if head.returncode != 0 or head.stdout.strip() != stale_oid or stale_oid == expected_oid:
raise AuthorityGitError("WORKTREE_RECOVERY_HEAD_INVALID")
symbolic = subprocess.run(
["git", "-C", str(path), "symbolic-ref", "-q", "HEAD"],
text=True, capture_output=True, check=False,
)
if symbolic.returncode != 1:
raise AuthorityGitError("WORKTREE_RECOVERY_NOT_DETACHED")
status = subprocess.run(
["git", "-C", str(path), "status", "--porcelain=v1", "--untracked-files=all"],
text=True, capture_output=True, check=False,
)
if status.returncode != 0 or status.stdout:
raise AuthorityGitError("WORKTREE_RECOVERY_NOT_CLEAN")
parents = self._git(["rev-list", "--parents", "-n", "1", stale_oid]).split()
if parents != [stale_oid, expected_oid]:
raise AuthorityGitError("WORKTREE_RECOVERY_LINEAGE_INVALID")
registered = [
line.removeprefix("worktree ")
for line in self._git(["worktree", "list", "--porcelain"]).splitlines()
if line.startswith("worktree ")
]
if registered.count(str(path)) != 1:
raise AuthorityGitError("WORKTREE_RECOVERY_REGISTRATION_INVALID")
self._git(["worktree", "remove", "--force", str(path)])
if path.exists() or path.is_symlink():
raise AuthorityGitError("WORKTREE_RECOVERY_REMOVE_FAILED")
recreated = self.create_detached_worktree(expected_oid, path.name, path.parent)
readback = subprocess.run(
["git", "-C", str(recreated), "rev-parse", "HEAD"],
text=True, capture_output=True, check=False,
)
detached = subprocess.run(
["git", "-C", str(recreated), "symbolic-ref", "-q", "HEAD"],
text=True, capture_output=True, check=False,
)
if readback.returncode != 0 or readback.stdout.strip() != expected_oid or detached.returncode != 1:
raise AuthorityGitError("WORKTREE_RECOVERY_READBACK_MISMATCH")
return stale_oid
def create_task_branch_CAS(self, branch: str, expected_base_oid: str, new_oid: str) -> str:
if not TASK_BRANCH_RE.fullmatch(branch):
raise AuthorityGitError("TASK_BRANCH_SCOPE_DENIED")
_oid(expected_base_oid, "expected_base_oid")
_oid(new_oid, "new_oid")
current = self.read_ref(branch)
if current is not None:
if current == new_oid:
return new_oid
raise AuthorityGitError("AUTHORITY_TARGET_BRANCH_ALREADY_EXISTS")
if not self.ancestry_check(expected_base_oid, new_oid):
raise AuthorityGitError("NEW_OID_NOT_DESCENDANT")
self._git(["update-ref", branch, new_oid, "0" * 40])
readback = self.read_ref(branch)
if readback != new_oid:
raise AuthorityGitError("AUTHORITY_CREATE_READBACK_MISMATCH")
return readback
def advance_task_branch_CAS(self, branch: str, expected_oid: str, new_oid: str) -> str:
if not TASK_BRANCH_RE.fullmatch(branch):
raise AuthorityGitError("TASK_BRANCH_SCOPE_DENIED")
@@ -13,6 +13,8 @@ BRIDGE_OPERATIONS = (
"getGovernedWriteCapabilities",
"createGovernedTask",
"acquireWriterLease",
"bootstrapWriterLease",
"renewWriterLease",
"commitFileGoverned",
"commitFilesGoverned",
"getGitOpsJob",
@@ -31,6 +33,8 @@ BRIDGE_OPERATIONS = (
OPERATION_REQUIRED_FIELDS = {
"createGovernedTask": ("task_id", "repository", "base_oid", "target_branch", "risk", "description", "idempotency_key"),
"acquireWriterLease": ("schema", "task_id", "repository", "branch_mode", "base_oid", "expected_branch_oid", "target_branch", "generation", "lease_seconds"),
"bootstrapWriterLease": ("schema", "task_id", "repository", "branch_mode", "base_oid", "expected_branch_oid", "target_branch", "generation", "lease_seconds"),
"renewWriterLease": ("schema", "task_id", "repository", "branch_mode", "base_oid", "expected_branch_oid", "target_branch", "generation", "lease_seconds"),
"commitFileGoverned": ("task_id", "repository", "expected_branch_oid", "target_branch", "generation", "idempotency_key", "path", "content_base64", "message"),
"commitFilesGoverned": ("task_id", "repository", "expected_branch_oid", "target_branch", "generation", "idempotency_key", "files", "message"),
"getGitOpsJob": ("job_id",),
@@ -46,6 +50,49 @@ OPERATION_REQUIRED_FIELDS = {
"getControlPlaneSnapshot": (),
}
BRIDGE_OPERATION_CONTRACTS = {
"acquireWriterLease": {
"method": "POST",
"path": "/v2/writer-lease",
"required_fields": OPERATION_REQUIRED_FIELDS["acquireWriterLease"],
"response_fields": (
"task_id", "job_id", "generation", "lease_seconds", "lease_expires_at",
"create_target_branch", "worktree_mode", "cas_ref_update_required",
),
},
"bootstrapWriterLease": {
"method": "POST",
"path": "/v2/writer-lease/bootstrap",
"required_fields": OPERATION_REQUIRED_FIELDS["bootstrapWriterLease"],
"response_fields": (
"task_id", "job_id", "generation", "lease_seconds", "lease_expires_at",
"create_target_branch", "worktree_mode", "cas_ref_update_required",
"recovered", "bootstrapped", "runtime_rehydrated",
"durable_branch_lease_present", "idempotent",
),
},
"renewWriterLease": {
"method": "POST",
"path": "/v2/writer-lease/renew",
"required_fields": OPERATION_REQUIRED_FIELDS["renewWriterLease"],
"response_fields": (
"task_id", "job_id", "generation", "lease_seconds", "lease_expires_at",
"recovered", "bootstrapped", "runtime_rehydrated",
"durable_branch_lease_present", "idempotent",
),
},
"commitFileGoverned": {
"method": "POST", "path": "/v2/commit-file",
"required_fields": OPERATION_REQUIRED_FIELDS["commitFileGoverned"],
"response_fields": ("job_id", "commit_oid", "commit_visible", "branch_mode", "target_branch", "generation"),
},
"commitFilesGoverned": {
"method": "POST", "path": "/v2/commit-files",
"required_fields": OPERATION_REQUIRED_FIELDS["commitFilesGoverned"],
"response_fields": ("job_id", "commit_oid", "commit_visible", "branch_mode", "target_branch", "generation"),
},
}
class RuntimeDriftError(RuntimeError):
pass
@@ -122,6 +169,14 @@ class BridgeV2Adapter:
"legacy_writes_enabled": False,
"governed_writes_enabled": True,
"operations": list(BRIDGE_OPERATIONS),
"operation_contracts": {
operation: {
**contract,
"required_fields": list(contract["required_fields"]),
"response_fields": list(contract["response_fields"]),
}
for operation, contract in BRIDGE_OPERATION_CONTRACTS.items()
},
"force_push": False,
"main_write": False,
"tag_write": False,
@@ -151,6 +206,8 @@ class BridgeV2Adapter:
def createGovernedTask(self, request: Mapping[str, Any]) -> Any: return self._call("createGovernedTask", request)
def acquireWriterLease(self, request: Mapping[str, Any]) -> Any: return self._call("acquireWriterLease", request)
def bootstrapWriterLease(self, request: Mapping[str, Any]) -> Any: return self._call("bootstrapWriterLease", request)
def renewWriterLease(self, request: Mapping[str, Any]) -> Any: return self._call("renewWriterLease", request)
def commitFileGoverned(self, request: Mapping[str, Any]) -> Any: return self._call("commitFileGoverned", request)
def commitFilesGoverned(self, request: Mapping[str, Any]) -> Any: return self._call("commitFilesGoverned", request)
def getGitOpsJob(self, request: Mapping[str, Any]) -> Any: return self._call("getGitOpsJob", request)
File diff suppressed because it is too large Load Diff
+131 -33
View File
@@ -51,11 +51,13 @@ class AuthorityPort(Protocol):
def branch_exists(self, branch: str) -> bool: ...
def branch_head(self, branch: str) -> str: ...
def is_ancestor(self, older: str, newer: str) -> bool: ...
def create_task_branch_cas(self, branch: str, expected_base: str, new: str) -> str: ...
def compare_and_swap_branch(self, branch: str, expected: str, new: str) -> str: ...
class RemotePort(Protocol):
def repository_identity(self) -> str: ...
def push_new_exact(self, branch: str, new: str) -> None: ...
def push_exact(self, branch: str, expected: str, new: str) -> None: ...
def read_git_ref(self, branch: str) -> str | None: ...
def read_api_branch(self, branch: str) -> str | None: ...
@@ -103,6 +105,9 @@ class AuthorityGitAdapter:
def is_ancestor(self, older: str, newer: str) -> bool:
return self.executor.ancestry_check(older, newer)
def create_task_branch_cas(self, branch: str, expected_base: str, new: str) -> str:
return self.executor.create_task_branch_CAS(self._ref(branch), expected_base, new)
def compare_and_swap_branch(self, branch: str, expected: str, new: str) -> str:
return self.executor.advance_task_branch_CAS(self._ref(branch), expected, new)
@@ -258,6 +263,15 @@ class GiteaContinuationRemote:
raise ContinuationExecutionError("GITEA_GIT_READBACK_INVALID")
return oid
def push_new_exact(self, branch: str, new: str) -> None:
self._require_projected()
self.repository_identity()
if self.read_git_ref(branch) is not None:
raise ContinuationExecutionError("GITEA_TARGET_BRANCH_ALREADY_EXISTS")
_run(["git", "push", self.remote_name, f"{new}:refs/heads/{branch}"], cwd=self.workspace)
if self.read_git_ref(branch) != new:
raise ContinuationExecutionError("GITEA_CREATE_READBACK_MISMATCH")
def push_exact(self, branch: str, expected: str, new: str) -> None:
self._require_projected()
self.repository_identity()
@@ -329,6 +343,7 @@ class ContinuationPermissions:
@dataclass(frozen=True, slots=True)
class ContinuationRuntimeBinding:
request: WriterLeaseRequestV2
job_id: str
workspace_path: Path
lease_not_before: datetime
lease_expires_at: datetime
@@ -389,13 +404,20 @@ class ContinuationExecutor:
def observe_generation(self, branch: str, generation: int) -> None:
self._observed_generations[branch] = max(generation, self._observed_generations.get(branch, 0))
def _max_generation(self, branch: str) -> int:
def _max_generation(self, branch: str, *, exclude_job_id: str | None = None) -> int:
observed = self._observed_generations.get(branch, 0)
with self.controller.store._connect() as connection:
row = connection.execute(
"SELECT MAX(generation) AS generation FROM branch_leases WHERE branch=?",
(branch,),
).fetchone()
if exclude_job_id is None:
row = connection.execute(
"SELECT MAX(generation) AS generation FROM branch_leases WHERE branch=?",
(branch,),
).fetchone()
else:
row = connection.execute(
"SELECT MAX(generation) AS generation FROM branch_leases "
"WHERE branch=? AND job_id!=?",
(branch, exclude_job_id),
).fetchone()
durable = int(row["generation"] or 0)
return max(observed, durable)
@@ -404,6 +426,7 @@ class ContinuationExecutor:
return {
"schema": WriterLeaseRequestV2.schema,
"task_id": request.task_id,
"job_id": request.job_id,
"repository": request.repository,
"branch_mode": request.branch_mode,
"base_oid": request.base_oid,
@@ -413,10 +436,21 @@ class ContinuationExecutor:
"lease_seconds": request.lease_seconds,
}
def _validate_preflight(self, binding: ContinuationRuntimeBinding) -> None:
def _validate_preflight(self, binding: ContinuationRuntimeBinding, *, job_id: str | None = None) -> None:
request = binding.request
normalized = validate_writer_lease_request_v2(self._request_mapping(request), self.authority)
if normalized != {**self._request_mapping(request), "create_target_branch": False, "worktree_mode": "detached_at_expected_head", "cas_ref_update_required": True}:
request_mapping = self._request_mapping(request)
normalized = validate_writer_lease_request_v2(request_mapping, self.authority)
expected_normalized = {
**request_mapping,
"create_target_branch": request.branch_mode == "CREATE_NEW",
"worktree_mode": (
"detached_at_base_oid"
if request.branch_mode == "CREATE_NEW"
else "detached_at_expected_head"
),
"cas_ref_update_required": True,
}
if normalized != expected_normalized:
raise ContinuationExecutionError("CONTINUATION_NORMALIZATION_INVALID")
if not binding.original_receipt_valid or not binding.original_grant_consumed:
raise ContinuationExecutionError("ORIGINAL_GRANT_LINEAGE_INVALID")
@@ -426,7 +460,7 @@ class ContinuationExecutor:
raise ContinuationExecutionError("LEASE_NOT_ACTIVE")
if now >= binding.lease_expires_at:
raise ContinuationExecutionError("LEASE_EXPIRED")
if request.generation <= self._max_generation(request.target_branch):
if request.generation <= self._max_generation(request.target_branch, exclude_job_id=job_id):
raise ContinuationExecutionError("GENERATION_NOT_MONOTONIC")
if not self.workspace.is_detached():
raise ContinuationExecutionError("WORKTREE_NOT_DETACHED")
@@ -452,57 +486,102 @@ class ContinuationExecutor:
"generation": request.generation,
}
def execute(self, binding: ContinuationRuntimeBinding, commit: ContinuationCommit) -> dict[str, object]:
payload = commit.payload()
submitted = self.controller.submit(self._job_request(binding, commit, payload), payload=payload)
current = self.controller.store.read(submitted.job_id)
if JobState(current["state"]) == JobState.COMPLETED:
def execute(
self,
binding: ContinuationRuntimeBinding,
commit: ContinuationCommit,
*,
before_complete: Callable[[str], None] | None = None,
) -> dict[str, object]:
commit.payload()
if not binding.job_id or binding.request.job_id != binding.job_id:
raise ContinuationExecutionError("RUNTIME_LINEAGE_MISSING_JOB_ID")
current = self.controller.store.read(binding.job_id)
current_state = JobState(current["state"])
if current_state == JobState.COMPLETED:
return self.controller.store._receipt(current, duplicate=True).as_dict()
self._validate_preflight(binding)
if current_state == JobState.REMOTE_VERIFIED:
receipt = json.loads(str(current["receipt_json"] or "{}"))
commit_oid = str(receipt.get("authority_oid") or "")
if not OID_RE.fullmatch(commit_oid):
raise ContinuationExecutionError("GOVERNED_COMMIT_OID_INVALID")
if before_complete is not None:
before_complete(commit_oid)
return self.controller.advance(
binding.job_id,
JobState.COMPLETED,
side_effect_state="REMOTE_VERIFIED",
receipt=receipt,
).as_dict()
self._validate_preflight(binding, job_id=binding.job_id)
request = binding.request
projected = False
try:
self.credential.project()
projected = True
row = self.controller.store.read(submitted.job_id)
if JobState(row["state"]) == JobState.REQUESTED:
row = self.controller.store.read(binding.job_id)
state = JobState(row["state"])
if state == JobState.REQUESTED:
self.controller.advance(
submitted.job_id,
binding.job_id,
JobState.AUTHORIZED,
side_effect_state="AUTHORIZED",
receipt={"task_id": request.task_id, "original_grant_consumed": True},
expected_revision=int(row["revision"]),
)
row = self.controller.store.read(submitted.job_id)
if JobState(row["state"]) == JobState.AUTHORIZED:
row = self.controller.store.read(binding.job_id)
state = JobState(row["state"])
if state == JobState.AUTHORIZED:
self.controller.lease_job(
submitted.job_id,
lease_id=f"lease-{submitted.job_id}",
binding.job_id,
lease_id=f"lease-{binding.job_id}",
lineage_anchor_oid=request.base_oid,
current_head_oid=request.expected_branch_oid,
generation=request.generation,
lease_expires_at=binding.lease_expires_at.astimezone(timezone.utc).isoformat(),
expected_revision=int(row["revision"]),
)
self.controller.advance(submitted.job_id, JobState.WORKTREE_READY, side_effect_state="WORKTREE_READY")
self.controller.advance(submitted.job_id, JobState.EXECUTING, side_effect_state="IN_PROGRESS")
row = self.controller.store.read(binding.job_id)
state = JobState(row["state"])
if state == JobState.LEASED:
self.controller.advance(binding.job_id, JobState.WORKTREE_READY,
side_effect_state="WORKTREE_READY", expected_revision=int(row["revision"]))
row = self.controller.store.read(binding.job_id)
state = JobState(row["state"])
if state == JobState.WORKTREE_READY:
self.controller.advance(binding.job_id, JobState.EXECUTING,
side_effect_state="IN_PROGRESS", expected_revision=int(row["revision"]))
elif state != JobState.EXECUTING:
raise ContinuationExecutionError("JOB_EXECUTION_STATE_INVALID")
new_oid = self.workspace.commit_files(commit.files, commit.message)
if not OID_RE.fullmatch(new_oid) or not self.authority.is_ancestor(request.expected_branch_oid, new_oid):
raise ContinuationExecutionError("NON_FAST_FORWARD_COMMIT")
authority_oid = self.authority.compare_and_swap_branch(request.target_branch, request.expected_branch_oid, new_oid)
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.remote.push_exact(request.target_branch, request.expected_branch_oid, new_oid)
self.controller.advance(binding.job_id, JobState.REMOTE_SYNCING, side_effect_state="IN_PROGRESS")
if request.branch_mode == "CREATE_NEW":
self.remote.push_new_exact(request.target_branch, new_oid)
else:
self.remote.push_exact(request.target_branch, request.expected_branch_oid, new_oid)
except Exception:
return self._remote_pending(submitted.job_id, authority_oid, new_oid)
return self._finish_remote_verification(submitted.job_id, authority_oid, new_oid)
return self._remote_pending(binding.job_id, authority_oid, new_oid)
return self._finish_remote_verification(
binding.job_id,
authority_oid,
new_oid,
before_complete=before_complete,
)
finally:
if projected:
self.credential.revoke()
@@ -525,7 +604,14 @@ class ContinuationExecutor:
)
return result.as_dict()
def _finish_remote_verification(self, job_id: str, authority_oid: str, new_oid: str) -> dict[str, object]:
def _finish_remote_verification(
self,
job_id: str,
authority_oid: str,
new_oid: str,
*,
before_complete: Callable[[str], None] | None = None,
) -> dict[str, object]:
row = self.controller.store.read(job_id)
branch = row["branch"]
request = json.loads(row["request_json"])["binding"]
@@ -542,8 +628,20 @@ class ContinuationExecutor:
"reconciliation_decision": decision.action.value,
"force_push": False,
}
self.controller.advance(job_id, JobState.REMOTE_VERIFIED, side_effect_state="REMOTE_VERIFIED", receipt=receipt)
return self.controller.advance(job_id, JobState.COMPLETED, side_effect_state="REMOTE_VERIFIED", receipt=receipt).as_dict()
self.controller.advance(
job_id,
JobState.REMOTE_VERIFIED,
side_effect_state="REMOTE_VERIFIED",
receipt=receipt,
)
if before_complete is not None:
before_complete(new_oid)
return self.controller.advance(
job_id,
JobState.COMPLETED,
side_effect_state="REMOTE_VERIFIED",
receipt=receipt,
).as_dict()
def reconcile(self, job_id: str) -> dict[str, object]:
row = self.controller.store.read(job_id)
@@ -49,6 +49,7 @@ class WriterLeaseRequestV2:
target_branch: str
generation: int
lease_seconds: int = 900
job_id: str = ""
schema = "aios.task-writer-lease.v2"
accepted_schemas = frozenset({"aios.task-writer-lease.v2", "aios.task-writer-lease-request.v2"})
@@ -56,6 +57,8 @@ class WriterLeaseRequestV2:
def validate(self, probe: BranchProbe) -> None:
if not self.task_id or self.repository != "aios/aios":
raise ContinuationError("request_identity_invalid")
if not self.job_id or not self.job_id.startswith("job-"):
raise ContinuationError("job_id_invalid")
if self.branch_mode not in {"CREATE_NEW", "CONTINUE_EXISTING"}:
raise ContinuationError("branch_mode_invalid")
_oid(self.base_oid, "base_oid")
@@ -27,6 +27,19 @@ class ServiceUnavailable(JobError):
pass
@dataclass(frozen=True, slots=True)
class LeaseRenewalResult:
"""Durable outcome of one writer-lease recovery attempt."""
renewed: bool
idempotent: bool
durable: bool
lease_expires_at: str | None
def __bool__(self) -> bool:
return self.renewed or self.idempotent
class BindingConflict(JobError):
pass
@@ -66,6 +79,14 @@ def utc_now() -> str:
return datetime.now(timezone.utc).isoformat().replace("+00:00", "Z")
def parse_utc(value: str) -> datetime:
normalized = value[:-1] + "+00:00" if value.endswith("Z") else value
parsed = datetime.fromisoformat(normalized)
if parsed.tzinfo is None:
raise ValueError("LEASE_EXPIRY_INVALID")
return parsed.astimezone(timezone.utc)
def canonical_json(value: Any) -> bytes:
return (
json.dumps(value, sort_keys=True, separators=(",", ":"), ensure_ascii=False) + "\n"
@@ -404,13 +425,16 @@ class GitOpsJobController:
self.store._mirror(result)
return self.store._receipt(result, duplicate=True)
def update_lease(self, lease_id: str, *, expected_revision: int, current_head_oid: str, generation: int, state: str = "ACTIVE") -> None:
def update_lease(self, lease_id: str, *, expected_revision: int, current_head_oid: str, generation: int, state: str = "ACTIVE", lease_expires_at: str | None = None) -> None:
if len(current_head_oid) != 40 or generation < 1:
raise JobError("LEASE_BINDING_INVALID")
try:
with self.store._connect() as connection:
connection.execute("BEGIN IMMEDIATE")
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))
if lease_expires_at is None:
updated = connection.execute("UPDATE branch_leases SET current_head_oid=?,generation=?,state=?,revision=revision+1 WHERE lease_id=? AND revision=?", (current_head_oid, generation, state, lease_id, expected_revision))
else:
updated = connection.execute("UPDATE branch_leases SET current_head_oid=?,generation=?,lease_expires_at=?,state=?,revision=revision+1 WHERE lease_id=? AND revision=?", (current_head_oid, generation, lease_expires_at, state, lease_id, expected_revision))
if updated.rowcount != 1:
raise JobError("LEASE_REVISION_CAS_CONFLICT")
connection.commit()
@@ -419,6 +443,94 @@ class GitOpsJobController:
except sqlite3.Error as exc:
raise ServiceUnavailable("JOB_DB_UNAVAILABLE") from exc
def renew_lease(
self,
*,
branch: str,
lineage_anchor_oid: str,
current_head_oid: str,
generation: int,
lease_expires_at: str,
) -> LeaseRenewalResult:
if (
not branch
or len(lineage_anchor_oid) != 40
or len(current_head_oid) != 40
or generation < 1
or not lease_expires_at.strip()
):
raise JobError("LEASE_BINDING_INVALID")
try:
with self.store._connect() as connection:
connection.execute("BEGIN IMMEDIATE")
rows = connection.execute(
"SELECT lease_id, revision, generation, state, lease_expires_at "
"FROM branch_leases "
"WHERE branch=? AND lineage_anchor_oid=? AND current_head_oid=? "
"AND generation=? AND state='ACTIVE' ORDER BY revision DESC",
(branch, lineage_anchor_oid, current_head_oid, generation),
).fetchall()
if not rows:
connection.commit()
return LeaseRenewalResult(False, False, False, None)
if len(rows) != 1:
raise JobError("LEASE_BINDING_CONFLICT")
row = rows[0]
try:
current_expires_at = parse_utc(str(row["lease_expires_at"]))
requested_expires_at = parse_utc(lease_expires_at)
except (TypeError, ValueError) as exc:
raise JobError("LEASE_EXPIRY_INVALID") from exc
now = datetime.now(timezone.utc)
if requested_expires_at <= now:
raise JobError("LEASE_EXPIRY_INVALID")
if current_expires_at > now:
connection.commit()
return LeaseRenewalResult(
False,
True,
True,
str(row["lease_expires_at"]),
)
updated = connection.execute(
"UPDATE branch_leases SET lease_expires_at=?,revision=revision+1 "
"WHERE lease_id=? AND revision=? AND generation=? "
"AND state='ACTIVE' AND lease_expires_at=?",
(
lease_expires_at,
row["lease_id"],
int(row["revision"]),
generation,
row["lease_expires_at"],
),
)
if updated.rowcount != 1:
current = connection.execute(
"SELECT lease_expires_at, generation, state FROM branch_leases "
"WHERE lease_id=?",
(row["lease_id"],),
).fetchone()
if (
current is not None
and int(current["generation"]) == generation
and current["state"] == "ACTIVE"
and parse_utc(str(current["lease_expires_at"])) > now
):
connection.commit()
return LeaseRenewalResult(
False,
True,
True,
str(current["lease_expires_at"]),
)
raise JobError("LEASE_REVISION_CAS_CONFLICT")
connection.commit()
return LeaseRenewalResult(True, False, True, lease_expires_at)
except JobError:
raise
except sqlite3.Error as exc:
raise ServiceUnavailable("JOB_DB_UNAVAILABLE") from exc
def advance(self, job_id: str, state: JobState, *, side_effect_state: str, receipt: Mapping[str, Any] | None = None, expected_revision: int | None = None) -> JobReceipt:
return self._advance(
job_id,
@@ -964,6 +964,7 @@ def validate_writer_lease_request_v2(raw: Mapping[str, Any], probe: Any) -> dict
required = {
"schema",
"task_id",
"job_id",
"repository",
"branch_mode",
"base_oid",
@@ -976,6 +977,7 @@ def validate_writer_lease_request_v2(raw: Mapping[str, Any], probe: Any) -> dict
raise ProvisioningError("LEASE_REQUEST_V2_FIELDS_REJECTED", 409)
request = WriterLeaseRequestV2(
task_id=str(raw["task_id"]),
job_id=str(raw["job_id"]),
repository=str(raw["repository"]),
branch_mode=str(raw["branch_mode"]),
base_oid=str(raw["base_oid"]),
@@ -988,6 +990,7 @@ def validate_writer_lease_request_v2(raw: Mapping[str, Any], probe: Any) -> dict
return {
"schema": WriterLeaseRequestV2.schema,
"task_id": request.task_id,
"job_id": request.job_id,
"repository": request.repository,
"branch_mode": request.branch_mode,
"base_oid": request.base_oid,
+56 -26
View File
@@ -34,6 +34,7 @@ from .models import (
)
from .policy import PolicyEngine
from .store import TaskPlaneStore
from .scope import bind_task_scope
from .worker_registry import WorkerRegistry
@@ -68,28 +69,54 @@ class TaskPlaneService:
self.delegation = DelegationEngine(store)
self.store.probe_attestation_verifier = self.policy.probe_attestation_verifier
def create_task(self, body: dict[str, Any], idempotency_key: str | None = None) -> dict[str, Any]:
def _authorized_task(self, task_id: str, actor: str, action: str) -> TaskRecord:
task = self.store.get_task(task_id, self.policy.actor_context(actor))
self.policy.assert_allowed(action, actor, task=task)
return task
def create_task(
self,
body: dict[str, Any],
idempotency_key: str | None = None,
*,
actor_identity: str | None = None,
) -> dict[str, Any]:
if body.get("parent_task_id"):
raise ForbiddenError("child tasks must use the delegate endpoint")
issuer = body.get("issuer", {})
actor = issuer.get("id", "anon") if isinstance(issuer, dict) else "anon"
declared_actor = issuer.get("id", "anon") if isinstance(issuer, dict) else "anon"
actor = actor_identity or declared_actor
self.policy.assert_allowed("create", actor)
try:
plan = TaskPlan.from_dict(body)
except (TypeError, ValueError) as exc:
raise ValidationError("invalid task plan") from exc
if actor_identity is not None:
plan = bind_task_scope(
plan, self.policy.actor_context(actor_identity), public_create=True
)
task = self.store.create_task_with_event(plan, idempotency_key=idempotency_key)
return task.to_dict()
def list_tasks(self, actor: str | None = None) -> list[dict[str, Any]]:
context = None
if actor is not None:
self.policy.assert_allowed("read", actor)
return [task.to_dict(include_task=False) for task in self.store.list_tasks()]
context = self.policy.actor_context(actor)
return [
task.to_dict(include_task=False)
for task in self.store.list_tasks(context)
]
def get_task(self, task_id: str, actor: str | None = None) -> dict[str, Any]:
context = None
if actor is not None:
self.policy.assert_allowed("read", actor, task=self.store.get_task(task_id))
return self.store.get_task(task_id).to_dict()
self.policy.assert_allowed("read", actor)
context = self.policy.actor_context(actor)
task = self.store.get_task(task_id, context)
if actor is not None:
self.policy.assert_allowed("read", actor, task=task)
return task.to_dict()
def get_worker(self, worker_id: str, actor: str | None = None) -> dict[str, Any]:
if actor is not None:
@@ -163,9 +190,8 @@ class TaskPlaneService:
if not worker_id:
raise ValidationError("worker_id required")
task = self.store.get_task(task_id)
task = self._authorized_task(task_id, actor, "claim")
self.policy.assert_readonly_worker_binding(actor, str(worker_id))
self.policy.assert_allowed("claim", actor, task=task)
lease_seconds = _bounded_int(
body.get("lease_seconds", 30), "lease_seconds", 1, 3600
)
@@ -184,10 +210,9 @@ class TaskPlaneService:
if not claim_id:
raise ValidationError("claim_id required")
actor = body.get("actor_identity") or body.get("worker_identity") or "worker"
task = self.store.get_task(task_id)
task = self._authorized_task(task_id, actor, "heartbeat")
claim = self.store.get_claim(claim_id)
self.policy.assert_readonly_worker_binding(actor, claim.worker_id)
self.policy.assert_allowed("heartbeat", actor, task=task)
if claim.worker_identity != actor:
raise ForbiddenError("claim identity mismatch")
lease_seconds = _bounded_int(
@@ -206,10 +231,9 @@ class TaskPlaneService:
message = body.get("message")
if progress is None:
raise ValidationError("progress required")
task = self.store.get_task(task_id)
task = self._authorized_task(task_id, actor, "progress")
claim = self.store.get_claim(claim_id)
self.policy.assert_readonly_worker_binding(actor, claim.worker_id)
self.policy.assert_allowed("progress", actor, task=task)
if task.task_plan.data_classification == "credential-secret":
if message not in {None, ""} or contains_secret_text(stage):
raise ValidationError(
@@ -234,7 +258,7 @@ class TaskPlaneService:
if not claim_id:
raise ValidationError("claim_id required")
actor = body.get("actor_identity") or body.get("worker_identity") or "worker"
task = self.store.get_task(task_id)
task = self._authorized_task(task_id, actor, "result")
status = body.get("status", "COMPLETED")
outputs = body.get("outputs", [])
@@ -247,7 +271,6 @@ class TaskPlaneService:
raise ValidationError("evidence items must be objects")
claim = self.store.get_claim(claim_id)
self.policy.assert_readonly_worker_binding(actor, claim.worker_id)
self.policy.assert_allowed("result", actor, task=task)
if claim.worker_identity != actor:
raise ForbiddenError("claim identity mismatch")
if task.task_plan.data_classification == "credential-secret":
@@ -326,8 +349,7 @@ class TaskPlaneService:
reason = body.get("reason", "failed")
actor = body.get("actor_identity") or body.get("worker_identity") or "worker"
self.policy.assert_allowed("fail", actor)
task = self.store.get_task(task_id)
self.policy.assert_allowed("fail", actor, task=task)
task = self._authorized_task(task_id, actor, "fail")
claim = self.store.get_claim(claim_id)
if claim.worker_identity != actor:
raise ForbiddenError("claim identity mismatch")
@@ -339,8 +361,7 @@ class TaskPlaneService:
actor = body.get("actor_identity") or body.get("worker_identity") or "owner"
self.policy.assert_allowed("cancel", actor)
reason = body.get("reason", "cancelled")
task = self.store.get_task(task_id)
self.policy.assert_allowed("cancel", actor, task=task)
task = self._authorized_task(task_id, actor, "cancel")
updated = self.store.cancel_task_with_event(
task_id, task.revision, reason=reason
)
@@ -349,8 +370,7 @@ class TaskPlaneService:
def delegate(self, task_id: str, body: dict[str, Any]) -> dict[str, Any]:
actor = body.get("actor_identity") or "owner"
self.policy.assert_allowed("delegate", actor)
parent = self.store.get_task(task_id)
self.policy.assert_allowed("delegate", actor, task=parent)
parent = self._authorized_task(task_id, actor, "delegate")
if "task" in body:
try:
child_plan = TaskPlan.from_dict(body["task"])
@@ -364,25 +384,31 @@ class TaskPlaneService:
except (TypeError, ValueError) as exc:
raise ValidationError("invalid child task plan") from exc
child_plan.parent_task_id = task_id
child_plan = bind_task_scope(
child_plan, self.policy.actor_context(actor), public_create=False
)
child = self.delegation.delegate(task_id, child_plan, caller=actor)
return child.to_dict()
def get_events(self, task_id: str, actor: str | None = None) -> list[dict[str, Any]]:
if actor is not None:
self.policy.assert_allowed("audit", actor, task=self.store.get_task(task_id))
self.store.get_task(task_id)
self._authorized_task(task_id, actor, "audit")
else:
self.store.get_task(task_id)
return self.store.list_events(task_id)
def get_evidence(self, task_id: str, actor: str | None = None) -> list[dict[str, Any]]:
if actor is not None:
self.policy.assert_allowed("audit", actor, task=self.store.get_task(task_id))
self.store.get_task(task_id)
self._authorized_task(task_id, actor, "audit")
else:
self.store.get_task(task_id)
return self.evidence.list(task_id)
def get_children(self, task_id: str, actor: str | None = None) -> list[str]:
if actor is not None:
self.policy.assert_allowed("read", actor, task=self.store.get_task(task_id))
self.store.get_task(task_id)
self._authorized_task(task_id, actor, "read")
else:
self.store.get_task(task_id)
return self.store.list_children(task_id)
@@ -506,7 +532,11 @@ class TaskPlaneRequestHandler(BaseHTTPRequestHandler):
):
raise ForbiddenError("issuer identity mismatch")
body["issuer"] = {"type": "authenticated", "id": identity}
return 201, self.service.create_task(body or {}, headers.get("idempotency-key"))
return 201, self.service.create_task(
body or {},
headers.get("idempotency-key"),
actor_identity=identity,
)
if body.get("actor_identity") not in {None, identity}:
raise ForbiddenError("actor identity mismatch")
body["actor_identity"] = identity
+27
View File
@@ -13,6 +13,8 @@ from dataclasses import dataclass, field
from datetime import datetime, timezone
from typing import Any
from .scope import normalize_task_scope
SENSITIVE_KEYS = {
"secret",
@@ -66,6 +68,12 @@ TASK_PLAN_KEYS = {
"allowed_input_paths",
"allowed_output_paths",
"output_policy",
"scope_type",
"tenant_id",
"user_id",
"project_id",
"conversation_id",
"run_id",
}
@@ -237,6 +245,12 @@ class TaskPlan:
schema_version: int = 1
task_id: str | None = None
task_type: str = "code.change"
scope_type: str = "legacy"
tenant_id: str | None = None
user_id: str | None = None
project_id: str | None = None
conversation_id: str | None = None
run_id: str | None = None
issuer: dict[str, Any] = field(default_factory=dict)
intent: str | None = None
required_capabilities: list[str] = field(default_factory=list)
@@ -311,10 +325,17 @@ class TaskPlan:
raise ValueError("credential-secret requires metadata-only logging")
if output_policy != "hash-count-status-only":
raise ValueError("credential-secret requires restricted output")
scope = normalize_task_scope(data)
return cls(
schema_version=schema_version,
task_id=validate_identifier(data["task_id"], "task_id"),
task_type=validate_type(data.get("task_type", "code.change"), "task_type"),
scope_type=scope.scope_type,
tenant_id=scope.tenant_id,
user_id=scope.user_id,
project_id=scope.project_id,
conversation_id=scope.conversation_id,
run_id=scope.run_id,
issuer=issuer,
intent=data.get("intent"),
required_capabilities=_normalize_list(data.get("required_capabilities", [])),
@@ -348,6 +369,12 @@ class TaskPlan:
"schema_version": self.schema_version,
"task_id": self.task_id,
"task_type": self.task_type,
"scope_type": self.scope_type,
"tenant_id": self.tenant_id,
"user_id": self.user_id,
"project_id": self.project_id,
"conversation_id": self.conversation_id,
"run_id": self.run_id,
"issuer": self.issuer,
"intent": self.intent,
"required_capabilities": list(self.required_capabilities),
+20 -1
View File
@@ -3,10 +3,11 @@
from __future__ import annotations
from dataclasses import dataclass
from typing import Callable
from typing import Any, Callable, Mapping
from .errors import ForbiddenError, LockedError
from .models import ProbeEvidence, ResultEnvelope, TaskRecord
from .scope import TaskActorContext, actor_context as build_actor_context
RISK_RANK = {"low": 1, "medium": 2, "high": 3}
@@ -93,6 +94,10 @@ class PolicyEngine:
result_signature_verifier: Callable[[ResultEnvelope], bool] | None = None,
probe_attestation_verifier: Callable[[ProbeEvidence], bool] | None = None,
identity_capabilities: dict[str, set[str]] | None = None,
identity_actor_contexts: Mapping[
str, TaskActorContext | Mapping[str, Any]
]
| None = None,
admin_identities: set[str] | None = None,
readonly_worker_execution_enabled: bool = False,
readonly_worker_profiles: list[ReadonlyWorkerProfile] | None = None,
@@ -109,12 +114,26 @@ class PolicyEngine:
identity: set(capabilities)
for identity, capabilities in (identity_capabilities or {}).items()
}
self.identity_actor_contexts = dict(identity_actor_contexts or {})
self.admin_identities = set(admin_identities or ())
self.readonly_worker_execution_enabled = readonly_worker_execution_enabled
self.readonly_worker_profiles = {
profile.identity: profile for profile in (readonly_worker_profiles or [])
}
def actor_context(self, identity: str) -> TaskActorContext:
configured = self.identity_actor_contexts.get(
identity, self.identity_actor_contexts.get("*")
)
capabilities = set(
self.identity_capabilities.get(
identity, self.identity_capabilities.get("*", set())
)
)
if identity in self.admin_identities:
capabilities.add("task.admin")
return build_actor_context(identity, configured, capabilities)
def _readonly_worker_allows(
self, action: str, actor: str, task: TaskRecord | None
) -> bool:
+181
View File
@@ -0,0 +1,181 @@
"""Trusted TaskPlane scope model and SQL authorization helpers."""
from __future__ import annotations
from dataclasses import dataclass, replace
from typing import Any, Mapping
from .errors import ForbiddenError, ValidationError
SCOPE_FIELDS = ("tenant_id", "user_id", "project_id", "conversation_id", "run_id")
PRIVILEGED_CAPABILITIES = frozenset(
{"*", "admin", "system.admin", "task.admin", "task.read.all"}
)
def _value(value: Any, field: str) -> Any:
if hasattr(value, "task_plan"):
value = value.task_plan
if isinstance(value, Mapping):
return value.get(field)
return getattr(value, field, None)
def _text(value: Any) -> str | None:
if value is None:
return None
result = str(value).strip()
return result or None
@dataclass(frozen=True)
class TaskScope:
scope_type: str
tenant_id: str | None = None
user_id: str | None = None
project_id: str | None = None
conversation_id: str | None = None
run_id: str | None = None
@dataclass(frozen=True)
class TaskActorContext:
identity: str
scope_type: str = "legacy"
tenant_id: str | None = None
user_id: str | None = None
project_id: str | None = None
conversation_id: str | None = None
run_id: str | None = None
capabilities: frozenset[str] = frozenset()
def normalize_task_scope(value: Any) -> TaskScope:
fields = {field: _text(_value(value, field)) for field in SCOPE_FIELDS}
declared = _text(_value(value, "scope_type"))
if declared is None:
declared = "tenant" if fields["tenant_id"] else "legacy"
if declared == "tenant":
if fields["tenant_id"] is None:
raise ValidationError(
"tenant scope requires tenant_id", code="TASK_SCOPE_TENANT_ID_REQUIRED"
)
elif declared == "legacy":
if any(fields.values()):
raise ValidationError(
"legacy scope cannot contain scoped ownership",
code="TASK_SCOPE_LEGACY_WITH_SCOPED_FIELDS",
)
elif declared == "system":
if any(fields[field] for field in ("tenant_id", "user_id", "project_id", "conversation_id")):
raise ValidationError(
"system scope cannot contain tenant ownership",
code="TASK_SCOPE_SYSTEM_WITH_TENANT_FIELDS",
)
else:
raise ValidationError("invalid task scope type", code="TASK_SCOPE_TYPE_INVALID")
return TaskScope(declared, **fields)
def actor_is_privileged(actor: TaskActorContext) -> bool:
return bool(actor.capabilities & PRIVILEGED_CAPABILITIES)
def actor_context(
identity: str,
configured: TaskActorContext | Mapping[str, Any] | None,
capabilities: set[str] | frozenset[str],
) -> TaskActorContext:
if isinstance(configured, TaskActorContext):
context = configured
else:
data = dict(configured or {})
data.pop("identity", None)
data.pop("capabilities", None)
context = TaskActorContext(identity=str(identity), **data)
scope = normalize_task_scope(context)
return replace(
context,
identity=str(identity),
scope_type=scope.scope_type,
tenant_id=scope.tenant_id,
user_id=scope.user_id,
project_id=scope.project_id,
conversation_id=scope.conversation_id,
run_id=scope.run_id,
capabilities=frozenset(str(item) for item in capabilities),
)
def bind_task_scope(plan: Any, actor: TaskActorContext, *, public_create: bool) -> Any:
requested = normalize_task_scope(plan)
if actor_is_privileged(actor):
trusted = normalize_task_scope(actor)
if requested.scope_type == "legacy" and trusted.scope_type != "legacy":
plan.scope_type = trusted.scope_type
for field in SCOPE_FIELDS:
setattr(plan, field, getattr(actor, field))
return plan
trusted = normalize_task_scope(actor)
if public_create and trusted.scope_type == "legacy":
raise ForbiddenError(
"authenticated identity has no tenant scope",
code="TASK_ACTOR_SCOPE_UNAVAILABLE",
)
if trusted.scope_type != "tenant":
if requested.scope_type == "legacy":
plan.scope_type = trusted.scope_type
for field in SCOPE_FIELDS:
setattr(plan, field, getattr(actor, field))
return plan
if requested.scope_type != trusted.scope_type:
raise ForbiddenError("task scope mismatch", code="TASK_SCOPE_MISMATCH_DENY")
return plan
if requested.scope_type not in {"legacy", "tenant"}:
raise ForbiddenError("task scope mismatch", code="TASK_SCOPE_MISMATCH_DENY")
updates: dict[str, Any] = {"scope_type": "tenant"}
for field in SCOPE_FIELDS:
declared = getattr(requested, field)
authoritative = getattr(actor, field)
if declared is not None and declared != authoritative:
raise ForbiddenError(
"task scope mismatch",
code="TASK_SCOPE_MISMATCH_DENY",
details={"field": field},
)
updates[field] = authoritative
for field, item in updates.items():
setattr(plan, field, item)
return plan
def actor_scope_sql(actor: TaskActorContext | None) -> tuple[str, tuple[Any, ...]]:
if actor is None or actor_is_privileged(actor):
return "", ()
scope = normalize_task_scope(actor)
if scope.scope_type == "legacy":
return "scope_type = 'legacy'", ()
if scope.scope_type == "system":
return "scope_type = 'system'", ()
clauses = ["scope_type = 'tenant'", "tenant_id = ?"]
params: list[Any] = [scope.tenant_id]
for field in ("user_id", "project_id", "conversation_id", "run_id"):
value = getattr(scope, field)
if value is None:
clauses.append(f"{field} IS NULL")
else:
clauses.append(f"({field} IS NULL OR {field} = ?)")
params.append(value)
return " AND ".join(clauses), tuple(params)
def task_scope_values(value: Any) -> tuple[str | None, ...]:
scope = normalize_task_scope(value)
return (
scope.scope_type,
scope.tenant_id,
scope.user_id,
scope.project_id,
scope.conversation_id,
scope.run_id,
)
+95 -11
View File
@@ -32,6 +32,7 @@ from .models import (
redact_value,
validate_identifier,
)
from .scope import TaskActorContext, actor_scope_sql, task_scope_values
EventEnvelope = tuple[str, str, str, str, str, float]
@@ -134,6 +135,12 @@ class TaskPlaneStore:
status TEXT NOT NULL,
task_json TEXT NOT NULL,
task_type TEXT NOT NULL,
scope_type TEXT NOT NULL DEFAULT 'legacy',
tenant_id TEXT,
user_id TEXT,
project_id TEXT,
conversation_id TEXT,
run_id TEXT,
root_task_id TEXT NOT NULL,
parent_task_id TEXT,
correlation_id TEXT,
@@ -272,6 +279,21 @@ class TaskPlaneStore:
)
"""
)
self._ensure_columns(
"tasks",
{
"scope_type": "TEXT NOT NULL DEFAULT 'legacy'",
"tenant_id": "TEXT",
"user_id": "TEXT",
"project_id": "TEXT",
"conversation_id": "TEXT",
"run_id": "TEXT",
},
)
self.conn.execute(
"UPDATE tasks SET scope_type='legacy' "
"WHERE scope_type IS NULL OR TRIM(scope_type)=''"
)
self._ensure_columns(
"workers",
{
@@ -299,6 +321,14 @@ class TaskPlaneStore:
)
self.conn.execute("CREATE INDEX IF NOT EXISTS ix_tasks_status ON tasks(status)")
self.conn.execute("CREATE INDEX IF NOT EXISTS ix_tasks_parent ON tasks(parent_task_id)")
self.conn.execute(
"CREATE INDEX IF NOT EXISTS ix_tasks_scope_tenant_project "
"ON tasks(scope_type, tenant_id, project_id)"
)
self.conn.execute(
"CREATE INDEX IF NOT EXISTS ix_tasks_scope_owner "
"ON tasks(scope_type, tenant_id, user_id, conversation_id, run_id)"
)
self.conn.execute("CREATE INDEX IF NOT EXISTS ix_workers_status ON workers(status)")
self.conn.execute("CREATE INDEX IF NOT EXISTS ix_claims_task ON claims(task_id)")
self.conn.execute(
@@ -347,6 +377,42 @@ class TaskPlaneStore:
raise NotFoundError("task")
task_json = json.loads(row["task_json"])
task_plan = TaskPlan.from_dict(task_json)
stored_scope = tuple(
row[field]
for field in (
"scope_type",
"tenant_id",
"user_id",
"project_id",
"conversation_id",
"run_id",
)
)
json_has_scope = any(
field in task_json
for field in (
"scope_type",
"tenant_id",
"user_id",
"project_id",
"conversation_id",
"run_id",
)
)
if json_has_scope and stored_scope != task_scope_values(task_plan):
raise ConflictError(
"task scope persistence mismatch",
code="TASK_SCOPE_PERSISTENCE_MISMATCH",
)
if not json_has_scope:
(
task_plan.scope_type,
task_plan.tenant_id,
task_plan.user_id,
task_plan.project_id,
task_plan.conversation_id,
task_plan.run_id,
) = stored_scope
task_plan.parent_task_id = row["parent_task_id"]
task_plan.correlation_id = row["correlation_id"]
return TaskRecord(
@@ -389,6 +455,7 @@ class TaskPlaneStore:
task_id = task_plan.task_id or f"task-{uuid.uuid4()}"
task_plan.task_id = task_id
task_json = redact_value(task_plan.to_dict(redact=False))
scope_values = task_scope_values(task_plan)
now = _now()
with self._transaction():
if idempotency_key:
@@ -397,7 +464,11 @@ class TaskPlaneStore:
(idempotency_key,),
).fetchone()
if existing:
if _canonical(json.loads(existing["task_json"])) != _canonical(task_json):
existing_plan = TaskPlan.from_dict(
json.loads(existing["task_json"])
)
existing_json = redact_value(existing_plan.to_dict(redact=False))
if _canonical(existing_json) != _canonical(task_json):
raise ConflictError("idempotency key payload mismatch")
return self.get_task(existing["task_id"])
root_task_id = task_id
@@ -412,14 +483,18 @@ class TaskPlaneStore:
self.conn.execute(
"""
INSERT INTO tasks
(task_id, revision, status, task_json, task_type, root_task_id, parent_task_id, correlation_id, idempotency_key, created_at, updated_at, failure_reason)
VALUES (?, 1, ?, ?, ?, ?, ?, ?, ?, ?, ?, NULL)
(task_id, revision, status, task_json, task_type,
scope_type, tenant_id, user_id, project_id, conversation_id, run_id,
root_task_id, parent_task_id, correlation_id, idempotency_key,
created_at, updated_at, failure_reason)
VALUES (?, 1, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, NULL)
""",
(
task_id,
"CREATED",
json.dumps(task_json, ensure_ascii=False),
task_plan.task_type,
*scope_values,
root_task_id,
task_plan.parent_task_id,
task_plan.correlation_id,
@@ -451,21 +526,30 @@ class TaskPlaneStore:
)
return self.get_task(task.task_id)
def get_task(self, task_id: str) -> TaskRecord:
def get_task(
self, task_id: str, actor_context: TaskActorContext | None = None
) -> TaskRecord:
task_id = _validated_id(task_id, "task_id")
predicate, scope_params = actor_scope_sql(actor_context)
sql = "SELECT * FROM tasks WHERE task_id=?"
if predicate:
sql += f" AND ({predicate})"
with self._lock:
row = self.conn.execute(
"SELECT * FROM tasks WHERE task_id=?", (task_id,)
).fetchone()
row = self.conn.execute(sql, (task_id, *scope_params)).fetchone()
if row is None:
raise NotFoundError(f"task {task_id} not found")
return self._row_to_task(row)
def list_tasks(self) -> list[TaskRecord]:
def list_tasks(
self, actor_context: TaskActorContext | None = None
) -> list[TaskRecord]:
predicate, scope_params = actor_scope_sql(actor_context)
sql = "SELECT * FROM tasks"
if predicate:
sql += f" WHERE ({predicate})"
sql += " ORDER BY created_at DESC"
with self._lock:
rows = self.conn.execute(
"SELECT * FROM tasks ORDER BY created_at DESC"
).fetchall()
rows = self.conn.execute(sql, scope_params).fetchall()
return [self._row_to_task(row) for row in rows]
def get_task_budget(self, task_id: str) -> Budget:
@@ -1,46 +0,0 @@
[Unit]
Description=AIOS generic governed Online Agent Worker (2026-08-15)
After=aios-agent-task-api.service aios-online-agent-worker-register-20260815.service
Wants=aios-online-agent-worker-register-20260815.service
[Service]
Type=simple
User=aios-engineering-worker
Group=aios-engineering-worker
SupplementaryGroups=gitops
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-cn-network-mkt-generic-20260815
Environment=AIOS_AGENT_TYPE=trusted-online-engineering-worker
Environment=AIOS_AGENT_VERSION=2026-08-15.3-generic
Environment=AIOS_AUTHORITY_GIT_DIR=/data/aios-gitops/repo.git
Environment=AIOS_WORKER_STATE_ROOT=/var/lib/aios-online-agent-worker-20260815
Environment=AIOS_WORKER_WORKSPACE_ROOT=/var/lib/aios-online-agent-worker-20260815/workspaces
Environment=AIOS_WORKER_POLL_SECONDS=5
Environment=AIOS_WORKER_HEARTBEAT_SECONDS=10
Environment=AIOS_WORKER_LEASE_SECONDS=300
ExecStart=/usr/bin/python3 -B /usr/local/lib/aios-online-agent-worker-20260815/online_agent_worker.py
Restart=always
RestartSec=5
TimeoutStopSec=15
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-20260815
UMask=0027
[Install]
WantedBy=multi-user.target
@@ -1,36 +0,0 @@
[Unit]
Description=AIOS generic Online Agent register-or-recover (2026-08-15)
After=network-online.target aios-gitops-model-api.service aios-agent-task-api.service
Wants=network-online.target
Before=aios-online-agent-worker-20260815.service
[Service]
Type=oneshot
RemainAfterExit=yes
User=root
Group=root
LoadCredential=bridge_control_token:/run/credentials/aios-gitops-model-api.service/agent_task_control_token
Environment=AIOS_AGENT_TASK_API_URL=http://127.0.0.1:18940
Environment=AIOS_AGENT_ID=online-engineering-worker-cn-network-mkt-generic-20260815
Environment=AIOS_AGENT_TYPE=trusted-online-engineering-worker
Environment=AIOS_AGENT_VERSION=2026-08-15.3-generic
Environment=AIOS_WORKER_ENCRYPTED_CREDENTIAL=/etc/credstore.encrypted/aios-online-agent-worker-20260815/agent_token
Environment=AIOS_WORKER_REGISTRATION_STATE=/var/lib/aios-online-agent-worker-20260815/registration
ExecStart=/usr/bin/python3 -B /usr/local/lib/aios-online-agent-worker-20260815/register.py
NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=strict
ProtectHome=true
ProtectKernelTunables=true
ProtectKernelModules=true
ProtectControlGroups=true
LockPersonality=true
RestrictSUIDSGID=true
RestrictRealtime=true
RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6
ReadWritePaths=/etc/credstore.encrypted /var/lib/aios-online-agent-worker-20260815
UMask=0077
TimeoutStartSec=120
[Install]
WantedBy=multi-user.target
+223
View File
@@ -240,6 +240,114 @@ components:
additionalProperties: false
properties:
candidate_id: {type: string}
BridgeWriterLeaseRecoveryRequest:
type: object
additionalProperties: false
required: [schema, task_id, repository, branch_mode, base_oid, expected_branch_oid, target_branch, generation, lease_seconds]
properties:
schema:
type: string
enum: [aios.task-writer-lease.v2, aios.task-writer-lease-request.v2]
task_id: {type: string, minLength: 1, maxLength: 256}
job_id: {type: string, pattern: '^job-[A-Za-z0-9][A-Za-z0-9._-]{7,255}$'}
repository: {type: string, const: aios/aios}
branch_mode: {type: string, enum: [CREATE_NEW, CONTINUE_EXISTING]}
base_oid: {type: string, pattern: '^[0-9a-f]{40}$'}
expected_branch_oid: {type: string, pattern: '^[0-9a-f]{40}$'}
target_branch: {type: string, pattern: '^agent/[A-Za-z0-9][A-Za-z0-9._/-]{2,179}$'}
generation: {type: integer, minimum: 1}
lease_seconds: {type: integer, minimum: 1, maximum: 86400}
BridgeWriterLeaseResponse:
type: object
additionalProperties: false
required: [schema, task_id, job_id, repository, branch_mode, base_oid, expected_branch_oid, target_branch, generation, lease_seconds, create_target_branch, worktree_mode, cas_ref_update_required]
properties:
schema: {type: string, enum: [aios.task-writer-lease.v2]}
task_id: {type: string}
job_id: {type: string, pattern: '^job-[A-Za-z0-9][A-Za-z0-9._-]{7,255}$'}
repository: {type: string, const: aios/aios}
branch_mode: {type: string, enum: [CREATE_NEW, CONTINUE_EXISTING]}
base_oid: {type: string, pattern: '^[0-9a-f]{40}$'}
expected_branch_oid: {type: string, pattern: '^[0-9a-f]{40}$'}
target_branch: {type: string, pattern: '^agent/[A-Za-z0-9][A-Za-z0-9._/-]{2,179}$'}
generation: {type: integer, minimum: 1}
lease_seconds: {type: integer, minimum: 1, maximum: 86400}
create_target_branch: {type: boolean}
worktree_mode: {type: string, enum: [detached_at_base_oid, detached_at_expected_head]}
cas_ref_update_required: {type: boolean, const: true}
lease_expires_at: {type: string, format: date-time}
BridgeWriterLeaseRecoveryResponse:
type: object
additionalProperties: false
required: [schema, task_id, job_id, repository, branch_mode, base_oid, expected_branch_oid, target_branch, generation, lease_seconds, lease_expires_at, create_target_branch, worktree_mode, cas_ref_update_required, recovered, bootstrapped, idempotent, runtime_rehydrated, durable_branch_lease_present]
properties:
schema: {type: string, enum: [aios.task-writer-lease.v2]}
task_id: {type: string}
job_id: {type: string, pattern: '^job-[A-Za-z0-9][A-Za-z0-9._-]{7,255}$'}
repository: {type: string, const: aios/aios}
branch_mode: {type: string, enum: [CREATE_NEW, CONTINUE_EXISTING]}
base_oid: {type: string, pattern: '^[0-9a-f]{40}$'}
expected_branch_oid: {type: string, pattern: '^[0-9a-f]{40}$'}
target_branch: {type: string, pattern: '^agent/[A-Za-z0-9][A-Za-z0-9._/-]{2,179}$'}
generation: {type: integer, minimum: 1}
lease_seconds: {type: integer, minimum: 1, maximum: 86400}
lease_expires_at: {type: string, format: date-time}
create_target_branch: {type: boolean, const: false}
worktree_mode: {type: string, enum: [detached_at_base_oid, detached_at_expected_head]}
cas_ref_update_required: {type: boolean, const: true}
recovered: {type: boolean}
bootstrapped: {type: boolean}
idempotent: {type: boolean}
runtime_rehydrated: {type: boolean}
durable_branch_lease_present: {type: boolean}
BridgeCommitFileRequest:
type: object
additionalProperties: false
required: [task_id, repository, expected_branch_oid, target_branch, generation, idempotency_key, path, content_base64, message]
properties:
task_id: {type: string, minLength: 1}
job_id: {type: string, pattern: '^job-[A-Za-z0-9][A-Za-z0-9._-]{7,255}$'}
repository: {type: string, const: aios/aios}
expected_branch_oid: {type: string, pattern: '^[0-9a-f]{40}$'}
target_branch: {type: string, pattern: '^agent/[A-Za-z0-9][A-Za-z0-9._/-]{2,179}$'}
generation: {type: integer, minimum: 1}
idempotency_key: {type: string, minLength: 1, maxLength: 256}
path: {type: string, minLength: 1, maxLength: 512}
content_base64: {type: string, format: byte}
message: {type: string, minLength: 1, maxLength: 200}
BridgeCommitFilesRequest:
type: object
additionalProperties: false
required: [task_id, repository, expected_branch_oid, target_branch, generation, idempotency_key, files, message]
properties:
task_id: {type: string, minLength: 1}
job_id: {type: string, pattern: '^job-[A-Za-z0-9][A-Za-z0-9._-]{7,255}$'}
repository: {type: string, const: aios/aios}
expected_branch_oid: {type: string, pattern: '^[0-9a-f]{40}$'}
target_branch: {type: string, pattern: '^agent/[A-Za-z0-9][A-Za-z0-9._/-]{2,179}$'}
generation: {type: integer, minimum: 1}
idempotency_key: {type: string, minLength: 1, maxLength: 256}
files: {type: object, additionalProperties: {type: string, format: byte}, minProperties: 1}
message: {type: string, minLength: 1, maxLength: 200}
BridgeCommitResponse:
type: object
additionalProperties: true
required: [job_id, commit_oid, commit_visible, branch_mode, target_branch, generation]
properties:
job_id: {type: string, pattern: '^job-[A-Za-z0-9][A-Za-z0-9._-]{7,255}$'}
commit_oid: {type: string, pattern: '^[0-9a-f]{40}$'}
commit_visible: {type: boolean}
branch_mode: {type: string, enum: [CONTINUE_EXISTING]}
target_branch: {type: string, pattern: '^agent/[A-Za-z0-9][A-Za-z0-9._/-]{2,179}$'}
generation: {type: integer, minimum: 1}
BridgeGovernedCapabilities:
type: object
required: [bridge_schema_version, governed_writes_enabled, operations, operation_contracts]
properties:
bridge_schema_version: {type: string, const: aios.chatgpt-gitops-bridge.v2}
governed_writes_enabled: {type: boolean, const: true}
operations: {type: array, items: {type: string}}
operation_contracts: {type: object, additionalProperties: true}
paths:
/createReleaseIntent:
post:
@@ -567,3 +675,118 @@ paths:
'200': {description: Safe GitOps plan}
'401': {description: Unauthorized}
x-openai-isConsequential: true
/v2/capabilities:
get:
operationId: getGovernedWriteCapabilities
summary: Read the governed Bridge v2 capability contract
responses:
'200':
description: Governed operations and their request/response contracts
content:
application/json:
schema: {$ref: '#/components/schemas/BridgeGovernedCapabilities'}
/v2/writer-lease:
post:
operationId: acquireWriterLease
summary: Acquire the exact governed writer lease for a task generation
security: [{bearerAuth: []}]
requestBody:
required: true
content:
application/json:
schema: {$ref: '#/components/schemas/BridgeWriterLeaseRecoveryRequest'}
responses:
'200':
description: Exact detached worktree lease binding
content:
application/json:
schema: {$ref: '#/components/schemas/BridgeWriterLeaseResponse'}
'400': {description: Invalid lease binding}
'409': {description: Branch, base, or generation conflict}
x-openai-isConsequential: true
/v2/writer-lease/bootstrap:
post:
operationId: bootstrapWriterLease
summary: Bootstrap a governed writer runtime without creating a durable lease
description: >
Staging-only runtime recovery. It validates the exact task, repository,
base OID, expected branch OID, target branch, and generation. It must
not create a branch, insert a branch lease, or consume a generation.
security: [{bearerAuth: []}]
requestBody:
required: true
content:
application/json:
schema: {$ref: '#/components/schemas/BridgeWriterLeaseRecoveryRequest'}
responses:
'200':
description: Runtime binding bootstrapped or returned idempotently
content:
application/json:
schema: {$ref: '#/components/schemas/BridgeWriterLeaseRecoveryResponse'}
'400': {description: Invalid lease binding}
'409': {description: A matching durable lease already exists or binding conflicts}
'503': {description: Runtime or durable state unavailable}
x-openai-isConsequential: true
/v2/writer-lease/renew:
post:
operationId: renewWriterLease
summary: Recover an expired governed writer lease without changing its binding
description: >
Renews only the current task generation and exact repository, base OID,
expected branch OID, and target branch. Durable renewal is revision-CAS
protected; runtime rehydration is reported separately.
security: [{bearerAuth: []}]
requestBody:
required: true
content:
application/json:
schema: {$ref: '#/components/schemas/BridgeWriterLeaseRecoveryRequest'}
responses:
'200':
description: Lease recovered or idempotently observed as already active
content:
application/json:
schema: {$ref: '#/components/schemas/BridgeWriterLeaseRecoveryResponse'}
'400': {description: Invalid schema, generation, or lease duration}
'409': {description: Task, base, repository, branch, generation, or durable lease conflict}
'503': {description: Durable job store or runtime unavailable}
x-openai-isConsequential: true
/v2/commit-file:
post:
operationId: commitFileGoverned
summary: Commit one file through the governed writer lease
security: [{bearerAuth: []}]
requestBody:
required: true
content:
application/json:
schema: {$ref: '#/components/schemas/BridgeCommitFileRequest'}
responses:
'200':
description: Authority and remote visibility receipt
content:
application/json:
schema: {$ref: '#/components/schemas/BridgeCommitResponse'}
'409': {description: Lease, expected-head, idempotency, or repository conflict}
'503': {description: Reconciliation required}
x-openai-isConsequential: true
/v2/commit-files:
post:
operationId: commitFilesGoverned
summary: Atomically commit a bounded set of files through the governed writer lease
security: [{bearerAuth: []}]
requestBody:
required: true
content:
application/json:
schema: {$ref: '#/components/schemas/BridgeCommitFilesRequest'}
responses:
'200':
description: Authority and remote visibility receipt
content:
application/json:
schema: {$ref: '#/components/schemas/BridgeCommitResponse'}
'409': {description: Lease, expected-head, idempotency, or repository conflict}
'503': {description: Reconciliation required}
x-openai-isConsequential: true
@@ -0,0 +1,54 @@
# R4.2 P0-A TaskPlane Scope Isolation Design
Status: approved candidate design. This document is not evidence of a protected-main merge or production deployment.
## Boundary
- Task: `r42-p0a-taskplane-scope-isolation-20260816`
- Governed Job: `job-3fd743b51971c6007923699c1233f84c`
- Repository: `aios/aios`
- Base: `0eb6e1a864edfdaeef37b438991707a110e21558`
- Target branch: `agent/r42-p0a-taskplane-scope-isolation-20260816`
- Generation: `1`
The implementation reuses this exact boundary. It does not create a replacement Task, Job, branch, generation, or idempotency key.
## Model and invariants
`TaskPlan` gains first-class `scope_type`, `tenant_id`, `user_id`, `project_id`, `conversation_id`, and `run_id` fields. The fields participate in accepted-key validation, parsing, serialization, persistence, and authorization.
Historical rows without scope data normalize to `scope_type=legacy`; no tenant is inferred. Tenant-scoped tasks require a non-empty `tenant_id`. System/global resources use an explicit `scope_type=system` invariant and do not receive a fabricated tenant. A task may add user, project, conversation, and run ownership within its tenant.
## Trusted ActorContext
TaskPlane defines a small immutable `TaskActorContext` used only for authorization. The HTTP authenticator returns an identity. `PolicyEngine` resolves that identity through server-owned policy configuration into the effective context and capabilities. Request fields such as `tenant_id`, `user_id`, `group_id`, and `role` are declarations only and never expand authority.
Public create, list, and get paths always use the resolved context. Internal compatibility calls may omit a context and retain explicit legacy behavior, but the HTTP adapter never uses that path.
## Persistence
SQLite remains the current runtime. `_create_schema()` performs an idempotent forward migration that adds the six scope columns and indexes, then backfills only missing `scope_type` values to `legacy`.
New writes persist scope in both the canonical `task_json` representation and first-class columns. Reads accept historical JSON without scope, using legacy columns, but reject a new-row mismatch between JSON and columns. SQL list/get predicates are parameterized and applied before row hydration.
PostgreSQL remains a separate migration target and is not claimed by this change.
## Authorization flow
1. HTTP bearer authentication yields an identity.
2. `PolicyEngine` verifies capability and resolves the identity to `TaskActorContext`.
3. Create binds the requested scope to the trusted context and rejects mismatches.
4. List/get pass the trusted context to `TaskPlaneStore`.
5. Store applies tenant and ownership predicates in SQL; privileged capabilities may explicitly cross scope.
Tenant actors cannot read legacy tasks, another tenant, or mismatched owned resources. Legacy actors can only read legacy tasks. System actors require explicit privileged capability for cross-scope access.
## Failure behavior
Missing tenant scope, forged scope, unknown scope types, JSON/column divergence, and unauthorized list/get operations fail closed with canonical TaskPlane errors. Public get does not reveal whether an out-of-scope task exists.
## Verification
Tests exercise real model round trips, SQLite migration/restart, Service/Policy/Store wiring, tenant A versus tenant B list/get denial, ownership filtering, forged create payload rejection, legacy isolation, privileged access, and SQL parameterization. Existing TaskPlane API, RBAC, recovery/security, and security-contract suites remain mandatory.
The candidate is complete only after real `TASK_CLAIMED`, `TASK_HEARTBEAT`, exact WriterLease, governed commit, OID convergence, result submission, and `TASK_VERIFIED`. Protected main and production remain unchanged.
@@ -0,0 +1,9 @@
# R4.2 P0-A TaskPlane Scope Isolation
Status: candidate implementation. This is not proof of protected-main promotion or production deployment.
Task scope is now represented by `scope_type`, `tenant_id`, `user_id`, `project_id`, `conversation_id`, and `run_id` in the model and SQLite persistence. Historical rows become `legacy`; their tenant is never guessed. Tenant tasks require `tenant_id`. System resources use `scope_type=system` without a fabricated tenant.
The HTTP adapter uses the authenticated identity. `PolicyEngine` resolves that identity through server-owned `identity_actor_contexts`; request payload fields do not grant scope. Public create binds the trusted scope, and public list/get apply parameterized scope predicates in `TaskPlaneStore`.
SQLite is the current runtime database. PostgreSQL remains a separate migration and runtime-acceptance gate.
@@ -1,54 +0,0 @@
# Generic Governed Online Agent Worker
This worker is a pull-based, restartable consumer for `aios.agent-task.v1`.
The service configuration contains only stable runtime identity, capability
names, API endpoints, heartbeat timing, and the credential projection. It does
not contain a task id, repository base OID, target branch, or plan-file path.
## Dynamic task contract
Engineering tasks carry `metadata.execution` with schema
`aios.online-agent.execution.v2`. The payload supplies the repository, exact
base OID, target branch, detached-worktree policy, adapter name, explicit file
change set, verification commands, and governed commit idempotency keys.
Task prose is descriptive only and is never passed to a shell.
The worker accepts only structured operations:
- `deterministic`: allow-listed file changes and test commands;
- `codex`: fail-closed until a task supplies an explicit Codex file/command
policy;
- `hermes`: fail-closed because the current Hermes runtime is a user-chat
service, not an engineering executor.
All command execution uses `shell=False`, fixed program allowlists, relative
paths beneath an ephemeral detached worktree, and output redaction. Direct
`git branch`, `update-ref`, `push`, `commit`, `reset`, `clean`, main,
production, tag, and baseline mutations are rejected by the worker.
## Governed write path
The worker calls the Bridge in this order:
1. `POST /v2/governed-task`;
2. `POST /v2/writer-lease`;
3. `POST /v2/commit-files`;
4. read-only authority-ref visibility verification.
CREATE_NEW is represented by the task payload and is normalized by the Bridge
lease authority. The worker never creates an authoritative ref itself.
## Lifecycle and recovery
The runtime registers with `register_or_recover`, receives its agent token
once, and stores only the encrypted systemd credential. The consumer loops over
poll, atomically claims one task, sends the first heartbeat, emits
`TASK_STARTED`, and continues heartbeats while executing.
State is atomically stored per task without agent or lease token material. If
the process stops after a governed commit but before result submission, the
next lease attempt reuses the stable governed idempotency key and submits the
stored commit evidence without issuing a second commit.
The control plane must independently verify submitted results; worker-local
success is not production or mainline authorization.
@@ -1,3 +0,0 @@
"""Generic governed Online Agent Worker runtime."""
__all__ = ["worker"]
@@ -1,937 +0,0 @@
#!/usr/bin/env python3
"""Generic, restartable Online Agent Worker.
The worker is deliberately task-agnostic. A task's signed/durable payload is
the only source of repository, branch, change, verification, and governance
parameters. There is no task id, base OID, branch name, or plan file in the
runtime configuration.
The worker performs local work only in an ephemeral detached worktree. All
authoritative ref updates go through the governed Bridge API; direct branch,
update-ref, push, main, production, tag, and baseline mutations are rejected.
"""
from __future__ import annotations
import base64
from dataclasses import dataclass
from datetime import datetime, timezone
from abc import ABC, abstractmethod
import hashlib
import json
import os
from pathlib import Path
import re
import shutil
import subprocess
import tempfile
import threading
import time
import urllib.error
import urllib.request
from typing import Any, Mapping
from urllib.parse import unquote
API_BASE = os.environ.get("AIOS_AGENT_TASK_API_URL", "http://127.0.0.1:18940").rstrip("/")
BRIDGE_BASE = os.environ.get("AIOS_GOVERNED_WRITE_API_URL", "http://127.0.0.1:18892").rstrip("/")
AGENT_ID = os.environ.get(
"AIOS_AGENT_ID", "online-engineering-worker-cn-network-mkt-generic-20260815"
)
AGENT_TYPE = os.environ.get("AIOS_AGENT_TYPE", "trusted-online-engineering-worker")
AGENT_VERSION = os.environ.get("AIOS_AGENT_VERSION", "2026-08-15.3-generic")
AUTHORITY_GIT_DIR = Path(
os.environ.get("AIOS_AUTHORITY_GIT_DIR", "/data/aios-gitops/repo.git")
)
STATE_ROOT = Path(
os.environ.get("AIOS_WORKER_STATE_ROOT", "/var/lib/aios-online-agent-worker-20260815")
)
WORKSPACE_ROOT = Path(
os.environ.get(
"AIOS_WORKER_WORKSPACE_ROOT", "/var/lib/aios-online-agent-worker-20260815/workspaces"
)
)
GIT_CONFIG_GLOBAL = STATE_ROOT / "gitconfig"
POLL_SECONDS = int(os.environ.get("AIOS_WORKER_POLL_SECONDS", "5"))
HEARTBEAT_SECONDS = int(os.environ.get("AIOS_WORKER_HEARTBEAT_SECONDS", "20"))
LEASE_SECONDS = int(os.environ.get("AIOS_WORKER_LEASE_SECONDS", "300"))
MAX_TASK_BYTES = 512 * 1024
MAX_FILE_BYTES = 2 * 1024 * 1024
OID_RE = re.compile(r"^[0-9a-f]{40}$")
SHA256_RE = re.compile(r"^[0-9a-f]{64}$")
ID_RE = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._:-]{2,179}$")
BRANCH_RE = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._/-]*$")
PATH_RE = re.compile(r"^[^\\\x00-\x1f\x7f]+$")
SENSITIVE_RE = re.compile(
r"(?i)(authorization\s*:\s*bearer\s+\S+|(?:password|passwd|secret|token|api[_-]?key)\s*[:=]\s*\S+)"
)
class WorkerError(RuntimeError):
def __init__(self, code: str, *, phase: str, details: Mapping[str, Any] | None = None) -> None:
super().__init__(code)
self.code = code
self.phase = phase
self.details = dict(details or {})
def now() -> str:
return datetime.now(timezone.utc).isoformat()
def digest_bytes(value: bytes) -> str:
return hashlib.sha256(value).hexdigest()
def digest_json(value: Any) -> str:
return digest_bytes(
json.dumps(value, ensure_ascii=False, sort_keys=True, separators=(",", ":")).encode("utf-8")
)
def sanitize(value: Any) -> Any:
if isinstance(value, Mapping):
cleaned: dict[str, Any] = {}
for key, inner in value.items():
lowered = str(key).lower()
if any(marker in lowered for marker in ("token", "secret", "credential", "password", "authorization")):
cleaned[str(key)] = "[REDACTED]"
else:
cleaned[str(key)] = sanitize(inner)
return cleaned
if isinstance(value, list):
return [sanitize(item) for item in value]
if isinstance(value, tuple):
return [sanitize(item) for item in value]
if isinstance(value, str):
return SENSITIVE_RE.sub("[REDACTED]", value)
return value
def excerpt(value: str, limit: int = 800) -> str:
value = str(sanitize(value))
return value if len(value) <= limit else value[: limit // 2] + "...[TRUNCATED]..." + value[-limit // 2 :]
def audit(event_type: str, **fields: Any) -> None:
STATE_ROOT.mkdir(mode=0o750, parents=True, exist_ok=True)
path = STATE_ROOT / "events.jsonl"
record = {
"event_type": event_type,
"created_at": now(),
"agent_id": AGENT_ID,
"credential_material_exposed": False,
"lease_material_exposed": False,
**sanitize(fields),
}
with path.open("a", encoding="utf-8") as handle:
handle.write(json.dumps(record, ensure_ascii=False, sort_keys=True) + "\n")
os.chmod(path, 0o640)
print(json.dumps(record, ensure_ascii=False, sort_keys=True), flush=True)
def atomic_json(path: Path, value: Mapping[str, Any], mode: int = 0o640) -> None:
path.parent.mkdir(mode=0o750, parents=True, exist_ok=True)
if path.parent.is_symlink() or path.is_symlink():
raise WorkerError("STATE_PATH_SYMLINK_REJECTED", phase="state")
fd, temporary = tempfile.mkstemp(prefix=f".{path.name}.", dir=str(path.parent))
try:
os.fchmod(fd, mode)
with os.fdopen(fd, "w", encoding="utf-8") as handle:
json.dump(sanitize(dict(value)), handle, ensure_ascii=False, sort_keys=True)
handle.write("\n")
handle.flush()
os.fsync(handle.fileno())
os.replace(temporary, path)
os.chmod(path, mode)
finally:
if os.path.exists(temporary):
os.unlink(temporary)
def safe_relative_path(value: Any, *, field: str = "path") -> str:
if not isinstance(value, str) or not value or len(value) > 240:
raise WorkerError(f"{field.upper()}_INVALID", phase="validation")
if unquote(value) != value or not PATH_RE.fullmatch(value):
raise WorkerError(f"{field.upper()}_INVALID", phase="validation")
if value.startswith(("/", "~")) or re.match(r"^[A-Za-z]:", value):
raise WorkerError(f"{field.upper()}_ABSOLUTE", phase="validation")
parts = value.replace("\\", "/").split("/")
if any(part in {"", ".", ".."} for part in parts):
raise WorkerError(f"{field.upper()}_TRAVERSAL", phase="validation")
return "/".join(parts)
def resolve_under(root: Path, relative: str, *, write: bool = False) -> Path:
relative = safe_relative_path(relative)
root = root.resolve()
candidate = (root / relative).resolve(strict=False)
if candidate != root and root not in candidate.parents:
raise WorkerError("WORKSPACE_ESCAPE", phase="validation")
current = root
for part in Path(relative).parts:
current /= part
if current.exists() and current.is_symlink():
raise WorkerError("WORKSPACE_SYMLINK_REJECTED", phase="validation")
if write and candidate.exists() and candidate.is_symlink():
raise WorkerError("WRITE_SYMLINK_REJECTED", phase="validation")
return candidate
def validate_oid(value: Any, field: str = "oid") -> str:
if not isinstance(value, str) or not OID_RE.fullmatch(value):
raise WorkerError(f"{field.upper()}_INVALID", phase="validation")
return value
def validate_sha(value: Any, field: str = "sha256") -> str:
if not isinstance(value, str) or not SHA256_RE.fullmatch(value):
raise WorkerError(f"{field.upper()}_INVALID", phase="validation")
return value
def validate_branch(value: Any) -> str:
if not isinstance(value, str) or not 3 <= len(value) <= 180:
raise WorkerError("TARGET_BRANCH_INVALID", phase="validation")
if value in {"main", "master", "HEAD"} or value.startswith(
("main/", "master/", "refs/", "tags/", "tag/", "release/", "releases/", "baseline/", "production/")
):
raise WorkerError("PROTECTED_BRANCH_REJECTED", phase="validation")
if (
value.startswith("/")
or value.endswith(("/", ".", ".lock"))
or ".." in value
or "//" in value
or "@{" in value
or any(ch in value for ch in " ~^:?*[\\")
or any(part in {"", ".", ".."} for part in value.split("/"))
or not BRANCH_RE.fullmatch(value)
):
raise WorkerError("TARGET_BRANCH_INVALID", phase="validation")
return value
def validate_program(argv: Any, *, internal_git: bool = False) -> list[str]:
if not isinstance(argv, list) or not argv or not all(isinstance(item, str) and item for item in argv):
raise WorkerError("STRUCTURED_ARGV_REQUIRED", phase="validation")
command = argv[0]
if command not in {"/usr/bin/git", "/usr/bin/python3"}:
raise WorkerError("PROGRAM_NOT_ALLOWLISTED", phase="validation", details={"program": Path(command).name})
if command.endswith("python3"):
if any(item in {"-c", "-", "--command"} for item in argv):
raise WorkerError("PYTHON_INLINE_CODE_REJECTED", phase="validation")
if "-m" in argv:
index = argv.index("-m")
if index + 1 >= len(argv) or argv[index + 1] not in {"pytest", "py_compile"}:
raise WorkerError("PYTHON_MODULE_NOT_ALLOWLISTED", phase="validation")
if command.endswith("git") and not internal_git:
rejected = {"push", "update-ref", "receive-pack", "send-pack", "reset", "clean", "branch", "commit", "merge", "rebase"}
if any(item in rejected for item in argv[1:]):
raise WorkerError("GIT_MUTATION_NOT_ALLOWLISTED", phase="validation")
return list(argv)
def run_command(
label: str,
argv: list[str],
*,
cwd: Path,
timeout: int,
internal_git: bool = False,
allowed_returncodes: tuple[int, ...] = (0,),
) -> dict[str, Any]:
validate_program(argv, internal_git=internal_git)
if not 1 <= timeout <= 3600:
raise WorkerError("COMMAND_TIMEOUT_INVALID", phase=label)
started = time.monotonic()
try:
completed = subprocess.run(
argv,
cwd=str(cwd),
text=True,
capture_output=True,
check=False,
shell=False,
timeout=timeout,
env={"PATH": "/usr/bin:/bin", "LC_ALL": "C.UTF-8", "GIT_CONFIG_GLOBAL": str(GIT_CONFIG_GLOBAL)},
)
except subprocess.TimeoutExpired as exc:
raise WorkerError("COMMAND_TIMEOUT", phase=label, details={"timeout_seconds": timeout}) from exc
result = {
"label": label,
"argv": [Path(item).name if index == 0 else item for index, item in enumerate(argv)],
"exit_code": completed.returncode,
"stdout_sha256": digest_bytes(completed.stdout.encode("utf-8", "replace")),
"stderr_sha256": digest_bytes(completed.stderr.encode("utf-8", "replace")),
"stdout_excerpt": excerpt(completed.stdout),
"stderr_excerpt": excerpt(completed.stderr),
"duration_ms": int((time.monotonic() - started) * 1000),
}
audit("COMMAND_RESULT", **result)
if completed.returncode not in allowed_returncodes:
raise WorkerError("COMMAND_FAILED", phase=label, details={"exit_code": completed.returncode, "stderr": excerpt(completed.stderr, 400)})
return result
@dataclass(frozen=True)
class ExecutionEnvelope:
task_id: str
repository: str
base_oid: str
target_branch: str
adapter: str
change_set: tuple[dict[str, Any], ...]
verification: tuple[dict[str, Any], ...]
governance: dict[str, Any]
workspace: dict[str, Any]
digest: str
@classmethod
def from_task(cls, task: Mapping[str, Any]) -> "ExecutionEnvelope":
task_id = str(task.get("task_id") or "")
if not ID_RE.fullmatch(task_id):
raise WorkerError("TASK_ID_INVALID", phase="validation")
metadata = task.get("metadata")
if not isinstance(metadata, Mapping):
raise WorkerError("TASK_METADATA_REQUIRED", phase="validation")
execution = metadata.get("execution")
if not isinstance(execution, Mapping):
raise WorkerError("DYNAMIC_EXECUTION_PAYLOAD_REQUIRED", phase="validation")
if execution.get("schema") != "aios.online-agent.execution.v2":
raise WorkerError("EXECUTION_SCHEMA_INVALID", phase="validation")
repository = execution.get("repository")
if not isinstance(repository, str) or not repository:
raise WorkerError("REPOSITORY_INVALID", phase="validation")
base_oid = validate_oid(execution.get("base_oid"), "base_oid")
target_branch = validate_branch(execution.get("target_branch"))
workspace = execution.get("workspace")
if not isinstance(workspace, Mapping):
raise WorkerError("WORKSPACE_POLICY_REQUIRED", phase="validation")
workspace_value = dict(workspace)
if workspace_value.get("authority_git_dir") != str(AUTHORITY_GIT_DIR):
raise WorkerError("AUTHORITY_ROOT_INVALID", phase="validation")
if workspace_value.get("read_only_repository") is not True:
raise WorkerError("REPOSITORY_WRITE_POLICY_INVALID", phase="validation")
if workspace_value.get("cleanup_policy") not in {"always", "cleanup_on_success"}:
raise WorkerError("WORKSPACE_CLEANUP_POLICY_INVALID", phase="validation")
adapter_value = execution.get("adapter")
if isinstance(adapter_value, Mapping):
adapter = str(adapter_value.get("name") or "")
else:
adapter = str(adapter_value or "")
if adapter not in {"deterministic", "codex", "hermes"}:
raise WorkerError("RUNTIME_ADAPTER_UNSUPPORTED", phase="validation")
change_set = execution.get("change_set")
if not isinstance(change_set, list) or not change_set or len(change_set) > 128:
raise WorkerError("CHANGE_SET_INVALID", phase="validation")
normalized_changes: list[dict[str, Any]] = []
for item in change_set:
if not isinstance(item, Mapping):
raise WorkerError("CHANGE_OPERATION_INVALID", phase="validation")
operation = str(item.get("operation") or "")
relative = safe_relative_path(item.get("path"))
normalized = dict(item)
normalized["operation"] = operation
normalized["path"] = relative
if operation == "write_file":
content = item.get("content")
if not isinstance(content, str) or len(content.encode("utf-8")) > MAX_FILE_BYTES:
raise WorkerError("WRITE_CONTENT_INVALID", phase="validation")
elif operation == "replace_text":
if not isinstance(item.get("old"), str) or not item.get("old") or not isinstance(item.get("new"), str):
raise WorkerError("REPLACE_TEXT_INVALID", phase="validation")
if len(str(item["old"]).encode("utf-8")) > MAX_FILE_BYTES:
raise WorkerError("REPLACE_TEXT_INVALID", phase="validation")
elif operation == "delete_file":
pass
else:
raise WorkerError("CHANGE_OPERATION_UNSUPPORTED", phase="validation", details={"operation": operation})
expected = item.get("expected_sha256")
if expected is not None:
validate_sha(expected, "expected_sha256")
normalized_changes.append(normalized)
verification = execution.get("verification") or []
if not isinstance(verification, list) or len(verification) > 128:
raise WorkerError("VERIFICATION_INVALID", phase="validation")
normalized_verification: list[dict[str, Any]] = []
for item in verification:
if not isinstance(item, Mapping):
raise WorkerError("VERIFICATION_COMMAND_INVALID", phase="validation")
label = str(item.get("label") or "verification")
argv = validate_program(item.get("argv"))
cwd_value = item.get("cwd")
cwd = "." if cwd_value in {None, "", "."} else safe_relative_path(cwd_value, field="cwd")
timeout = item.get("timeout_seconds", 120)
if not isinstance(timeout, int) or isinstance(timeout, bool) or not 1 <= timeout <= 3600:
raise WorkerError("VERIFICATION_TIMEOUT_INVALID", phase="validation")
normalized_verification.append({"label": label, "argv": argv, "cwd": cwd, "timeout_seconds": timeout})
governance = execution.get("governance")
if not isinstance(governance, Mapping):
raise WorkerError("GOVERNANCE_POLICY_REQUIRED", phase="validation")
governance_value = dict(governance)
governed_task_id = str(governance_value.get("task_id") or f"{task_id}-writer")
if not ID_RE.fullmatch(governed_task_id):
raise WorkerError("GOVERNED_TASK_ID_INVALID", phase="validation")
idempotency_key = str(governance_value.get("idempotency_key") or f"{task_id}-governed-v1")
commit_key = str(governance_value.get("commit_idempotency_key") or f"{task_id}-commit-v1")
for field, value in (("idempotency_key", idempotency_key), ("commit_idempotency_key", commit_key)):
if not re.fullmatch(r"[A-Za-z0-9][A-Za-z0-9._:-]{7,159}", value):
raise WorkerError(f"{field.upper()}_INVALID", phase="validation")
governance_value.update(
{
"task_id": governed_task_id,
"idempotency_key": idempotency_key,
"commit_idempotency_key": commit_key,
"message": str(governance_value.get("message") or f"chore(gitops): execute task {task_id}"),
}
)
for field in ("main_write", "force_push", "production_write", "tag_write", "baseline_write", "protected_ref_bypass"):
if governance_value.get(field, False) is not False:
raise WorkerError("GOVERNANCE_POLICY_REJECTED", phase="validation", details={"field": field})
allowed_files = governance_value.get("allowed_files")
if allowed_files is not None:
if not isinstance(allowed_files, list):
raise WorkerError("ALLOWED_FILES_INVALID", phase="validation")
allowed = {safe_relative_path(item, field="allowed_file") for item in allowed_files}
if not {item["path"] for item in normalized_changes}.issubset(allowed):
raise WorkerError("CHANGE_OUTSIDE_ALLOWED_FILES", phase="validation")
governance_value["allowed_files"] = sorted(allowed)
return cls(
task_id=task_id,
repository=repository,
base_oid=base_oid,
target_branch=target_branch,
adapter=adapter,
change_set=tuple(normalized_changes),
verification=tuple(normalized_verification),
governance=governance_value,
workspace=workspace_value,
digest=digest_json(execution),
)
class RuntimeAdapter(ABC):
name: str
@abstractmethod
def execute(self, envelope: ExecutionEnvelope, worktree: Path) -> dict[str, Any]:
raise NotImplementedError
class DeterministicExecutor(RuntimeAdapter):
name = "deterministic"
@staticmethod
def _write_atomic(path: Path, content: str) -> None:
path.parent.mkdir(mode=0o750, parents=True, exist_ok=True)
fd, temporary = tempfile.mkstemp(prefix=f".{path.name}.", dir=str(path.parent))
try:
with os.fdopen(fd, "w", encoding="utf-8") as handle:
handle.write(content)
handle.flush()
os.fsync(handle.fileno())
os.replace(temporary, path)
finally:
if os.path.exists(temporary):
os.unlink(temporary)
def execute(self, envelope: ExecutionEnvelope, worktree: Path) -> dict[str, Any]:
changed: set[str] = set()
for change in envelope.change_set:
relative = str(change["path"])
path = resolve_under(worktree, relative, write=True)
expected = change.get("expected_sha256")
current = path.read_bytes() if path.is_file() else None
if expected is not None and (current is None or digest_bytes(current) != expected):
raise WorkerError("WORKTREE_SOURCE_DRIFT", phase="implementing", details={"path": relative})
operation = change["operation"]
if operation == "write_file":
self._write_atomic(path, str(change["content"]))
elif operation == "replace_text":
if current is None:
raise WorkerError("REPLACE_SOURCE_MISSING", phase="implementing", details={"path": relative})
original = current.decode("utf-8")
old = str(change["old"])
count = original.count(old)
expected_count = int(change.get("count", 1))
if count != expected_count:
raise WorkerError("REPLACE_TEXT_MATCH_COUNT_INVALID", phase="implementing", details={"path": relative, "count": count})
self._write_atomic(path, original.replace(old, str(change["new"])))
elif operation == "delete_file":
if path.exists():
path.unlink()
changed.add(relative)
audit("REPOSITORY_CHANGE_APPLIED", operation=operation, path=relative, sha256=digest_bytes(path.read_bytes()) if path.exists() else None)
commands: list[dict[str, Any]] = []
if changed:
commands.append(run_command("git_intent_to_add", ["/usr/bin/git", "-C", str(worktree), "add", "-N", "--", *sorted(changed)], cwd=worktree, timeout=120, internal_git=True))
commands.append(run_command("git_diff_check", ["/usr/bin/git", "-C", str(worktree), "diff", "--check"], cwd=worktree, timeout=120))
commands.append(run_command("git_diff_stat", ["/usr/bin/git", "-C", str(worktree), "diff", "--no-ext-diff", "--stat"], cwd=worktree, timeout=120))
tests: list[dict[str, Any]] = []
for item in envelope.verification:
cwd = worktree if item["cwd"] == "." else resolve_under(worktree, item["cwd"])
tests.append(run_command(item["label"], item["argv"], cwd=cwd, timeout=item["timeout_seconds"]))
return {
"changed_files": sorted(changed),
"commands": commands,
"tests": tests,
"adapter": self.name,
}
class CodexExecutor(RuntimeAdapter):
name = "codex"
def execute(self, envelope: ExecutionEnvelope, worktree: Path) -> dict[str, Any]:
raise WorkerError(
"CODEX_ADAPTER_REQUIRES_EXPLICIT_POLICY",
phase="adapter",
details={"reason": "Codex is not allowed to mutate a governed worktree without an explicit file/command policy"},
)
class HermesExecutor(RuntimeAdapter):
name = "hermes"
def execute(self, envelope: ExecutionEnvelope, worktree: Path) -> dict[str, Any]:
raise WorkerError(
"HERMES_ENGINEERING_ADAPTER_UNAVAILABLE",
phase="adapter",
details={"reason": "The live Hermes service is a user-chat runtime, not an engineering executor"},
)
def adapter_for(name: str) -> RuntimeAdapter:
return {"deterministic": DeterministicExecutor, "codex": CodexExecutor, "hermes": HermesExecutor}[name]()
class HttpClient:
@staticmethod
def call(base: str, method: str, route: str, token: str | None, body: Mapping[str, Any] | None = None, timeout: int = 30) -> tuple[int, dict[str, Any]]:
payload = None if body is None else json.dumps(body, ensure_ascii=False, sort_keys=True, separators=(",", ":")).encode("utf-8")
headers = {"Accept": "application/json"}
if token:
headers["Authorization"] = "Bearer " + token
if payload is not None:
headers["Content-Type"] = "application/json"
request = urllib.request.Request(base + route, data=payload, headers=headers, method=method)
try:
with urllib.request.urlopen(request, timeout=timeout) as response:
value = json.loads(response.read().decode("utf-8"))
return response.status, value if isinstance(value, dict) else {}
except urllib.error.HTTPError as exc:
raw = exc.read(8192).decode("utf-8", "replace")
try:
value = json.loads(raw)
except json.JSONDecodeError:
value = {"error": "HTTP_ERROR"}
return exc.code, value if isinstance(value, dict) else {"error": "HTTP_ERROR"}
except (urllib.error.URLError, TimeoutError):
return 599, {"error": "SERVICE_UNAVAILABLE"}
class AgentTaskClient:
def __init__(self, token: str) -> None:
self.token = token
def poll(self) -> list[dict[str, Any]]:
status, response = HttpClient.call(API_BASE, "POST", "/v1/tasks/poll", self.token, {"limit": 100})
if status != 200:
raise WorkerError("TASK_POLL_FAILED", phase="poll", details={"api_status": status, "error": response.get("error")})
tasks = response.get("tasks")
return [dict(item) for item in tasks if isinstance(item, Mapping)] if isinstance(tasks, list) else []
def claim(self, task_id: str) -> tuple[int, dict[str, Any]]:
return HttpClient.call(API_BASE, "POST", f"/v1/tasks/{task_id}/claim", self.token, {"lease_seconds": LEASE_SECONDS})
def heartbeat(self, task_id: str, lease_token: str, stage: str) -> tuple[int, dict[str, Any]]:
return HttpClient.call(
API_BASE,
"POST",
f"/v1/tasks/{task_id}/heartbeat",
self.token,
{"lease_token": lease_token, "lease_seconds": LEASE_SECONDS, "progress": {"stage": stage, "worker_version": AGENT_VERSION}},
)
def result(self, task_id: str, lease_token: str, idempotency_key: str, result: Mapping[str, Any]) -> tuple[int, dict[str, Any]]:
return HttpClient.call(
API_BASE,
"POST",
f"/v1/tasks/{task_id}/result",
self.token,
{"lease_token": lease_token, "idempotency_key": idempotency_key, "result": dict(result)},
)
class BridgeClient:
def call(self, route: str, body: Mapping[str, Any]) -> tuple[int, dict[str, Any]]:
return HttpClient.call(BRIDGE_BASE, "POST", route, None, body, timeout=60)
class LeaseHeartbeat:
def __init__(self, client: AgentTaskClient, task_id: str, lease_token: str) -> None:
self.client = client
self.task_id = task_id
self.lease_token = lease_token
self.stage = "claimed"
self.failed: str | None = None
self.stop_event = threading.Event()
self.lock = threading.Lock()
self.thread = threading.Thread(target=self._run, name="online-agent-heartbeat", daemon=True)
def first(self) -> None:
status, response = self.client.heartbeat(self.task_id, self.lease_token, self.stage)
if not 200 <= status < 300:
self.failed = str(response.get("error") or status)
raise WorkerError("TASK_HEARTBEAT_FAILED", phase="heartbeat", details={"api_status": status, "error": self.failed})
audit("TASK_HEARTBEAT", task_id=self.task_id, stage=self.stage, api_status=status)
def start(self) -> None:
self.thread.start()
def set_stage(self, stage: str) -> None:
with self.lock:
self.stage = stage
def _run(self) -> None:
while not self.stop_event.wait(HEARTBEAT_SECONDS):
with self.lock:
stage = self.stage
try:
status, response = self.client.heartbeat(self.task_id, self.lease_token, stage)
if not 200 <= status < 300:
self.failed = str(response.get("error") or status)
audit("TASK_HEARTBEAT_FAILED", task_id=self.task_id, stage=stage, api_status=status, error=self.failed)
else:
audit("TASK_HEARTBEAT", task_id=self.task_id, stage=stage, api_status=status)
except Exception as exc: # pragma: no cover - defensive runtime guard
self.failed = type(exc).__name__
audit("TASK_HEARTBEAT_FAILED", task_id=self.task_id, stage=stage, error=self.failed)
def ensure(self) -> None:
if self.failed:
raise WorkerError("TASK_LEASE_LOST", phase="heartbeat", details={"error": self.failed})
def close(self) -> None:
self.stop_event.set()
if self.thread.ident is not None:
self.thread.join(timeout=2)
def load_agent_token() -> str:
directory = Path(os.environ.get("CREDENTIALS_DIRECTORY", ""))
path = directory / "agent_token"
if not path.is_file() or path.is_symlink():
raise WorkerError("AGENT_RUNTIME_CREDENTIAL_UNAVAILABLE", phase="startup")
value = path.read_text(encoding="utf-8").strip()
if len(value) < 24:
raise WorkerError("AGENT_RUNTIME_CREDENTIAL_INVALID", phase="startup")
return value
def ensure_roots() -> None:
if AUTHORITY_GIT_DIR.is_symlink() or STATE_ROOT.is_symlink() or WORKSPACE_ROOT.is_symlink():
raise WorkerError("ROOT_SYMLINK_REJECTED", phase="startup")
if not AUTHORITY_GIT_DIR.is_dir():
raise WorkerError("AUTHORITY_GIT_DIR_UNAVAILABLE", phase="startup")
STATE_ROOT.mkdir(mode=0o750, parents=True, exist_ok=True)
WORKSPACE_ROOT.mkdir(mode=0o750, parents=True, exist_ok=True)
if GIT_CONFIG_GLOBAL.is_symlink():
raise WorkerError("GIT_CONFIG_SYMLINK_REJECTED", phase="startup")
expected = f"[safe]\n\tdirectory = {AUTHORITY_GIT_DIR}\n"
if GIT_CONFIG_GLOBAL.exists() and GIT_CONFIG_GLOBAL.read_text(encoding="utf-8") != expected:
raise WorkerError("GIT_CONFIG_BINDING_CONFLICT", phase="startup")
if not GIT_CONFIG_GLOBAL.exists():
GIT_CONFIG_GLOBAL.write_text(expected, encoding="utf-8")
os.chmod(GIT_CONFIG_GLOBAL, 0o640)
def task_state_path(task_id: str) -> Path:
if not ID_RE.fullmatch(task_id):
raise WorkerError("TASK_ID_INVALID", phase="state")
return STATE_ROOT / "tasks" / f"{task_id}.json"
def load_task_state(task_id: str, execution_digest: str) -> dict[str, Any] | None:
path = task_state_path(task_id)
if not path.is_file() or path.is_symlink():
return None
try:
value = json.loads(path.read_text(encoding="utf-8"))
except Exception:
raise WorkerError("TASK_STATE_INVALID", phase="state")
if not isinstance(value, dict):
raise WorkerError("TASK_STATE_INVALID", phase="state")
if value.get("execution_digest") not in {None, execution_digest}:
raise WorkerError("TASK_PAYLOAD_DRIFT", phase="recovery")
return value
def save_task_state(task_id: str, value: Mapping[str, Any]) -> None:
atomic_json(task_state_path(task_id), value)
def new_run_root() -> Path:
ensure_roots()
root = Path(tempfile.mkdtemp(prefix="task-", dir=str(WORKSPACE_ROOT)))
os.chmod(root, 0o750)
return root
def cleanup_run_root(path: Path) -> None:
root = WORKSPACE_ROOT.resolve()
candidate = path.resolve(strict=False)
if candidate == root or root not in candidate.parents:
raise WorkerError("CLEANUP_SCOPE_INVALID", phase="cleanup")
if candidate.exists() or candidate.is_symlink():
shutil.rmtree(candidate)
def prepare_worktree(run_root: Path, base_oid: str) -> Path:
clone = run_root / "repository"
worktree = run_root / "worktree"
run_command("workspace_clone", ["/usr/bin/git", "clone", "--no-local", "--no-hardlinks", str(AUTHORITY_GIT_DIR), str(clone)], cwd=run_root, timeout=300, internal_git=True)
run_command("workspace_base_probe", ["/usr/bin/git", "-C", str(clone), "rev-parse", "--verify", base_oid + "^{commit}"], cwd=run_root, timeout=60)
run_command("workspace_worktree_create", ["/usr/bin/git", "-C", str(clone), "worktree", "add", "--detach", str(worktree), base_oid], cwd=run_root, timeout=300, internal_git=True)
return worktree
def governed_commit(envelope: ExecutionEnvelope, worktree: Path, bridge: BridgeClient, heartbeat: LeaseHeartbeat) -> dict[str, Any]:
governance = envelope.governance
create_body = {
"task_id": governance["task_id"],
"repository": envelope.repository,
"base_oid": envelope.base_oid,
"target_branch": envelope.target_branch,
"risk": str(governance.get("risk") or "engineering"),
"description": str(governance.get("description") or f"Governed execution for {envelope.task_id}"),
"idempotency_key": governance["idempotency_key"],
}
status, response = bridge.call("/v2/governed-task", create_body)
if not 200 <= status < 300:
raise WorkerError("GOVERNED_TASK_CREATE_FAILED", phase="committing", details={"api_status": status, "error": response.get("error")})
generation = int(response.get("generation") or 0)
expected_branch_oid = str(response.get("expected_branch_oid") or envelope.base_oid)
branch_mode = str(response.get("branch_mode") or "CREATE_NEW")
lease_body = {
"schema": "aios.task-writer-lease.v2",
"task_id": governance["task_id"],
"repository": envelope.repository,
"branch_mode": branch_mode,
"base_oid": envelope.base_oid,
"expected_branch_oid": expected_branch_oid,
"target_branch": envelope.target_branch,
"generation": generation,
"lease_seconds": int(governance.get("lease_seconds") or 900),
}
status, lease_response = bridge.call("/v2/writer-lease", lease_body)
if not 200 <= status < 300:
raise WorkerError("WRITER_LEASE_ACQUIRE_FAILED", phase="committing", details={"api_status": status, "error": lease_response.get("error")})
heartbeat.ensure()
files: dict[str, str] = {}
paths = governance.get("allowed_files") or [item["path"] for item in envelope.change_set]
for relative in sorted({safe_relative_path(item, field="allowed_file") for item in paths}):
path = resolve_under(worktree, relative)
if not path.is_file():
raise WorkerError("COMMIT_FILE_MISSING", phase="committing", details={"path": relative})
files[relative] = base64.b64encode(path.read_bytes()).decode("ascii")
commit_body = {
"task_id": governance["task_id"],
"repository": envelope.repository,
"expected_branch_oid": expected_branch_oid,
"target_branch": envelope.target_branch,
"generation": generation,
"idempotency_key": governance["commit_idempotency_key"],
"files": files,
"message": governance["message"],
}
status, commit_response = bridge.call("/v2/commit-files", commit_body)
if not 200 <= status < 300:
raise WorkerError("GOVERNED_COMMIT_FAILED", phase="committing", details={"api_status": status, "error": commit_response.get("error")})
commit_oid = commit_response.get("commit_oid")
if not isinstance(commit_oid, str) or not OID_RE.fullmatch(commit_oid):
raise WorkerError("GOVERNED_COMMIT_OID_INVALID", phase="committing")
visible_oid: str | None = None
for _ in range(5):
probe = subprocess.run(
["/usr/bin/git", "--git-dir", str(AUTHORITY_GIT_DIR), "rev-parse", "--verify", f"refs/heads/{envelope.target_branch}"],
text=True,
capture_output=True,
check=False,
env={"PATH": "/usr/bin:/bin", "LC_ALL": "C.UTF-8", "GIT_CONFIG_GLOBAL": str(GIT_CONFIG_GLOBAL)},
timeout=30,
)
if probe.returncode == 0:
visible_oid = probe.stdout.strip()
if visible_oid == commit_oid:
break
time.sleep(1)
if visible_oid != commit_oid:
raise WorkerError("GIT_VISIBILITY_MISMATCH", phase="verification", details={"commit_oid": commit_oid, "visible_oid": visible_oid})
return {
"governed_task_id": governance["task_id"],
"branch_mode": branch_mode,
"generation": generation,
"target_branch": envelope.target_branch,
"commit_oid": commit_oid,
"visible_oid": visible_oid,
"commit_idempotency_key": governance["commit_idempotency_key"],
"api_commit_visible": commit_response.get("commit_visible"),
"direct_main_write": False,
"force_push": False,
"production_write": False,
"protected_ref_bypass": False,
}
def build_result(task_id: str, envelope: ExecutionEnvelope, execution: Mapping[str, Any], governed: Mapping[str, Any]) -> dict[str, Any]:
return {
"status": "SUCCEEDED",
"verification_state": "VERIFIED_BY_WORKER_PENDING_SERVER_REVIEW",
"task_id": task_id,
"execution_backend": "generic-online-agent-worker-v2",
"adapter": envelope.adapter,
"execution_digest": envelope.digest,
"repository": envelope.repository,
"base_oid": envelope.base_oid,
"target_branch": envelope.target_branch,
"changed_files": execution.get("changed_files", []),
"commands": [{k: item.get(k) for k in ("label", "exit_code", "stdout_sha256", "stderr_sha256", "duration_ms")} for item in execution.get("commands", [])],
"tests": [{k: item.get(k) for k in ("label", "exit_code", "stdout_sha256", "stderr_sha256", "duration_ms")} for item in execution.get("tests", [])],
"governed": dict(governed),
"workspace_cleanup": "PASS",
"secret_material_exposed": False,
"direct_main_write": False,
"force_push": False,
"production_write": False,
"protected_ref_bypass": False,
}
class WorkerSupervisor:
def __init__(self, token: str) -> None:
self.agent = AgentTaskClient(token)
self.bridge = BridgeClient()
def process_claim(self, task: Mapping[str, Any], claim: Mapping[str, Any]) -> None:
task_id = str(claim.get("task_id") or task.get("task_id") or "")
lease_token = claim.get("lease_token")
if not isinstance(lease_token, str) or len(lease_token) < 24:
raise WorkerError("LEASE_TOKEN_NOT_RETURNED", phase="claim")
audit("TASK_CLAIMED", task_id=task_id, attempt=claim.get("attempt"), revision=claim.get("revision"))
heartbeat = LeaseHeartbeat(self.agent, task_id, lease_token)
try:
heartbeat.first()
heartbeat.set_stage("started")
heartbeat.start()
audit("TASK_STARTED", task_id=task_id, attempt=claim.get("attempt"))
envelope = ExecutionEnvelope.from_task(task)
state = load_task_state(task_id, envelope.digest) or {"task_id": task_id, "execution_digest": envelope.digest, "phase": "claimed"}
governed = state.get("governed") if isinstance(state.get("governed"), Mapping) else None
if isinstance(governed, Mapping) and isinstance(governed.get("commit_oid"), str) and OID_RE.fullmatch(governed["commit_oid"]):
result = state.get("result")
if not isinstance(result, Mapping):
result = {"status": "SUCCEEDED", "verification_state": "VERIFIED_BY_WORKER_PENDING_SERVER_REVIEW", "task_id": task_id, "execution_digest": envelope.digest, "governed": dict(governed), "recovered_without_duplicate_commit": True, "direct_main_write": False, "production_write": False, "force_push": False, "protected_ref_bypass": False}
audit("TASK_RECOVERED_COMMIT", task_id=task_id, commit_oid=governed["commit_oid"], duplicate_commit=False)
else:
run_root = new_run_root()
try:
worktree = prepare_worktree(run_root, envelope.base_oid)
state.update({"phase": "workspace_prepared", "execution_digest": envelope.digest, "attempt": claim.get("attempt")})
save_task_state(task_id, state)
adapter = adapter_for(envelope.adapter)
execution = adapter.execute(envelope, worktree)
state.update({"phase": "verification_passed", "execution": execution})
save_task_state(task_id, state)
heartbeat.ensure()
heartbeat.set_stage("committing")
governed = governed_commit(envelope, worktree, self.bridge, heartbeat)
result = build_result(task_id, envelope, execution, governed)
state.update({"phase": "commit_done", "governed": governed, "result": result})
save_task_state(task_id, state)
finally:
cleanup_run_root(run_root)
audit("WORKSPACE_CLEANED", task_id=task_id, cleanup="PASS")
heartbeat.ensure()
heartbeat.set_stage("result")
result_key = str(result.get("governed", {}).get("commit_idempotency_key") or f"{task_id}-result-{envelope.digest[:16]}")
status, response = self.agent.result(task_id, lease_token, result_key, result)
if not 200 <= status < 300:
raise WorkerError("TASK_RESULT_SUBMIT_FAILED", phase="result", details={"api_status": status, "error": response.get("error")})
audit("TASK_RESULT_SUBMITTED", task_id=task_id, api_status=status, result_idempotency_key=result_key, commit_oid=result.get("governed", {}).get("commit_oid"))
except WorkerError as exc:
audit("TASK_FAILED", task_id=task_id, error=exc.code, phase=exc.phase, details=exc.details)
failure_key = f"{task_id}-attempt-{claim.get('attempt', 0)}-failure"
failure = {
"status": "FAILED",
"verification_state": "UNVERIFIED",
"task_id": task_id,
"execution_backend": "generic-online-agent-worker-v2",
"error": exc.code,
"phase": exc.phase,
"details": exc.details,
"direct_main_write": False,
"force_push": False,
"production_write": False,
"protected_ref_bypass": False,
"secret_material_exposed": False,
}
try:
status, response = self.agent.result(task_id, lease_token, failure_key, failure)
audit("TASK_FAILURE_RESULT_SUBMITTED", task_id=task_id, api_status=status, error=response.get("error"))
except Exception as submit_exc: # pragma: no cover - defensive runtime guard
audit("TASK_FAILURE_RESULT_SUBMIT_FAILED", task_id=task_id, error=type(submit_exc).__name__)
finally:
heartbeat.close()
def once(self) -> int:
try:
tasks = self.agent.poll()
audit("TASK_POLL", visible_count=len(tasks))
except WorkerError as exc:
audit("TASK_POLL_FAILED", error=exc.code, phase=exc.phase, details=exc.details)
return 10
dynamic_tasks: list[dict[str, Any]] = []
for task in tasks:
metadata = task.get("metadata")
execution = metadata.get("execution") if isinstance(metadata, Mapping) else None
if not isinstance(execution, Mapping) or execution.get("schema") != "aios.online-agent.execution.v2":
audit("TASK_SKIPPED_NON_DYNAMIC", task_id=task.get("task_id"), reason="DYNAMIC_EXECUTION_PAYLOAD_REQUIRED")
continue
dynamic_tasks.append(task)
for task in dynamic_tasks:
task_id = str(task.get("task_id") or "")
status, claim = self.agent.claim(task_id)
if status == 409:
audit("TASK_CLAIM_SKIPPED", task_id=task_id, api_status=status, error=claim.get("error"))
continue
if status != 200:
audit("TASK_CLAIM_FAILED", task_id=task_id, api_status=status, error=claim.get("error"))
continue
self.process_claim(task, claim)
return 0
return 0
def run_forever(self) -> int:
ensure_roots()
audit("WORKER_STARTED", task_agnostic=True, api_base=API_BASE, bridge_base=BRIDGE_BASE, adapter_names=["deterministic", "codex", "hermes"], runtime_only_credentials=True)
while True:
self.once()
time.sleep(POLL_SECONDS)
def main() -> int:
try:
token = load_agent_token()
supervisor = WorkerSupervisor(token)
if os.environ.get("AIOS_WORKER_ONCE") == "1":
ensure_roots()
return supervisor.once()
return supervisor.run_forever()
except WorkerError as exc:
audit("WORKER_START_FAILED", error=exc.code, phase=exc.phase, details=exc.details)
return 20
if __name__ == "__main__":
raise SystemExit(main())
@@ -1,157 +0,0 @@
#!/usr/bin/env python3
"""Register or recover the generic Online Agent using runtime-only secrets."""
from __future__ import annotations
import json
import os
from pathlib import Path
import subprocess
import tempfile
import urllib.error
import urllib.request
from datetime import datetime, timezone
from typing import Any
BASE = os.environ.get("AIOS_AGENT_TASK_API_URL", "http://127.0.0.1:18940").rstrip("/")
AGENT_ID = os.environ.get("AIOS_AGENT_ID", "online-engineering-worker-cn-network-mkt-generic-20260815")
AGENT_TYPE = os.environ.get("AIOS_AGENT_TYPE", "trusted-online-engineering-worker")
AGENT_VERSION = os.environ.get("AIOS_AGENT_VERSION", "2026-08-15.3-generic")
PROJECTION_PROVIDER = "BridgeV2RuntimeCredentialProjection"
STATE_DIR = Path(os.environ.get("AIOS_WORKER_REGISTRATION_STATE", "/var/lib/aios-online-agent-worker-20260815/registration"))
CREDENTIAL_PATH = Path(os.environ.get("AIOS_WORKER_ENCRYPTED_CREDENTIAL", "/etc/credstore.encrypted/aios-online-agent-worker-20260815/agent_token"))
IDENTITY_PATH = STATE_DIR / "identity.json"
AUDIT_PATH = STATE_DIR / "events.jsonl"
CAPABILITIES = [
"online_agent_task_api", "task.poll", "task.claim", "task.heartbeat", "task.result",
"engineering.consumer", "engineering.execute", "workspace.worktree", "git.read", "git.diff",
"git.test", "governed.write.branch", "governed.write.file",
]
def now() -> str:
return datetime.now(timezone.utc).isoformat()
def audit(event_type: str, **fields: Any) -> None:
STATE_DIR.mkdir(mode=0o750, parents=True, exist_ok=True)
record = {"event_type": event_type, "created_at": now(), "agent_id": AGENT_ID, "credential_material_exposed": False, **fields}
with AUDIT_PATH.open("a", encoding="utf-8") as handle:
handle.write(json.dumps(record, ensure_ascii=False, sort_keys=True) + "\n")
os.chmod(AUDIT_PATH, 0o640)
print(json.dumps(record, ensure_ascii=False, sort_keys=True), flush=True)
def api_call(method: str, route: str, token: str, body: dict[str, Any] | None = None) -> tuple[int, dict[str, Any]]:
payload = None if body is None else json.dumps(body, ensure_ascii=False, sort_keys=True, separators=(",", ":")).encode("utf-8")
headers = {"Authorization": "Bearer " + token, "Accept": "application/json"}
if payload is not None:
headers["Content-Type"] = "application/json"
request = urllib.request.Request(BASE + route, data=payload, headers=headers, method=method)
try:
with urllib.request.urlopen(request, timeout=15) as response:
value = json.loads(response.read().decode("utf-8"))
return response.status, value if isinstance(value, dict) else {}
except urllib.error.HTTPError as exc:
try:
value = json.loads(exc.read(2048).decode("utf-8"))
except Exception:
value = {"error": "HTTP_ERROR"}
return exc.code, value if isinstance(value, dict) else {"error": "HTTP_ERROR"}
except (urllib.error.URLError, TimeoutError):
return 599, {"error": "AGENT_TASK_API_UNAVAILABLE"}
def read_identity() -> dict[str, Any] | None:
if not IDENTITY_PATH.is_file() or IDENTITY_PATH.is_symlink():
return None
try:
value = json.loads(IDENTITY_PATH.read_text(encoding="utf-8"))
except Exception:
return None
return value if isinstance(value, dict) else None
def identity_matches(value: dict[str, Any] | None) -> bool:
return bool(value and value.get("agent_id") == AGENT_ID and value.get("agent_type") == AGENT_TYPE and value.get("agent_version") == AGENT_VERSION and value.get("credential_projection_provider") == PROJECTION_PROVIDER and value.get("task_scope") == "dynamic")
def encrypted_credential_is_valid() -> bool:
if not CREDENTIAL_PATH.is_file() or CREDENTIAL_PATH.is_symlink():
return False
result = subprocess.run(["systemd-creds", "decrypt", "--with-key=host", str(CREDENTIAL_PATH), "/dev/null"], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, check=False, timeout=15)
return result.returncode == 0
def write_identity() -> None:
value = {"agent_id": AGENT_ID, "agent_type": AGENT_TYPE, "agent_version": AGENT_VERSION, "task_scope": "dynamic", "credential_projection_provider": PROJECTION_PROVIDER, "credential_delivery": "systemd-LoadCredentialEncrypted", "task_api_url": BASE, "task_api_schema": "aios.agent-task.v1", "execution_backend": "generic-online-agent-worker-v2", "runtime_only_secret_delivery": True, "production_write": False, "main_write": False, "force_push": False, "tag_write": False, "baseline_write": False, "writer_lease": True, "created_at": now()}
STATE_DIR.mkdir(mode=0o750, parents=True, exist_ok=True)
fd, temporary = tempfile.mkstemp(prefix="identity.", dir=str(STATE_DIR))
try:
with os.fdopen(fd, "w", encoding="utf-8") as handle:
json.dump(value, handle, ensure_ascii=False, sort_keys=True)
handle.write("\n")
os.chmod(temporary, 0o640)
os.replace(temporary, IDENTITY_PATH)
finally:
if os.path.exists(temporary):
os.unlink(temporary)
def encrypt_agent_token(agent_token: str) -> None:
CREDENTIAL_PATH.parent.mkdir(mode=0o700, parents=True, exist_ok=True)
fd, temporary = tempfile.mkstemp(prefix=".online-agent-token.", dir=str(CREDENTIAL_PATH.parent))
os.close(fd)
os.unlink(temporary)
try:
result = subprocess.run(["systemd-creds", "encrypt", "--with-key=host", "--name=agent_token", "/dev/stdin", temporary], input=(agent_token + "\n").encode("utf-8"), stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, check=False, timeout=20)
if result.returncode != 0:
raise RuntimeError("AGENT_CREDENTIAL_ENCRYPT_FAILED")
os.chmod(temporary, 0o600)
os.replace(temporary, CREDENTIAL_PATH)
os.chmod(CREDENTIAL_PATH, 0o600)
finally:
if os.path.exists(temporary):
os.unlink(temporary)
def register_or_recover() -> str:
STATE_DIR.mkdir(mode=0o750, parents=True, exist_ok=True)
existing = read_identity()
if CREDENTIAL_PATH.exists():
if not identity_matches(existing):
raise RuntimeError("AGENT_IDENTITY_BINDING_CONFLICT")
if not encrypted_credential_is_valid():
raise RuntimeError("AGENT_ENCRYPTED_CREDENTIAL_INVALID")
audit("AGENT_RECOVERED", credential_delivery="systemd-LoadCredentialEncrypted", credential_projection_provider=PROJECTION_PROVIDER, register_or_recover=True)
return "RECOVERED"
if existing is not None:
raise RuntimeError("AGENT_CREDENTIAL_MISSING_AFTER_PRIOR_REGISTRATION")
credential_directory = Path(os.environ.get("CREDENTIALS_DIRECTORY", ""))
control_path = credential_directory / "bridge_control_token"
if not control_path.is_file():
raise RuntimeError("BRIDGE_RUNTIME_CONTROL_CREDENTIAL_UNAVAILABLE")
control_token = control_path.read_text(encoding="utf-8").strip()
if len(control_token) < 32:
raise RuntimeError("BRIDGE_RUNTIME_CONTROL_CREDENTIAL_INVALID")
status, response = api_call("POST", "/v1/agents/register", control_token, {"agent_id": AGENT_ID, "agent_type": AGENT_TYPE, "agent_version": AGENT_VERSION, "capabilities": CAPABILITIES, "metadata": {"task_scope": "dynamic", "credential_projection_provider": PROJECTION_PROVIDER, "runtime_only_secret_delivery": True, "task_api_schema": "aios.agent-task.v1", "execution_backend": "generic-online-agent-worker-v2", "production_write": False, "main_write": False, "force_push": False, "tag_write": False, "baseline_write": False, "writer_lease": True}})
if status == 409 and response.get("error") == "AGENT_ALREADY_EXISTS":
raise RuntimeError("AGENT_ALREADY_EXISTS_RECOVERY_CREDENTIAL_UNAVAILABLE")
if status != 201:
raise RuntimeError("AGENT_REGISTER_FAILED_" + str(response.get("error") or status))
agent_token = response.get("agent_token")
if not isinstance(agent_token, str) or len(agent_token) < 24:
raise RuntimeError("AGENT_TOKEN_NOT_RETURNED")
encrypt_agent_token(agent_token)
write_identity()
audit("AGENT_REGISTERED", credential_delivery="systemd-LoadCredentialEncrypted", credential_projection_provider=PROJECTION_PROVIDER, register_or_recover=True)
return "REGISTERED"
if __name__ == "__main__":
try:
raise SystemExit(0 if register_or_recover() in {"REGISTERED", "RECOVERED"} else 20)
except Exception as exc:
audit("AGENT_REGISTRATION_FAILED", error=type(exc).__name__)
raise SystemExit(20)
@@ -1,79 +0,0 @@
from __future__ import annotations
import os
from pathlib import Path
from aios_online_agent_worker.worker import ExecutionEnvelope, WorkerError, sanitize # noqa: E402
BASE = "a" * 40
def payload(task_id: str, branch: str, *, base_oid: str = BASE) -> dict[str, object]:
return {
"task_id": task_id,
"metadata": {
"execution": {
"schema": "aios.online-agent.execution.v2",
"repository": "aios/aios",
"base_oid": base_oid,
"target_branch": branch,
"adapter": "deterministic",
"workspace": {
"authority_git_dir": str(Path(os.environ.get("AIOS_AUTHORITY_GIT_DIR", "/data/aios-gitops/repo.git"))),
"read_only_repository": True,
"cleanup_policy": "always",
},
"change_set": [
{
"operation": "write_file",
"path": "tests/generated_marker.py",
"content": "def test_marker():\n assert True\n",
"expected_sha256": None,
}
],
"verification": [
{"label": "compile", "argv": ["/usr/bin/python3", "-m", "py_compile", "tests/generated_marker.py"], "cwd": ".", "timeout_seconds": 60}
],
"governance": {
"task_id": f"{task_id}-writer",
"idempotency_key": f"{task_id}-governed-v1",
"commit_idempotency_key": f"{task_id}-commit-v1",
"allowed_files": ["tests/generated_marker.py"],
"main_write": False,
"force_push": False,
"production_write": False,
"tag_write": False,
"baseline_write": False,
"protected_ref_bypass": False,
},
}
},
}
def test_dynamic_payload_is_not_bound_to_task_or_base() -> None:
first = ExecutionEnvelope.from_task(payload("task-alpha-20260815", "agent/alpha-20260815"))
second = ExecutionEnvelope.from_task(payload("task-beta-20260815", "agent/beta-20260815", base_oid="b" * 40))
assert first.task_id != second.task_id
assert first.base_oid != second.base_oid
assert first.target_branch != second.target_branch
def test_protected_branch_is_rejected() -> None:
try:
ExecutionEnvelope.from_task(payload("task-main-20260815", "main"))
except WorkerError as exc:
assert exc.code == "PROTECTED_BRANCH_REJECTED"
else:
raise AssertionError("protected branch accepted")
def test_task_payload_does_not_persist_secret_values() -> None:
value = sanitize({"agent_token": "agt-secret", "nested": {"authorization": "Bearer secret"}})
assert value == {"agent_token": "[REDACTED]", "nested": {"authorization": "[REDACTED]"}}
def test_execution_digest_is_stable() -> None:
value = payload("task-digest-20260815", "agent/digest-20260815")
assert ExecutionEnvelope.from_task(value).digest == ExecutionEnvelope.from_task(value).digest
@@ -45,6 +45,9 @@ class ShadowClientRBACPublicInterfaceTests(unittest.TestCase):
identity_verifier=self.IdentityVerifier({"shadow-client"}),
execution_enabled=False,
identity_capabilities={"shadow-client": set(capabilities)},
identity_actor_contexts={
"shadow-client": {"scope_type": "system"}
},
)
service = self.TaskPlaneService(store, policy=policy)
@@ -0,0 +1,298 @@
from __future__ import annotations
import json
import os
import sqlite3
import sys
import threading
import urllib.error
import urllib.request
from http.server import ThreadingHTTPServer
import pytest
ROOT = os.getcwd()
if os.path.join(ROOT, "active-runtime") not in sys.path:
sys.path.append(os.path.join(ROOT, "active-runtime"))
from app.task_plane.api import TaskPlaneRequestHandler, TaskPlaneService
from app.task_plane.errors import ForbiddenError, NotFoundError, ValidationError
from app.task_plane.models import TaskPlan
from app.task_plane.policy import PolicyEngine
from app.task_plane.store import TaskPlaneStore
def _policy() -> PolicyEngine:
return PolicyEngine(
identity_capabilities={
"actor-a": {"task.creator", "task.read"},
"actor-b": {"task.creator", "task.read"},
"reader-a": {"task.read"},
"reader-b": {"task.read"},
"owner": {"task.creator", "task.read", "task.read.all"},
"unbound": {"task.creator", "task.read"},
},
identity_actor_contexts={
"actor-a": {
"scope_type": "tenant",
"tenant_id": "tenant-a",
"user_id": "user-a",
"project_id": "project-a",
"conversation_id": "conversation-a",
"run_id": "run-a",
},
"reader-a": {
"scope_type": "tenant",
"tenant_id": "tenant-a",
"user_id": "user-a",
"project_id": "project-a",
"conversation_id": "conversation-a",
"run_id": "run-a",
},
"actor-b": {
"scope_type": "tenant",
"tenant_id": "tenant-b",
"user_id": "user-b",
"project_id": "project-b",
},
"reader-b": {
"scope_type": "tenant",
"tenant_id": "tenant-b",
"user_id": "user-b",
"project_id": "project-b",
},
"owner": {"scope_type": "system"},
},
)
def _service() -> TaskPlaneService:
return TaskPlaneService(TaskPlaneStore(":memory:"), policy=_policy())
def test_taskplan_scope_fields_round_trip_as_first_class_values() -> None:
plan = TaskPlan.from_dict(
{
"task_id": "task-scope-roundtrip",
"scope_type": "tenant",
"tenant_id": "tenant-a",
"user_id": "user-a",
"project_id": "project-a",
"conversation_id": "conversation-a",
"run_id": "run-a",
}
)
payload = plan.to_dict()
assert TaskPlan.from_dict(payload).to_dict() == payload
def test_tenant_scope_requires_tenant_id_and_system_scope_does_not() -> None:
with pytest.raises(ValidationError) as exc:
TaskPlan.from_dict({"task_id": "task-missing-tenant", "scope_type": "tenant"})
assert exc.value.error_code == "TASK_SCOPE_TENANT_ID_REQUIRED"
system = TaskPlan.from_dict(
{"task_id": "task-system", "scope_type": "system", "run_id": "system-run"}
)
assert system.tenant_id is None
def test_ACTOR_BINDING_AUTHZ_MERGE_uses_server_scope() -> None:
service = _service()
created = service.create_task(
{"task_id": "task-a", "task_type": "code.change"},
actor_identity="actor-a",
)
assert created["plan"]["tenant_id"] == "tenant-a"
assert created["plan"]["user_id"] == "user-a"
assert created["plan"]["project_id"] == "project-a"
declared = service.create_task(
{
"task_id": "task-a-declared",
"tenant_id": "tenant-a",
"user_id": "user-a",
"project_id": "project-a",
"conversation_id": "conversation-a",
"run_id": "run-a",
},
actor_identity="actor-a",
)
assert declared["plan"]["scope_type"] == "tenant"
def test_TASK_SCOPE_MISMATCH_DENY_rejects_forged_client_scope() -> None:
service = _service()
with pytest.raises(ForbiddenError) as exc:
service.create_task(
{
"task_id": "task-forged",
"scope_type": "tenant",
"tenant_id": "tenant-b",
"user_id": "user-b",
"project_id": "project-b",
},
actor_identity="actor-a",
)
assert exc.value.error_code == "TASK_SCOPE_MISMATCH_DENY"
def test_public_create_fails_closed_without_server_actor_scope() -> None:
service = _service()
with pytest.raises(ForbiddenError) as exc:
service.create_task(
{"task_id": "task-unbound"}, actor_identity="unbound"
)
assert exc.value.error_code == "TASK_ACTOR_SCOPE_UNAVAILABLE"
def test_TASKPLANE_LIST_AUTHZ_DENY_and_TASK_OWNERSHIP_FILTER() -> None:
service = _service()
service.create_task({"task_id": "task-a"}, actor_identity="actor-a")
service.create_task({"task_id": "task-b"}, actor_identity="actor-b")
assert [item["task_id"] for item in service.list_tasks("reader-a")] == ["task-a"]
assert [item["task_id"] for item in service.list_tasks("reader-b")] == ["task-b"]
def test_TASKPLANE_GET_AUTHZ_DENY_does_not_disclose_cross_tenant_task() -> None:
service = _service()
service.create_task({"task_id": "task-a"}, actor_identity="actor-a")
with pytest.raises(NotFoundError):
service.get_task("task-a", "reader-b")
def test_http_authentication_drives_server_scope_and_cross_tenant_404() -> None:
service = _service()
class Handler(TaskPlaneRequestHandler):
pass
Handler.service = service
Handler.authenticator = lambda token: {
"token-a": "actor-a",
"token-b": "reader-b",
}.get(token)
server = ThreadingHTTPServer(("127.0.0.1", 0), Handler)
thread = threading.Thread(target=server.serve_forever, daemon=True)
thread.start()
base = f"http://127.0.0.1:{server.server_port}"
try:
request = urllib.request.Request(
base + "/tasks",
data=json.dumps({"task_id": "task-http-a"}).encode(),
headers={
"Authorization": "Bearer token-a",
"Content-Type": "application/json",
},
method="POST",
)
with urllib.request.urlopen(request, timeout=3) as response:
created = json.loads(response.read())
assert created["plan"]["tenant_id"] == "tenant-a"
denied = urllib.request.Request(
base + "/tasks/task-http-a",
headers={"Authorization": "Bearer token-b"},
)
with pytest.raises(urllib.error.HTTPError) as exc:
urllib.request.urlopen(denied, timeout=3)
assert exc.value.code == 404
finally:
server.shutdown()
server.server_close()
thread.join(timeout=3)
def test_mutating_endpoint_is_scoped_before_cancel() -> None:
policy = PolicyEngine(
execution_enabled=True,
identity_capabilities={
"actor-a": {"task.creator", "task.read"},
"actor-b": {"task.cancel"},
},
identity_actor_contexts={
"actor-a": {"scope_type": "tenant", "tenant_id": "tenant-a"},
"actor-b": {"scope_type": "tenant", "tenant_id": "tenant-b"},
},
)
service = TaskPlaneService(TaskPlaneStore(":memory:"), policy=policy)
service.create_task({"task_id": "task-a"}, actor_identity="actor-a")
with pytest.raises(NotFoundError):
service.cancel("task-a", {"actor_identity": "actor-b"})
def test_privileged_server_actor_can_cross_scope() -> None:
service = _service()
service.create_task({"task_id": "task-a"}, actor_identity="actor-a")
service.create_task({"task_id": "task-b"}, actor_identity="actor-b")
assert {item["task_id"] for item in service.list_tasks("owner")} == {
"task-a",
"task-b",
}
def test_scope_sql_parameters_do_not_interpolate_tenant_values() -> None:
service = _service()
service.create_task({"task_id": "task-a"}, actor_identity="actor-a")
policy = PolicyEngine(
identity_capabilities={"attacker": {"task.read"}},
identity_actor_contexts={
"attacker": {
"scope_type": "tenant",
"tenant_id": "tenant-a' OR 1=1 --",
}
},
)
attacker_service = TaskPlaneService(service.store, policy=policy)
assert attacker_service.list_tasks("attacker") == []
def test_existing_sqlite_rows_migrate_to_legacy_without_tenant_guess(tmp_path) -> None:
path = tmp_path / "legacy.sqlite3"
connection = sqlite3.connect(path)
connection.execute(
"""
CREATE TABLE tasks (
task_id TEXT PRIMARY KEY, revision INTEGER NOT NULL, status TEXT NOT NULL,
task_json TEXT NOT NULL, task_type TEXT NOT NULL, root_task_id TEXT NOT NULL,
parent_task_id TEXT, correlation_id TEXT, idempotency_key TEXT UNIQUE,
created_at REAL NOT NULL, updated_at REAL NOT NULL, failure_reason TEXT
)
"""
)
connection.execute(
"INSERT INTO tasks VALUES (?,?,?,?,?,?,?,?,?,?,?,?)",
(
"task-legacy",
1,
"CREATED",
json.dumps({"task_id": "task-legacy"}),
"code.change",
"task-legacy",
None,
None,
"legacy-key",
1.0,
1.0,
None,
),
)
connection.commit()
connection.close()
store = TaskPlaneStore(str(path))
task = store.get_task("task-legacy")
assert task.task_plan.scope_type == "legacy"
assert task.task_plan.tenant_id is None
replay = store.create_task(
TaskPlan.from_dict({"task_id": "task-legacy"}),
idempotency_key="legacy-key",
)
assert replay.task_id == "task-legacy"
assert tuple(
store.conn.execute(
"SELECT scope_type,tenant_id FROM tasks WHERE task_id='task-legacy'"
).fetchone()
) == ("legacy", None)
tenant_context = _policy().actor_context("reader-a")
with pytest.raises(NotFoundError):
store.get_task("task-legacy", tenant_context)
store.close()
+122 -1
View File
@@ -2,6 +2,7 @@ import base64
from pathlib import Path
from aios_authority_git_executor import AuthorityGitExecutor
from aios_chatgpt_bridge_v2 import BridgeV2Adapter, OPERATION_REQUIRED_FIELDS
from aios_chatgpt_bridge_v2_server import BridgeHandler, RuntimeConfig
from aios_continuation_executor import GiteaContinuationRemote, ServerSideCredentialProjection
from aios_continuation_lease import WriterLeaseRequestV2
@@ -26,6 +27,7 @@ class Probe:
def test_create_new_lease_is_exact_base_and_absent():
request = WriterLeaseRequestV2(
job_id="job-bridge-runtime-lineage-test",
task_id="TASK-P0-BRIDGE-V21-DURABLE-RECONCILIATION-20260809",
repository="aios/aios",
branch_mode="CREATE_NEW",
@@ -39,6 +41,7 @@ def test_create_new_lease_is_exact_base_and_absent():
def test_create_new_lease_rejects_existing_target():
request = WriterLeaseRequestV2(
job_id="job-bridge-runtime-lineage-test",
task_id="task",
repository="aios/aios",
branch_mode="CREATE_NEW",
@@ -75,7 +78,7 @@ def test_slash_branch_uses_paginated_collection():
def test_http_route_contract_is_complete():
expected = {
"/v2/runtime", "/v2/capabilities", "/v2/governed-task",
"/v2/writer-lease", "/v2/commit-file", "/v2/commit-files",
"/v2/writer-lease", "/v2/writer-lease/bootstrap", "/v2/writer-lease/renew", "/v2/commit-file", "/v2/commit-files",
"/v2/job", "/v2/job/resume", "/v2/job/reconcile",
"/v2/tag/approval-request",
"/v2/tag/publish",
@@ -88,3 +91,121 @@ def test_http_route_contract_is_complete():
from aios_chatgpt_bridge_v2_server import ROUTES
assert set(ROUTES) == expected
def test_renew_writer_lease_is_exposed_in_action_and_capability_contracts():
adapter = BridgeV2Adapter(runtime_status={})
capabilities = adapter.getGovernedWriteCapabilities()
contract = capabilities["operation_contracts"]["renewWriterLease"]
assert contract["method"] == "POST"
assert contract["path"] == "/v2/writer-lease/renew"
assert tuple(contract["required_fields"]) == OPERATION_REQUIRED_FIELDS["renewWriterLease"]
assert "runtime_rehydrated" in contract["response_fields"]
action_schema = Path("docs/gitops/openapi-chatgpt-actions.yaml").read_text(encoding="utf-8")
assert "/v2/writer-lease/renew:" in action_schema
assert "operationId: renewWriterLease" in action_schema
assert "BridgeWriterLeaseRecoveryRequest" in action_schema
assert "BridgeWriterLeaseRecoveryResponse" in action_schema
assert {
capabilities["operation_contracts"][operation]["path"]
for operation in (
"acquireWriterLease",
"renewWriterLease",
"commitFileGoverned",
"commitFilesGoverned",
)
} == {
"/v2/writer-lease",
"/v2/writer-lease/renew",
"/v2/commit-file",
"/v2/commit-files",
}
def test_bootstrap_writer_lease_is_exposed_in_action_and_capability_contracts():
adapter = BridgeV2Adapter(runtime_status={})
capabilities = adapter.getGovernedWriteCapabilities()
contract = capabilities["operation_contracts"]["bootstrapWriterLease"]
assert contract["method"] == "POST"
assert contract["path"] == "/v2/writer-lease/bootstrap"
assert tuple(contract["required_fields"]) == OPERATION_REQUIRED_FIELDS["bootstrapWriterLease"]
assert "bootstrapped" in contract["response_fields"]
assert "durable_branch_lease_present" in contract["response_fields"]
action_schema = Path("docs/gitops/openapi-chatgpt-actions.yaml").read_text(encoding="utf-8")
assert "/v2/writer-lease/bootstrap:" in action_schema
assert "operationId: bootstrapWriterLease" in action_schema
assert "bootstrapped: {type: boolean}" in action_schema
assert "durable_branch_lease_present: {type: boolean}" in action_schema
def test_http_allows_explicit_job_id_on_lease_and_commit_requests():
handler = BridgeHandler.__new__(BridgeHandler)
lease_body = {
"schema": WriterLeaseRequestV2.schema,
"task_id": "task",
"job_id": "job-explicit-runtime-lineage",
"repository": "aios/aios",
"branch_mode": "CONTINUE_EXISTING",
"base_oid": "a" * 40,
"expected_branch_oid": "a" * 40,
"target_branch": "agent/explicit-runtime-lineage",
"generation": 2,
"lease_seconds": 120,
}
handler._allowed("acquireWriterLease", lease_body)
handler._allowed(
"commitFilesGoverned",
{
"task_id": "task",
"job_id": "job-explicit-runtime-lineage",
"repository": "aios/aios",
"expected_branch_oid": "a" * 40,
"target_branch": "agent/explicit-runtime-lineage",
"generation": 2,
"idempotency_key": "commit-explicit-runtime-lineage",
"files": {"docs/x.txt": "eA=="},
"message": "test",
},
)
def test_http_allows_omitted_job_id_for_durable_server_resolution():
handler = BridgeHandler.__new__(BridgeHandler)
lease_body = {
"schema": WriterLeaseRequestV2.schema,
"task_id": "task",
"repository": "aios/aios",
"branch_mode": "CONTINUE_EXISTING",
"base_oid": "a" * 40,
"expected_branch_oid": "a" * 40,
"target_branch": "agent/implicit-runtime-lineage",
"generation": 2,
"lease_seconds": 120,
}
handler._allowed("acquireWriterLease", lease_body)
handler._allowed(
"commitFilesGoverned",
{
"task_id": "task",
"repository": "aios/aios",
"expected_branch_oid": "a" * 40,
"target_branch": "agent/implicit-runtime-lineage",
"generation": 2,
"idempotency_key": "commit-implicit-runtime-lineage",
"files": {"docs/x.txt": "eA=="},
"message": "test",
},
)
def test_capability_contracts_return_exact_job_id_lineage():
contracts = BridgeV2Adapter(runtime_status={}).getGovernedWriteCapabilities()["operation_contracts"]
for operation in ("acquireWriterLease", "bootstrapWriterLease", "renewWriterLease"):
assert "job_id" in contracts[operation]["response_fields"]
for operation in ("commitFileGoverned", "commitFilesGoverned"):
assert "job_id" in contracts[operation]["response_fields"]
@@ -0,0 +1,304 @@
from __future__ import annotations
from pathlib import Path
import re
import json
from types import SimpleNamespace
import pytest
import yaml
from aios_chatgpt_bridge_v2_server import GovernedRuntime
from aios_continuation_executor import ContinuationExecutor
from aios_continuation_lease import WriterLeaseRequestV2
from aios_gitops_job_controller import JobState
TASK = "continuation-normalization-test"
JOB = "job-continuation-normalization-test"
REPO = "aios/aios"
BASE = "a" * 40
BRANCH = "agent/continuation-normalization-test"
class Authority:
def __init__(self, head: str | None) -> None:
self.head = head
def read_ref(self, ref: str) -> str | None:
assert ref == "refs/heads/" + BRANCH
return self.head
def commit_exists(self, oid: str) -> bool:
return oid == BASE
def branch_exists(self, branch: str) -> bool:
assert branch == BRANCH
return self.head is not None
def branch_head(self, branch: str) -> str:
assert branch == BRANCH and self.head is not None
return self.head
def is_ancestor(self, older: str, newer: str) -> bool:
return older == BASE and newer == BASE
def lease(**overrides: object) -> WriterLeaseRequestV2:
values = {
"task_id": TASK,
"job_id": JOB,
"repository": REPO,
"branch_mode": "CREATE_NEW",
"base_oid": BASE,
"expected_branch_oid": BASE,
"target_branch": BRANCH,
"generation": 1,
"lease_seconds": 120,
}
values.update(overrides)
return WriterLeaseRequestV2(**values)
def runtime(*, head: str | None = BASE, **binding_overrides: object) -> GovernedRuntime:
value = GovernedRuntime.__new__(GovernedRuntime)
value.authority = Authority(head)
binding = {
"task_id": TASK,
"job_id": JOB,
"repository": REPO,
"base_oid": BASE,
"expected_branch_oid": BASE,
"target_branch": BRANCH,
"generation": 1,
"branch_mode": "CREATE_NEW",
}
binding.update(binding_overrides)
value.bindings = {TASK: binding}
return value
def test_same_lineage_create_new_normalizes_to_continue_existing() -> None:
normalized = runtime()._normalize_runtime_lease(lease())
assert normalized.branch_mode == "CONTINUE_EXISTING"
assert normalized.task_id == TASK
assert normalized.job_id == JOB
assert normalized.repository == REPO
assert normalized.target_branch == BRANCH
assert normalized.expected_branch_oid == BASE
assert normalized.generation == 1
def test_unprovisioned_create_new_stays_create_new() -> None:
assert runtime(head=None)._normalize_runtime_lease(lease()).branch_mode == "CREATE_NEW"
@pytest.mark.parametrize(
("field", "value"),
(
("repository", "other/repo"),
("base_oid", "b" * 40),
("expected_branch_oid", "b" * 40),
("target_branch", "agent/foreign"),
("generation", 2),
),
)
def test_binding_mismatch_fails_closed(field: str, value: object) -> None:
with pytest.raises(RuntimeError, match="LEASE_BINDING_CONFLICT"):
runtime(**{field: value})._normalize_runtime_lease(lease())
def test_drifted_existing_head_fails_closed() -> None:
with pytest.raises(RuntimeError, match="EXPECTED_BRANCH_OID_CONFLICT"):
runtime(head="b" * 40)._normalize_runtime_lease(lease())
def test_openapi_contract_parses_and_requires_exact_commit_lineage() -> None:
document = yaml.safe_load(
Path("docs/gitops/openapi-chatgpt-actions.yaml").read_text(encoding="utf-8")
)
schema = document["components"]["schemas"]["BridgeCommitResponse"]
assert {
"job_id",
"commit_oid",
"commit_visible",
"branch_mode",
"target_branch",
"generation",
} <= set(schema["required"])
def test_changed_production_files_pass_secret_scan() -> None:
paths = [
Path("active-runtime/app/aios_authority_git_executor.py"),
Path("active-runtime/app/aios_chatgpt_bridge_v2_server.py"),
Path("active-runtime/app/aios_continuation_executor.py"),
Path("active-runtime/app/aios_continuation_lease.py"),
Path("active-runtime/app/authorized_task_writer_provisioning.py"),
Path("active-runtime/app/aios_chatgpt_bridge_v2.py"),
Path("docs/gitops/openapi-chatgpt-actions.yaml"),
]
expression = re.compile(
r"(?i)(BEGIN (RSA|OPENSSH|EC) PRIVATE KEY|"
r"authorization:\\s*bearer\\s+[A-Za-z0-9._-]{12,}|"
r"api[_-]?key\\s*[:=]\\s*['\\\"][^'\\\"]{12,})"
)
hits = []
for path in paths:
for number, line in enumerate(
path.read_text(encoding="utf-8", errors="replace").splitlines(), 1
):
if expression.search(line):
hits.append(f"{path}:{number}")
assert hits == []
def test_remote_verified_job_is_not_completed_when_head_cas_callback_fails() -> None:
class Store:
def read(self, _job_id):
return {
"branch": BRANCH,
"request_json": json.dumps(
{"binding": {"expected_branch_oid": BASE}}
),
}
class Controller:
def __init__(self):
self.store = Store()
self.states = []
def advance(self, _job_id, state, **_kwargs):
self.states.append(state)
return SimpleNamespace(as_dict=lambda: {"state": state.value})
executor = ContinuationExecutor.__new__(ContinuationExecutor)
executor.controller = Controller()
executor._observe_remote = lambda _branch, oid: (oid, oid, True)
def fail_cas(_oid):
raise RuntimeError("LEASE_CAS_CONFLICT")
with pytest.raises(RuntimeError, match="LEASE_CAS_CONFLICT"):
executor._finish_remote_verification(
JOB,
"b" * 40,
"b" * 40,
before_complete=fail_cas,
)
assert executor.controller.states == [JobState.REMOTE_VERIFIED]
@pytest.mark.parametrize("branch_mode", ("CREATE_NEW", "CONTINUE_EXISTING"))
def test_executor_preflight_accepts_the_exact_mode_dependent_normalized_shape(
branch_mode: str,
) -> None:
from datetime import datetime, timedelta, timezone
from aios_continuation_executor import (
ContinuationPermissions,
ContinuationRuntimeBinding,
)
request = lease(branch_mode=branch_mode)
now = datetime.now(timezone.utc)
executor = ContinuationExecutor.__new__(ContinuationExecutor)
executor.authority = Authority(None if branch_mode == "CREATE_NEW" else BASE)
executor.now = lambda: now
executor._max_generation = lambda _branch, *, exclude_job_id=None: 0
executor.workspace = SimpleNamespace(
is_detached=lambda: True,
head_oid=lambda: BASE,
)
executor.remote = SimpleNamespace(repository_identity=lambda: REPO)
binding = ContinuationRuntimeBinding(
request=request,
job_id=JOB,
workspace_path=Path("/tmp/continuation-normalization-test"),
lease_not_before=now - timedelta(seconds=1),
lease_expires_at=now + timedelta(seconds=60),
original_receipt_valid=True,
original_grant_consumed=True,
permissions=ContinuationPermissions(),
)
executor._validate_preflight(binding, job_id=JOB)
def test_create_new_executor_resumes_executing_and_uses_absence_bound_writers() -> None:
from datetime import datetime, timedelta, timezone
from aios_continuation_executor import (
ContinuationCommit,
ContinuationPermissions,
ContinuationRuntimeBinding,
)
new_oid = "b" * 40
class Store:
def read(self, _job_id: str) -> dict[str, object]:
return {"state": "EXECUTING", "revision": 4}
class Controller:
def __init__(self) -> None:
self.store = Store()
self.advanced: list[str] = []
def advance(self, _job_id: str, state: JobState, **_kwargs: object) -> object:
self.advanced.append(state.value)
return SimpleNamespace(as_dict=lambda: {"state": state.value})
class Workspace:
def commit_files(self, _files: object, _message: str) -> str:
return new_oid
class CreateAuthority:
def __init__(self) -> None:
self.created: list[tuple[str, str, str]] = []
def is_ancestor(self, older: str, newer: str) -> bool:
return older == BASE and newer == new_oid
def create_task_branch_cas(self, branch: str, base: str, new: str) -> str:
self.created.append((branch, base, new))
return new
def compare_and_swap_branch(self, *_args: object) -> str:
raise AssertionError("existing-branch CAS must not serve CREATE_NEW")
class CreateRemote:
def __init__(self) -> None:
self.created: list[tuple[str, str]] = []
def push_new_exact(self, branch: str, new: str) -> None:
self.created.append((branch, new))
def push_exact(self, *_args: object) -> None:
raise AssertionError("existing-branch push must not serve CREATE_NEW")
class Credential:
def project(self) -> None: pass
def revoke(self) -> None: pass
controller=Controller(); authority=CreateAuthority(); remote=CreateRemote()
executor=ContinuationExecutor(controller,authority,Workspace(),remote,Credential())
executor._validate_preflight=lambda *_args,**_kwargs: None
executor._finish_remote_verification=lambda *_args,**_kwargs: {
"job_id":JOB,"state":"COMPLETED","receipt":{"authority_oid":new_oid,"commit_visible":True}
}
now=datetime.now(timezone.utc)
binding=ContinuationRuntimeBinding(
request=lease(branch_mode="CREATE_NEW"),job_id=JOB,
workspace_path=Path("/tmp/create-new-executor-resume"),
lease_not_before=now-timedelta(seconds=1),lease_expires_at=now+timedelta(seconds=60),
original_receipt_valid=True,original_grant_consumed=True,
permissions=ContinuationPermissions(),
)
result=executor.execute(
binding,ContinuationCommit("create-new-test",{"docs/test.txt":b"ok"},"test")
)
assert result["state"]=="COMPLETED"
assert authority.created==[(BRANCH,BASE,new_oid)]
assert remote.created==[(BRANCH,new_oid)]
assert controller.advanced==["LOCAL_VERIFIED","REMOTE_SYNCING"]
+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) == ""