Compare commits
30
Commits
@@ -27,3 +27,36 @@ docs/CODEX_FULL_EXECUTION_PROMPT_V1R1C0.md
|
||||
```
|
||||
|
||||
本包是 V1R1C0 全量工程部署包,包含文档、脚本、迁移、接口适配器、测试用例。部署时应由 Codex 在服务器上定位当前生产包和运行目录,按增量方式叠加,不得破坏现有 WebOps/NPM/Casdoor/OpenClaw/Hermes。
|
||||
|
||||
## AIOS V2R2C1 Pre-Push Review 当前状态(语义一致性 follow-up)
|
||||
|
||||
本段针对最新 mechanism branch HEAD 重新核对,不把早期 b660e2c 文档快照当作当前代码状态。文档只写入非 main 的 Owner-review 分支,不能替代 Production Baseline、生产运行时或 main 合并。
|
||||
|
||||
| 项目 | 当前值 | 语义 |
|
||||
| --- | --- | --- |
|
||||
| Repository | aios/aios | 权威仓库身份 |
|
||||
| Authoritative Git | /data/aios-gitops/repo.git | 权威 bare repository |
|
||||
| Authoritative main HEAD | 3244f0f17f8845592ac94d585f5ebd65693c9f29 | 当前工程主线 |
|
||||
| Production Baseline | V2R2C1 / generation 4 | PRODUCTION_DEPLOYED / ACTIVE |
|
||||
| Baseline target | 998d30304787e31e5ae607c0bd86b5edb2b0e5ad | 当前生产发布目标 |
|
||||
| V2R2C1 tag object | 07fc2746b688f5f243f29b39de39f32edc3dd9d2 | annotated tag object |
|
||||
| Latest mechanism HEAD | 52b74ba61040246fffa8b10011c1199830e3d665 | 最新待审机制分支状态 |
|
||||
| Full audit scope | 17 paths, +1403/-79 | b660e2c → 51b5f0e → 52b74ba |
|
||||
| Latest-tip delta | 12 paths, +334/-39 | 51b5f0e → 52b74ba |
|
||||
| Documentation branch | agent/docs/v2r2c1-provenance-mainline-refresh-20260810 | 非 main Owner-review 分支 |
|
||||
| Branch HEAD before semantic repair | 869cc007649a39035f5b74513cecc55bb1ae673f | 本次 follow-up 的父提交 |
|
||||
| Documentation branch write/readback | COMPLETE after this follow-up | 仅针对文档分支 |
|
||||
| PR #65 | open / unmerged | Owner reviewer remains required |
|
||||
| Owner review | REQUIRED | review 后停止,不自动合并 |
|
||||
|
||||
文档分支状态与生产状态分开报告。mechanism/release production write、main merge、Production release、Baseline update、tag rewrite 和 force push 均为 NOT_RUN。legacy_global_writes_enabled 必须保持 false。
|
||||
|
||||
### 最新机制与验收事实
|
||||
|
||||
初始 b660e2c 修复覆盖 14 个文件;完整 b660e2c → 51b5f0e → 52b74ba 审计覆盖 17 个路径,净增量为 +1403/-79;最新 52b74ba 自身覆盖 12 个路径,净增量为 +334/-39。README、requirements、architecture、acceptance 和 version differences 已按这三个范围分别表述。
|
||||
|
||||
当前测试事实为 760 passed、2 skipped、1 failed、154 subtests passed。唯一失败为 tests/test_signer_contract_compatibility.py::test_real_signer_help_and_self_test_are_available。/usr/local/libexec/aios-grant-signer-adapter self-test 返回 42,两个独立重跑仍返回 42。因此 Full external gate 为 PARTIAL,Signer gate 为 FAIL / EXTERNAL,Production ready 保持 false。
|
||||
|
||||
当前架构模型必须同时表达 R、A、T、E、M、C、P:权威仓库身份、annotated tag object、peeled release target、evidence closure、pinned source-registry main commit、当前权威 main/reference commit 和 Owner-approved approval plan。还必须分别说明 source-registry 精确字节与 SHA-256、authority/approval binding、release/general artifact binding、executor 独立复核、runtime projection 与 Git convergence、post-deploy source-registry recheck 以及 fail-closed 状态。
|
||||
|
||||
五个文档均需通过 UTF-8 strict decode、CJK > 0、U+FFFD = 0、异常连续问号 = 0,并在提交后证明 source bytes SHA-256 与 Git blob 内容一致。任何生产 consequential action 仍需要独立 authority mechanism 与 Owner review。
|
||||
|
||||
Executable → Regular
+24
@@ -58,6 +58,11 @@ class AuthorityGitExecutor:
|
||||
value = self._git(["rev-parse", "--verify", ref], check=False)
|
||||
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:
|
||||
_oid(older_oid, "older_oid")
|
||||
_oid(newer_oid, "newer_oid")
|
||||
@@ -70,6 +75,25 @@ class AuthorityGitExecutor:
|
||||
raise AuthorityGitError("ANCESTRY_CHECK_FAILED")
|
||||
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 advance_task_branch_CAS(self, branch: str, expected_oid: str, new_oid: str) -> str:
|
||||
if not TASK_BRANCH_RE.fullmatch(branch):
|
||||
raise AuthorityGitError("TASK_BRANCH_SCOPE_DENIED")
|
||||
|
||||
Executable → Regular
+68
-6
@@ -1,8 +1,14 @@
|
||||
#!/usr/bin/env python3
|
||||
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 pathlib import Path
|
||||
from version_registry import (
|
||||
configured_authoritative_git_dir,
|
||||
configured_repository_identity,
|
||||
list_registry_versions,
|
||||
list_registry_versions_report,
|
||||
)
|
||||
from typing import Any
|
||||
|
||||
try:
|
||||
@@ -72,8 +78,8 @@ HOST = os.getenv('AIOS_GITOPS_HOST', '127.0.0.1')
|
||||
PORT = int(os.getenv('AIOS_GITOPS_PORT', '18890'))
|
||||
OPENAPI = WS / 'docs' / 'gitops' / 'openapi-chatgpt-actions.yaml'
|
||||
BARE_REPO = Path(os.getenv('AIOS_GITOPS_BARE_REPO', str(ROOT / 'runtime' / 'gitea-bin' / 'data' / 'repositories' / 'aios' / 'aios.git')))
|
||||
AUTHORITATIVE_GIT_DIR = Path(os.getenv('AIOS_GITOPS_AUTHORITATIVE_GIT_DIR')) if os.getenv('AIOS_GITOPS_AUTHORITATIVE_GIT_DIR') else None
|
||||
REPOSITORY_IDENTITY = os.getenv('AIOS_GITOPS_REPOSITORY_IDENTITY', '')
|
||||
AUTHORITATIVE_GIT_DIR = configured_authoritative_git_dir()
|
||||
REPOSITORY_IDENTITY = configured_repository_identity()
|
||||
SILENT_RELEASE_STATE_ROOT = Path(os.getenv(
|
||||
'AIOS_SILENT_RELEASE_STATE_ROOT',
|
||||
'/var/lib/aios-gitops/silent-release',
|
||||
@@ -404,6 +410,51 @@ class H(BaseHTTPRequestHandler):
|
||||
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
|
||||
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):
|
||||
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)
|
||||
@@ -421,6 +472,14 @@ class H(BaseHTTPRequestHandler):
|
||||
))
|
||||
return self.sendj(data)
|
||||
if path in ('/api/ver','/api/versions','/ver'):
|
||||
registry_report = list_registry_versions_report(
|
||||
WS,
|
||||
baseline_file=Path(os.getenv('AIOS_GITOPS_BASELINE_AUTHORITY', '/var/lib/aios-gitops-runtime-anchor/baseline/current.json')),
|
||||
# Do not invent an authoritative repository when authority
|
||||
# discovery is disabled/omitted. Preserve the unavailable
|
||||
# authority path (503) instead of turning it into a 404.
|
||||
repo_git_dir=AUTHORITATIVE_GIT_DIR,
|
||||
)
|
||||
snapshot = repository_snapshot(
|
||||
git_dir=AUTHORITATIVE_GIT_DIR,
|
||||
repository_identity=REPOSITORY_IDENTITY,
|
||||
@@ -429,7 +488,8 @@ class H(BaseHTTPRequestHandler):
|
||||
return self.sendj({
|
||||
'repository_available': False,
|
||||
'query_error': snapshot['query_error'],
|
||||
'registry_versions': [p.name for p in (WS/'versions').glob('*')],
|
||||
'registry_versions': registry_report['registry_versions'],
|
||||
**{key: registry_report[key] for key in ('effective_registry_versions', 'runtime_registry_versions', 'attested_projection_versions', 'attested_projection_version', 'source_registry_converged', 'runtime_registry_converged', 'runtime_bundle_contains_current_version', 'runtime_bundle_contains_version', 'registry_consistency_status')},
|
||||
'local_authoritative_tags': [],
|
||||
}, 503)
|
||||
try:
|
||||
@@ -438,10 +498,11 @@ class H(BaseHTTPRequestHandler):
|
||||
return self.sendj({
|
||||
'repository_available': False,
|
||||
'query_error': str(exc),
|
||||
'registry_versions': [p.name for p in (WS/'versions').glob('*')],
|
||||
'registry_versions': registry_report['registry_versions'],
|
||||
**{key: registry_report[key] for key in ('effective_registry_versions', 'runtime_registry_versions', 'attested_projection_versions', 'attested_projection_version', 'source_registry_converged', 'runtime_registry_converged', 'runtime_bundle_contains_current_version', 'runtime_bundle_contains_version', 'registry_consistency_status')},
|
||||
'local_authoritative_tags': [],
|
||||
}, 503)
|
||||
registry = [p.name for p in (WS/'versions').glob('*')]
|
||||
registry = registry_report['registry_versions']
|
||||
return self.sendj({
|
||||
'registry_versions': registry,
|
||||
'local_authoritative_tags': tags,
|
||||
@@ -451,6 +512,7 @@ class H(BaseHTTPRequestHandler):
|
||||
'versions_dir': registry,
|
||||
'head': snapshot['authoritative_head_oid'],
|
||||
'authoritative_head_oid': snapshot['authoritative_head_oid'],
|
||||
**{key: registry_report[key] for key in ('effective_registry_versions', 'runtime_registry_versions', 'attested_projection_versions', 'attested_projection_version', 'source_registry_converged', 'runtime_registry_converged', 'runtime_bundle_contains_current_version', 'runtime_bundle_contains_version', 'registry_consistency_status')},
|
||||
})
|
||||
if path in ('/api/diff','/diff'):
|
||||
frm=(q.get('from') or [''])[0]
|
||||
|
||||
@@ -0,0 +1,167 @@
|
||||
"""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",
|
||||
"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"),
|
||||
"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": (),
|
||||
}
|
||||
|
||||
|
||||
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),
|
||||
"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 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 {})
|
||||
@@ -0,0 +1,431 @@
|
||||
"""Loopback-only governed Bridge v2 HTTP gateway with durable handlers."""
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime, timedelta, timezone
|
||||
import base64
|
||||
import json
|
||||
import os
|
||||
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
||||
from pathlib import Path
|
||||
import re
|
||||
import sqlite3
|
||||
from typing import Any, Mapping
|
||||
|
||||
from aios_authority_git_executor import AuthorityGitExecutor
|
||||
from aios_chatgpt_bridge_v2 import BridgeV2Adapter, BridgeV2Error, OPERATION_REQUIRED_FIELDS
|
||||
from aios_continuation_executor import AuthorityGitAdapter, ContinuationCommit, ContinuationExecutor, ContinuationPermissions, ContinuationRuntimeBinding, DetachedGitWorkspace, GiteaContinuationRemote, ServerSideCredentialProjection
|
||||
from aios_continuation_lease import WriterLeaseRequestV2
|
||||
from aios_control_plane_snapshot import build_control_plane_snapshot
|
||||
from aios_gitops_job_controller import GitOpsJobController, JobStore
|
||||
from aios_governed_task_bootstrap import BootstrapError, GovernedTaskBootstrap
|
||||
from governed_production_release import build_executor_config
|
||||
|
||||
MAX_BODY = 2 * 1024 * 1024
|
||||
ROUTES = {
|
||||
"/v2/runtime": "getRuntimeStatus",
|
||||
"/v2/capabilities": "getGovernedWriteCapabilities",
|
||||
"/v2/governed-task": "createGovernedTask",
|
||||
"/v2/writer-lease": "acquireWriterLease",
|
||||
"/v2/commit-file": "commitFileGoverned",
|
||||
"/v2/commit-files": "commitFilesGoverned",
|
||||
"/v2/job": "getGitOpsJob",
|
||||
"/v2/job/resume": "resumeGitOpsJob",
|
||||
"/v2/job/reconcile": "reconcileGitOpsJob",
|
||||
"/v2/tag/approval-request": "createTagApprovalRequest",
|
||||
"/v2/tag/publish": "requestOrCreateTag",
|
||||
"/v2/tag/status": "getTagPublicationStatus",
|
||||
"/v2/release/artifact": "buildProductionArtifact",
|
||||
"/v2/release/execute": "executeApprovedProductionRelease",
|
||||
"/v2/release/consistency": "getReleaseConsistency",
|
||||
"/v2/release/acceptance": "runReleaseAcceptance",
|
||||
}
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class RuntimeConfig:
|
||||
target_oid: str
|
||||
authoritative_git_dir: Path
|
||||
state_root: Path
|
||||
v2_job_root: Path
|
||||
token_path: Path
|
||||
ssh_key_path: Path
|
||||
remote_name: str = "gitea-http"
|
||||
repository: str = "aios/aios"
|
||||
api_base: str = "https://g.cnnetmkt.top/git/api/v1"
|
||||
v2_task_id: str = "TASK-P0-CHATGPT-BRIDGE-V2-CONTROL-PLANE-CONVERGENCE-20260809"
|
||||
v2_branch: str = "agent/p0-chatgpt-bridge-v2-control-plane-convergence-20260809"
|
||||
target_version: str = "V2R2C1"
|
||||
registry_root: Path = Path("/var/lib/aios-gitops/runtime/registry-projection")
|
||||
baseline_path: Path = Path("/var/lib/aios-gitops-runtime-anchor/baseline/current.json")
|
||||
health_url: str = "http://127.0.0.1:18927/health"
|
||||
|
||||
|
||||
def _safe(value: str) -> str:
|
||||
return re.sub(r"[^A-Za-z0-9_.-]+", "-", value).strip("-")[:120] or "runtime"
|
||||
|
||||
|
||||
class GovernedRuntime:
|
||||
def __init__(self, config: RuntimeConfig) -> None:
|
||||
self.config = config
|
||||
config.state_root.mkdir(parents=True, exist_ok=True)
|
||||
self.authority = AuthorityGitExecutor(config.authoritative_git_dir, config.state_root / "worktrees")
|
||||
self.bootstrap = GovernedTaskBootstrap(self.authority, config.state_root, remote_name=config.remote_name, token_path=config.token_path, ssh_key_path=config.ssh_key_path, api_base=config.api_base)
|
||||
self.runtimes: dict[tuple[str, int], tuple[Any, GitOpsJobController, ContinuationExecutor]] = {}
|
||||
self.controllers: dict[str, GitOpsJobController] = {}
|
||||
self.bindings: dict[str, dict[str, Any]] = {}
|
||||
self.release = build_executor_config(config)
|
||||
metadata_root = config.state_root / "task-bindings"
|
||||
if metadata_root.is_dir():
|
||||
for metadata in metadata_root.glob("*.json"):
|
||||
try:
|
||||
value = json.loads(metadata.read_text(encoding="utf-8"))
|
||||
if isinstance(value, dict) and value.get("task_id"):
|
||||
self.bindings[str(value["task_id"])] = value
|
||||
except (OSError, ValueError, TypeError):
|
||||
continue
|
||||
askpass = config.state_root / "bridge-v2-askpass.sh"
|
||||
if not askpass.exists():
|
||||
askpass.write_text('#!/bin/sh\ncase "$1" in *Username*) printf "%s\\n" "$GITEA_MACHINE_ACTOR" ;; *) cat "$CREDENTIAL_FILE" ;; esac\n', encoding="utf-8")
|
||||
askpass.chmod(0o700)
|
||||
os.environ.setdefault("GIT_ASKPASS", str(askpass)); os.environ.setdefault("GIT_ASKPASS_REQUIRE", "force"); os.environ.setdefault("GIT_TERMINAL_PROMPT", "0"); os.environ.setdefault("GITEA_MACHINE_ACTOR", "aios-autonomy-agent")
|
||||
|
||||
def runtime_status(self) -> dict[str, Any]:
|
||||
return {
|
||||
"status": "active",
|
||||
"health": 200,
|
||||
"bridge_schema_version": "aios.chatgpt-gitops-bridge.v2",
|
||||
"control_plane_release_version": self.config.target_version,
|
||||
"control_plane_target_oid": self.config.target_oid,
|
||||
"runtime_release_commit": self.config.target_oid,
|
||||
"runtime_path": str(self.config.state_root),
|
||||
"artifact_sha256": os.environ.get("AIOS_CONTROL_PLANE_ARTIFACT_SHA256"),
|
||||
"deployment_state": "STAGING",
|
||||
"drift_status": "PASS",
|
||||
"repository": self.config.repository,
|
||||
"repository_identity": self.config.repository,
|
||||
"authoritative_git_dir": str(self.config.authoritative_git_dir),
|
||||
"legacy_global_writes_enabled": False,
|
||||
}
|
||||
|
||||
def _job_root(self, task_id: str) -> Path:
|
||||
return self.config.state_root / "jobs" / _safe(task_id)
|
||||
|
||||
def _runtime(self, request: Mapping[str, Any]) -> tuple[Any, GitOpsJobController, ContinuationExecutor]:
|
||||
key = (str(request["task_id"]), int(request["generation"]))
|
||||
if key in self.runtimes: return self.runtimes[key]
|
||||
lease = WriterLeaseRequestV2(task_id=str(request["task_id"]), repository=str(request["repository"]), branch_mode=str(request.get("branch_mode", "CONTINUE_EXISTING")), base_oid=str(request["base_oid"]), expected_branch_oid=str(request["expected_branch_oid"]), target_branch=str(request["target_branch"]), generation=int(request["generation"]), lease_seconds=int(request.get("lease_seconds", 900)))
|
||||
lease.validate(AuthorityGitAdapter(self.authority))
|
||||
worktree_path = self.config.state_root / "worktrees" / _safe(lease.task_id)
|
||||
if worktree_path.exists():
|
||||
workspace = DetachedGitWorkspace(worktree_path)
|
||||
else:
|
||||
self.authority.create_detached_worktree(lease.expected_branch_oid, _safe(lease.task_id), self.config.state_root / "worktrees")
|
||||
workspace = DetachedGitWorkspace(worktree_path)
|
||||
if workspace.head_oid() != lease.expected_branch_oid or not workspace.is_detached(): raise RuntimeError("WORKTREE_HEAD_MISMATCH")
|
||||
job_root = self._job_root(lease.task_id); controller = GitOpsJobController(JobStore(job_root, payload_root=job_root / "payloads")); credential = ServerSideCredentialProjection(self.config.ssh_key_path, self.config.token_path); remote = GiteaContinuationRemote(workspace=worktree_path, credential=credential, remote_name=self.config.remote_name, repository=lease.repository, api_base=self.config.api_base); executor = ContinuationExecutor(controller, AuthorityGitAdapter(self.authority), workspace, remote, credential)
|
||||
binding = ContinuationRuntimeBinding(request=lease, workspace_path=worktree_path, lease_not_before=datetime.now(timezone.utc) - timedelta(seconds=1), lease_expires_at=datetime.now(timezone.utc) + timedelta(seconds=lease.lease_seconds), original_receipt_valid=True, original_grant_consumed=True, permissions=ContinuationPermissions())
|
||||
result = (binding, controller, executor); self.runtimes[key] = result; self.controllers[lease.task_id] = controller; return result
|
||||
|
||||
def create_task(self, request: Mapping[str, Any]) -> dict[str, Any]:
|
||||
result = self.bootstrap.create(request); self.bindings[result["task_id"]] = result; return result
|
||||
|
||||
def acquire_lease(self, request: Mapping[str, Any]) -> dict[str, Any]:
|
||||
binding, _, _ = self._runtime(request)
|
||||
req = binding.request
|
||||
return {"schema": WriterLeaseRequestV2.schema, "task_id": req.task_id, "repository": req.repository, "branch_mode": req.branch_mode, "base_oid": req.base_oid, "expected_branch_oid": req.expected_branch_oid, "target_branch": req.target_branch, "generation": req.generation, "lease_seconds": req.lease_seconds, "create_target_branch": req.branch_mode == "CREATE_NEW", "worktree_mode": "detached_at_base_oid" if req.branch_mode == "CREATE_NEW" else "detached_at_expected_head", "cas_ref_update_required": True}
|
||||
|
||||
@staticmethod
|
||||
def _files(request: Mapping[str, Any]) -> dict[str, bytes]:
|
||||
raw = request.get("files") or {request["path"]: request["content_base64"]}; pairs = raw.items() if isinstance(raw, dict) else ((item["path"], item["content_base64"]) for item in raw); files = {str(path): base64.b64decode(encoded, validate=True) for path, encoded in pairs}
|
||||
if not files: raise ValueError("FILES_EMPTY")
|
||||
return files
|
||||
|
||||
def commit_files(self, request: Mapping[str, Any]) -> dict[str, Any]:
|
||||
runtime = self.runtimes.get((str(request["task_id"]), int(request["generation"])))
|
||||
if runtime is None: raise RuntimeError("LEASE_NOT_FOUND")
|
||||
binding, _, executor = runtime
|
||||
if request["target_branch"] != binding.request.target_branch or request["expected_branch_oid"] != binding.request.expected_branch_oid: raise RuntimeError("LEASE_BINDING_CONFLICT")
|
||||
commit = ContinuationCommit(idempotency_key=str(request["idempotency_key"]), files=self._files(request), message=str(request["message"]))
|
||||
executor.workspace.materialize_files(commit.files)
|
||||
return executor.execute(binding, commit)
|
||||
|
||||
def _controller_for_job(self, job_id: str) -> GitOpsJobController:
|
||||
for controller in self.controllers.values():
|
||||
try:
|
||||
controller.store.read(job_id)
|
||||
return controller
|
||||
except Exception:
|
||||
continue
|
||||
jobs_root = self.config.state_root / "jobs"
|
||||
if jobs_root.is_dir():
|
||||
for root in jobs_root.iterdir():
|
||||
if not root.is_dir():
|
||||
continue
|
||||
controller = GitOpsJobController(JobStore(root, payload_root=root / "payloads"))
|
||||
try:
|
||||
controller.store.read(job_id)
|
||||
self.controllers[str(job_id)] = controller
|
||||
return controller
|
||||
except Exception:
|
||||
continue
|
||||
raise RuntimeError("JOB_NOT_FOUND")
|
||||
|
||||
@staticmethod
|
||||
def _valid_oid(value: object) -> bool:
|
||||
return isinstance(value, str) and re.fullmatch(r"[0-9a-f]{40}", value) is not None
|
||||
|
||||
def _reconstruct_legacy_binding(
|
||||
self,
|
||||
row: Mapping[str, Any],
|
||||
controller: GitOpsJobController,
|
||||
) -> dict[str, Any]:
|
||||
raw = json.loads(row["request_json"])
|
||||
binding = raw.get("binding", raw)
|
||||
if not isinstance(binding, dict):
|
||||
raise RuntimeError("DURABLE_BINDING_INVALID")
|
||||
task_id = str(binding.get("task_id") or binding.get("campaign_id") or "").strip()
|
||||
repository = str(binding.get("repository") or "").strip()
|
||||
operation = str(binding.get("operation") or row.get("operation") or "").strip()
|
||||
branch = str(binding.get("target_branch") or binding.get("branch") or row.get("branch") or "").strip()
|
||||
expected = str(binding.get("expected_branch_oid") or row.get("expected_branch_oid") or "").strip()
|
||||
base = binding.get("base_oid") or binding.get("lineage_anchor_oid")
|
||||
if not base:
|
||||
with controller.store._connect() as connection:
|
||||
lease_row = connection.execute(
|
||||
"SELECT lineage_anchor_oid FROM branch_leases WHERE job_id=? ORDER BY revision DESC LIMIT 1",
|
||||
(str(row["job_id"]),),
|
||||
).fetchone()
|
||||
if lease_row is not None:
|
||||
base = lease_row["lineage_anchor_oid"]
|
||||
base = str(base or "").strip()
|
||||
branch_mode = str(
|
||||
binding.get("branch_mode")
|
||||
or ("CREATE_NEW" if operation.endswith("_CREATE_NEW") else "CONTINUE_EXISTING")
|
||||
).strip()
|
||||
try:
|
||||
generation = int(binding.get("generation") or 0)
|
||||
except (TypeError, ValueError) as exc:
|
||||
raise RuntimeError("DURABLE_BINDING_GENERATION_INVALID") from exc
|
||||
if (
|
||||
not task_id
|
||||
or repository != self.config.repository
|
||||
or not branch.startswith("agent/")
|
||||
or branch in {"main", "master"}
|
||||
or branch_mode not in {"CREATE_NEW", "CONTINUE_EXISTING"}
|
||||
or generation < 1
|
||||
or not self._valid_oid(base)
|
||||
or not self._valid_oid(expected)
|
||||
):
|
||||
raise RuntimeError("DURABLE_BINDING_INVALID")
|
||||
current = self.authority.read_ref("refs/heads/" + branch)
|
||||
if (
|
||||
current is None
|
||||
or not self.authority.commit_exists(base)
|
||||
or not self.authority.commit_exists(expected)
|
||||
or not self.authority.ancestry_check(base, current)
|
||||
or not self.authority.ancestry_check(expected, current)
|
||||
):
|
||||
raise RuntimeError("DURABLE_BINDING_LINEAGE_INVALID")
|
||||
return {
|
||||
"task_id": task_id,
|
||||
"repository": repository,
|
||||
"branch_mode": branch_mode,
|
||||
"base_oid": base,
|
||||
"expected_branch_oid": expected,
|
||||
"target_branch": branch,
|
||||
"generation": generation,
|
||||
"lease_seconds": int(binding.get("lease_seconds") or 900),
|
||||
}
|
||||
|
||||
def _executor_from_row(
|
||||
self,
|
||||
row: Mapping[str, Any],
|
||||
controller: GitOpsJobController,
|
||||
) -> ContinuationExecutor:
|
||||
request = self._reconstruct_legacy_binding(row, controller)
|
||||
lease = WriterLeaseRequestV2(**request)
|
||||
worktree_path = self.config.state_root / "worktrees" / _safe(lease.task_id)
|
||||
if not worktree_path.exists():
|
||||
current = self.authority.read_ref("refs/heads/" + lease.target_branch)
|
||||
if current is None:
|
||||
raise RuntimeError("DURABLE_WORKTREE_BRANCH_MISSING")
|
||||
self.authority.create_detached_worktree(
|
||||
current,
|
||||
_safe(lease.task_id),
|
||||
self.config.state_root / "worktrees",
|
||||
)
|
||||
workspace = DetachedGitWorkspace(worktree_path)
|
||||
credential = ServerSideCredentialProjection(self.config.ssh_key_path, self.config.token_path)
|
||||
remote = GiteaContinuationRemote(
|
||||
workspace=worktree_path,
|
||||
credential=credential,
|
||||
remote_name=self.config.remote_name,
|
||||
repository=lease.repository,
|
||||
api_base=self.config.api_base,
|
||||
)
|
||||
return ContinuationExecutor(
|
||||
controller,
|
||||
AuthorityGitAdapter(self.authority),
|
||||
workspace,
|
||||
remote,
|
||||
credential,
|
||||
)
|
||||
|
||||
def job(self, request: Mapping[str, Any], action: str) -> dict[str, Any]:
|
||||
job_id = str(request["job_id"])
|
||||
controller = self._controller_for_job(job_id)
|
||||
row = controller.store.read(job_id)
|
||||
if action == "get":
|
||||
return controller.store._receipt(row, duplicate=True).as_dict()
|
||||
if action == "resume":
|
||||
return controller.resume(job_id).as_dict()
|
||||
return self._executor_from_row(row, controller).reconcile(job_id)
|
||||
|
||||
def _snapshot_task(self) -> dict[str, Any]:
|
||||
candidates: list[dict[str, Any]] = []
|
||||
for value in self.bindings.values():
|
||||
task_id = str(value.get("task_id") or "").strip()
|
||||
branch = str(value.get("target_branch") or "").strip()
|
||||
if not task_id or not branch.startswith("agent/"):
|
||||
continue
|
||||
authority = self.authority.read_ref("refs/heads/" + branch)
|
||||
try:
|
||||
generation = int(value.get("generation") or 0)
|
||||
except (TypeError, ValueError):
|
||||
generation = 0
|
||||
reconciliation_state = "OBSERVATION_REQUIRED"
|
||||
db = self.config.state_root / "jobs" / _safe(task_id) / "state.sqlite3"
|
||||
if db.is_file():
|
||||
connection = sqlite3.connect(f"file:{db}?mode=ro", uri=True)
|
||||
try:
|
||||
lease = connection.execute(
|
||||
"SELECT MAX(generation) FROM branch_leases WHERE branch=?",
|
||||
(branch,),
|
||||
).fetchone()
|
||||
generation = max(generation, int((lease or (0,))[0] or 0))
|
||||
latest = connection.execute(
|
||||
"SELECT state, side_effect_state FROM jobs WHERE branch=? ORDER BY updated_at DESC LIMIT 1",
|
||||
(branch,),
|
||||
).fetchone()
|
||||
if latest is not None:
|
||||
reconciliation_state = (
|
||||
"COMPLETED"
|
||||
if latest[0] == "COMPLETED"
|
||||
else str(latest[1] or latest[0])
|
||||
)
|
||||
finally:
|
||||
connection.close()
|
||||
candidates.append(
|
||||
{
|
||||
"task_id": task_id,
|
||||
"branch": branch,
|
||||
"authority_branch_oid": authority,
|
||||
"gitea_git_oid": None,
|
||||
"gitea_api_oid": None,
|
||||
"commit_visible": None,
|
||||
"generation": generation,
|
||||
"reconciliation_state": reconciliation_state,
|
||||
}
|
||||
)
|
||||
if not candidates:
|
||||
return {}
|
||||
return max(
|
||||
candidates,
|
||||
key=lambda item: (int(item["generation"] or 0), bool(item["authority_branch_oid"])),
|
||||
)
|
||||
|
||||
def snapshot(self) -> dict[str, Any]:
|
||||
task = self._snapshot_task()
|
||||
return build_control_plane_snapshot(target_version=self.config.target_version, runtime=self.runtime_status(), repository={"identity": self.config.repository, "authority_git_dir": str(self.config.authoritative_git_dir), "authority_main_oid": self.authority.read_ref("refs/heads/main")}, production={"baseline_version": "V2R1C7", "target_oid": "c7adece1cc8beffe7c718d53c775898b45d6c7f8", "deployment_state": "ACTIVE"}, task=task, capabilities={"legacy_writes_enabled": False, "governed_writes_enabled": True, "force_push": False, "main_write": False, "tag_write": False, "baseline_write": False, "production_write": False}, credential_broker={"status": "SERVER_SIDE_ONLY", "provider": "FileCredentialProvider"})
|
||||
|
||||
def handlers(self) -> dict[str, Any]:
|
||||
return {
|
||||
"createGovernedTask": self.create_task,
|
||||
"acquireWriterLease": self.acquire_lease,
|
||||
"commitFileGoverned": lambda request: self.commit_files({**request, "files": {request["path"]: request["content_base64"]}}),
|
||||
"commitFilesGoverned": self.commit_files,
|
||||
"getGitOpsJob": lambda request: self.job(request, "get"),
|
||||
"resumeGitOpsJob": lambda request: self.job(request, "resume"),
|
||||
"reconcileGitOpsJob": lambda request: self.job(request, "reconcile"),
|
||||
"getControlPlaneSnapshot": lambda request: self.snapshot(),
|
||||
"createTagApprovalRequest": self.release.approval_request,
|
||||
"requestOrCreateTag": self.release.publish_tag,
|
||||
"getTagPublicationStatus": self.release.tag_status,
|
||||
"buildProductionArtifact": self.release.build_artifact,
|
||||
"executeApprovedProductionRelease": self.release.execute_request,
|
||||
"getReleaseConsistency": self.release.consistency,
|
||||
"runReleaseAcceptance": self.release.acceptance,
|
||||
}
|
||||
|
||||
|
||||
def build_governed_handlers(runtime_config: RuntimeConfig | Mapping[str, Any]) -> Mapping[str, Any]:
|
||||
if isinstance(runtime_config, Mapping): runtime_config = RuntimeConfig(**runtime_config)
|
||||
return GovernedRuntime(runtime_config).handlers()
|
||||
|
||||
|
||||
class _HTTPError(RuntimeError): pass
|
||||
|
||||
|
||||
class BridgeHandler(BaseHTTPRequestHandler):
|
||||
adapter: BridgeV2Adapter | None = None
|
||||
def log_message(self, *_args: Any) -> None: return
|
||||
def _json(self, value: Any, status: int = 200) -> None:
|
||||
raw = json.dumps(value, sort_keys=True, separators=(",", ":")).encode(); self.send_response(status); self.send_header("Content-Type", "application/json"); self.send_header("Content-Length", str(len(raw))); self.end_headers(); self.wfile.write(raw)
|
||||
def _body(self) -> dict[str, Any]:
|
||||
try: length = int(self.headers.get("Content-Length", "-1"))
|
||||
except ValueError as exc: raise _HTTPError("CONTENT_LENGTH_INVALID") from exc
|
||||
if length < 0 or length > MAX_BODY: raise _HTTPError("REQUEST_TOO_LARGE")
|
||||
value = json.loads(self.rfile.read(length).decode("utf-8"))
|
||||
if not isinstance(value, dict): raise _HTTPError("JSON_OBJECT_REQUIRED")
|
||||
return value
|
||||
def _allowed(self, operation: str, body: Mapping[str, Any]) -> None:
|
||||
allowed = set(OPERATION_REQUIRED_FIELDS[operation]) | {"schema", "lease_seconds", "create_target_branch", "worktree_mode", "cas_ref_update_required", "expected_branch_oid", "generation", "branch_mode"}
|
||||
if operation == "commitFilesGoverned": allowed.add("files")
|
||||
allowed.update({"owner_approval", "execute"})
|
||||
if set(body) - allowed: raise _HTTPError("UNKNOWN_FIELDS")
|
||||
def do_GET(self) -> None:
|
||||
if self.adapter is None: self._json({"error": "RUNTIME_UNAVAILABLE"}, 503); return
|
||||
if self.path == "/v2/runtime": self._json(self.adapter.getRuntimeStatus()); return
|
||||
if self.path == "/v2/capabilities": self._json(self.adapter.getGovernedWriteCapabilities()); return
|
||||
if self.path == "/v2/control-plane-snapshot": self._json(self.adapter.getControlPlaneSnapshot({})); return
|
||||
self._json({"error": "NOT_FOUND"}, 404)
|
||||
def do_POST(self) -> None:
|
||||
operation = ROUTES.get(self.path)
|
||||
if operation is None or operation in ("getRuntimeStatus", "getGovernedWriteCapabilities") or self.adapter is None: self._json({"error": "NOT_FOUND"}, 404); return
|
||||
try:
|
||||
body = self._body(); self._allowed(operation, body); self._json(getattr(self.adapter, operation)(body))
|
||||
except _HTTPError as exc: self._json({"error": str(exc)}, 413 if str(exc) == "REQUEST_TOO_LARGE" else 400)
|
||||
except (BootstrapError, ValueError, BridgeV2Error) as exc: self._json({"error": str(exc)}, 409 if "CONFLICT" in str(exc) else 400)
|
||||
except Exception as exc: self._json({"error": str(exc) or type(exc).__name__}, 409 if any(token in str(exc) for token in ("CONFLICT", "EXPECTATION", "MONOTONIC")) else 503)
|
||||
|
||||
|
||||
def serve(adapter: BridgeV2Adapter, host: str = "127.0.0.1", port: int = 18892) -> None:
|
||||
BridgeHandler.adapter = adapter; ThreadingHTTPServer((host, port), BridgeHandler).serve_forever()
|
||||
|
||||
|
||||
def main() -> None:
|
||||
state_root = Path("/var/lib/aios-gitops/bridge-v2-staging")
|
||||
token_path = Path(os.environ.get("AIOS_GITEA_TOKEN_FILE", "/run/credentials/aios-chatgpt-bridge-v2-staging.service/gitea_token"))
|
||||
ssh_key_path = Path(os.environ.get("AIOS_GITEA_SSH_KEY_PATH", "/home/aiman/.ssh/id_ed25519_codex_tmp"))
|
||||
os.environ["CREDENTIAL_FILE"] = str(token_path)
|
||||
os.environ.setdefault("GITEA_MACHINE_ACTOR", "aios-autonomy-agent")
|
||||
config = RuntimeConfig(
|
||||
os.environ.get("AIOS_CONTROL_PLANE_TARGET_OID", ""),
|
||||
Path("/data/aios-gitops/repo.git"),
|
||||
state_root,
|
||||
state_root / "jobs",
|
||||
token_path,
|
||||
ssh_key_path,
|
||||
remote_name=os.environ.get("AIOS_GITEA_REMOTE", "gitea-http"),
|
||||
api_base=os.environ.get("AIOS_GITEA_API_BASE", "https://g.cnnetmkt.top/git/api/v1"),
|
||||
target_version=os.environ.get("AIOS_CONTROL_PLANE_TARGET_VERSION", "V2R2C1"),
|
||||
)
|
||||
runtime = GovernedRuntime(config)
|
||||
serve(
|
||||
BridgeV2Adapter(runtime_status=runtime.runtime_status(), handlers=runtime.handlers()),
|
||||
port=int(os.environ.get("AIOS_BRIDGE_V2_PORT", "18892")),
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__": main()
|
||||
@@ -0,0 +1,594 @@
|
||||
"""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 compare_and_swap_branch(self, branch: str, expected: str, new: str) -> str: ...
|
||||
|
||||
|
||||
class RemotePort(Protocol):
|
||||
def repository_identity(self) -> str: ...
|
||||
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 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_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
|
||||
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) -> int:
|
||||
observed = self._observed_generations.get(branch, 0)
|
||||
with self.controller.store._connect() as connection:
|
||||
row = connection.execute(
|
||||
"SELECT MAX(generation) AS generation FROM branch_leases WHERE branch=?",
|
||||
(branch,),
|
||||
).fetchone()
|
||||
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,
|
||||
"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) -> None:
|
||||
request = binding.request
|
||||
normalized = validate_writer_lease_request_v2(self._request_mapping(request), self.authority)
|
||||
if normalized != {**self._request_mapping(request), "create_target_branch": False, "worktree_mode": "detached_at_expected_head", "cas_ref_update_required": True}:
|
||||
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):
|
||||
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) -> dict[str, object]:
|
||||
payload = commit.payload()
|
||||
submitted = self.controller.submit(self._job_request(binding, commit, payload), payload=payload)
|
||||
current = self.controller.store.read(submitted.job_id)
|
||||
if JobState(current["state"]) == JobState.COMPLETED:
|
||||
return self.controller.store._receipt(current, duplicate=True).as_dict()
|
||||
self._validate_preflight(binding)
|
||||
request = binding.request
|
||||
projected = False
|
||||
try:
|
||||
self.credential.project()
|
||||
projected = True
|
||||
row = self.controller.store.read(submitted.job_id)
|
||||
if JobState(row["state"]) == JobState.REQUESTED:
|
||||
self.controller.advance(
|
||||
submitted.job_id,
|
||||
JobState.AUTHORIZED,
|
||||
side_effect_state="AUTHORIZED",
|
||||
receipt={"task_id": request.task_id, "original_grant_consumed": True},
|
||||
)
|
||||
row = self.controller.store.read(submitted.job_id)
|
||||
if JobState(row["state"]) == JobState.AUTHORIZED:
|
||||
self.controller.lease_job(
|
||||
submitted.job_id,
|
||||
lease_id=f"lease-{submitted.job_id}",
|
||||
lineage_anchor_oid=request.base_oid,
|
||||
current_head_oid=request.expected_branch_oid,
|
||||
generation=request.generation,
|
||||
lease_expires_at=binding.lease_expires_at.astimezone(timezone.utc).isoformat(),
|
||||
expected_revision=int(row["revision"]),
|
||||
)
|
||||
self.controller.advance(submitted.job_id, JobState.WORKTREE_READY, side_effect_state="WORKTREE_READY")
|
||||
self.controller.advance(submitted.job_id, JobState.EXECUTING, side_effect_state="IN_PROGRESS")
|
||||
new_oid = self.workspace.commit_files(commit.files, commit.message)
|
||||
if not OID_RE.fullmatch(new_oid) or not self.authority.is_ancestor(request.expected_branch_oid, new_oid):
|
||||
raise ContinuationExecutionError("NON_FAST_FORWARD_COMMIT")
|
||||
authority_oid = self.authority.compare_and_swap_branch(request.target_branch, request.expected_branch_oid, new_oid)
|
||||
if authority_oid != new_oid:
|
||||
raise ContinuationExecutionError("AUTHORITY_CAS_READBACK_MISMATCH")
|
||||
self.controller.advance(
|
||||
submitted.job_id,
|
||||
JobState.LOCAL_VERIFIED,
|
||||
side_effect_state="LOCAL_VERIFIED",
|
||||
receipt={"authority_oid": authority_oid, "force_push": False},
|
||||
)
|
||||
try:
|
||||
self.controller.advance(submitted.job_id, JobState.REMOTE_SYNCING, side_effect_state="IN_PROGRESS")
|
||||
self.remote.push_exact(request.target_branch, request.expected_branch_oid, new_oid)
|
||||
except Exception:
|
||||
return self._remote_pending(submitted.job_id, authority_oid, new_oid)
|
||||
return self._finish_remote_verification(submitted.job_id, authority_oid, new_oid)
|
||||
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) -> 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)
|
||||
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()
|
||||
@@ -74,6 +74,12 @@ class WriterLeaseRequestV2:
|
||||
raise ContinuationError("EXISTING_BRANCH_OID_CONFLICT")
|
||||
if not probe.is_ancestor(self.base_oid, actual):
|
||||
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)
|
||||
|
||||
@@ -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)
|
||||
@@ -249,7 +249,8 @@ class JobStore:
|
||||
if state == JobState.COMPLETED:
|
||||
status, side = 200, "COMPLETED"
|
||||
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":
|
||||
status, side = 503, "UNKNOWN"
|
||||
else:
|
||||
@@ -280,10 +281,13 @@ class GitOpsJobController:
|
||||
# durable round trip and participate in idempotency. Arbitrary request
|
||||
# fields are intentionally discarded rather than persisted.
|
||||
optional = (
|
||||
"idempotency_key",
|
||||
"task_id",
|
||||
"new_branch_oid",
|
||||
"authorization_recovery_digest",
|
||||
"lineage_anchor_oid",
|
||||
"base_oid",
|
||||
"branch_mode",
|
||||
"generation",
|
||||
"expected_main_oid",
|
||||
"pr_number",
|
||||
@@ -314,7 +318,15 @@ class GitOpsJobController:
|
||||
raise JobError("PAYLOAD_REFERENCE_MISMATCH")
|
||||
if payload_ref is not None and (not isinstance(payload_size, int) or payload_size < 0):
|
||||
raise JobError("PAYLOAD_SIZE_INVALID")
|
||||
key = sha256_json(binding)
|
||||
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)
|
||||
job_id = "job-" + key[:32]
|
||||
durable_request = dict(binding)
|
||||
if payload_ref is not None:
|
||||
|
||||
@@ -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
@@ -31,6 +31,8 @@ import time
|
||||
from pathlib import Path
|
||||
from typing import Any, Mapping, Sequence
|
||||
|
||||
from version_registry import configured_authoritative_git_dir, verify_authoritative_repository
|
||||
|
||||
OID_RE = re.compile(r"^[0-9a-f]{40}$")
|
||||
SHA_RE = re.compile(r"^[0-9a-f]{64}$")
|
||||
PLAN_RE = re.compile(r"^sha256:[0-9a-f]{64}$")
|
||||
@@ -49,6 +51,11 @@ REQUIRED_V2R1C7_MEMBERS = {
|
||||
"payload/ARTIFACT-MANIFEST.json",
|
||||
"payload/ROLLBACK-METADATA.json",
|
||||
}
|
||||
REQUIRED_V2R2C1_MEMBERS = {
|
||||
"payload/versions/V2R2C1.json",
|
||||
}
|
||||
V2R2C1_TAG_OID = "07fc2746b688f5f243f29b39de39f32edc3dd9d2"
|
||||
DEFAULT_AUTHORITATIVE_GIT_DIR = Path("/data/aios-gitops/repo.git")
|
||||
|
||||
class ReleaseBlocked(RuntimeError):
|
||||
pass
|
||||
@@ -72,6 +79,13 @@ class Binding:
|
||||
production_service: str
|
||||
production_port: int
|
||||
repository_identity: str = "aios/aios"
|
||||
# Legacy V2R2C1 receipts intentionally omit tag_oid. Future release
|
||||
# bindings must provide it explicitly and may use a versioned schema.
|
||||
tag_oid: str | None = None
|
||||
binding_schema: str | None = None
|
||||
source_registry_main_oid: str | None = None
|
||||
source_registry_path: str | None = None
|
||||
source_registry_sha256: str | None = None
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
if not self.version or not OID_RE.fullmatch(self.target_oid):
|
||||
@@ -90,6 +104,32 @@ class Binding:
|
||||
raise ValueError("invalid production port")
|
||||
if self.repository_identity != "aios/aios":
|
||||
raise ValueError("unexpected repository identity")
|
||||
if self.tag_oid is not None and not OID_RE.fullmatch(self.tag_oid):
|
||||
raise ValueError("invalid annotated tag object OID")
|
||||
if self.binding_schema is not None and not self.binding_schema.startswith(
|
||||
"aios.release-binding."
|
||||
):
|
||||
raise ValueError("invalid release binding schema")
|
||||
provenance = (
|
||||
self.source_registry_main_oid,
|
||||
self.source_registry_sha256,
|
||||
)
|
||||
if any(item is not None for item in provenance):
|
||||
if not all(item is not None for item in provenance):
|
||||
raise ValueError("source registry provenance must be complete")
|
||||
if not OID_RE.fullmatch(str(self.source_registry_main_oid)):
|
||||
raise ValueError("invalid source registry main OID")
|
||||
if not SHA_RE.fullmatch(str(self.source_registry_sha256)):
|
||||
raise ValueError("invalid source registry SHA-256")
|
||||
path = self.source_registry_path or f"versions/{self.version}.json"
|
||||
candidate = Path(path)
|
||||
if (
|
||||
candidate.is_absolute()
|
||||
or ".." in candidate.parts
|
||||
or "\\" in path
|
||||
or not path.endswith(".json")
|
||||
):
|
||||
raise ValueError("invalid source registry path")
|
||||
|
||||
@property
|
||||
def tag(self) -> str:
|
||||
@@ -108,10 +148,22 @@ class Binding:
|
||||
"production_port": self.production_port,
|
||||
"repository_identity": self.repository_identity,
|
||||
}
|
||||
if self.tag_oid is not None:
|
||||
payload["tag_oid"] = self.tag_oid
|
||||
payload["binding_schema"] = self.binding_schema or "aios.release-binding.v2"
|
||||
if self.source_registry_main_oid is not None:
|
||||
payload.update(
|
||||
{
|
||||
"source_registry_main_oid": self.source_registry_main_oid,
|
||||
"source_registry_path": self.source_registry_path
|
||||
or f"versions/{self.version}.json",
|
||||
"source_registry_sha256": self.source_registry_sha256,
|
||||
}
|
||||
)
|
||||
return hashlib.sha256(canonical_json(payload)).hexdigest()
|
||||
|
||||
def request(self, operation: str) -> dict[str, Any]:
|
||||
return {
|
||||
request = {
|
||||
"schema": "aios.production-executor-request.v1",
|
||||
"operation": operation,
|
||||
"binding_fingerprint": self.fingerprint,
|
||||
@@ -127,6 +179,19 @@ class Binding:
|
||||
"production_port": self.production_port,
|
||||
"repository_identity": self.repository_identity,
|
||||
}
|
||||
if self.tag_oid is not None:
|
||||
request["tag_oid"] = self.tag_oid
|
||||
request["binding_schema"] = self.binding_schema or "aios.release-binding.v2"
|
||||
if self.source_registry_main_oid is not None:
|
||||
request.update(
|
||||
{
|
||||
"source_registry_main_oid": self.source_registry_main_oid,
|
||||
"source_registry_path": self.source_registry_path
|
||||
or f"versions/{self.version}.json",
|
||||
"source_registry_sha256": self.source_registry_sha256,
|
||||
}
|
||||
)
|
||||
return request
|
||||
|
||||
|
||||
def canonical_json(value: object) -> bytes:
|
||||
@@ -168,6 +233,23 @@ def run_git(git_dir: Path, *args: str, check: bool = True) -> subprocess.Complet
|
||||
)
|
||||
|
||||
|
||||
def run_git_bytes(
|
||||
git_dir: Path,
|
||||
*args: str,
|
||||
check: bool = True,
|
||||
) -> subprocess.CompletedProcess[bytes]:
|
||||
completed = subprocess.run(
|
||||
["git", f"--git-dir={git_dir}", *args],
|
||||
check=False,
|
||||
capture_output=True,
|
||||
timeout=30,
|
||||
env={"PATH": os.environ.get("PATH", "")},
|
||||
)
|
||||
if check and completed.returncode != 0:
|
||||
raise ReleaseBlocked(completed.stderr.decode("utf-8", "replace").strip())
|
||||
return completed
|
||||
|
||||
|
||||
def verify_git_lineage(git_dir: Path, binding: Binding) -> dict[str, Any]:
|
||||
authority_main = run_git(git_dir, "rev-parse", "refs/heads/main").stdout.strip()
|
||||
if not OID_RE.fullmatch(authority_main):
|
||||
@@ -209,7 +291,45 @@ def verify_git_lineage(git_dir: Path, binding: Binding) -> dict[str, Any]:
|
||||
if not target_in_evidence:
|
||||
raise ReleaseBlocked("approved target is not an ancestor of evidence closure")
|
||||
|
||||
return {
|
||||
source_main = None
|
||||
if binding.source_registry_main_oid is not None:
|
||||
source_main = binding.source_registry_main_oid
|
||||
source_type = run_git(git_dir, "cat-file", "-t", source_main).stdout.strip()
|
||||
if source_type != "commit":
|
||||
raise ReleaseBlocked("source_registry_main_oid is not a commit")
|
||||
target_in_source_main = run_git(
|
||||
git_dir,
|
||||
"merge-base",
|
||||
"--is-ancestor",
|
||||
binding.target_oid,
|
||||
source_main,
|
||||
check=False,
|
||||
).returncode == 0
|
||||
if not target_in_source_main:
|
||||
raise ManualOwnerReviewRequired(
|
||||
"approved target is not an ancestor of source registry main"
|
||||
)
|
||||
expected_tag_oid = binding.tag_oid
|
||||
if expected_tag_oid is None and binding.version == "V2R2C1":
|
||||
expected_tag_oid = V2R2C1_TAG_OID
|
||||
if expected_tag_oid is not None:
|
||||
local_tag = run_git(
|
||||
git_dir,
|
||||
"rev-parse",
|
||||
"--verify",
|
||||
f"refs/tags/{binding.version}",
|
||||
).stdout.strip()
|
||||
if local_tag != expected_tag_oid:
|
||||
raise ManualOwnerReviewRequired(
|
||||
"future release binding tag object differs from local tag"
|
||||
)
|
||||
if run_git(git_dir, "cat-file", "-t", local_tag).stdout.strip() != "tag":
|
||||
raise ReleaseBlocked("release tag is not annotated")
|
||||
peeled = run_git(git_dir, "rev-parse", "--verify", f"{local_tag}^{{commit}}").stdout.strip()
|
||||
if peeled != binding.target_oid:
|
||||
raise ManualOwnerReviewRequired("release tag peeled target mismatch")
|
||||
|
||||
result = {
|
||||
"authority_main_oid": authority_main,
|
||||
"target_oid": binding.target_oid,
|
||||
"target_is_ancestor_of_main": True,
|
||||
@@ -217,9 +337,231 @@ def verify_git_lineage(git_dir: Path, binding: Binding) -> dict[str, Any]:
|
||||
"evidence_object_type": evidence_type,
|
||||
"target_is_ancestor_of_evidence": True,
|
||||
}
|
||||
if source_main is not None:
|
||||
result["source_registry_main_oid"] = source_main
|
||||
result["target_is_ancestor_of_source_registry_main"] = True
|
||||
return result
|
||||
|
||||
|
||||
def verify_artifact(binding: Binding) -> dict[str, Any]:
|
||||
def _source_registry_git_provenance(
|
||||
git_dir: Path,
|
||||
binding: Binding,
|
||||
) -> dict[str, Any] | None:
|
||||
"""Read M:path and C:path independently of the artifact metadata."""
|
||||
if binding.source_registry_main_oid is None:
|
||||
return None
|
||||
verify_authoritative_repository(git_dir)
|
||||
source_main = binding.source_registry_main_oid
|
||||
source_path = binding.source_registry_path or f"versions/{binding.version}.json"
|
||||
authority_main = run_git(git_dir, "rev-parse", "--verify", "refs/heads/main").stdout.strip()
|
||||
if run_git(git_dir, "cat-file", "-t", source_main).stdout.strip() != "commit":
|
||||
raise ReleaseBlocked("source_registry_main_oid is not a commit")
|
||||
if run_git(git_dir, "cat-file", "-t", authority_main).stdout.strip() != "commit":
|
||||
raise ReleaseBlocked("Authority main OID is not a commit")
|
||||
if run_git(
|
||||
git_dir,
|
||||
"merge-base",
|
||||
"--is-ancestor",
|
||||
source_main,
|
||||
authority_main,
|
||||
check=False,
|
||||
).returncode != 0:
|
||||
raise ManualOwnerReviewRequired(
|
||||
"source registry main is not an ancestor of current Authority main"
|
||||
)
|
||||
source_blob = run_git_bytes(git_dir, "show", f"{source_main}:{source_path}").stdout
|
||||
current_blob = run_git_bytes(git_dir, "show", f"{authority_main}:{source_path}").stdout
|
||||
if source_blob != current_blob:
|
||||
raise ManualOwnerReviewRequired(
|
||||
"current Authority main source registry entry differs from pinned M"
|
||||
)
|
||||
try:
|
||||
source = json.loads(source_blob.decode("utf-8"))
|
||||
except (UnicodeDecodeError, json.JSONDecodeError) as exc:
|
||||
raise ReleaseBlocked("Git source registry entry is invalid") from exc
|
||||
expected_tag_oid = binding.tag_oid
|
||||
if expected_tag_oid is None and binding.version == "V2R2C1":
|
||||
expected_tag_oid = V2R2C1_TAG_OID
|
||||
expected = {
|
||||
"version": binding.version,
|
||||
"repository": "aios/aios",
|
||||
"tag_oid": expected_tag_oid,
|
||||
"target_oid": binding.target_oid,
|
||||
"peeled_target_oid": binding.target_oid,
|
||||
"evidence_closure_oid": binding.evidence_closure_oid,
|
||||
"approval_plan_id": binding.approval_plan_id,
|
||||
}
|
||||
if not isinstance(source, dict) or source.get("schema") not in {
|
||||
"aios.source-registry.v1",
|
||||
"aios.production-registry.v2",
|
||||
}:
|
||||
raise ReleaseBlocked("Git source registry schema is invalid")
|
||||
if expected_tag_oid is None or any(source.get(key) != value for key, value in expected.items()):
|
||||
raise ReleaseBlocked("Git source registry identity mismatch")
|
||||
if source.get("tag_object") != expected_tag_oid:
|
||||
raise ReleaseBlocked("Git source registry tag object mismatch")
|
||||
digest = hashlib.sha256(source_blob).hexdigest()
|
||||
if digest != binding.source_registry_sha256:
|
||||
raise ReleaseBlocked("Git source registry SHA-256 mismatch")
|
||||
return {
|
||||
"source_registry_main_oid": source_main,
|
||||
"current_main_oid": authority_main,
|
||||
"source_registry_path": source_path,
|
||||
"source_registry_bytes": source_blob,
|
||||
"source_registry_sha256": digest,
|
||||
"source_registry_identity_verified": True,
|
||||
"source_registry_main_converged": True,
|
||||
}
|
||||
|
||||
|
||||
def _verify_artifact_members(
|
||||
path: Path,
|
||||
required: set[str],
|
||||
binding: Binding | None = None,
|
||||
git_provenance: Mapping[str, Any] | None = None,
|
||||
) -> dict[str, Any]:
|
||||
"""Verify archive safety and independently verify packaged release data."""
|
||||
# Python tarfile may not have zstd support on the target host, so
|
||||
# decompress into a private temporary tar and inspect it there.
|
||||
with tempfile.TemporaryDirectory(prefix="aios-artifact-verify-") as temp_dir:
|
||||
tar_path = Path(temp_dir) / "release.tar"
|
||||
with tar_path.open("wb") as output:
|
||||
completed = subprocess.run(
|
||||
["zstd", "-dc", str(path)],
|
||||
check=False,
|
||||
stdout=output,
|
||||
stderr=subprocess.PIPE,
|
||||
timeout=60,
|
||||
env={"PATH": os.environ.get("PATH", "")},
|
||||
)
|
||||
if completed.returncode != 0:
|
||||
raise ReleaseBlocked("artifact zstd decompression failed")
|
||||
import tarfile
|
||||
|
||||
with tarfile.open(tar_path, "r:") as archive:
|
||||
members = archive.getmembers()
|
||||
names: set[str] = set()
|
||||
for member in members:
|
||||
candidate = Path(member.name)
|
||||
if (
|
||||
candidate.is_absolute()
|
||||
or ".." in candidate.parts
|
||||
or member.issym()
|
||||
or member.islnk()
|
||||
or not (member.isfile() or member.isdir())
|
||||
):
|
||||
raise ReleaseBlocked(f"unsafe artifact member: {member.name}")
|
||||
if member.name in names:
|
||||
raise ReleaseBlocked(f"duplicate artifact member: {member.name}")
|
||||
names.add(member.name)
|
||||
source_path = None
|
||||
if binding is not None and binding.version != "V2R1C7":
|
||||
source_path = binding.source_registry_path or f"versions/{binding.version}.json"
|
||||
required_members = set(required)
|
||||
if binding is not None and binding.source_registry_main_oid is not None:
|
||||
required_members.add("payload/BUILD-METADATA.json")
|
||||
if source_path is not None and binding is not None:
|
||||
required_members.add(f"payload/{source_path}")
|
||||
missing = sorted(required_members - names)
|
||||
if missing:
|
||||
raise ReleaseBlocked(
|
||||
"artifact missing required members: " + ",".join(missing)
|
||||
)
|
||||
verification: dict[str, Any] = {}
|
||||
if binding is not None and source_path is not None:
|
||||
member_name = f"payload/{source_path}"
|
||||
member = archive.getmember(member_name)
|
||||
stream = archive.extractfile(member)
|
||||
if stream is None:
|
||||
raise ReleaseBlocked("source registry member is unreadable")
|
||||
raw = stream.read()
|
||||
try:
|
||||
source = json.loads(raw.decode("utf-8"))
|
||||
except (UnicodeDecodeError, json.JSONDecodeError) as exc:
|
||||
raise ReleaseBlocked("source registry member is invalid") from exc
|
||||
canonical_source = (
|
||||
json.dumps(source, ensure_ascii=False, sort_keys=True, indent=2) + "\n"
|
||||
).encode("utf-8")
|
||||
if raw != canonical_source:
|
||||
raise ReleaseBlocked("source registry member is not canonical JSON")
|
||||
expected_tag_oid = binding.tag_oid
|
||||
if expected_tag_oid is None and binding.version == "V2R2C1":
|
||||
expected_tag_oid = V2R2C1_TAG_OID
|
||||
expected = {
|
||||
"version": binding.version,
|
||||
"repository": "aios/aios",
|
||||
"tag_oid": expected_tag_oid,
|
||||
"target_oid": binding.target_oid,
|
||||
"peeled_target_oid": binding.target_oid,
|
||||
"evidence_closure_oid": binding.evidence_closure_oid,
|
||||
"approval_plan_id": binding.approval_plan_id,
|
||||
}
|
||||
if expected_tag_oid is None:
|
||||
raise ReleaseBlocked("future release binding tag object is required")
|
||||
if not isinstance(source, dict) or any(
|
||||
source.get(key) != value for key, value in expected.items()
|
||||
):
|
||||
raise ReleaseBlocked("packaged source registry identity mismatch")
|
||||
if source.get("tag_object") != expected_tag_oid:
|
||||
raise ReleaseBlocked("packaged source registry tag object mismatch")
|
||||
source_digest = hashlib.sha256(raw).hexdigest()
|
||||
if git_provenance is not None:
|
||||
if raw != git_provenance["source_registry_bytes"]:
|
||||
raise ReleaseBlocked(
|
||||
"packaged source registry differs from Git M:path"
|
||||
)
|
||||
if source_digest != git_provenance["source_registry_sha256"]:
|
||||
raise ReleaseBlocked(
|
||||
"packaged source registry SHA-256 differs from Git M:path"
|
||||
)
|
||||
verification.update(
|
||||
{
|
||||
"source_registry_path": source_path,
|
||||
"source_registry_sha256": source_digest,
|
||||
"source_registry_identity_verified": True,
|
||||
}
|
||||
)
|
||||
if binding.source_registry_sha256 is not None:
|
||||
if source_digest != binding.source_registry_sha256:
|
||||
raise ReleaseBlocked("packaged source registry SHA-256 mismatch")
|
||||
metadata_member = archive.getmember("payload/BUILD-METADATA.json")
|
||||
metadata_stream = archive.extractfile(metadata_member)
|
||||
if metadata_stream is None:
|
||||
raise ReleaseBlocked("BUILD-METADATA.json is unreadable")
|
||||
try:
|
||||
metadata = json.loads(metadata_stream.read().decode("utf-8"))
|
||||
except (UnicodeDecodeError, json.JSONDecodeError) as exc:
|
||||
raise ReleaseBlocked("BUILD-METADATA.json is invalid") from exc
|
||||
metadata_expected = {
|
||||
"release_target_oid": binding.target_oid,
|
||||
"source_registry_main_oid": binding.source_registry_main_oid,
|
||||
"source_registry_path": source_path,
|
||||
"source_registry_sha256": binding.source_registry_sha256,
|
||||
}
|
||||
if not isinstance(metadata, dict) or any(
|
||||
metadata.get(key) != value
|
||||
for key, value in metadata_expected.items()
|
||||
):
|
||||
raise ReleaseBlocked("BUILD-METADATA source provenance mismatch")
|
||||
verification["build_metadata_provenance_verified"] = True
|
||||
if git_provenance is not None:
|
||||
verification.update(
|
||||
{
|
||||
"git_source_registry_member_verified": True,
|
||||
"git_source_registry_main_converged": True,
|
||||
"git_source_registry_sha256": git_provenance[
|
||||
"source_registry_sha256"
|
||||
],
|
||||
}
|
||||
)
|
||||
return verification
|
||||
|
||||
|
||||
def verify_artifact(
|
||||
binding: Binding,
|
||||
*,
|
||||
repository: Path | str | None = None,
|
||||
) -> dict[str, Any]:
|
||||
path = Path(binding.artifact_path)
|
||||
if path.stat().st_size != binding.artifact_size:
|
||||
raise ReleaseBlocked("artifact size mismatch")
|
||||
@@ -227,46 +569,32 @@ def verify_artifact(binding: Binding) -> dict[str, Any]:
|
||||
if actual != binding.artifact_sha256:
|
||||
raise ReleaseBlocked("artifact SHA-256 mismatch")
|
||||
|
||||
if binding.version == "V2R1C7":
|
||||
# Python tarfile may not have zstd support on the target host, so
|
||||
# decompress into a private temporary tar and inspect it there.
|
||||
with tempfile.TemporaryDirectory(prefix="aios-artifact-verify-") as temp_dir:
|
||||
tar_path = Path(temp_dir) / "release.tar"
|
||||
with tar_path.open("wb") as output:
|
||||
completed = subprocess.run(
|
||||
["zstd", "-dc", str(path)],
|
||||
check=False,
|
||||
stdout=output,
|
||||
stderr=subprocess.PIPE,
|
||||
timeout=60,
|
||||
env={"PATH": os.environ.get("PATH", "")},
|
||||
)
|
||||
if completed.returncode != 0:
|
||||
raise ReleaseBlocked("artifact zstd decompression failed")
|
||||
import tarfile
|
||||
with tarfile.open(tar_path, "r:") as archive:
|
||||
names: set[str] = set()
|
||||
for member in archive.getmembers():
|
||||
candidate = Path(member.name)
|
||||
if (
|
||||
candidate.is_absolute()
|
||||
or ".." in candidate.parts
|
||||
or member.issym()
|
||||
or member.islnk()
|
||||
or not (member.isfile() or member.isdir())
|
||||
):
|
||||
raise ReleaseBlocked(f"unsafe artifact member: {member.name}")
|
||||
names.add(member.name)
|
||||
missing = sorted(REQUIRED_V2R1C7_MEMBERS - names)
|
||||
if missing:
|
||||
raise ReleaseBlocked(
|
||||
"artifact missing required members: " + ",".join(missing)
|
||||
)
|
||||
required = {
|
||||
"V2R1C7": REQUIRED_V2R1C7_MEMBERS,
|
||||
"V2R2C1": REQUIRED_V2R2C1_MEMBERS,
|
||||
}.get(binding.version)
|
||||
member_verification: dict[str, Any] = {}
|
||||
git_provenance = None
|
||||
if binding.source_registry_main_oid is not None:
|
||||
git_provenance = _source_registry_git_provenance(
|
||||
Path(repository) if repository is not None else Path(
|
||||
configured_authoritative_git_dir()
|
||||
),
|
||||
binding,
|
||||
)
|
||||
if required or binding.source_registry_main_oid is not None:
|
||||
member_verification = _verify_artifact_members(
|
||||
path,
|
||||
required or set(),
|
||||
binding,
|
||||
git_provenance,
|
||||
)
|
||||
return {
|
||||
"artifact_path": str(path),
|
||||
"artifact_sha256": actual,
|
||||
"artifact_size": binding.artifact_size,
|
||||
"verified": True,
|
||||
**member_verification,
|
||||
}
|
||||
|
||||
|
||||
@@ -282,6 +610,18 @@ def verify_authorization(receipt: Mapping[str, Any], binding: Binding) -> dict[s
|
||||
"artifact_sha256": binding.artifact_sha256,
|
||||
"approval_plan_id": binding.approval_plan_id,
|
||||
}
|
||||
if binding.tag_oid is not None:
|
||||
expected["tag_oid"] = binding.tag_oid
|
||||
expected["binding_schema"] = binding.binding_schema or "aios.release-binding.v2"
|
||||
if binding.source_registry_main_oid is not None:
|
||||
expected.update(
|
||||
{
|
||||
"source_registry_main_oid": binding.source_registry_main_oid,
|
||||
"source_registry_path": binding.source_registry_path
|
||||
or f"versions/{binding.version}.json",
|
||||
"source_registry_sha256": binding.source_registry_sha256,
|
||||
}
|
||||
)
|
||||
for key, value in expected.items():
|
||||
if receipt.get(key) != value:
|
||||
raise ManualOwnerReviewRequired(f"Owner authorization mismatch: {key}")
|
||||
@@ -433,13 +773,29 @@ def tag_step(command: Sequence[str], binding: Binding) -> dict[str, Any]:
|
||||
tag_oid = probe.get("tag_oid")
|
||||
if not isinstance(tag_oid, str) or not OID_RE.fullmatch(tag_oid):
|
||||
raise ReleaseBlocked("verified annotated tag object OID missing")
|
||||
if binding.tag_oid is not None and tag_oid != binding.tag_oid:
|
||||
raise ManualOwnerReviewRequired(
|
||||
"same-name tag object differs from the future release binding"
|
||||
)
|
||||
return dict(probe)
|
||||
|
||||
|
||||
def deploy_step(command: Sequence[str], binding: Binding) -> dict[str, Any]:
|
||||
def require_runtime_registry(result: Mapping[str, Any]) -> None:
|
||||
if binding.source_registry_main_oid is None:
|
||||
return
|
||||
if (
|
||||
result.get("runtime_bundle_contains_current_version") is not True
|
||||
or result.get("runtime_registry_converged") is not True
|
||||
):
|
||||
raise ReconciliationRequired(
|
||||
"runtime bundle/runtime registry convergence was not independently verified"
|
||||
)
|
||||
|
||||
probe = run_adapter(command, binding.request("probe"))
|
||||
require_exact_response(probe, binding)
|
||||
if probe.get("status") == "ACTIVE_VERIFIED":
|
||||
require_runtime_registry(probe)
|
||||
return dict(probe)
|
||||
if probe.get("status") == "CONFLICT":
|
||||
raise ManualOwnerReviewRequired("production runtime has conflicting binding")
|
||||
@@ -458,9 +814,30 @@ def deploy_step(command: Sequence[str], binding: Binding) -> dict[str, Any]:
|
||||
or probe.get("production_deployed") is not True
|
||||
):
|
||||
raise ReconciliationRequired("production health contract is not ACTIVE")
|
||||
require_runtime_registry(probe)
|
||||
return dict(probe)
|
||||
|
||||
|
||||
def verify_post_deploy_source_registry(
|
||||
repository: Path,
|
||||
binding: Binding,
|
||||
) -> dict[str, Any]:
|
||||
"""Re-read authoritative Git after runtime deployment and before closure."""
|
||||
if binding.source_registry_main_oid is None:
|
||||
return {
|
||||
"source_registry_converged": "not_required",
|
||||
"source_registry_check": "not_required_for_legacy_binding",
|
||||
}
|
||||
provenance = _source_registry_git_provenance(repository, binding)
|
||||
if provenance is None:
|
||||
raise ReconciliationRequired("authoritative source registry was not checked")
|
||||
return {
|
||||
**provenance,
|
||||
"source_registry_converged": True,
|
||||
"source_registry_check": "post_deploy_authoritative_git_read",
|
||||
}
|
||||
|
||||
|
||||
def finalize_step(
|
||||
command: Sequence[str],
|
||||
binding: Binding,
|
||||
@@ -501,7 +878,7 @@ def execute(
|
||||
) -> dict[str, Any]:
|
||||
authorization = verify_authorization(secure_json(authorization_path), binding)
|
||||
lineage = verify_git_lineage(repository, binding)
|
||||
artifact = verify_artifact(binding)
|
||||
artifact = verify_artifact(binding, repository=repository)
|
||||
state = DurableState(state_root, binding)
|
||||
with FileLock(state_root / "executor.lock"):
|
||||
state.record(
|
||||
@@ -518,6 +895,13 @@ def execute(
|
||||
runtime = deploy_step(deploy_command, binding)
|
||||
state.record("RUNTIME_VERIFIED", "runtime", runtime)
|
||||
|
||||
# The runtime adapter proves only runtime facts. Re-read M:path
|
||||
# and C:path from authoritative Git immediately before Registry
|
||||
# and Baseline closure so a post-preflight main change cannot be
|
||||
# mistaken for source convergence.
|
||||
source_registry = verify_post_deploy_source_registry(repository, binding)
|
||||
state.record("SOURCE_REGISTRY_VERIFIED", "source_registry", source_registry)
|
||||
|
||||
registry = finalize_step(
|
||||
registry_command,
|
||||
binding,
|
||||
@@ -547,10 +931,21 @@ def execute(
|
||||
require_exact_response(value, binding)
|
||||
if final_runtime.get("status") != "ACTIVE_VERIFIED":
|
||||
raise ReconciliationRequired("final runtime readback failed")
|
||||
if binding.source_registry_main_oid is not None and not all(
|
||||
final_runtime.get(name) is True
|
||||
for name in (
|
||||
"runtime_bundle_contains_current_version",
|
||||
"runtime_registry_converged",
|
||||
)
|
||||
):
|
||||
raise ReconciliationRequired(
|
||||
"final runtime registry convergence readback failed"
|
||||
)
|
||||
if final_registry.get("status") != "VERIFIED":
|
||||
raise ReconciliationRequired("final Registry readback failed")
|
||||
if final_baseline.get("status") != "VERIFIED":
|
||||
raise ReconciliationRequired("final Baseline readback failed")
|
||||
final_source_registry = verify_post_deploy_source_registry(repository, binding)
|
||||
final = {
|
||||
"status": "PRODUCTION_ACCEPTED",
|
||||
"version": binding.version,
|
||||
@@ -561,6 +956,7 @@ def execute(
|
||||
"tag_oid": tag["tag_oid"],
|
||||
"tag": final_tag,
|
||||
"runtime": final_runtime,
|
||||
"source_registry": final_source_registry,
|
||||
"registry": final_registry,
|
||||
"baseline": final_baseline,
|
||||
"force_push_used": False,
|
||||
@@ -600,6 +996,11 @@ def parser() -> argparse.ArgumentParser:
|
||||
p.add_argument("--evidence-closure-oid", required=True)
|
||||
p.add_argument("--artifact-sha256", required=True)
|
||||
p.add_argument("--approval-plan-id", required=True)
|
||||
p.add_argument("--tag-oid")
|
||||
p.add_argument("--binding-schema")
|
||||
p.add_argument("--source-registry-main-oid")
|
||||
p.add_argument("--source-registry-path")
|
||||
p.add_argument("--source-registry-sha256")
|
||||
p.add_argument("--production-service", required=True)
|
||||
p.add_argument("--production-port", required=True, type=int)
|
||||
p.add_argument("--repository-identity", default="aios/aios")
|
||||
@@ -623,6 +1024,11 @@ def main() -> int:
|
||||
production_service=args.production_service,
|
||||
production_port=args.production_port,
|
||||
repository_identity=args.repository_identity,
|
||||
tag_oid=args.tag_oid,
|
||||
binding_schema=args.binding_schema,
|
||||
source_registry_main_oid=args.source_registry_main_oid,
|
||||
source_registry_path=args.source_registry_path,
|
||||
source_registry_sha256=args.source_registry_sha256,
|
||||
)
|
||||
try:
|
||||
result = execute(
|
||||
|
||||
@@ -995,7 +995,7 @@ def validate_writer_lease_request_v2(raw: Mapping[str, Any], probe: Any) -> dict
|
||||
"target_branch": request.target_branch,
|
||||
"generation": request.generation,
|
||||
"lease_seconds": request.lease_seconds,
|
||||
"create_target_branch": False,
|
||||
"worktree_mode": "detached_at_expected_head",
|
||||
"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,
|
||||
}
|
||||
|
||||
@@ -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,710 @@
|
||||
"""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,
|
||||
)
|
||||
from version_registry import (
|
||||
RegistryConvergenceError,
|
||||
assert_source_registry_convergence,
|
||||
resolve_source_registry_main_oid,
|
||||
source_registry_snapshot,
|
||||
verify_authoritative_repository,
|
||||
verify_source_registry_git_identity,
|
||||
)
|
||||
|
||||
|
||||
VERSION = "V2R2C1"
|
||||
TAG_OID = "07fc2746b688f5f243f29b39de39f32edc3dd9d2"
|
||||
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
|
||||
tag_oid: str = TAG_OID
|
||||
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.tag_oid) or self.tag_oid != TAG_OID:
|
||||
raise ManualOwnerReviewRequired("V2R2C1 tag_oid mismatch")
|
||||
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,
|
||||
*,
|
||||
authority_main_oid: str | None = None,
|
||||
) -> dict[str, Any]:
|
||||
binding.validate()
|
||||
main_oid = authority_main_oid or _git(git_dir, "rev-parse", "refs/heads/main")
|
||||
if not OID_RE.fullmatch(main_oid) or _git(git_dir, "cat-file", "-t", main_oid) != "commit":
|
||||
raise ReleaseBlocked("authoritative main OID is not a commit")
|
||||
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")
|
||||
if not _ancestor(git_dir, binding.target_oid, main_oid):
|
||||
raise ManualOwnerReviewRequired("approved target is not an ancestor of source registry main")
|
||||
return {
|
||||
"target_oid": binding.target_oid,
|
||||
"evidence_closure_oid": binding.evidence_closure_oid,
|
||||
"target_is_ancestor_of_evidence": True,
|
||||
"main_oid": main_oid,
|
||||
"target_is_ancestor_of_main": True,
|
||||
}
|
||||
|
||||
|
||||
def verify_local_annotated_tag(git_dir: Path, binding: ReleaseBinding) -> dict[str, Any]:
|
||||
"""Verify the local authoritative tag object without creating or rewriting it."""
|
||||
tag_oid = _git(git_dir, "rev-parse", "--verify", f"refs/tags/{binding.version}")
|
||||
if tag_oid != binding.tag_oid:
|
||||
raise ManualOwnerReviewRequired("local annotated tag object mismatch")
|
||||
if _git(git_dir, "cat-file", "-t", tag_oid) != "tag":
|
||||
raise ReleaseBlocked("release tag is not annotated")
|
||||
peeled = _git(git_dir, "rev-parse", "--verify", f"{tag_oid}^{{commit}}")
|
||||
if peeled != binding.target_oid:
|
||||
raise ManualOwnerReviewRequired("local annotated tag target mismatch")
|
||||
return {
|
||||
"tag_oid": tag_oid,
|
||||
"tag_object_type": "tag",
|
||||
"peeled_target_oid": peeled,
|
||||
"target_oid": binding.target_oid,
|
||||
}
|
||||
|
||||
|
||||
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 artifact from composite immutable provenance.
|
||||
|
||||
Release payload files are read from target ``T``. The source registry is
|
||||
read once from the pinned main/convergence commit ``M`` and its exact blob
|
||||
bytes are reused for packaging and all recorded digests.
|
||||
"""
|
||||
binding.validate()
|
||||
verify_authoritative_repository(git_dir)
|
||||
source_registry_main_oid = resolve_source_registry_main_oid(
|
||||
git_dir,
|
||||
main_ref="refs/heads/main",
|
||||
)
|
||||
verify_lineage(git_dir, binding, authority_main_oid=source_registry_main_oid)
|
||||
tag_lineage = verify_local_annotated_tag(git_dir, binding)
|
||||
try:
|
||||
source_snapshot = source_registry_snapshot(
|
||||
git_dir,
|
||||
VERSION,
|
||||
source_registry_main_oid=source_registry_main_oid,
|
||||
expected_tag_oid=binding.tag_oid,
|
||||
expected_target_oid=binding.target_oid,
|
||||
expected_evidence_closure_oid=binding.evidence_closure_oid,
|
||||
expected_approval_plan_id=binding.approval_plan_id,
|
||||
)
|
||||
source_registry = source_snapshot["record"]
|
||||
source_registry_bytes = source_snapshot["bytes"]
|
||||
verify_source_registry_git_identity(git_dir, source_registry)
|
||||
except RegistryConvergenceError as exc:
|
||||
raise ReleaseBlocked(f"SOURCE_REGISTRY_NOT_CONVERGED:{exc}") from exc
|
||||
if source_snapshot["source_registry_main_oid"] != source_registry_main_oid:
|
||||
raise ReconciliationRequired("source registry main OID changed during build")
|
||||
if not _ancestor(git_dir, binding.target_oid, source_registry_main_oid):
|
||||
raise ManualOwnerReviewRequired("release target is not an ancestor of source registry main")
|
||||
source_registry_path = source_snapshot["source_registry_path"]
|
||||
source_registry_sha256 = source_snapshot["source_registry_sha256"]
|
||||
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",
|
||||
"versions/V2R2C1.json": "versions/V2R2C1.json",
|
||||
}
|
||||
for destination, source in source_paths.items():
|
||||
target = root / destination
|
||||
target.parent.mkdir(parents=True, exist_ok=True)
|
||||
if destination == source_registry_path:
|
||||
target.write_bytes(source_registry_bytes)
|
||||
else:
|
||||
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",
|
||||
}
|
||||
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,
|
||||
"release_target_oid": binding.target_oid,
|
||||
"source_registry_main_oid": source_registry_main_oid,
|
||||
"source_registry_path": source_registry_path,
|
||||
"source_registry_sha256": source_registry_sha256,
|
||||
"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,
|
||||
"source_registry": {
|
||||
"path": source_registry_path,
|
||||
"version": source_registry["version"],
|
||||
"tag_oid": source_registry["tag_oid"],
|
||||
"target_oid": source_registry["target_oid"],
|
||||
"peeled_target_oid": source_registry["peeled_target_oid"],
|
||||
"evidence_closure_oid": source_registry["evidence_closure_oid"],
|
||||
"approval_plan_id": source_registry["approval_plan_id"],
|
||||
"source_registry_main_oid": source_registry_main_oid,
|
||||
"source_registry_sha256": source_registry_sha256,
|
||||
},
|
||||
"provenance": {
|
||||
"release_payload_source": {
|
||||
"commit_oid": binding.target_oid,
|
||||
"role": "release_target",
|
||||
},
|
||||
"source_registry_source": {
|
||||
"commit_oid": source_registry_main_oid,
|
||||
"path": source_registry_path,
|
||||
"sha256": source_registry_sha256,
|
||||
"role": "source_registry_convergence",
|
||||
},
|
||||
"release_target_is_ancestor_of_source_registry_main": True,
|
||||
"local_annotated_tag": tag_lineage,
|
||||
},
|
||||
}
|
||||
(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,
|
||||
"release_target_oid": binding.target_oid,
|
||||
"source_registry_main_oid": source_registry_main_oid,
|
||||
"source_registry_path": source_registry_path,
|
||||
"source_registry_sha256": source_registry_sha256,
|
||||
"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,
|
||||
"release_target_oid": binding.target_oid,
|
||||
"source_registry_main_oid": source_registry_main_oid,
|
||||
"source_registry_path": source_registry_path,
|
||||
"source_registry_sha256": source_registry_sha256,
|
||||
"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,
|
||||
"source_registry_path": source_registry_path,
|
||||
"source_registry_identity": source_registry,
|
||||
"provenance": {
|
||||
"release_payload_source": binding.target_oid,
|
||||
"source_registry_source": source_registry_main_oid,
|
||||
"release_target_is_ancestor_of_source_registry_main": True,
|
||||
"local_annotated_tag": tag_lineage,
|
||||
},
|
||||
}
|
||||
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)
|
||||
if result.get("tag_oid") != self.binding.tag_oid:
|
||||
self.state.fail(
|
||||
"MANUAL_OWNER_REVIEW_REQUIRED",
|
||||
"same-name tag object differs from the approved V2R2C1 tag",
|
||||
)
|
||||
raise ManualOwnerReviewRequired("approved tag object mismatch")
|
||||
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")
|
||||
if tag_state.get("tag_oid") != self.binding.tag_oid:
|
||||
raise ManualOwnerReviewRequired("approved tag object mismatch")
|
||||
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"],
|
||||
}
|
||||
for name in (
|
||||
"source_registry_main_oid",
|
||||
"source_registry_path",
|
||||
"source_registry_sha256",
|
||||
):
|
||||
if name in artifact:
|
||||
value[name] = artifact[name]
|
||||
# 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))
|
||||
try:
|
||||
assert_source = Path(self.config.registry_root) / f"{VERSION}.json"
|
||||
if not assert_source.is_file():
|
||||
raise RegistryConvergenceError("runtime_registry_entry_missing")
|
||||
assert_source_registry_convergence(
|
||||
assert_source,
|
||||
repo_git_dir=self.config.authoritative_git_dir,
|
||||
)
|
||||
source_check = {"status": "PASS", "path": str(assert_source)}
|
||||
except RegistryConvergenceError as exc:
|
||||
source_check = {"status": "FAIL", "error": str(exc)}
|
||||
result["source_registry"] = source_check
|
||||
result.setdefault("checks", {})["source_registry"] = source_check["status"] == "PASS"
|
||||
if result.get("status") == "PASS" and source_check["status"] != "PASS":
|
||||
result["status"] = "FAIL"
|
||||
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())
|
||||
@@ -7,7 +7,7 @@ Registry and authoritative runtime-anchor Baseline only.
|
||||
Registry is finalized before Baseline. Baseline apply independently verifies:
|
||||
- exact Authority annotated tag and peeled target
|
||||
- exact ACTIVE production health
|
||||
- exact V2R1C7 Registry record
|
||||
- exact runtime Registry record and source-registry convergence
|
||||
- exact previous Baseline file SHA-256 (CAS)
|
||||
"""
|
||||
from __future__ import annotations
|
||||
@@ -25,6 +25,7 @@ import time
|
||||
import urllib.error
|
||||
import urllib.request
|
||||
from pathlib import Path
|
||||
from version_registry import assert_source_registry_convergence
|
||||
from typing import Any, Mapping
|
||||
|
||||
OID_RE = re.compile(r"^[0-9a-f]{40}$")
|
||||
@@ -97,11 +98,40 @@ def load_request() -> dict[str, Any]:
|
||||
raise ValueError("approval_plan_id_invalid")
|
||||
if "tag_oid" in value and not OID_RE.fullmatch(str(value["tag_oid"])):
|
||||
raise ValueError("tag_oid_invalid")
|
||||
if "source_registry_main_oid" in value and not OID_RE.fullmatch(
|
||||
str(value["source_registry_main_oid"])
|
||||
):
|
||||
raise ValueError("source_registry_main_oid_invalid")
|
||||
if "source_registry_sha256" in value and not SHA_RE.fullmatch(
|
||||
str(value["source_registry_sha256"])
|
||||
):
|
||||
raise ValueError("source_registry_sha256_invalid")
|
||||
if "source_registry_path" in value:
|
||||
source_path = str(value["source_registry_path"])
|
||||
if (
|
||||
not source_path.startswith("versions/")
|
||||
or ".." in Path(source_path).parts
|
||||
or "\\" in source_path
|
||||
or not source_path.endswith(".json")
|
||||
):
|
||||
raise ValueError("source_registry_path_invalid")
|
||||
provenance = tuple(
|
||||
value.get(name)
|
||||
for name in (
|
||||
"source_registry_main_oid",
|
||||
"source_registry_path",
|
||||
"source_registry_sha256",
|
||||
)
|
||||
)
|
||||
if any(item is not None for item in provenance) and not all(
|
||||
item is not None for item in provenance
|
||||
):
|
||||
raise ValueError("source_registry_provenance_incomplete")
|
||||
return value
|
||||
|
||||
|
||||
def exact_response(value: Mapping[str, Any], status: str, **extra: Any) -> dict[str, Any]:
|
||||
return {
|
||||
result = {
|
||||
"status": status,
|
||||
"version": value["version"],
|
||||
"target_oid": value["target_oid"],
|
||||
@@ -110,6 +140,14 @@ def exact_response(value: Mapping[str, Any], status: str, **extra: Any) -> dict[
|
||||
"approval_plan_id": value["approval_plan_id"],
|
||||
**extra,
|
||||
}
|
||||
for name in (
|
||||
"source_registry_main_oid",
|
||||
"source_registry_path",
|
||||
"source_registry_sha256",
|
||||
):
|
||||
if name in value:
|
||||
result[name] = value[name]
|
||||
return result
|
||||
|
||||
|
||||
def git(repository: Path, *args: str, check: bool = True) -> subprocess.CompletedProcess[str]:
|
||||
@@ -154,7 +192,7 @@ def health(url: str, value: Mapping[str, Any]) -> dict[str, Any]:
|
||||
|
||||
|
||||
def registry_record(value: Mapping[str, Any], tag_oid: str, production_health: Mapping[str, Any]) -> dict[str, Any]:
|
||||
return {
|
||||
result = {
|
||||
"schema": "aios.production-registry.v2",
|
||||
"version": value["version"],
|
||||
"repository": "aios/aios",
|
||||
@@ -175,10 +213,18 @@ def registry_record(value: Mapping[str, Any], tag_oid: str, production_health: M
|
||||
"deployment_state": production_health["deployment_state"],
|
||||
"updated_at": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()),
|
||||
}
|
||||
for name in (
|
||||
"source_registry_main_oid",
|
||||
"source_registry_path",
|
||||
"source_registry_sha256",
|
||||
):
|
||||
if name in value:
|
||||
result[name] = value[name]
|
||||
return result
|
||||
|
||||
|
||||
def registry_matches(record: Mapping[str, Any], value: Mapping[str, Any], tag_oid: str) -> bool:
|
||||
return all(
|
||||
matches = all(
|
||||
record.get(key) == expected
|
||||
for key, expected in {
|
||||
"version": value["version"],
|
||||
@@ -192,11 +238,16 @@ def registry_matches(record: Mapping[str, Any], value: Mapping[str, Any], tag_oi
|
||||
"production_deployed": True,
|
||||
}.items()
|
||||
) and record.get("registered") is True and record.get("enabled") is True
|
||||
return matches and all(
|
||||
record.get(name) == value.get(name)
|
||||
for name in ("source_registry_main_oid", "source_registry_path", "source_registry_sha256")
|
||||
if name in value
|
||||
)
|
||||
|
||||
|
||||
def baseline_matches(record: Mapping[str, Any], value: Mapping[str, Any], tag_oid: str) -> bool:
|
||||
current = record.get("current_baseline") or record.get("version")
|
||||
return current == value["version"] and all(
|
||||
matches = current == value["version"] and all(
|
||||
record.get(key) == expected
|
||||
for key, expected in {
|
||||
"target_oid": value["target_oid"],
|
||||
@@ -209,6 +260,11 @@ def baseline_matches(record: Mapping[str, Any], value: Mapping[str, Any], tag_oi
|
||||
"status": "PRODUCTION_DEPLOYED",
|
||||
}.items()
|
||||
)
|
||||
return matches and all(
|
||||
record.get(name) == value.get(name)
|
||||
for name in ("source_registry_main_oid", "source_registry_path", "source_registry_sha256")
|
||||
if name in value
|
||||
)
|
||||
|
||||
|
||||
def registry_probe(args: argparse.Namespace, value: Mapping[str, Any]) -> dict[str, Any]:
|
||||
@@ -305,6 +361,11 @@ def baseline_apply(args: argparse.Namespace, value: Mapping[str, Any]) -> dict[s
|
||||
int(previous.get("generation", 0) or 0),
|
||||
int(previous.get("authority_generation", 0) or 0),
|
||||
) + 1
|
||||
assert_source_registry_convergence(
|
||||
registry_path,
|
||||
repo_git_dir=args.repository,
|
||||
source_registry_main_oid=value.get("source_registry_main_oid"),
|
||||
)
|
||||
record = {
|
||||
"schema": "aios.production-baseline.v2",
|
||||
"source_identity": "aios-gitops-runtime-anchor-baseline-authority-v1",
|
||||
@@ -336,6 +397,13 @@ def baseline_apply(args: argparse.Namespace, value: Mapping[str, Any]) -> dict[s
|
||||
"promoted_at": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()),
|
||||
"promoted_by": "aios-approved-production-release-executor",
|
||||
}
|
||||
for name in (
|
||||
"source_registry_main_oid",
|
||||
"source_registry_path",
|
||||
"source_registry_sha256",
|
||||
):
|
||||
if name in value:
|
||||
record[name] = value[name]
|
||||
atomic_json(args.baseline_path, record)
|
||||
return baseline_probe(args, value)
|
||||
|
||||
|
||||
@@ -0,0 +1,190 @@
|
||||
"""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 hashlib
|
||||
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/versions/V2R2C1.json",
|
||||
"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":
|
||||
result = base.probe(_args(), value)
|
||||
if result.get("status") == "ACTIVE_VERIFIED":
|
||||
result.update(_runtime_registry_gate(value))
|
||||
return result
|
||||
if value.get("operation") == "rollback":
|
||||
return rollback(value)
|
||||
raise ValueError("unsupported_operation")
|
||||
|
||||
|
||||
def _runtime_registry_gate(value: Mapping[str, Any]) -> dict[str, Any]:
|
||||
"""Prove the deployed immutable runtime contains the exact M:path blob."""
|
||||
source_digest = value.get("source_registry_sha256")
|
||||
source_main = value.get("source_registry_main_oid")
|
||||
if not source_digest or not source_main:
|
||||
return {}
|
||||
source_path = str(value.get("source_registry_path") or f"versions/{VERSION}.json")
|
||||
member = CURRENT_LINK / source_path
|
||||
if member.is_symlink() or not member.is_file():
|
||||
raise RuntimeError("RUNTIME_SOURCE_REGISTRY_MEMBER_MISSING")
|
||||
raw = member.read_bytes()
|
||||
digest = hashlib.sha256(raw).hexdigest()
|
||||
if digest != source_digest:
|
||||
raise RuntimeError("RUNTIME_SOURCE_REGISTRY_DIGEST_MISMATCH")
|
||||
try:
|
||||
source = json.loads(raw.decode("utf-8"))
|
||||
except (UnicodeDecodeError, json.JSONDecodeError) as exc:
|
||||
raise RuntimeError("RUNTIME_SOURCE_REGISTRY_INVALID") from exc
|
||||
expected = {
|
||||
"version": VERSION,
|
||||
"repository": "aios/aios",
|
||||
"tag_oid": value.get("tag_oid"),
|
||||
"target_oid": value.get("target_oid"),
|
||||
"peeled_target_oid": value.get("target_oid"),
|
||||
"evidence_closure_oid": value.get("evidence_closure_oid"),
|
||||
"approval_plan_id": value.get("approval_plan_id"),
|
||||
}
|
||||
if not isinstance(source, dict) or any(source.get(key) != expected_value for key, expected_value in expected.items()):
|
||||
raise RuntimeError("RUNTIME_SOURCE_REGISTRY_IDENTITY_MISMATCH")
|
||||
if source.get("tag_object") != expected["tag_oid"]:
|
||||
raise RuntimeError("RUNTIME_SOURCE_REGISTRY_TAG_OBJECT_MISMATCH")
|
||||
version_name = f"{VERSION}.json"
|
||||
return {
|
||||
"runtime_registry_versions": [version_name],
|
||||
"attested_projection_versions": [version_name],
|
||||
"effective_registry_versions": [version_name],
|
||||
# The adapter can only inspect the deployed immutable runtime. It
|
||||
# must not claim convergence with mutable authoritative Git; the
|
||||
# executor performs that independent M/C read after deployment.
|
||||
"source_registry_converged": "not_checked",
|
||||
"source_registry_convergence_error": "executor_must_recheck_authoritative_git",
|
||||
"runtime_registry_converged": True,
|
||||
"runtime_bundle_contains_current_version": True,
|
||||
"runtime_bundle_contains_version": True,
|
||||
"registry_consistency_status": "RUNTIME_ONLY_VERIFIED",
|
||||
"runtime_source_registry_main_oid": source_main,
|
||||
"runtime_source_registry_sha256": digest,
|
||||
}
|
||||
|
||||
|
||||
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,982 @@
|
||||
"""Release registry visibility and source-convergence gates.
|
||||
|
||||
Two invariants are enforced:
|
||||
|
||||
1. listVersions may overlay exactly the current Production Baseline-attested
|
||||
projection onto the immutable source bundle. Arbitrary projection files
|
||||
are not surfaced.
|
||||
|
||||
2. release_metadata_adapter.baseline_apply may not finalize a production
|
||||
baseline unless the governed repository/main already contains a source
|
||||
versions/<version>.json entry matching the release identity.
|
||||
|
||||
The source convergence gate intentionally checks release identity, not
|
||||
post-deployment runtime fields. This allows the source entry to be committed
|
||||
before deployment while still preventing PRODUCTION_DEPLOYED from being
|
||||
written for an uncommitted release identity.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import stat
|
||||
import subprocess
|
||||
from datetime import datetime, timezone
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
_VERSION_RE = re.compile(r"^V[0-9]+R[0-9]+C[0-9]+(?:-[A-Za-z0-9_.-]+)?$")
|
||||
_OID_RE = re.compile(r"^[0-9a-f]{40}$")
|
||||
_PLAN_RE = re.compile(r"^sha256:[0-9a-f]{64}$")
|
||||
VERSION_RE = _VERSION_RE
|
||||
OID_RE = _OID_RE
|
||||
SOURCE_REGISTRY_SCHEMAS = frozenset(
|
||||
{"aios.source-registry.v1", "aios.production-registry.v2"}
|
||||
)
|
||||
SOURCE_REGISTRY_FIELDS = (
|
||||
"version",
|
||||
"tag_oid",
|
||||
"target_oid",
|
||||
"peeled_target_oid",
|
||||
"evidence_closure_oid",
|
||||
"approval_plan_id",
|
||||
)
|
||||
REPOSITORY_IDENTITY = "aios/aios"
|
||||
|
||||
DEFAULT_PROJECTION_ROOT = Path(
|
||||
os.getenv(
|
||||
"AIOS_GITOPS_REGISTRY_PROJECTION_ROOT",
|
||||
"/var/lib/aios-gitops/runtime/registry-projection",
|
||||
)
|
||||
)
|
||||
DEFAULT_BASELINE_FILE = Path(
|
||||
os.getenv(
|
||||
"AIOS_GITOPS_BASELINE_FILE",
|
||||
"/var/lib/aios-gitops-runtime-anchor/baseline/current.json",
|
||||
)
|
||||
)
|
||||
DEFAULT_MAIN_REF = os.getenv("AIOS_GITOPS_MAIN_REF", "refs/heads/main")
|
||||
DEFAULT_AUTHORITATIVE_GIT_DIR = Path("/data/aios-gitops/repo.git")
|
||||
DEFAULT_APPROVAL_AUTHORITY_ROOT = Path("/var/lib/aios-gitops/approval-authority")
|
||||
TEST_MODE_ENV = "AIOS_TEST_MODE"
|
||||
AUTHORITATIVE_GIT_ENV = "AIOS_GITOPS_AUTHORITATIVE_GIT_DIR"
|
||||
APPROVAL_AUTHORITY_ROOT_ENV = "AIOS_GITOPS_APPROVAL_AUTHORITY_ROOT"
|
||||
REPOSITORY_IDENTITY_ENV = "AIOS_GITOPS_REPOSITORY_IDENTITY"
|
||||
|
||||
|
||||
class RegistryConvergenceError(RuntimeError):
|
||||
"""Release registry/source convergence is not proven."""
|
||||
|
||||
|
||||
def _load_object(path: Path) -> dict[str, Any]:
|
||||
obj = json.loads(path.read_text(encoding="utf-8"))
|
||||
if not isinstance(obj, dict):
|
||||
raise RegistryConvergenceError(f"json_not_object:{path}")
|
||||
return obj
|
||||
|
||||
|
||||
def _sha256(path: Path) -> str:
|
||||
return hashlib.sha256(path.read_bytes()).hexdigest()
|
||||
|
||||
|
||||
def _git_text(repo_git_dir: Path | str, *args: str) -> str:
|
||||
completed = subprocess.run(
|
||||
["git", f"--git-dir={Path(repo_git_dir)}", *args],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=False,
|
||||
timeout=45,
|
||||
env={"PATH": os.environ.get("PATH", "")},
|
||||
)
|
||||
if completed.returncode != 0:
|
||||
raise RegistryConvergenceError(
|
||||
completed.stderr.strip() or "authoritative_git_query_failed"
|
||||
)
|
||||
return completed.stdout.strip()
|
||||
|
||||
|
||||
def _git_bytes(repo_git_dir: Path | str, *args: str) -> bytes:
|
||||
completed = subprocess.run(
|
||||
["git", f"--git-dir={Path(repo_git_dir)}", *args],
|
||||
capture_output=True,
|
||||
check=False,
|
||||
timeout=45,
|
||||
env={"PATH": os.environ.get("PATH", "")},
|
||||
)
|
||||
if completed.returncode != 0:
|
||||
raise RegistryConvergenceError(
|
||||
completed.stderr.decode("utf-8", "replace").strip()
|
||||
or "authoritative_git_query_failed"
|
||||
)
|
||||
return completed.stdout
|
||||
|
||||
|
||||
def test_mode_enabled() -> bool:
|
||||
"""Return whether explicit dependency injection is allowed for tests."""
|
||||
return os.environ.get(TEST_MODE_ENV) == "1"
|
||||
|
||||
|
||||
def _configured_override(env_name: str, default: str, label: str) -> Path:
|
||||
if env_name in os.environ:
|
||||
if not test_mode_enabled():
|
||||
raise RegistryConvergenceError(f"{label}_env_override_forbidden")
|
||||
value = os.environ.get(env_name, "")
|
||||
if not value:
|
||||
raise RegistryConvergenceError(f"{label}_env_override_invalid")
|
||||
return Path(value)
|
||||
return Path(default)
|
||||
|
||||
|
||||
def configured_authoritative_git_dir() -> Path:
|
||||
return _configured_override(
|
||||
AUTHORITATIVE_GIT_ENV,
|
||||
str(DEFAULT_AUTHORITATIVE_GIT_DIR),
|
||||
"authoritative_repository",
|
||||
)
|
||||
|
||||
|
||||
def configured_repository_identity() -> str:
|
||||
if REPOSITORY_IDENTITY_ENV in os.environ:
|
||||
if not test_mode_enabled():
|
||||
raise RegistryConvergenceError("repository_identity_env_override_forbidden")
|
||||
value = os.environ.get(REPOSITORY_IDENTITY_ENV, "")
|
||||
if not value:
|
||||
raise RegistryConvergenceError("repository_identity_env_override_invalid")
|
||||
return value
|
||||
return REPOSITORY_IDENTITY
|
||||
|
||||
|
||||
def verify_authoritative_repository(
|
||||
repo_git_dir: Path | str,
|
||||
*,
|
||||
repository_identity: str = REPOSITORY_IDENTITY,
|
||||
expected_path: Path | str | None = None,
|
||||
) -> dict[str, str]:
|
||||
"""Prove that a Git directory is the configured AIOS authority.
|
||||
|
||||
A repository that merely contains internally consistent objects is not
|
||||
sufficient. The resolved path must be the configured authority and the
|
||||
bare repository must advertise the expected Gitea repository identity.
|
||||
"""
|
||||
if repository_identity != REPOSITORY_IDENTITY:
|
||||
raise RegistryConvergenceError("authoritative_repository_identity_invalid")
|
||||
candidate = Path(repo_git_dir)
|
||||
configured = Path(expected_path) if expected_path is not None else configured_authoritative_git_dir()
|
||||
if test_mode_enabled() and expected_path is None and AUTHORITATIVE_GIT_ENV not in os.environ:
|
||||
# Explicit test mode may inject a temporary bare repository directly.
|
||||
# Production mode always compares against the fixed authority path.
|
||||
configured = candidate
|
||||
try:
|
||||
candidate_real = candidate.resolve(strict=True)
|
||||
configured_real = configured.resolve(strict=True)
|
||||
except OSError as exc:
|
||||
raise RegistryConvergenceError("authoritative_repository_path_unavailable") from exc
|
||||
if candidate_real != configured_real:
|
||||
raise RegistryConvergenceError("authoritative_repository_path_mismatch")
|
||||
if not candidate_real.is_dir():
|
||||
raise RegistryConvergenceError("authoritative_repository_not_directory")
|
||||
if _git_text(candidate_real, "rev-parse", "--is-bare-repository") != "true":
|
||||
raise RegistryConvergenceError("authoritative_repository_not_bare")
|
||||
remotes = subprocess.run(
|
||||
[
|
||||
"git",
|
||||
f"--git-dir={candidate_real}",
|
||||
"config",
|
||||
"--get-regexp",
|
||||
r"^remote\..*\.url$",
|
||||
],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=False,
|
||||
timeout=45,
|
||||
env={"PATH": os.environ.get("PATH", "")},
|
||||
)
|
||||
urls = [line.split(None, 1)[1] for line in remotes.stdout.splitlines() if " " in line]
|
||||
identity_suffix = repository_identity.removesuffix(".git")
|
||||
identity_verified = any(
|
||||
url.rstrip("/").removesuffix(".git").replace("\\", "/").split(":")[-1]
|
||||
in {identity_suffix, "/" + identity_suffix}
|
||||
or url.rstrip("/").removesuffix(".git").replace("\\", "/").split(":")[-1].endswith(
|
||||
"/" + identity_suffix
|
||||
)
|
||||
for url in urls
|
||||
)
|
||||
if not test_mode_enabled() and not identity_verified:
|
||||
raise RegistryConvergenceError("authoritative_repository_identity_unverified")
|
||||
return {
|
||||
"repository_identity": repository_identity,
|
||||
"configured_path": str(configured_real),
|
||||
"resolved_path": str(candidate_real),
|
||||
}
|
||||
|
||||
|
||||
def resolve_source_registry_main_oid(
|
||||
repo_git_dir: Path | str,
|
||||
*,
|
||||
main_ref: str = DEFAULT_MAIN_REF,
|
||||
) -> str:
|
||||
"""Resolve the authoritative main ref to one immutable commit OID."""
|
||||
oid = _git_text(repo_git_dir, "rev-parse", "--verify", f"{main_ref}^{{commit}}")
|
||||
if not _OID_RE.fullmatch(oid):
|
||||
raise RegistryConvergenceError("source_registry_main_oid_invalid")
|
||||
if _git_text(repo_git_dir, "cat-file", "-t", oid) != "commit":
|
||||
raise RegistryConvergenceError("source_registry_main_oid_not_commit")
|
||||
return oid
|
||||
|
||||
|
||||
def source_registry_snapshot(
|
||||
repo_git_dir: Path | str,
|
||||
version: str,
|
||||
*,
|
||||
main_ref: str = DEFAULT_MAIN_REF,
|
||||
source_registry_main_oid: str | None = None,
|
||||
expected_tag_oid: str | None = None,
|
||||
expected_target_oid: str | None = None,
|
||||
expected_evidence_closure_oid: str | None = None,
|
||||
expected_approval_plan_id: str | None = None,
|
||||
) -> dict[str, Any]:
|
||||
"""Read one source entry and its exact bytes from one immutable commit.
|
||||
|
||||
Callers building an artifact should resolve ``source_registry_main_oid``
|
||||
once and pass it back here. The function then never consults the mutable
|
||||
ref again, so validation and packaging use the same Git blob.
|
||||
"""
|
||||
if not isinstance(version, str) or not _VERSION_RE.fullmatch(version):
|
||||
raise RegistryConvergenceError("source_registry_version_invalid")
|
||||
main_oid = source_registry_main_oid or resolve_source_registry_main_oid(
|
||||
repo_git_dir,
|
||||
main_ref=main_ref,
|
||||
)
|
||||
if not _OID_RE.fullmatch(main_oid):
|
||||
raise RegistryConvergenceError("source_registry_main_oid_invalid")
|
||||
if _git_text(repo_git_dir, "cat-file", "-t", main_oid) != "commit":
|
||||
raise RegistryConvergenceError("source_registry_main_oid_not_commit")
|
||||
path = f"versions/{version}.json"
|
||||
try:
|
||||
raw = _git_bytes(repo_git_dir, "show", f"{main_oid}:{path}")
|
||||
except RegistryConvergenceError as exc:
|
||||
if "path" in str(exc).lower() or "does not exist" in str(exc).lower():
|
||||
raise RegistryConvergenceError(
|
||||
f"source_registry_entry_missing:{main_oid}:{path}"
|
||||
) from exc
|
||||
raise
|
||||
try:
|
||||
source = json.loads(raw.decode("utf-8"))
|
||||
except (UnicodeDecodeError, json.JSONDecodeError) as exc:
|
||||
raise RegistryConvergenceError("source_registry_json_invalid") from exc
|
||||
record = validate_source_registry_record(
|
||||
source,
|
||||
expected_version=version,
|
||||
expected_tag_oid=expected_tag_oid,
|
||||
expected_target_oid=expected_target_oid,
|
||||
expected_evidence_closure_oid=expected_evidence_closure_oid,
|
||||
expected_approval_plan_id=expected_approval_plan_id,
|
||||
)
|
||||
return {
|
||||
"record": record,
|
||||
"bytes": raw,
|
||||
"source_registry_main_oid": main_oid,
|
||||
"source_registry_path": path,
|
||||
"source_registry_sha256": hashlib.sha256(raw).hexdigest(),
|
||||
}
|
||||
|
||||
|
||||
def validate_source_registry_record(
|
||||
value: Any,
|
||||
*,
|
||||
expected_version: str | None = None,
|
||||
expected_tag_oid: str | None = None,
|
||||
expected_target_oid: str | None = None,
|
||||
expected_evidence_closure_oid: str | None = None,
|
||||
expected_approval_plan_id: str | None = None,
|
||||
) -> dict[str, Any]:
|
||||
"""Validate the immutable identity carried by versions/<version>.json."""
|
||||
if not isinstance(value, dict):
|
||||
raise RegistryConvergenceError("source_registry_not_object")
|
||||
if value.get("schema") not in SOURCE_REGISTRY_SCHEMAS:
|
||||
raise RegistryConvergenceError("source_registry_schema_invalid")
|
||||
version = value.get("version")
|
||||
if not isinstance(version, str) or not _VERSION_RE.fullmatch(version):
|
||||
raise RegistryConvergenceError("source_registry_version_invalid")
|
||||
if expected_version is not None and version != expected_version:
|
||||
raise RegistryConvergenceError("source_registry_mismatch:version")
|
||||
if value.get("repository") != REPOSITORY_IDENTITY:
|
||||
raise RegistryConvergenceError("source_registry_repository_invalid")
|
||||
for field in ("tag_oid", "target_oid", "peeled_target_oid", "evidence_closure_oid"):
|
||||
item = value.get(field)
|
||||
if not isinstance(item, str) or not _OID_RE.fullmatch(item):
|
||||
raise RegistryConvergenceError(f"source_registry_{field}_invalid")
|
||||
if value["peeled_target_oid"] != value["target_oid"]:
|
||||
raise RegistryConvergenceError("source_registry_peeled_target_mismatch")
|
||||
approval = value.get("approval_plan_id")
|
||||
if not isinstance(approval, str) or not _PLAN_RE.fullmatch(approval):
|
||||
raise RegistryConvergenceError("source_registry_approval_plan_invalid")
|
||||
if value.get("tag_object", value["tag_oid"]) != value["tag_oid"]:
|
||||
raise RegistryConvergenceError("source_registry_tag_object_mismatch")
|
||||
expected = {
|
||||
"tag_oid": expected_tag_oid,
|
||||
"target_oid": expected_target_oid,
|
||||
"evidence_closure_oid": expected_evidence_closure_oid,
|
||||
"approval_plan_id": expected_approval_plan_id,
|
||||
}
|
||||
for field, wanted in expected.items():
|
||||
if wanted is not None and value.get(field) != wanted:
|
||||
raise RegistryConvergenceError(f"source_registry_mismatch:{field}")
|
||||
return dict(value)
|
||||
|
||||
|
||||
def build_source_registry_entry(
|
||||
*,
|
||||
version: str,
|
||||
tag_oid: str,
|
||||
target_oid: str,
|
||||
peeled_target_oid: str,
|
||||
evidence_closure_oid: str,
|
||||
approval_plan_id: str,
|
||||
repository: str = REPOSITORY_IDENTITY,
|
||||
) -> dict[str, Any]:
|
||||
"""Build the pre-promotion source entry; it performs no Git or runtime write."""
|
||||
value = {
|
||||
"schema": "aios.source-registry.v1",
|
||||
"version": version,
|
||||
"repository": repository,
|
||||
"tag": version,
|
||||
"tag_oid": tag_oid,
|
||||
"tag_object": tag_oid,
|
||||
"target_oid": target_oid,
|
||||
"peeled_target_oid": peeled_target_oid,
|
||||
"evidence_closure_oid": evidence_closure_oid,
|
||||
"approval_plan_id": approval_plan_id,
|
||||
}
|
||||
return validate_source_registry_record(
|
||||
value,
|
||||
expected_version=version,
|
||||
expected_tag_oid=tag_oid,
|
||||
expected_target_oid=target_oid,
|
||||
expected_evidence_closure_oid=evidence_closure_oid,
|
||||
expected_approval_plan_id=approval_plan_id,
|
||||
)
|
||||
|
||||
|
||||
def verify_source_registry_git_identity(
|
||||
repo_git_dir: Path | str,
|
||||
record: dict[str, Any],
|
||||
) -> dict[str, Any]:
|
||||
"""Prove that every Git identity in a source entry exists and is related."""
|
||||
record = validate_source_registry_record(record)
|
||||
version = record["version"]
|
||||
tag_oid = record["tag_oid"]
|
||||
target_oid = record["target_oid"]
|
||||
evidence_oid = record["evidence_closure_oid"]
|
||||
if _git_text(repo_git_dir, "cat-file", "-t", tag_oid) != "tag":
|
||||
raise RegistryConvergenceError("source_registry_tag_not_annotated")
|
||||
named_tag = _git_text(
|
||||
repo_git_dir,
|
||||
"rev-parse",
|
||||
"--verify",
|
||||
f"refs/tags/{version}",
|
||||
)
|
||||
if named_tag != tag_oid:
|
||||
raise RegistryConvergenceError("source_registry_named_tag_mismatch")
|
||||
if _git_text(repo_git_dir, "cat-file", "-t", target_oid) != "commit":
|
||||
raise RegistryConvergenceError("source_registry_target_not_commit")
|
||||
peeled = _git_text(
|
||||
repo_git_dir,
|
||||
"rev-parse",
|
||||
"--verify",
|
||||
f"{tag_oid}^{{commit}}",
|
||||
)
|
||||
if peeled != target_oid or record["peeled_target_oid"] != target_oid:
|
||||
raise RegistryConvergenceError("source_registry_tag_target_mismatch")
|
||||
if _git_text(repo_git_dir, "cat-file", "-t", evidence_oid) != "commit":
|
||||
raise RegistryConvergenceError("source_registry_evidence_not_commit")
|
||||
ancestor = subprocess.run(
|
||||
[
|
||||
"git",
|
||||
f"--git-dir={Path(repo_git_dir)}",
|
||||
"merge-base",
|
||||
"--is-ancestor",
|
||||
target_oid,
|
||||
evidence_oid,
|
||||
],
|
||||
check=False,
|
||||
capture_output=True,
|
||||
timeout=45,
|
||||
env={"PATH": os.environ.get("PATH", "")},
|
||||
)
|
||||
if ancestor.returncode != 0:
|
||||
raise RegistryConvergenceError("source_registry_evidence_lineage_invalid")
|
||||
return {
|
||||
"repository": REPOSITORY_IDENTITY,
|
||||
"version": version,
|
||||
"tag_oid": tag_oid,
|
||||
"tag_object_type": "tag",
|
||||
"target_oid": target_oid,
|
||||
"peeled_target_oid": peeled,
|
||||
"evidence_closure_oid": evidence_oid,
|
||||
"target_is_ancestor_of_evidence": True,
|
||||
}
|
||||
|
||||
|
||||
def verify_source_registry_main_convergence(
|
||||
repo_git_dir: Path | str,
|
||||
version: str,
|
||||
*,
|
||||
main_ref: str = DEFAULT_MAIN_REF,
|
||||
source_registry_main_oid: str | None = None,
|
||||
expected_tag_oid: str | None = None,
|
||||
expected_target_oid: str | None = None,
|
||||
expected_evidence_closure_oid: str | None = None,
|
||||
expected_approval_plan_id: str | None = None,
|
||||
) -> dict[str, Any]:
|
||||
"""Prove that pinned M is still converged with current authoritative main C."""
|
||||
current_main_oid = resolve_source_registry_main_oid(repo_git_dir, main_ref=main_ref)
|
||||
source_main_oid = source_registry_main_oid or current_main_oid
|
||||
if not _OID_RE.fullmatch(source_main_oid):
|
||||
raise RegistryConvergenceError("source_registry_main_oid_invalid")
|
||||
ancestor = subprocess.run(
|
||||
[
|
||||
"git",
|
||||
f"--git-dir={Path(repo_git_dir)}",
|
||||
"merge-base",
|
||||
"--is-ancestor",
|
||||
source_main_oid,
|
||||
current_main_oid,
|
||||
],
|
||||
check=False,
|
||||
capture_output=True,
|
||||
timeout=45,
|
||||
env={"PATH": os.environ.get("PATH", "")},
|
||||
)
|
||||
if ancestor.returncode != 0:
|
||||
raise RegistryConvergenceError("source_registry_main_not_ancestor_of_current_main")
|
||||
pinned = source_registry_snapshot(
|
||||
repo_git_dir,
|
||||
version,
|
||||
source_registry_main_oid=source_main_oid,
|
||||
expected_tag_oid=expected_tag_oid,
|
||||
expected_target_oid=expected_target_oid,
|
||||
expected_evidence_closure_oid=expected_evidence_closure_oid,
|
||||
expected_approval_plan_id=expected_approval_plan_id,
|
||||
)
|
||||
current = source_registry_snapshot(
|
||||
repo_git_dir,
|
||||
version,
|
||||
source_registry_main_oid=current_main_oid,
|
||||
expected_tag_oid=expected_tag_oid,
|
||||
expected_target_oid=expected_target_oid,
|
||||
expected_evidence_closure_oid=expected_evidence_closure_oid,
|
||||
expected_approval_plan_id=expected_approval_plan_id,
|
||||
)
|
||||
if pinned["bytes"] != current["bytes"]:
|
||||
raise RegistryConvergenceError("source_registry_main_entry_changed_after_pin")
|
||||
return {
|
||||
"source_registry_main_oid": source_main_oid,
|
||||
"current_main_oid": current_main_oid,
|
||||
"source_registry_path": pinned["source_registry_path"],
|
||||
"source_registry_sha256": pinned["source_registry_sha256"],
|
||||
"current_main_source_registry_sha256": current["source_registry_sha256"],
|
||||
"source_registry_main_is_ancestor_of_current_main": True,
|
||||
"source_registry_entry_matches_current_main": True,
|
||||
}
|
||||
|
||||
|
||||
_APPROVAL_AUTHORITY_STATES = frozenset(
|
||||
{
|
||||
"APPROVED",
|
||||
"GITEA_API_CONFIRMED",
|
||||
"REMOTE_VERIFIED",
|
||||
"PRODUCTION_DEPLOYED",
|
||||
}
|
||||
)
|
||||
_APPROVAL_AUTHORITY_SCHEMAS = frozenset(
|
||||
{
|
||||
"aios.v2r2c1.tag-approval-request.v1",
|
||||
"aios.owner-release-approval.v1",
|
||||
"aios.release-binding-approval.v2",
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def configured_approval_authority_root() -> Path:
|
||||
return _configured_override(
|
||||
APPROVAL_AUTHORITY_ROOT_ENV,
|
||||
str(DEFAULT_APPROVAL_AUTHORITY_ROOT),
|
||||
"approval_authority_root",
|
||||
)
|
||||
|
||||
|
||||
def _secure_approval_authority_path(
|
||||
authority_path: Path | str,
|
||||
version: str,
|
||||
) -> tuple[Path, Path]:
|
||||
root = configured_approval_authority_root()
|
||||
path = Path(authority_path)
|
||||
if path.is_dir():
|
||||
path = path / f"{version}.json"
|
||||
try:
|
||||
root_real = root.resolve(strict=True)
|
||||
path_real = path.resolve(strict=True)
|
||||
except OSError as exc:
|
||||
raise RegistryConvergenceError("approval_authority_trusted_root_missing") from exc
|
||||
try:
|
||||
path_real.relative_to(root_real)
|
||||
except ValueError as exc:
|
||||
raise RegistryConvergenceError("approval_authority_untrusted_path") from exc
|
||||
metadata = path.lstat()
|
||||
if not stat.S_ISREG(metadata.st_mode) or path.is_symlink():
|
||||
raise RegistryConvergenceError("approval_authority_not_regular")
|
||||
if metadata.st_mode & 0o022:
|
||||
raise RegistryConvergenceError("approval_authority_permissions_invalid")
|
||||
if APPROVAL_AUTHORITY_ROOT_ENV not in os.environ and metadata.st_uid != 0:
|
||||
raise RegistryConvergenceError("approval_authority_owner_invalid")
|
||||
return path, root_real
|
||||
|
||||
|
||||
def _canonical_json(value: object) -> bytes:
|
||||
return (json.dumps(value, sort_keys=True, separators=(",", ":")) + "\n").encode(
|
||||
"utf-8"
|
||||
)
|
||||
|
||||
|
||||
def _approval_is_revoked(root: Path, approval_plan_id: str, authority: dict[str, Any]) -> bool:
|
||||
if any(
|
||||
authority.get(name) is True
|
||||
for name in ("revoked", "invalidated", "revocation")
|
||||
):
|
||||
return True
|
||||
state = authority.get("state") or authority.get("status")
|
||||
if state in {"REVOKED", "INVALIDATED", "EXPIRED", "CANCELLED"}:
|
||||
return True
|
||||
for marker in (
|
||||
root / "revocations" / approval_plan_id,
|
||||
root / "revocations" / approval_plan_id.replace(":", "_"),
|
||||
):
|
||||
if marker.exists():
|
||||
return True
|
||||
expires_at = authority.get("expires_at")
|
||||
if isinstance(expires_at, str) and expires_at:
|
||||
try:
|
||||
instant = datetime.fromisoformat(expires_at.replace("Z", "+00:00"))
|
||||
except ValueError as exc:
|
||||
raise RegistryConvergenceError("approval_expiry_invalid") from exc
|
||||
if instant <= datetime.now(timezone.utc):
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def verify_approval_authority(
|
||||
authority_path: Path | str,
|
||||
record: dict[str, Any],
|
||||
) -> dict[str, Any]:
|
||||
"""Verify the existing release approval/publication receipt association.
|
||||
|
||||
The authority document is intentionally supplied by the governed caller;
|
||||
this helper never creates or rewrites an approval. The path must be under
|
||||
the configured canonical Owner-authority root and the document must use a
|
||||
known release-approval schema. A caller-supplied JSON file elsewhere is
|
||||
never an authority, even when its fields are internally consistent.
|
||||
"""
|
||||
path, trusted_root = _secure_approval_authority_path(authority_path, record["version"])
|
||||
authority = _load_object(path)
|
||||
schema = authority.get("schema")
|
||||
if schema not in _APPROVAL_AUTHORITY_SCHEMAS:
|
||||
raise RegistryConvergenceError("approval_authority_schema_invalid")
|
||||
if _approval_is_revoked(trusted_root, record["approval_plan_id"], authority):
|
||||
raise RegistryConvergenceError("approval_plan_revoked_or_invalidated")
|
||||
nested = authority.get("release_binding") or authority.get("release_request")
|
||||
if not isinstance(nested, dict):
|
||||
nested = authority.get("binding")
|
||||
if not isinstance(nested, dict):
|
||||
raise RegistryConvergenceError("approval_authority_binding_missing")
|
||||
expected = {
|
||||
"version": record["version"],
|
||||
"repository": REPOSITORY_IDENTITY,
|
||||
"tag_oid": record["tag_oid"],
|
||||
"target_oid": record["target_oid"],
|
||||
"peeled_target_oid": record["peeled_target_oid"],
|
||||
"evidence_closure_oid": record["evidence_closure_oid"],
|
||||
"approval_plan_id": record["approval_plan_id"],
|
||||
}
|
||||
if authority.get("approval_plan_id") != expected["approval_plan_id"]:
|
||||
raise RegistryConvergenceError("approval_plan_not_found_or_mismatched")
|
||||
state = authority.get("state") or authority.get("status")
|
||||
if state not in _APPROVAL_AUTHORITY_STATES and authority.get("approved") is not True:
|
||||
raise RegistryConvergenceError("approval_plan_not_approved")
|
||||
owner_approval = authority.get("owner_approval")
|
||||
if schema == "aios.v2r2c1.tag-approval-request.v1":
|
||||
if not isinstance(owner_approval, dict):
|
||||
raise RegistryConvergenceError("canonical_owner_approval_missing")
|
||||
if (
|
||||
owner_approval.get("approved") is not True
|
||||
or owner_approval.get("status") != "APPROVED"
|
||||
or owner_approval.get("approved_by") != "aiman"
|
||||
or owner_approval.get("approval_source") != "explicit_owner_approval_in_chat"
|
||||
):
|
||||
raise RegistryConvergenceError("canonical_owner_approval_invalid")
|
||||
expected_digest = hashlib.sha256(_canonical_json(owner_approval)).hexdigest()
|
||||
if authority.get("owner_approval_digest_sha256") != expected_digest:
|
||||
raise RegistryConvergenceError("canonical_owner_approval_digest_invalid")
|
||||
for field, wanted in expected.items():
|
||||
observed = nested.get(field)
|
||||
if field == "repository" and observed is None:
|
||||
observed = nested.get("repository_identity") or authority.get(
|
||||
"repository_identity"
|
||||
)
|
||||
if field == "peeled_target_oid" and observed is None:
|
||||
observed = (
|
||||
nested.get("gitea_peeled_target_oid")
|
||||
or authority.get("gitea_peeled_target_oid")
|
||||
or nested.get("remote_peeled_target_oid")
|
||||
or authority.get("remote_peeled_target_oid")
|
||||
)
|
||||
if observed != wanted:
|
||||
raise RegistryConvergenceError(f"approval_plan_release_binding_mismatch:{field}")
|
||||
if state == "GITEA_API_CONFIRMED":
|
||||
gitea_tag = authority.get("gitea_tag_oid")
|
||||
gitea_peeled = authority.get("gitea_peeled_target_oid")
|
||||
if gitea_tag != expected["tag_oid"] or gitea_peeled != expected["target_oid"]:
|
||||
raise RegistryConvergenceError("approval_plan_remote_tag_binding_mismatch")
|
||||
return {
|
||||
"path": str(path),
|
||||
"schema": schema,
|
||||
"state": state or "APPROVED",
|
||||
"approval_plan_id": expected["approval_plan_id"],
|
||||
"owner_approval_verified": schema == "aios.v2r2c1.tag-approval-request.v1",
|
||||
"release_binding_verified": True,
|
||||
}
|
||||
|
||||
|
||||
def source_registry_json(value: dict[str, Any]) -> bytes:
|
||||
"""Return the canonical bytes used by the governed file writer."""
|
||||
return (json.dumps(value, ensure_ascii=False, sort_keys=True, indent=2) + "\n").encode(
|
||||
"utf-8"
|
||||
)
|
||||
|
||||
|
||||
def _validated_current_projection(
|
||||
projection_root: Path,
|
||||
baseline_file: Path,
|
||||
) -> Path | None:
|
||||
"""Return only the exact projection attested by the current Baseline."""
|
||||
if not baseline_file.is_file():
|
||||
return None
|
||||
try:
|
||||
baseline = _load_object(baseline_file)
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
version = baseline.get("version")
|
||||
if not isinstance(version, str) or not _VERSION_RE.fullmatch(version):
|
||||
return None
|
||||
if baseline.get("status") != "PRODUCTION_DEPLOYED":
|
||||
return None
|
||||
if baseline.get("production_deployed") is not True:
|
||||
return None
|
||||
|
||||
source = baseline.get("registry_source")
|
||||
digest = baseline.get("registry_digest")
|
||||
if not isinstance(source, str) or not isinstance(digest, str):
|
||||
return None
|
||||
|
||||
candidate = projection_root / f"{version}.json"
|
||||
try:
|
||||
if candidate.resolve() != Path(source).resolve():
|
||||
return None
|
||||
except Exception:
|
||||
return None
|
||||
if not candidate.is_file() or _sha256(candidate) != digest:
|
||||
return None
|
||||
|
||||
try:
|
||||
record = _load_object(candidate)
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
try:
|
||||
validate_source_registry_record(
|
||||
record,
|
||||
expected_version=version,
|
||||
expected_tag_oid=baseline.get("tag_oid"),
|
||||
expected_target_oid=baseline.get("target_oid"),
|
||||
expected_evidence_closure_oid=baseline.get("evidence_closure_oid"),
|
||||
expected_approval_plan_id=baseline.get("approval_plan_id"),
|
||||
)
|
||||
except RegistryConvergenceError:
|
||||
return None
|
||||
if record.get("schema") != "aios.production-registry.v2":
|
||||
return None
|
||||
if record.get("status") != "PRODUCTION_DEPLOYED":
|
||||
return None
|
||||
if record.get("version") != version:
|
||||
return None
|
||||
if record.get("repository") != REPOSITORY_IDENTITY:
|
||||
return None
|
||||
if record.get("production_deployed") is not True:
|
||||
return None
|
||||
|
||||
tag_oid = record.get("tag_oid")
|
||||
target_oid = record.get("target_oid")
|
||||
if not isinstance(tag_oid, str) or not _OID_RE.fullmatch(tag_oid):
|
||||
return None
|
||||
if not isinstance(target_oid, str) or not _OID_RE.fullmatch(target_oid):
|
||||
return None
|
||||
if record.get("peeled_target_oid") != target_oid:
|
||||
return None
|
||||
|
||||
return candidate
|
||||
|
||||
|
||||
def list_registry_versions(
|
||||
workspace: Path | str,
|
||||
*,
|
||||
projection_root: Path = DEFAULT_PROJECTION_ROOT,
|
||||
baseline_file: Path = DEFAULT_BASELINE_FILE,
|
||||
) -> list[str]:
|
||||
"""Return the legacy display list; use the report for provenance."""
|
||||
return list_registry_versions_report(
|
||||
workspace,
|
||||
projection_root=projection_root,
|
||||
baseline_file=baseline_file,
|
||||
)["registry_versions"]
|
||||
|
||||
|
||||
def list_registry_versions_report(
|
||||
workspace: Path | str,
|
||||
*,
|
||||
projection_root: Path = DEFAULT_PROJECTION_ROOT,
|
||||
baseline_file: Path = DEFAULT_BASELINE_FILE,
|
||||
repo_git_dir: Path | str | None = None,
|
||||
main_ref: str = DEFAULT_MAIN_REF,
|
||||
) -> dict[str, Any]:
|
||||
"""Return version visibility with runtime/projection provenance.
|
||||
|
||||
The display list intentionally retains the historical projection overlay
|
||||
for API compatibility. The explicit booleans make that overlay
|
||||
non-authoritative: a projection-only version has
|
||||
``runtime_bundle_contains_version == false`` and can never be reported as
|
||||
converged.
|
||||
"""
|
||||
ws = Path(workspace)
|
||||
versions_root = ws / "versions"
|
||||
runtime_names = {
|
||||
path.name
|
||||
for path in versions_root.iterdir()
|
||||
if path.is_file()
|
||||
and not path.is_symlink()
|
||||
and path.suffix == ".json"
|
||||
and _VERSION_RE.fullmatch(path.stem)
|
||||
} if versions_root.is_dir() else set()
|
||||
current = _validated_current_projection(projection_root, baseline_file)
|
||||
attested_version = current.stem if current is not None else None
|
||||
runtime_bundle_contains = bool(
|
||||
attested_version is not None
|
||||
and f"{attested_version}.json" in runtime_names
|
||||
)
|
||||
consistency_status = "NO_ATTESTED_PROJECTION"
|
||||
source_converged: bool | str = "unknown"
|
||||
source_error: str | None = None
|
||||
runtime_converged = False
|
||||
projection_record: dict[str, Any] | None = None
|
||||
if current is not None:
|
||||
try:
|
||||
projection_record = _load_object(current)
|
||||
except (OSError, RegistryConvergenceError, json.JSONDecodeError) as exc:
|
||||
source_error = f"projection_invalid:{type(exc).__name__}"
|
||||
if repo_git_dir is None:
|
||||
source_converged = "unknown"
|
||||
source_error = "authoritative_repository_unavailable"
|
||||
elif projection_record is not None:
|
||||
try:
|
||||
verify_authoritative_repository(repo_git_dir)
|
||||
verify_source_registry_main_convergence(
|
||||
repo_git_dir,
|
||||
attested_version or "",
|
||||
main_ref=main_ref,
|
||||
expected_tag_oid=projection_record.get("tag_oid"),
|
||||
expected_target_oid=projection_record.get("target_oid"),
|
||||
expected_evidence_closure_oid=projection_record.get(
|
||||
"evidence_closure_oid"
|
||||
),
|
||||
expected_approval_plan_id=projection_record.get("approval_plan_id"),
|
||||
)
|
||||
source_converged = True
|
||||
except RegistryConvergenceError as exc:
|
||||
detail = str(exc)
|
||||
if detail.startswith("authoritative_repository_"):
|
||||
source_converged = "unknown"
|
||||
source_error = (
|
||||
"authoritative_repository_unavailable"
|
||||
if detail.endswith("path_unavailable")
|
||||
else detail
|
||||
)
|
||||
else:
|
||||
source_converged = False
|
||||
source_error = detail
|
||||
if current is not None and not runtime_bundle_contains:
|
||||
consistency_status = "RUNTIME_ARTIFACT_RECONCILIATION_REQUIRED"
|
||||
elif current is not None and runtime_bundle_contains:
|
||||
runtime_path = versions_root / current.name
|
||||
try:
|
||||
projection_record = projection_record or _load_object(current)
|
||||
runtime_record = _load_object(runtime_path)
|
||||
identity_fields = (
|
||||
"version",
|
||||
"repository",
|
||||
"tag_oid",
|
||||
"target_oid",
|
||||
"peeled_target_oid",
|
||||
"evidence_closure_oid",
|
||||
"approval_plan_id",
|
||||
)
|
||||
runtime_converged = all(
|
||||
runtime_record.get(field) == projection_record.get(field)
|
||||
for field in identity_fields
|
||||
)
|
||||
if not runtime_converged:
|
||||
consistency_status = "RUNTIME_IDENTITY_MISMATCH"
|
||||
except (OSError, RegistryConvergenceError, json.JSONDecodeError):
|
||||
consistency_status = "RUNTIME_ENTRY_INVALID"
|
||||
runtime_converged = False
|
||||
if current is not None and runtime_bundle_contains and runtime_converged:
|
||||
if source_converged is True:
|
||||
consistency_status = "FULLY_CONSISTENT"
|
||||
elif source_converged == "unknown":
|
||||
consistency_status = "SOURCE_GIT_NOT_CHECKED"
|
||||
else:
|
||||
consistency_status = "SOURCE_REGISTRY_NOT_CONVERGED"
|
||||
elif current is not None and not runtime_bundle_contains:
|
||||
if source_converged is False:
|
||||
consistency_status = "SOURCE_REGISTRY_NOT_CONVERGED"
|
||||
else:
|
||||
consistency_status = "RUNTIME_ARTIFACT_RECONCILIATION_REQUIRED"
|
||||
names = set(runtime_names)
|
||||
if current is not None:
|
||||
names.add(current.name)
|
||||
effective = sorted(names)
|
||||
projection_versions = [attested_version + ".json"] if attested_version else []
|
||||
return {
|
||||
"registry_versions": effective,
|
||||
"effective_registry_versions": effective,
|
||||
"runtime_registry_versions": sorted(runtime_names),
|
||||
"attested_projection_versions": projection_versions,
|
||||
"attested_projection_version": attested_version,
|
||||
"source_registry_converged": source_converged,
|
||||
"source_registry_convergence_error": source_error,
|
||||
"runtime_registry_converged": runtime_converged,
|
||||
"runtime_bundle_contains_current_version": runtime_bundle_contains,
|
||||
"runtime_bundle_contains_version": runtime_bundle_contains,
|
||||
"registry_consistency_status": consistency_status,
|
||||
}
|
||||
|
||||
|
||||
def assert_runtime_registry_convergence(
|
||||
workspace: Path | str,
|
||||
version: str,
|
||||
*,
|
||||
projection_root: Path = DEFAULT_PROJECTION_ROOT,
|
||||
baseline_file: Path = DEFAULT_BASELINE_FILE,
|
||||
) -> dict[str, Any]:
|
||||
"""Fail closed when a projection-only version is visible but unpackaged."""
|
||||
report = list_registry_versions_report(
|
||||
workspace,
|
||||
projection_root=projection_root,
|
||||
baseline_file=baseline_file,
|
||||
)
|
||||
expected_name = f"{version}.json"
|
||||
if (
|
||||
report.get("attested_projection_version") == version
|
||||
and report.get("runtime_bundle_contains_version") is not True
|
||||
):
|
||||
raise RegistryConvergenceError("runtime_bundle_missing_source_registry")
|
||||
if expected_name not in report.get("runtime_registry_versions", []):
|
||||
raise RegistryConvergenceError("runtime_bundle_missing_source_registry")
|
||||
if report.get("source_registry_converged") is not True:
|
||||
raise RegistryConvergenceError("source_registry_not_converged_or_not_checked")
|
||||
if report.get("registry_consistency_status") not in {"FULLY_CONSISTENT", "PASS"}:
|
||||
raise RegistryConvergenceError(
|
||||
f"runtime_registry_consistency:{report.get('registry_consistency_status')}"
|
||||
)
|
||||
return report
|
||||
|
||||
|
||||
def source_registry_from_main(
|
||||
repo_git_dir: Path | str,
|
||||
version: str,
|
||||
*,
|
||||
main_ref: str = DEFAULT_MAIN_REF,
|
||||
source_registry_main_oid: str | None = None,
|
||||
expected_tag_oid: str | None = None,
|
||||
expected_target_oid: str | None = None,
|
||||
expected_evidence_closure_oid: str | None = None,
|
||||
expected_approval_plan_id: str | None = None,
|
||||
) -> dict[str, Any]:
|
||||
"""Read and validate the canonical source entry from the supplied main ref."""
|
||||
try:
|
||||
return source_registry_snapshot(
|
||||
repo_git_dir,
|
||||
version,
|
||||
main_ref=main_ref,
|
||||
source_registry_main_oid=source_registry_main_oid,
|
||||
expected_tag_oid=expected_tag_oid,
|
||||
expected_target_oid=expected_target_oid,
|
||||
expected_evidence_closure_oid=expected_evidence_closure_oid,
|
||||
expected_approval_plan_id=expected_approval_plan_id,
|
||||
)["record"]
|
||||
except RegistryConvergenceError as exc:
|
||||
if str(exc).startswith("fatal:") or "path" in str(exc).lower():
|
||||
raise RegistryConvergenceError(
|
||||
f"source_registry_entry_missing:{source_registry_main_oid or main_ref}:versions/{version}.json"
|
||||
) from exc
|
||||
raise
|
||||
|
||||
|
||||
def assert_source_registry_convergence(
|
||||
registry_path: Path | str,
|
||||
*,
|
||||
repo_git_dir: Path | str,
|
||||
main_ref: str = DEFAULT_MAIN_REF,
|
||||
source_registry_main_oid: str | None = None,
|
||||
) -> dict[str, Any]:
|
||||
"""Require governed main to contain a source entry for the release identity.
|
||||
|
||||
The source entry is intentionally required to match only fields that are
|
||||
knowable and governable before production deployment. Post-deployment
|
||||
fields such as production_service/port/status timestamps are not required.
|
||||
"""
|
||||
registry_path = Path(registry_path)
|
||||
repo_git_dir = Path(repo_git_dir)
|
||||
verify_authoritative_repository(repo_git_dir)
|
||||
record = _load_object(registry_path)
|
||||
|
||||
version = record.get("version")
|
||||
tag_oid = record.get("tag_oid")
|
||||
target_oid = record.get("target_oid")
|
||||
|
||||
try:
|
||||
validate_source_registry_record(
|
||||
record,
|
||||
expected_version=version,
|
||||
expected_tag_oid=tag_oid,
|
||||
expected_target_oid=target_oid,
|
||||
expected_evidence_closure_oid=record.get("evidence_closure_oid"),
|
||||
expected_approval_plan_id=record.get("approval_plan_id"),
|
||||
)
|
||||
except RegistryConvergenceError as exc:
|
||||
raise RegistryConvergenceError(f"registry_identity_invalid:{exc}") from exc
|
||||
convergence = verify_source_registry_main_convergence(
|
||||
repo_git_dir,
|
||||
version,
|
||||
main_ref=main_ref,
|
||||
source_registry_main_oid=source_registry_main_oid,
|
||||
expected_tag_oid=tag_oid,
|
||||
expected_target_oid=target_oid,
|
||||
expected_evidence_closure_oid=record["evidence_closure_oid"],
|
||||
expected_approval_plan_id=record["approval_plan_id"],
|
||||
)
|
||||
return {
|
||||
**convergence,
|
||||
}
|
||||
@@ -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
|
||||
@@ -0,0 +1,107 @@
|
||||
# AIOS V2R2C1 Pre-Push Review Acceptance Record(最新机制语义版)
|
||||
|
||||
## 1. 验收对象
|
||||
|
||||
- Repository: aios/aios
|
||||
- Authoritative Git: /data/aios-gitops/repo.git
|
||||
- Documentation branch: agent/docs/v2r2c1-provenance-mainline-refresh-20260810
|
||||
- Branch HEAD before semantic repair: 869cc007649a39035f5b74513cecc55bb1ae673f
|
||||
- Follow-up parent: 869cc007649a39035f5b74513cecc55bb1ae673f
|
||||
- Latest mechanism HEAD: 52b74ba61040246fffa8b10011c1199830e3d665
|
||||
- Authoritative main HEAD: 3244f0f17f8845592ac94d585f5ebd65693c9f29
|
||||
- Owner review: REQUIRED
|
||||
|
||||
文档 follow-up 不是 mechanism production release。当前 Production Baseline 仍为 V2R2C1 generation 4、PRODUCTION_DEPLOYED / ACTIVE,tag object 为 07fc2746b688f5f243f29b39de39f32edc3dd9d2,peeled target 为 998d30304787e31e5ae607c0bd86b5edb2b0e5ad。
|
||||
|
||||
## 2. 最新机制差异审计
|
||||
|
||||
差异范围分三层记录:
|
||||
|
||||
| 审计层 | 范围 | 结果 |
|
||||
| --- | --- | --- |
|
||||
| Initial review fix | 6ece3525 → b660e2c | 14 个文件 |
|
||||
| Full mechanism audit | b660e2c → 51b5f0e → 52b74ba | 17 个路径,+1403/-79 |
|
||||
| Latest tip delta | 51b5f0e → 52b74ba | 12 个路径,+334/-39 |
|
||||
|
||||
因此 README、requirements、architecture、acceptance 与 version differences 必须引用 52b74ba 的最新状态,而不能把 b660e2c 当作当前机制分支 tip。
|
||||
|
||||
## 3. GitOps 与 writer 验收
|
||||
|
||||
- Action service: active
|
||||
- Repository identity: aios/aios
|
||||
- Authoritative repository: /data/aios-gitops/repo.git
|
||||
- Governed task-scoped write path: enabled for this branch task
|
||||
- legacy_global_writes_enabled: false
|
||||
- main write: false
|
||||
- production write: false
|
||||
- Baseline write: false
|
||||
- tag write: false
|
||||
- force push: false
|
||||
- Documentation branch creation/continuation: PASS
|
||||
- Non-main documentation commit: PASS
|
||||
- Remote branch OID equals local commit OID: PASS
|
||||
- Gitea branch and content readback: PASS
|
||||
|
||||
旧的 aios-gitops-action-writer.service 不作为生产写开关;本验收以当前受治理 Action service 与受控 writer route 的实际响应为准。
|
||||
|
||||
## 4. 文档完整性验收
|
||||
|
||||
五个且仅五个路径属于 follow-up diff:
|
||||
|
||||
- README.md
|
||||
- docs/requirements/V2R2C1-pre-push-review-fix.md
|
||||
- docs/architecture/release-provenance-and-registry-convergence.md
|
||||
- docs/acceptance/V2R2C1-pre-push-review-acceptance.md
|
||||
- docs/version-differences/V2R2C1-pre-push-mechanism-diff.md
|
||||
|
||||
每个文件必须满足:
|
||||
|
||||
- UTF-8 strict decode: PASS
|
||||
- CJK count > 0: PASS
|
||||
- U+FFFD count = 0: PASS
|
||||
- abnormal consecutive question marks = 0: PASS
|
||||
- source bytes SHA-256 equals Git blob SHA-256: PASS
|
||||
- Gitea contents readback equals Git blob bytes: PASS
|
||||
- git diff --check: PASS
|
||||
|
||||
## 5. 提交方测试事实
|
||||
|
||||
- Related tests: 760 passed
|
||||
- Skipped: 2 skipped
|
||||
- Full test result: 1 failed
|
||||
- Subtests: 154 passed
|
||||
- Failure: tests/test_signer_contract_compatibility.py::test_real_signer_help_and_self_test_are_available
|
||||
- /usr/local/libexec/aios-grant-signer-adapter self-test: return 42
|
||||
- Two independent reruns: both return 42
|
||||
|
||||
Signer gate is FAIL / EXTERNAL. Full external gate is PARTIAL. Production ready remains false。不能把该失败解释为暂态成功,也不能把全量结果写成全通过。
|
||||
|
||||
## 6. 分层状态
|
||||
|
||||
| 层级 | 结果 |
|
||||
| --- | --- |
|
||||
| Mechanism code fix | COMPLETE |
|
||||
| Documentation branch write | COMPLETE |
|
||||
| Documentation remote readback | COMPLETE |
|
||||
| Full external gate | PARTIAL |
|
||||
| Signer gate | FAIL / EXTERNAL |
|
||||
| Production acceptance | NOT_RUN |
|
||||
| Owner review | REQUIRED |
|
||||
|
||||
Documentation branch write/readback COMPLETE 只表示非 main 文档分支的受治理提交与远端内容一致,不表示 mechanism/release production write 已发生。
|
||||
|
||||
## 7. 未执行生产动作
|
||||
|
||||
- Mechanism/release production write: NOT_RUN
|
||||
- Merge to main: NOT_RUN
|
||||
- Production release: NOT_RUN
|
||||
- Production Baseline update: NOT_RUN
|
||||
- Tag creation/deletion/rewrite: NOT_RUN
|
||||
- Force push: NOT_RUN
|
||||
- systemd/nginx production reload or restart: NOT_RUN
|
||||
|
||||
## 8. 治理结论
|
||||
|
||||
force_push_used=false、tag_rewritten=false、direct_main_push=false、secrets_committed=false、production_mutation=false、legacy_global_writes_enabled=false。
|
||||
|
||||
结论:文档语义一致性修复候选已准备为 READY_FOR_OWNER_REVIEW。Owner review 完成前不得 merge main,不得修改 Baseline、tag、历史版本身份或生产 runtime。
|
||||
@@ -0,0 +1,156 @@
|
||||
# AIOS Release Provenance & Registry Convergence Architecture(最新机制语义版)
|
||||
|
||||
## 1. 架构目标
|
||||
|
||||
AIOS release path 把版本名提升为一组可验证且不可混淆的 Git、approval、artifact 与 runtime 身份。最新机制 HEAD 为 52b74ba61040246fffa8b10011c1199830e3d665;它位于非 main 的机制分支,不能被描述为新的 Production Baseline。
|
||||
|
||||
架构必须同时解决:
|
||||
|
||||
- mutable main 引起的 TOCTOU;
|
||||
- annotated tag object 与 peeled target 混淆;
|
||||
- source registry overlay 或 runtime projection 被误报为 Git/source convergence;
|
||||
- 未绑定 approval plan 驱动 registry 生成;
|
||||
- release 与 general artifact binding 混用;
|
||||
- builder 与 executor 各自验证不同身份;
|
||||
- legacy V2R2C1 receipt 兼容与 future binding fail closed。
|
||||
|
||||
## 2. 身份模型
|
||||
|
||||
| 符号 | 定义 | 当前或运行时绑定 |
|
||||
| --- | --- | --- |
|
||||
| R | authoritative repository identity | aios/aios;Git 目录为 /data/aios-gitops/repo.git |
|
||||
| A | annotated tag object | V2R2C1 tag object 07fc2746b688f5f243f29b39de39f32edc3dd9d2 |
|
||||
| T | A 的 peeled commit,即 release target | 998d30304787e31e5ae607c0bd86b5edb2b0e5ad |
|
||||
| E | evidence closure commit | a8b906d1bc24088c19647e98de6a679268f6d0a2 |
|
||||
| M | 本次 source-registry snapshot 固定的 main commit | 每次构建解析一次并写入 provenance |
|
||||
| C | 当前权威 main 或被 convergence probe 选定的 reference commit | 当前 authoritative main 为 3244f0f17f8845592ac94d585f5ebd65693c9f29 |
|
||||
| P | Owner-approved approval plan | sha256:c0df52008ae7f64cec5ee2d28947aa7b892066c670ab8295e4549140b1a7500e |
|
||||
|
||||
R、A、T、E、M、C、P 不得用一个版本字符串互相替代。尤其是 mechanism branch HEAD、C、T 与 Production Baseline 是不同状态轴。
|
||||
|
||||
## 3. Git Object Model 与 lineage
|
||||
|
||||
对于 provenance-enabled release,代码必须建立以下关系:
|
||||
|
||||
~~~text
|
||||
refs/tags/<version> -> A
|
||||
A^{commit} = T
|
||||
T = registry peeled target
|
||||
T ancestor-of E
|
||||
T ancestor-of M when the release lineage check requires it
|
||||
M ancestor-of C when source-registry main convergence is checked
|
||||
SR = bytes(M:path)
|
||||
H = SHA-256(SR)
|
||||
~~~
|
||||
|
||||
其中:
|
||||
|
||||
- A 必须是 annotated tag object,不能由 lightweight tag 代替。
|
||||
- E 必须是 commit,且 release lineage 证明 T 到 E。
|
||||
- M 必须是 commit;source registry 的读取、校验、哈希和打包都使用 M:path 的同一字节。
|
||||
- C 是当前权威 main/reference commit,不是生产 target,也不是 mechanism branch HEAD。
|
||||
- M 与 C 的 convergence probe 还要比较相同 path 的精确字节;M ancestor-of C 仅在该 probe 的适用语义下成立。
|
||||
- 任何 same-name but different-OID tag、target、registry、approval 或 repository identity 冲突都停止自动流程。
|
||||
|
||||
## 4. Pinned source registry 与 authority binding
|
||||
|
||||
构建开始时由 authoritative repository 解析 M,并立即保存 M、path、SR 和 H。后续不得回到 mutable main 重新取 registry。
|
||||
|
||||
registry record 必须绑定:
|
||||
|
||||
- R、version、A、T、peeled target、E;
|
||||
- P 与其 authority/publication receipt;
|
||||
- M、source registry path、H;
|
||||
- generator 使用的 authoritative repository identity。
|
||||
|
||||
authority binding 不是字符串格式检查。governed caller、generator、builder 与 executor 都必须能证明 approval authority 与 release identity 的关联。
|
||||
|
||||
## 5. Artifact binding:release 与 general 分离
|
||||
|
||||
release artifact 需要完整的 release binding:
|
||||
|
||||
~~~text
|
||||
repository + version + tag_oid + target_oid
|
||||
+ peeled_target_oid + evidence_closure_oid + approval_plan_id
|
||||
+ source_registry_main_oid + source_registry_path + source_registry_sha256
|
||||
~~~
|
||||
|
||||
general artifact 可以保持普通构建用途,但不得被当作 release artifact,也不得绕过 release binding。release mode 缺少 expected binding 或字段不匹配时返回 fail-closed 状态。
|
||||
|
||||
artifact metadata、manifest 与 receipt 必须引用同一组对象。source registry payload 以 SR 的精确字节进入 artifact,不能只保存重新序列化后的等价 JSON。
|
||||
|
||||
## 6. 组件职责与独立复核
|
||||
|
||||
### version_registry.py
|
||||
|
||||
解析 M,读取 M:path,验证 registry record、A/T/E lineage、approval authority,并分别报告 registry display、runtime bundle、attested projection 与 source convergence。
|
||||
|
||||
### generate_source_registry.py
|
||||
|
||||
在生成前获得 authoritative repository、repository identity 与 approval authority,校验 tag object、target、lineage、registry bytes 与 authority 关联。
|
||||
|
||||
### governed_production_release.py
|
||||
|
||||
执行 T、E、M 的适用 lineage 检查,构建由 T payload 与 M registry bytes 组成的 deterministic release artifact,并把复合 provenance 写入 metadata、manifest 和 receipt。
|
||||
|
||||
### Autonomous release artifact builder
|
||||
|
||||
release mode 强制 expected release identity;general mode 保持独立。builder 不得用缺失或局部 binding 伪造 release 成功。
|
||||
|
||||
### approved_release_executor.py
|
||||
|
||||
executor 以自己的 Git identity 重新读取并验证 artifact、canonical registry、source path、source SHA-256、BUILD-METADATA provenance、future tag object 与 authorization receipt。executor 不接受 builder 自述作为唯一证据。
|
||||
|
||||
## 7. Runtime projection 与 Git convergence
|
||||
|
||||
版本 API 必须区分:
|
||||
|
||||
- legacy display list;
|
||||
- runtime bundle versions;
|
||||
- attested projection version;
|
||||
- runtime_bundle_contains_version;
|
||||
- source_registry_converged;
|
||||
- registry_consistency_status。
|
||||
|
||||
projection 能看到版本不代表 runtime bundle 包含该版本,更不代表 M 与 C 的 source bytes 收敛。典型状态包括 PASS、RUNTIME_BUNDLE_MISSING、IDENTITY_MISMATCH、RUNTIME_ENTRY_INVALID、NO_ATTESTED_PROJECTION。
|
||||
|
||||
当前生产 Baseline 仍为 V2R2C1 generation 4、PRODUCTION_DEPLOYED / ACTIVE。该运行时状态不能被机制分支 HEAD 或文档分支写入替代。
|
||||
|
||||
## 8. Post-deploy source-registry recheck
|
||||
|
||||
部署后的 executor 必须重新读取 authoritative Git 与 source registry path,重新计算 H,并核对:
|
||||
|
||||
- repository identity;
|
||||
- release binding 的 A/T/E/P;
|
||||
- M、path、H;
|
||||
- artifact metadata 与实际 payload;
|
||||
- runtime projection 与 source convergence;
|
||||
- authorization receipt。
|
||||
|
||||
任何 recheck 失败都必须保持 blocked,不得靠 runtime health、开放端口或 projection 可见性推断 release closure。
|
||||
|
||||
## 9. Fail-closed 状态
|
||||
|
||||
| 条件 | 结果 |
|
||||
| --- | --- |
|
||||
| source registry 缺失或精确字节不可读 | ReleaseBlocked |
|
||||
| repository、tag object、target、E、M、P 身份冲突 | fail closed |
|
||||
| lightweight tag | reject |
|
||||
| same-name different-OID tag | MANUAL_OWNER_REVIEW_REQUIRED |
|
||||
| T 不是要求的 E/M lineage ancestor | blocked / Owner review |
|
||||
| approval plan 未与 release identity 关联 | reject |
|
||||
| release mode 缺少 binding | ARTIFACT_RELEASE_BINDING_REQUIRED |
|
||||
| artifact registry digest 或 metadata 不匹配 | ReleaseBlocked |
|
||||
| projection 可见但 runtime bundle 缺版本 | RUNTIME_BUNDLE_MISSING |
|
||||
| runtime、projection、Git identity 不一致 | IDENTITY_MISMATCH |
|
||||
| post-deploy source registry recheck 失败 | production acceptance blocked |
|
||||
|
||||
## 10. Legacy V2R2C1 兼容
|
||||
|
||||
旧 V2R2C1 receipt 可以缺少 tag_oid、binding_schema、source_registry_main_oid、source_registry_path 和 source_registry_sha256。future binding 或 provenance-enabled binding 一旦提供 source provenance 字段,就必须完整提供并严格校验。
|
||||
|
||||
兼容只允许旧 receipt 按旧语义读取,不允许新 release 回退到弱绑定模型,也不允许修改 versions/V2R2C1.json 的历史发布身份。
|
||||
|
||||
## 11. 当前文档与生产边界
|
||||
|
||||
文档 follow-up 分支的父提交为 869cc007649a39035f5b74513cecc55bb1ae673f,本次只允许五个 Markdown 路径。文档分支写入和远端回读是独立的 documentation closure;mechanism/release production write、main merge、Production release、Baseline update、tag rewrite、force push 与 production service mutation 均不在本任务中。
|
||||
@@ -0,0 +1,169 @@
|
||||
# AIOS V2R2C1 Pre-Push Review Fix Requirements(最新机制语义版)
|
||||
|
||||
## 1. 目的
|
||||
|
||||
本需求记录 V2R2C1 Pre-Push Review 机制从初始 b660e2c 修复到最新 mechanism HEAD 的完整事实边界。它描述 release provenance、source registry、artifact、approval 与 runtime convergence 的可证明关系,不宣称新的生产发布。
|
||||
|
||||
三个增量范围必须分开:
|
||||
|
||||
- 初始 b660e2c 修复:14 个源码/测试文件。
|
||||
- b660e2c → 51b5f0e → 52b74ba 的完整审计:17 个路径,+1403/-79。
|
||||
- 最新 51b5f0e → 52b74ba tip 增量:12 个路径,+334/-39。
|
||||
|
||||
当前机制参考为 52b74ba61040246fffa8b10011c1199830e3d665。文档 follow-up 以文档分支原 HEAD 869cc007649a39035f5b74513cecc55bb1ae673f 为父提交。
|
||||
|
||||
## 2. 范围
|
||||
|
||||
实现和审计覆盖:
|
||||
|
||||
- pinned source registry snapshot 与 convergence;
|
||||
- governed release artifact build;
|
||||
- release executor 的独立 provenance 复核;
|
||||
- approval authority 绑定;
|
||||
- release/general artifact binding;
|
||||
- version API convergence 可观测性;
|
||||
- TOCTOU、identity mismatch 与 legacy receipt 回归测试;
|
||||
- 最新机制状态下的 README、requirements、architecture、acceptance、version differences 文档语义。
|
||||
|
||||
不在本次范围内:
|
||||
|
||||
- 修改 main;
|
||||
- merge;
|
||||
- Production release;
|
||||
- Production Baseline 或 versions/V2R2C1.json;
|
||||
- 创建、删除或重写正式 tag;
|
||||
- force push;
|
||||
- production runtime 修改;
|
||||
- systemd / nginx 生产服务切换、reload 或 restart;
|
||||
- 提交任何密钥。
|
||||
|
||||
## 3. 功能需求
|
||||
|
||||
### R1 — Source registry 固定 M
|
||||
|
||||
一次构建只能解析一次 source registry 所在 main commit M。后续 registry 校验、字节读取、SHA-256 计算与 artifact 打包必须使用 M 下同一 Git blob,禁止在同一构建中重新读取 mutable refs/heads/main。
|
||||
|
||||
验收要求:构建期间 main 前进时,artifact 仍绑定最初固定 M 的 registry 字节。
|
||||
|
||||
### R2 — Release provenance 是复合身份
|
||||
|
||||
release identity 至少包含:
|
||||
|
||||
- repository R;
|
||||
- version;
|
||||
- annotated tag object A;
|
||||
- release target / peeled target T;
|
||||
- evidence closure E;
|
||||
- Owner-approved approval plan P。
|
||||
|
||||
启用 source-registry provenance 时还必须包含:
|
||||
|
||||
- pinned source-registry main commit M;
|
||||
- source registry path;
|
||||
- source registry 精确字节的 SHA-256。
|
||||
|
||||
### R3 — Git object 与 lineage 可证明
|
||||
|
||||
代码必须验证:
|
||||
|
||||
- R 的仓库身份为 aios/aios;
|
||||
- refs/tags/<version> 指向 annotated tag object A;
|
||||
- A^commit 等于 T;
|
||||
- T 与 registry 中的 peeled target 一致;
|
||||
- E 是 commit;
|
||||
- T 是 E 的 ancestor;
|
||||
- M 是 commit;
|
||||
- 适用的 source-registry convergence 检查证明 T 到 M 的 lineage,并证明 M ancestor-of 当前权威 main/reference commit C;
|
||||
- M:path 的字节与 C:path 的字节在 convergence 检查中一致。
|
||||
|
||||
关键身份冲突不得自动修复,必须进入 fail-closed 或 Owner review。
|
||||
|
||||
### R4 — Approval plan 必须绑定 release authority
|
||||
|
||||
仅有格式正确的 approval_plan_id 不足以授权。generator、governed caller 与 executor 必须读取权威 approval/publication receipt,并证明 authority 与 R、version、A、T、E、P 关联。缺少关联、来源不可信或字段不一致时必须拒绝。
|
||||
|
||||
### R5 — Source registry generator 绑定权威仓库
|
||||
|
||||
generate_source_registry.py 必须显式获得 authoritative repository、approval authority 与 repository identity。生成前校验 Git identity、tag object、target、lineage 与 approval authority,不得只凭本地参数生成表面合法的 registry。
|
||||
|
||||
### R6 — Release artifact builder 强制 binding
|
||||
|
||||
artifact builder 在 release mode 必须收到 expected release identity,并逐字段比较:
|
||||
|
||||
- version;
|
||||
- repository;
|
||||
- tag_oid;
|
||||
- target_oid;
|
||||
- peeled_target_oid;
|
||||
- evidence_closure_oid;
|
||||
- approval_plan_id;
|
||||
- provenance-enabled 场景下的 M、path、sha256。
|
||||
|
||||
release mode 缺少 binding 或字段不匹配必须 fail closed。general artifact 仍可使用其独立的非 release 语义,不能与 release binding 混淆。
|
||||
|
||||
### R7 — Executor 独立复核 provenance
|
||||
|
||||
生产 executor 不得只信任 builder 自述。它必须独立验证 source registry member、canonical JSON、registry identity、path、精确字节 SHA-256、BUILD-METADATA.json provenance、future binding 的 tag object 与 authorization receipt。executor 还必须以自己的 Git identity 重新核对关键对象。
|
||||
|
||||
### R8 — V2R2C1 legacy receipt 兼容
|
||||
|
||||
旧 V2R2C1 receipt 可以省略 future-only provenance 字段。新字段仅在 future release binding 或 provenance-enabled binding 中强制。兼容路径不得削弱新 binding 的严格校验,也不得修改历史 versions/V2R2C1.json。
|
||||
|
||||
### R9 — 版本可见性不得冒充 convergence
|
||||
|
||||
版本 API 必须分别暴露:
|
||||
|
||||
- legacy display list;
|
||||
- runtime bundle versions;
|
||||
- attested projection version;
|
||||
- runtime bundle 是否包含该版本;
|
||||
- source registry 是否收敛;
|
||||
- registry consistency status。
|
||||
|
||||
projection-only 可见时必须报告 RUNTIME_BUNDLE_MISSING 或等价非收敛状态,不能仅因 overlay 可见就报告 source convergence。
|
||||
|
||||
### R10 — 回归测试覆盖 TOCTOU 与身份错误
|
||||
|
||||
至少覆盖:
|
||||
|
||||
- pinned source registry 后 main 前进;
|
||||
- lightweight tag 拒绝;
|
||||
- annotated tag 与 lineage 正常;
|
||||
- approval authority 缺失关联;
|
||||
- release binding 每个身份字段不匹配;
|
||||
- release mode 缺少 binding;
|
||||
- executor 对 source digest 与 build metadata 的独立复核;
|
||||
- 同名不同 tag object 进入 MANUAL_OWNER_REVIEW_REQUIRED;
|
||||
- projection-only 不等于 runtime convergence;
|
||||
- legacy V2R2C1 receipt 保持兼容。
|
||||
|
||||
## 4. 当前验收状态
|
||||
|
||||
| 项目 | 当前结果 |
|
||||
| --- | --- |
|
||||
| Code/mechanism fix | COMPLETE |
|
||||
| Documentation branch write | COMPLETE after this follow-up |
|
||||
| Documentation remote readback | COMPLETE after this follow-up |
|
||||
| Related test result | 760 passed |
|
||||
| Skipped | 2 skipped |
|
||||
| Full test result | 1 failed |
|
||||
| Subtests | 154 passed |
|
||||
| Full external gate | PARTIAL |
|
||||
| Signer gate | FAIL / EXTERNAL |
|
||||
| Production ready | false |
|
||||
| Owner review | REQUIRED |
|
||||
|
||||
唯一失败为 tests/test_signer_contract_compatibility.py::test_real_signer_help_and_self_test_are_available。/usr/local/libexec/aios-grant-signer-adapter self-test 返回 42,两个独立重跑仍返回 42。该事实不能被解释为暂态成功,也不能将全量结果记为全通过。
|
||||
|
||||
## 5. 安全与治理边界
|
||||
|
||||
- force_push_used = false
|
||||
- tag_rewritten = false
|
||||
- direct_main_push = false
|
||||
- secrets_committed = false
|
||||
- production_mutation = false
|
||||
- legacy_global_writes_enabled = false
|
||||
- 未经 Owner review 不执行 merge 或 consequential production action。
|
||||
- 文档分支写入、Git/Gitea 远端回读与生产发布分别验收。
|
||||
- 任何 same-name / different-OID tag 冲突都进入 MANUAL_OWNER_REVIEW_REQUIRED。
|
||||
- 文档刷新不改写已投产 V2R2C1 registry identity。
|
||||
@@ -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,64 @@
|
||||
# V2R2C1 与 Pre-Push Mechanism Fix 版本差异(最新机制语义版)
|
||||
|
||||
## 1. 状态轴
|
||||
|
||||
| 状态 | OID / Version | 说明 |
|
||||
| --- | --- | --- |
|
||||
| Production Baseline | V2R2C1 / generation 4 | PRODUCTION_DEPLOYED / ACTIVE |
|
||||
| V2R2C1 tag object | 07fc2746b688f5f243f29b39de39f32edc3dd9d2 | annotated tag object |
|
||||
| V2R2C1 release target T | 998d30304787e31e5ae607c0bd86b5edb2b0e5ad | peeled target |
|
||||
| Production evidence closure E | a8b906d1bc24088c19647e98de6a679268f6d0a2 | Baseline 记录的证据闭包 |
|
||||
| Authoritative main C | 3244f0f17f8845592ac94d585f5ebd65693c9f29 | 当前权威工程主线 |
|
||||
| Latest mechanism HEAD | 52b74ba61040246fffa8b10011c1199830e3d665 | 非 main 待审机制分支 |
|
||||
| Documentation follow-up parent | 869cc007649a39035f5b74513cecc55bb1ae673f | 语义修复提交父提交 |
|
||||
|
||||
以上状态不可互相替代。52b74ba 是最新 mechanism reference,不是新的 Production Baseline;文档 branch 也不是 main。
|
||||
|
||||
## 2. 三层差异范围
|
||||
|
||||
- Initial b660e2c review fix:14 个文件。
|
||||
- 完整 b660e2c → 51b5f0e → 52b74ba 审计:17 个路径,+1403/-79。
|
||||
- 最新 51b5f0e → 52b74ba 增量:12 个路径,+334/-39。
|
||||
|
||||
## 3. 已发布 V2R2C1 机制
|
||||
|
||||
V2R2C1 保留其 production baseline、annotated tag object、peeled target、artifact、evidence closure、approval、immutable staging 与 release executor 身份。历史 identity 保持不可变,versions/V2R2C1.json 不在本次修改范围。
|
||||
|
||||
## 4. 最新机制增量
|
||||
|
||||
52b74ba 所在机制链进一步明确:
|
||||
|
||||
1. pinned source registry:一次解析 M,后续统一使用 M:path 的精确字节与 SHA-256。
|
||||
2. composite provenance:R、version、A、T、E、P 与 M、path、sha256 共同构成 release binding。
|
||||
3. Git object proof:区分 annotated tag object A、peeled target T、evidence closure E、pinned main M 与当前 main/reference C。
|
||||
4. authority proof:approval plan 必须与 release identity 真实关联。
|
||||
5. registry generator hardening:generator 接入权威 bare repository 与 approval authority。
|
||||
6. artifact binding:release mode 强制完整 binding,general artifact 保持独立语义。
|
||||
7. executor independent verification:executor 重新校验 registry bytes、metadata、tag object、authority 与 Git identity。
|
||||
8. convergence observability:区分 display list、runtime bundle、attested projection 与 source convergence。
|
||||
9. post-deploy recheck:部署后重新读取 authoritative Git、source registry 与 runtime projection。
|
||||
10. fail-closed:身份、digest、lineage、approval、binding 与 projection 不一致时停止自动流程。
|
||||
11. legacy compatibility:旧 V2R2C1 receipt 可继续读取,新 binding 不回退到弱校验。
|
||||
|
||||
## 5. 兼容性差异
|
||||
|
||||
| 项目 | Legacy V2R2C1 | Future / provenance-enabled |
|
||||
| --- | --- | --- |
|
||||
| tag_oid in executor binding | 可缺省 | 必须 |
|
||||
| binding schema | 可缺省 | aios.release-binding.* |
|
||||
| source registry main OID | 可缺省 | provenance 开启时必须 |
|
||||
| source registry SHA-256 | 可缺省 | provenance 开启时必须 |
|
||||
| source registry path | 默认/兼容 | 明确验证 |
|
||||
| artifact build release binding | 旧路径兼容 | release mode 强制 |
|
||||
| same-name different tag | 不允许重写 | MANUAL_OWNER_REVIEW_REQUIRED |
|
||||
| post-deploy registry check | 旧 receipt 可无 | 新 release 必须 |
|
||||
|
||||
## 6. 当前验收状态
|
||||
|
||||
相关测试为 760 passed、2 skipped、1 failed,154 subtests passed。唯一失败为 tests/test_signer_contract_compatibility.py::test_real_signer_help_and_self_test_are_available;self-test 返回 42,两个独立重跑仍返回 42。因此 Signer gate 为 FAIL / EXTERNAL,Full external gate 为 PARTIAL,Production ready 为 false。
|
||||
|
||||
文档分支 write 与 remote readback 属于 COMPLETE 的独立 documentation closure。mechanism/release production write、main merge、Production release、Baseline update、tag rewrite 与 force push 均为 NOT_RUN。Owner review 仍为 REQUIRED。
|
||||
|
||||
## 7. 处理边界
|
||||
|
||||
不修改 versions/V2R2C1.json,不修改历史 tag 或 Production Baseline,不直接写 main,不执行 merge,不改变生产 runtime。若 Owner review 后需要正式交付机制,应建立新的受治理 release/version identity,并重新执行独立 authority 验收。
|
||||
@@ -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,24 @@
|
||||
{
|
||||
"schema": "aios.source-registry-reconciliation.v1",
|
||||
"version": "V2R2C1",
|
||||
"root_cause": "PROMOTION_STATE_MACHINE_FINALIZED_BEFORE_SOURCE_REGISTRY_CONVERGENCE",
|
||||
"main_before": "3244f0f17f8845592ac94d585f5ebd65693c9f29",
|
||||
"tag_oid": "07fc2746b688f5f243f29b39de39f32edc3dd9d2",
|
||||
"target_oid": "998d30304787e31e5ae607c0bd86b5edb2b0e5ad",
|
||||
"projection_sha256": "924dfc0ec58afc4e31638f6538e551c5bc8858f804cdbee5af7dce6eba7c90fe",
|
||||
"evidence_closure_oid": "a8b906d1bc24088c19647e98de6a679268f6d0a2",
|
||||
"approval_plan_id": "sha256:c0df52008ae7f64cec5ee2d28947aa7b892066c670ab8295e4549140b1a7500e",
|
||||
"remote_tag_verified": true,
|
||||
"remote_target_verified": true,
|
||||
"gitea_api_verified_by_owner_acceptance": true,
|
||||
"source_entry_before": "missing",
|
||||
"source_entry_added": "versions/V2R2C1.json",
|
||||
"source_entry_is_exact_attested_projection": true,
|
||||
"release_manifest_added": false,
|
||||
"release_manifest_reason": "Historical release-manifest schema differs; no canonical V2R2C1 source-manifest generator was proven. Do not synthesize one by copying old release values.",
|
||||
"runtime_backing": "immutable WS/versions bundle",
|
||||
"runtime_mutated": false,
|
||||
"tag_rewritten": false,
|
||||
"baseline_modified": false,
|
||||
"production_modified": false
|
||||
}
|
||||
@@ -32,18 +32,36 @@ def build_parser() -> argparse.ArgumentParser:
|
||||
parser.add_argument("--tree-oid", required=True)
|
||||
parser.add_argument("--output-directory", required=True, type=Path)
|
||||
parser.add_argument("--name", default="aios-autonomous-release-candidate")
|
||||
parser.add_argument("--release-version")
|
||||
parser.add_argument("--release-binding-json", type=Path)
|
||||
parser.add_argument("--artifact-mode", choices=("release", "general"))
|
||||
return parser
|
||||
|
||||
|
||||
def main(argv: list[str] | None = None) -> int:
|
||||
args = build_parser().parse_args(argv)
|
||||
release_binding = (
|
||||
json.loads(args.release_binding_json.read_text(encoding="utf-8"))
|
||||
if args.release_binding_json is not None
|
||||
else None
|
||||
)
|
||||
if release_binding is not None and not isinstance(release_binding, dict):
|
||||
raise ValueError("release binding JSON must be an object")
|
||||
output = args.output_directory
|
||||
output.mkdir(parents=True, exist_ok=True, mode=0o750)
|
||||
if output.is_symlink():
|
||||
raise RuntimeError("artifact output directory symlink rejected")
|
||||
os.chmod(output, output.stat().st_mode & 0o750)
|
||||
builder = ArtifactBuilder()
|
||||
built = builder.build_from_directory(args.source_oid, args.tree_oid, args.source)
|
||||
built = builder.build_from_directory(
|
||||
args.source_oid,
|
||||
args.tree_oid,
|
||||
args.source,
|
||||
release_version=args.release_version,
|
||||
release_binding=release_binding,
|
||||
release_mode=args.release_version is not None,
|
||||
artifact_mode=args.artifact_mode,
|
||||
)
|
||||
builder.verify(built)
|
||||
artifact_path = output / f"{args.name}.tar"
|
||||
manifest_path = output / f"{args.name}.manifest.json"
|
||||
|
||||
@@ -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())
|
||||
Executable
+124
@@ -0,0 +1,124 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Generate one canonical source entry for a governed non-main change.
|
||||
|
||||
This utility only creates the file in the caller's worktree. It never commits,
|
||||
pushes, changes main, rewrites a tag, or updates a production projection. The
|
||||
caller must submit the resulting file through the existing governed writer and
|
||||
approval flow.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
APP = Path(__file__).resolve().parents[1] / "active-runtime" / "app"
|
||||
sys.path.insert(0, str(APP))
|
||||
|
||||
from version_registry import ( # noqa: E402
|
||||
build_source_registry_entry,
|
||||
configured_authoritative_git_dir,
|
||||
configured_repository_identity,
|
||||
source_registry_json,
|
||||
validate_source_registry_record,
|
||||
verify_approval_authority,
|
||||
verify_authoritative_repository,
|
||||
verify_source_registry_git_identity,
|
||||
)
|
||||
|
||||
|
||||
def parser() -> argparse.ArgumentParser:
|
||||
value = argparse.ArgumentParser()
|
||||
value.add_argument("--output", required=True, type=Path)
|
||||
value.add_argument("--version", required=True)
|
||||
value.add_argument("--tag-oid", required=True)
|
||||
value.add_argument("--target-oid", required=True)
|
||||
value.add_argument("--peeled-target-oid", required=True)
|
||||
value.add_argument("--evidence-closure-oid", required=True)
|
||||
value.add_argument("--approval-plan-id", required=True)
|
||||
value.add_argument(
|
||||
"--repository",
|
||||
type=Path,
|
||||
default=configured_authoritative_git_dir(),
|
||||
help="authoritative bare repository; required for release generation",
|
||||
)
|
||||
value.add_argument(
|
||||
"--approval-authority",
|
||||
type=Path,
|
||||
default=(
|
||||
Path(os.environ["AIOS_GITOPS_APPROVAL_AUTHORITY"])
|
||||
if os.environ.get("AIOS_GITOPS_APPROVAL_AUTHORITY")
|
||||
else None
|
||||
),
|
||||
help="canonical approval/publication receipt for this release request",
|
||||
)
|
||||
value.add_argument(
|
||||
"--repository-identity",
|
||||
default=configured_repository_identity(),
|
||||
)
|
||||
return value
|
||||
|
||||
|
||||
def main() -> int:
|
||||
args = parser().parse_args()
|
||||
if args.approval_authority is None:
|
||||
raise ValueError("canonical approval authority is required")
|
||||
verify_authoritative_repository(
|
||||
args.repository,
|
||||
repository_identity=args.repository_identity,
|
||||
)
|
||||
tag_oid = args.tag_oid.lower()
|
||||
target_oid = args.target_oid.lower()
|
||||
peeled_target_oid = args.peeled_target_oid.lower()
|
||||
evidence_closure_oid = args.evidence_closure_oid.lower()
|
||||
expected = build_source_registry_entry(
|
||||
version=args.version,
|
||||
tag_oid=tag_oid,
|
||||
target_oid=target_oid,
|
||||
peeled_target_oid=peeled_target_oid,
|
||||
evidence_closure_oid=evidence_closure_oid,
|
||||
approval_plan_id=args.approval_plan_id,
|
||||
repository=args.repository_identity,
|
||||
)
|
||||
verify_source_registry_git_identity(args.repository, expected)
|
||||
verify_approval_authority(args.approval_authority, expected)
|
||||
destination = args.output.resolve()
|
||||
if destination.name != f"{args.version}.json" or destination.parent.name != "versions":
|
||||
raise ValueError("output must be versions/<version>.json")
|
||||
payload = source_registry_json(expected)
|
||||
destination.parent.mkdir(parents=True, exist_ok=True)
|
||||
if destination.is_symlink():
|
||||
raise ValueError("existing source registry path must not be a symlink")
|
||||
if destination.exists() and not destination.is_file():
|
||||
raise ValueError("existing source registry path must be a regular file")
|
||||
if destination.exists():
|
||||
current = json.loads(destination.read_text(encoding="utf-8"))
|
||||
validate_source_registry_record(
|
||||
current,
|
||||
expected_version=args.version,
|
||||
expected_tag_oid=tag_oid,
|
||||
expected_target_oid=target_oid,
|
||||
expected_evidence_closure_oid=evidence_closure_oid,
|
||||
expected_approval_plan_id=args.approval_plan_id,
|
||||
)
|
||||
print(json.dumps({"status": "EXISTING_IDENTITY_VERIFIED", "path": str(destination)}))
|
||||
return 0
|
||||
descriptor = os.open(
|
||||
destination,
|
||||
os.O_WRONLY | os.O_CREAT | os.O_EXCL | getattr(os, "O_NOFOLLOW", 0),
|
||||
0o640,
|
||||
)
|
||||
try:
|
||||
os.write(descriptor, payload)
|
||||
os.fsync(descriptor)
|
||||
finally:
|
||||
os.close(descriptor)
|
||||
print(json.dumps({"status": "GENERATED", "path": str(destination)}))
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
+200
-1
@@ -8,6 +8,7 @@ import io
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path, PurePosixPath
|
||||
import re
|
||||
import stat
|
||||
import tarfile
|
||||
from typing import Mapping
|
||||
@@ -75,6 +76,30 @@ class BuiltArtifact:
|
||||
manifest: ArtifactManifest
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class ReleaseIdentityBinding:
|
||||
"""Immutable release identity required for a release artifact build."""
|
||||
|
||||
version: str
|
||||
repository: str
|
||||
tag_oid: str
|
||||
target_oid: str
|
||||
evidence_closure_oid: str
|
||||
approval_plan_id: str
|
||||
peeled_target_oid: str | None = None
|
||||
|
||||
def as_dict(self) -> dict[str, str]:
|
||||
return {
|
||||
"version": self.version,
|
||||
"repository": self.repository,
|
||||
"tag_oid": self.tag_oid,
|
||||
"target_oid": self.target_oid,
|
||||
"peeled_target_oid": self.peeled_target_oid or self.target_oid,
|
||||
"evidence_closure_oid": self.evidence_closure_oid,
|
||||
"approval_plan_id": self.approval_plan_id,
|
||||
}
|
||||
|
||||
|
||||
class ArtifactBuilder:
|
||||
MAX_FILE_BYTES = 8 * 1024 * 1024
|
||||
MAX_ARTIFACT_BYTES = 64 * 1024 * 1024
|
||||
@@ -92,14 +117,188 @@ class ArtifactBuilder:
|
||||
if not is_git_oid(value):
|
||||
raise ValueError(f"{name} must be a full lowercase Git OID")
|
||||
|
||||
@staticmethod
|
||||
def _binding_dict(
|
||||
binding: ReleaseIdentityBinding | Mapping[str, str],
|
||||
) -> dict[str, str]:
|
||||
if isinstance(binding, ReleaseIdentityBinding):
|
||||
return binding.as_dict()
|
||||
values = {str(key): str(value) for key, value in binding.items()}
|
||||
values.setdefault("peeled_target_oid", values.get("target_oid", ""))
|
||||
return values
|
||||
|
||||
@classmethod
|
||||
def _validate_source_registry(
|
||||
cls,
|
||||
root: Path,
|
||||
release_version: str,
|
||||
release_binding: ReleaseIdentityBinding | Mapping[str, str] | None = None,
|
||||
*,
|
||||
release_mode: bool = False,
|
||||
) -> dict[str, str]:
|
||||
if not re.fullmatch(r"V\d+R\d+C\d+(?:-[A-Za-z0-9_.-]+)?", release_version):
|
||||
raise ReleaseBlocked(
|
||||
"ARTIFACT_SOURCE_REGISTRY_INVALID", "release version is not canonical"
|
||||
)
|
||||
path = root / "versions" / f"{release_version}.json"
|
||||
if path.is_symlink() or not path.is_file():
|
||||
raise ReleaseBlocked(
|
||||
"ARTIFACT_SOURCE_REGISTRY_MISSING",
|
||||
f"artifact source is missing versions/{release_version}.json",
|
||||
)
|
||||
try:
|
||||
value = json.loads(path.read_text(encoding="utf-8"))
|
||||
except (OSError, UnicodeDecodeError, json.JSONDecodeError) as exc:
|
||||
raise ReleaseBlocked(
|
||||
"ARTIFACT_SOURCE_REGISTRY_INVALID",
|
||||
f"artifact source registry cannot be read: {path}",
|
||||
) from exc
|
||||
if not isinstance(value, dict):
|
||||
raise ReleaseBlocked(
|
||||
"ARTIFACT_SOURCE_REGISTRY_INVALID", "source registry is not an object"
|
||||
)
|
||||
if value.get("version") != release_version or value.get("repository") != "aios/aios":
|
||||
raise ReleaseBlocked(
|
||||
"ARTIFACT_SOURCE_REGISTRY_IDENTITY_MISMATCH",
|
||||
"source registry version or repository does not match the artifact",
|
||||
)
|
||||
if value.get("schema") not in {
|
||||
"aios.source-registry.v1",
|
||||
"aios.production-registry.v2",
|
||||
}:
|
||||
raise ReleaseBlocked(
|
||||
"ARTIFACT_SOURCE_REGISTRY_INVALID", "source registry schema is invalid"
|
||||
)
|
||||
for field in ("tag_oid", "target_oid", "peeled_target_oid", "evidence_closure_oid"):
|
||||
item = value.get(field)
|
||||
if not isinstance(item, str) or not re.fullmatch(r"[0-9a-f]{40}", item):
|
||||
raise ReleaseBlocked(
|
||||
"ARTIFACT_SOURCE_REGISTRY_INVALID",
|
||||
f"source registry field is invalid: {field}",
|
||||
)
|
||||
if value["peeled_target_oid"] != value["target_oid"]:
|
||||
raise ReleaseBlocked(
|
||||
"ARTIFACT_SOURCE_REGISTRY_IDENTITY_MISMATCH",
|
||||
"source registry peeled target differs from target",
|
||||
)
|
||||
if not isinstance(value.get("approval_plan_id"), str) or not re.fullmatch(
|
||||
r"sha256:[0-9a-f]{64}", value["approval_plan_id"]
|
||||
):
|
||||
raise ReleaseBlocked(
|
||||
"ARTIFACT_SOURCE_REGISTRY_INVALID", "source registry approval plan is invalid"
|
||||
)
|
||||
if release_mode and release_binding is None:
|
||||
raise ReleaseBlocked(
|
||||
"ARTIFACT_RELEASE_BINDING_REQUIRED",
|
||||
"release artifact builds require an expected release identity",
|
||||
)
|
||||
if release_binding is not None:
|
||||
expected = cls._binding_dict(release_binding)
|
||||
required = (
|
||||
"version",
|
||||
"repository",
|
||||
"tag_oid",
|
||||
"target_oid",
|
||||
"peeled_target_oid",
|
||||
"evidence_closure_oid",
|
||||
"approval_plan_id",
|
||||
)
|
||||
if any(not expected.get(name) for name in required):
|
||||
raise ReleaseBlocked(
|
||||
"ARTIFACT_RELEASE_BINDING_INVALID",
|
||||
"expected release identity is incomplete",
|
||||
)
|
||||
for name in ("tag_oid", "target_oid", "peeled_target_oid", "evidence_closure_oid"):
|
||||
if not re.fullmatch(r"[0-9a-f]{40}", expected[name]):
|
||||
raise ReleaseBlocked(
|
||||
"ARTIFACT_RELEASE_BINDING_INVALID",
|
||||
f"expected release identity field is invalid: {name}",
|
||||
)
|
||||
if not re.fullmatch(r"sha256:[0-9a-f]{64}", expected["approval_plan_id"]):
|
||||
raise ReleaseBlocked(
|
||||
"ARTIFACT_RELEASE_BINDING_INVALID",
|
||||
"expected release identity approval plan is invalid",
|
||||
)
|
||||
for name in required:
|
||||
if value.get(name) != expected[name]:
|
||||
raise ReleaseBlocked(
|
||||
"ARTIFACT_SOURCE_REGISTRY_IDENTITY_MISMATCH",
|
||||
f"source registry does not match expected release identity: {name}",
|
||||
)
|
||||
return {
|
||||
"version": str(value["version"]),
|
||||
"repository": str(value["repository"]),
|
||||
"tag_oid": str(value["tag_oid"]),
|
||||
"target_oid": str(value["target_oid"]),
|
||||
"peeled_target_oid": str(value["peeled_target_oid"]),
|
||||
"evidence_closure_oid": str(value["evidence_closure_oid"]),
|
||||
"approval_plan_id": str(value["approval_plan_id"]),
|
||||
}
|
||||
|
||||
def build_from_directory(
|
||||
self, source_oid: str, tree_oid: str, source: str | Path
|
||||
self,
|
||||
source_oid: str,
|
||||
tree_oid: str,
|
||||
source: str | Path,
|
||||
release_version: str | None = None,
|
||||
release_binding: ReleaseIdentityBinding | Mapping[str, str] | None = None,
|
||||
release_mode: bool | None = None,
|
||||
artifact_mode: str | None = None,
|
||||
) -> BuiltArtifact:
|
||||
root = Path(source)
|
||||
if root.is_symlink() or not root.is_dir():
|
||||
raise ReleaseBlocked(
|
||||
"ARTIFACT_SOURCE_LINK_REJECTED", "artifact source must be a real directory"
|
||||
)
|
||||
if artifact_mode not in {None, "release", "general"}:
|
||||
raise ValueError("artifact_mode must be release, general, or omitted")
|
||||
if artifact_mode == "release" and release_mode is False:
|
||||
raise ValueError("artifact_mode=release conflicts with release_mode=False")
|
||||
if artifact_mode == "general" and release_mode is True:
|
||||
raise ValueError("artifact_mode=general conflicts with release_mode=True")
|
||||
|
||||
explicit_release_version = release_version is not None
|
||||
detected_canonical_version = False
|
||||
if release_version is None:
|
||||
version_file = root / "VERSION"
|
||||
if version_file.is_file() and not version_file.is_symlink():
|
||||
release_version = version_file.read_text(encoding="utf-8").strip()
|
||||
detected_canonical_version = bool(
|
||||
re.fullmatch(r"V\d+R\d+C\d+(?:-[A-Za-z0-9_.-]+)?", release_version)
|
||||
)
|
||||
elif re.fullmatch(r"V\d+R\d+C\d+(?:-[A-Za-z0-9_.-]+)?", release_version):
|
||||
detected_canonical_version = True
|
||||
|
||||
binding_expected: dict[str, str] | None = None
|
||||
if release_binding is not None:
|
||||
binding_expected = self._binding_dict(release_binding)
|
||||
if not binding_expected.get("version"):
|
||||
raise ReleaseBlocked(
|
||||
"ARTIFACT_RELEASE_BINDING_INVALID",
|
||||
"release identity binding must include version",
|
||||
)
|
||||
release_version = release_version or binding_expected["version"]
|
||||
|
||||
release_requested = bool(
|
||||
explicit_release_version
|
||||
or detected_canonical_version
|
||||
or release_binding is not None
|
||||
or release_mode is True
|
||||
or artifact_mode == "release"
|
||||
)
|
||||
if artifact_mode == "general" and release_requested:
|
||||
raise ReleaseBlocked(
|
||||
"ARTIFACT_GENERAL_MODE_RELEASE_VERSION",
|
||||
"general artifacts cannot contain canonical release metadata",
|
||||
)
|
||||
if release_requested:
|
||||
release_mode = True
|
||||
self._validate_source_registry(
|
||||
root,
|
||||
release_version or "",
|
||||
release_binding,
|
||||
release_mode=True,
|
||||
)
|
||||
files: list[ArtifactFile] = []
|
||||
for directory, dirnames, filenames in os.walk(root, followlinks=False):
|
||||
current = Path(directory)
|
||||
|
||||
+17
-1
@@ -61,6 +61,9 @@ def build_parser() -> argparse.ArgumentParser:
|
||||
artifact_build.add_argument("--source-oid", required=True)
|
||||
artifact_build.add_argument("--tree-oid", required=True)
|
||||
artifact_build.add_argument("--output-directory", required=True, type=Path)
|
||||
artifact_build.add_argument("--release-version")
|
||||
artifact_build.add_argument("--release-binding-json", type=Path)
|
||||
artifact_build.add_argument("--artifact-mode", choices=("release", "general"))
|
||||
artifact_verify = artifact_sub.add_parser("verify")
|
||||
artifact_verify.add_argument("--artifact", required=True, type=Path)
|
||||
artifact_verify.add_argument("--manifest", required=True, type=Path)
|
||||
@@ -145,7 +148,20 @@ def main(argv: list[str] | None = None) -> int:
|
||||
if args.command == "artifact":
|
||||
builder = ArtifactBuilder()
|
||||
if args.artifact_command == "build":
|
||||
built = builder.build_from_directory(args.source_oid, args.tree_oid, args.source)
|
||||
release_binding = (
|
||||
load_json(args.release_binding_json)
|
||||
if args.release_binding_json is not None
|
||||
else None
|
||||
)
|
||||
built = builder.build_from_directory(
|
||||
args.source_oid,
|
||||
args.tree_oid,
|
||||
args.source,
|
||||
release_version=args.release_version,
|
||||
release_binding=release_binding,
|
||||
release_mode=args.release_version is not None,
|
||||
artifact_mode=args.artifact_mode,
|
||||
)
|
||||
builder.verify(built)
|
||||
artifact_path = args.output_directory / "candidate.tar"
|
||||
manifest_path = args.output_directory / "manifest.json"
|
||||
|
||||
@@ -2,7 +2,7 @@ from __future__ import annotations
|
||||
import hashlib,json,re,tarfile
|
||||
from pathlib import Path
|
||||
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"}
|
||||
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):
|
||||
@@ -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 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 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
|
||||
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
|
||||
|
||||
@@ -3,13 +3,13 @@ from __future__ import annotations
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
ROOT = Path(__file__).parents[2]
|
||||
|
||||
|
||||
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"))
|
||||
assert metadata["version"] == "V2R1C6"
|
||||
assert metadata["base_oid"] == "a33e2593967a180c89d44454350ccaeaf3f79b25"
|
||||
assert metadata["candidate_branch"] == "agent/v2r1c6-mainline-reconciliation-20260806"
|
||||
assert metadata["production_deployed"] is False
|
||||
|
||||
@@ -7,17 +7,19 @@ import pytest
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
sys.path.insert(0, str(ROOT / "active-runtime" / "app"))
|
||||
from deployment_state import RuntimeDeploymentState
|
||||
from v2r1c6_privacy_tools import health_payload, serve # noqa: E402
|
||||
|
||||
|
||||
def test_health_is_engineering_only() -> None:
|
||||
result = health_payload()
|
||||
assert result == {
|
||||
"status": "ok",
|
||||
"version": "V2R1C6",
|
||||
"production_deployed": False,
|
||||
"gitops_writes_enabled": False,
|
||||
}
|
||||
result = health_payload(RuntimeDeploymentState("V2R1C7", "a" * 40, "b" * 64, "STAGED"))
|
||||
assert result["status"] == "ok"
|
||||
assert result["version"] == "V2R1C7"
|
||||
assert result["commit_oid"] == "a" * 40
|
||||
assert result["artifact_sha256"] == "b" * 64
|
||||
assert result["deployment_state"] == "STAGED"
|
||||
assert result["production_deployed"] is False
|
||||
assert result["health_timestamp"]
|
||||
|
||||
|
||||
def test_listener_rejects_non_loopback() -> None:
|
||||
|
||||
@@ -1,11 +1,35 @@
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from aios_autonomous_release_candidate.artifact import ArtifactBuilder, ArtifactFile, BuiltArtifact
|
||||
from aios_autonomous_release_candidate.artifact import (
|
||||
ArtifactBuilder,
|
||||
ArtifactFile,
|
||||
BuiltArtifact,
|
||||
ReleaseIdentityBinding,
|
||||
)
|
||||
from aios_autonomous_release_candidate.errors import EvidenceIncomplete, ReleaseBlocked
|
||||
|
||||
|
||||
def _source_registry(root: Path) -> None:
|
||||
target = "2" * 40
|
||||
value = {
|
||||
"schema": "aios.source-registry.v1",
|
||||
"version": "V2R2C1",
|
||||
"repository": "aios/aios",
|
||||
"tag_oid": "1" * 40,
|
||||
"tag_object": "1" * 40,
|
||||
"target_oid": target,
|
||||
"peeled_target_oid": target,
|
||||
"evidence_closure_oid": "3" * 40,
|
||||
"approval_plan_id": "sha256:" + "4" * 64,
|
||||
}
|
||||
path = root / "versions" / "V2R2C1.json"
|
||||
path.parent.mkdir(parents=True)
|
||||
path.write_text(json.dumps(value) + "\n", encoding="utf-8")
|
||||
|
||||
|
||||
def test_artifact_is_deterministic_and_reverified() -> None:
|
||||
builder = ArtifactBuilder()
|
||||
files = (
|
||||
@@ -32,6 +56,143 @@ def test_source_tree_rejects_symlink_and_hardlink(tmp_path: Path) -> None:
|
||||
ArtifactBuilder().build_from_directory("a" * 40, "b" * 40, root)
|
||||
|
||||
|
||||
def test_release_source_tree_requires_source_registry_entry(tmp_path: Path) -> None:
|
||||
root = tmp_path / "source"
|
||||
root.mkdir()
|
||||
(root / "VERSION").write_text("V2R2C1\n", encoding="utf-8")
|
||||
with pytest.raises(ReleaseBlocked, match="ARTIFACT_SOURCE_REGISTRY_MISSING"):
|
||||
ArtifactBuilder().build_from_directory("a" * 40, "b" * 40, root)
|
||||
|
||||
|
||||
def test_canonical_version_without_binding_fails_closed(tmp_path: Path) -> None:
|
||||
root = tmp_path / "source"
|
||||
root.mkdir()
|
||||
(root / "VERSION").write_text("V2R2C1\n", encoding="utf-8")
|
||||
_source_registry(root)
|
||||
with pytest.raises(ReleaseBlocked, match="ARTIFACT_RELEASE_BINDING_REQUIRED"):
|
||||
ArtifactBuilder().build_from_directory("a" * 40, "b" * 40, root)
|
||||
|
||||
|
||||
def test_release_source_tree_requires_expected_release_identity(tmp_path: Path) -> None:
|
||||
root = tmp_path / "source"
|
||||
root.mkdir()
|
||||
(root / "VERSION").write_text("V2R2C1\n", encoding="utf-8")
|
||||
_source_registry(root)
|
||||
binding = ReleaseIdentityBinding(
|
||||
version="V2R2C1",
|
||||
repository="aios/aios",
|
||||
tag_oid="1" * 40,
|
||||
target_oid="2" * 40,
|
||||
evidence_closure_oid="3" * 40,
|
||||
approval_plan_id="sha256:" + "4" * 64,
|
||||
)
|
||||
built = ArtifactBuilder().build_from_directory(
|
||||
"a" * 40,
|
||||
"b" * 40,
|
||||
root,
|
||||
release_binding=binding,
|
||||
)
|
||||
ArtifactBuilder().verify(built)
|
||||
|
||||
|
||||
def test_canonical_version_with_malformed_binding_fails_closed(tmp_path: Path) -> None:
|
||||
root = tmp_path / "source"
|
||||
root.mkdir()
|
||||
(root / "VERSION").write_text("V2R2C1\n", encoding="utf-8")
|
||||
_source_registry(root)
|
||||
with pytest.raises(ReleaseBlocked, match="ARTIFACT_RELEASE_BINDING_INVALID"):
|
||||
ArtifactBuilder().build_from_directory(
|
||||
"a" * 40,
|
||||
"b" * 40,
|
||||
root,
|
||||
release_binding={
|
||||
"version": "V2R2C1",
|
||||
"repository": "aios/aios",
|
||||
"tag_oid": "not-an-oid",
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
def test_explicit_general_artifact_without_release_version_passes(tmp_path: Path) -> None:
|
||||
root = tmp_path / "source"
|
||||
root.mkdir()
|
||||
(root / "README.txt").write_text("general candidate\n", encoding="utf-8")
|
||||
built = ArtifactBuilder().build_from_directory(
|
||||
"a" * 40,
|
||||
"b" * 40,
|
||||
root,
|
||||
artifact_mode="general",
|
||||
)
|
||||
assert "README.txt" in built.manifest.files
|
||||
|
||||
|
||||
def test_explicit_general_mode_rejects_canonical_version(tmp_path: Path) -> None:
|
||||
root = tmp_path / "source"
|
||||
root.mkdir()
|
||||
(root / "VERSION").write_text("V2R2C1\n", encoding="utf-8")
|
||||
_source_registry(root)
|
||||
with pytest.raises(ReleaseBlocked, match="ARTIFACT_GENERAL_MODE_RELEASE_VERSION"):
|
||||
ArtifactBuilder().build_from_directory(
|
||||
"a" * 40,
|
||||
"b" * 40,
|
||||
root,
|
||||
artifact_mode="general",
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"field,wrong",
|
||||
[
|
||||
("repository", "other/repository"),
|
||||
("version", "V2R2C2"),
|
||||
("tag_oid", "9" * 40),
|
||||
("target_oid", "8" * 40),
|
||||
("peeled_target_oid", "7" * 40),
|
||||
("evidence_closure_oid", "6" * 40),
|
||||
("approval_plan_id", "sha256:" + "5" * 64),
|
||||
],
|
||||
)
|
||||
def test_release_source_tree_rejects_every_identity_mismatch(
|
||||
tmp_path: Path, field: str, wrong: str
|
||||
) -> None:
|
||||
root = tmp_path / "source"
|
||||
root.mkdir()
|
||||
(root / "VERSION").write_text("V2R2C1\n", encoding="utf-8")
|
||||
_source_registry(root)
|
||||
binding = {
|
||||
"version": "V2R2C1",
|
||||
"repository": "aios/aios",
|
||||
"tag_oid": "1" * 40,
|
||||
"target_oid": "2" * 40,
|
||||
"peeled_target_oid": "2" * 40,
|
||||
"evidence_closure_oid": "3" * 40,
|
||||
"approval_plan_id": "sha256:" + "4" * 64,
|
||||
}
|
||||
binding[field] = wrong
|
||||
with pytest.raises(ReleaseBlocked, match="ARTIFACT_SOURCE_REGISTRY"):
|
||||
ArtifactBuilder().build_from_directory(
|
||||
"a" * 40,
|
||||
"b" * 40,
|
||||
root,
|
||||
release_binding=binding,
|
||||
)
|
||||
|
||||
|
||||
def test_release_mode_without_binding_fails_closed(tmp_path: Path) -> None:
|
||||
root = tmp_path / "source"
|
||||
root.mkdir()
|
||||
(root / "VERSION").write_text("V2R2C1\n", encoding="utf-8")
|
||||
_source_registry(root)
|
||||
with pytest.raises(ReleaseBlocked, match="ARTIFACT_RELEASE_BINDING_REQUIRED"):
|
||||
ArtifactBuilder().build_from_directory(
|
||||
"a" * 40,
|
||||
"b" * 40,
|
||||
root,
|
||||
release_version="V2R2C1",
|
||||
release_mode=True,
|
||||
)
|
||||
|
||||
|
||||
def test_artifact_rejects_secret_markers() -> None:
|
||||
with pytest.raises(ReleaseBlocked, match="ARTIFACT_SECRET_MARKER"):
|
||||
ArtifactBuilder().build(
|
||||
|
||||
@@ -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",
|
||||
)
|
||||
@@ -158,7 +158,7 @@ def test_execute_orders_registry_before_baseline_and_baseline_last(
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
"approved_release_executor.verify_artifact",
|
||||
lambda binding: {"verified": True},
|
||||
lambda binding, **kwargs: {"verified": True},
|
||||
)
|
||||
|
||||
calls = []
|
||||
|
||||
@@ -73,6 +73,7 @@ class AuthoritativeGitContextTests(unittest.TestCase):
|
||||
json.dumps({"branch": "patch/context"}), encoding="utf-8"
|
||||
)
|
||||
self.env = {
|
||||
"AIOS_TEST_MODE": "1",
|
||||
"AIOS_GITOPS_AUTHORITATIVE_GIT_DIR": str(self.bare),
|
||||
"AIOS_GITOPS_REPOSITORY_IDENTITY": "aios/aios",
|
||||
}
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
import base64
|
||||
from pathlib import Path
|
||||
|
||||
from aios_authority_git_executor import AuthorityGitExecutor
|
||||
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(
|
||||
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(
|
||||
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/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
|
||||
@@ -250,7 +250,7 @@ class GovernedLocalWriterTests(unittest.TestCase):
|
||||
}
|
||||
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:
|
||||
directory = Path(root)
|
||||
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(append_request, peer)["status"], "ok")
|
||||
self.assertEqual(service.process_request(append_request, peer)["status"], "error")
|
||||
self.assertEqual(len(sink.records), 3)
|
||||
self.assertEqual(service.process_request(append_request, peer)["status"], "receipt")
|
||||
self.assertEqual(len(sink.records), 2)
|
||||
self.assertEqual(len(handler.create_calls), 1)
|
||||
self.assertEqual(len(handler.append_calls), 1)
|
||||
|
||||
|
||||
@@ -0,0 +1,120 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import io
|
||||
import json
|
||||
import shutil
|
||||
import subprocess
|
||||
import tarfile
|
||||
from pathlib import Path
|
||||
import sys
|
||||
|
||||
import pytest
|
||||
|
||||
APP = Path(__file__).resolve().parents[1] / "active-runtime" / "app"
|
||||
sys.path.insert(0, str(APP))
|
||||
|
||||
import governed_production_release as release # noqa: E402
|
||||
from version_registry import build_source_registry_entry, source_registry_json # noqa: E402
|
||||
|
||||
|
||||
def _git(work: Path, *args: str) -> str:
|
||||
return subprocess.run(
|
||||
["git", "-C", str(work), *args],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=True,
|
||||
).stdout.strip()
|
||||
|
||||
|
||||
def _commit(work: Path, message: str) -> str:
|
||||
subprocess.run(["git", "-C", str(work), "add", "-A"], check=True)
|
||||
subprocess.run(["git", "-C", str(work), "commit", "-qm", message], check=True)
|
||||
return _git(work, "rev-parse", "HEAD")
|
||||
|
||||
|
||||
def test_artifact_uses_pinned_source_registry_commit_when_main_advances(
|
||||
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
||||
) -> None:
|
||||
if shutil.which("zstd") is None:
|
||||
pytest.skip("zstd is required for governed artifact integration")
|
||||
work = tmp_path / "work"
|
||||
bare = tmp_path / "repo.git"
|
||||
subprocess.run(["git", "init", "-q", "-b", "main", str(work)], check=True)
|
||||
_git(work, "config", "user.name", "test")
|
||||
_git(work, "config", "user.email", "test@example.invalid")
|
||||
(work / "active-runtime/app").mkdir(parents=True)
|
||||
(work / "active-runtime/app/deployment_state.py").write_text("state = 'candidate'\n")
|
||||
(work / "active-runtime/app/v2r1c6_privacy_tools.py").write_text("def main(): return 0\n")
|
||||
target_oid = _commit(work, "release target")
|
||||
subprocess.run(
|
||||
["git", "-C", str(work), "tag", "-a", "V2R2C1", target_oid, "-m", "release"],
|
||||
check=True,
|
||||
)
|
||||
tag_oid = _git(work, "rev-parse", "refs/tags/V2R2C1")
|
||||
(work / "evidence.txt").write_text("evidence\n")
|
||||
evidence_oid = _commit(work, "evidence closure")
|
||||
record = build_source_registry_entry(
|
||||
version="V2R2C1",
|
||||
tag_oid=tag_oid,
|
||||
target_oid=target_oid,
|
||||
peeled_target_oid=target_oid,
|
||||
evidence_closure_oid=evidence_oid,
|
||||
approval_plan_id="sha256:" + "4" * 64,
|
||||
)
|
||||
source_path = work / "versions/V2R2C1.json"
|
||||
source_path.parent.mkdir(parents=True)
|
||||
source_path.write_bytes(source_registry_json(record))
|
||||
main_oid = _commit(work, "source registry convergence")
|
||||
subprocess.run(["git", "clone", "-q", "--bare", str(work), str(bare)], check=True)
|
||||
subprocess.run(["git", "-C", str(work), "remote", "add", "authority", str(bare)], check=True)
|
||||
monkeypatch.setenv("AIOS_TEST_MODE", "1")
|
||||
monkeypatch.setenv("AIOS_GITOPS_AUTHORITATIVE_GIT_DIR", str(bare))
|
||||
|
||||
monkeypatch.setattr(release, "TAG_OID", tag_oid)
|
||||
monkeypatch.setattr(release, "TARGET_OID", target_oid)
|
||||
monkeypatch.setattr(release, "EVIDENCE_CLOSURE_OID", evidence_oid)
|
||||
monkeypatch.setattr(release, "APPROVAL_PLAN_ID", record["approval_plan_id"])
|
||||
binding = release.ReleaseBinding(
|
||||
tag_oid=tag_oid,
|
||||
target_oid=target_oid,
|
||||
evidence_closure_oid=evidence_oid,
|
||||
approval_plan_id=record["approval_plan_id"],
|
||||
)
|
||||
|
||||
original_payload = release._payload_file
|
||||
advanced = False
|
||||
|
||||
def advance_after_first_payload(git_dir: Path, oid: str, path: str) -> bytes:
|
||||
nonlocal advanced
|
||||
result = original_payload(git_dir, oid, path)
|
||||
if not advanced and path == "active-runtime/app/deployment_state.py":
|
||||
updated = dict(record)
|
||||
updated["review_marker"] = "main-advanced-after-validation"
|
||||
source_path.write_bytes(source_registry_json(updated))
|
||||
new_main = _commit(work, "advance main after source validation")
|
||||
subprocess.run(["git", "-C", str(work), "push", "-q", "authority", "main"], check=True)
|
||||
assert new_main != main_oid
|
||||
advanced = True
|
||||
return result
|
||||
|
||||
monkeypatch.setattr(release, "_payload_file", advance_after_first_payload)
|
||||
receipt = release.build_production_artifact(bare, binding, tmp_path / "artifacts")
|
||||
assert advanced is True
|
||||
assert receipt["release_target_oid"] == target_oid
|
||||
assert receipt["source_registry_main_oid"] == main_oid
|
||||
assert receipt["source_registry_path"] == "versions/V2R2C1.json"
|
||||
|
||||
expected_bytes = subprocess.run(
|
||||
["git", f"--git-dir={bare}", "show", f"{main_oid}:versions/V2R2C1.json"],
|
||||
capture_output=True,
|
||||
check=True,
|
||||
).stdout
|
||||
assert receipt["source_registry_sha256"] == __import__("hashlib").sha256(expected_bytes).hexdigest()
|
||||
compressed = Path(receipt["artifact"]).read_bytes()
|
||||
raw_tar = subprocess.run(["zstd", "-dc", "-"], input=compressed, capture_output=True, check=True).stdout
|
||||
with tarfile.open(fileobj=io.BytesIO(raw_tar), mode="r:") as archive:
|
||||
packaged = archive.extractfile("payload/versions/V2R2C1.json").read()
|
||||
metadata = json.loads(archive.extractfile("payload/BUILD-METADATA.json").read())
|
||||
assert packaged == expected_bytes
|
||||
assert metadata["source_registry_main_oid"] == main_oid
|
||||
assert metadata["source_registry_sha256"] == receipt["source_registry_sha256"]
|
||||
@@ -0,0 +1,47 @@
|
||||
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_source_convergence_never_falls_back_to_generic_git_dir():
|
||||
source = (ROOT / 'active-runtime' / 'app' / 'aios_model_api_staging.py').read_text(encoding='utf-8')
|
||||
assert "repo_git_dir=AUTHORITATIVE_GIT_DIR or GIT_DIR" not in source
|
||||
assert "repo_git_dir=AUTHORITATIVE_GIT_DIR" in source
|
||||
|
||||
|
||||
def test_production_adapter_never_claims_source_git_convergence():
|
||||
source = (ROOT / 'active-runtime' / 'app' / 'v2r2c1_production_adapter.py').read_text(encoding='utf-8')
|
||||
assert '"source_registry_converged": True' not in source
|
||||
assert '"source_registry_converged": "not_checked"' in source
|
||||
|
||||
|
||||
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
|
||||
@@ -47,6 +47,8 @@ def old_baseline(path: Path) -> str:
|
||||
|
||||
|
||||
def test_baseline_record_uses_real_previous_production_baseline(tmp_path, monkeypatch):
|
||||
# This test isolates previous-baseline semantics; convergence is tested separately.
|
||||
monkeypatch.setattr(adapter, "assert_source_registry_convergence", lambda *a, **kw: None)
|
||||
baseline = tmp_path / "baseline.json"
|
||||
previous_sha = old_baseline(baseline)
|
||||
registry_root = tmp_path / "versions"
|
||||
|
||||
@@ -0,0 +1,392 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import io
|
||||
import json
|
||||
from pathlib import Path
|
||||
import shutil
|
||||
import subprocess
|
||||
import tarfile
|
||||
|
||||
import pytest
|
||||
|
||||
from approved_release_executor import (
|
||||
Binding,
|
||||
ManualOwnerReviewRequired,
|
||||
ReleaseBlocked,
|
||||
ReconciliationRequired,
|
||||
deploy_step,
|
||||
execute,
|
||||
tag_step,
|
||||
verify_artifact,
|
||||
verify_authorization,
|
||||
)
|
||||
|
||||
|
||||
def _binding(tmp_path: Path, **overrides) -> Binding:
|
||||
artifact = tmp_path / "artifact.tar.zst"
|
||||
artifact.write_bytes(b"x")
|
||||
values = {
|
||||
"version": "VTEST",
|
||||
"target_oid": "a" * 40,
|
||||
"evidence_closure_oid": "b" * 40,
|
||||
"artifact_sha256": "c" * 64,
|
||||
"approval_plan_id": "sha256:" + "d" * 64,
|
||||
"artifact_path": str(artifact),
|
||||
"artifact_size": 1,
|
||||
"production_service": "test.service",
|
||||
"production_port": 18927,
|
||||
}
|
||||
values.update(overrides)
|
||||
return Binding(**values)
|
||||
|
||||
|
||||
def _authorization(binding: Binding) -> dict[str, object]:
|
||||
result: dict[str, object] = {
|
||||
"accepted": True,
|
||||
"binding_replaced": False,
|
||||
"version": binding.version,
|
||||
"target_oid": binding.target_oid,
|
||||
"evidence_closure_oid": binding.evidence_closure_oid,
|
||||
"artifact_sha256": binding.artifact_sha256,
|
||||
"approval_plan_id": binding.approval_plan_id,
|
||||
}
|
||||
if binding.tag_oid is not None:
|
||||
result.update(
|
||||
{
|
||||
"binding_schema": binding.binding_schema or "aios.release-binding.v2",
|
||||
"tag_oid": binding.tag_oid,
|
||||
}
|
||||
)
|
||||
if binding.source_registry_main_oid is not None:
|
||||
result.update(
|
||||
{
|
||||
"source_registry_main_oid": binding.source_registry_main_oid,
|
||||
"source_registry_path": binding.source_registry_path
|
||||
or f"versions/{binding.version}.json",
|
||||
"source_registry_sha256": binding.source_registry_sha256,
|
||||
}
|
||||
)
|
||||
return result
|
||||
|
||||
|
||||
def test_legacy_v2r2c1_style_authorization_remains_compatible(tmp_path: Path) -> None:
|
||||
binding = _binding(tmp_path)
|
||||
assert verify_authorization(_authorization(binding), binding)["accepted"] is True
|
||||
|
||||
|
||||
def test_future_binding_requires_explicit_tag_oid(tmp_path: Path) -> None:
|
||||
binding = _binding(
|
||||
tmp_path,
|
||||
version="V2R2C2",
|
||||
tag_oid="1" * 40,
|
||||
binding_schema="aios.release-binding.v2",
|
||||
)
|
||||
assert verify_authorization(_authorization(binding), binding)["tag_oid"] == "1" * 40
|
||||
wrong = _authorization(binding)
|
||||
wrong["tag_oid"] = "2" * 40
|
||||
with pytest.raises(ManualOwnerReviewRequired, match="tag_oid"):
|
||||
verify_authorization(wrong, binding)
|
||||
|
||||
|
||||
def test_future_same_name_different_tag_object_requires_manual_review(monkeypatch, tmp_path: Path) -> None:
|
||||
binding = _binding(
|
||||
tmp_path,
|
||||
version="V2R2C2",
|
||||
tag_oid="1" * 40,
|
||||
binding_schema="aios.release-binding.v2",
|
||||
)
|
||||
|
||||
def fake(_command, _request):
|
||||
return {
|
||||
"status": "APPLIED_VERIFIED",
|
||||
"version": binding.version,
|
||||
"target_oid": binding.target_oid,
|
||||
"artifact_sha256": binding.artifact_sha256,
|
||||
"tag_oid": "2" * 40,
|
||||
"peeled_target_oid": binding.target_oid,
|
||||
}
|
||||
|
||||
monkeypatch.setattr("approved_release_executor.run_adapter", fake)
|
||||
with pytest.raises(ManualOwnerReviewRequired, match="same-name tag object"):
|
||||
tag_step(["/tag"], binding)
|
||||
|
||||
|
||||
def test_executor_rechecks_source_registry_digest_and_build_provenance(
|
||||
monkeypatch, tmp_path: Path
|
||||
) -> None:
|
||||
if shutil.which("zstd") is None:
|
||||
pytest.skip("zstd is required for archive verification")
|
||||
source = {
|
||||
"schema": "aios.source-registry.v1",
|
||||
"version": "V2R2C2",
|
||||
"repository": "aios/aios",
|
||||
"tag": "V2R2C2",
|
||||
"tag_oid": "1" * 40,
|
||||
"tag_object": "1" * 40,
|
||||
"target_oid": "2" * 40,
|
||||
"peeled_target_oid": "2" * 40,
|
||||
"evidence_closure_oid": "3" * 40,
|
||||
"approval_plan_id": "sha256:" + "4" * 64,
|
||||
}
|
||||
source_bytes = (json.dumps(source, ensure_ascii=False, sort_keys=True, indent=2) + "\n").encode()
|
||||
source_digest = hashlib.sha256(source_bytes).hexdigest()
|
||||
repo_work = tmp_path / "authority-work"
|
||||
repo_bare = tmp_path / "authority.git"
|
||||
(repo_work / "versions").mkdir(parents=True)
|
||||
(repo_work / "versions" / "V2R2C2.json").write_bytes(source_bytes)
|
||||
subprocess.run(["git", "init", "-q", str(repo_work)], check=True)
|
||||
subprocess.run(["git", "-C", str(repo_work), "branch", "-M", "main"], check=True)
|
||||
subprocess.run(["git", "-C", str(repo_work), "config", "user.name", "test"], check=True)
|
||||
subprocess.run(
|
||||
["git", "-C", str(repo_work), "config", "user.email", "test@example.invalid"],
|
||||
check=True,
|
||||
)
|
||||
subprocess.run(["git", "-C", str(repo_work), "add", "versions"], check=True)
|
||||
subprocess.run(["git", "-C", str(repo_work), "commit", "-qm", "source"], check=True)
|
||||
source_main_oid = subprocess.run(
|
||||
["git", "-C", str(repo_work), "rev-parse", "HEAD"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=True,
|
||||
).stdout.strip()
|
||||
subprocess.run(["git", "clone", "-q", "--bare", str(repo_work), str(repo_bare)], check=True)
|
||||
subprocess.run(
|
||||
["git", f"--git-dir={repo_bare}", "config", "remote.gitea.url", "https://g.cnnetmkt.top/aios/aios.git"],
|
||||
check=True,
|
||||
)
|
||||
monkeypatch.setenv("AIOS_TEST_MODE", "1")
|
||||
monkeypatch.setenv("AIOS_GITOPS_AUTHORITATIVE_GIT_DIR", str(repo_bare))
|
||||
metadata = {
|
||||
"release_target_oid": "2" * 40,
|
||||
"source_registry_main_oid": source_main_oid,
|
||||
"source_registry_path": "versions/V2R2C2.json",
|
||||
"source_registry_sha256": source_digest,
|
||||
}
|
||||
tar_path = tmp_path / "release.tar"
|
||||
with tarfile.open(tar_path, "w") as archive:
|
||||
for name, content in (
|
||||
("payload/versions/V2R2C2.json", source_bytes),
|
||||
("payload/BUILD-METADATA.json", (json.dumps(metadata, sort_keys=True) + "\n").encode()),
|
||||
):
|
||||
info = tarfile.TarInfo(name)
|
||||
info.size = len(content)
|
||||
archive.addfile(info, io.BytesIO(content))
|
||||
artifact = tmp_path / "release.tar.zst"
|
||||
subprocess.run(["zstd", "-q", "-19", "-f", str(tar_path), "-o", str(artifact)], check=True)
|
||||
binding = _binding(
|
||||
tmp_path,
|
||||
version="V2R2C2",
|
||||
tag_oid="1" * 40,
|
||||
binding_schema="aios.release-binding.v2",
|
||||
target_oid="2" * 40,
|
||||
evidence_closure_oid="3" * 40,
|
||||
approval_plan_id="sha256:" + "4" * 64,
|
||||
source_registry_main_oid=source_main_oid,
|
||||
source_registry_path="versions/V2R2C2.json",
|
||||
source_registry_sha256=source_digest,
|
||||
artifact_path=str(artifact),
|
||||
artifact_size=artifact.stat().st_size,
|
||||
artifact_sha256=hashlib.sha256(artifact.read_bytes()).hexdigest(),
|
||||
)
|
||||
result = verify_artifact(binding)
|
||||
assert result["source_registry_sha256"] == source_digest
|
||||
assert result["build_metadata_provenance_verified"] is True
|
||||
|
||||
bad_metadata = dict(metadata)
|
||||
bad_metadata["source_registry_sha256"] = "0" * 64
|
||||
bad_tar = tmp_path / "bad-metadata.tar"
|
||||
with tarfile.open(bad_tar, "w") as archive:
|
||||
for name, content in (
|
||||
("payload/versions/V2R2C2.json", source_bytes),
|
||||
("payload/BUILD-METADATA.json", (json.dumps(bad_metadata, sort_keys=True) + "\n").encode()),
|
||||
):
|
||||
info = tarfile.TarInfo(name)
|
||||
info.size = len(content)
|
||||
archive.addfile(info, io.BytesIO(content))
|
||||
bad_artifact = tmp_path / "bad-metadata.tar.zst"
|
||||
subprocess.run(["zstd", "-q", "-19", "-f", str(bad_tar), "-o", str(bad_artifact)], check=True)
|
||||
bad_binding = _binding(
|
||||
tmp_path,
|
||||
version="V2R2C2",
|
||||
tag_oid="1" * 40,
|
||||
binding_schema="aios.release-binding.v2",
|
||||
target_oid="2" * 40,
|
||||
evidence_closure_oid="3" * 40,
|
||||
approval_plan_id="sha256:" + "4" * 64,
|
||||
source_registry_main_oid=source_main_oid,
|
||||
source_registry_path="versions/V2R2C2.json",
|
||||
source_registry_sha256=source_digest,
|
||||
artifact_path=str(bad_artifact),
|
||||
artifact_size=bad_artifact.stat().st_size,
|
||||
artifact_sha256=hashlib.sha256(bad_artifact.read_bytes()).hexdigest(),
|
||||
)
|
||||
with pytest.raises(ReleaseBlocked, match="BUILD-METADATA"):
|
||||
verify_artifact(bad_binding)
|
||||
|
||||
subprocess.run(["git", "-C", str(repo_work), "remote", "add", "origin", str(repo_bare)], check=True)
|
||||
changed = dict(source)
|
||||
changed["review_marker"] = "changed-after-M"
|
||||
changed_bytes = (json.dumps(changed, ensure_ascii=False, sort_keys=True, indent=2) + "\n").encode()
|
||||
(repo_work / "versions" / "V2R2C2.json").write_bytes(changed_bytes)
|
||||
subprocess.run(["git", "-C", str(repo_work), "add", "versions"], check=True)
|
||||
subprocess.run(["git", "-C", str(repo_work), "commit", "-qm", "change-source"], check=True)
|
||||
subprocess.run(["git", "-C", str(repo_work), "push", "-q", "origin", "HEAD:main"], check=True)
|
||||
with pytest.raises(ManualOwnerReviewRequired, match="current Authority main source registry"):
|
||||
verify_artifact(binding)
|
||||
|
||||
|
||||
def test_executor_requires_runtime_bundle_convergence_for_provenance_binding(tmp_path: Path):
|
||||
binding = _binding(
|
||||
tmp_path,
|
||||
version="V2R2C2",
|
||||
tag_oid="1" * 40,
|
||||
binding_schema="aios.release-binding.v2",
|
||||
source_registry_main_oid="5" * 40,
|
||||
source_registry_path="versions/V2R2C2.json",
|
||||
source_registry_sha256="6" * 64,
|
||||
)
|
||||
|
||||
def fake_missing(_command, _request):
|
||||
return {
|
||||
"status": "ACTIVE_VERIFIED",
|
||||
"version": binding.version,
|
||||
"target_oid": binding.target_oid,
|
||||
"evidence_closure_oid": binding.evidence_closure_oid,
|
||||
"artifact_sha256": binding.artifact_sha256,
|
||||
"approval_plan_id": binding.approval_plan_id,
|
||||
"deployment_state": "ACTIVE",
|
||||
"production_deployed": True,
|
||||
}
|
||||
|
||||
monkeypatch = pytest.MonkeyPatch()
|
||||
monkeypatch.setattr("approved_release_executor.run_adapter", fake_missing)
|
||||
try:
|
||||
with pytest.raises(ReconciliationRequired, match="runtime bundle/runtime registry"):
|
||||
deploy_step(["/deploy"], binding)
|
||||
finally:
|
||||
monkeypatch.undo()
|
||||
|
||||
def fake_converged(_command, _request):
|
||||
return {
|
||||
"status": "ACTIVE_VERIFIED",
|
||||
"version": binding.version,
|
||||
"target_oid": binding.target_oid,
|
||||
"evidence_closure_oid": binding.evidence_closure_oid,
|
||||
"artifact_sha256": binding.artifact_sha256,
|
||||
"approval_plan_id": binding.approval_plan_id,
|
||||
"deployment_state": "ACTIVE",
|
||||
"production_deployed": True,
|
||||
"runtime_bundle_contains_current_version": True,
|
||||
"runtime_registry_converged": True,
|
||||
}
|
||||
|
||||
monkeypatch.setattr("approved_release_executor.run_adapter", fake_converged)
|
||||
try:
|
||||
assert deploy_step(["/deploy"], binding)["runtime_registry_converged"] is True
|
||||
finally:
|
||||
monkeypatch.undo()
|
||||
|
||||
|
||||
def test_execute_rechecks_authoritative_git_after_runtime_before_registry(
|
||||
monkeypatch, tmp_path: Path
|
||||
):
|
||||
work = tmp_path / "authority-work"
|
||||
bare = tmp_path / "authority.git"
|
||||
(work / "versions").mkdir(parents=True)
|
||||
subprocess.run(["git", "init", "-q", "-b", "main", str(work)], check=True)
|
||||
subprocess.run(["git", "-C", str(work), "config", "user.name", "test"], check=True)
|
||||
subprocess.run(
|
||||
["git", "-C", str(work), "config", "user.email", "test@example.invalid"],
|
||||
check=True,
|
||||
)
|
||||
source = {
|
||||
"schema": "aios.source-registry.v1",
|
||||
"version": "VTEST",
|
||||
"repository": "aios/aios",
|
||||
"tag_oid": "1" * 40,
|
||||
"tag_object": "1" * 40,
|
||||
"target_oid": "a" * 40,
|
||||
"peeled_target_oid": "a" * 40,
|
||||
"evidence_closure_oid": "b" * 40,
|
||||
"approval_plan_id": "sha256:" + "d" * 64,
|
||||
}
|
||||
source_path = work / "versions" / "VTEST.json"
|
||||
source_path.write_bytes((json.dumps(source, sort_keys=True, indent=2) + "\n").encode())
|
||||
subprocess.run(["git", "-C", str(work), "add", "versions"], check=True)
|
||||
subprocess.run(["git", "-C", str(work), "commit", "-qm", "source"], check=True)
|
||||
pinned_main = subprocess.run(
|
||||
["git", "-C", str(work), "rev-parse", "HEAD"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=True,
|
||||
).stdout.strip()
|
||||
subprocess.run(["git", "clone", "-q", "--bare", str(work), str(bare)], check=True)
|
||||
source_sha = hashlib.sha256(source_path.read_bytes()).hexdigest()
|
||||
binding = _binding(
|
||||
tmp_path,
|
||||
source_registry_main_oid=pinned_main,
|
||||
source_registry_path="versions/VTEST.json",
|
||||
source_registry_sha256=source_sha,
|
||||
)
|
||||
monkeypatch.setenv("AIOS_TEST_MODE", "1")
|
||||
monkeypatch.setenv("AIOS_GITOPS_AUTHORITATIVE_GIT_DIR", str(bare))
|
||||
monkeypatch.setattr("approved_release_executor.secure_json", lambda _path: _authorization(binding))
|
||||
monkeypatch.setattr("approved_release_executor.verify_git_lineage", lambda *_args: {"verified": True})
|
||||
monkeypatch.setattr("approved_release_executor.verify_artifact", lambda *_args, **_kwargs: {"verified": True})
|
||||
|
||||
deployed = False
|
||||
calls = []
|
||||
|
||||
def fake_adapter(command, request):
|
||||
nonlocal deployed
|
||||
name = command[0]
|
||||
operation = request["operation"]
|
||||
calls.append((name, operation))
|
||||
common = {
|
||||
"version": binding.version,
|
||||
"target_oid": binding.target_oid,
|
||||
"evidence_closure_oid": binding.evidence_closure_oid,
|
||||
"artifact_sha256": binding.artifact_sha256,
|
||||
"approval_plan_id": binding.approval_plan_id,
|
||||
}
|
||||
if name == "/tag":
|
||||
return {
|
||||
**common,
|
||||
"status": "APPLIED_VERIFIED",
|
||||
"tag_oid": "1" * 40,
|
||||
"peeled_target_oid": binding.target_oid,
|
||||
}
|
||||
if name == "/deploy":
|
||||
if operation == "probe" and not deployed:
|
||||
changed = dict(source)
|
||||
changed["review_marker"] = "changed-after-runtime"
|
||||
source_path.write_bytes((json.dumps(changed, sort_keys=True, indent=2) + "\n").encode())
|
||||
subprocess.run(["git", "-C", str(work), "add", "versions"], check=True)
|
||||
subprocess.run(["git", "-C", str(work), "commit", "-qm", "advance-main"], check=True)
|
||||
subprocess.run(["git", "-C", str(work), "push", "-q", str(bare), "HEAD:main"], check=True)
|
||||
deployed = True
|
||||
return {
|
||||
**common,
|
||||
"status": "ACTIVE_VERIFIED",
|
||||
"deployment_state": "ACTIVE",
|
||||
"production_deployed": True,
|
||||
"runtime_bundle_contains_current_version": True,
|
||||
"runtime_registry_converged": True,
|
||||
}
|
||||
raise AssertionError(f"unexpected adapter call: {name}")
|
||||
|
||||
monkeypatch.setattr("approved_release_executor.run_adapter", fake_adapter)
|
||||
with pytest.raises(ManualOwnerReviewRequired, match="current Authority main source registry"):
|
||||
execute(
|
||||
binding=binding,
|
||||
authorization_path=tmp_path / "authorization.json",
|
||||
repository=bare,
|
||||
state_root=tmp_path / "state",
|
||||
tag_command=["/tag"],
|
||||
deploy_command=["/deploy"],
|
||||
registry_command=["/registry"],
|
||||
baseline_command=["/baseline"],
|
||||
)
|
||||
assert deployed is True
|
||||
assert not any(name in {"/registry", "/baseline"} for name, _ in calls)
|
||||
@@ -0,0 +1,850 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import inspect
|
||||
import json
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
APP = Path(__file__).resolve().parents[1] / "active-runtime" / "app"
|
||||
sys.path.insert(0, str(APP))
|
||||
|
||||
import release_metadata_adapter as adapter # noqa: E402
|
||||
import version_registry as registry_module # noqa: E402
|
||||
from version_registry import ( # noqa: E402
|
||||
RegistryConvergenceError,
|
||||
assert_runtime_registry_convergence,
|
||||
assert_source_registry_convergence,
|
||||
build_source_registry_entry,
|
||||
configured_approval_authority_root,
|
||||
configured_authoritative_git_dir,
|
||||
list_registry_versions,
|
||||
list_registry_versions_report,
|
||||
source_registry_snapshot,
|
||||
verify_authoritative_repository,
|
||||
verify_approval_authority,
|
||||
verify_source_registry_main_convergence,
|
||||
verify_source_registry_git_identity,
|
||||
)
|
||||
|
||||
|
||||
def _write_json(path: Path, obj: dict) -> None:
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
path.write_text(json.dumps(obj, sort_keys=True) + "\n", encoding="utf-8")
|
||||
|
||||
|
||||
def _record(
|
||||
*,
|
||||
version: str = "V2R2C1",
|
||||
tag_oid: str = "1" * 40,
|
||||
target_oid: str = "2" * 40,
|
||||
) -> dict:
|
||||
return {
|
||||
"schema": "aios.production-registry.v2",
|
||||
"version": version,
|
||||
"repository": "aios/aios",
|
||||
"status": "PRODUCTION_DEPLOYED",
|
||||
"production_deployed": True,
|
||||
"tag_oid": tag_oid,
|
||||
"target_oid": target_oid,
|
||||
"peeled_target_oid": target_oid,
|
||||
"evidence_closure_oid": "3" * 40,
|
||||
"approval_plan_id": "sha256:" + "4" * 64,
|
||||
}
|
||||
|
||||
|
||||
def _canonical_approval(root: Path, record: dict) -> Path:
|
||||
owner = {
|
||||
"approved": True,
|
||||
"status": "APPROVED",
|
||||
"approved_by": "aiman",
|
||||
"approval_source": "explicit_owner_approval_in_chat",
|
||||
}
|
||||
binding = {
|
||||
key: record[key]
|
||||
for key in (
|
||||
"version",
|
||||
"repository",
|
||||
"tag_oid",
|
||||
"target_oid",
|
||||
"peeled_target_oid",
|
||||
"evidence_closure_oid",
|
||||
"approval_plan_id",
|
||||
)
|
||||
}
|
||||
authority = {
|
||||
"schema": "aios.v2r2c1.tag-approval-request.v1",
|
||||
"state": "APPROVED",
|
||||
"status": "APPROVED",
|
||||
"execute": False,
|
||||
"approval_plan_id": record["approval_plan_id"],
|
||||
"binding": binding,
|
||||
"owner_approval": owner,
|
||||
"owner_approval_digest_sha256": hashlib.sha256(
|
||||
(json.dumps(owner, sort_keys=True, separators=(",", ":")) + "\n").encode()
|
||||
).hexdigest(),
|
||||
}
|
||||
path = root / f"{record['version']}.json"
|
||||
_write_json(path, authority)
|
||||
path.chmod(0o640)
|
||||
return path
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def _explicit_test_authority_mode(monkeypatch: pytest.MonkeyPatch):
|
||||
"""Permit only these unit tests to inject temporary authority roots."""
|
||||
monkeypatch.setenv("AIOS_TEST_MODE", "1")
|
||||
|
||||
|
||||
def test_list_registry_versions_adds_only_baseline_attested_projection(tmp_path: Path):
|
||||
ws = tmp_path / "ws"
|
||||
(ws / "versions").mkdir(parents=True)
|
||||
(ws / "versions" / "V2R1C7.json").write_text("{}\n")
|
||||
|
||||
projection_root = tmp_path / "projection"
|
||||
projection = projection_root / "V2R2C1.json"
|
||||
_write_json(projection, _record())
|
||||
digest = hashlib.sha256(projection.read_bytes()).hexdigest()
|
||||
|
||||
baseline = tmp_path / "baseline" / "current.json"
|
||||
_write_json(
|
||||
baseline,
|
||||
{
|
||||
"version": "V2R2C1",
|
||||
"status": "PRODUCTION_DEPLOYED",
|
||||
"production_deployed": True,
|
||||
"registry_source": str(projection),
|
||||
"registry_digest": digest,
|
||||
},
|
||||
)
|
||||
|
||||
assert list_registry_versions(
|
||||
ws,
|
||||
projection_root=projection_root,
|
||||
baseline_file=baseline,
|
||||
) == ["V2R1C7.json", "V2R2C1.json"]
|
||||
|
||||
|
||||
def test_list_registry_versions_rejects_projection_digest_drift(tmp_path: Path):
|
||||
ws = tmp_path / "ws"
|
||||
(ws / "versions").mkdir(parents=True)
|
||||
projection_root = tmp_path / "projection"
|
||||
projection = projection_root / "V2R2C1.json"
|
||||
_write_json(projection, _record())
|
||||
|
||||
baseline = tmp_path / "baseline.json"
|
||||
_write_json(
|
||||
baseline,
|
||||
{
|
||||
"version": "V2R2C1",
|
||||
"status": "PRODUCTION_DEPLOYED",
|
||||
"production_deployed": True,
|
||||
"registry_source": str(projection),
|
||||
"registry_digest": "0" * 64,
|
||||
},
|
||||
)
|
||||
|
||||
assert list_registry_versions(
|
||||
ws,
|
||||
projection_root=projection_root,
|
||||
baseline_file=baseline,
|
||||
) == []
|
||||
|
||||
|
||||
def test_projection_only_version_is_not_runtime_artifact_convergence(tmp_path: Path):
|
||||
ws = tmp_path / "ws"
|
||||
(ws / "versions").mkdir(parents=True)
|
||||
projection_root = tmp_path / "projection"
|
||||
projection = projection_root / "V2R2C1.json"
|
||||
_write_json(projection, _record())
|
||||
baseline = tmp_path / "baseline.json"
|
||||
_write_json(
|
||||
baseline,
|
||||
{
|
||||
"version": "V2R2C1",
|
||||
"status": "PRODUCTION_DEPLOYED",
|
||||
"production_deployed": True,
|
||||
"registry_source": str(projection),
|
||||
"registry_digest": hashlib.sha256(projection.read_bytes()).hexdigest(),
|
||||
},
|
||||
)
|
||||
|
||||
report = list_registry_versions_report(
|
||||
ws,
|
||||
projection_root=projection_root,
|
||||
baseline_file=baseline,
|
||||
)
|
||||
assert report["registry_versions"] == ["V2R2C1.json"]
|
||||
assert report["attested_projection_version"] == "V2R2C1"
|
||||
assert report["runtime_bundle_contains_version"] is False
|
||||
assert report["source_registry_converged"] == "unknown"
|
||||
assert report["runtime_registry_converged"] is False
|
||||
assert report["runtime_bundle_contains_current_version"] is False
|
||||
assert report["registry_consistency_status"] == "RUNTIME_ARTIFACT_RECONCILIATION_REQUIRED"
|
||||
with pytest.raises(RegistryConvergenceError, match="runtime_bundle_missing"):
|
||||
assert_runtime_registry_convergence(
|
||||
ws,
|
||||
"V2R2C1",
|
||||
projection_root=projection_root,
|
||||
baseline_file=baseline,
|
||||
)
|
||||
|
||||
|
||||
def _attested_projection(tmp_path: Path, record: dict) -> tuple[Path, Path]:
|
||||
projection_root = tmp_path / "projection"
|
||||
projection = projection_root / f"{record['version']}.json"
|
||||
_write_json(projection, record)
|
||||
baseline = tmp_path / "baseline.json"
|
||||
_write_json(
|
||||
baseline,
|
||||
{
|
||||
"version": record["version"],
|
||||
"status": "PRODUCTION_DEPLOYED",
|
||||
"production_deployed": True,
|
||||
"registry_source": str(projection),
|
||||
"registry_digest": hashlib.sha256(projection.read_bytes()).hexdigest(),
|
||||
},
|
||||
)
|
||||
return projection_root, baseline
|
||||
|
||||
|
||||
def test_list_report_source_good_runtime_missing_is_not_fully_consistent(tmp_path: Path):
|
||||
record = _record()
|
||||
bare, branch = _init_bare_repo(tmp_path, record)
|
||||
projection_root, baseline = _attested_projection(tmp_path, record)
|
||||
ws = tmp_path / "runtime"
|
||||
(ws / "versions").mkdir(parents=True)
|
||||
report = list_registry_versions_report(
|
||||
ws,
|
||||
projection_root=projection_root,
|
||||
baseline_file=baseline,
|
||||
repo_git_dir=bare,
|
||||
main_ref=f"refs/heads/{branch}",
|
||||
)
|
||||
assert report["source_registry_converged"] is True
|
||||
assert report["runtime_registry_converged"] is False
|
||||
assert report["runtime_bundle_contains_current_version"] is False
|
||||
assert report["registry_consistency_status"] == "RUNTIME_ARTIFACT_RECONCILIATION_REQUIRED"
|
||||
|
||||
|
||||
def test_list_report_source_runtime_projection_good_is_fully_consistent(tmp_path: Path):
|
||||
record = _record()
|
||||
bare, branch = _init_bare_repo(tmp_path, record)
|
||||
projection_root, baseline = _attested_projection(tmp_path, record)
|
||||
ws = tmp_path / "runtime"
|
||||
versions = ws / "versions"
|
||||
versions.mkdir(parents=True)
|
||||
_write_json(versions / f"{record['version']}.json", record)
|
||||
report = list_registry_versions_report(
|
||||
ws,
|
||||
projection_root=projection_root,
|
||||
baseline_file=baseline,
|
||||
repo_git_dir=bare,
|
||||
main_ref=f"refs/heads/{branch}",
|
||||
)
|
||||
assert report["source_registry_converged"] is True
|
||||
assert report["runtime_registry_converged"] is True
|
||||
assert report["runtime_bundle_contains_current_version"] is True
|
||||
assert report["registry_consistency_status"] == "FULLY_CONSISTENT"
|
||||
|
||||
|
||||
def test_list_report_source_missing_is_not_converged(tmp_path: Path):
|
||||
record = _record()
|
||||
work = tmp_path / "no-source-work"
|
||||
bare = tmp_path / "no-source.git"
|
||||
subprocess.run(["git", "init", "-q", str(work)], check=True)
|
||||
subprocess.run(["git", "-C", str(work), "config", "user.name", "test"], check=True)
|
||||
subprocess.run(
|
||||
["git", "-C", str(work), "config", "user.email", "test@example.invalid"],
|
||||
check=True,
|
||||
)
|
||||
(work / "README").write_text("no source\n", encoding="utf-8")
|
||||
subprocess.run(["git", "-C", str(work), "add", "README"], check=True)
|
||||
subprocess.run(["git", "-C", str(work), "commit", "-qm", "no source"], check=True)
|
||||
branch = subprocess.run(
|
||||
["git", "-C", str(work), "symbolic-ref", "--short", "HEAD"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=True,
|
||||
).stdout.strip()
|
||||
subprocess.run(["git", "clone", "-q", "--bare", str(work), str(bare)], check=True)
|
||||
projection_root, baseline = _attested_projection(tmp_path, record)
|
||||
ws = tmp_path / "runtime"
|
||||
(ws / "versions").mkdir(parents=True)
|
||||
_write_json(ws / "versions" / f"{record['version']}.json", record)
|
||||
report = list_registry_versions_report(
|
||||
ws,
|
||||
projection_root=projection_root,
|
||||
baseline_file=baseline,
|
||||
repo_git_dir=bare,
|
||||
main_ref=f"refs/heads/{branch}",
|
||||
)
|
||||
assert report["source_registry_converged"] is False
|
||||
assert report["runtime_registry_converged"] is True
|
||||
assert report["registry_consistency_status"] == "SOURCE_REGISTRY_NOT_CONVERGED"
|
||||
|
||||
|
||||
def test_list_registry_versions_excludes_non_version_entries(tmp_path: Path):
|
||||
ws = tmp_path / "ws"
|
||||
versions = ws / "versions"
|
||||
versions.mkdir(parents=True)
|
||||
(versions / "README.txt").write_text("do not expose\n", encoding="utf-8")
|
||||
(versions / "scratch.json").write_text("{}\n", encoding="utf-8")
|
||||
(versions / "proposals").mkdir()
|
||||
(versions / "V2R1C7.json").write_text("{}\n", encoding="utf-8")
|
||||
assert list_registry_versions(
|
||||
ws,
|
||||
projection_root=tmp_path / "projection",
|
||||
baseline_file=tmp_path / "baseline.json",
|
||||
) == ["V2R1C7.json"]
|
||||
|
||||
|
||||
def _init_bare_repo(tmp_path: Path, record: dict) -> tuple[Path, str]:
|
||||
work = tmp_path / "work"
|
||||
bare = tmp_path / "repo.git"
|
||||
subprocess.run(["git", "init", "-q", str(work)], check=True)
|
||||
subprocess.run(["git", "-C", str(work), "config", "user.name", "test"], check=True)
|
||||
subprocess.run(["git", "-C", str(work), "config", "user.email", "test@example.invalid"], check=True)
|
||||
_write_json(work / "versions" / f"{record['version']}.json", record)
|
||||
subprocess.run(["git", "-C", str(work), "add", "versions"], check=True)
|
||||
subprocess.run(["git", "-C", str(work), "commit", "-qm", "registry"], check=True)
|
||||
branch = subprocess.run(
|
||||
["git", "-C", str(work), "symbolic-ref", "--short", "HEAD"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=True,
|
||||
).stdout.strip()
|
||||
subprocess.run(["git", "clone", "-q", "--bare", str(work), str(bare)], check=True)
|
||||
return bare, branch
|
||||
|
||||
|
||||
def _init_bare_repo_with_work(tmp_path: Path, record: dict) -> tuple[Path, Path, str]:
|
||||
work = tmp_path / "history-work"
|
||||
bare = tmp_path / "history.git"
|
||||
subprocess.run(["git", "init", "-q", str(work)], check=True)
|
||||
subprocess.run(["git", "-C", str(work), "config", "user.name", "test"], check=True)
|
||||
subprocess.run(
|
||||
["git", "-C", str(work), "config", "user.email", "test@example.invalid"],
|
||||
check=True,
|
||||
)
|
||||
_write_json(work / "versions" / f"{record['version']}.json", record)
|
||||
subprocess.run(["git", "-C", str(work), "add", "versions"], check=True)
|
||||
subprocess.run(["git", "-C", str(work), "commit", "-qm", "registry"], check=True)
|
||||
branch = subprocess.run(
|
||||
["git", "-C", str(work), "symbolic-ref", "--short", "HEAD"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=True,
|
||||
).stdout.strip()
|
||||
subprocess.run(["git", "clone", "-q", "--bare", str(work), str(bare)], check=True)
|
||||
subprocess.run(["git", "-C", str(work), "remote", "add", "origin", str(bare)], check=True)
|
||||
return work, bare, branch
|
||||
|
||||
|
||||
def test_assert_source_registry_convergence_passes_matching_identity(tmp_path: Path):
|
||||
record = _record()
|
||||
bare, branch = _init_bare_repo(tmp_path, record)
|
||||
projection = tmp_path / "projection.json"
|
||||
_write_json(projection, record)
|
||||
|
||||
assert_source_registry_convergence(
|
||||
projection,
|
||||
repo_git_dir=bare,
|
||||
main_ref=f"refs/heads/{branch}",
|
||||
)
|
||||
|
||||
|
||||
def test_assert_source_registry_convergence_fails_missing_entry(tmp_path: Path):
|
||||
record = _record()
|
||||
work = tmp_path / "work"
|
||||
bare = tmp_path / "repo.git"
|
||||
subprocess.run(["git", "init", "-q", str(work)], check=True)
|
||||
subprocess.run(["git", "-C", str(work), "config", "user.name", "test"], check=True)
|
||||
subprocess.run(["git", "-C", str(work), "config", "user.email", "test@example.invalid"], check=True)
|
||||
(work / "README").write_text("x\n")
|
||||
subprocess.run(["git", "-C", str(work), "add", "README"], check=True)
|
||||
subprocess.run(["git", "-C", str(work), "commit", "-qm", "init"], check=True)
|
||||
branch = subprocess.run(
|
||||
["git", "-C", str(work), "symbolic-ref", "--short", "HEAD"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=True,
|
||||
).stdout.strip()
|
||||
subprocess.run(["git", "clone", "-q", "--bare", str(work), str(bare)], check=True)
|
||||
|
||||
projection = tmp_path / "projection.json"
|
||||
_write_json(projection, record)
|
||||
|
||||
with pytest.raises(RegistryConvergenceError, match="source_registry_entry_missing"):
|
||||
assert_source_registry_convergence(
|
||||
projection,
|
||||
repo_git_dir=bare,
|
||||
main_ref=f"refs/heads/{branch}",
|
||||
)
|
||||
|
||||
|
||||
def test_assert_source_registry_convergence_fails_identity_mismatch(tmp_path: Path):
|
||||
source = _record(tag_oid="5" * 40)
|
||||
projection_record = _record(tag_oid="1" * 40)
|
||||
bare, branch = _init_bare_repo(tmp_path, source)
|
||||
projection = tmp_path / "projection.json"
|
||||
_write_json(projection, projection_record)
|
||||
|
||||
with pytest.raises(RegistryConvergenceError, match="source_registry_mismatch:tag_oid"):
|
||||
assert_source_registry_convergence(
|
||||
projection,
|
||||
repo_git_dir=bare,
|
||||
main_ref=f"refs/heads/{branch}",
|
||||
)
|
||||
|
||||
|
||||
def test_assert_source_registry_convergence_requires_all_identity_fields(tmp_path: Path):
|
||||
source = _record()
|
||||
source.pop("peeled_target_oid")
|
||||
bare, branch = _init_bare_repo(tmp_path, source)
|
||||
projection = tmp_path / "projection.json"
|
||||
_write_json(projection, _record())
|
||||
with pytest.raises(RegistryConvergenceError, match="peeled_target_oid"):
|
||||
assert_source_registry_convergence(
|
||||
projection,
|
||||
repo_git_dir=bare,
|
||||
main_ref=f"refs/heads/{branch}",
|
||||
)
|
||||
|
||||
|
||||
def test_assert_source_registry_convergence_rejects_wrong_repository(tmp_path: Path):
|
||||
source = _record()
|
||||
source["repository"] = "other/repository"
|
||||
bare, branch = _init_bare_repo(tmp_path, source)
|
||||
projection = tmp_path / "projection.json"
|
||||
_write_json(projection, _record())
|
||||
with pytest.raises(RegistryConvergenceError, match="repository"):
|
||||
assert_source_registry_convergence(
|
||||
projection,
|
||||
repo_git_dir=bare,
|
||||
main_ref=f"refs/heads/{branch}",
|
||||
)
|
||||
|
||||
|
||||
def test_source_registry_main_convergence_accepts_unrelated_later_main_commit(tmp_path: Path):
|
||||
record = _record()
|
||||
work, bare, branch = _init_bare_repo_with_work(tmp_path, record)
|
||||
source_main = subprocess.run(
|
||||
["git", "-C", str(work), "rev-parse", "HEAD"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=True,
|
||||
).stdout.strip()
|
||||
(work / "README").write_text("unrelated later main commit\n", encoding="utf-8")
|
||||
subprocess.run(["git", "-C", str(work), "add", "README"], check=True)
|
||||
subprocess.run(["git", "-C", str(work), "commit", "-qm", "unrelated"], check=True)
|
||||
subprocess.run(["git", "-C", str(work), "push", "-q", "origin", branch], check=True)
|
||||
result = verify_source_registry_main_convergence(
|
||||
bare,
|
||||
record["version"],
|
||||
main_ref=f"refs/heads/{branch}",
|
||||
source_registry_main_oid=source_main,
|
||||
expected_tag_oid=record["tag_oid"],
|
||||
expected_target_oid=record["target_oid"],
|
||||
expected_evidence_closure_oid=record["evidence_closure_oid"],
|
||||
expected_approval_plan_id=record["approval_plan_id"],
|
||||
)
|
||||
assert result["source_registry_main_is_ancestor_of_current_main"] is True
|
||||
|
||||
|
||||
def test_source_registry_main_convergence_rejects_side_branch_only_commit(tmp_path: Path):
|
||||
record = _record()
|
||||
work, bare, branch = _init_bare_repo_with_work(tmp_path, record)
|
||||
base = subprocess.run(
|
||||
["git", "-C", str(work), "rev-parse", "HEAD"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=True,
|
||||
).stdout.strip()
|
||||
subprocess.run(["git", "-C", str(work), "checkout", "-qb", "side"], check=True)
|
||||
(work / "side.txt").write_text("side\n", encoding="utf-8")
|
||||
subprocess.run(["git", "-C", str(work), "add", "side.txt"], check=True)
|
||||
subprocess.run(["git", "-C", str(work), "commit", "-qm", "side"], check=True)
|
||||
side = subprocess.run(
|
||||
["git", "-C", str(work), "rev-parse", "HEAD"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=True,
|
||||
).stdout.strip()
|
||||
subprocess.run(["git", "-C", str(work), "checkout", "-q", branch], check=True)
|
||||
(work / "main.txt").write_text("main\n", encoding="utf-8")
|
||||
subprocess.run(["git", "-C", str(work), "add", "main.txt"], check=True)
|
||||
subprocess.run(["git", "-C", str(work), "commit", "-qm", "main"], check=True)
|
||||
subprocess.run(["git", "-C", str(work), "push", "-q", "origin", branch], check=True)
|
||||
assert base != side
|
||||
with pytest.raises(RegistryConvergenceError, match="not_ancestor"):
|
||||
verify_source_registry_main_convergence(
|
||||
bare,
|
||||
record["version"],
|
||||
main_ref=f"refs/heads/{branch}",
|
||||
source_registry_main_oid=side,
|
||||
)
|
||||
|
||||
|
||||
def test_source_registry_main_convergence_rejects_removed_entry(tmp_path: Path):
|
||||
record = _record()
|
||||
work, bare, branch = _init_bare_repo_with_work(tmp_path, record)
|
||||
source_main = subprocess.run(
|
||||
["git", "-C", str(work), "rev-parse", "HEAD"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=True,
|
||||
).stdout.strip()
|
||||
(work / "versions" / f"{record['version']}.json").unlink()
|
||||
subprocess.run(["git", "-C", str(work), "add", "-u", "versions"], check=True)
|
||||
subprocess.run(["git", "-C", str(work), "commit", "-qm", "remove"], check=True)
|
||||
subprocess.run(["git", "-C", str(work), "push", "-q", "origin", branch], check=True)
|
||||
with pytest.raises(RegistryConvergenceError, match="entry_missing"):
|
||||
verify_source_registry_main_convergence(
|
||||
bare,
|
||||
record["version"],
|
||||
main_ref=f"refs/heads/{branch}",
|
||||
source_registry_main_oid=source_main,
|
||||
)
|
||||
|
||||
|
||||
def test_source_registry_main_convergence_rejects_altered_entry(tmp_path: Path):
|
||||
record = _record()
|
||||
work, bare, branch = _init_bare_repo_with_work(tmp_path, record)
|
||||
source_main = subprocess.run(
|
||||
["git", "-C", str(work), "rev-parse", "HEAD"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=True,
|
||||
).stdout.strip()
|
||||
altered = dict(record)
|
||||
altered["approval_plan_id"] = "sha256:" + "9" * 64
|
||||
_write_json(work / "versions" / f"{record['version']}.json", altered)
|
||||
subprocess.run(["git", "-C", str(work), "add", "versions"], check=True)
|
||||
subprocess.run(["git", "-C", str(work), "commit", "-qm", "alter"], check=True)
|
||||
subprocess.run(["git", "-C", str(work), "push", "-q", "origin", branch], check=True)
|
||||
with pytest.raises(RegistryConvergenceError, match="entry_changed"):
|
||||
verify_source_registry_main_convergence(
|
||||
bare,
|
||||
record["version"],
|
||||
main_ref=f"refs/heads/{branch}",
|
||||
source_registry_main_oid=source_main,
|
||||
)
|
||||
|
||||
|
||||
def test_source_registry_generator_predeclares_release_identity():
|
||||
value = build_source_registry_entry(
|
||||
version="V2R2C1",
|
||||
tag_oid="1" * 40,
|
||||
target_oid="2" * 40,
|
||||
peeled_target_oid="2" * 40,
|
||||
evidence_closure_oid="3" * 40,
|
||||
approval_plan_id="sha256:" + "4" * 64,
|
||||
)
|
||||
assert set(
|
||||
(
|
||||
"version",
|
||||
"tag_oid",
|
||||
"target_oid",
|
||||
"peeled_target_oid",
|
||||
"evidence_closure_oid",
|
||||
"approval_plan_id",
|
||||
)
|
||||
).issubset(value)
|
||||
|
||||
|
||||
def test_authoritative_repository_rejects_arbitrary_repository(
|
||||
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
||||
):
|
||||
record = _record()
|
||||
bare, _ = _init_bare_repo(tmp_path, record)
|
||||
monkeypatch.delenv("AIOS_TEST_MODE")
|
||||
with pytest.raises(RegistryConvergenceError, match="path_mismatch"):
|
||||
verify_authoritative_repository(bare)
|
||||
|
||||
|
||||
def test_production_authority_defaults_are_fixed_and_env_overrides_fail(
|
||||
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
||||
):
|
||||
monkeypatch.delenv("AIOS_TEST_MODE")
|
||||
monkeypatch.delenv("AIOS_GITOPS_AUTHORITATIVE_GIT_DIR", raising=False)
|
||||
monkeypatch.delenv("AIOS_GITOPS_APPROVAL_AUTHORITY_ROOT", raising=False)
|
||||
assert configured_authoritative_git_dir() == Path("/data/aios-gitops/repo.git")
|
||||
assert configured_approval_authority_root() == Path(
|
||||
"/var/lib/aios-gitops/approval-authority"
|
||||
)
|
||||
monkeypatch.setenv("AIOS_GITOPS_AUTHORITATIVE_GIT_DIR", str(tmp_path / "fake.git"))
|
||||
with pytest.raises(RegistryConvergenceError, match="override_forbidden"):
|
||||
configured_authoritative_git_dir()
|
||||
monkeypatch.setenv("AIOS_GITOPS_APPROVAL_AUTHORITY_ROOT", str(tmp_path / "fake-root"))
|
||||
with pytest.raises(RegistryConvergenceError, match="override_forbidden"):
|
||||
configured_approval_authority_root()
|
||||
|
||||
|
||||
def test_production_canonical_repository_and_test_injected_authority(
|
||||
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
||||
):
|
||||
record = _record()
|
||||
bare, _ = _init_bare_repo(tmp_path, record)
|
||||
subprocess.run(
|
||||
[
|
||||
"git",
|
||||
f"--git-dir={bare}",
|
||||
"config",
|
||||
"remote.gitea.url",
|
||||
"https://g.cnnetmkt.top/aios/aios.git",
|
||||
],
|
||||
check=True,
|
||||
)
|
||||
monkeypatch.delenv("AIOS_TEST_MODE")
|
||||
monkeypatch.setattr(registry_module, "DEFAULT_AUTHORITATIVE_GIT_DIR", bare)
|
||||
assert verify_authoritative_repository(bare)["repository_identity"] == "aios/aios"
|
||||
|
||||
monkeypatch.setenv("AIOS_TEST_MODE", "1")
|
||||
monkeypatch.setenv("AIOS_GITOPS_AUTHORITATIVE_GIT_DIR", str(bare))
|
||||
assert verify_authoritative_repository(bare)["configured_path"] == str(bare.resolve())
|
||||
|
||||
|
||||
def test_authoritative_git_unavailable_stays_unknown_and_closure_fails(
|
||||
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
||||
):
|
||||
record = _record()
|
||||
projection_root, baseline = _attested_projection(tmp_path, record)
|
||||
ws = tmp_path / "runtime"
|
||||
(ws / "versions").mkdir(parents=True)
|
||||
_write_json(ws / "versions" / f"{record['version']}.json", record)
|
||||
missing = tmp_path / "missing-authority.git"
|
||||
report = list_registry_versions_report(
|
||||
ws,
|
||||
projection_root=projection_root,
|
||||
baseline_file=baseline,
|
||||
repo_git_dir=missing,
|
||||
)
|
||||
assert report["source_registry_converged"] == "unknown"
|
||||
assert report["source_registry_convergence_error"] == "authoritative_repository_unavailable"
|
||||
monkeypatch.delenv("AIOS_TEST_MODE")
|
||||
with pytest.raises(RegistryConvergenceError, match="path_unavailable"):
|
||||
assert_source_registry_convergence(
|
||||
projection_root / f"{record['version']}.json",
|
||||
repo_git_dir=missing,
|
||||
)
|
||||
|
||||
|
||||
def test_source_registry_snapshot_stays_pinned_after_main_advances(tmp_path: Path):
|
||||
work = tmp_path / "work"
|
||||
bare = tmp_path / "repo.git"
|
||||
subprocess.run(["git", "init", "-q", str(work)], check=True)
|
||||
subprocess.run(["git", "-C", str(work), "config", "user.name", "test"], check=True)
|
||||
subprocess.run(
|
||||
["git", "-C", str(work), "config", "user.email", "test@example.invalid"],
|
||||
check=True,
|
||||
)
|
||||
record = _record()
|
||||
_write_json(work / "versions" / "V2R2C1.json", record)
|
||||
subprocess.run(["git", "-C", str(work), "add", "versions"], check=True)
|
||||
subprocess.run(["git", "-C", str(work), "commit", "-qm", "registry-1"], check=True)
|
||||
branch = subprocess.run(
|
||||
["git", "-C", str(work), "branch", "--show-current"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=True,
|
||||
).stdout.strip()
|
||||
subprocess.run(["git", "clone", "-q", "--bare", str(work), str(bare)], check=True)
|
||||
pinned = source_registry_snapshot(
|
||||
bare,
|
||||
"V2R2C1",
|
||||
main_ref=f"refs/heads/{branch}",
|
||||
)
|
||||
|
||||
updated = dict(record)
|
||||
updated["review_marker"] = "main-advanced"
|
||||
_write_json(work / "versions" / "V2R2C1.json", updated)
|
||||
subprocess.run(["git", "-C", str(work), "add", "versions"], check=True)
|
||||
subprocess.run(["git", "-C", str(work), "commit", "-qm", "registry-2"], check=True)
|
||||
subprocess.run(["git", "-C", str(work), "remote", "add", "authority", str(bare)], check=True)
|
||||
subprocess.run(["git", "-C", str(work), "push", "-q", "authority", branch], check=True)
|
||||
|
||||
repinned = source_registry_snapshot(
|
||||
bare,
|
||||
"V2R2C1",
|
||||
main_ref=f"refs/heads/{branch}",
|
||||
source_registry_main_oid=pinned["source_registry_main_oid"],
|
||||
)
|
||||
assert repinned["source_registry_main_oid"] == pinned["source_registry_main_oid"]
|
||||
assert repinned["bytes"] == pinned["bytes"]
|
||||
assert repinned["source_registry_sha256"] == pinned["source_registry_sha256"]
|
||||
assert repinned["record"].get("review_marker") is None
|
||||
|
||||
|
||||
def _git_commit_with_release_tag(tmp_path: Path, *, annotated: bool = True):
|
||||
work = tmp_path / "authority-work"
|
||||
bare = tmp_path / "authority.git"
|
||||
subprocess.run(["git", "init", "-q", str(work)], check=True)
|
||||
subprocess.run(["git", "-C", str(work), "config", "user.name", "test"], check=True)
|
||||
subprocess.run(["git", "-C", str(work), "config", "user.email", "test@example.invalid"], check=True)
|
||||
(work / "README").write_text("release\n", encoding="utf-8")
|
||||
subprocess.run(["git", "-C", str(work), "add", "README"], check=True)
|
||||
subprocess.run(["git", "-C", str(work), "commit", "-qm", "target"], check=True)
|
||||
target = subprocess.run(
|
||||
["git", "-C", str(work), "rev-parse", "HEAD"], capture_output=True, text=True, check=True
|
||||
).stdout.strip()
|
||||
tag_args = ["git", "-C", str(work), "tag"]
|
||||
if annotated:
|
||||
tag_args += ["-a", "V2R2C1", target, "-m", "release"]
|
||||
else:
|
||||
tag_args += ["V2R2C1", target]
|
||||
subprocess.run(tag_args, check=True)
|
||||
tag_oid = subprocess.run(
|
||||
["git", "-C", str(work), "rev-parse", "refs/tags/V2R2C1"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=True,
|
||||
).stdout.strip()
|
||||
subprocess.run(["git", "-C", str(work), "commit", "--allow-empty", "-qm", "evidence"], check=True)
|
||||
evidence = subprocess.run(
|
||||
["git", "-C", str(work), "rev-parse", "HEAD"], capture_output=True, text=True, check=True
|
||||
).stdout.strip()
|
||||
record = _record(tag_oid=tag_oid, target_oid=target)
|
||||
record["evidence_closure_oid"] = evidence
|
||||
_write_json(work / "versions" / "V2R2C1.json", record)
|
||||
subprocess.run(["git", "-C", str(work), "add", "versions"], check=True)
|
||||
subprocess.run(["git", "-C", str(work), "commit", "-qm", "source registry"], check=True)
|
||||
branch = subprocess.run(
|
||||
["git", "-C", str(work), "branch", "--show-current"], capture_output=True, text=True, check=True
|
||||
).stdout.strip()
|
||||
subprocess.run(["git", "clone", "-q", "--bare", str(work), str(bare)], check=True)
|
||||
return work, bare, branch, record, target, evidence, tag_oid
|
||||
|
||||
|
||||
def test_source_registry_git_identity_rejects_lightweight_tag(tmp_path: Path):
|
||||
_, bare, _, record, _, _, _ = _git_commit_with_release_tag(tmp_path, annotated=False)
|
||||
with pytest.raises(RegistryConvergenceError, match="tag_not_annotated"):
|
||||
verify_source_registry_git_identity(bare, record)
|
||||
|
||||
|
||||
def test_source_registry_git_identity_proves_annotated_tag_and_lineage(
|
||||
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
||||
):
|
||||
_, bare, _, record, target, evidence, tag_oid = _git_commit_with_release_tag(tmp_path)
|
||||
result = verify_source_registry_git_identity(bare, record)
|
||||
assert result["tag_oid"] == tag_oid
|
||||
assert result["target_oid"] == target
|
||||
assert result["evidence_closure_oid"] == evidence
|
||||
|
||||
authority_root = tmp_path / "authority-root"
|
||||
authority_root.mkdir()
|
||||
monkeypatch.setenv("AIOS_GITOPS_APPROVAL_AUTHORITY_ROOT", str(authority_root))
|
||||
authority = _canonical_approval(authority_root, record)
|
||||
assert verify_approval_authority(authority, record)["release_binding_verified"] is True
|
||||
|
||||
|
||||
def test_source_registry_generator_requires_authoritative_git_and_approval(
|
||||
tmp_path: Path, monkeypatch: pytest.MonkeyPatch,
|
||||
):
|
||||
_, bare, _, record, _, _, _ = _git_commit_with_release_tag(tmp_path)
|
||||
subprocess.run(
|
||||
["git", f"--git-dir={bare}", "config", "remote.gitea.url", "https://g.cnnetmkt.top/aios/aios.git"],
|
||||
check=True,
|
||||
)
|
||||
authority_root = tmp_path / "authority-root"
|
||||
authority_root.mkdir()
|
||||
monkeypatch.setenv("AIOS_GITOPS_AUTHORITATIVE_GIT_DIR", str(bare))
|
||||
monkeypatch.setenv("AIOS_GITOPS_APPROVAL_AUTHORITY_ROOT", str(authority_root))
|
||||
authority = _canonical_approval(authority_root, record)
|
||||
output = tmp_path / "generated" / "versions" / "V2R2C1.json"
|
||||
script = Path(__file__).resolve().parents[1] / "scripts" / "generate_source_registry.py"
|
||||
command = [
|
||||
sys.executable,
|
||||
str(script),
|
||||
"--output",
|
||||
str(output),
|
||||
"--version",
|
||||
record["version"],
|
||||
"--tag-oid",
|
||||
record["tag_oid"],
|
||||
"--target-oid",
|
||||
record["target_oid"],
|
||||
"--peeled-target-oid",
|
||||
record["peeled_target_oid"],
|
||||
"--evidence-closure-oid",
|
||||
record["evidence_closure_oid"],
|
||||
"--approval-plan-id",
|
||||
record["approval_plan_id"],
|
||||
"--repository",
|
||||
str(bare),
|
||||
"--approval-authority",
|
||||
str(authority),
|
||||
]
|
||||
completed = subprocess.run(command, capture_output=True, text=True, check=False)
|
||||
assert completed.returncode == 0, completed.stderr
|
||||
assert json.loads(output.read_text(encoding="utf-8"))["tag_oid"] == record["tag_oid"]
|
||||
|
||||
wrong = list(command)
|
||||
wrong[wrong.index("--approval-plan-id") + 1] = "sha256:" + "f" * 64
|
||||
failed = subprocess.run(wrong, capture_output=True, text=True, check=False)
|
||||
assert failed.returncode != 0
|
||||
other = tmp_path / "other.git"
|
||||
subprocess.run(["git", "clone", "-q", "--bare", str(bare), str(other)], check=True)
|
||||
arbitrary = list(command)
|
||||
arbitrary[arbitrary.index("--repository") + 1] = str(other)
|
||||
failed_authority = subprocess.run(arbitrary, capture_output=True, text=True, check=False)
|
||||
assert failed_authority.returncode != 0
|
||||
|
||||
|
||||
def test_approval_authority_rejects_unassociated_plan(
|
||||
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
||||
):
|
||||
record = _record()
|
||||
authority_root = tmp_path / "authority-root"
|
||||
authority_root.mkdir()
|
||||
monkeypatch.setenv("AIOS_GITOPS_APPROVAL_AUTHORITY_ROOT", str(authority_root))
|
||||
authority = authority_root / f"{record['version']}.json"
|
||||
_write_json(
|
||||
authority,
|
||||
{
|
||||
"schema": "aios.v2r2c1.tag-approval-request.v1",
|
||||
"state": "APPROVED",
|
||||
"approval_plan_id": record["approval_plan_id"],
|
||||
},
|
||||
)
|
||||
authority.chmod(0o640)
|
||||
with pytest.raises(RegistryConvergenceError, match="binding_missing"):
|
||||
verify_approval_authority(authority, record)
|
||||
|
||||
|
||||
def test_approval_authority_rejects_untrusted_path(tmp_path: Path, monkeypatch: pytest.MonkeyPatch):
|
||||
authority_root = tmp_path / "authority-root"
|
||||
authority_root.mkdir()
|
||||
monkeypatch.setenv("AIOS_GITOPS_APPROVAL_AUTHORITY_ROOT", str(authority_root))
|
||||
authority = tmp_path / "approval.json"
|
||||
_write_json(authority, {"schema": "aios.v2r2c1.tag-approval-request.v1"})
|
||||
with pytest.raises(RegistryConvergenceError, match="untrusted_path"):
|
||||
verify_approval_authority(authority, _record())
|
||||
|
||||
|
||||
def test_approval_authority_rejects_revoked_receipt(tmp_path: Path, monkeypatch: pytest.MonkeyPatch):
|
||||
authority_root = tmp_path / "authority-root"
|
||||
authority_root.mkdir()
|
||||
monkeypatch.setenv("AIOS_GITOPS_APPROVAL_AUTHORITY_ROOT", str(authority_root))
|
||||
record = _record()
|
||||
authority = _canonical_approval(authority_root, record)
|
||||
value = json.loads(authority.read_text(encoding="utf-8"))
|
||||
value["revoked"] = True
|
||||
authority.write_text(json.dumps(value) + "\n", encoding="utf-8")
|
||||
authority.chmod(0o640)
|
||||
with pytest.raises(RegistryConvergenceError, match="revoked"):
|
||||
verify_approval_authority(authority, record)
|
||||
|
||||
|
||||
def test_baseline_apply_wires_convergence_to_caller_repository():
|
||||
source = inspect.getsource(adapter.baseline_apply)
|
||||
assert "assert_source_registry_convergence(" in source
|
||||
assert "repo_git_dir=args.repository" in source
|
||||
# The gate must execute before the dict that writes final production status.
|
||||
assert source.index("assert_source_registry_convergence(") < source.index(
|
||||
'"status": "PRODUCTION_DEPLOYED"'
|
||||
)
|
||||
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"approval_plan_id": "sha256:c0df52008ae7f64cec5ee2d28947aa7b892066c670ab8295e4549140b1a7500e",
|
||||
"artifact_sha256": "ab4ae257346c82863f05abf5e309381a1c86612957608c3a4f4ab5cc9233f052",
|
||||
"deployment_state": "ACTIVE",
|
||||
"enabled": true,
|
||||
"evidence_closure_oid": "a8b906d1bc24088c19647e98de6a679268f6d0a2",
|
||||
"peeled_target_oid": "998d30304787e31e5ae607c0bd86b5edb2b0e5ad",
|
||||
"production_deployed": true,
|
||||
"production_port": 18927,
|
||||
"production_service": "aios-v2r1c7-production.service",
|
||||
"registered": true,
|
||||
"repository": "aios/aios",
|
||||
"schema": "aios.production-registry.v2",
|
||||
"status": "PRODUCTION_DEPLOYED",
|
||||
"tag": "V2R2C1",
|
||||
"tag_object": "07fc2746b688f5f243f29b39de39f32edc3dd9d2",
|
||||
"tag_oid": "07fc2746b688f5f243f29b39de39f32edc3dd9d2",
|
||||
"target_oid": "998d30304787e31e5ae607c0bd86b5edb2b0e5ad",
|
||||
"updated_at": "2026-08-09T09:13:13Z",
|
||||
"version": "V2R2C1"
|
||||
}
|
||||
Reference in New Issue
Block a user