Compare commits
18
Commits
Executable → Regular
+109
@@ -58,6 +58,11 @@ class AuthorityGitExecutor:
|
|||||||
value = self._git(["rev-parse", "--verify", ref], check=False)
|
value = self._git(["rev-parse", "--verify", ref], check=False)
|
||||||
return value if value and OID_RE.fullmatch(value) else None
|
return value if value and OID_RE.fullmatch(value) else None
|
||||||
|
|
||||||
|
def commit_exists(self, oid: str) -> bool:
|
||||||
|
_oid(oid, "commit_oid")
|
||||||
|
result = subprocess.run(["git", f"--git-dir={self.git_dir}", "cat-file", "-e", oid + "^{commit}"], capture_output=True, check=False)
|
||||||
|
return result.returncode == 0
|
||||||
|
|
||||||
def ancestry_check(self, older_oid: str, newer_oid: str) -> bool:
|
def ancestry_check(self, older_oid: str, newer_oid: str) -> bool:
|
||||||
_oid(older_oid, "older_oid")
|
_oid(older_oid, "older_oid")
|
||||||
_oid(newer_oid, "newer_oid")
|
_oid(newer_oid, "newer_oid")
|
||||||
@@ -70,6 +75,110 @@ class AuthorityGitExecutor:
|
|||||||
raise AuthorityGitError("ANCESTRY_CHECK_FAILED")
|
raise AuthorityGitError("ANCESTRY_CHECK_FAILED")
|
||||||
return result.returncode == 0
|
return result.returncode == 0
|
||||||
|
|
||||||
|
def create_detached_worktree(
|
||||||
|
self,
|
||||||
|
base_oid: str,
|
||||||
|
worktree_name: str,
|
||||||
|
root: Path | None = None,
|
||||||
|
) -> Path:
|
||||||
|
_oid(base_oid, "base_oid")
|
||||||
|
if not worktree_name or "/" in worktree_name or "\\" in worktree_name or worktree_name in {".", ".."}:
|
||||||
|
raise AuthorityGitError("WORKTREE_NAME_INVALID")
|
||||||
|
parent = root or self.worktree_root
|
||||||
|
if not parent.is_absolute() or parent.is_symlink():
|
||||||
|
raise AuthorityGitError("WORKTREE_ROOT_INVALID")
|
||||||
|
path = parent / worktree_name
|
||||||
|
if path.exists() or path.is_symlink():
|
||||||
|
raise AuthorityGitError("WORKTREE_ALREADY_EXISTS")
|
||||||
|
parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
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:
|
def advance_task_branch_CAS(self, branch: str, expected_oid: str, new_oid: str) -> str:
|
||||||
if not TASK_BRANCH_RE.fullmatch(branch):
|
if not TASK_BRANCH_RE.fullmatch(branch):
|
||||||
raise AuthorityGitError("TASK_BRANCH_SCOPE_DENIED")
|
raise AuthorityGitError("TASK_BRANCH_SCOPE_DENIED")
|
||||||
|
|||||||
Executable → Regular
+47
-1
@@ -1,6 +1,6 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
import hashlib, hmac, json, os, re, subprocess, sys, time, urllib.parse, uuid
|
import hashlib, hmac, json, os, re, subprocess, sys, time, urllib.parse, urllib.request, uuid
|
||||||
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any
|
from typing import Any
|
||||||
@@ -404,6 +404,51 @@ class H(BaseHTTPRequestHandler):
|
|||||||
if not a['ok']: self.sendj({'error':'unauthorized','hint':'Bearer token required'},401); return None
|
if not a['ok']: self.sendj({'error':'unauthorized','hint':'Bearer token required'},401); return None
|
||||||
if '*' not in a['scopes'] and scope not in a['scopes']: self.sendj({'error':'forbidden','required_scope':scope,'account':a['account']},403); return None
|
if '*' not in a['scopes'] and scope not in a['scopes']: self.sendj({'error':'forbidden','required_scope':scope,'account':a['account']},403); return None
|
||||||
return a
|
return a
|
||||||
|
def bridge_release(self, path, data):
|
||||||
|
route = {
|
||||||
|
'/createTagApprovalRequest': '/v2/tag/approval-request',
|
||||||
|
'/requestOrCreateTag': '/v2/tag/publish',
|
||||||
|
'/getTagPublicationStatus': '/v2/tag/status',
|
||||||
|
'/buildProductionArtifact': '/v2/release/artifact',
|
||||||
|
'/executeApprovedProductionRelease': '/v2/release/execute',
|
||||||
|
'/getReleaseConsistency': '/v2/release/consistency',
|
||||||
|
'/runReleaseAcceptance': '/v2/release/acceptance',
|
||||||
|
}.get(path)
|
||||||
|
if route is None:
|
||||||
|
raise ValueError('governed_release_route_invalid')
|
||||||
|
base = os.getenv('AIOS_BRIDGE_V2_URL', 'http://127.0.0.1:18892').rstrip('/')
|
||||||
|
request = urllib.request.Request(
|
||||||
|
base + route,
|
||||||
|
data=json.dumps(data, sort_keys=True, separators=(',', ':')).encode('utf-8'),
|
||||||
|
headers={'Content-Type': 'application/json'},
|
||||||
|
method='POST',
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
with urllib.request.urlopen(request, timeout=30) as response:
|
||||||
|
result = json.loads(response.read().decode('utf-8'))
|
||||||
|
return self.sendj(result, response.status)
|
||||||
|
except urllib.error.HTTPError as exc:
|
||||||
|
try:
|
||||||
|
body = json.loads(exc.read().decode('utf-8'))
|
||||||
|
except Exception:
|
||||||
|
body = {'error': 'bridge_http_error'}
|
||||||
|
return self.sendj(body, exc.code)
|
||||||
|
except (urllib.error.URLError, TimeoutError) as exc:
|
||||||
|
return self.sendj({'error': 'GOVERNED_BRIDGE_UNAVAILABLE', 'detail': type(exc).__name__}, 503)
|
||||||
|
def do_POST(self):
|
||||||
|
path = urllib.parse.urlparse(self.path).path
|
||||||
|
if path in {
|
||||||
|
'/createTagApprovalRequest', '/requestOrCreateTag', '/getTagPublicationStatus',
|
||||||
|
'/buildProductionArtifact', '/executeApprovedProductionRelease',
|
||||||
|
'/getReleaseConsistency', '/runReleaseAcceptance',
|
||||||
|
}:
|
||||||
|
actor = self.need('tag:create')
|
||||||
|
if not actor:
|
||||||
|
return
|
||||||
|
return self.bridge_release(path, self.body())
|
||||||
|
if reject_non_read_http(self, 'POST', path):
|
||||||
|
return
|
||||||
|
return self.sendj({'error': 'not_found', 'path': path}, 404)
|
||||||
def do_GET(self):
|
def do_GET(self):
|
||||||
u=urllib.parse.urlparse(self.path); path=u.path.rstrip('/') if u.path != '/' else u.path
|
u=urllib.parse.urlparse(self.path); path=u.path.rstrip('/') if u.path != '/' else u.path
|
||||||
if path not in READ_ONLY_GET_PATHS: return self.sendj({'error':'not_found','path':path},404)
|
if path not in READ_ONLY_GET_PATHS: return self.sendj({'error':'not_found','path':path},404)
|
||||||
@@ -905,3 +950,4 @@ def main():
|
|||||||
print(f'AIOS ChatGPT GitOps adapter listening on {HOST}:{PORT}')
|
print(f'AIOS ChatGPT GitOps adapter listening on {HOST}:{PORT}')
|
||||||
ThreadingHTTPServer((HOST,PORT), H).serve_forever()
|
ThreadingHTTPServer((HOST,PORT), H).serve_forever()
|
||||||
if __name__ == '__main__': main()
|
if __name__ == '__main__': main()
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,224 @@
|
|||||||
|
"""ChatGPT GitOps Bridge v2 contract and governed release operations."""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from dataclasses import dataclass
|
||||||
|
import hashlib
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Any, Callable, Mapping
|
||||||
|
|
||||||
|
|
||||||
|
BRIDGE_SCHEMA_VERSION = "aios.chatgpt-gitops-bridge.v2"
|
||||||
|
BRIDGE_OPERATIONS = (
|
||||||
|
"getRuntimeStatus",
|
||||||
|
"getGovernedWriteCapabilities",
|
||||||
|
"createGovernedTask",
|
||||||
|
"acquireWriterLease",
|
||||||
|
"bootstrapWriterLease",
|
||||||
|
"renewWriterLease",
|
||||||
|
"commitFileGoverned",
|
||||||
|
"commitFilesGoverned",
|
||||||
|
"getGitOpsJob",
|
||||||
|
"resumeGitOpsJob",
|
||||||
|
"reconcileGitOpsJob",
|
||||||
|
"createTagApprovalRequest",
|
||||||
|
"requestOrCreateTag",
|
||||||
|
"getTagPublicationStatus",
|
||||||
|
"buildProductionArtifact",
|
||||||
|
"executeApprovedProductionRelease",
|
||||||
|
"getReleaseConsistency",
|
||||||
|
"runReleaseAcceptance",
|
||||||
|
"getControlPlaneSnapshot",
|
||||||
|
)
|
||||||
|
|
||||||
|
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",),
|
||||||
|
"resumeGitOpsJob": ("job_id", "expected_revision"),
|
||||||
|
"reconcileGitOpsJob": ("job_id", "expected_revision"),
|
||||||
|
"createTagApprovalRequest": ("version", "target_oid", "evidence_closure_oid", "approval_plan_id", "owner_approval", "execute"),
|
||||||
|
"requestOrCreateTag": ("version", "target_oid", "evidence_closure_oid", "approval_plan_id", "execute"),
|
||||||
|
"getTagPublicationStatus": ("version",),
|
||||||
|
"buildProductionArtifact": ("version", "target_oid", "evidence_closure_oid", "approval_plan_id"),
|
||||||
|
"executeApprovedProductionRelease": ("version", "target_oid", "evidence_closure_oid", "approval_plan_id", "execute"),
|
||||||
|
"getReleaseConsistency": ("version", "target_oid", "evidence_closure_oid", "approval_plan_id"),
|
||||||
|
"runReleaseAcceptance": ("version", "target_oid", "evidence_closure_oid", "approval_plan_id"),
|
||||||
|
"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
|
||||||
|
|
||||||
|
|
||||||
|
class BridgeV2Error(RuntimeError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True, slots=True)
|
||||||
|
class ControlPlaneReleaseManifest:
|
||||||
|
control_plane_release_version: str
|
||||||
|
control_plane_target_oid: str
|
||||||
|
artifact_sha256: str
|
||||||
|
runtime_path: Path
|
||||||
|
bridge_schema_version: str
|
||||||
|
repository_identity: str
|
||||||
|
authoritative_git_dir: Path
|
||||||
|
deployment_state: str
|
||||||
|
|
||||||
|
def as_dict(self) -> dict[str, object]:
|
||||||
|
return {
|
||||||
|
"control_plane_release_version": self.control_plane_release_version,
|
||||||
|
"control_plane_target_oid": self.control_plane_target_oid,
|
||||||
|
"artifact_sha256": self.artifact_sha256,
|
||||||
|
"runtime_path": str(self.runtime_path),
|
||||||
|
"bridge_schema_version": self.bridge_schema_version,
|
||||||
|
"repository_identity": self.repository_identity,
|
||||||
|
"authoritative_git_dir": str(self.authoritative_git_dir),
|
||||||
|
"deployment_state": self.deployment_state,
|
||||||
|
}
|
||||||
|
|
||||||
|
def validate(self, artifact: Path, *, observed_oid: str, approved_root: Path = Path("/opt/aios/gitops-control-plane")) -> dict[str, object]:
|
||||||
|
try:
|
||||||
|
runtime = self.runtime_path.resolve(strict=True)
|
||||||
|
root = approved_root.resolve(strict=True)
|
||||||
|
artifact_path = artifact.resolve(strict=True)
|
||||||
|
runtime.relative_to(root)
|
||||||
|
artifact_path.relative_to(runtime)
|
||||||
|
except (OSError, ValueError):
|
||||||
|
raise RuntimeDriftError("CONTROL_PLANE_RUNTIME_DRIFT") from None
|
||||||
|
mutable_markers = {"workspace", "workspaces", "worktree", "worktrees"}
|
||||||
|
if mutable_markers.intersection(part.lower() for part in runtime.parts):
|
||||||
|
raise RuntimeDriftError("CONTROL_PLANE_RUNTIME_DRIFT")
|
||||||
|
valid = (
|
||||||
|
len(self.control_plane_target_oid) == 40
|
||||||
|
and observed_oid == self.control_plane_target_oid
|
||||||
|
and len(self.artifact_sha256) == 64
|
||||||
|
and hashlib.sha256(artifact_path.read_bytes()).hexdigest() == self.artifact_sha256
|
||||||
|
and self.bridge_schema_version == BRIDGE_SCHEMA_VERSION
|
||||||
|
and self.repository_identity == "aios/aios"
|
||||||
|
and self.authoritative_git_dir == Path("/data/aios-gitops/repo.git")
|
||||||
|
and self.authoritative_git_dir.is_absolute()
|
||||||
|
and self.deployment_state == "STAGING"
|
||||||
|
)
|
||||||
|
if not valid:
|
||||||
|
raise RuntimeDriftError("CONTROL_PLANE_RUNTIME_DRIFT")
|
||||||
|
return {**self.as_dict(), "drift_status": "PASS"}
|
||||||
|
|
||||||
|
|
||||||
|
class BridgeV2Adapter:
|
||||||
|
"""Thin operation adapter; all mutating handlers are injected seams."""
|
||||||
|
|
||||||
|
def __init__(self, *, runtime_status: Mapping[str, Any], handlers: Mapping[str, Callable[[Mapping[str, Any]], Any]] | None = None) -> None:
|
||||||
|
self.runtime_status = dict(runtime_status)
|
||||||
|
self.handlers = dict(handlers or {})
|
||||||
|
|
||||||
|
def getRuntimeStatus(self) -> dict[str, Any]:
|
||||||
|
return {"bridge_schema_version": BRIDGE_SCHEMA_VERSION, **self.runtime_status}
|
||||||
|
|
||||||
|
def getGovernedWriteCapabilities(self) -> dict[str, Any]:
|
||||||
|
return {
|
||||||
|
"bridge_schema_version": BRIDGE_SCHEMA_VERSION,
|
||||||
|
"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,
|
||||||
|
"baseline_write": False,
|
||||||
|
"production_write": False,
|
||||||
|
"approved_release_operations": True,
|
||||||
|
"approved_release_scope": {
|
||||||
|
"version": "V2R2C1",
|
||||||
|
"target_oid": "998d30304787e31e5ae607c0bd86b5edb2b0e5ad",
|
||||||
|
"evidence_closure_oid": "a8b906d1bc24088c19647e98de6a679268f6d0a2",
|
||||||
|
"approval_plan_id": "sha256:c0df52008ae7f64cec5ee2d28947aa7b892066c670ab8295e4549140b1a7500e",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def legacy_write_disabled() -> dict[str, object]:
|
||||||
|
return {"error": "LEGACY_WRITE_DISABLED", "replacement": "createGovernedTask", "governed_writes_available": True}
|
||||||
|
|
||||||
|
def _call(self, operation: str, request: Mapping[str, Any]) -> Any:
|
||||||
|
required = OPERATION_REQUIRED_FIELDS[operation]
|
||||||
|
if any(key not in request for key in required):
|
||||||
|
raise BridgeV2Error("BRIDGE_REQUEST_FIELDS_MISSING")
|
||||||
|
handler = self.handlers.get(operation)
|
||||||
|
if handler is None:
|
||||||
|
raise BridgeV2Error("GOVERNED_HANDLER_UNAVAILABLE")
|
||||||
|
return handler(dict(request))
|
||||||
|
|
||||||
|
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)
|
||||||
|
def resumeGitOpsJob(self, request: Mapping[str, Any]) -> Any: return self._call("resumeGitOpsJob", request)
|
||||||
|
def reconcileGitOpsJob(self, request: Mapping[str, Any]) -> Any: return self._call("reconcileGitOpsJob", request)
|
||||||
|
def createTagApprovalRequest(self, request: Mapping[str, Any]) -> Any: return self._call("createTagApprovalRequest", request)
|
||||||
|
def requestOrCreateTag(self, request: Mapping[str, Any]) -> Any: return self._call("requestOrCreateTag", request)
|
||||||
|
def getTagPublicationStatus(self, request: Mapping[str, Any]) -> Any: return self._call("getTagPublicationStatus", request)
|
||||||
|
def buildProductionArtifact(self, request: Mapping[str, Any]) -> Any: return self._call("buildProductionArtifact", request)
|
||||||
|
def executeApprovedProductionRelease(self, request: Mapping[str, Any]) -> Any: return self._call("executeApprovedProductionRelease", request)
|
||||||
|
def getReleaseConsistency(self, request: Mapping[str, Any]) -> Any: return self._call("getReleaseConsistency", request)
|
||||||
|
def runReleaseAcceptance(self, request: Mapping[str, Any]) -> Any: return self._call("runReleaseAcceptance", request)
|
||||||
|
|
||||||
|
def getControlPlaneSnapshot(self, request: Mapping[str, Any] | None = None) -> Any: return self._call("getControlPlaneSnapshot", request or {})
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,692 @@
|
|||||||
|
"""Governed continuation executor for exact existing task branches.
|
||||||
|
|
||||||
|
The executor composes the existing continuation lease and durable Job store.
|
||||||
|
All Git and Gitea side effects live behind injected narrow interfaces so the
|
||||||
|
state machine can prove Authority, Git-network, API-branch, and commit-object
|
||||||
|
agreement before it reports completion.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import base64
|
||||||
|
from dataclasses import asdict, dataclass
|
||||||
|
from datetime import datetime, timezone
|
||||||
|
import hashlib
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
from pathlib import Path, PurePosixPath
|
||||||
|
import re
|
||||||
|
import subprocess
|
||||||
|
from typing import Callable, Mapping, Protocol
|
||||||
|
from urllib.error import HTTPError, URLError
|
||||||
|
from urllib.parse import quote, urlsplit
|
||||||
|
from urllib.request import Request, urlopen
|
||||||
|
|
||||||
|
from aios_authority_git_executor import AuthorityGitExecutor
|
||||||
|
from aios_continuation_lease import WriterLeaseRequestV2
|
||||||
|
from aios_gitops_job_controller import GitOpsJobController, JobState
|
||||||
|
from aios_gitops_reconciliation_core import ReconciliationAction, RemoteObservation, decide_reconciliation
|
||||||
|
from authorized_task_writer_provisioning import validate_writer_lease_request_v2
|
||||||
|
|
||||||
|
|
||||||
|
OID_RE = re.compile(r"^[0-9a-f]{40}$")
|
||||||
|
SECRET_PATTERNS = (
|
||||||
|
re.compile(rb"gh[pousr]_[A-Za-z0-9]{20,}"),
|
||||||
|
re.compile(rb"(?i)(?:token|password|secret|api[_-]?key)\s*[:=]\s*[\"'][^\"'\s]{12,}[\"']"),
|
||||||
|
re.compile(rb"-----BEGIN " + rb"(?:RSA |EC |OPENSSH )?PRIVATE KEY-----"),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class ContinuationExecutionError(RuntimeError):
|
||||||
|
"""Stable fail-closed continuation execution error."""
|
||||||
|
|
||||||
|
|
||||||
|
class DetachedWorkspace(Protocol):
|
||||||
|
def is_detached(self) -> bool: ...
|
||||||
|
def head_oid(self) -> str: ...
|
||||||
|
def commit_files(self, files: Mapping[str, bytes], message: str) -> str: ...
|
||||||
|
|
||||||
|
|
||||||
|
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: ...
|
||||||
|
def commit_visible(self, oid: str) -> bool: ...
|
||||||
|
|
||||||
|
|
||||||
|
class CredentialProjection(Protocol):
|
||||||
|
def project(self) -> None: ...
|
||||||
|
def revoke(self) -> None: ...
|
||||||
|
|
||||||
|
|
||||||
|
def _run(argv: list[str], *, cwd: Path | None = None, environment: Mapping[str, str] | None = None) -> str:
|
||||||
|
result = subprocess.run(
|
||||||
|
argv,
|
||||||
|
cwd=cwd,
|
||||||
|
env=None if environment is None else dict(environment),
|
||||||
|
text=True,
|
||||||
|
capture_output=True,
|
||||||
|
check=False,
|
||||||
|
)
|
||||||
|
if result.returncode != 0:
|
||||||
|
raise ContinuationExecutionError("SUBPROCESS_FAILED")
|
||||||
|
return result.stdout.strip()
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(slots=True)
|
||||||
|
class AuthorityGitAdapter:
|
||||||
|
"""Branch-name adapter over the existing narrow AuthorityGitExecutor."""
|
||||||
|
|
||||||
|
executor: AuthorityGitExecutor
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _ref(branch: str) -> str:
|
||||||
|
return f"refs/heads/{branch}"
|
||||||
|
|
||||||
|
def branch_exists(self, branch: str) -> bool:
|
||||||
|
return self.executor.read_ref(self._ref(branch)) is not None
|
||||||
|
|
||||||
|
def branch_head(self, branch: str) -> str:
|
||||||
|
value = self.executor.read_ref(self._ref(branch))
|
||||||
|
if value is None:
|
||||||
|
raise ContinuationExecutionError("EXISTING_BRANCH_MISSING")
|
||||||
|
return value
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(slots=True)
|
||||||
|
class DetachedGitWorkspace:
|
||||||
|
path: Path
|
||||||
|
|
||||||
|
def __post_init__(self) -> None:
|
||||||
|
if not self.path.is_absolute() or self.path.is_symlink() or not self.path.is_dir():
|
||||||
|
raise ContinuationExecutionError("WORKTREE_PATH_INVALID")
|
||||||
|
top = Path(_run(["git", "rev-parse", "--show-toplevel"], cwd=self.path)).resolve()
|
||||||
|
if top != self.path.resolve():
|
||||||
|
raise ContinuationExecutionError("WORKTREE_PATH_INVALID")
|
||||||
|
|
||||||
|
def is_detached(self) -> bool:
|
||||||
|
result = subprocess.run(
|
||||||
|
["git", "symbolic-ref", "-q", "HEAD"],
|
||||||
|
cwd=self.path,
|
||||||
|
capture_output=True,
|
||||||
|
check=False,
|
||||||
|
)
|
||||||
|
return result.returncode == 1
|
||||||
|
|
||||||
|
def head_oid(self) -> str:
|
||||||
|
return _run(["git", "rev-parse", "HEAD"], cwd=self.path)
|
||||||
|
|
||||||
|
def _dirty_paths(self) -> set[str]:
|
||||||
|
command = subprocess.run(
|
||||||
|
["git", "status", "--porcelain=v1", "--untracked-files=all"],
|
||||||
|
cwd=self.path,
|
||||||
|
text=True,
|
||||||
|
capture_output=True,
|
||||||
|
check=False,
|
||||||
|
)
|
||||||
|
if command.returncode != 0:
|
||||||
|
raise ContinuationExecutionError("SUBPROCESS_FAILED")
|
||||||
|
output = command.stdout
|
||||||
|
result: set[str] = set()
|
||||||
|
for line in output.splitlines():
|
||||||
|
path = line[3:]
|
||||||
|
if " -> " in path:
|
||||||
|
path = path.split(" -> ", 1)[1]
|
||||||
|
result.add(path)
|
||||||
|
return result
|
||||||
|
|
||||||
|
def materialize_files(self, files: Mapping[str, bytes]) -> None:
|
||||||
|
dirty = self._dirty_paths()
|
||||||
|
expected_paths = set(files)
|
||||||
|
if dirty and dirty != expected_paths:
|
||||||
|
raise ContinuationExecutionError("WORKTREE_DIRTY_SCOPE_MISMATCH")
|
||||||
|
for relative, content in files.items():
|
||||||
|
target = (self.path / relative).resolve()
|
||||||
|
try:
|
||||||
|
target.relative_to(self.path.resolve())
|
||||||
|
except ValueError as exc:
|
||||||
|
raise ContinuationExecutionError("COMMIT_PATH_REJECTED") from exc
|
||||||
|
if target.is_symlink():
|
||||||
|
raise ContinuationExecutionError("COMMIT_PATH_REJECTED")
|
||||||
|
target.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
target.write_bytes(content)
|
||||||
|
|
||||||
|
def commit_files(self, files: Mapping[str, bytes], message: str) -> str:
|
||||||
|
expected_paths = set(files)
|
||||||
|
if self._dirty_paths() != expected_paths:
|
||||||
|
raise ContinuationExecutionError("WORKTREE_DIRTY_SCOPE_MISMATCH")
|
||||||
|
for relative, content in files.items():
|
||||||
|
target = (self.path / relative).resolve()
|
||||||
|
try:
|
||||||
|
target.relative_to(self.path.resolve())
|
||||||
|
except ValueError as exc:
|
||||||
|
raise ContinuationExecutionError("COMMIT_PATH_REJECTED") from exc
|
||||||
|
if not target.is_file() or target.is_symlink() or target.read_bytes() != content:
|
||||||
|
raise ContinuationExecutionError("AUTHORING_PAYLOAD_MISMATCH")
|
||||||
|
_run(["git", "add", "--", *sorted(expected_paths)], cwd=self.path)
|
||||||
|
staged = set(_run(["git", "diff", "--cached", "--name-only"], cwd=self.path).splitlines())
|
||||||
|
if staged != expected_paths:
|
||||||
|
raise ContinuationExecutionError("STAGED_SCOPE_MISMATCH")
|
||||||
|
environment = os.environ.copy()
|
||||||
|
environment.update(
|
||||||
|
{
|
||||||
|
"GIT_AUTHOR_NAME": "AIOS Governed Continuation",
|
||||||
|
"GIT_AUTHOR_EMAIL": "aios-governed-continuation@localhost",
|
||||||
|
"GIT_COMMITTER_NAME": "AIOS Governed Continuation",
|
||||||
|
"GIT_COMMITTER_EMAIL": "aios-governed-continuation@localhost",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
_run(["git", "commit", "--no-gpg-sign", "-m", message, "--", *sorted(expected_paths)], cwd=self.path, environment=environment)
|
||||||
|
if not self.is_detached():
|
||||||
|
raise ContinuationExecutionError("WORKTREE_NOT_DETACHED")
|
||||||
|
return self.head_oid()
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(slots=True)
|
||||||
|
class ServerSideCredentialProjection:
|
||||||
|
ssh_key_path: Path
|
||||||
|
api_token_path: Path
|
||||||
|
active: bool = False
|
||||||
|
|
||||||
|
def project(self) -> None:
|
||||||
|
for path in (self.ssh_key_path, self.api_token_path):
|
||||||
|
if path.is_symlink() or not path.is_file() or path.stat().st_size < 32:
|
||||||
|
raise ContinuationExecutionError("CREDENTIAL_PROJECTION_MISSING")
|
||||||
|
self.active = True
|
||||||
|
|
||||||
|
def revoke(self) -> None:
|
||||||
|
self.active = False
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(slots=True)
|
||||||
|
class GiteaContinuationRemote:
|
||||||
|
workspace: Path
|
||||||
|
credential: ServerSideCredentialProjection
|
||||||
|
remote_name: str = "gitea"
|
||||||
|
repository: str = "aios/aios"
|
||||||
|
api_base: str = "https://g.cnnetmkt.top/git/api/v1"
|
||||||
|
expected_host: str = "g.cnnetmkt.top"
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def validate_remote_url(value: str, expected_host: str = "g.cnnetmkt.top") -> None:
|
||||||
|
if value.startswith(("/", "file:")) or value.endswith("/data/aios-gitops/repo.git"):
|
||||||
|
raise ContinuationExecutionError("GITEA_REMOTE_IDENTITY_INVALID")
|
||||||
|
if "://" in value:
|
||||||
|
parsed = urlsplit(value)
|
||||||
|
host, path = parsed.hostname, parsed.path
|
||||||
|
else:
|
||||||
|
location = value.split("@", 1)[-1]
|
||||||
|
host, separator, path = location.partition(":")
|
||||||
|
if not separator:
|
||||||
|
raise ContinuationExecutionError("GITEA_REMOTE_IDENTITY_INVALID")
|
||||||
|
path = "/" + path
|
||||||
|
if host != expected_host or path.rstrip("/") not in {"/aios/aios.git", "/git/aios/aios.git"}:
|
||||||
|
raise ContinuationExecutionError("GITEA_REMOTE_IDENTITY_INVALID")
|
||||||
|
|
||||||
|
def repository_identity(self) -> str:
|
||||||
|
url = _run(["git", "remote", "get-url", self.remote_name], cwd=self.workspace)
|
||||||
|
self.validate_remote_url(url, self.expected_host)
|
||||||
|
if self.repository != "aios/aios":
|
||||||
|
raise ContinuationExecutionError("REMOTE_REPOSITORY_IDENTITY_MISMATCH")
|
||||||
|
return self.repository
|
||||||
|
|
||||||
|
def _require_projected(self) -> None:
|
||||||
|
if not self.credential.active:
|
||||||
|
raise ContinuationExecutionError("CREDENTIAL_PROJECTION_MISSING")
|
||||||
|
|
||||||
|
def read_git_ref(self, branch: str) -> str | None:
|
||||||
|
self._require_projected()
|
||||||
|
output = _run(["git", "ls-remote", self.remote_name, f"refs/heads/{branch}"], cwd=self.workspace)
|
||||||
|
if not output:
|
||||||
|
return None
|
||||||
|
oid, ref = output.split(maxsplit=1)
|
||||||
|
if ref != f"refs/heads/{branch}" or not OID_RE.fullmatch(oid):
|
||||||
|
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()
|
||||||
|
if self.read_git_ref(branch) != expected:
|
||||||
|
raise ContinuationExecutionError("GITEA_BRANCH_EXPECTATION_MISMATCH")
|
||||||
|
_run(["git", "push", self.remote_name, f"{new}:refs/heads/{branch}"], cwd=self.workspace)
|
||||||
|
|
||||||
|
def _api(self, path: str) -> object:
|
||||||
|
self._require_projected()
|
||||||
|
token = self.credential.api_token_path.read_text(encoding="utf-8").strip()
|
||||||
|
request = Request(
|
||||||
|
self.api_base.rstrip("/") + path,
|
||||||
|
headers={"Accept": "application/json", "Authorization": f"token {token}"},
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
with urlopen(request, timeout=20) as response:
|
||||||
|
return json.loads(response.read().decode("utf-8"))
|
||||||
|
except HTTPError as exc:
|
||||||
|
if exc.code == 404:
|
||||||
|
return None
|
||||||
|
raise ContinuationExecutionError(f"GITEA_API_HTTP_{exc.code}") from exc
|
||||||
|
except (URLError, TimeoutError, OSError, json.JSONDecodeError) as exc:
|
||||||
|
raise ContinuationExecutionError("GITEA_API_UNAVAILABLE") from exc
|
||||||
|
|
||||||
|
def read_api_branch(self, branch: str) -> str | None:
|
||||||
|
owner, name = self.repository.split("/", 1)
|
||||||
|
page = 1
|
||||||
|
while page <= 100:
|
||||||
|
value = self._api(f"/repos/{quote(owner)}/{quote(name)}/branches?page={page}&limit=50")
|
||||||
|
if value is None:
|
||||||
|
return None
|
||||||
|
if not isinstance(value, list):
|
||||||
|
raise ContinuationExecutionError("GITEA_API_BRANCH_COLLECTION_INVALID")
|
||||||
|
for item in value:
|
||||||
|
if not isinstance(item, dict) or item.get("name") != branch:
|
||||||
|
continue
|
||||||
|
oid = item.get("commit", {}).get("id")
|
||||||
|
if not isinstance(oid, str) or not OID_RE.fullmatch(oid):
|
||||||
|
raise ContinuationExecutionError("GITEA_API_BRANCH_INVALID")
|
||||||
|
return oid
|
||||||
|
if len(value) < 50:
|
||||||
|
return None
|
||||||
|
page += 1
|
||||||
|
raise ContinuationExecutionError("GITEA_API_BRANCH_PAGINATION_LIMIT")
|
||||||
|
|
||||||
|
def commit_visible(self, oid: str) -> bool:
|
||||||
|
owner, name = self.repository.split("/", 1)
|
||||||
|
value = self._api(f"/repos/{quote(owner)}/{quote(name)}/git/commits/{oid}")
|
||||||
|
if not isinstance(value, dict):
|
||||||
|
return False
|
||||||
|
observed = value.get("sha") or value.get("id")
|
||||||
|
return observed == oid
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True, slots=True)
|
||||||
|
class ContinuationPermissions:
|
||||||
|
create_target_branch: bool = False
|
||||||
|
force_push: bool = False
|
||||||
|
tag_write: bool = False
|
||||||
|
baseline_write: bool = False
|
||||||
|
production_write: bool = False
|
||||||
|
main_write: bool = False
|
||||||
|
|
||||||
|
def validate(self) -> None:
|
||||||
|
if any(asdict(self).values()):
|
||||||
|
raise ContinuationExecutionError("MUTATION_PERMISSION_DENIED")
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True, slots=True)
|
||||||
|
class ContinuationRuntimeBinding:
|
||||||
|
request: WriterLeaseRequestV2
|
||||||
|
job_id: str
|
||||||
|
workspace_path: Path
|
||||||
|
lease_not_before: datetime
|
||||||
|
lease_expires_at: datetime
|
||||||
|
original_receipt_valid: bool
|
||||||
|
original_grant_consumed: bool
|
||||||
|
permissions: ContinuationPermissions
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True, slots=True)
|
||||||
|
class ContinuationCommit:
|
||||||
|
idempotency_key: str
|
||||||
|
files: Mapping[str, bytes]
|
||||||
|
message: str
|
||||||
|
|
||||||
|
def payload(self) -> bytes:
|
||||||
|
if not self.idempotency_key or not self.message.strip() or not self.files:
|
||||||
|
raise ContinuationExecutionError("COMMIT_REQUEST_INVALID")
|
||||||
|
normalized: dict[str, str] = {}
|
||||||
|
for path, content in self.files.items():
|
||||||
|
pure = PurePosixPath(path)
|
||||||
|
if (
|
||||||
|
not isinstance(path, str)
|
||||||
|
or pure.is_absolute()
|
||||||
|
or ".." in pure.parts
|
||||||
|
or path.startswith((".git/", "baseline/", "production/"))
|
||||||
|
or not isinstance(content, bytes)
|
||||||
|
):
|
||||||
|
raise ContinuationExecutionError("COMMIT_PATH_REJECTED")
|
||||||
|
if any(pattern.search(content) for pattern in SECRET_PATTERNS):
|
||||||
|
raise ContinuationExecutionError("SECRET_LIKE_PAYLOAD_REJECTED")
|
||||||
|
normalized[path] = base64.b64encode(content).decode("ascii")
|
||||||
|
return json.dumps(
|
||||||
|
{"files_base64": normalized, "message": self.message},
|
||||||
|
sort_keys=True,
|
||||||
|
separators=(",", ":"),
|
||||||
|
).encode("utf-8")
|
||||||
|
|
||||||
|
|
||||||
|
class ContinuationExecutor:
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
controller: GitOpsJobController,
|
||||||
|
authority: AuthorityPort,
|
||||||
|
workspace: DetachedWorkspace,
|
||||||
|
remote: RemotePort,
|
||||||
|
credential: CredentialProjection,
|
||||||
|
*,
|
||||||
|
now: Callable[[], datetime] = lambda: datetime.now(timezone.utc),
|
||||||
|
) -> None:
|
||||||
|
self.controller = controller
|
||||||
|
self.authority = authority
|
||||||
|
self.workspace = workspace
|
||||||
|
self.remote = remote
|
||||||
|
self.credential = credential
|
||||||
|
self.now = now
|
||||||
|
self._observed_generations: dict[str, int] = {}
|
||||||
|
|
||||||
|
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, *, exclude_job_id: str | None = None) -> int:
|
||||||
|
observed = self._observed_generations.get(branch, 0)
|
||||||
|
with self.controller.store._connect() as connection:
|
||||||
|
if exclude_job_id is None:
|
||||||
|
row = connection.execute(
|
||||||
|
"SELECT MAX(generation) AS generation FROM branch_leases WHERE branch=?",
|
||||||
|
(branch,),
|
||||||
|
).fetchone()
|
||||||
|
else:
|
||||||
|
row = connection.execute(
|
||||||
|
"SELECT MAX(generation) AS generation FROM branch_leases "
|
||||||
|
"WHERE branch=? AND job_id!=?",
|
||||||
|
(branch, exclude_job_id),
|
||||||
|
).fetchone()
|
||||||
|
durable = int(row["generation"] or 0)
|
||||||
|
return max(observed, durable)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _request_mapping(request: WriterLeaseRequestV2) -> dict[str, object]:
|
||||||
|
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,
|
||||||
|
"expected_branch_oid": request.expected_branch_oid,
|
||||||
|
"target_branch": request.target_branch,
|
||||||
|
"generation": request.generation,
|
||||||
|
"lease_seconds": request.lease_seconds,
|
||||||
|
}
|
||||||
|
|
||||||
|
def _validate_preflight(self, binding: ContinuationRuntimeBinding, *, job_id: str | None = None) -> None:
|
||||||
|
request = binding.request
|
||||||
|
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")
|
||||||
|
binding.permissions.validate()
|
||||||
|
now = self.now()
|
||||||
|
if now < binding.lease_not_before:
|
||||||
|
raise ContinuationExecutionError("LEASE_NOT_ACTIVE")
|
||||||
|
if now >= binding.lease_expires_at:
|
||||||
|
raise ContinuationExecutionError("LEASE_EXPIRED")
|
||||||
|
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")
|
||||||
|
if self.workspace.head_oid() != request.expected_branch_oid:
|
||||||
|
raise ContinuationExecutionError("WORKTREE_HEAD_MISMATCH")
|
||||||
|
if self.remote.repository_identity() != request.repository:
|
||||||
|
raise ContinuationExecutionError("REMOTE_REPOSITORY_IDENTITY_MISMATCH")
|
||||||
|
|
||||||
|
def _job_request(self, binding: ContinuationRuntimeBinding, commit: ContinuationCommit, payload: bytes) -> dict[str, object]:
|
||||||
|
request = binding.request
|
||||||
|
return {
|
||||||
|
"idempotency_key": commit.idempotency_key,
|
||||||
|
"campaign_id": request.task_id,
|
||||||
|
"task_id": request.task_id,
|
||||||
|
"repository": request.repository,
|
||||||
|
"operation": "CONTINUATION_COMMIT_FILES",
|
||||||
|
"branch": request.target_branch,
|
||||||
|
"expected_branch_oid": request.expected_branch_oid,
|
||||||
|
"base_oid": request.base_oid,
|
||||||
|
"branch_mode": request.branch_mode,
|
||||||
|
"payload_digest": hashlib.sha256(payload).hexdigest(),
|
||||||
|
"lineage_anchor_oid": request.base_oid,
|
||||||
|
"generation": request.generation,
|
||||||
|
}
|
||||||
|
|
||||||
|
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()
|
||||||
|
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(binding.job_id)
|
||||||
|
state = JobState(row["state"])
|
||||||
|
if state == JobState.REQUESTED:
|
||||||
|
self.controller.advance(
|
||||||
|
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(binding.job_id)
|
||||||
|
state = JobState(row["state"])
|
||||||
|
if state == JobState.AUTHORIZED:
|
||||||
|
self.controller.lease_job(
|
||||||
|
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"]),
|
||||||
|
)
|
||||||
|
row = self.controller.store.read(binding.job_id)
|
||||||
|
state = JobState(row["state"])
|
||||||
|
if state == JobState.LEASED:
|
||||||
|
self.controller.advance(binding.job_id, JobState.WORKTREE_READY,
|
||||||
|
side_effect_state="WORKTREE_READY", expected_revision=int(row["revision"]))
|
||||||
|
row = self.controller.store.read(binding.job_id)
|
||||||
|
state = JobState(row["state"])
|
||||||
|
if state == JobState.WORKTREE_READY:
|
||||||
|
self.controller.advance(binding.job_id, JobState.EXECUTING,
|
||||||
|
side_effect_state="IN_PROGRESS", expected_revision=int(row["revision"]))
|
||||||
|
elif state != JobState.EXECUTING:
|
||||||
|
raise ContinuationExecutionError("JOB_EXECUTION_STATE_INVALID")
|
||||||
|
new_oid = self.workspace.commit_files(commit.files, commit.message)
|
||||||
|
if not OID_RE.fullmatch(new_oid) or not self.authority.is_ancestor(request.expected_branch_oid, new_oid):
|
||||||
|
raise ContinuationExecutionError("NON_FAST_FORWARD_COMMIT")
|
||||||
|
if request.branch_mode == "CREATE_NEW":
|
||||||
|
authority_oid = self.authority.create_task_branch_cas(request.target_branch, request.base_oid, new_oid)
|
||||||
|
else:
|
||||||
|
authority_oid = self.authority.compare_and_swap_branch(request.target_branch, request.expected_branch_oid, new_oid)
|
||||||
|
if authority_oid != new_oid:
|
||||||
|
raise ContinuationExecutionError("AUTHORITY_CAS_READBACK_MISMATCH")
|
||||||
|
self.controller.advance(
|
||||||
|
binding.job_id,
|
||||||
|
JobState.LOCAL_VERIFIED,
|
||||||
|
side_effect_state="LOCAL_VERIFIED",
|
||||||
|
receipt={"authority_oid": authority_oid, "force_push": False},
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
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(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()
|
||||||
|
|
||||||
|
def _observe_remote(self, branch: str, oid: str) -> tuple[str | None, str | None, bool]:
|
||||||
|
return self.remote.read_git_ref(branch), self.remote.read_api_branch(branch), self.remote.commit_visible(oid)
|
||||||
|
|
||||||
|
def _remote_pending(self, job_id: str, authority_oid: str, new_oid: str) -> dict[str, object]:
|
||||||
|
receipt = {
|
||||||
|
"authority_oid": authority_oid,
|
||||||
|
"desired_remote_oid": new_oid,
|
||||||
|
"remote_verified": False,
|
||||||
|
"force_push": False,
|
||||||
|
}
|
||||||
|
result = self.controller.advance(
|
||||||
|
job_id,
|
||||||
|
JobState.RECONCILIATION_REQUIRED,
|
||||||
|
side_effect_state="REMOTE_PUSH_PENDING",
|
||||||
|
receipt=receipt,
|
||||||
|
)
|
||||||
|
return result.as_dict()
|
||||||
|
|
||||||
|
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"]
|
||||||
|
git_oid, api_oid, visible = self._observe_remote(branch, new_oid)
|
||||||
|
decision = decide_reconciliation(RemoteObservation("aios/aios", branch, new_oid, authority_oid, git_oid, api_oid, visible, datetime.now(timezone.utc).isoformat(), request.get("expected_branch_oid")))
|
||||||
|
if decision.action is not ReconciliationAction.MARK_REMOTE_VERIFIED:
|
||||||
|
return self._remote_pending(job_id, authority_oid, new_oid)
|
||||||
|
receipt = {
|
||||||
|
"authority_oid": authority_oid,
|
||||||
|
"git_remote_oid": git_oid,
|
||||||
|
"api_branch_oid": api_oid,
|
||||||
|
"commit_visible": True,
|
||||||
|
"remote_verified": True,
|
||||||
|
"reconciliation_decision": decision.action.value,
|
||||||
|
"force_push": False,
|
||||||
|
}
|
||||||
|
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)
|
||||||
|
state = JobState(row["state"])
|
||||||
|
if state == JobState.COMPLETED:
|
||||||
|
return self.controller.store._receipt(row, duplicate=True).as_dict()
|
||||||
|
if state != JobState.RECONCILIATION_REQUIRED:
|
||||||
|
raise ContinuationExecutionError("JOB_NOT_RECONCILABLE")
|
||||||
|
request = json.loads(row["request_json"])["binding"]
|
||||||
|
receipt = json.loads(row["receipt_json"])
|
||||||
|
new_oid = receipt["desired_remote_oid"]
|
||||||
|
authority_oid = self.authority.branch_head(row["branch"])
|
||||||
|
try:
|
||||||
|
generation = int(request.get("generation") or 0)
|
||||||
|
except (TypeError, ValueError) as exc:
|
||||||
|
raise ContinuationExecutionError("DURABLE_BINDING_GENERATION_INVALID") from exc
|
||||||
|
if generation > 0:
|
||||||
|
with self.controller.store._connect() as connection:
|
||||||
|
earlier = connection.execute(
|
||||||
|
"SELECT 1 FROM jobs WHERE branch=? AND state=? "
|
||||||
|
"AND CAST(json_extract(request_json, '$.binding.generation') AS INTEGER) < ? LIMIT 1",
|
||||||
|
(row["branch"], JobState.RECONCILIATION_REQUIRED.value, generation),
|
||||||
|
).fetchone()
|
||||||
|
if earlier is not None:
|
||||||
|
raise ContinuationExecutionError("EARLIER_RECONCILIATION_REQUIRED")
|
||||||
|
self.credential.project()
|
||||||
|
try:
|
||||||
|
git_oid, api_oid, visible = self._observe_remote(row["branch"], new_oid)
|
||||||
|
if authority_oid == git_oid == api_oid == new_oid and visible:
|
||||||
|
self.controller.advance(job_id, JobState.RECONCILING, side_effect_state="OBSERVATION_FIRST")
|
||||||
|
return self._finish_remote_verification(job_id, new_oid, new_oid)
|
||||||
|
expected_oid = request["expected_branch_oid"]
|
||||||
|
if (
|
||||||
|
self.authority.is_ancestor(new_oid, authority_oid)
|
||||||
|
and git_oid is not None
|
||||||
|
and git_oid == api_oid
|
||||||
|
and self.authority.is_ancestor(git_oid, new_oid)
|
||||||
|
):
|
||||||
|
self.controller.advance(job_id, JobState.RECONCILING, side_effect_state="OBSERVATION_FIRST_CHAIN")
|
||||||
|
self.remote.push_exact(row["branch"], git_oid, new_oid)
|
||||||
|
return self._finish_remote_verification(job_id, new_oid, new_oid)
|
||||||
|
if authority_oid == new_oid and git_oid == api_oid == expected_oid:
|
||||||
|
self.controller.advance(job_id, JobState.RECONCILING, side_effect_state="OBSERVATION_FIRST_RETRY")
|
||||||
|
self.remote.push_exact(row["branch"], expected_oid, new_oid)
|
||||||
|
return self._finish_remote_verification(job_id, new_oid, new_oid)
|
||||||
|
return self.controller.store._receipt(row, duplicate=True).as_dict()
|
||||||
|
finally:
|
||||||
|
self.credential.revoke()
|
||||||
@@ -49,6 +49,7 @@ class WriterLeaseRequestV2:
|
|||||||
target_branch: str
|
target_branch: str
|
||||||
generation: int
|
generation: int
|
||||||
lease_seconds: int = 900
|
lease_seconds: int = 900
|
||||||
|
job_id: str = ""
|
||||||
|
|
||||||
schema = "aios.task-writer-lease.v2"
|
schema = "aios.task-writer-lease.v2"
|
||||||
accepted_schemas = frozenset({"aios.task-writer-lease.v2", "aios.task-writer-lease-request.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:
|
def validate(self, probe: BranchProbe) -> None:
|
||||||
if not self.task_id or self.repository != "aios/aios":
|
if not self.task_id or self.repository != "aios/aios":
|
||||||
raise ContinuationError("request_identity_invalid")
|
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"}:
|
if self.branch_mode not in {"CREATE_NEW", "CONTINUE_EXISTING"}:
|
||||||
raise ContinuationError("branch_mode_invalid")
|
raise ContinuationError("branch_mode_invalid")
|
||||||
_oid(self.base_oid, "base_oid")
|
_oid(self.base_oid, "base_oid")
|
||||||
@@ -74,6 +77,12 @@ class WriterLeaseRequestV2:
|
|||||||
raise ContinuationError("EXISTING_BRANCH_OID_CONFLICT")
|
raise ContinuationError("EXISTING_BRANCH_OID_CONFLICT")
|
||||||
if not probe.is_ancestor(self.base_oid, actual):
|
if not probe.is_ancestor(self.base_oid, actual):
|
||||||
raise ContinuationError("BASE_NOT_ANCESTOR_OF_BRANCH")
|
raise ContinuationError("BASE_NOT_ANCESTOR_OF_BRANCH")
|
||||||
|
elif hasattr(probe, "commit_exists") and not probe.commit_exists(self.base_oid):
|
||||||
|
raise ContinuationError("BASE_COMMIT_NOT_FOUND")
|
||||||
|
elif self.branch_mode == "CREATE_NEW" and probe.branch_exists(self.target_branch):
|
||||||
|
raise ContinuationError("TARGET_BRANCH_ALREADY_EXISTS")
|
||||||
|
elif self.branch_mode == "CREATE_NEW" and self.base_oid != self.expected_branch_oid:
|
||||||
|
raise ContinuationError("CREATE_NEW_BASE_EXPECTATION_MISMATCH")
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True, slots=True)
|
@dataclass(frozen=True, slots=True)
|
||||||
|
|||||||
@@ -0,0 +1,26 @@
|
|||||||
|
"""Non-sensitive control-plane snapshot assembly."""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import Any, Mapping
|
||||||
|
|
||||||
|
|
||||||
|
def _copy(value: Mapping[str, Any] | None, keys: tuple[str, ...]) -> dict[str, Any]:
|
||||||
|
value = value or {}
|
||||||
|
return {key: value.get(key) for key in keys}
|
||||||
|
|
||||||
|
|
||||||
|
def build_control_plane_snapshot(*, target_version: str, runtime: Mapping[str, Any] | None = None, repository: Mapping[str, Any] | None = None, production: Mapping[str, Any] | None = None, task: Mapping[str, Any] | None = None, capabilities: Mapping[str, Any] | None = None, credential_broker: Mapping[str, Any] | None = None) -> dict[str, Any]:
|
||||||
|
return {
|
||||||
|
"version": {"target_release": target_version},
|
||||||
|
"runtime": _copy(runtime, ("bridge_schema_version", "control_plane_release_version", "runtime_release_commit", "runtime_path", "artifact_sha256", "drift_status")),
|
||||||
|
"repository": _copy(repository, ("identity", "authority_git_dir", "authority_main_oid")),
|
||||||
|
"production": _copy(production, ("baseline_version", "target_oid", "deployment_state")),
|
||||||
|
"task": _copy(task, ("task_id", "branch", "authority_branch_oid", "gitea_git_oid", "gitea_api_oid", "commit_visible", "generation", "reconciliation_state")),
|
||||||
|
"capabilities": _copy(capabilities, ("legacy_writes_enabled", "governed_writes_enabled", "force_push", "main_write", "tag_write", "baseline_write", "production_write")),
|
||||||
|
"credential_broker": _copy(credential_broker, ("status", "provider")),
|
||||||
|
"secret_material_exposed": False,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def getControlPlaneSnapshot(**kwargs: Any) -> dict[str, Any]:
|
||||||
|
return build_control_plane_snapshot(**kwargs)
|
||||||
@@ -0,0 +1,78 @@
|
|||||||
|
"""Metadata-only, server-side credential lease broker."""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from datetime import datetime, timedelta, timezone
|
||||||
|
import os
|
||||||
|
from pathlib import Path
|
||||||
|
import secrets
|
||||||
|
from typing import Any, Callable, Mapping
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True, slots=True)
|
||||||
|
class CredentialLease:
|
||||||
|
lease_id: str
|
||||||
|
task_id: str
|
||||||
|
repository: str
|
||||||
|
target_branch: str
|
||||||
|
generation: int
|
||||||
|
issued_at: str
|
||||||
|
expires_at: str
|
||||||
|
scopes: tuple[str, ...]
|
||||||
|
provider: str
|
||||||
|
state: str = "ACTIVE"
|
||||||
|
|
||||||
|
def as_dict(self) -> dict[str, Any]:
|
||||||
|
return {"lease_id": self.lease_id, "task_id": self.task_id, "repository": self.repository, "target_branch": self.target_branch, "generation": self.generation, "issued_at": self.issued_at, "expires_at": self.expires_at, "scopes": list(self.scopes), "provider": self.provider, "state": self.state}
|
||||||
|
|
||||||
|
|
||||||
|
class FileCredentialProvider:
|
||||||
|
def __init__(self, token_path: str | os.PathLike[str], *, actor: str = "aios-autonomy-agent") -> None:
|
||||||
|
self.token_path = Path(token_path)
|
||||||
|
self.actor = actor
|
||||||
|
|
||||||
|
def environment(self, lease: CredentialLease) -> dict[str, str]:
|
||||||
|
if not self.token_path.is_file():
|
||||||
|
raise RuntimeError("CREDENTIAL_PROVIDER_UNAVAILABLE")
|
||||||
|
return {"CREDENTIAL_FILE": str(self.token_path), "GITEA_MACHINE_ACTOR": self.actor}
|
||||||
|
|
||||||
|
|
||||||
|
class CredentialBroker:
|
||||||
|
def __init__(self, provider: Any, *, now: Callable[[], datetime] | None = None) -> None:
|
||||||
|
self.provider = provider
|
||||||
|
self.now = now or (lambda: datetime.now(timezone.utc))
|
||||||
|
self._leases: dict[str, CredentialLease] = {}
|
||||||
|
|
||||||
|
def issue(self, *, task_id: str, repository: str, target_branch: str, generation: int, scopes: tuple[str, ...] = ("git:push", "gitea:read"), lease_seconds: int = 900) -> dict[str, Any]:
|
||||||
|
if repository != "aios/aios" or not target_branch.startswith("agent/") or generation < 1 or not 1 <= lease_seconds <= 86400:
|
||||||
|
raise ValueError("CREDENTIAL_LEASE_BINDING_INVALID")
|
||||||
|
now = self.now().astimezone(timezone.utc)
|
||||||
|
lease = CredentialLease("lease-" + secrets.token_hex(16), task_id, repository, target_branch, generation, now.isoformat(), (now + timedelta(seconds=lease_seconds)).isoformat(), tuple(scopes), type(self.provider).__name__)
|
||||||
|
self._leases[lease.lease_id] = lease
|
||||||
|
return lease.as_dict()
|
||||||
|
|
||||||
|
def _get(self, lease_id: str) -> CredentialLease:
|
||||||
|
lease = self._leases.get(lease_id)
|
||||||
|
if lease is None:
|
||||||
|
raise KeyError("CREDENTIAL_LEASE_NOT_FOUND")
|
||||||
|
if lease.state == "ACTIVE" and self.now().astimezone(timezone.utc) >= datetime.fromisoformat(lease.expires_at):
|
||||||
|
lease = CredentialLease(**{**lease.as_dict(), "scopes": tuple(lease.scopes), "state": "EXPIRED"})
|
||||||
|
self._leases[lease_id] = lease
|
||||||
|
return lease
|
||||||
|
|
||||||
|
def environment(self, lease_id: str) -> Mapping[str, str]:
|
||||||
|
lease = self._get(lease_id)
|
||||||
|
if lease.state != "ACTIVE":
|
||||||
|
raise RuntimeError("CREDENTIAL_LEASE_NOT_ACTIVE")
|
||||||
|
return self.provider.environment(lease)
|
||||||
|
|
||||||
|
def status(self, lease_id: str) -> dict[str, Any]:
|
||||||
|
return self._get(lease_id).as_dict()
|
||||||
|
|
||||||
|
def revoke(self, lease_id: str) -> dict[str, Any]:
|
||||||
|
lease = self._get(lease_id)
|
||||||
|
if lease.state == "REVOKED":
|
||||||
|
return lease.as_dict()
|
||||||
|
updated = CredentialLease(**{**lease.as_dict(), "scopes": tuple(lease.scopes), "state": "REVOKED"})
|
||||||
|
self._leases[lease_id] = updated
|
||||||
|
return updated.as_dict()
|
||||||
@@ -0,0 +1,93 @@
|
|||||||
|
"""Canonical Gitea transport and slash-branch verification."""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from dataclasses import dataclass
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
from pathlib import Path
|
||||||
|
import subprocess
|
||||||
|
from typing import Any, Callable, Mapping, Optional
|
||||||
|
from urllib.parse import quote
|
||||||
|
from urllib.request import Request, urlopen
|
||||||
|
|
||||||
|
from aios_gitops_reconciliation_core import RemoteObservation
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True, slots=True)
|
||||||
|
class GiteaPublisherConfig:
|
||||||
|
repository: str = "aios/aios"
|
||||||
|
api_base: str = "https://g.cnnetmkt.top/git/api/v1"
|
||||||
|
remote_name: str = "gitea-http"
|
||||||
|
|
||||||
|
|
||||||
|
class GiteaPublisher:
|
||||||
|
def __init__(self, workspace: Path, *, config: GiteaPublisherConfig = GiteaPublisherConfig(), credential: Any = None, api_get: Callable[[str], Any] | None = None, git_ref: Callable[[str], Optional[str]] | None = None) -> None:
|
||||||
|
self.workspace = Path(workspace)
|
||||||
|
self.config = config
|
||||||
|
self.credential = credential
|
||||||
|
self._api_get = api_get
|
||||||
|
self._git_ref = git_ref
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def validate_repository(repository: str) -> None:
|
||||||
|
if repository != "aios/aios":
|
||||||
|
raise ValueError("REPOSITORY_IDENTITY_MISMATCH")
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def validate_branch(branch: str) -> None:
|
||||||
|
if not isinstance(branch, str) or not branch.startswith("agent/") or branch in {"main", "master"}:
|
||||||
|
raise ValueError("BRANCH_SCOPE_DENIED")
|
||||||
|
|
||||||
|
def repository_identity(self) -> str:
|
||||||
|
self.validate_repository(self.config.repository)
|
||||||
|
return self.config.repository
|
||||||
|
|
||||||
|
def _environment(self) -> dict[str, str]:
|
||||||
|
if self.credential is not None and hasattr(self.credential, "environment"):
|
||||||
|
return dict(self.credential.environment())
|
||||||
|
return {}
|
||||||
|
|
||||||
|
def read_git_ref(self, branch: str) -> Optional[str]:
|
||||||
|
self.validate_branch(branch)
|
||||||
|
if self._git_ref is not None:
|
||||||
|
return self._git_ref(branch)
|
||||||
|
result = subprocess.run(["git", "-C", str(self.workspace), "ls-remote", self.config.remote_name, f"refs/heads/{branch}"], text=True, capture_output=True, check=False, env={**os.environ, **self._environment(), "GIT_TERMINAL_PROMPT": "0"})
|
||||||
|
if result.returncode != 0 or not result.stdout.strip():
|
||||||
|
return None
|
||||||
|
return result.stdout.split()[0]
|
||||||
|
|
||||||
|
def _api(self, path: str) -> Any:
|
||||||
|
if self._api_get is not None:
|
||||||
|
return self._api_get(path)
|
||||||
|
headers = {"Accept": "application/json"}
|
||||||
|
env = self._environment()
|
||||||
|
if env.get("AIOS_GITEA_TOKEN"):
|
||||||
|
headers["Authorization"] = "token " + env["AIOS_GITEA_TOKEN"]
|
||||||
|
with urlopen(Request(self.config.api_base.rstrip("/") + path, headers=headers), timeout=20) as response:
|
||||||
|
return json.loads(response.read() or b"{}")
|
||||||
|
|
||||||
|
def read_api_branch(self, branch: str) -> Optional[str]:
|
||||||
|
self.validate_branch(branch)
|
||||||
|
for page in range(1, 101):
|
||||||
|
value = self._api(f"/repos/aios/aios/branches?page={page}&limit=50")
|
||||||
|
if not isinstance(value, list):
|
||||||
|
return None
|
||||||
|
for item in value:
|
||||||
|
if isinstance(item, Mapping) and item.get("name") == branch:
|
||||||
|
commit = item.get("commit")
|
||||||
|
return commit.get("id") if isinstance(commit, Mapping) else None
|
||||||
|
if len(value) < 50:
|
||||||
|
return None
|
||||||
|
raise RuntimeError("GITEA_BRANCH_PAGINATION_LIMIT")
|
||||||
|
|
||||||
|
def commit_visible(self, oid: str) -> bool:
|
||||||
|
if not isinstance(oid, str) or len(oid) != 40:
|
||||||
|
return False
|
||||||
|
try:
|
||||||
|
value = self._api(f"/repos/aios/aios/git/commits/{quote(oid, safe='')}")
|
||||||
|
return isinstance(value, Mapping) and value.get("sha", oid) == oid
|
||||||
|
except Exception:
|
||||||
|
return False
|
||||||
|
|
||||||
|
def observe(self, branch: str, desired_oid: str, authority_oid: Optional[str], *, expected_oid: Optional[str] = None, observed_at: str = "") -> RemoteObservation:
|
||||||
|
return RemoteObservation(self.repository_identity(), branch, desired_oid, authority_oid, self.read_git_ref(branch), self.read_api_branch(branch), self.commit_visible(desired_oid), observed_at, expected_oid)
|
||||||
@@ -27,6 +27,19 @@ class ServiceUnavailable(JobError):
|
|||||||
pass
|
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):
|
class BindingConflict(JobError):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@@ -66,6 +79,14 @@ def utc_now() -> str:
|
|||||||
return datetime.now(timezone.utc).isoformat().replace("+00:00", "Z")
|
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:
|
def canonical_json(value: Any) -> bytes:
|
||||||
return (
|
return (
|
||||||
json.dumps(value, sort_keys=True, separators=(",", ":"), ensure_ascii=False) + "\n"
|
json.dumps(value, sort_keys=True, separators=(",", ":"), ensure_ascii=False) + "\n"
|
||||||
@@ -249,7 +270,8 @@ class JobStore:
|
|||||||
if state == JobState.COMPLETED:
|
if state == JobState.COMPLETED:
|
||||||
status, side = 200, "COMPLETED"
|
status, side = 200, "COMPLETED"
|
||||||
elif state in _RECOVERABLE:
|
elif state in _RECOVERABLE:
|
||||||
status, side = 202, "RESUME_SAME_JOB"
|
status = 202
|
||||||
|
side = row["side_effect_state"] if row["side_effect_state"] == "REMOTE_PUSH_PENDING" else "RESUME_SAME_JOB"
|
||||||
elif row["side_effect_state"] == "UNKNOWN":
|
elif row["side_effect_state"] == "UNKNOWN":
|
||||||
status, side = 503, "UNKNOWN"
|
status, side = 503, "UNKNOWN"
|
||||||
else:
|
else:
|
||||||
@@ -280,10 +302,13 @@ class GitOpsJobController:
|
|||||||
# durable round trip and participate in idempotency. Arbitrary request
|
# durable round trip and participate in idempotency. Arbitrary request
|
||||||
# fields are intentionally discarded rather than persisted.
|
# fields are intentionally discarded rather than persisted.
|
||||||
optional = (
|
optional = (
|
||||||
|
"idempotency_key",
|
||||||
"task_id",
|
"task_id",
|
||||||
"new_branch_oid",
|
"new_branch_oid",
|
||||||
"authorization_recovery_digest",
|
"authorization_recovery_digest",
|
||||||
"lineage_anchor_oid",
|
"lineage_anchor_oid",
|
||||||
|
"base_oid",
|
||||||
|
"branch_mode",
|
||||||
"generation",
|
"generation",
|
||||||
"expected_main_oid",
|
"expected_main_oid",
|
||||||
"pr_number",
|
"pr_number",
|
||||||
@@ -314,6 +339,14 @@ class GitOpsJobController:
|
|||||||
raise JobError("PAYLOAD_REFERENCE_MISMATCH")
|
raise JobError("PAYLOAD_REFERENCE_MISMATCH")
|
||||||
if payload_ref is not None and (not isinstance(payload_size, int) or payload_size < 0):
|
if payload_ref is not None and (not isinstance(payload_size, int) or payload_size < 0):
|
||||||
raise JobError("PAYLOAD_SIZE_INVALID")
|
raise JobError("PAYLOAD_SIZE_INVALID")
|
||||||
|
client_idempotency_key = binding.get("idempotency_key")
|
||||||
|
if client_idempotency_key is not None:
|
||||||
|
if len(client_idempotency_key) > 128 or not all(
|
||||||
|
character.isalnum() or character in "._:-" for character in client_idempotency_key
|
||||||
|
):
|
||||||
|
raise JobError("IDEMPOTENCY_KEY_INVALID")
|
||||||
|
key = sha256_json({"client_idempotency_key": client_idempotency_key})
|
||||||
|
else:
|
||||||
key = sha256_json(binding)
|
key = sha256_json(binding)
|
||||||
job_id = "job-" + key[:32]
|
job_id = "job-" + key[:32]
|
||||||
durable_request = dict(binding)
|
durable_request = dict(binding)
|
||||||
@@ -392,13 +425,16 @@ class GitOpsJobController:
|
|||||||
self.store._mirror(result)
|
self.store._mirror(result)
|
||||||
return self.store._receipt(result, duplicate=True)
|
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:
|
if len(current_head_oid) != 40 or generation < 1:
|
||||||
raise JobError("LEASE_BINDING_INVALID")
|
raise JobError("LEASE_BINDING_INVALID")
|
||||||
try:
|
try:
|
||||||
with self.store._connect() as connection:
|
with self.store._connect() as connection:
|
||||||
connection.execute("BEGIN IMMEDIATE")
|
connection.execute("BEGIN IMMEDIATE")
|
||||||
|
if lease_expires_at is None:
|
||||||
updated = connection.execute("UPDATE branch_leases SET current_head_oid=?,generation=?,state=?,revision=revision+1 WHERE lease_id=? AND revision=?", (current_head_oid, generation, state, lease_id, expected_revision))
|
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:
|
if updated.rowcount != 1:
|
||||||
raise JobError("LEASE_REVISION_CAS_CONFLICT")
|
raise JobError("LEASE_REVISION_CAS_CONFLICT")
|
||||||
connection.commit()
|
connection.commit()
|
||||||
@@ -407,6 +443,94 @@ class GitOpsJobController:
|
|||||||
except sqlite3.Error as exc:
|
except sqlite3.Error as exc:
|
||||||
raise ServiceUnavailable("JOB_DB_UNAVAILABLE") from 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:
|
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(
|
return self._advance(
|
||||||
job_id,
|
job_id,
|
||||||
|
|||||||
@@ -0,0 +1,80 @@
|
|||||||
|
"""Pure, observation-first decisions for governed GitOps reconciliation."""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from enum import Enum
|
||||||
|
import re
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
OID_RE = re.compile(r"^[0-9a-f]{40}$")
|
||||||
|
|
||||||
|
|
||||||
|
class ReconciliationAction(str, Enum):
|
||||||
|
NOOP = "NOOP"
|
||||||
|
PUSH_EXACT = "PUSH_EXACT"
|
||||||
|
VERIFY_REMOTE = "VERIFY_REMOTE"
|
||||||
|
MARK_REMOTE_VERIFIED = "MARK_REMOTE_VERIFIED"
|
||||||
|
MARK_RECONCILIATION_REQUIRED = "MARK_RECONCILIATION_REQUIRED"
|
||||||
|
FAIL_CONFLICT = "FAIL_CONFLICT"
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True, slots=True)
|
||||||
|
class RemoteObservation:
|
||||||
|
repository_identity: str
|
||||||
|
branch: str
|
||||||
|
desired_oid: str
|
||||||
|
authority_oid: Optional[str]
|
||||||
|
remote_git_oid: Optional[str]
|
||||||
|
remote_api_oid: Optional[str]
|
||||||
|
commit_visible: bool
|
||||||
|
observed_at: str
|
||||||
|
expected_oid: Optional[str] = None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def all_equal(self) -> bool:
|
||||||
|
values = (self.authority_oid, self.remote_git_oid, self.remote_api_oid, self.desired_oid)
|
||||||
|
return all(isinstance(value, str) for value in values) and len(set(values)) == 1
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True, slots=True)
|
||||||
|
class ReconciliationDecision:
|
||||||
|
action: ReconciliationAction
|
||||||
|
state: str
|
||||||
|
reason: str
|
||||||
|
retry_safe: bool = False
|
||||||
|
|
||||||
|
|
||||||
|
def _valid_oid(value: Optional[str]) -> bool:
|
||||||
|
return value is None or bool(OID_RE.fullmatch(value))
|
||||||
|
|
||||||
|
|
||||||
|
def decide_reconciliation(observation: RemoteObservation) -> ReconciliationDecision:
|
||||||
|
"""Decide from a fresh observation; never complete on a missing remote."""
|
||||||
|
if observation.repository_identity != "aios/aios":
|
||||||
|
return ReconciliationDecision(ReconciliationAction.FAIL_CONFLICT, "CONFLICT", "REPOSITORY_IDENTITY_MISMATCH")
|
||||||
|
if not observation.branch.startswith("agent/"):
|
||||||
|
return ReconciliationDecision(ReconciliationAction.FAIL_CONFLICT, "CONFLICT", "BRANCH_SCOPE_DENIED")
|
||||||
|
if not _valid_oid(observation.desired_oid) or not _valid_oid(observation.expected_oid):
|
||||||
|
return ReconciliationDecision(ReconciliationAction.FAIL_CONFLICT, "CONFLICT", "OID_INVALID")
|
||||||
|
if observation.authority_oid is None:
|
||||||
|
return ReconciliationDecision(ReconciliationAction.MARK_RECONCILIATION_REQUIRED, "RECONCILIATION_REQUIRED", "AUTHORITY_UNAVAILABLE")
|
||||||
|
if observation.authority_oid != observation.desired_oid:
|
||||||
|
return ReconciliationDecision(ReconciliationAction.MARK_RECONCILIATION_REQUIRED, "RECONCILIATION_REQUIRED", "AUTHORITY_CONFLICT")
|
||||||
|
present = [oid for oid in (observation.remote_git_oid, observation.remote_api_oid) if oid is not None]
|
||||||
|
if any(oid not in {observation.desired_oid, observation.expected_oid} for oid in present):
|
||||||
|
return ReconciliationDecision(ReconciliationAction.FAIL_CONFLICT, "CONFLICT", "REMOTE_OID_CONFLICT")
|
||||||
|
if observation.all_equal and observation.commit_visible:
|
||||||
|
return ReconciliationDecision(ReconciliationAction.MARK_REMOTE_VERIFIED, "REMOTE_VERIFIED", "ALL_OIDS_EQUAL_AND_VISIBLE")
|
||||||
|
if observation.remote_git_oid is None and observation.remote_api_oid is None:
|
||||||
|
return ReconciliationDecision(ReconciliationAction.MARK_RECONCILIATION_REQUIRED, "REMOTE_PUSH_PENDING", "AUTHORITY_DESIRED_REMOTE_MISSING")
|
||||||
|
if observation.remote_git_oid == observation.desired_oid or observation.remote_api_oid == observation.desired_oid:
|
||||||
|
return ReconciliationDecision(ReconciliationAction.VERIFY_REMOTE, "REMOTE_VERIFY_PENDING", "REMOTE_PROOF_PARTIAL")
|
||||||
|
if observation.expected_oid is not None and observation.remote_git_oid == observation.expected_oid and observation.remote_api_oid == observation.expected_oid:
|
||||||
|
return ReconciliationDecision(ReconciliationAction.PUSH_EXACT, "REMOTE_PUSH_PENDING", "SAFE_EXACT_FAST_FORWARD_RETRY", retry_safe=True)
|
||||||
|
if not observation.commit_visible:
|
||||||
|
return ReconciliationDecision(ReconciliationAction.VERIFY_REMOTE, "COMMIT_VISIBILITY_PENDING", "COMMIT_VISIBILITY_LAG")
|
||||||
|
return ReconciliationDecision(ReconciliationAction.MARK_RECONCILIATION_REQUIRED, "RECONCILIATION_REQUIRED", "PARTIAL_REMOTE_OBSERVATION")
|
||||||
|
|
||||||
|
|
||||||
|
def reconcile_observation(**kwargs: object) -> ReconciliationDecision:
|
||||||
|
return decide_reconciliation(RemoteObservation(**kwargs))
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
"""Central fail-closed policy seam for governed task and continuation requests."""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from dataclasses import dataclass
|
||||||
|
import re
|
||||||
|
from typing import Any, Mapping
|
||||||
|
|
||||||
|
OID_RE = re.compile(r"^[0-9a-f]{40}$")
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True, slots=True)
|
||||||
|
class GovernedPolicyInput:
|
||||||
|
identity: str
|
||||||
|
task_id: str
|
||||||
|
repository: str
|
||||||
|
operation: str
|
||||||
|
base_oid: str
|
||||||
|
expected_branch_oid: str
|
||||||
|
target_branch: str
|
||||||
|
generation: int
|
||||||
|
risk: str = "engineering"
|
||||||
|
requested_capabilities: tuple[str, ...] = ()
|
||||||
|
previous_generation: int | None = None
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True, slots=True)
|
||||||
|
class PolicyDecision:
|
||||||
|
decision: str
|
||||||
|
reason: str
|
||||||
|
|
||||||
|
def as_dict(self) -> dict[str, str]:
|
||||||
|
return {"decision": self.decision, "reason": self.reason}
|
||||||
|
|
||||||
|
|
||||||
|
def evaluate_policy(value: GovernedPolicyInput | Mapping[str, Any]) -> PolicyDecision:
|
||||||
|
if isinstance(value, Mapping):
|
||||||
|
value = GovernedPolicyInput(**value)
|
||||||
|
if value.repository != "aios/aios":
|
||||||
|
return PolicyDecision("DENY", "REPOSITORY_IDENTITY_MISMATCH")
|
||||||
|
if not OID_RE.fullmatch(value.base_oid) or not OID_RE.fullmatch(value.expected_branch_oid):
|
||||||
|
return PolicyDecision("DENY", "OID_INVALID")
|
||||||
|
if not value.target_branch.startswith("agent/") or value.target_branch in {"main", "master"}:
|
||||||
|
return PolicyDecision("DENY", "BRANCH_SCOPE_DENIED")
|
||||||
|
if "force_push" in value.requested_capabilities or "force" in value.operation.lower():
|
||||||
|
return PolicyDecision("DENY", "FORCE_PUSH_DENIED")
|
||||||
|
if {"main_write", "tag_write", "baseline_write", "production_write", "production_deploy", "tag_mutation", "baseline_mutation"}.intersection(value.requested_capabilities):
|
||||||
|
return PolicyDecision("DENY", "MUTATION_SCOPE_DENIED")
|
||||||
|
if not isinstance(value.generation, int) or isinstance(value.generation, bool) or value.generation < 1:
|
||||||
|
return PolicyDecision("DENY", "GENERATION_INVALID")
|
||||||
|
if value.previous_generation is not None and value.generation <= value.previous_generation:
|
||||||
|
return PolicyDecision("MANUAL_OWNER_REVIEW_REQUIRED", "GENERATION_NOT_MONOTONIC")
|
||||||
|
if not value.task_id:
|
||||||
|
return PolicyDecision("DENY", "TASK_ID_INVALID")
|
||||||
|
return PolicyDecision("ALLOW", "GOVERNED_ENGINEERING_SCOPE")
|
||||||
|
|
||||||
|
|
||||||
|
class GovernedPolicy:
|
||||||
|
def decide(self, request: GovernedPolicyInput | Mapping[str, Any]) -> PolicyDecision:
|
||||||
|
return evaluate_policy(request)
|
||||||
@@ -0,0 +1,84 @@
|
|||||||
|
"""Durable governed task binding for CREATE_NEW and CONTINUE_EXISTING."""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from dataclasses import dataclass, asdict
|
||||||
|
import hashlib
|
||||||
|
import json
|
||||||
|
from pathlib import Path
|
||||||
|
import re
|
||||||
|
from typing import Any, Mapping
|
||||||
|
|
||||||
|
from aios_gitops_job_controller import GitOpsJobController, JobStore
|
||||||
|
|
||||||
|
OID_RE = re.compile(r"^[0-9a-f]{40}$")
|
||||||
|
BRANCH_RE = re.compile(r"^agent/[A-Za-z0-9][A-Za-z0-9._/-]{2,179}$")
|
||||||
|
|
||||||
|
|
||||||
|
class BootstrapError(RuntimeError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True, slots=True)
|
||||||
|
class GovernedTaskBinding:
|
||||||
|
task_id: str
|
||||||
|
repository: str
|
||||||
|
base_oid: str
|
||||||
|
target_branch: str
|
||||||
|
risk: str
|
||||||
|
description: str
|
||||||
|
idempotency_key: str
|
||||||
|
expected_branch_oid: str
|
||||||
|
branch_mode: str
|
||||||
|
generation: int
|
||||||
|
job_id: str | None = None
|
||||||
|
|
||||||
|
def response(self, *, idempotent: bool = False) -> dict[str, Any]:
|
||||||
|
return {"task_id": self.task_id, "repository": self.repository, "base_oid": self.base_oid, "target_branch": self.target_branch, "expected_branch_oid": self.expected_branch_oid, "generation": self.generation, "branch_mode": self.branch_mode, "create_target_branch": self.branch_mode == "CREATE_NEW", "worktree_mode": "detached_at_base_oid" if self.branch_mode == "CREATE_NEW" else "detached_at_expected_head", "cas_ref_update_required": True, "idempotent": idempotent, "state": "BOUND", "job_id": self.job_id}
|
||||||
|
|
||||||
|
|
||||||
|
def _safe(value: str) -> str:
|
||||||
|
return re.sub(r"[^A-Za-z0-9_.-]+", "-", value).strip("-")[:120] or "task"
|
||||||
|
|
||||||
|
|
||||||
|
class GovernedTaskBootstrap:
|
||||||
|
def __init__(self, authority: Any, root: Path, *, remote_name: str, token_path: Path, ssh_key_path: Path, api_base: str = "https://g.cnnetmkt.top/git/api/v1") -> None:
|
||||||
|
self.authority = authority; self.root = Path(root); self.remote_name = remote_name; self.token_path = Path(token_path); self.ssh_key_path = Path(ssh_key_path); self.api_base = api_base; self.task_meta_root = self.root / "task-bindings"; self.job_root = self.root / "jobs"; self.task_meta_root.mkdir(parents=True, exist_ok=True); self.job_root.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
def _store(self, task_id: str) -> tuple[JobStore, GitOpsJobController]:
|
||||||
|
root = self.job_root / _safe(task_id); store = JobStore(root, payload_root=root / "payloads"); return store, GitOpsJobController(store)
|
||||||
|
|
||||||
|
def _metadata(self, task_id: str) -> Path:
|
||||||
|
return self.task_meta_root / (_safe(task_id) + ".json")
|
||||||
|
|
||||||
|
def _read_existing(self, task_id: str) -> GovernedTaskBinding | None:
|
||||||
|
path = self._metadata(task_id)
|
||||||
|
if not path.is_file() or path.is_symlink(): return None
|
||||||
|
return GovernedTaskBinding(**json.loads(path.read_text(encoding="utf-8")))
|
||||||
|
|
||||||
|
def _write(self, binding: GovernedTaskBinding) -> None:
|
||||||
|
path = self._metadata(binding.task_id); temporary = path.with_suffix(".tmp"); temporary.write_text(json.dumps(asdict(binding), sort_keys=True), encoding="utf-8"); temporary.replace(path)
|
||||||
|
|
||||||
|
def create(self, request: Mapping[str, Any]) -> dict[str, Any]:
|
||||||
|
values = {key: str(request[key]) for key in ("task_id", "repository", "base_oid", "target_branch", "risk", "description", "idempotency_key")}
|
||||||
|
if values["repository"] != "aios/aios" or not OID_RE.fullmatch(values["base_oid"]): raise BootstrapError("TASK_BINDING_INVALID")
|
||||||
|
if not BRANCH_RE.fullmatch(values["target_branch"]): raise BootstrapError("TARGET_BRANCH_INVALID")
|
||||||
|
existing = self._read_existing(values["task_id"])
|
||||||
|
if existing is not None:
|
||||||
|
if any(getattr(existing, key) != values[key] for key in ("repository", "base_oid", "target_branch", "risk", "description")): raise BootstrapError("TASK_BINDING_CONFLICT")
|
||||||
|
return existing.response(idempotent=True)
|
||||||
|
if not self.authority.commit_exists(values["base_oid"]): raise BootstrapError("BASE_COMMIT_NOT_FOUND")
|
||||||
|
current = self.authority.read_ref("refs/heads/" + values["target_branch"])
|
||||||
|
expected = str(request.get("expected_branch_oid") or values["base_oid"])
|
||||||
|
if current is None:
|
||||||
|
mode, generation = "CREATE_NEW", 1
|
||||||
|
if expected != values["base_oid"]: raise BootstrapError("EXPECTED_BASE_CONFLICT")
|
||||||
|
else:
|
||||||
|
if expected != current: raise BootstrapError("EXISTING_BRANCH_OID_CONFLICT")
|
||||||
|
mode, generation = "CONTINUE_EXISTING", int(request.get("generation", 2))
|
||||||
|
if generation < 1: raise BootstrapError("GENERATION_INVALID")
|
||||||
|
payload = json.dumps(values, sort_keys=True, separators=(",", ":")).encode()
|
||||||
|
_, controller = self._store(values["task_id"])
|
||||||
|
receipt = controller.submit({"idempotency_key": values["idempotency_key"], "campaign_id": values["task_id"], "task_id": values["task_id"], "repository": values["repository"], "operation": "GOVERNED_TASK_CREATE", "branch": values["target_branch"], "expected_branch_oid": expected, "payload_digest": hashlib.sha256(payload).hexdigest(), "lineage_anchor_oid": values["base_oid"]}, payload=payload)
|
||||||
|
binding = GovernedTaskBinding(**values, expected_branch_oid=expected, branch_mode=mode, generation=generation, job_id=receipt.job_id)
|
||||||
|
self._write(binding)
|
||||||
|
return binding.response()
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -964,6 +964,7 @@ def validate_writer_lease_request_v2(raw: Mapping[str, Any], probe: Any) -> dict
|
|||||||
required = {
|
required = {
|
||||||
"schema",
|
"schema",
|
||||||
"task_id",
|
"task_id",
|
||||||
|
"job_id",
|
||||||
"repository",
|
"repository",
|
||||||
"branch_mode",
|
"branch_mode",
|
||||||
"base_oid",
|
"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)
|
raise ProvisioningError("LEASE_REQUEST_V2_FIELDS_REJECTED", 409)
|
||||||
request = WriterLeaseRequestV2(
|
request = WriterLeaseRequestV2(
|
||||||
task_id=str(raw["task_id"]),
|
task_id=str(raw["task_id"]),
|
||||||
|
job_id=str(raw["job_id"]),
|
||||||
repository=str(raw["repository"]),
|
repository=str(raw["repository"]),
|
||||||
branch_mode=str(raw["branch_mode"]),
|
branch_mode=str(raw["branch_mode"]),
|
||||||
base_oid=str(raw["base_oid"]),
|
base_oid=str(raw["base_oid"]),
|
||||||
@@ -988,6 +990,7 @@ def validate_writer_lease_request_v2(raw: Mapping[str, Any], probe: Any) -> dict
|
|||||||
return {
|
return {
|
||||||
"schema": WriterLeaseRequestV2.schema,
|
"schema": WriterLeaseRequestV2.schema,
|
||||||
"task_id": request.task_id,
|
"task_id": request.task_id,
|
||||||
|
"job_id": request.job_id,
|
||||||
"repository": request.repository,
|
"repository": request.repository,
|
||||||
"branch_mode": request.branch_mode,
|
"branch_mode": request.branch_mode,
|
||||||
"base_oid": request.base_oid,
|
"base_oid": request.base_oid,
|
||||||
@@ -995,7 +998,7 @@ def validate_writer_lease_request_v2(raw: Mapping[str, Any], probe: Any) -> dict
|
|||||||
"target_branch": request.target_branch,
|
"target_branch": request.target_branch,
|
||||||
"generation": request.generation,
|
"generation": request.generation,
|
||||||
"lease_seconds": request.lease_seconds,
|
"lease_seconds": request.lease_seconds,
|
||||||
"create_target_branch": False,
|
"create_target_branch": request.branch_mode == "CREATE_NEW",
|
||||||
"worktree_mode": "detached_at_expected_head",
|
"worktree_mode": "detached_at_base_oid" if request.branch_mode == "CREATE_NEW" else "detached_at_expected_head",
|
||||||
"cas_ref_update_required": True,
|
"cas_ref_update_required": True,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,84 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Single canonical Production Baseline authority for the GitOps reader.
|
||||||
|
|
||||||
|
The API and the one-shot writer import this module. Workspace and release
|
||||||
|
copies are projections only; they are never selected as a fallback source.
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import hashlib
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
|
||||||
|
AUTHORITY_PATH = Path(os.getenv(
|
||||||
|
"AIOS_GITOPS_BASELINE_AUTHORITY",
|
||||||
|
"/var/lib/aios-gitops-runtime-anchor/baseline/current.json",
|
||||||
|
))
|
||||||
|
AUTHORITY_SHA_PATH = AUTHORITY_PATH.with_suffix(".sha256")
|
||||||
|
|
||||||
|
|
||||||
|
class BaselineAuthorityError(RuntimeError):
|
||||||
|
"""Fail-closed error raised for an unavailable or inconsistent authority."""
|
||||||
|
|
||||||
|
|
||||||
|
class BaselineAuthority:
|
||||||
|
def __init__(self, path: Path | str = AUTHORITY_PATH):
|
||||||
|
self.path = Path(path)
|
||||||
|
self.sha_path = self.path.with_suffix(".sha256")
|
||||||
|
|
||||||
|
def _raw(self) -> bytes:
|
||||||
|
try:
|
||||||
|
raw = self.path.read_bytes()
|
||||||
|
except OSError as exc:
|
||||||
|
raise BaselineAuthorityError(f"authority_unavailable:{exc}") from exc
|
||||||
|
if not raw:
|
||||||
|
raise BaselineAuthorityError("authority_empty")
|
||||||
|
return raw
|
||||||
|
|
||||||
|
def read(self) -> dict[str, Any]:
|
||||||
|
raw = self._raw()
|
||||||
|
digest = hashlib.sha256(raw).hexdigest()
|
||||||
|
try:
|
||||||
|
value = json.loads(raw.decode("utf-8"))
|
||||||
|
except (UnicodeDecodeError, json.JSONDecodeError) as exc:
|
||||||
|
raise BaselineAuthorityError("authority_invalid_json") from exc
|
||||||
|
if not isinstance(value, dict):
|
||||||
|
raise BaselineAuthorityError("authority_not_object")
|
||||||
|
expected = str(value.get("authority_sha256", ""))
|
||||||
|
if expected and expected != digest:
|
||||||
|
raise BaselineAuthorityError("authority_embedded_digest_mismatch")
|
||||||
|
if self.sha_path.exists():
|
||||||
|
sidecar = self.sha_path.read_text(encoding="utf-8").strip().split()[0]
|
||||||
|
if sidecar != digest:
|
||||||
|
raise BaselineAuthorityError("authority_sidecar_digest_mismatch")
|
||||||
|
required = {
|
||||||
|
"current_baseline", "previous_baseline", "target_oid", "tag_oid",
|
||||||
|
"evidence_closure_oid", "approval_plan_id", "status", "generation",
|
||||||
|
"source_identity",
|
||||||
|
}
|
||||||
|
missing = sorted(required.difference(value))
|
||||||
|
if missing:
|
||||||
|
raise BaselineAuthorityError("authority_missing:" + ",".join(missing))
|
||||||
|
value = dict(value)
|
||||||
|
value["authority_sha256"] = digest
|
||||||
|
value["authority_path"] = str(self.path)
|
||||||
|
return value
|
||||||
|
|
||||||
|
def read_consistent(self, policy: dict[str, Any] | None = None) -> dict[str, Any]:
|
||||||
|
value = self.read()
|
||||||
|
if policy:
|
||||||
|
expected = policy.get("release_binding", policy)
|
||||||
|
for key, field in (("version", "current_baseline"),
|
||||||
|
("previous_version", "previous_baseline"),
|
||||||
|
("target_oid", "target_oid"),
|
||||||
|
("tag_oid", "tag_oid"),
|
||||||
|
("evidence_closure_oid", "evidence_closure_oid"),
|
||||||
|
("approval_plan_id", "approval_plan_id")):
|
||||||
|
wanted = expected.get(key)
|
||||||
|
if wanted is not None and value.get(field) != wanted:
|
||||||
|
raise BaselineAuthorityError(f"authority_binding_mismatch:{field}")
|
||||||
|
return value
|
||||||
|
|
||||||
@@ -0,0 +1,568 @@
|
|||||||
|
"""Governed V2R2C1 production release orchestration.
|
||||||
|
|
||||||
|
This module is deliberately scoped to the Owner-approved V2R2C1 binding. It
|
||||||
|
uses the existing TagPublisher and metadata adapters; it does not introduce a
|
||||||
|
second Git writer, a second durable job store, or an alternate baseline.
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import fcntl
|
||||||
|
import hashlib
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
import subprocess
|
||||||
|
import tarfile
|
||||||
|
import tempfile
|
||||||
|
import time
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Any, Mapping
|
||||||
|
|
||||||
|
from gitea_tag_publication import (
|
||||||
|
GiteaTagClient,
|
||||||
|
GitTagBackend,
|
||||||
|
PublicationManualOwnerReviewRequired,
|
||||||
|
PublicationPending,
|
||||||
|
PublicationReconciliationRequired,
|
||||||
|
PublicationRequest,
|
||||||
|
TagPublisher,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
VERSION = "V2R2C1"
|
||||||
|
TARGET_OID = "998d30304787e31e5ae607c0bd86b5edb2b0e5ad"
|
||||||
|
EVIDENCE_CLOSURE_OID = "a8b906d1bc24088c19647e98de6a679268f6d0a2"
|
||||||
|
APPROVAL_PLAN_ID = "sha256:c0df52008ae7f64cec5ee2d28947aa7b892066c670ab8295e4549140b1a7500e"
|
||||||
|
REPOSITORY = "aios/aios"
|
||||||
|
SERVICE = "aios-v2r1c7-production.service"
|
||||||
|
PORT = 18927
|
||||||
|
OID_RE = re.compile(r"^[0-9a-f]{40}$")
|
||||||
|
SHA_RE = re.compile(r"^[0-9a-f]{64}$")
|
||||||
|
|
||||||
|
|
||||||
|
class ReleaseBlocked(RuntimeError):
|
||||||
|
"""A release gate failed and no unsafe recovery is permitted."""
|
||||||
|
|
||||||
|
|
||||||
|
class ManualOwnerReviewRequired(ReleaseBlocked):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class ReconciliationRequired(ReleaseBlocked):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def canonical(value: object) -> bytes:
|
||||||
|
return (json.dumps(value, sort_keys=True, separators=(",", ":")) + "\n").encode()
|
||||||
|
|
||||||
|
|
||||||
|
def sha256_file(path: Path) -> str:
|
||||||
|
if path.is_symlink() or not path.is_file():
|
||||||
|
raise ReleaseBlocked("artifact must be a regular file")
|
||||||
|
digest = hashlib.sha256()
|
||||||
|
with path.open("rb") as stream:
|
||||||
|
for block in iter(lambda: stream.read(1024 * 1024), b""):
|
||||||
|
digest.update(block)
|
||||||
|
return digest.hexdigest()
|
||||||
|
|
||||||
|
|
||||||
|
def atomic_json(path: Path, value: Mapping[str, Any], mode: int = 0o600) -> None:
|
||||||
|
path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
temporary = path.with_name(path.name + ".tmp")
|
||||||
|
temporary.write_bytes(canonical(dict(value)))
|
||||||
|
os.chmod(temporary, mode)
|
||||||
|
os.replace(temporary, path)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class ReleaseBinding:
|
||||||
|
version: str = VERSION
|
||||||
|
target_oid: str = TARGET_OID
|
||||||
|
evidence_closure_oid: str = EVIDENCE_CLOSURE_OID
|
||||||
|
approval_plan_id: str = APPROVAL_PLAN_ID
|
||||||
|
repository: str = REPOSITORY
|
||||||
|
production_service: str = SERVICE
|
||||||
|
production_port: int = PORT
|
||||||
|
|
||||||
|
def validate(self) -> None:
|
||||||
|
if self.version != VERSION or self.repository != REPOSITORY:
|
||||||
|
raise ManualOwnerReviewRequired("release binding is outside V2R2C1 scope")
|
||||||
|
if not OID_RE.fullmatch(self.target_oid) or self.target_oid != TARGET_OID:
|
||||||
|
raise ManualOwnerReviewRequired("V2R2C1 target_oid mismatch")
|
||||||
|
if not OID_RE.fullmatch(self.evidence_closure_oid) or self.evidence_closure_oid != EVIDENCE_CLOSURE_OID:
|
||||||
|
raise ManualOwnerReviewRequired("V2R2C1 evidence closure mismatch")
|
||||||
|
if self.approval_plan_id != APPROVAL_PLAN_ID:
|
||||||
|
raise ManualOwnerReviewRequired("V2R2C1 approval plan mismatch")
|
||||||
|
if self.production_service != SERVICE or int(self.production_port) != PORT:
|
||||||
|
raise ManualOwnerReviewRequired("production service binding mismatch")
|
||||||
|
|
||||||
|
def as_dict(self) -> dict[str, Any]:
|
||||||
|
return {
|
||||||
|
"version": self.version,
|
||||||
|
"target_oid": self.target_oid,
|
||||||
|
"evidence_closure_oid": self.evidence_closure_oid,
|
||||||
|
"approval_plan_id": self.approval_plan_id,
|
||||||
|
"repository": self.repository,
|
||||||
|
"production_service": self.production_service,
|
||||||
|
"production_port": self.production_port,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class StateLock:
|
||||||
|
def __init__(self, path: Path):
|
||||||
|
self.path = path
|
||||||
|
self.handle = None
|
||||||
|
|
||||||
|
def __enter__(self) -> "StateLock":
|
||||||
|
self.path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
self.handle = self.path.open("a+", encoding="utf-8")
|
||||||
|
fcntl.flock(self.handle.fileno(), fcntl.LOCK_EX)
|
||||||
|
return self
|
||||||
|
|
||||||
|
def __exit__(self, exc_type, exc, tb) -> None:
|
||||||
|
if self.handle is not None:
|
||||||
|
fcntl.flock(self.handle.fileno(), fcntl.LOCK_UN)
|
||||||
|
self.handle.close()
|
||||||
|
|
||||||
|
|
||||||
|
class ReleaseState:
|
||||||
|
def __init__(self, root: Path, binding: ReleaseBinding):
|
||||||
|
self.root = root
|
||||||
|
self.path = root / "release-state.json"
|
||||||
|
self.binding = binding
|
||||||
|
root.mkdir(parents=True, exist_ok=True)
|
||||||
|
if self.path.exists():
|
||||||
|
value = json.loads(self.path.read_text(encoding="utf-8"))
|
||||||
|
if value.get("binding") != binding.as_dict():
|
||||||
|
raise ManualOwnerReviewRequired("persisted release binding conflict")
|
||||||
|
self.value = value
|
||||||
|
else:
|
||||||
|
self.value = {
|
||||||
|
"schema": "aios.v2r2c1.governed-production-release-state.v1",
|
||||||
|
"binding": binding.as_dict(),
|
||||||
|
"state": "INITIAL",
|
||||||
|
"completed": {},
|
||||||
|
"history": [],
|
||||||
|
}
|
||||||
|
self.save()
|
||||||
|
|
||||||
|
def save(self) -> None:
|
||||||
|
atomic_json(self.path, self.value)
|
||||||
|
|
||||||
|
def record(self, state: str, step: str, result: Mapping[str, Any]) -> None:
|
||||||
|
safe = json.loads(json.dumps(dict(result)))
|
||||||
|
self.value["state"] = state
|
||||||
|
self.value.setdefault("completed", {})[step] = safe
|
||||||
|
self.value.setdefault("history", []).append(
|
||||||
|
{"at": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()), "state": state, "step": step, "result": safe}
|
||||||
|
)
|
||||||
|
self.save()
|
||||||
|
|
||||||
|
def fail(self, state: str, reason: str) -> None:
|
||||||
|
self.value["state"] = state
|
||||||
|
self.value.setdefault("history", []).append(
|
||||||
|
{"at": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()), "state": state, "reason": str(reason)[:500]}
|
||||||
|
)
|
||||||
|
self.save()
|
||||||
|
|
||||||
|
|
||||||
|
def _git(git_dir: Path, *args: str, check: bool = True) -> str:
|
||||||
|
completed = subprocess.run(
|
||||||
|
["git", f"--git-dir={git_dir}", *args],
|
||||||
|
check=False,
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
timeout=45,
|
||||||
|
env={"PATH": os.environ.get("PATH", "")},
|
||||||
|
)
|
||||||
|
if check and completed.returncode != 0:
|
||||||
|
raise ReconciliationRequired(completed.stderr.strip() or "authoritative git query failed")
|
||||||
|
return completed.stdout.strip()
|
||||||
|
|
||||||
|
|
||||||
|
def _ancestor(git_dir: Path, ancestor: str, descendant: str) -> bool:
|
||||||
|
return subprocess.run(
|
||||||
|
["git", f"--git-dir={git_dir}", "merge-base", "--is-ancestor", ancestor, descendant],
|
||||||
|
check=False,
|
||||||
|
capture_output=True,
|
||||||
|
timeout=45,
|
||||||
|
env={"PATH": os.environ.get("PATH", "")},
|
||||||
|
).returncode == 0
|
||||||
|
|
||||||
|
|
||||||
|
def verify_lineage(git_dir: Path, binding: ReleaseBinding) -> dict[str, Any]:
|
||||||
|
binding.validate()
|
||||||
|
if _git(git_dir, "cat-file", "-t", binding.target_oid) != "commit":
|
||||||
|
raise ReleaseBlocked("approved target_oid is not a commit")
|
||||||
|
if _git(git_dir, "cat-file", "-t", binding.evidence_closure_oid) != "commit":
|
||||||
|
raise ReleaseBlocked("evidence closure is not a commit")
|
||||||
|
if not _ancestor(git_dir, binding.target_oid, binding.evidence_closure_oid):
|
||||||
|
raise ManualOwnerReviewRequired("approved target is not an ancestor of evidence closure")
|
||||||
|
return {
|
||||||
|
"target_oid": binding.target_oid,
|
||||||
|
"evidence_closure_oid": binding.evidence_closure_oid,
|
||||||
|
"target_is_ancestor_of_evidence": True,
|
||||||
|
"main_oid": _git(git_dir, "rev-parse", "refs/heads/main"),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def verify_owner_approval(value: Mapping[str, Any], binding: ReleaseBinding) -> dict[str, Any]:
|
||||||
|
approval = value.get("owner_approval")
|
||||||
|
if not isinstance(approval, Mapping):
|
||||||
|
raise ReleaseBlocked("OWNER_APPROVAL_RECEIPT_REQUIRED")
|
||||||
|
if approval.get("approved") is not True or approval.get("status") != "APPROVED":
|
||||||
|
raise ReleaseBlocked("OWNER_APPROVAL_NOT_ACCEPTED")
|
||||||
|
if approval.get("approved_by") != "aiman":
|
||||||
|
raise ManualOwnerReviewRequired("OWNER_APPROVAL_ACTOR_MISMATCH")
|
||||||
|
if approval.get("approval_source") != "explicit_owner_approval_in_chat":
|
||||||
|
raise ManualOwnerReviewRequired("OWNER_APPROVAL_SOURCE_MISMATCH")
|
||||||
|
for key, expected in binding.as_dict().items():
|
||||||
|
if key in {"repository", "production_service", "production_port"}:
|
||||||
|
continue
|
||||||
|
if approval.get(key) != expected:
|
||||||
|
raise ManualOwnerReviewRequired("OWNER_APPROVAL_BINDING_MISMATCH:" + key)
|
||||||
|
return dict(approval)
|
||||||
|
|
||||||
|
|
||||||
|
def _payload_file(git_dir: Path, oid: str, path: str) -> bytes:
|
||||||
|
completed = subprocess.run(
|
||||||
|
["git", f"--git-dir={git_dir}", "show", f"{oid}:{path}"],
|
||||||
|
check=False,
|
||||||
|
capture_output=True,
|
||||||
|
timeout=45,
|
||||||
|
env={"PATH": os.environ.get("PATH", "")},
|
||||||
|
)
|
||||||
|
if completed.returncode != 0:
|
||||||
|
raise ReleaseBlocked("required release source missing:" + path)
|
||||||
|
return completed.stdout
|
||||||
|
|
||||||
|
|
||||||
|
def _commit_epoch(git_dir: Path, oid: str) -> int:
|
||||||
|
return int(_git(git_dir, "show", "-s", "--format=%ct", oid))
|
||||||
|
|
||||||
|
|
||||||
|
def build_production_artifact(
|
||||||
|
git_dir: Path,
|
||||||
|
binding: ReleaseBinding,
|
||||||
|
output_root: Path,
|
||||||
|
) -> dict[str, Any]:
|
||||||
|
"""Build a deterministic executable V2R2C1 health artifact from target_oid."""
|
||||||
|
binding.validate()
|
||||||
|
verify_lineage(git_dir, binding)
|
||||||
|
epoch = _commit_epoch(git_dir, binding.target_oid)
|
||||||
|
output_root.mkdir(parents=True, exist_ok=True)
|
||||||
|
stem = f"aios-{VERSION}-{binding.target_oid[:12]}"
|
||||||
|
output = output_root / (stem + ".tar.zst")
|
||||||
|
with tempfile.TemporaryDirectory(prefix="v2r2c1-artifact-") as temp:
|
||||||
|
root = Path(temp) / "payload"
|
||||||
|
root.mkdir(parents=True)
|
||||||
|
source_paths = {
|
||||||
|
"active-runtime/app/deployment_state.py": "active-runtime/app/deployment_state.py",
|
||||||
|
"active-runtime/app/v2r1c6_privacy_tools.py": "active-runtime/app/v2r1c6_privacy_tools.py",
|
||||||
|
}
|
||||||
|
for destination, source in source_paths.items():
|
||||||
|
target = root / destination
|
||||||
|
target.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
target.write_bytes(_payload_file(git_dir, binding.target_oid, source))
|
||||||
|
generated: dict[str, str] = {
|
||||||
|
"VERSION": VERSION + "\n",
|
||||||
|
"active-runtime/app/v2r2c1_health_service.py": (
|
||||||
|
"from v2r1c6_privacy_tools import main\n\n"
|
||||||
|
"if __name__ == '__main__':\n raise SystemExit(main())\n"
|
||||||
|
),
|
||||||
|
"deploy/aios-v2/systemd/aios-v2r1c7-production.service": (
|
||||||
|
"[Unit]\nDescription=AIOS V2R2C1 governed health contract service\n"
|
||||||
|
"After=network.target\n\n[Service]\nType=simple\n"
|
||||||
|
"User=aios-v2r1c7\nGroup=aios-v2r1c7\n"
|
||||||
|
"WorkingDirectory=/opt/aios/v2r2c1\n"
|
||||||
|
"EnvironmentFile=/etc/aios/aios-v2r2c1.env\n"
|
||||||
|
"ExecStart=/usr/bin/python3 /opt/aios/v2r2c1/active-runtime/app/v2r2c1_health_service.py --serve --host 127.0.0.1 --port ${AIOS_V2R1C7_PORT}\n"
|
||||||
|
"Restart=on-failure\nRestartSec=5\nNoNewPrivileges=true\nPrivateTmp=true\n"
|
||||||
|
"ProtectHome=true\nProtectSystem=strict\nProtectKernelLogs=true\n"
|
||||||
|
"ProtectKernelModules=true\nProtectKernelTunables=true\nProtectControlGroups=true\n"
|
||||||
|
"RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6\nIPAddressDeny=any\n"
|
||||||
|
"IPAddressAllow=localhost\nUMask=0077\n\n[Install]\nWantedBy=multi-user.target\n"
|
||||||
|
),
|
||||||
|
"deploy/aios-v2/v2r2c1-runtime-config.json": json.dumps({
|
||||||
|
"schema": "aios.v2r2c1.production-runtime.v1",
|
||||||
|
"version": VERSION,
|
||||||
|
"target_oid": binding.target_oid,
|
||||||
|
"service": SERVICE,
|
||||||
|
"port": PORT,
|
||||||
|
"health_path": "/health",
|
||||||
|
"deployment_state_source": "environment_and_state_file",
|
||||||
|
}, sort_keys=True, separators=(",", ":")) + "\n",
|
||||||
|
"deploy/aios-v2/v2r2c1-rollback.json": json.dumps({
|
||||||
|
"schema": "aios.v2r2c1.rollback-metadata.v1",
|
||||||
|
"rollback_version": "V2R1C7",
|
||||||
|
"rollback_service": SERVICE,
|
||||||
|
"rollback_target_is_previous_production": True,
|
||||||
|
"automatic_rollback": False,
|
||||||
|
}, sort_keys=True, separators=(",", ":")) + "\n",
|
||||||
|
"scripts/probe_v2r2c1_health.py": (
|
||||||
|
"import json,sys,urllib.request\n"
|
||||||
|
"url=sys.argv[1] if len(sys.argv)>1 else 'http://127.0.0.1:18927/health'\n"
|
||||||
|
"with urllib.request.urlopen(url, timeout=5) as r: print(json.dumps(json.load(r), sort_keys=True))\n"
|
||||||
|
),
|
||||||
|
"scripts/rollback_v2r2c1_release.sh": "#!/bin/sh\nset -eu\nprintf '%s\\n' 'rollback requires governed owner-approved executor'\nexit 2\n",
|
||||||
|
"release-manifests/V2R2C1.json": json.dumps({
|
||||||
|
"schema": "aios.release-source.v2",
|
||||||
|
"version": VERSION,
|
||||||
|
"repository": REPOSITORY,
|
||||||
|
"target_oid": binding.target_oid,
|
||||||
|
"evidence_closure_oid": binding.evidence_closure_oid,
|
||||||
|
"production_deployed": False,
|
||||||
|
"deployment_state": "STAGING",
|
||||||
|
"artifact_kind": "executable-production-health",
|
||||||
|
}, sort_keys=True, separators=(",", ":")) + "\n",
|
||||||
|
}
|
||||||
|
for rel, text in generated.items():
|
||||||
|
target = root / rel
|
||||||
|
target.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
target.write_text(text, encoding="utf-8")
|
||||||
|
if rel.startswith("scripts/"):
|
||||||
|
target.chmod(0o755)
|
||||||
|
metadata = {
|
||||||
|
"schema": "aios.release-artifact-build.v3",
|
||||||
|
"version": VERSION,
|
||||||
|
"repository": REPOSITORY,
|
||||||
|
"source_head_oid": binding.target_oid,
|
||||||
|
"evidence_closure_oid": binding.evidence_closure_oid,
|
||||||
|
"source_date_epoch": epoch,
|
||||||
|
"clean_source": True,
|
||||||
|
"executable_payload": True,
|
||||||
|
"production_deployed": False,
|
||||||
|
"tag_created": False,
|
||||||
|
"baseline_changed": False,
|
||||||
|
"secrets_included": False,
|
||||||
|
}
|
||||||
|
(root / "BUILD-METADATA.json").write_bytes(canonical(metadata))
|
||||||
|
(root / "ROLLBACK-METADATA.json").write_bytes(canonical(json.loads((root / "deploy/aios-v2/v2r2c1-rollback.json").read_text(encoding="utf-8"))))
|
||||||
|
names = sorted("payload/" + str(path.relative_to(root)).replace(os.sep, "/") for path in root.rglob("*") if path.is_file())
|
||||||
|
(root / "ARTIFACT-MANIFEST.json").write_bytes(canonical({
|
||||||
|
"schema": "aios.release-artifact-manifest.v3",
|
||||||
|
"version": VERSION,
|
||||||
|
"source_head_oid": binding.target_oid,
|
||||||
|
"evidence_closure_oid": binding.evidence_closure_oid,
|
||||||
|
"repository": REPOSITORY,
|
||||||
|
"files": names + ["payload/ARTIFACT-MANIFEST.json"],
|
||||||
|
"deployable": True,
|
||||||
|
"executable_payload": True,
|
||||||
|
"production_deployed": False,
|
||||||
|
"tag_created": False,
|
||||||
|
"baseline_changed": False,
|
||||||
|
}))
|
||||||
|
tar_path = Path(temp) / "release.tar"
|
||||||
|
with tarfile.open(tar_path, "w") as archive:
|
||||||
|
for path in sorted(root.rglob("*")):
|
||||||
|
relative = str(path.relative_to(root)).replace(os.sep, "/")
|
||||||
|
info = archive.gettarinfo(str(path), arcname="payload/" + relative)
|
||||||
|
info.uid = info.gid = 0
|
||||||
|
info.uname = "root"
|
||||||
|
info.gname = "root"
|
||||||
|
info.mtime = epoch
|
||||||
|
info.mode = 0o755 if path.is_dir() or path.suffix == ".sh" else 0o644
|
||||||
|
if path.is_file():
|
||||||
|
with path.open("rb") as stream:
|
||||||
|
archive.addfile(info, stream)
|
||||||
|
else:
|
||||||
|
archive.addfile(info)
|
||||||
|
subprocess.run(["zstd", "-T1", "-19", "--no-progress", "-q", "-f", str(tar_path), "-o", str(output)], check=True, timeout=120)
|
||||||
|
digest = sha256_file(output)
|
||||||
|
receipt = {
|
||||||
|
"schema": "aios.release-artifact-receipt.v3",
|
||||||
|
"version": VERSION,
|
||||||
|
"repository": REPOSITORY,
|
||||||
|
"source_head_oid": binding.target_oid,
|
||||||
|
"evidence_closure_oid": binding.evidence_closure_oid,
|
||||||
|
"artifact": str(output),
|
||||||
|
"artifact_sha256": digest,
|
||||||
|
"artifact_size": output.stat().st_size,
|
||||||
|
"deployable": True,
|
||||||
|
"executable_payload": True,
|
||||||
|
"production_deployed": False,
|
||||||
|
"tag_created": False,
|
||||||
|
"baseline_changed": False,
|
||||||
|
}
|
||||||
|
atomic_json(output.with_suffix(output.suffix + ".receipt.json"), receipt, mode=0o640)
|
||||||
|
return receipt
|
||||||
|
|
||||||
|
|
||||||
|
class GovernedProductionReleaseExecutor:
|
||||||
|
def __init__(self, config: Any):
|
||||||
|
self.config = config
|
||||||
|
self.binding = ReleaseBinding()
|
||||||
|
self.binding.validate()
|
||||||
|
self.root = Path(config.state_root) / "v2r2c1-governed-production-release"
|
||||||
|
self.state = ReleaseState(self.root, self.binding)
|
||||||
|
workspace = self.root / "publication-workspace"
|
||||||
|
workspace.mkdir(parents=True, exist_ok=True)
|
||||||
|
self.publisher = TagPublisher(
|
||||||
|
workspace,
|
||||||
|
GitTagBackend(workspace, authoritative_repo=Path(config.authoritative_git_dir)),
|
||||||
|
GiteaTagClient(config.api_base, REPOSITORY, Path(config.token_path)),
|
||||||
|
config.remote_name,
|
||||||
|
state_root=self.root / "tag-publication",
|
||||||
|
)
|
||||||
|
|
||||||
|
def _request(self, request: Mapping[str, Any]) -> PublicationRequest:
|
||||||
|
binding = self.binding
|
||||||
|
for key, expected in binding.as_dict().items():
|
||||||
|
if key in request and request[key] != expected:
|
||||||
|
raise ManualOwnerReviewRequired("release request binding mismatch:" + key)
|
||||||
|
return PublicationRequest(binding.version, binding.target_oid, binding.evidence_closure_oid, binding.approval_plan_id)
|
||||||
|
|
||||||
|
def approval_request(self, request: Mapping[str, Any]) -> dict[str, Any]:
|
||||||
|
if request.get("execute") is not False:
|
||||||
|
raise ReleaseBlocked("TAG_APPROVAL_REQUEST_MUST_BE_DRY_RUN")
|
||||||
|
owner = verify_owner_approval(request, self.binding)
|
||||||
|
with StateLock(self.root / "release.lock"):
|
||||||
|
current = self.state.value.get("approval")
|
||||||
|
approval_digest = hashlib.sha256(canonical(owner)).hexdigest()
|
||||||
|
record = {
|
||||||
|
"schema": "aios.v2r2c1.tag-approval-request.v1",
|
||||||
|
"binding": self.binding.as_dict(),
|
||||||
|
"owner_approval": owner,
|
||||||
|
"owner_approval_digest_sha256": approval_digest,
|
||||||
|
"execute": False,
|
||||||
|
"recorded_at": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()),
|
||||||
|
}
|
||||||
|
if current and current != record:
|
||||||
|
raise ManualOwnerReviewRequired("persisted Owner approval differs")
|
||||||
|
self.state.value["approval"] = record
|
||||||
|
self.state.record("APPROVAL_REQUEST_RECORDED", "tag_approval_request", record)
|
||||||
|
return {"status": "RECORDED", "execute": False, "binding": self.binding.as_dict(), "owner_approval_digest_sha256": approval_digest}
|
||||||
|
|
||||||
|
def _require_approval(self) -> None:
|
||||||
|
approval = self.state.value.get("approval")
|
||||||
|
if not isinstance(approval, Mapping) or approval.get("binding") != self.binding.as_dict():
|
||||||
|
raise ReleaseBlocked("OWNER_APPROVAL_REQUEST_NOT_RECORDED")
|
||||||
|
|
||||||
|
def publish_tag(self, request: Mapping[str, Any]) -> dict[str, Any]:
|
||||||
|
if request.get("execute") is not True:
|
||||||
|
raise ReleaseBlocked("TAG_PUBLICATION_REQUIRES_EXECUTE_TRUE")
|
||||||
|
self._require_approval()
|
||||||
|
publication = self._request(request)
|
||||||
|
with StateLock(self.root / "release.lock"):
|
||||||
|
try:
|
||||||
|
verified = self.publisher._verify_publication(publication, allow_push=True, previously_deployed=False)
|
||||||
|
except PublicationManualOwnerReviewRequired as exc:
|
||||||
|
self.state.fail("MANUAL_OWNER_REVIEW_REQUIRED", str(exc))
|
||||||
|
raise ManualOwnerReviewRequired(str(exc)) from exc
|
||||||
|
except PublicationPending as exc:
|
||||||
|
self.state.fail("REMOTE_PUSH_PENDING", str(exc))
|
||||||
|
raise ReconciliationRequired(str(exc)) from exc
|
||||||
|
except PublicationReconciliationRequired as exc:
|
||||||
|
self.state.fail("RECONCILIATION_REQUIRED", str(exc))
|
||||||
|
raise ReconciliationRequired(str(exc)) from exc
|
||||||
|
result = dict(verified)
|
||||||
|
self.state.record("TAG_VERIFIED", "tag", result)
|
||||||
|
return {"status": "REMOTE_VERIFIED", **result}
|
||||||
|
|
||||||
|
def tag_status(self, request: Mapping[str, Any]) -> dict[str, Any]:
|
||||||
|
self._request(request)
|
||||||
|
return self.publisher.get_tag_publication_status(VERSION)
|
||||||
|
|
||||||
|
def build_artifact(self, request: Mapping[str, Any]) -> dict[str, Any]:
|
||||||
|
self._require_approval()
|
||||||
|
self._request(request)
|
||||||
|
receipt = build_production_artifact(Path(self.config.authoritative_git_dir), self.binding, self.root / "artifacts")
|
||||||
|
self.state.record("ARTIFACT_VERIFIED", "production_artifact", receipt)
|
||||||
|
return {"status": "VERIFIED", **receipt}
|
||||||
|
|
||||||
|
def execute_request(self, request: Mapping[str, Any]) -> dict[str, Any]:
|
||||||
|
self._require_approval()
|
||||||
|
publication = self._request(request)
|
||||||
|
tag_state = self.state.value.get("completed", {}).get("tag")
|
||||||
|
if not isinstance(tag_state, Mapping) or tag_state.get("state") not in {"GITEA_API_CONFIRMED", "REMOTE_VERIFIED"}:
|
||||||
|
raise ReleaseBlocked("REMOTE_TAG_PROOF_REQUIRED")
|
||||||
|
artifact = self.state.value.get("completed", {}).get("production_artifact")
|
||||||
|
if not isinstance(artifact, Mapping):
|
||||||
|
artifact = self.build_artifact(request)
|
||||||
|
value = {
|
||||||
|
**self.binding.as_dict(),
|
||||||
|
"tag_oid": tag_state.get("tag_oid") or tag_state.get("gitea_tag_oid"),
|
||||||
|
"artifact_path": artifact["artifact"],
|
||||||
|
"artifact_size": int(artifact["artifact_size"]),
|
||||||
|
"artifact_sha256": artifact["artifact_sha256"],
|
||||||
|
}
|
||||||
|
# The production adapter is privileged and is run through the root
|
||||||
|
# approved-release service boundary by the caller. This handler is
|
||||||
|
# still useful for a root-owned staging invocation and remains
|
||||||
|
# fail-closed when the adapter is not available.
|
||||||
|
import v2r2c1_production_adapter
|
||||||
|
runtime = v2r2c1_production_adapter.execute_request({"schema": "aios.production-executor-request.v1", "operation": "apply", **value})
|
||||||
|
if runtime.get("status") != "ACTIVE_VERIFIED":
|
||||||
|
self.state.fail("RECONCILIATION_REQUIRED", "production health did not verify")
|
||||||
|
raise ReconciliationRequired("production health did not verify")
|
||||||
|
self.state.record("PRODUCTION_VERIFIED", "production", runtime)
|
||||||
|
return {"status": "PRODUCTION_VERIFIED", "runtime": runtime, "artifact": artifact, "tag_oid": value["tag_oid"]}
|
||||||
|
|
||||||
|
def consistency(self, request: Mapping[str, Any]) -> dict[str, Any]:
|
||||||
|
publication = self._request(request)
|
||||||
|
result = self.publisher.get_release_consistency(publication)
|
||||||
|
result["release_binding"] = self.binding.as_dict()
|
||||||
|
result["production_health"] = _health(self.config.health_url, self.binding, self.state.value.get("completed", {}).get("production_artifact", {}))
|
||||||
|
result["registry"] = _read_json(Path(self.config.registry_root) / f"{VERSION}.json")
|
||||||
|
result["baseline"] = _read_json(Path(self.config.baseline_path))
|
||||||
|
return result
|
||||||
|
|
||||||
|
def acceptance(self, request: Mapping[str, Any]) -> dict[str, Any]:
|
||||||
|
result = self.consistency(request)
|
||||||
|
result["accepted"] = result.get("status") == "PASS" and result.get("production_health", {}).get("status") == "ACTIVE_VERIFIED"
|
||||||
|
self.state.record("ACCEPTED" if result["accepted"] else "RECONCILIATION_REQUIRED", "acceptance", result)
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def _read_json(path: Path) -> dict[str, Any]:
|
||||||
|
try:
|
||||||
|
value = json.loads(path.read_text(encoding="utf-8"))
|
||||||
|
except FileNotFoundError:
|
||||||
|
return {"status": "MISSING", "path": str(path)}
|
||||||
|
if not isinstance(value, dict):
|
||||||
|
return {"status": "INVALID", "path": str(path)}
|
||||||
|
return {"status": "PRESENT", "path": str(path), **value}
|
||||||
|
|
||||||
|
|
||||||
|
def _health(url: str, binding: ReleaseBinding, artifact: Mapping[str, Any]) -> dict[str, Any]:
|
||||||
|
import urllib.request
|
||||||
|
try:
|
||||||
|
with urllib.request.urlopen(url, timeout=5) as response:
|
||||||
|
body = json.loads(response.read().decode("utf-8"))
|
||||||
|
status = response.status
|
||||||
|
except Exception as exc:
|
||||||
|
return {"status": "UNAVAILABLE", "error": type(exc).__name__}
|
||||||
|
expected_artifact = artifact.get("artifact_sha256")
|
||||||
|
exact = (
|
||||||
|
status == 200
|
||||||
|
and body.get("version") == binding.version
|
||||||
|
and body.get("commit_oid") == binding.target_oid
|
||||||
|
and (not expected_artifact or body.get("artifact_sha256") == expected_artifact)
|
||||||
|
and body.get("deployment_state") == "ACTIVE"
|
||||||
|
and body.get("production_deployed") is True
|
||||||
|
)
|
||||||
|
return {"status": "ACTIVE_VERIFIED" if exact else "CONFLICT", "http_status": status, **body}
|
||||||
|
|
||||||
|
|
||||||
|
def build_executor_config(config: Any) -> GovernedProductionReleaseExecutor:
|
||||||
|
return GovernedProductionReleaseExecutor(config)
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> int:
|
||||||
|
parser = argparse.ArgumentParser()
|
||||||
|
parser.add_argument("operation", choices=("build-artifact", "status"))
|
||||||
|
parser.add_argument("--git-dir", type=Path, default=Path("/data/aios-gitops/repo.git"))
|
||||||
|
parser.add_argument("--output-root", type=Path, default=Path("/var/lib/aios-gitops/approved-artifacts/V2R2C1"))
|
||||||
|
args = parser.parse_args()
|
||||||
|
binding = ReleaseBinding()
|
||||||
|
if args.operation == "build-artifact":
|
||||||
|
print(json.dumps(build_production_artifact(args.git_dir, binding, args.output_root), sort_keys=True))
|
||||||
|
else:
|
||||||
|
print(json.dumps({"binding": binding.as_dict(), "status": "READY"}, sort_keys=True))
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
raise SystemExit(main())
|
||||||
@@ -0,0 +1,135 @@
|
|||||||
|
"""V2R2C1 adapter using the existing V2R1C7 deployment primitives.
|
||||||
|
|
||||||
|
The service identity and loopback port remain stable for a safe cutover. The
|
||||||
|
new release is installed under its own immutable directory and keeps the
|
||||||
|
previous V2R1C7 release available for an explicit, owner-approved rollback.
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import shutil
|
||||||
|
import sys
|
||||||
|
from argparse import Namespace
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Any, Mapping
|
||||||
|
|
||||||
|
import v2r1c7_production_adapter as base
|
||||||
|
|
||||||
|
|
||||||
|
VERSION = "V2R2C1"
|
||||||
|
SERVICE = "aios-v2r1c7-production.service"
|
||||||
|
PORT = 18927
|
||||||
|
RELEASE_ROOT = Path("/opt/aios/releases")
|
||||||
|
CURRENT_LINK = Path("/opt/aios/v2r2c1")
|
||||||
|
ENV_FILE = Path("/etc/aios/aios-v2r2c1.env")
|
||||||
|
STATE_FILE = Path("/var/lib/aios/v2r2c1/deployment-state.json")
|
||||||
|
UNIT_FILE = Path("/etc/systemd/system/aios-v2r1c7-production.service")
|
||||||
|
ROLLBACK_ROOT = Path("/var/lib/aios/v2r2c1/rollback")
|
||||||
|
|
||||||
|
REQUIRED_MEMBERS = {
|
||||||
|
"payload/VERSION",
|
||||||
|
"payload/active-runtime/app/deployment_state.py",
|
||||||
|
"payload/active-runtime/app/v2r1c6_privacy_tools.py",
|
||||||
|
"payload/active-runtime/app/v2r2c1_health_service.py",
|
||||||
|
"payload/deploy/aios-v2/systemd/aios-v2r1c7-production.service",
|
||||||
|
"payload/deploy/aios-v2/v2r2c1-runtime-config.json",
|
||||||
|
"payload/deploy/aios-v2/v2r2c1-rollback.json",
|
||||||
|
"payload/scripts/probe_v2r2c1_health.py",
|
||||||
|
"payload/scripts/rollback_v2r2c1_release.sh",
|
||||||
|
"payload/BUILD-METADATA.json",
|
||||||
|
"payload/ARTIFACT-MANIFEST.json",
|
||||||
|
"payload/ROLLBACK-METADATA.json",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _configure_base() -> None:
|
||||||
|
base.VERSION = VERSION
|
||||||
|
base.SERVICE = SERVICE
|
||||||
|
base.PORT = PORT
|
||||||
|
base.REQUIRED_MEMBERS = REQUIRED_MEMBERS
|
||||||
|
|
||||||
|
|
||||||
|
def _args() -> Namespace:
|
||||||
|
return Namespace(
|
||||||
|
release_root=RELEASE_ROOT,
|
||||||
|
current_link=CURRENT_LINK,
|
||||||
|
env_file=ENV_FILE,
|
||||||
|
state_file=STATE_FILE,
|
||||||
|
unit_file=UNIT_FILE,
|
||||||
|
health_timeout=45,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def load_request() -> dict[str, Any]:
|
||||||
|
_configure_base()
|
||||||
|
value = base.load_request()
|
||||||
|
if value.get("version") != VERSION or value.get("production_service") != SERVICE or int(value.get("production_port", -1)) != PORT:
|
||||||
|
raise ValueError("V2R2C1 production binding mismatch")
|
||||||
|
return value
|
||||||
|
|
||||||
|
|
||||||
|
def _backup(path: Path, name: str) -> None:
|
||||||
|
if path.is_file() and not path.is_symlink():
|
||||||
|
ROLLBACK_ROOT.mkdir(parents=True, exist_ok=True)
|
||||||
|
destination = ROLLBACK_ROOT / name
|
||||||
|
if not destination.exists():
|
||||||
|
shutil.copy2(path, destination)
|
||||||
|
|
||||||
|
|
||||||
|
def execute_request(value: Mapping[str, Any]) -> dict[str, Any]:
|
||||||
|
_configure_base()
|
||||||
|
if value.get("schema") != "aios.production-executor-request.v1":
|
||||||
|
raise ValueError("request_schema_invalid")
|
||||||
|
if value.get("operation") == "apply":
|
||||||
|
_backup(UNIT_FILE, "aios-v2r1c7-production.service.previous")
|
||||||
|
_backup(Path("/etc/aios/aios-v2r1c7.env"), "aios-v2r1c7.env.previous")
|
||||||
|
_backup(Path("/var/lib/aios/v2r1c7/deployment-state.json"), "v2r1c7-deployment-state.previous")
|
||||||
|
return base.install_release(_args(), value)
|
||||||
|
if value.get("operation") == "probe":
|
||||||
|
return base.probe(_args(), value)
|
||||||
|
if value.get("operation") == "rollback":
|
||||||
|
return rollback(value)
|
||||||
|
raise ValueError("unsupported_operation")
|
||||||
|
|
||||||
|
|
||||||
|
def rollback(value: Mapping[str, Any]) -> dict[str, Any]:
|
||||||
|
"""Explicit rollback only; this adapter never invokes it automatically."""
|
||||||
|
import subprocess
|
||||||
|
|
||||||
|
subprocess.run(["systemctl", "stop", SERVICE], check=False, capture_output=True, text=True, timeout=45)
|
||||||
|
previous_unit = ROLLBACK_ROOT / "aios-v2r1c7-production.service.previous"
|
||||||
|
previous_env = ROLLBACK_ROOT / "aios-v2r1c7.env.previous"
|
||||||
|
previous_state = ROLLBACK_ROOT / "v2r1c7-deployment-state.previous"
|
||||||
|
if not previous_unit.is_file() or not previous_env.is_file():
|
||||||
|
raise RuntimeError("ROLLBACK_RECEIPT_MISSING")
|
||||||
|
UNIT_FILE.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
ENV_FILE.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
shutil.copy2(previous_unit, UNIT_FILE)
|
||||||
|
shutil.copy2(previous_env, Path("/etc/aios/aios-v2r1c7.env"))
|
||||||
|
if previous_state.is_file():
|
||||||
|
Path("/var/lib/aios/v2r1c7").mkdir(parents=True, exist_ok=True)
|
||||||
|
shutil.copy2(previous_state, Path("/var/lib/aios/v2r1c7/deployment-state.json"))
|
||||||
|
subprocess.run(["systemctl", "daemon-reload"], check=True, capture_output=True, text=True, timeout=45)
|
||||||
|
subprocess.run(["systemctl", "restart", SERVICE], check=True, capture_output=True, text=True, timeout=45)
|
||||||
|
result = base.health("http://127.0.0.1:18927/health", {
|
||||||
|
"version": "V2R1C7",
|
||||||
|
"target_oid": str(json.loads(Path("/var/lib/aios/v2r1c7/deployment-state.json").read_text(encoding="utf-8")).get("commit_oid")),
|
||||||
|
"artifact_sha256": str(json.loads(Path("/var/lib/aios/v2r1c7/deployment-state.json").read_text(encoding="utf-8")).get("artifact_sha256")),
|
||||||
|
})
|
||||||
|
return {"status": "ROLLED_BACK", "rollback_health": result, "automatic_rollback": False}
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> int:
|
||||||
|
try:
|
||||||
|
value = load_request()
|
||||||
|
result = execute_request(value)
|
||||||
|
print(json.dumps(result, sort_keys=True))
|
||||||
|
return 0
|
||||||
|
except Exception as exc:
|
||||||
|
print(json.dumps({"status": "FAIL_CLOSED", "error": type(exc).__name__, "reason": str(exc)}, sort_keys=True))
|
||||||
|
return 2
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
raise SystemExit(main())
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
[Unit]
|
||||||
|
Description=AIOS ChatGPT Bridge v2 staging control plane
|
||||||
|
After=network-online.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=simple
|
||||||
|
User=aios-bridge-staging
|
||||||
|
Group=aios-bridge-staging
|
||||||
|
WorkingDirectory=/opt/aios/gitops-control-plane/current
|
||||||
|
Environment=AIOS_BRIDGE_SCHEMA_VERSION=aios.chatgpt-gitops-bridge.v2
|
||||||
|
Environment=AIOS_GITOPS_WRITES_ENABLED=false
|
||||||
|
Environment=AIOS_GITOPS_DURABLE_JOB_EXECUTION_ENABLED=true
|
||||||
|
Environment=AIOS_DEPLOYMENT_STATE=STAGING
|
||||||
|
ExecStart=/usr/bin/python3 /opt/aios/gitops-control-plane/current/bridge-v2-staging-server.py
|
||||||
|
NoNewPrivileges=true
|
||||||
|
PrivateTmp=true
|
||||||
|
ProtectSystem=strict
|
||||||
|
ProtectHome=true
|
||||||
|
ReadWritePaths=/var/lib/aios-gitops/bridge-v2-staging
|
||||||
|
Restart=on-failure
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
[Unit]
|
||||||
|
Description=AIOS GitOps Model API V2R2C1 immutable staging runtime
|
||||||
|
After=network-online.target
|
||||||
|
Wants=network-online.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=simple
|
||||||
|
User=aiman
|
||||||
|
Group=gitops
|
||||||
|
WorkingDirectory=@RUNTIME_ROOT@
|
||||||
|
Environment=PYTHONUNBUFFERED=1
|
||||||
|
Environment=PYTHONDONTWRITEBYTECODE=1
|
||||||
|
Environment=AIOS_GITOPS_ROOT=/var/lib/aios-gitops
|
||||||
|
Environment=AIOS_GITOPS_WORKSPACE=@RUNTIME_ROOT@
|
||||||
|
Environment=AIOS_GITOPS_REPO=/data/aios-gitops/repo.git
|
||||||
|
Environment=AIOS_GITOPS_BARE_REPO=/data/aios-gitops/repo.git
|
||||||
|
Environment=AIOS_GITOPS_AUTHORITATIVE_GIT_DIR=/data/aios-gitops/repo.git
|
||||||
|
Environment=AIOS_GITOPS_REPOSITORY_IDENTITY=aios/aios
|
||||||
|
Environment=AIOS_GITOPS_BASELINE_AUTHORITY=/var/lib/aios-gitops-runtime-anchor/baseline/current.json
|
||||||
|
Environment=AIOS_GITOPS_WRITES_ENABLED=false
|
||||||
|
Environment=AIOS_GITOPS_DURABLE_JOB_EXECUTION_ENABLED=false
|
||||||
|
Environment=AIOS_GITOPS_JOB_CONTROLLER_ENABLED=false
|
||||||
|
Environment=AIOS_GITOPS_WORKSPACE_ROLE=runtime-anchor
|
||||||
|
Environment=AIOS_GITOPS_RUNTIME_OID=@SOURCE_OID@
|
||||||
|
Environment=AIOS_GITOPS_ARTIFACT_SHA256=@ARTIFACT_SHA256@
|
||||||
|
Environment=AIOS_GITOPS_DEPLOYMENT_STATE=STAGING
|
||||||
|
Environment=AIOS_GITOPS_MODEL_API_SERVICE=aios-gitops-model-api-v2r2c1-staging.service
|
||||||
|
Environment=AIOS_SILENT_RELEASE_EXECUTION_ENABLED=false
|
||||||
|
Environment=AIOS_SILENT_RELEASE_STATE_ROOT=/var/lib/aios-gitops/model-api-staging/silent-release
|
||||||
|
Environment=AIOS_BRIDGE_V2_URL=http://127.0.0.1:18892
|
||||||
|
Environment=AIOS_GITOPS_HOST=127.0.0.1
|
||||||
|
Environment=AIOS_GITOPS_PORT=18893
|
||||||
|
Environment=PYTHONPATH=@RUNTIME_ROOT@/app:@RUNTIME_ROOT@/runtime-agent-os
|
||||||
|
ExecStart=/usr/bin/python3 @RUNTIME_ROOT@/app/aios_model_api_staging.py
|
||||||
|
Restart=on-failure
|
||||||
|
RestartSec=2
|
||||||
|
NoNewPrivileges=true
|
||||||
|
PrivateTmp=true
|
||||||
|
ProtectHome=true
|
||||||
|
ProtectSystem=strict
|
||||||
|
ProtectKernelTunables=true
|
||||||
|
ProtectKernelModules=true
|
||||||
|
ProtectControlGroups=true
|
||||||
|
LockPersonality=true
|
||||||
|
RestrictSUIDSGID=true
|
||||||
|
RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6
|
||||||
|
ReadOnlyPaths=@RUNTIME_ROOT@ /data/aios-gitops/repo.git /var/lib/aios-gitops-runtime-anchor/baseline
|
||||||
|
ReadWritePaths=/var/lib/aios-gitops/model-api-staging
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
@@ -240,6 +240,114 @@ components:
|
|||||||
additionalProperties: false
|
additionalProperties: false
|
||||||
properties:
|
properties:
|
||||||
candidate_id: {type: string}
|
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:
|
paths:
|
||||||
/createReleaseIntent:
|
/createReleaseIntent:
|
||||||
post:
|
post:
|
||||||
@@ -567,3 +675,118 @@ paths:
|
|||||||
'200': {description: Safe GitOps plan}
|
'200': {description: Safe GitOps plan}
|
||||||
'401': {description: Unauthorized}
|
'401': {description: Unauthorized}
|
||||||
x-openai-isConsequential: true
|
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,15 @@
|
|||||||
|
# Bridge v2.1 Durable Reconciliation Bootstrap
|
||||||
|
|
||||||
|
This is the planning-only bootstrap artifact for
|
||||||
|
`TASK-P0-BRIDGE-V21-DURABLE-RECONCILIATION-20260809`.
|
||||||
|
|
||||||
|
The task is bound to the exact base commit
|
||||||
|
`3f670c387692b8865a10d5b419468d8a0b64d7a4` and the development branch
|
||||||
|
`agent/p0-bridge-v21-durable-reconciliation-20260809`.
|
||||||
|
|
||||||
|
The first governed commit records the design boundary and acceptance contract
|
||||||
|
only. It does not implement the v2.1 body, mutate main, create tags, change
|
||||||
|
Baseline, or touch production. Future work must preserve absent-ref CAS,
|
||||||
|
observation-first recovery, durable JobStore state, exact Authority/Gitea/API
|
||||||
|
reconciliation, and the existing secret-scanning and credential-projection
|
||||||
|
boundaries.
|
||||||
+150
@@ -0,0 +1,150 @@
|
|||||||
|
# ChatGPT Bridge v2 Control Plane Convergence Design
|
||||||
|
|
||||||
|
Date: 2026-08-09
|
||||||
|
Task: `TASK-P0-CHATGPT-BRIDGE-V2-CONTROL-PLANE-CONVERGENCE-20260809`
|
||||||
|
Repository: `aios/aios`
|
||||||
|
Approved development base: `2e8fa22989808a448065d1d76b5b18948774a637`
|
||||||
|
Target branch: `agent/p0-chatgpt-bridge-v2-control-plane-convergence-20260809`
|
||||||
|
|
||||||
|
## Scope and authority
|
||||||
|
|
||||||
|
This design converges the ChatGPT GitOps Bridge, the existing Authorized Task Writer, the continuation lease protocol, the Durable GitOps Job Controller, and immutable control-plane runtime identity.
|
||||||
|
|
||||||
|
The development base is exact and must never be replaced with a dynamically resolved `HEAD`, `latest`, or `main`. If Authority `refs/heads/main` differs from the approved OID before the first governed writer push, execution fails with `BASE_OID_CHANGED`.
|
||||||
|
|
||||||
|
This task does not authorize production changes. The following remain immutable:
|
||||||
|
|
||||||
|
- Production Baseline `V2R1C7`.
|
||||||
|
- Production target OID `c7adece1cc8beffe7c718d53c775898b45d6c7f8`.
|
||||||
|
- Production tag OID `82792347937c0df59a8e02d6162775252bcad2e2`.
|
||||||
|
- Production Registry, services, deployment state, runtime, tags, and release evidence.
|
||||||
|
- Authority `main`.
|
||||||
|
|
||||||
|
Force push, history rewrite, direct main push, production mutation, tag mutation, baseline mutation, and secret commits are always denied.
|
||||||
|
|
||||||
|
## Immutable control-plane runtime identity
|
||||||
|
|
||||||
|
A control-plane release is installed into a versioned immutable directory equivalent to:
|
||||||
|
|
||||||
|
```text
|
||||||
|
/opt/aios/gitops-control-plane/<release-id-or-oid>/
|
||||||
|
```
|
||||||
|
|
||||||
|
The release manifest binds:
|
||||||
|
|
||||||
|
```text
|
||||||
|
control_plane_release_version
|
||||||
|
control_plane_target_oid
|
||||||
|
artifact_sha256
|
||||||
|
runtime_path
|
||||||
|
bridge_schema_version
|
||||||
|
repository_identity
|
||||||
|
authoritative_git_dir
|
||||||
|
deployment_state
|
||||||
|
```
|
||||||
|
|
||||||
|
The validator is independent from the mutable Git workspace. It rejects execution when the runtime path is a workspace/worktree, the observed runtime commit or artifact digest differs from the manifest, the repository identity is not `aios/aios`, the authoritative Git directory is absent or mismatched, or the Bridge schema version is absent or unsupported.
|
||||||
|
|
||||||
|
Every failure is fail closed with `CONTROL_PLANE_RUNTIME_DRIFT`. No fallback to a mutable workspace is permitted. The installer and systemd template are staging-safe; this task may install and restart only a staging/test service.
|
||||||
|
|
||||||
|
## Bridge v2 adapter
|
||||||
|
|
||||||
|
The Bridge schema identity is `aios.chatgpt-gitops-bridge.v2`. It is independent from the production baseline, control-plane release version, and runtime release commit.
|
||||||
|
|
||||||
|
The Bridge exposes:
|
||||||
|
|
||||||
|
- `getRuntimeStatus`
|
||||||
|
- `getGovernedWriteCapabilities`
|
||||||
|
- `createGovernedTask`
|
||||||
|
- `acquireWriterLease`
|
||||||
|
- `commitFileGoverned`
|
||||||
|
- `commitFilesGoverned`
|
||||||
|
- `getGitOpsJob`
|
||||||
|
- `resumeGitOpsJob`
|
||||||
|
- `reconcileGitOpsJob`
|
||||||
|
|
||||||
|
The adapter reuses existing mechanisms and does not create a second writer, lease protocol, branch state store, job database, or direct Git mutation path.
|
||||||
|
|
||||||
|
## Governed task and lease flow
|
||||||
|
|
||||||
|
`createGovernedTask` binds an exact task ID, repository, approved base OID, non-main branch, risk, and description. Existing branches are read back and bound to their observed full OID. Idempotent replay returns the original binding; conflicting replay fails closed.
|
||||||
|
|
||||||
|
`acquireWriterLease` uses `WriterLeaseRequestV2`. Continuation uses:
|
||||||
|
|
||||||
|
```text
|
||||||
|
branch_mode=CONTINUE_EXISTING
|
||||||
|
expected_branch_oid=<full OID>
|
||||||
|
generation=<monotonic integer>
|
||||||
|
create_target_branch=false
|
||||||
|
worktree_mode=detached_at_expected_head
|
||||||
|
cas_ref_update_required=true
|
||||||
|
```
|
||||||
|
|
||||||
|
Credentials remain server-side and never appear in responses, Git, durable job metadata, or logs.
|
||||||
|
|
||||||
|
## Governed commit flow
|
||||||
|
|
||||||
|
`commitFileGoverned` and `commitFilesGoverned` submit work to the existing `GitOpsJobController` and `JobStore`. The server computes payload digests. Raw file content is stored only in the content-addressed payload domain. Secret scanning, protected-path policy, repository binding, lease validation, and branch authorization run before side effects.
|
||||||
|
|
||||||
|
`commitFilesGoverned` is one atomic Git commit and enforces bounded file count, per-file size, and total payload size. Any failure leaves no partial commit or ref update.
|
||||||
|
|
||||||
|
All ref updates use compare-and-swap. Stale `expected_branch_oid` returns conflict and never triggers automatic rebase. After push, Authority and remote Gitea refs are independently read and must equal the resulting commit OID.
|
||||||
|
|
||||||
|
## Durable job lifecycle
|
||||||
|
|
||||||
|
Job reads return only non-sensitive metadata: `job_id`, `state`, `revision`, `side_effect_state`, `retryable`, `binding`, and `receipt_summary`.
|
||||||
|
|
||||||
|
Terminal jobs cannot be changed by ordinary resume. Resume requires `job_id` and `expected_revision` and uses revision CAS. Reconciliation is observation-first: for `UNKNOWN`, `RECONCILIATION_REQUIRED`, or `REMOTE_PUSH_PENDING`, local, Authority, and remote refs are read before state changes. A blind repeated push is never reconciliation.
|
||||||
|
|
||||||
|
## Legacy behavior
|
||||||
|
|
||||||
|
Legacy writes remain disabled while governed writes are enabled:
|
||||||
|
|
||||||
|
```text
|
||||||
|
legacy_writes_enabled=false
|
||||||
|
governed_writes_enabled=true
|
||||||
|
```
|
||||||
|
|
||||||
|
The global `AIOS_GITOPS_WRITES_ENABLED` flag is never enabled as the solution. Legacy operations remain temporarily visible for compatibility and diagnostics, but return:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"error": "LEGACY_WRITE_DISABLED",
|
||||||
|
"replacement": "createGovernedTask",
|
||||||
|
"governed_writes_available": true
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## OpenAPI convergence
|
||||||
|
|
||||||
|
`docs/gitops/openapi-chatgpt-actions.yaml` is the canonical Bridge v2 schema and integrates the Authorized Task Writer fragment. The canonical CAS field is `expected_branch_oid`. `expected_head_oid` may be accepted only at an explicit deprecated adapter boundary.
|
||||||
|
|
||||||
|
Contract tests require exact equality between OpenAPI required fields, runtime parser required fields, and generated GPT Action schema required fields.
|
||||||
|
|
||||||
|
## Acceptance
|
||||||
|
|
||||||
|
Automated coverage includes runtime path/OID/digest/repository drift, exact task/branch/OID/lease/CAS behavior, job DB failure, restart during remote verification, remote timeout, response loss after remote success, idempotency replay/conflict, concurrent branch movement, unauthorized or main branch writes, protected paths, secret-like payloads, disabled governed writer, missing/expired lease, wrong repository identity, and observation-first reconciliation.
|
||||||
|
|
||||||
|
The staging sequence is:
|
||||||
|
|
||||||
|
```text
|
||||||
|
getRuntimeStatus
|
||||||
|
getGovernedWriteCapabilities
|
||||||
|
createGovernedTask
|
||||||
|
acquireWriterLease
|
||||||
|
commitFilesGoverned
|
||||||
|
getGitOpsJob
|
||||||
|
verify Authority branch ref
|
||||||
|
verify remote Gitea branch ref
|
||||||
|
restart staging control-plane service
|
||||||
|
getRuntimeStatus
|
||||||
|
acquire continuation lease
|
||||||
|
second governed commit
|
||||||
|
verify remote ref again
|
||||||
|
```
|
||||||
|
|
||||||
|
Completion requires all tests, staging restart continuation, Authority/remote OID equality, unchanged production invariants, no force push, no tag rewrite, no direct main push, and no secret commit.
|
||||||
|
|
||||||
|
## Implementation boundaries
|
||||||
|
|
||||||
|
Implementation is limited to the Bridge, existing writer and job-controller adapters, runtime identity/installer, staging systemd template, OpenAPI documents, tests, and engineering evidence. Hermes Adapter integration and production cutover are outside this task and require separate approval.
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
[pytest]
|
||||||
|
testpaths = tests
|
||||||
|
python_files = test_*.py
|
||||||
|
addopts = -ra --import-mode=importlib
|
||||||
@@ -0,0 +1,243 @@
|
|||||||
|
{
|
||||||
|
"authority_proof": {
|
||||||
|
"branch_oid": "2c6293160ed567a2098715edbb296741577c5bff",
|
||||||
|
"commit_visible": true,
|
||||||
|
"main_oid": "2e8fa22989808a448065d1d76b5b18948774a637",
|
||||||
|
"v2r2c1_tag_oid": "998d30304787e31e5ae607c0bd86b5edb2b0e5ad"
|
||||||
|
},
|
||||||
|
"baseline_authority": {
|
||||||
|
"artifact_sha256": "492338b895b152a55015fd92ea1d9dba2314894ab00c0ba219a78f3cef544b8a",
|
||||||
|
"authority_sha256": "d8e23bad49bdb3e4192ba708c3dfe7fc5b7cdc6a513c2f87acd6412a51867231",
|
||||||
|
"current_baseline": "V2R2C1",
|
||||||
|
"raw_byte_sidecar_verification": "PASS",
|
||||||
|
"target_oid": "998d30304787e31e5ae607c0bd86b5edb2b0e5ad"
|
||||||
|
},
|
||||||
|
"control_plane_source_oid": "b781129bd5a8b53c05ab648361424d2800166ee4",
|
||||||
|
"diff_stat": "active-runtime/app/aios_authority_git_executor.py | 24 +\n active-runtime/app/aios_chatgpt_actions_api.py | 48 +-\n active-runtime/app/aios_chatgpt_bridge_v2.py | 167 ++++\n .../app/aios_chatgpt_bridge_v2_server.py | 431 +++++++++\n active-runtime/app/aios_continuation_executor.py | 594 ++++++++++++\n active-runtime/app/aios_continuation_lease.py | 6 +\n active-runtime/app/aios_control_plane_snapshot.py | 26 +\n active-runtime/app/aios_credential_broker.py | 78 ++\n active-runtime/app/aios_gitea_publisher.py | 93 ++\n active-runtime/app/aios_gitops_job_controller.py | 16 +-\n .../app/aios_gitops_reconciliation_core.py | 80 ++\n active-runtime/app/aios_governed_policy.py | 59 ++\n active-runtime/app/aios_governed_task_bootstrap.py | 84 ++\n active-runtime/app/aios_model_api_staging.py | 1022 ++++++++++++++++++++\n .../app/authorized_task_writer_provisioning.py | 4 +-\n active-runtime/app/baseline_authority.py | 84 ++\n active-runtime/app/governed_production_release.py | 568 +++++++++++\n active-runtime/app/v2r2c1_production_adapter.py | 135 +++\n .../systemd/aios-chatgpt-bridge-v2-staging.service | 23 +\n .../aios-gitops-model-api-v2r2c1-staging.service | 51 +\n ...09-bridge-v2.1-durable-reconciliation-design.md | 15 +\n ...t-bridge-v2-control-plane-convergence-design.md | 150 +++\n pytest.ini | 4 +\n scripts/build_v2r2c1_model_api_staging.py | 145 +++\n .../aios_release_controller/manifest.py | 4 +-\n .../test_v2r1c6_release_consistency.py | 4 +-\n .../aios_v2_candidate/test_v2r1c6_release_ready.py | 16 +-\n tests/conftest.py | 65 ++\n tests/test_bridge_v2_governed_http_bootstrap.py | 90 ++\n tests/test_governed_local_writer.py | 6 +-\n tests/test_model_api_immutable_staging.py | 35 +\n 31 files changed, 4108 insertions(+), 19 deletions(-)",
|
||||||
|
"evidence_closure_oid": "a8b906d1bc24088c19647e98de6a679268f6d0a2",
|
||||||
|
"forbidden_operations": [
|
||||||
|
"direct_main_push",
|
||||||
|
"main_merge_without_owner_approval",
|
||||||
|
"force_push",
|
||||||
|
"tag_mutation",
|
||||||
|
"baseline_mutation",
|
||||||
|
"registry_mutation",
|
||||||
|
"production_restart",
|
||||||
|
"production_cutover",
|
||||||
|
"live_model_api_cutover",
|
||||||
|
"history_rewrite",
|
||||||
|
"secret_commit"
|
||||||
|
],
|
||||||
|
"governance": {
|
||||||
|
"baseline_modified": false,
|
||||||
|
"direct_main_push": false,
|
||||||
|
"force_push_used": false,
|
||||||
|
"history_rewritten": false,
|
||||||
|
"live_model_api_cutover": false,
|
||||||
|
"main_merged": false,
|
||||||
|
"owner_approval_respected": true,
|
||||||
|
"production_changed": false,
|
||||||
|
"production_restarted": false,
|
||||||
|
"registry_modified": false,
|
||||||
|
"secrets_committed": false,
|
||||||
|
"tag_rewritten": false
|
||||||
|
},
|
||||||
|
"included_path_count": 31,
|
||||||
|
"integration_branch": "agent/v2r2c1-post-release-mainline-convergence-20260809",
|
||||||
|
"integration_head_oid": "2c6293160ed567a2098715edbb296741577c5bff",
|
||||||
|
"logical_commits": [
|
||||||
|
{
|
||||||
|
"message": "feat(v2r2c1): converge durable control-plane core",
|
||||||
|
"oid": "40b685d1c70c767c7a79a2b184348e58e3e319ae",
|
||||||
|
"parent_oid": "2e8fa22989808a448065d1d76b5b18948774a637"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"message": "docs(v2r2c1): converge durable runtime and design sources",
|
||||||
|
"oid": "5f616311bab38b0dac296ed8b0e69660e031a20e",
|
||||||
|
"parent_oid": "40b685d1c70c767c7a79a2b184348e58e3e319ae"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"message": "feat(v2r2c1): integrate governed release overlay",
|
||||||
|
"oid": "57780e316e320a5128cc2c9f12adbb19ae57aef5",
|
||||||
|
"parent_oid": "5f616311bab38b0dac296ed8b0e69660e031a20e"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"message": "feat(v2r2c1): wire governed release operations into durable bridge",
|
||||||
|
"oid": "e41b8d323a3e7a40440f5d32221f524074bea5fe",
|
||||||
|
"parent_oid": "57780e316e320a5128cc2c9f12adbb19ae57aef5"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"message": "test: repair canonical pytest repository entrypoint",
|
||||||
|
"oid": "b20cb8130e4c68267d5d85b02eed51cb8d4f41c0",
|
||||||
|
"parent_oid": "e41b8d323a3e7a40440f5d32221f524074bea5fe"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"message": "test: make whole-suite collection deterministic",
|
||||||
|
"oid": "c6a1693efaede9dace9b929d884a09e162a02c89",
|
||||||
|
"parent_oid": "b20cb8130e4c68267d5d85b02eed51cb8d4f41c0"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"message": "test: preserve legacy suite imports under canonical collection",
|
||||||
|
"oid": "91f2ea0591787d3dbf8c2e27ab0460629d86900e",
|
||||||
|
"parent_oid": "c6a1693efaede9dace9b929d884a09e162a02c89"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"message": "test: expose repository test helpers to canonical collection",
|
||||||
|
"oid": "85897d3f6d708271dff16767265f9d14b2f29801",
|
||||||
|
"parent_oid": "91f2ea0591787d3dbf8c2e27ab0460629d86900e"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"message": "fix(v2r2c1): close slash-branch and suite contract regressions",
|
||||||
|
"oid": "f1110d24f30d8441f2df04e0774ee230b95c2e02",
|
||||||
|
"parent_oid": "85897d3f6d708271dff16767265f9d14b2f29801"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"message": "test: align writer regression with durable idempotent replay",
|
||||||
|
"oid": "c10d2a5dfcc394dde7a7022952d7b423725747a3",
|
||||||
|
"parent_oid": "f1110d24f30d8441f2df04e0774ee230b95c2e02"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"message": "feat(v2r2c1): source baseline authority for model-api staging",
|
||||||
|
"oid": "f969aedaf61501a866435c712be01ff2cfe43fb1",
|
||||||
|
"parent_oid": "c10d2a5dfcc394dde7a7022952d7b423725747a3"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"message": "feat(v2r2c1): add immutable model api staging runtime",
|
||||||
|
"oid": "4ab70bfdb4d34aa9d1dabc4230a3367eaa93cc9b",
|
||||||
|
"parent_oid": "f969aedaf61501a866435c712be01ff2cfe43fb1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"message": "fix(v2r2c1): avoid builder secret-scan false positives",
|
||||||
|
"oid": "7c4e86bfc1f39e643e672968d1fdedbd72ace56d",
|
||||||
|
"parent_oid": "4ab70bfdb4d34aa9d1dabc4230a3367eaa93cc9b"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"message": "fix(v2r2c1): isolate model api staging port",
|
||||||
|
"oid": "7564363a30bfa7fa81ac5c7c545ec704984ec543",
|
||||||
|
"parent_oid": "7c4e86bfc1f39e643e672968d1fdedbd72ace56d"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"message": "fix(v2r2c1): bind baseline authority and openapi staging path",
|
||||||
|
"oid": "2c6293160ed567a2098715edbb296741577c5bff",
|
||||||
|
"parent_oid": "7564363a30bfa7fa81ac5c7c545ec704984ec543"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"main_base_oid": "2e8fa22989808a448065d1d76b5b18948774a637",
|
||||||
|
"mandatory_gates": {
|
||||||
|
"baseline_registry_readback_staging_pass": true,
|
||||||
|
"durable_source_path_manifest_complete": true,
|
||||||
|
"existing_v2r2c1_suite_pass": true,
|
||||||
|
"governed_release_suite_pass": true,
|
||||||
|
"integration_authority_git_api_equal": true,
|
||||||
|
"integration_branch_governed": true,
|
||||||
|
"logical_slice_commits_remote_verified": true,
|
||||||
|
"main_base_exact_before_branch_create": true,
|
||||||
|
"model_api_immutable_runtime_staging_pass": true,
|
||||||
|
"owner_merge_request_generated_execute_false": true,
|
||||||
|
"production_unchanged": true,
|
||||||
|
"pytest_collection_entrypoints_fixed": true,
|
||||||
|
"pytest_tests_results_recorded": true
|
||||||
|
},
|
||||||
|
"model_api_staging": {
|
||||||
|
"artifact_sha256": "492338b895b152a55015fd92ea1d9dba2314894ab00c0ba219a78f3cef544b8a",
|
||||||
|
"baseline": {
|
||||||
|
"authority_sha256": "d8e23bad49bdb3e4192ba708c3dfe7fc5b7cdc6a513c2f87acd6412a51867231",
|
||||||
|
"current_baseline": "V2R2C1",
|
||||||
|
"deployment_state": "ACTIVE",
|
||||||
|
"evidence_closure_oid": "a8b906d1bc24088c19647e98de6a679268f6d0a2",
|
||||||
|
"generation": 4,
|
||||||
|
"previous_baseline": "V2R1C7",
|
||||||
|
"production_deployed": true,
|
||||||
|
"status": "PRODUCTION_DEPLOYED",
|
||||||
|
"tag_oid": "07fc2746b688f5f243f29b39de39f32edc3dd9d2",
|
||||||
|
"target_oid": "998d30304787e31e5ae607c0bd86b5edb2b0e5ad"
|
||||||
|
},
|
||||||
|
"baseline_status": 200,
|
||||||
|
"deployment_state": "STAGING",
|
||||||
|
"health": {
|
||||||
|
"artifact_sha256": "492338b895b152a55015fd92ea1d9dba2314894ab00c0ba219a78f3cef544b8a",
|
||||||
|
"commit_oid": "2c6293160ed567a2098715edbb296741577c5bff",
|
||||||
|
"deployment_state": "STAGING",
|
||||||
|
"governed_release_routing": "bridge-v2",
|
||||||
|
"legacy_global_writes_enabled": false,
|
||||||
|
"production_deployed": false,
|
||||||
|
"runtime_mode": "IMMUTABLE_STAGING",
|
||||||
|
"status": "ok",
|
||||||
|
"version": "V2R2C1"
|
||||||
|
},
|
||||||
|
"health_status": 200,
|
||||||
|
"legacy_write_probe": "423_BLOCKED",
|
||||||
|
"port": 18893,
|
||||||
|
"restart_acceptance": "PASS",
|
||||||
|
"runtime_root": "/opt/aios/gitops-model-api/2c6293160ed567a2098715edbb296741577c5bff/runtime",
|
||||||
|
"source_oid": "2c6293160ed567a2098715edbb296741577c5bff",
|
||||||
|
"systemd": "MainPID=788782\nExecStart={ path=/usr/bin/python3 ; argv[]=/usr/bin/python3 /opt/aios/gitops-model-api/2c6293160ed567a2098715edbb296741577c5bff/runtime/app/aios_model_api_staging.py ; ignore_errors=no ; start_time=[Sun 2026-08-09 19:11:19 CST] ; stop_time=[n/a] ; pid=788782 ; code=(null) ; status=0/0 }\nActiveState=active\nSubState=running",
|
||||||
|
"unit": "aios-gitops-model-api-v2r2c1-staging.service",
|
||||||
|
"versions": {
|
||||||
|
"authoritative_head_oid": "2e8fa22989808a448065d1d76b5b18948774a637",
|
||||||
|
"baseline_generation": 4,
|
||||||
|
"baseline_sha256": "d8e23bad49bdb3e4192ba708c3dfe7fc5b7cdc6a513c2f87acd6412a51867231",
|
||||||
|
"baseline_version": "V2R2C1",
|
||||||
|
"query_error": null
|
||||||
|
},
|
||||||
|
"versions_status": 200
|
||||||
|
},
|
||||||
|
"owner_merge_request": {
|
||||||
|
"control_plane_source_oid": "b781129bd5a8b53c05ab648361424d2800166ee4",
|
||||||
|
"execute": false,
|
||||||
|
"integration_head_oid": "b2cf510e11b712bb99f20ded81459b6524257394",
|
||||||
|
"main_expected_old_oid": "2e8fa22989808a448065d1d76b5b18948774a637",
|
||||||
|
"release_target_oid": "998d30304787e31e5ae607c0bd86b5edb2b0e5ad",
|
||||||
|
"report_commit_oid": "b2cf510e11b712bb99f20ded81459b6524257394",
|
||||||
|
"status": "OWNER_APPROVAL_REQUIRED"
|
||||||
|
},
|
||||||
|
"placeholder_fields": [],
|
||||||
|
"production": {
|
||||||
|
"baseline_mutation": false,
|
||||||
|
"health": {
|
||||||
|
"artifact_sha256": "ab4ae257346c82863f05abf5e309381a1c86612957608c3a4f4ab5cc9233f052",
|
||||||
|
"commit_oid": "998d30304787e31e5ae607c0bd86b5edb2b0e5ad",
|
||||||
|
"deployment_state": "ACTIVE",
|
||||||
|
"health_timestamp": "2026-08-09T11:21:16.872581+00:00",
|
||||||
|
"production_deployed": true,
|
||||||
|
"status": "ok",
|
||||||
|
"version": "V2R2C1"
|
||||||
|
},
|
||||||
|
"health_status": 200,
|
||||||
|
"live_model_api_show": "MainPID=2816107\nActiveState=active\nSubState=running",
|
||||||
|
"live_model_api_unchanged": true,
|
||||||
|
"production_cutover": false,
|
||||||
|
"production_restart": false,
|
||||||
|
"registry_mutation": false,
|
||||||
|
"shadow_service_unchanged": true,
|
||||||
|
"shadow_show": "MainPID=1505658\nActiveState=active\nSubState=running",
|
||||||
|
"tag_mutation": false
|
||||||
|
},
|
||||||
|
"release_target_oid": "998d30304787e31e5ae607c0bd86b5edb2b0e5ad",
|
||||||
|
"remote_proof": {
|
||||||
|
"api_branch_oid": "2c6293160ed567a2098715edbb296741577c5bff",
|
||||||
|
"api_verification": "paginated_exact_name",
|
||||||
|
"commit_visible": true,
|
||||||
|
"git_remote_oid": "2c6293160ed567a2098715edbb296741577c5bff",
|
||||||
|
"main_api_oid": "2e8fa22989808a448065d1d76b5b18948774a637",
|
||||||
|
"main_git_oid": "2e8fa22989808a448065d1d76b5b18948774a637"
|
||||||
|
},
|
||||||
|
"report_commit_parent_oid": "2c6293160ed567a2098715edbb296741577c5bff",
|
||||||
|
"repository": "aios/aios",
|
||||||
|
"schema": "aios.v2r2c1.post-release-mainline-convergence.acceptance.v1",
|
||||||
|
"status": "V2R2C1_MAINLINE_CONVERGENCE_READY_FOR_OWNER_MERGE",
|
||||||
|
"success_status": "V2R2C1_MAINLINE_CONVERGENCE_READY_FOR_OWNER_MERGE",
|
||||||
|
"task_id": "TASK-P0-V2R2C1-POST-RELEASE-MAINLINE-CONVERGENCE-20260809",
|
||||||
|
"tests": {
|
||||||
|
"collection": "PASS",
|
||||||
|
"command": "pytest tests",
|
||||||
|
"failed": 0,
|
||||||
|
"passed": 709,
|
||||||
|
"skipped": 2,
|
||||||
|
"skipped_tests": [
|
||||||
|
"tests/test_gitea_tag_publication_live.py (live opt-in)",
|
||||||
|
"tests/test_signer_contract_compatibility.py (live integration opt-in)"
|
||||||
|
],
|
||||||
|
"subtests": 154
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,89 @@
|
|||||||
|
# V2R2C1 post-release mainline convergence diff summary
|
||||||
|
|
||||||
|
- task: `TASK-P0-V2R2C1-POST-RELEASE-MAINLINE-CONVERGENCE-20260809`
|
||||||
|
- repository: `aios/aios`
|
||||||
|
- main base: `2e8fa22989808a448065d1d76b5b18948774a637`
|
||||||
|
- release target: `998d30304787e31e5ae607c0bd86b5edb2b0e5ad`
|
||||||
|
- control-plane source: `b781129bd5a8b53c05ab648361424d2800166ee4`
|
||||||
|
- implementation integration head before this report commit: `2c6293160ed567a2098715edbb296741577c5bff`
|
||||||
|
- branch: `agent/v2r2c1-post-release-mainline-convergence-20260809`
|
||||||
|
|
||||||
|
## Logical governed commits
|
||||||
|
|
||||||
|
| OID | Parent | Message |
|
||||||
|
|---|---|---|
|
||||||
|
| `40b685d1c70c767c7a79a2b184348e58e3e319ae` | `2e8fa22989808a448065d1d76b5b18948774a637` | feat(v2r2c1): converge durable control-plane core |
|
||||||
|
| `5f616311bab38b0dac296ed8b0e69660e031a20e` | `40b685d1c70c767c7a79a2b184348e58e3e319ae` | docs(v2r2c1): converge durable runtime and design sources |
|
||||||
|
| `57780e316e320a5128cc2c9f12adbb19ae57aef5` | `5f616311bab38b0dac296ed8b0e69660e031a20e` | feat(v2r2c1): integrate governed release overlay |
|
||||||
|
| `e41b8d323a3e7a40440f5d32221f524074bea5fe` | `57780e316e320a5128cc2c9f12adbb19ae57aef5` | feat(v2r2c1): wire governed release operations into durable bridge |
|
||||||
|
| `b20cb8130e4c68267d5d85b02eed51cb8d4f41c0` | `e41b8d323a3e7a40440f5d32221f524074bea5fe` | test: repair canonical pytest repository entrypoint |
|
||||||
|
| `c6a1693efaede9dace9b929d884a09e162a02c89` | `b20cb8130e4c68267d5d85b02eed51cb8d4f41c0` | test: make whole-suite collection deterministic |
|
||||||
|
| `91f2ea0591787d3dbf8c2e27ab0460629d86900e` | `c6a1693efaede9dace9b929d884a09e162a02c89` | test: preserve legacy suite imports under canonical collection |
|
||||||
|
| `85897d3f6d708271dff16767265f9d14b2f29801` | `91f2ea0591787d3dbf8c2e27ab0460629d86900e` | test: expose repository test helpers to canonical collection |
|
||||||
|
| `f1110d24f30d8441f2df04e0774ee230b95c2e02` | `85897d3f6d708271dff16767265f9d14b2f29801` | fix(v2r2c1): close slash-branch and suite contract regressions |
|
||||||
|
| `c10d2a5dfcc394dde7a7022952d7b423725747a3` | `f1110d24f30d8441f2df04e0774ee230b95c2e02` | test: align writer regression with durable idempotent replay |
|
||||||
|
| `f969aedaf61501a866435c712be01ff2cfe43fb1` | `c10d2a5dfcc394dde7a7022952d7b423725747a3` | feat(v2r2c1): source baseline authority for model-api staging |
|
||||||
|
| `4ab70bfdb4d34aa9d1dabc4230a3367eaa93cc9b` | `f969aedaf61501a866435c712be01ff2cfe43fb1` | feat(v2r2c1): add immutable model api staging runtime |
|
||||||
|
| `7c4e86bfc1f39e643e672968d1fdedbd72ace56d` | `4ab70bfdb4d34aa9d1dabc4230a3367eaa93cc9b` | fix(v2r2c1): avoid builder secret-scan false positives |
|
||||||
|
| `7564363a30bfa7fa81ac5c7c545ec704984ec543` | `7c4e86bfc1f39e643e672968d1fdedbd72ace56d` | fix(v2r2c1): isolate model api staging port |
|
||||||
|
| `2c6293160ed567a2098715edbb296741577c5bff` | `7564363a30bfa7fa81ac5c7c545ec704984ec543` | fix(v2r2c1): bind baseline authority and openapi staging path |
|
||||||
|
|
||||||
|
## Exact diff stat
|
||||||
|
|
||||||
|
```text
|
||||||
|
active-runtime/app/aios_authority_git_executor.py | 24 +
|
||||||
|
active-runtime/app/aios_chatgpt_actions_api.py | 48 +-
|
||||||
|
active-runtime/app/aios_chatgpt_bridge_v2.py | 167 ++++
|
||||||
|
.../app/aios_chatgpt_bridge_v2_server.py | 431 +++++++++
|
||||||
|
active-runtime/app/aios_continuation_executor.py | 594 ++++++++++++
|
||||||
|
active-runtime/app/aios_continuation_lease.py | 6 +
|
||||||
|
active-runtime/app/aios_control_plane_snapshot.py | 26 +
|
||||||
|
active-runtime/app/aios_credential_broker.py | 78 ++
|
||||||
|
active-runtime/app/aios_gitea_publisher.py | 93 ++
|
||||||
|
active-runtime/app/aios_gitops_job_controller.py | 16 +-
|
||||||
|
.../app/aios_gitops_reconciliation_core.py | 80 ++
|
||||||
|
active-runtime/app/aios_governed_policy.py | 59 ++
|
||||||
|
active-runtime/app/aios_governed_task_bootstrap.py | 84 ++
|
||||||
|
active-runtime/app/aios_model_api_staging.py | 1022 ++++++++++++++++++++
|
||||||
|
.../app/authorized_task_writer_provisioning.py | 4 +-
|
||||||
|
active-runtime/app/baseline_authority.py | 84 ++
|
||||||
|
active-runtime/app/governed_production_release.py | 568 +++++++++++
|
||||||
|
active-runtime/app/v2r2c1_production_adapter.py | 135 +++
|
||||||
|
.../systemd/aios-chatgpt-bridge-v2-staging.service | 23 +
|
||||||
|
.../aios-gitops-model-api-v2r2c1-staging.service | 51 +
|
||||||
|
...09-bridge-v2.1-durable-reconciliation-design.md | 15 +
|
||||||
|
...t-bridge-v2-control-plane-convergence-design.md | 150 +++
|
||||||
|
pytest.ini | 4 +
|
||||||
|
scripts/build_v2r2c1_model_api_staging.py | 145 +++
|
||||||
|
.../aios_release_controller/manifest.py | 4 +-
|
||||||
|
.../test_v2r1c6_release_consistency.py | 4 +-
|
||||||
|
.../aios_v2_candidate/test_v2r1c6_release_ready.py | 16 +-
|
||||||
|
tests/conftest.py | 65 ++
|
||||||
|
tests/test_bridge_v2_governed_http_bootstrap.py | 90 ++
|
||||||
|
tests/test_governed_local_writer.py | 6 +-
|
||||||
|
tests/test_model_api_immutable_staging.py | 35 +
|
||||||
|
31 files changed, 4108 insertions(+), 19 deletions(-)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Included durable source domains
|
||||||
|
|
||||||
|
- Bridge v2 adapter/server and governed HTTP contracts.
|
||||||
|
- Durable reconciliation core, continuation executor/lease, Authority Git CAS adapter, job controller compatibility.
|
||||||
|
- Gitea publisher/verifier, credential broker, governed policy, control-plane snapshot, release routing.
|
||||||
|
- Canonical pytest entrypoint/conftest and regression coverage.
|
||||||
|
- Source-controlled immutable model-api staging runtime, builder and systemd template.
|
||||||
|
- Selected design specifications required for durable mainline context.
|
||||||
|
|
||||||
|
## Explicitly excluded
|
||||||
|
|
||||||
|
- `main` ref mutation or merge without Owner approval.
|
||||||
|
- Production service, Registry, Baseline body/sidecar, production tags and mutable `/var/lib` state.
|
||||||
|
- Installed `/opt` artifacts, temporary staging transition state and task-local receipts/credentials.
|
||||||
|
- Production cutover/restart, force push, history rewrite and plaintext secrets.
|
||||||
|
|
||||||
|
## Verification
|
||||||
|
|
||||||
|
- `pytest tests`: **709 passed, 2 skipped, 154 subtests, 0 failed**.
|
||||||
|
- Staging model-api: immutable OID/digest bound, port 18893, health/base/ver/OpenAPI readback PASS, restart recovery PASS.
|
||||||
|
- Authority/Gitea Git/Gitea API commit equality and commit visibility PASS.
|
||||||
|
- Production remains V2R2C1 and was not restarted or cut over.
|
||||||
@@ -0,0 +1,254 @@
|
|||||||
|
{
|
||||||
|
"control_plane_source_oid": "b781129bd5a8b53c05ab648361424d2800166ee4",
|
||||||
|
"diff_stat": "active-runtime/app/aios_authority_git_executor.py | 24 +\n active-runtime/app/aios_chatgpt_actions_api.py | 48 +-\n active-runtime/app/aios_chatgpt_bridge_v2.py | 167 ++++\n .../app/aios_chatgpt_bridge_v2_server.py | 431 +++++++++\n active-runtime/app/aios_continuation_executor.py | 594 ++++++++++++\n active-runtime/app/aios_continuation_lease.py | 6 +\n active-runtime/app/aios_control_plane_snapshot.py | 26 +\n active-runtime/app/aios_credential_broker.py | 78 ++\n active-runtime/app/aios_gitea_publisher.py | 93 ++\n active-runtime/app/aios_gitops_job_controller.py | 16 +-\n .../app/aios_gitops_reconciliation_core.py | 80 ++\n active-runtime/app/aios_governed_policy.py | 59 ++\n active-runtime/app/aios_governed_task_bootstrap.py | 84 ++\n active-runtime/app/aios_model_api_staging.py | 1022 ++++++++++++++++++++\n .../app/authorized_task_writer_provisioning.py | 4 +-\n active-runtime/app/baseline_authority.py | 84 ++\n active-runtime/app/governed_production_release.py | 568 +++++++++++\n active-runtime/app/v2r2c1_production_adapter.py | 135 +++\n .../systemd/aios-chatgpt-bridge-v2-staging.service | 23 +\n .../aios-gitops-model-api-v2r2c1-staging.service | 51 +\n ...09-bridge-v2.1-durable-reconciliation-design.md | 15 +\n ...t-bridge-v2-control-plane-convergence-design.md | 150 +++\n pytest.ini | 4 +\n scripts/build_v2r2c1_model_api_staging.py | 145 +++\n .../aios_release_controller/manifest.py | 4 +-\n .../test_v2r1c6_release_consistency.py | 4 +-\n .../aios_v2_candidate/test_v2r1c6_release_ready.py | 16 +-\n tests/conftest.py | 65 ++\n tests/test_bridge_v2_governed_http_bootstrap.py | 90 ++\n tests/test_governed_local_writer.py | 6 +-\n tests/test_model_api_immutable_staging.py | 35 +\n 31 files changed, 4108 insertions(+), 19 deletions(-)",
|
||||||
|
"evidence_closure_oid": "a8b906d1bc24088c19647e98de6a679268f6d0a2",
|
||||||
|
"excluded_paths": [
|
||||||
|
{
|
||||||
|
"path": "refs/heads/main",
|
||||||
|
"reason": "main merge requires separate Owner approval; unchanged"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "refs/tags/*",
|
||||||
|
"reason": "formal tags are outside engineering convergence scope; unchanged"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "baseline/current.json and sidecar",
|
||||||
|
"reason": "production mutable authority and sidecar; read-only verified"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "/var/lib/aios-gitops/**",
|
||||||
|
"reason": "host runtime state and receipts excluded from repository source"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "/opt/aios/**",
|
||||||
|
"reason": "installed immutable staging artifacts excluded; digest recorded in reports"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "credentials and secrets",
|
||||||
|
"reason": "availability-only; no plaintext credential committed"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "production runtime and Registry",
|
||||||
|
"reason": "production cutover/registry mutation forbidden"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"included_paths": [
|
||||||
|
{
|
||||||
|
"path": "active-runtime/app/aios_authority_git_executor.py",
|
||||||
|
"status": "M"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "active-runtime/app/aios_chatgpt_actions_api.py",
|
||||||
|
"status": "M"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "active-runtime/app/aios_chatgpt_bridge_v2.py",
|
||||||
|
"status": "A"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "active-runtime/app/aios_chatgpt_bridge_v2_server.py",
|
||||||
|
"status": "A"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "active-runtime/app/aios_continuation_executor.py",
|
||||||
|
"status": "A"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "active-runtime/app/aios_continuation_lease.py",
|
||||||
|
"status": "M"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "active-runtime/app/aios_control_plane_snapshot.py",
|
||||||
|
"status": "A"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "active-runtime/app/aios_credential_broker.py",
|
||||||
|
"status": "A"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "active-runtime/app/aios_gitea_publisher.py",
|
||||||
|
"status": "A"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "active-runtime/app/aios_gitops_job_controller.py",
|
||||||
|
"status": "M"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "active-runtime/app/aios_gitops_reconciliation_core.py",
|
||||||
|
"status": "A"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "active-runtime/app/aios_governed_policy.py",
|
||||||
|
"status": "A"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "active-runtime/app/aios_governed_task_bootstrap.py",
|
||||||
|
"status": "A"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "active-runtime/app/aios_model_api_staging.py",
|
||||||
|
"status": "A"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "active-runtime/app/authorized_task_writer_provisioning.py",
|
||||||
|
"status": "M"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "active-runtime/app/baseline_authority.py",
|
||||||
|
"status": "A"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "active-runtime/app/governed_production_release.py",
|
||||||
|
"status": "A"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "active-runtime/app/v2r2c1_production_adapter.py",
|
||||||
|
"status": "A"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "deploy/systemd/aios-chatgpt-bridge-v2-staging.service",
|
||||||
|
"status": "A"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "deploy/systemd/aios-gitops-model-api-v2r2c1-staging.service",
|
||||||
|
"status": "A"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "docs/superpowers/specs/2026-08-09-bridge-v2.1-durable-reconciliation-design.md",
|
||||||
|
"status": "A"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "docs/superpowers/specs/2026-08-09-chatgpt-bridge-v2-control-plane-convergence-design.md",
|
||||||
|
"status": "A"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "pytest.ini",
|
||||||
|
"status": "A"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "scripts/build_v2r2c1_model_api_staging.py",
|
||||||
|
"status": "A"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "services/aios-release-controller/aios_release_controller/manifest.py",
|
||||||
|
"status": "M"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "tests/aios_v2_candidate/test_v2r1c6_release_consistency.py",
|
||||||
|
"status": "M"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "tests/aios_v2_candidate/test_v2r1c6_release_ready.py",
|
||||||
|
"status": "M"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "tests/conftest.py",
|
||||||
|
"status": "A"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "tests/test_bridge_v2_governed_http_bootstrap.py",
|
||||||
|
"status": "A"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "tests/test_governed_local_writer.py",
|
||||||
|
"status": "M"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "tests/test_model_api_immutable_staging.py",
|
||||||
|
"status": "A"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"integration_branch": "agent/v2r2c1-post-release-mainline-convergence-20260809",
|
||||||
|
"integration_head_oid": "2c6293160ed567a2098715edbb296741577c5bff",
|
||||||
|
"logical_commits": [
|
||||||
|
{
|
||||||
|
"message": "feat(v2r2c1): converge durable control-plane core",
|
||||||
|
"oid": "40b685d1c70c767c7a79a2b184348e58e3e319ae",
|
||||||
|
"parent_oid": "2e8fa22989808a448065d1d76b5b18948774a637"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"message": "docs(v2r2c1): converge durable runtime and design sources",
|
||||||
|
"oid": "5f616311bab38b0dac296ed8b0e69660e031a20e",
|
||||||
|
"parent_oid": "40b685d1c70c767c7a79a2b184348e58e3e319ae"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"message": "feat(v2r2c1): integrate governed release overlay",
|
||||||
|
"oid": "57780e316e320a5128cc2c9f12adbb19ae57aef5",
|
||||||
|
"parent_oid": "5f616311bab38b0dac296ed8b0e69660e031a20e"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"message": "feat(v2r2c1): wire governed release operations into durable bridge",
|
||||||
|
"oid": "e41b8d323a3e7a40440f5d32221f524074bea5fe",
|
||||||
|
"parent_oid": "57780e316e320a5128cc2c9f12adbb19ae57aef5"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"message": "test: repair canonical pytest repository entrypoint",
|
||||||
|
"oid": "b20cb8130e4c68267d5d85b02eed51cb8d4f41c0",
|
||||||
|
"parent_oid": "e41b8d323a3e7a40440f5d32221f524074bea5fe"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"message": "test: make whole-suite collection deterministic",
|
||||||
|
"oid": "c6a1693efaede9dace9b929d884a09e162a02c89",
|
||||||
|
"parent_oid": "b20cb8130e4c68267d5d85b02eed51cb8d4f41c0"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"message": "test: preserve legacy suite imports under canonical collection",
|
||||||
|
"oid": "91f2ea0591787d3dbf8c2e27ab0460629d86900e",
|
||||||
|
"parent_oid": "c6a1693efaede9dace9b929d884a09e162a02c89"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"message": "test: expose repository test helpers to canonical collection",
|
||||||
|
"oid": "85897d3f6d708271dff16767265f9d14b2f29801",
|
||||||
|
"parent_oid": "91f2ea0591787d3dbf8c2e27ab0460629d86900e"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"message": "fix(v2r2c1): close slash-branch and suite contract regressions",
|
||||||
|
"oid": "f1110d24f30d8441f2df04e0774ee230b95c2e02",
|
||||||
|
"parent_oid": "85897d3f6d708271dff16767265f9d14b2f29801"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"message": "test: align writer regression with durable idempotent replay",
|
||||||
|
"oid": "c10d2a5dfcc394dde7a7022952d7b423725747a3",
|
||||||
|
"parent_oid": "f1110d24f30d8441f2df04e0774ee230b95c2e02"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"message": "feat(v2r2c1): source baseline authority for model-api staging",
|
||||||
|
"oid": "f969aedaf61501a866435c712be01ff2cfe43fb1",
|
||||||
|
"parent_oid": "c10d2a5dfcc394dde7a7022952d7b423725747a3"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"message": "feat(v2r2c1): add immutable model api staging runtime",
|
||||||
|
"oid": "4ab70bfdb4d34aa9d1dabc4230a3367eaa93cc9b",
|
||||||
|
"parent_oid": "f969aedaf61501a866435c712be01ff2cfe43fb1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"message": "fix(v2r2c1): avoid builder secret-scan false positives",
|
||||||
|
"oid": "7c4e86bfc1f39e643e672968d1fdedbd72ace56d",
|
||||||
|
"parent_oid": "4ab70bfdb4d34aa9d1dabc4230a3367eaa93cc9b"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"message": "fix(v2r2c1): isolate model api staging port",
|
||||||
|
"oid": "7564363a30bfa7fa81ac5c7c545ec704984ec543",
|
||||||
|
"parent_oid": "7c4e86bfc1f39e643e672968d1fdedbd72ace56d"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"message": "fix(v2r2c1): bind baseline authority and openapi staging path",
|
||||||
|
"oid": "2c6293160ed567a2098715edbb296741577c5bff",
|
||||||
|
"parent_oid": "7564363a30bfa7fa81ac5c7c545ec704984ec543"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"main_base_oid": "2e8fa22989808a448065d1d76b5b18948774a637",
|
||||||
|
"release_target_oid": "998d30304787e31e5ae607c0bd86b5edb2b0e5ad",
|
||||||
|
"remote_proof": {
|
||||||
|
"api_branch_oid": "2c6293160ed567a2098715edbb296741577c5bff",
|
||||||
|
"api_verification": "paginated_exact_name",
|
||||||
|
"commit_visible": true,
|
||||||
|
"git_remote_oid": "2c6293160ed567a2098715edbb296741577c5bff",
|
||||||
|
"main_api_oid": "2e8fa22989808a448065d1d76b5b18948774a637",
|
||||||
|
"main_git_oid": "2e8fa22989808a448065d1d76b5b18948774a637"
|
||||||
|
},
|
||||||
|
"report_commit_parent_oid": "2c6293160ed567a2098715edbb296741577c5bff",
|
||||||
|
"repository": "aios/aios",
|
||||||
|
"schema": "aios.v2r2c1.post-release-mainline-convergence.path-manifest.v1",
|
||||||
|
"task_id": "TASK-P0-V2R2C1-POST-RELEASE-MAINLINE-CONVERGENCE-20260809"
|
||||||
|
}
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
{
|
||||||
|
"artifact_sha256": "492338b895b152a55015fd92ea1d9dba2314894ab00c0ba219a78f3cef544b8a",
|
||||||
|
"baseline_authority_path": "/var/lib/aios-gitops-runtime-anchor/baseline/current.json",
|
||||||
|
"checks": {
|
||||||
|
"artifact_sha256": "PASS",
|
||||||
|
"baseline_registry_readback": "PASS",
|
||||||
|
"health_contract": "PASS",
|
||||||
|
"legacy_write_rejection": "PASS",
|
||||||
|
"live_service_unchanged": "PASS",
|
||||||
|
"manifest_binding": "PASS",
|
||||||
|
"openapi_readback": "PASS",
|
||||||
|
"restart_recovery": "PASS"
|
||||||
|
},
|
||||||
|
"current_live_port": 18890,
|
||||||
|
"current_live_service": "aios-gitops-model-api.service",
|
||||||
|
"deployment_state": "STAGING",
|
||||||
|
"execute": false,
|
||||||
|
"governed_release_routing": "bridge-v2",
|
||||||
|
"legacy_global_writes_enabled": false,
|
||||||
|
"live_cutover": false,
|
||||||
|
"owner_actions_required": [
|
||||||
|
"review exact source OID and artifact SHA",
|
||||||
|
"approve a separate future live model-api cutover",
|
||||||
|
"approve any production service transition"
|
||||||
|
],
|
||||||
|
"production_restart": false,
|
||||||
|
"repository_identity": "aios/aios",
|
||||||
|
"runtime_mode": "IMMUTABLE_STAGING",
|
||||||
|
"schema": "aios.v2r2c1.model-api-runtime-activation-plan.v1",
|
||||||
|
"source_oid": "2c6293160ed567a2098715edbb296741577c5bff",
|
||||||
|
"staging_port": 18893,
|
||||||
|
"staging_runtime_root": "/opt/aios/gitops-model-api/2c6293160ed567a2098715edbb296741577c5bff/runtime",
|
||||||
|
"staging_service": "aios-gitops-model-api-v2r2c1-staging.service",
|
||||||
|
"status": "OWNER_APPROVAL_REQUIRED",
|
||||||
|
"target_version": "V2R2C1",
|
||||||
|
"task_id": "TASK-P0-V2R2C1-POST-RELEASE-MAINLINE-CONVERGENCE-20260809"
|
||||||
|
}
|
||||||
@@ -0,0 +1,145 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
from __future__ import annotations
|
||||||
|
import argparse
|
||||||
|
import hashlib
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
from pathlib import Path
|
||||||
|
import tarfile
|
||||||
|
import tempfile
|
||||||
|
|
||||||
|
SCHEMA = 'aios.v2r2c1.model-api-staging-manifest.v1'
|
||||||
|
REQUIRED_APP = (
|
||||||
|
'aios_model_api_staging.py',
|
||||||
|
'aios_gitops_write_guard.py',
|
||||||
|
'authoritative_git.py',
|
||||||
|
'baseline_authority.py',
|
||||||
|
'gitea_tag_publication.py',
|
||||||
|
'silent_release_controller.py',
|
||||||
|
'silent_release_hardening.py',
|
||||||
|
'aios_gitops_job_controller.py',
|
||||||
|
)
|
||||||
|
OPTIONAL_APP = (
|
||||||
|
'aios_chatgpt_actions_api.py',
|
||||||
|
'aios_continuation_executor.py',
|
||||||
|
'aios_continuation_lease.py',
|
||||||
|
'aios_control_plane_snapshot.py',
|
||||||
|
)
|
||||||
|
|
||||||
|
def sha256(path: Path) -> str:
|
||||||
|
h = hashlib.sha256()
|
||||||
|
with path.open('rb') as fh:
|
||||||
|
for chunk in iter(lambda: fh.read(1024 * 1024), b''):
|
||||||
|
h.update(chunk)
|
||||||
|
return h.hexdigest()
|
||||||
|
|
||||||
|
def private_marker(path: Path) -> bool:
|
||||||
|
import re
|
||||||
|
text = path.read_text(encoding='utf-8', errors='ignore')
|
||||||
|
patterns = (
|
||||||
|
r'-----BEGIN [A-Z ]*PRIVATE KEY-----',
|
||||||
|
r'(?<![A-Za-z0-9])sk-[A-Za-z0-9_-]{20,}(?![A-Za-z0-9_-])',
|
||||||
|
r'(?<![A-Za-z0-9])ghp_[A-Za-z0-9]{20,}(?![A-Za-z0-9])',
|
||||||
|
r'(?i)aws_secret_access_key\s*[:=]\s*[A-Za-z0-9/+=]{16,}',
|
||||||
|
)
|
||||||
|
return any(re.search(pattern, text) for pattern in patterns)
|
||||||
|
|
||||||
|
def main() -> int:
|
||||||
|
ap = argparse.ArgumentParser()
|
||||||
|
ap.add_argument('--source-root', required=True, type=Path)
|
||||||
|
ap.add_argument('--output-root', required=True, type=Path)
|
||||||
|
ap.add_argument('--source-oid', required=True)
|
||||||
|
args = ap.parse_args()
|
||||||
|
oid = args.source_oid.lower()
|
||||||
|
if len(oid) != 40 or any(c not in '0123456789abcdef' for c in oid):
|
||||||
|
raise SystemExit('source oid must be exact 40 hex')
|
||||||
|
source = args.source_root.resolve(strict=True)
|
||||||
|
output = args.output_root.resolve()
|
||||||
|
app = source / 'active-runtime' / 'app'
|
||||||
|
runtime = source / 'runtime' / 'agent-os'
|
||||||
|
docs = source / 'docs' / 'gitops'
|
||||||
|
if not app.is_dir():
|
||||||
|
raise SystemExit('active-runtime/app missing')
|
||||||
|
paths = []
|
||||||
|
for name in REQUIRED_APP:
|
||||||
|
path = app / name
|
||||||
|
if not path.is_file():
|
||||||
|
raise SystemExit(f'required app dependency missing: {name}')
|
||||||
|
paths.append((Path('app') / name, path))
|
||||||
|
for name in OPTIONAL_APP:
|
||||||
|
path = app / name
|
||||||
|
if path.is_file():
|
||||||
|
paths.append((Path('app') / name, path))
|
||||||
|
for rel in ('orchestrator/__init__.py', 'orchestrator/action_monitor.py'):
|
||||||
|
path = runtime / rel
|
||||||
|
if not path.is_file():
|
||||||
|
raise SystemExit(f'required runtime dependency missing: {rel}')
|
||||||
|
paths.append((Path('runtime-agent-os') / rel, path))
|
||||||
|
openapi = docs / 'openapi-chatgpt-actions.yaml'
|
||||||
|
if openapi.is_file():
|
||||||
|
paths.append((Path('docs') / 'gitops' / 'openapi-chatgpt-actions.yaml', openapi))
|
||||||
|
for folder in ('config/model_registry', 'config/model_priority', 'patches', 'versions'):
|
||||||
|
root = source / folder
|
||||||
|
if root.is_dir():
|
||||||
|
for path in sorted(root.rglob('*')):
|
||||||
|
if path.is_file() and path.suffix.lower() in {'.json', '.yaml', '.yml', '.md'}:
|
||||||
|
paths.append((Path(folder) / path.relative_to(root), path))
|
||||||
|
for rel, path in paths:
|
||||||
|
if private_marker(path):
|
||||||
|
raise SystemExit(f'private marker rejected: {rel}')
|
||||||
|
included = [str(rel).replace('\\', '/') for rel, _ in paths]
|
||||||
|
manifest = {
|
||||||
|
'schema': SCHEMA,
|
||||||
|
'target_version': 'V2R2C1',
|
||||||
|
'source_oid': oid,
|
||||||
|
'repository_identity': 'aios/aios',
|
||||||
|
'deployment_state': 'STAGING',
|
||||||
|
'runtime_mode': 'IMMUTABLE_STAGING',
|
||||||
|
'runtime_entrypoint': 'app/aios_model_api_staging.py',
|
||||||
|
'legacy_global_writes_enabled': False,
|
||||||
|
'governed_release_routing': 'bridge-v2',
|
||||||
|
'artifact_sha256': 'provided-by-build-receipt-and-install-manifest',
|
||||||
|
'included_paths': included,
|
||||||
|
}
|
||||||
|
output.mkdir(parents=True, exist_ok=True)
|
||||||
|
archive = output / f'aios-v2r2c1-model-api-{oid}.tar.gz'
|
||||||
|
with tempfile.TemporaryDirectory(prefix='v2r2c1-model-api-', dir=str(output)) as tmp:
|
||||||
|
root = Path(tmp) / 'runtime'
|
||||||
|
for rel, path in paths:
|
||||||
|
target = root / rel
|
||||||
|
target.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
target.write_bytes(path.read_bytes())
|
||||||
|
(root / 'manifest.json').write_text(json.dumps(manifest, ensure_ascii=False, indent=2, sort_keys=True) + '\n', encoding='utf-8')
|
||||||
|
with tarfile.open(archive, 'w:gz', format=tarfile.PAX_FORMAT) as tf:
|
||||||
|
for path in sorted(root.rglob('*')):
|
||||||
|
rel = path.relative_to(root.parent)
|
||||||
|
info = tf.gettarinfo(str(path), arcname=str(rel).replace('\\', '/'))
|
||||||
|
info.mtime = 0
|
||||||
|
info.uid = 0
|
||||||
|
info.gid = 0
|
||||||
|
info.uname = 'root'
|
||||||
|
info.gname = 'root'
|
||||||
|
if path.is_file():
|
||||||
|
with path.open('rb') as fh:
|
||||||
|
tf.addfile(info, fh)
|
||||||
|
else:
|
||||||
|
tf.addfile(info)
|
||||||
|
digest = sha256(archive)
|
||||||
|
receipt = {
|
||||||
|
'schema': 'aios.v2r2c1.model-api-staging-build-receipt.v1',
|
||||||
|
'target_version': 'V2R2C1',
|
||||||
|
'source_oid': oid,
|
||||||
|
'repository_identity': 'aios/aios',
|
||||||
|
'artifact_path': str(archive),
|
||||||
|
'artifact_sha256': digest,
|
||||||
|
'deployment_state': 'STAGING',
|
||||||
|
'manifest_schema': SCHEMA,
|
||||||
|
'included_paths': included,
|
||||||
|
}
|
||||||
|
receipt_path = output / f'aios-v2r2c1-model-api-{oid}.receipt.json'
|
||||||
|
receipt_path.write_text(json.dumps(receipt, ensure_ascii=False, indent=2, sort_keys=True) + '\n', encoding='utf-8')
|
||||||
|
print(json.dumps(receipt, ensure_ascii=False, indent=2, sort_keys=True))
|
||||||
|
return 0
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
raise SystemExit(main())
|
||||||
@@ -2,7 +2,7 @@ from __future__ import annotations
|
|||||||
import hashlib,json,re,tarfile
|
import hashlib,json,re,tarfile
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any
|
from typing import Any
|
||||||
OID_RE=re.compile(r"^[0-9a-f]{40}$"); SHA_RE=re.compile(r"^[0-9a-f]{64}$"); VERSION_RE=re.compile(r"^V2R1C[0-9]+$")
|
OID_RE=re.compile(r"^[0-9a-f]{40}$"); SHA_RE=re.compile(r"^[0-9a-f]{64}$"); VERSION_RE=re.compile(r"^V[0-9]+R[0-9]+C[0-9]+(?:-[A-Za-z0-9_.-]+)?$")
|
||||||
REQUIRED_SOURCE={"schema","version","repository","base_ref","base_oid","binding_mode","release_readiness","production_deployed","tag_created","baseline_changed","production_service","production_port","rollback_metadata","controller_schema","required_checks"}
|
REQUIRED_SOURCE={"schema","version","repository","base_ref","base_oid","binding_mode","release_readiness","production_deployed","tag_created","baseline_changed","production_service","production_port","rollback_metadata","controller_schema","required_checks"}
|
||||||
REQUEST_KEYS={"schema","version","repository","base_oid","target_oid","artifact_uri","artifact_sha256","artifact_size","evidence_closure_oid","approval_plan_id","execute","production_deployed","production_service","production_port","rollback_metadata","current_tag_oid","source_manifest_sha256"}
|
REQUEST_KEYS={"schema","version","repository","base_oid","target_oid","artifact_uri","artifact_sha256","artifact_size","evidence_closure_oid","approval_plan_id","execute","production_deployed","production_service","production_port","rollback_metadata","current_tag_oid","source_manifest_sha256"}
|
||||||
class ManifestValidationError(ValueError):
|
class ManifestValidationError(ValueError):
|
||||||
@@ -62,7 +62,7 @@ def validate_release_request(source_manifest:dict[str,Any],request_path:Path,exp
|
|||||||
if not isinstance(request["approval_plan_id"],str) or not request["approval_plan_id"].startswith("sha256:"): raise ManifestValidationError("APPROVAL_PLAN","invalid approval plan")
|
if not isinstance(request["approval_plan_id"],str) or not request["approval_plan_id"].startswith("sha256:"): raise ManifestValidationError("APPROVAL_PLAN","invalid approval plan")
|
||||||
if request["execute"] is not False or request["production_deployed"] is not False: raise ManifestValidationError("EXECUTION_NOT_ALLOWED","must be non-executing")
|
if request["execute"] is not False or request["production_deployed"] is not False: raise ManifestValidationError("EXECUTION_NOT_ALLOWED","must be non-executing")
|
||||||
if request["production_service"]!=source_manifest["production_service"] or request["production_port"]!=source_manifest["production_port"]: raise ManifestValidationError("SERVICE_IDENTITY","request service mismatch")
|
if request["production_service"]!=source_manifest["production_service"] or request["production_port"]!=source_manifest["production_port"]: raise ManifestValidationError("SERVICE_IDENTITY","request service mismatch")
|
||||||
if not isinstance(request["current_tag_oid"],str) or request["current_tag_oid"] not in {"V2R1C6","9e65bd59baea392294c0f253046d3ab116f63145"}: raise ManifestValidationError("PROTECTED_TAG","previous tag binding changed")
|
if not isinstance(request["current_tag_oid"],str) or request["current_tag_oid"] not in {"V2R1C7","82792347937c0df59a8e02d6162775252bcad2e2"}: raise ManifestValidationError("PROTECTED_TAG","previous production tag binding changed")
|
||||||
_sha(request["source_manifest_sha256"],"source_manifest_sha256"); return request
|
_sha(request["source_manifest_sha256"],"source_manifest_sha256"); return request
|
||||||
def build_approval_request(*,source_manifest_path:Path,target_oid:str,artifact_uri:str,artifact_sha256:str,artifact_size:int,evidence_closure_oid:str,rollback_metadata:str,current_tag_oid:str)->dict[str,Any]:
|
def build_approval_request(*,source_manifest_path:Path,target_oid:str,artifact_uri:str,artifact_sha256:str,artifact_size:int,evidence_closure_oid:str,rollback_metadata:str,current_tag_oid:str)->dict[str,Any]:
|
||||||
source=_json_object(source_manifest_path); _oid(target_oid,"target_oid"); _sha(artifact_sha256,"artifact_sha256"); _oid(evidence_closure_oid,"evidence_closure_oid"); request={"schema":"aios.release-request.v2","version":source["version"],"repository":source["repository"],"base_oid":source["base_oid"],"target_oid":target_oid,"artifact_uri":artifact_uri,"artifact_sha256":artifact_sha256,"artifact_size":artifact_size,"evidence_closure_oid":evidence_closure_oid,"execute":False,"production_deployed":False,"production_service":source["production_service"],"production_port":source["production_port"],"rollback_metadata":rollback_metadata,"current_tag_oid":current_tag_oid,"source_manifest_sha256":sha256_file(source_manifest_path)}; request["approval_plan_id"]="sha256:"+sha256_bytes(_canonical(request)); return request
|
source=_json_object(source_manifest_path); _oid(target_oid,"target_oid"); _sha(artifact_sha256,"artifact_sha256"); _oid(evidence_closure_oid,"evidence_closure_oid"); request={"schema":"aios.release-request.v2","version":source["version"],"repository":source["repository"],"base_oid":source["base_oid"],"target_oid":target_oid,"artifact_uri":artifact_uri,"artifact_sha256":artifact_sha256,"artifact_size":artifact_size,"evidence_closure_oid":evidence_closure_oid,"execute":False,"production_deployed":False,"production_service":source["production_service"],"production_port":source["production_port"],"rollback_metadata":rollback_metadata,"current_tag_oid":current_tag_oid,"source_manifest_sha256":sha256_file(source_manifest_path)}; request["approval_plan_id"]="sha256:"+sha256_bytes(_canonical(request)); return request
|
||||||
|
|||||||
@@ -3,13 +3,13 @@ from __future__ import annotations
|
|||||||
import json
|
import json
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
ROOT = Path(__file__).parents[2]
|
ROOT = Path(__file__).parents[2]
|
||||||
|
|
||||||
|
|
||||||
def test_version_and_candidate_binding():
|
def test_version_and_candidate_binding():
|
||||||
assert (ROOT / "VERSION").read_text(encoding="utf-8").strip() == "V2R1C6"
|
assert (ROOT / "VERSION").read_text(encoding="utf-8").strip() == "V2R1C7"
|
||||||
metadata = json.loads((ROOT / "versions/V2R1C6.json").read_text(encoding="utf-8"))
|
metadata = json.loads((ROOT / "versions/V2R1C6.json").read_text(encoding="utf-8"))
|
||||||
|
assert metadata["version"] == "V2R1C6"
|
||||||
assert metadata["base_oid"] == "a33e2593967a180c89d44454350ccaeaf3f79b25"
|
assert metadata["base_oid"] == "a33e2593967a180c89d44454350ccaeaf3f79b25"
|
||||||
assert metadata["candidate_branch"] == "agent/v2r1c6-mainline-reconciliation-20260806"
|
assert metadata["candidate_branch"] == "agent/v2r1c6-mainline-reconciliation-20260806"
|
||||||
assert metadata["production_deployed"] is False
|
assert metadata["production_deployed"] is False
|
||||||
|
|||||||
@@ -7,17 +7,19 @@ import pytest
|
|||||||
|
|
||||||
ROOT = Path(__file__).resolve().parents[2]
|
ROOT = Path(__file__).resolve().parents[2]
|
||||||
sys.path.insert(0, str(ROOT / "active-runtime" / "app"))
|
sys.path.insert(0, str(ROOT / "active-runtime" / "app"))
|
||||||
|
from deployment_state import RuntimeDeploymentState
|
||||||
from v2r1c6_privacy_tools import health_payload, serve # noqa: E402
|
from v2r1c6_privacy_tools import health_payload, serve # noqa: E402
|
||||||
|
|
||||||
|
|
||||||
def test_health_is_engineering_only() -> None:
|
def test_health_is_engineering_only() -> None:
|
||||||
result = health_payload()
|
result = health_payload(RuntimeDeploymentState("V2R1C7", "a" * 40, "b" * 64, "STAGED"))
|
||||||
assert result == {
|
assert result["status"] == "ok"
|
||||||
"status": "ok",
|
assert result["version"] == "V2R1C7"
|
||||||
"version": "V2R1C6",
|
assert result["commit_oid"] == "a" * 40
|
||||||
"production_deployed": False,
|
assert result["artifact_sha256"] == "b" * 64
|
||||||
"gitops_writes_enabled": False,
|
assert result["deployment_state"] == "STAGED"
|
||||||
}
|
assert result["production_deployed"] is False
|
||||||
|
assert result["health_timestamp"]
|
||||||
|
|
||||||
|
|
||||||
def test_listener_rejects_non_loopback() -> None:
|
def test_listener_rejects_non_loopback() -> None:
|
||||||
|
|||||||
@@ -0,0 +1,65 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import importlib.util
|
||||||
|
import sys
|
||||||
|
import types
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
REPOSITORY_ROOT = Path(__file__).resolve().parents[1]
|
||||||
|
TEST_ROOT = REPOSITORY_ROOT / "tests"
|
||||||
|
SERVICE_PACKAGE_ROOT = REPOSITORY_ROOT / "services" / "aios-autonomous-release-candidate"
|
||||||
|
ACTIVE_RUNTIME_ROOT = REPOSITORY_ROOT / "active-runtime"
|
||||||
|
ACTIVE_RUNTIME_APP = ACTIVE_RUNTIME_ROOT / "app"
|
||||||
|
|
||||||
|
for path in (TEST_ROOT, SERVICE_PACKAGE_ROOT, ACTIVE_RUNTIME_APP):
|
||||||
|
value = str(path)
|
||||||
|
if value not in sys.path:
|
||||||
|
sys.path.insert(0, value)
|
||||||
|
|
||||||
|
|
||||||
|
def load_module(relative: str):
|
||||||
|
path = REPOSITORY_ROOT / relative
|
||||||
|
spec = importlib.util.spec_from_file_location("v2r1c6_privacy_tools", path)
|
||||||
|
if spec is None or spec.loader is None:
|
||||||
|
raise RuntimeError("module_load_failed")
|
||||||
|
module = importlib.util.module_from_spec(spec)
|
||||||
|
sys.modules[spec.name] = module
|
||||||
|
spec.loader.exec_module(module)
|
||||||
|
return module
|
||||||
|
|
||||||
|
|
||||||
|
def _install_active_runtime_namespace() -> None:
|
||||||
|
package = sys.modules.get("active_runtime")
|
||||||
|
if package is None:
|
||||||
|
package = types.ModuleType("active_runtime")
|
||||||
|
package.__path__ = [str(ACTIVE_RUNTIME_ROOT)]
|
||||||
|
package.__package__ = "active_runtime"
|
||||||
|
sys.modules["active_runtime"] = package
|
||||||
|
|
||||||
|
app = sys.modules.get("active_runtime.app")
|
||||||
|
if app is None:
|
||||||
|
app = types.ModuleType("active_runtime.app")
|
||||||
|
app.__path__ = [str(ACTIVE_RUNTIME_APP)]
|
||||||
|
app.__package__ = "active_runtime"
|
||||||
|
sys.modules["active_runtime.app"] = app
|
||||||
|
package.app = app
|
||||||
|
|
||||||
|
|
||||||
|
def _install_legacy_test_module(name: str, relative: str) -> None:
|
||||||
|
if name in sys.modules:
|
||||||
|
return
|
||||||
|
path = REPOSITORY_ROOT / relative
|
||||||
|
spec = importlib.util.spec_from_file_location(name, path)
|
||||||
|
if spec is None or spec.loader is None:
|
||||||
|
raise RuntimeError("legacy_test_module_load_failed:" + name)
|
||||||
|
module = importlib.util.module_from_spec(spec)
|
||||||
|
sys.modules[name] = module
|
||||||
|
spec.loader.exec_module(module)
|
||||||
|
|
||||||
|
|
||||||
|
_install_active_runtime_namespace()
|
||||||
|
sys.modules.setdefault("conftest", sys.modules[__name__])
|
||||||
|
_install_legacy_test_module(
|
||||||
|
"test_native_adapter_contracts",
|
||||||
|
"tests/autonomous_release/test_native_adapter_contracts.py",
|
||||||
|
)
|
||||||
@@ -0,0 +1,211 @@
|
|||||||
|
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
|
||||||
|
|
||||||
|
|
||||||
|
class Probe:
|
||||||
|
def __init__(self, present=False):
|
||||||
|
self.present = present
|
||||||
|
|
||||||
|
def branch_exists(self, _branch):
|
||||||
|
return self.present
|
||||||
|
|
||||||
|
def branch_head(self, _branch):
|
||||||
|
return "a" * 40
|
||||||
|
|
||||||
|
def is_ancestor(self, _older, _newer):
|
||||||
|
return True
|
||||||
|
|
||||||
|
def commit_exists(self, _oid):
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
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",
|
||||||
|
base_oid="a" * 40,
|
||||||
|
expected_branch_oid="a" * 40,
|
||||||
|
target_branch="agent/p0-bridge-v21-durable-reconciliation-20260809",
|
||||||
|
generation=1,
|
||||||
|
)
|
||||||
|
request.validate(Probe())
|
||||||
|
|
||||||
|
|
||||||
|
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",
|
||||||
|
base_oid="a" * 40,
|
||||||
|
expected_branch_oid="a" * 40,
|
||||||
|
target_branch="agent/existing-target",
|
||||||
|
generation=1,
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
request.validate(Probe(True))
|
||||||
|
except Exception as exc:
|
||||||
|
assert str(exc) == "TARGET_BRANCH_ALREADY_EXISTS"
|
||||||
|
else:
|
||||||
|
raise AssertionError("existing CREATE_NEW target must fail")
|
||||||
|
|
||||||
|
|
||||||
|
def test_slash_branch_uses_paginated_collection():
|
||||||
|
credential = ServerSideCredentialProjection(Path("/tmp/key"), Path("/tmp/token"), active=True)
|
||||||
|
calls = []
|
||||||
|
|
||||||
|
class FakeRemote(GiteaContinuationRemote):
|
||||||
|
def _api(self, path):
|
||||||
|
calls.append(path)
|
||||||
|
if "page=1" in path:
|
||||||
|
return [{"name": f"agent/other-{i}", "commit": {"id": "b" * 40}} for i in range(50)]
|
||||||
|
return [{"name": "agent/slash/branch", "commit": {"id": "c" * 40}}]
|
||||||
|
|
||||||
|
remote = FakeRemote(Path("/tmp"), credential)
|
||||||
|
assert remote.read_api_branch("agent/slash/branch") == "c" * 40
|
||||||
|
assert calls[0].endswith("branches?page=1&limit=50")
|
||||||
|
assert calls[1].endswith("branches?page=2&limit=50")
|
||||||
|
|
||||||
|
|
||||||
|
def test_http_route_contract_is_complete():
|
||||||
|
expected = {
|
||||||
|
"/v2/runtime", "/v2/capabilities", "/v2/governed-task",
|
||||||
|
"/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",
|
||||||
|
"/v2/tag/status",
|
||||||
|
"/v2/release/artifact",
|
||||||
|
"/v2/release/execute",
|
||||||
|
"/v2/release/consistency",
|
||||||
|
"/v2/release/acceptance",
|
||||||
|
}
|
||||||
|
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"]
|
||||||
@@ -250,7 +250,7 @@ class GovernedLocalWriterTests(unittest.TestCase):
|
|||||||
}
|
}
|
||||||
self.assertEqual(service.process_request(secret_request, peer)["status"], "error")
|
self.assertEqual(service.process_request(secret_request, peer)["status"], "error")
|
||||||
|
|
||||||
def test_one_shot_consumption(self) -> None:
|
def test_idempotent_replay_returns_receipt(self) -> None:
|
||||||
with tempfile.TemporaryDirectory() as root:
|
with tempfile.TemporaryDirectory() as root:
|
||||||
directory = Path(root)
|
directory = Path(root)
|
||||||
auth = self._load_auth(self._make_auth_payload(), directory)
|
auth = self._load_auth(self._make_auth_payload(), directory)
|
||||||
@@ -283,8 +283,8 @@ class GovernedLocalWriterTests(unittest.TestCase):
|
|||||||
|
|
||||||
self.assertEqual(service.process_request(create_request, peer)["status"], "ok")
|
self.assertEqual(service.process_request(create_request, peer)["status"], "ok")
|
||||||
self.assertEqual(service.process_request(append_request, peer)["status"], "ok")
|
self.assertEqual(service.process_request(append_request, peer)["status"], "ok")
|
||||||
self.assertEqual(service.process_request(append_request, peer)["status"], "error")
|
self.assertEqual(service.process_request(append_request, peer)["status"], "receipt")
|
||||||
self.assertEqual(len(sink.records), 3)
|
self.assertEqual(len(sink.records), 2)
|
||||||
self.assertEqual(len(handler.create_calls), 1)
|
self.assertEqual(len(handler.create_calls), 1)
|
||||||
self.assertEqual(len(handler.append_calls), 1)
|
self.assertEqual(len(handler.append_calls), 1)
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,35 @@
|
|||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
ROOT = Path(__file__).resolve().parents[1]
|
||||||
|
|
||||||
|
|
||||||
|
def test_model_api_source_is_immutable_and_authority_bound():
|
||||||
|
source = (ROOT / 'active-runtime' / 'app' / 'aios_model_api_staging.py').read_text(encoding='utf-8')
|
||||||
|
assert 'BaselineAuthority' in source
|
||||||
|
assert 'BASELINE_AUTHORITY = BaselineAuthority()' in source
|
||||||
|
assert 'LEGACY_GIT_WRITE_COMMANDS' in source
|
||||||
|
assert 'BaselineAuthorityError' in source
|
||||||
|
assert 'IMMUTABLE_STAGING' in source
|
||||||
|
assert 'LEGACY_GLOBAL_WRITES_DISABLED' in source
|
||||||
|
assert "'production_deployed': False" in source
|
||||||
|
assert "'governed_release_routing': 'bridge-v2'" in source
|
||||||
|
|
||||||
|
|
||||||
|
def test_model_api_staging_unit_is_non_production_and_read_only():
|
||||||
|
unit = (ROOT / 'deploy' / 'systemd' / 'aios-gitops-model-api-v2r2c1-staging.service').read_text(encoding='utf-8')
|
||||||
|
assert 'aios-gitops-model-api-v2r2c1-staging.service' in unit
|
||||||
|
assert 'AIOS_GITOPS_WRITES_ENABLED=false' in unit
|
||||||
|
assert 'AIOS_GITOPS_DURABLE_JOB_EXECUTION_ENABLED=false' in unit
|
||||||
|
assert 'AIOS_GITOPS_PORT=18893' in unit
|
||||||
|
assert 'aios_model_api_staging.py' in unit
|
||||||
|
assert 'production' not in unit.lower().replace('model-api-v2r2c1-staging', '')
|
||||||
|
|
||||||
|
|
||||||
|
def test_model_api_builder_manifest_contract():
|
||||||
|
builder = (ROOT / 'scripts' / 'build_v2r2c1_model_api_staging.py').read_text(encoding='utf-8')
|
||||||
|
assert 'aios.v2r2c1.model-api-staging-manifest.v1' in builder
|
||||||
|
assert "'target_version': 'V2R2C1'" in builder
|
||||||
|
assert "'repository_identity': 'aios/aios'" in builder
|
||||||
|
assert "'deployment_state': 'STAGING'" in builder
|
||||||
|
assert "'legacy_global_writes_enabled': False" in builder
|
||||||
|
assert 'private marker rejected' in builder
|
||||||
@@ -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) == ""
|
||||||
Reference in New Issue
Block a user