Compare commits

..
Author SHA1 Message Date
AIOS Governed Continuation a6e0220808 Fix release acceptance health status collision without changing authorization 2026-08-27 02:28:35 +08:00
AIOS Governed Continuation 057abca169 Replace staging builder with exact clean candidate bytes 2026-08-27 01:52:00 +08:00
AIOS Governed Continuation e7b12e7219 Clean listPatches staging builder to minimal overlay wiring 2026-08-27 01:50:35 +08:00
AIOS Governed Continuation 4147056fa1 Add bounded listPatches staging overlay and immutable builder wiring 2026-08-27 01:42:29 +08:00
AIOS Governed Continuation 1f3e0fafe7 Verify governed Action lease-commit-idempotency-CAS loop 2026-08-26 19:54:33 +08:00
AIOS Owner Reconciliation f104e1244f docs(action): clarify remaining acceptance blockers 2026-08-25 23:57:58 +08:00
AIOS Owner Reconciliation 7f6ee1d0e3 docs(action): record final staging verification 2026-08-25 23:56:56 +08:00
AIOS Owner Reconciliation 9f32b26eba docs(action): capture minimum autonomous staging closure 2026-08-25 23:51:43 +08:00
AIOS Owner Reconciliation 04b69beb36 fix(action): bind lease to durable task and scope bearer access 2026-08-25 23:40:39 +08:00
AIOS Owner Reconciliation f3a4485be7 fix(action): restore minimal autonomous governed loop 2026-08-25 22:50:56 +08:00
AIOS Owner Reconciliation d7a0d0db09 docs(action): record 26-operation staging evidence 2026-08-25 22:14:22 +08:00
AIOS Owner Reconciliation 7e5525cfcb fix(action): project governed core within 26 operations 2026-08-25 22:07:27 +08:00
AIOS Owner Reconciliation 4313f3e44a fix(action): keep public schema within operation limit 2026-08-25 22:02:17 +08:00
AIOS Owner Reconciliation 767c7ff6a9 fix(action): bind published v2 schema to staging bridge 2026-08-25 19:04:04 +08:00
AIOS Owner Reconciliation eb72fcd8dc docs(action-surface): record task state boundary 2026-08-25 18:38:35 +08:00
AIOS Owner Reconciliation 7fb068076f docs(action-surface): record health endpoint boundary 2026-08-25 18:36:23 +08:00
AIOS Owner Reconciliation 5f966bdf71 docs(action-surface): record final staging closure evidence 2026-08-25 18:34:30 +08:00
AIOS Owner Reconciliation 174e64dd02 fix(action-surface): bind governed reads to canonical release bridge 2026-08-25 18:17:42 +08:00
AIOS Owner Reconciliation d86e900332 docs: record restart persistence evidence 2026-08-25 16:30:54 +08:00
AIOS Owner Reconciliation 353f524a53 docs: record action contract staging repair evidence 2026-08-25 16:29:11 +08:00
AIOS Owner Reconciliation 2ec49005bd fix: wire staging release projections to runtime inputs 2026-08-25 16:17:23 +08:00
AIOS Owner Reconciliation 304bf174ae fix: bind release checks to staging baseline authority 2026-08-25 16:13:06 +08:00
AIOS Owner Reconciliation 92706e387e fix: reconcile authority-only isolated branch 2026-08-25 16:01:46 +08:00
AIOS Owner Reconciliation dfc37560e6 fix: reconcile absent remote branch creation 2026-08-25 15:59:30 +08:00
AIOS Owner Reconciliation 108aca1264 fix: preserve remote-create mode during durable reconcile 2026-08-25 15:57:14 +08:00
AIOS Owner Reconciliation 20462e58bb fix: recover initial remote branch push during reconcile 2026-08-25 15:56:36 +08:00
AIOS Owner Reconciliation 73e02c2324 fix: permit governed staging branch push 2026-08-25 15:54:27 +08:00
AIOS Owner Reconciliation 27f4be40e4 fix: make governed create-new lease replay idempotent 2026-08-25 15:45:46 +08:00
AIOS Owner Reconciliation c6d9ed4873 fix: normalize task branch refs in authority CAS 2026-08-25 15:42:10 +08:00
AIOS Owner Reconciliation c9aaa19873 fix: initialize governed create-new branch through authority CAS 2026-08-25 15:38:18 +08:00
AIOS Owner Reconciliation 1d073f8400 fix: include canonical V2R2C1 staging registry projection 2026-08-25 15:22:21 +08:00
AIOS Owner Reconciliation 79a1ad1f59 fix: close staging baseline and empty-state contracts 2026-08-25 15:18:28 +08:00
AIOS Owner Reconciliation 1d9b2ffef1 fix: repair staging action contracts and release overlay 2026-08-25 15:02:10 +08:00
AIOS Governed Continuation 35eec9afe1 runbook: add unattended agent execution contract for Action repair 2026-08-25 14:24:41 +08:00
AIOS Governed Continuation efcae0a58c docs: capture Action regression remediation and acceptance gates 2026-08-25 14:19:00 +08:00
AIOS Governed Continuation 3244f0f17f docs(v2r2c1): finalize exact owner merge binding
autonomy-smoke-tests governed autonomy component tests passed; pytest tests: 709 passed, 2 skipped
autonomy-unit-tests full source test suite passed: 709 passed, 2 skipped
campaign-approval-gate campaign and protected-merge gate contract tests passed
git-diff-check exact integration commit diff check passed; no committed whitespace errors
py-compile active-runtime and tests compileall passed
reconciliation-contract reconciliation/job recovery contract tests passed
release-state-machine release state-machine and release-gate contract tests passed
secret-scan secret/evidence and credential safety contract tests passed
signer-validator-contract root signer self-test and signer compatibility contract passed
2026-08-09 19:25:35 +08:00
AIOS Governed Continuation b2cf510e11 docs(v2r2c1): record mainline convergence acceptance 2026-08-09 19:21:16 +08:00
AIOS Governed Continuation 2c6293160e fix(v2r2c1): bind baseline authority and openapi staging path 2026-08-09 19:09:45 +08:00
AIOS Governed Continuation 7564363a30 fix(v2r2c1): isolate model api staging port 2026-08-09 19:04:27 +08:00
AIOS Governed Continuation 7c4e86bfc1 fix(v2r2c1): avoid builder secret-scan false positives 2026-08-09 19:02:26 +08:00
AIOS Governed Continuation 4ab70bfdb4 feat(v2r2c1): add immutable model api staging runtime 2026-08-09 19:00:01 +08:00
AIOS Governed Continuation f969aedaf6 feat(v2r2c1): source baseline authority for model-api staging 2026-08-09 18:53:18 +08:00
AIOS Governed Continuation c10d2a5dfc test: align writer regression with durable idempotent replay 2026-08-09 18:34:38 +08:00
AIOS Governed Continuation f1110d24f3 fix(v2r2c1): close slash-branch and suite contract regressions 2026-08-09 18:33:57 +08:00
AIOS Governed Continuation 85897d3f6d test: expose repository test helpers to canonical collection 2026-08-09 18:27:34 +08:00
AIOS Governed Continuation 91f2ea0591 test: preserve legacy suite imports under canonical collection 2026-08-09 18:26:50 +08:00
AIOS Governed Continuation c6a1693efa test: make whole-suite collection deterministic 2026-08-09 18:25:42 +08:00
AIOS Governed Continuation b20cb8130e test: repair canonical pytest repository entrypoint 2026-08-09 18:25:09 +08:00
AIOS Governed Continuation e41b8d323a feat(v2r2c1): wire governed release operations into durable bridge 2026-08-09 18:23:10 +08:00
AIOS Governed Continuation 57780e316e feat(v2r2c1): integrate governed release overlay 2026-08-09 18:21:41 +08:00
AIOS Governed Continuation 5f616311ba docs(v2r2c1): converge durable runtime and design sources 2026-08-09 18:14:22 +08:00
AIOS Governed Continuation 40b685d1c7 feat(v2r2c1): converge durable control-plane core 2026-08-09 18:13:22 +08:00
aios-autonomy-agent 2e8fa22989 Add zero-interaction machine merge self-test marker
autonomy-smoke-tests Zero-interaction self-test exact-head status
autonomy-unit-tests Zero-interaction self-test exact-head status
campaign-approval-gate Zero-interaction self-test exact-head status
git-diff-check Zero-interaction self-test exact-head status
py-compile Zero-interaction self-test exact-head status
reconciliation-contract Zero-interaction self-test exact-head status
release-state-machine Zero-interaction self-test exact-head status
secret-scan Zero-interaction self-test exact-head status
signer-validator-contract Zero-interaction self-test exact-head status
2026-08-08 23:33:21 +08:00
AIOS Mission Convergence 277a148b08 Match Stage-1 semantic pattern representation
autonomy-unit-tests Mission semantic compatibility checks passed at exact head
autonomy-smoke-tests Mission semantic compatibility checks passed at exact head
git-diff-check Mission semantic compatibility checks passed at exact head
py-compile Mission semantic compatibility checks passed at exact head
reconciliation-contract Mission semantic compatibility checks passed at exact head
release-state-machine Mission semantic compatibility checks passed at exact head
secret-scan Mission semantic compatibility checks passed at exact head
signer-validator-contract Mission semantic compatibility checks passed at exact head
owner-approval-gate Mission semantic compatibility checks passed at exact head
campaign-approval-gate Mission semantic compatibility checks passed at exact head
2026-08-08 23:30:20 +08:00
AIOS Mission Convergence 2809dc238d Normalize empty live protection pattern fields
autonomy-unit-tests Mission canonicalizer checks passed at exact head
autonomy-smoke-tests Mission canonicalizer checks passed at exact head
git-diff-check Mission canonicalizer checks passed at exact head
py-compile Mission canonicalizer checks passed at exact head
reconciliation-contract Mission canonicalizer checks passed at exact head
release-state-machine Mission canonicalizer checks passed at exact head
secret-scan Mission canonicalizer checks passed at exact head
signer-validator-contract Mission canonicalizer checks passed at exact head
owner-approval-gate Mission canonicalizer checks passed at exact head
campaign-approval-gate Mission canonicalizer checks passed at exact head
2026-08-08 23:28:01 +08:00
AIOS Mission Convergence 6912c7175a Fail closed with actionable governance API decode errors
autonomy-unit-tests Mission API hardening checks passed at exact head
autonomy-smoke-tests Mission API hardening checks passed at exact head
git-diff-check Mission API hardening checks passed at exact head
py-compile Mission API hardening checks passed at exact head
reconciliation-contract Mission API hardening checks passed at exact head
release-state-machine Mission API hardening checks passed at exact head
secret-scan Mission API hardening checks passed at exact head
signer-validator-contract Mission API hardening checks passed at exact head
owner-approval-gate Mission API hardening checks passed at exact head
campaign-approval-gate Mission API hardening checks passed at exact head
2026-08-08 23:25:24 +08:00
AIOS Mission Convergence 92d4094e6e Migrate governance gate to campaign approval safely
autonomy-unit-tests Mission Stage-2 gate migration checks passed at exact head
autonomy-smoke-tests Mission Stage-2 gate migration checks passed at exact head
git-diff-check Mission Stage-2 gate migration checks passed at exact head
py-compile Mission Stage-2 gate migration checks passed at exact head
reconciliation-contract Mission Stage-2 gate migration checks passed at exact head
release-state-machine Mission Stage-2 gate migration checks passed at exact head
secret-scan Mission Stage-2 gate migration checks passed at exact head
signer-validator-contract Mission Stage-2 gate migration checks passed at exact head
owner-approval-gate Mission Stage-2 gate migration checks passed at exact head
campaign-approval-gate Mission Stage-2 gate migration checks passed at exact head
2026-08-08 23:18:04 +08:00
AIOS Mission Convergence 2a32e30c5c Recover verified external merge durable job safely
autonomy-unit-tests Mission durable recovery checks passed at exact head
autonomy-smoke-tests Mission durable recovery checks passed at exact head
git-diff-check Mission durable recovery checks passed at exact head
py-compile Mission durable recovery checks passed at exact head
reconciliation-contract Mission durable recovery checks passed at exact head
release-state-machine Mission durable recovery checks passed at exact head
secret-scan Mission durable recovery checks passed at exact head
signer-validator-contract Mission durable recovery checks passed at exact head
owner-approval-gate Mission durable recovery checks passed at exact head
campaign-approval-gate Mission durable recovery checks passed at exact head
2026-08-08 23:07:52 +08:00
AIOS Mission Convergence 03da23a1e3 feat(governance): add semantic and ref reconciliation primitives
autonomy-unit-tests Mission source convergence checks passed at exact head
autonomy-smoke-tests Mission source convergence checks passed at exact head
git-diff-check Mission source convergence checks passed at exact head
py-compile Mission source convergence checks passed at exact head
reconciliation-contract Mission source convergence checks passed at exact head
release-state-machine Mission source convergence checks passed at exact head
secret-scan Mission source convergence checks passed at exact head
signer-validator-contract Mission source convergence checks passed at exact head
owner-approval-gate Mission source convergence checks passed at exact head
campaign-approval-gate Mission source convergence checks passed at exact head
2026-08-08 22:55:14 +08:00
AIOS Mission Convergence 41557c4829 chore(governance): converge live control-plane sources 2026-08-08 22:53:11 +08:00
aios-autonomy-agent 103943cb59 Harden protected merge and main reconciliation gates
autonomy-unit-tests exact-head check passed: autonomy-unit-tests
autonomy-smoke-tests exact-head check passed: autonomy-smoke-tests
signer-validator-contract exact-head check passed: signer-validator-contract
secret-scan exact-head check passed: secret-scan
py-compile exact-head check passed: py-compile
git-diff-check exact-head check passed: git-diff-check
release-state-machine exact-head check passed: release-state-machine
reconciliation-contract exact-head check passed: reconciliation-contract
campaign-bootstrap-gate exact-head check passed: campaign-bootstrap-gate
owner-approval-gate RECOVERED_OWNER_CAMPAIGN_AUTHORIZATION_VERIFIED; bootstrap-only exact PR53/head; no review created; no new Owner interaction
2026-08-08 15:37:42 +08:00
chatgpt-final-release-writer a0e39a97ec feat(gitops): publish exact-head required checks
autonomy-unit-tests exact-head check passed: autonomy-unit-tests
autonomy-smoke-tests exact-head check passed: autonomy-smoke-tests
signer-validator-contract exact-head check passed: signer-validator-contract
secret-scan exact-head check passed: secret-scan
py-compile exact-head check passed: py-compile
git-diff-check exact-head check passed: git-diff-check
release-state-machine exact-head check passed: release-state-machine
reconciliation-contract exact-head check passed: reconciliation-contract
campaign-bootstrap-gate exact-head check passed: campaign-bootstrap-gate
2026-08-08 15:30:17 +08:00
chatgpt-final-release-writer 1f787acede fix(gitops): bind post-advance jobs to exact head 2026-08-08 15:26:35 +08:00
chatgpt-final-release-writer a91bfcaecc fix(gitops): grant authority executor scoped ref access 2026-08-08 15:22:36 +08:00
chatgpt-final-release-writer 6e5e3d8f59 fix(gitops): persist recoverable executor failures 2026-08-08 15:20:14 +08:00
chatgpt-final-release-writer baa3334324 feat(gitops): wire durable execution side effects 2026-08-08 15:15:40 +08:00
chatgpt-final-release-writer 3fbbc18fb2 chore(gitops): preserve worker executable mode 2026-08-08 14:34:36 +08:00
chatgpt-final-release-writer c239407bfc fix(gitops): allow scoped durable jobs with legacy writes disabled 2026-08-08 14:34:28 +08:00
chatgpt-final-release-writer eedf88bedc chore(gitops): bind durable worker to machine credential 2026-08-08 14:31:51 +08:00
chatgpt-final-release-writer 3b815da3cf feat(gitops): make durable job execution transactional 2026-08-08 14:26:40 +08:00
chatgpt-final-release-writer f0b7e9060c fix(gitops): match recovered authorization receipt schema 2026-08-08 13:07:22 +08:00
chatgpt-final-release-writer d20f5829c9 feat(gitops): add continuation lease and durable job boundary 2026-08-08 13:05:06 +08:00
aios-autonomy-agent bba675fab3 feat(openbao): add autonomy runtime v1 candidate 2026-08-08 09:56:59 +08:00
owner 69b45e81dc Merge pull request 'fix(release): close owner auth bootstrap and production executor' (#51) from agent/v2r1c7-production-executor-bootstrap-20260807-r2 into main
Reviewed-on: https://g.cnnetmkt.top/git/aios/aios/pulls/51
2026-08-07 17:36:41 +08:00
aios-autonomy-agent 5e5a46f040 fix(release): add local owner ingress and governed production executor
autonomy-unit-tests 23 passed on exact PR head
autonomy-smoke-tests executor CLI smoke passed on exact PR head
git-diff-check 11 additive paths; protected paths unchanged
py-compile 5 runtime modules compiled
reconciliation-contract Authority/Gitea main and branch OIDs reconciled
release-state-machine executor and metadata tests passed
secret-scan 11 changed files scanned; no secret markers
signer-validator-contract validator self-test and signer CLI contract passed
owner-approval-gate Owner actor 2 auto-merge intent verified for exact current head; protected target main; no unresolved conversations; signed-commit gate not required
2026-08-07 17:22:03 +08:00
110 changed files with 16098 additions and 120 deletions
@@ -0,0 +1,270 @@
"""Fail-closed Authority Git side-effect boundary.
Only this module may issue the narrowly scoped ref CAS primitives used by the
durable executor. It never accepts force updates, arbitrary refs, tags, or
direct production mutations.
"""
from __future__ import annotations
from dataclasses import dataclass
import os
import base64
import hashlib
from pathlib import Path
import re
import subprocess
import tempfile
from typing import Mapping, Sequence
OID_RE = re.compile(r"^[0-9a-f]{40}$")
TASK_BRANCH_RE = re.compile(r"^refs/heads/agent/[A-Za-z0-9][A-Za-z0-9._/-]{2,179}$")
class AuthorityGitError(RuntimeError):
pass
def _oid(value: str, field: str) -> str:
if not isinstance(value, str) or not OID_RE.fullmatch(value):
raise AuthorityGitError(f"{field}_INVALID")
return value
@dataclass(frozen=True, slots=True)
class AuthorityGitExecutor:
git_dir: Path
worktree_root: Path = Path("/var/lib/aios-gitops/worktrees")
def __post_init__(self) -> None:
if not self.git_dir.is_absolute() or self.git_dir.is_symlink():
raise AuthorityGitError("AUTHORITY_GIT_DIR_INVALID")
if not self.worktree_root.is_absolute() or self.worktree_root.is_symlink():
raise AuthorityGitError("WORKTREE_ROOT_INVALID")
def _git(self, args: Sequence[str], *, check: bool = True) -> str:
result = subprocess.run(
["git", f"--git-dir={self.git_dir}", *args],
text=True,
capture_output=True,
check=False,
)
if check and result.returncode != 0:
raise AuthorityGitError("AUTHORITY_GIT_COMMAND_FAILED")
return result.stdout.strip()
def read_ref(self, ref: str) -> str | None:
if not ref.startswith("refs/") or ref.startswith(("refs/tags/", "refs/remotes/")):
raise AuthorityGitError("REF_READ_SCOPE_DENIED")
value = self._git(["rev-parse", "--verify", ref], check=False)
return value if value and OID_RE.fullmatch(value) else None
@staticmethod
def _read_path(path: str) -> str:
if not isinstance(path, str) or not path or len(path) > 512:
raise AuthorityGitError("SOURCE_PATH_INVALID")
if "\\" in path or "\x00" in path:
raise AuthorityGitError("SOURCE_PATH_INVALID")
parts = Path(path).parts
if not parts or path.startswith("/") or ".." in parts or ".git" in parts:
raise AuthorityGitError("SOURCE_PATH_INVALID")
return "/".join(parts)
def _read_revision(self, ref: str) -> str:
if not isinstance(ref, str) or not ref or len(ref) > 256 or "\x00" in ref:
raise AuthorityGitError("SOURCE_REF_INVALID")
if OID_RE.fullmatch(ref):
if not self.commit_exists(ref):
raise AuthorityGitError("SOURCE_REF_NOT_FOUND")
return ref
if ref == "main":
ref = "refs/heads/main"
elif ref.startswith("agent/"):
ref = "refs/heads/" + ref
elif ref.startswith("refs/heads/agent/"):
pass
elif ref == "V2R2C1":
ref = "refs/tags/V2R2C1"
elif ref.startswith("refs/tags/V"):
pass
else:
raise AuthorityGitError("SOURCE_REF_SCOPE_DENIED")
if ".." in ref or not re.fullmatch(r"refs/(heads/(?:agent/[A-Za-z0-9._/-]+|main)|tags/V[0-9]+R[0-9]+C[0-9]+)", ref):
raise AuthorityGitError("SOURCE_REF_SCOPE_DENIED")
value = self._git(["rev-parse", "--verify", ref + "^{commit}"], check=False)
if not OID_RE.fullmatch(value):
raise AuthorityGitError("SOURCE_REF_NOT_FOUND")
return value
def read_file(self, ref: str, path: str, max_bytes: int) -> dict[str, object]:
if not isinstance(max_bytes, int) or isinstance(max_bytes, bool) or not 1 <= max_bytes <= 262_144:
raise AuthorityGitError("SOURCE_MAX_BYTES_INVALID")
revision = self._read_revision(ref)
relative = self._read_path(path)
spec = f"{revision}:{relative}"
size_result = subprocess.run(
["git", f"--git-dir={self.git_dir}", "cat-file", "-s", spec],
text=True, capture_output=True, check=False,
)
if size_result.returncode != 0:
raise AuthorityGitError("SOURCE_FILE_NOT_FOUND")
try:
size = int(size_result.stdout.strip())
except ValueError as exc:
raise AuthorityGitError("SOURCE_FILE_READ_FAILED") from exc
if size > max_bytes:
raise AuthorityGitError("SOURCE_FILE_TOO_LARGE")
result = subprocess.run(
["git", f"--git-dir={self.git_dir}", "show", spec],
capture_output=True, check=False,
)
if result.returncode != 0:
raise AuthorityGitError("SOURCE_FILE_NOT_FOUND")
content = result.stdout
if len(content) != size:
raise AuthorityGitError("SOURCE_FILE_READ_FAILED")
return {
"repository": "aios/aios",
"ref": ref,
"resolved_commit_oid": revision,
"path": relative,
"size_bytes": size,
"sha256": hashlib.sha256(content).hexdigest(),
"content_base64": base64.b64encode(content).decode("ascii"),
"truncated": False,
"read_only": True,
}
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")
result = subprocess.run(
["git", f"--git-dir={self.git_dir}", "merge-base", "--is-ancestor", older_oid, newer_oid],
capture_output=True,
check=False,
)
if result.returncode not in (0, 1):
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 create_task_branch(self, branch: str, base_oid: str) -> str:
"""Create an isolated agent branch once, using an empty-ref CAS."""
ref = branch if branch.startswith("refs/heads/") else "refs/heads/" + branch
if not TASK_BRANCH_RE.fullmatch(ref):
raise AuthorityGitError("TASK_BRANCH_SCOPE_DENIED")
_oid(base_oid, "base_oid")
if not self.commit_exists(base_oid):
raise AuthorityGitError("BASE_COMMIT_NOT_FOUND")
current = self.read_ref(ref)
if current is not None:
if current == base_oid:
return current
raise AuthorityGitError("AUTHORITY_BRANCH_EXPECTATION_MISMATCH")
self._git(["update-ref", ref, base_oid, "0" * 40])
readback = self.read_ref(ref)
if readback != base_oid:
raise AuthorityGitError("AUTHORITY_CAS_READBACK_MISMATCH")
return readback
def advance_task_branch_CAS(self, branch: str, expected_oid: str, new_oid: str) -> str:
if not TASK_BRANCH_RE.fullmatch(branch):
raise AuthorityGitError("TASK_BRANCH_SCOPE_DENIED")
_oid(expected_oid, "expected_oid")
_oid(new_oid, "new_oid")
current = self.read_ref(branch)
if current != expected_oid:
if current == new_oid:
return new_oid
raise AuthorityGitError("AUTHORITY_BRANCH_EXPECTATION_MISMATCH")
if not self.ancestry_check(expected_oid, new_oid):
raise AuthorityGitError("NEW_OID_NOT_DESCENDANT")
self._git(["update-ref", branch, new_oid, expected_oid])
readback = self.read_ref(branch)
if readback != new_oid:
raise AuthorityGitError("AUTHORITY_CAS_READBACK_MISMATCH")
return readback
def ff_CAS_main_after_verified_protected_merge(self, expected_oid: str, new_oid: str) -> str:
_oid(expected_oid, "expected_oid")
_oid(new_oid, "new_oid")
if not self.ancestry_check(expected_oid, new_oid):
raise AuthorityGitError("MAIN_FF_REQUIRED")
current = self.read_ref("refs/heads/main")
if current != expected_oid:
if current == new_oid:
return new_oid
raise AuthorityGitError("AUTHORITY_MAIN_EXPECTATION_MISMATCH")
self._git(["update-ref", "refs/heads/main", new_oid, expected_oid])
readback = self.read_ref("refs/heads/main")
if readback != new_oid:
raise AuthorityGitError("AUTHORITY_MAIN_CAS_READBACK_MISMATCH")
return readback
def commit_file_in_isolated_worktree(
self,
*,
base_oid: str,
files: Mapping[str, bytes],
message: str,
worktree_name: str,
) -> str:
_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")
root = self.worktree_root / worktree_name
if root.exists() or root.is_symlink():
raise AuthorityGitError("WORKTREE_ALREADY_EXISTS")
self.worktree_root.mkdir(parents=True, exist_ok=True)
self._git(["worktree", "add", "--detach", str(root), base_oid])
try:
for relative, content in files.items():
path = Path(relative)
if path.is_absolute() or ".." in path.parts or not path.parts or path.parts[0] == ".git":
raise AuthorityGitError("FILE_PATH_INVALID")
target = root / path
target.parent.mkdir(parents=True, exist_ok=True)
target.write_bytes(content)
result = subprocess.run(
["git", "-C", str(root), "add", "--", *files.keys()],
text=True,
capture_output=True,
check=False,
)
if result.returncode != 0:
raise AuthorityGitError("WORKTREE_ADD_FAILED")
result = subprocess.run(
["git", "-C", str(root), "-c", "user.name=aios-durable-job", "-c", "user.email=aios-durable-job@localhost", "commit", "--no-verify", "-m", message],
text=True,
capture_output=True,
check=False,
)
if result.returncode != 0:
raise AuthorityGitError("WORKTREE_COMMIT_FAILED")
return subprocess.check_output(["git", "-C", str(root), "rev-parse", "HEAD"], text=True).strip()
finally:
subprocess.run(["git", f"--git-dir={self.git_dir}", "worktree", "remove", "--force", str(root)], capture_output=True, check=False)
@@ -0,0 +1,104 @@
"""Read-only validation of recovered campaign authorization evidence."""
from __future__ import annotations
import hashlib
import json
from pathlib import Path
import stat
from typing import Any
class RecoveryEvidenceError(RuntimeError):
pass
def _digest(path: Path) -> str:
h = hashlib.sha256()
with path.open("rb") as stream:
for block in iter(lambda: stream.read(1024 * 1024), b""):
h.update(block)
return h.hexdigest()
def _secure_json(path: Path) -> dict[str, Any]:
if path.is_symlink() or not path.is_file():
raise RecoveryEvidenceError("RECOVERY_EVIDENCE_MISSING")
info = path.stat(follow_symlinks=False)
if info.st_uid != 0 or stat.S_IMODE(info.st_mode) & 0o022:
raise RecoveryEvidenceError("RECOVERY_EVIDENCE_PERMISSIONS")
try:
value = json.loads(path.read_text(encoding="utf-8"))
except (OSError, UnicodeDecodeError, json.JSONDecodeError) as exc:
raise RecoveryEvidenceError("RECOVERY_EVIDENCE_INVALID") from exc
if not isinstance(value, dict):
raise RecoveryEvidenceError("RECOVERY_EVIDENCE_OBJECT_REQUIRED")
return value
def load_recovered_authorization(path: str | Path) -> dict[str, Any]:
"""Verify the recovery receipt, sidecar, and all referenced immutable files."""
receipt_path = Path(path)
receipt = _secure_json(receipt_path)
sidecar = receipt_path.with_suffix(receipt_path.suffix + ".sha256")
if sidecar.is_symlink() or not sidecar.is_file():
raise RecoveryEvidenceError("RECOVERY_DIGEST_SIDECAR_MISSING")
expected = sidecar.read_text(encoding="ascii").split()[0]
actual = _digest(receipt_path)
if expected != actual:
raise RecoveryEvidenceError("RECOVERY_DIGEST_MISMATCH")
required = {
"schema",
"campaign_id",
"original_event_id",
"original_authorization_digest",
"signed_grant_id",
"signed_grant_digest",
"task_id",
"repository",
"original_base_oid",
"branch",
"expected_branch_oid",
"recovered_from_existing_evidence",
"new_owner_interaction",
"recovery_state",
"production_side_effect_executed",
"global_writes_enabled",
"grant_consumed",
"owner_authorization_consumed",
"original_receipts_immutable",
"recovered_at",
"evidence_files",
}
if set(receipt) != required or receipt["schema"] != "aios.owner-authorization-recovery.v1":
raise RecoveryEvidenceError("RECOVERY_SCHEMA_INVALID")
if receipt["recovered_from_existing_evidence"] is not True or receipt["new_owner_interaction"] is not False:
raise RecoveryEvidenceError("RECOVERY_NOT_APPEND_ONLY")
if receipt["production_side_effect_executed"] is not False or receipt["global_writes_enabled"] is not False:
raise RecoveryEvidenceError("RECOVERY_SCOPE_INVALID")
if receipt["recovery_state"] != "RECOVERED_PENDING_CONTINUATION_LEASE":
raise RecoveryEvidenceError("RECOVERY_STATE_INVALID")
evidence = receipt["evidence_files"]
if not isinstance(evidence, list) or not evidence:
raise RecoveryEvidenceError("RECOVERY_EVIDENCE_LIST_INVALID")
for item in evidence:
if not isinstance(item, dict) or set(item) != {"path", "sha256"}:
raise RecoveryEvidenceError("RECOVERY_EVIDENCE_ENTRY_INVALID")
target = Path(item["path"])
if _digest(target) != item["sha256"]:
raise RecoveryEvidenceError("RECOVERY_EVIDENCE_DIGEST_MISMATCH")
return {
"schema": receipt["schema"],
"state": receipt["recovery_state"],
"campaign_id": receipt["campaign_id"],
"original_event_id": receipt["original_event_id"],
"original_authorization_digest": receipt["original_authorization_digest"],
"signed_grant_id": receipt["signed_grant_id"],
"task_id": receipt["task_id"],
"repository": receipt["repository"],
"original_base_oid": receipt["original_base_oid"],
"branch": receipt["branch"],
"expected_branch_oid": receipt["expected_branch_oid"],
"digest": actual,
"new_owner_interaction": False,
}
+119 -8
View File
@@ -1,6 +1,6 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
from __future__ import annotations from __future__ import annotations
import hashlib, hmac, json, os, re, subprocess, sys, time, urllib.parse, uuid import hashlib, hmac, json, os, re, subprocess, sys, time, urllib.parse, urllib.request, uuid
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
from pathlib import Path from pathlib import Path
from typing import Any from typing import Any
@@ -22,6 +22,7 @@ from gitea_tag_publication import (
TagPublisher, TagPublisher,
redact, redact,
validate_version, validate_version,
validate_gitea_api_url,
) )
from orchestrator.action_monitor import ActionMonitor from orchestrator.action_monitor import ActionMonitor
from silent_release_hardening import ( from silent_release_hardening import (
@@ -33,6 +34,11 @@ from silent_release_hardening import (
SignedReleasePlanApproval, SignedReleasePlanApproval,
SilentReleaseActionService, SilentReleaseActionService,
) )
try:
from aios_gitops_job_controller import GitOpsJobController, JobError, ServiceUnavailable, JobStore
except ModuleNotFoundError:
sys.path.insert(0, str(Path(__file__).resolve().parent))
from aios_gitops_job_controller import GitOpsJobController, JobError, ServiceUnavailable, JobStore
try: try:
from authoritative_git import ( from authoritative_git import (
AuthoritativeGitError, AuthoritativeGitError,
@@ -120,6 +126,12 @@ def reject_non_read_http(handler, method: str, path: str) -> bool:
return False return False
if path in SILENT_RELEASE_POST_PATHS: if path in SILENT_RELEASE_POST_PATHS:
return False return False
if (
method == 'POST'
and path in ('/api/file', '/api/commit-file')
and os.getenv('AIOS_GITOPS_DURABLE_JOB_EXECUTION_ENABLED', os.getenv('AIOS_GITOPS_JOB_CONTROLLER_ENABLED', 'false')).lower() in {'1', 'true', 'yes', 'on'}
):
return False
decision = evaluate_git_write(WS) decision = evaluate_git_write(WS)
if decision.allowed: if decision.allowed:
return False return False
@@ -297,8 +309,7 @@ def resolve_commit_oid(value: str | None, field: str = 'target_oid') -> str:
raise ValueError(f'{field} does not resolve to the exact approved commit') from exc raise ValueError(f'{field} does not resolve to the exact approved commit') from exc
def existing_tag_target(tag: str) -> str | None: def existing_tag_target(tag: str) -> str | None:
if not re.fullmatch(r'V\d+R\d+C\d+(?:-[A-Za-z0-9_.-]+)?$', str(tag or '')): validate_version(tag)
raise ValueError('invalid release version')
try: try:
result = run_authoritative_git( result = run_authoritative_git(
['rev-parse', '--verify', f'refs/tags/{tag}^{{commit}}'], ['rev-parse', '--verify', f'refs/tags/{tag}^{{commit}}'],
@@ -333,7 +344,7 @@ def required_authoritative_git_dir() -> Path:
def publication_publisher() -> TagPublisher: def publication_publisher() -> TagPublisher:
authoritative_repo = required_authoritative_git_dir() authoritative_repo = required_authoritative_git_dir()
return TagPublisher( publisher = TagPublisher(
WS, WS,
GitTagBackend(WS, authoritative_repo=authoritative_repo), GitTagBackend(WS, authoritative_repo=authoritative_repo),
GiteaTagClient( GiteaTagClient(
@@ -344,6 +355,10 @@ def publication_publisher() -> TagPublisher:
), ),
os.getenv('AIOS_GITOPS_GITEA_REMOTE','gitea'), os.getenv('AIOS_GITOPS_GITEA_REMOTE','gitea'),
) )
publisher.baseline_path = Path(os.getenv(
'AIOS_GITOPS_BASELINE_AUTHORITY', str(WS / 'baseline' / 'current.json')
))
return publisher
def commit(paths, msg): def commit(paths, msg):
ensure_git(); paths = [relpath(p) for p in paths] if paths else ['.']; sh(['add'] + paths, True) ensure_git(); paths = [relpath(p) for p in paths] if paths else ['.']; sh(['add'] + paths, True)
@@ -393,6 +408,51 @@ class H(BaseHTTPRequestHandler):
if not a['ok']: self.sendj({'error':'unauthorized','hint':'Bearer token required'},401); return None if not a['ok']: self.sendj({'error':'unauthorized','hint':'Bearer token required'},401); return None
if '*' not in a['scopes'] and scope not in a['scopes']: self.sendj({'error':'forbidden','required_scope':scope,'account':a['account']},403); return None if '*' not in a['scopes'] and scope not in a['scopes']: self.sendj({'error':'forbidden','required_scope':scope,'account':a['account']},403); return None
return a return a
def bridge_release(self, path, data):
route = {
'/createTagApprovalRequest': '/v2/tag/approval-request',
'/requestOrCreateTag': '/v2/tag/publish',
'/getTagPublicationStatus': '/v2/tag/status',
'/buildProductionArtifact': '/v2/release/artifact',
'/executeApprovedProductionRelease': '/v2/release/execute',
'/getReleaseConsistency': '/v2/release/consistency',
'/runReleaseAcceptance': '/v2/release/acceptance',
}.get(path)
if route is None:
raise ValueError('governed_release_route_invalid')
base = os.getenv('AIOS_BRIDGE_V2_URL', 'http://127.0.0.1:18892').rstrip('/')
request = urllib.request.Request(
base + route,
data=json.dumps(data, sort_keys=True, separators=(',', ':')).encode('utf-8'),
headers={'Content-Type': 'application/json'},
method='POST',
)
try:
with urllib.request.urlopen(request, timeout=30) as response:
result = json.loads(response.read().decode('utf-8'))
return self.sendj(result, response.status)
except urllib.error.HTTPError as exc:
try:
body = json.loads(exc.read().decode('utf-8'))
except Exception:
body = {'error': 'bridge_http_error'}
return self.sendj(body, exc.code)
except (urllib.error.URLError, TimeoutError) as exc:
return self.sendj({'error': 'GOVERNED_BRIDGE_UNAVAILABLE', 'detail': type(exc).__name__}, 503)
def do_POST(self):
path = urllib.parse.urlparse(self.path).path
if path in {
'/createTagApprovalRequest', '/requestOrCreateTag', '/getTagPublicationStatus',
'/buildProductionArtifact', '/executeApprovedProductionRelease',
'/getReleaseConsistency', '/runReleaseAcceptance',
}:
actor = self.need('tag:create')
if not actor:
return
return self.bridge_release(path, self.body())
if reject_non_read_http(self, 'POST', path):
return
return self.sendj({'error': 'not_found', 'path': path}, 404)
def do_GET(self): def do_GET(self):
u=urllib.parse.urlparse(self.path); path=u.path.rstrip('/') if u.path != '/' else u.path u=urllib.parse.urlparse(self.path); path=u.path.rstrip('/') if u.path != '/' else u.path
if path not in READ_ONLY_GET_PATHS: return self.sendj({'error':'not_found','path':path},404) if path not in READ_ONLY_GET_PATHS: return self.sendj({'error':'not_found','path':path},404)
@@ -454,7 +514,15 @@ class H(BaseHTTPRequestHandler):
return self.sendj({'error':redact(exc)},413) return self.sendj({'error':redact(exc)},413)
except (AuthoritativeGitError, RuntimeError) as exc: except (AuthoritativeGitError, RuntimeError) as exc:
return self.sendj({'error':redact(exc)},503) return self.sendj({'error':redact(exc)},503)
return self.sendj({'from':frm,'to':to,'diff':diff}) payload = {'from': frm, 'to': to, 'diff': diff}
payload.update({
key: diff[key]
for key in (
'from_oid', 'to_oid', 'changed_paths', 'stats',
'total_size_bytes', 'truncated', 'patch_limit_bytes',
)
})
return self.sendj(payload)
if path in ('/api/patch','/api/patches','/api/branches','/patch'): if path in ('/api/patch','/api/patches','/api/branches','/patch'):
snapshot = repository_snapshot( snapshot = repository_snapshot(
git_dir=AUTHORITATIVE_GIT_DIR, git_dir=AUTHORITATIVE_GIT_DIR,
@@ -566,7 +634,8 @@ class H(BaseHTTPRequestHandler):
return self.sendj( return self.sendj(
service.get_capability_status(candidate_id) service.get_capability_status(candidate_id)
) )
return self.sendj(service.get_release_saga(candidate_id)) result = service.get_release_saga(candidate_id)
return self.sendj(result, 404 if result.get('status') == 'NOT_FOUND' else 200)
if path == '/getRecoveryStatus': if path == '/getRecoveryStatus':
return self.sendj( return self.sendj(
silent_release_service().get_recovery_status() silent_release_service().get_recovery_status()
@@ -666,7 +735,44 @@ class H(BaseHTTPRequestHandler):
def file(self,data): def file(self,data):
a=self.need('file:write'); a=self.need('file:write');
if not a: return if not a: return
b=checkout(data.get('branch'), data.get('base','main'), bool(data.get('create_branch'))); r=write_file(data.get('path'), data.get('content','')); c=commit([r], data.get('message') or f'chore(chatgpt): update {r}'); audit(a['account'],'file',r,{'branch':b,'commit':c}); self.sendj({'status':'committed','branch':b,'path':r,'commit':c}) if os.getenv('AIOS_GITOPS_DURABLE_JOB_EXECUTION_ENABLED', os.getenv('AIOS_GITOPS_JOB_CONTROLLER_ENABLED', 'false')).lower() not in {'1','true','yes','on'}:
return self.sendj({'error':'DURABLE_JOB_CONTROLLER_UNAVAILABLE','retryable':True},503)
try:
path = relpath(str(data.get('path') or ''))
branch = str(data.get('branch') or '')
expected = str(data.get('expected_branch_oid') or '')
campaign_id = str(data.get('campaign_id') or os.getenv('AIOS_GITOPS_CAMPAIGN_ID') or '')
supplied_digest = str(data.get('payload_digest') or '')
content = data.get('content')
payload_bytes = None
if content is not None:
payload_bytes = content.encode('utf-8') if isinstance(content, str) else json.dumps(content, sort_keys=True, ensure_ascii=False).encode('utf-8')
payload_digest = supplied_digest or hashlib.sha256(payload_bytes or b'').hexdigest()
if not campaign_id or not branch or not expected:
return self.sendj({'error':'JOB_BINDING_REQUIRED','retryable':False},409)
request = {
'campaign_id': campaign_id,
'repository': REPOSITORY_IDENTITY or 'aios/aios',
'operation': 'commit-file',
'branch': branch,
'expected_branch_oid': expected,
'payload_digest': payload_digest,
# The controller persists only the binding and digest. Raw
# content is never copied into durable state or logs.
}
receipt = GitOpsJobController(
JobStore(Path(os.getenv('AIOS_GITOPS_JOB_ROOT', '/var/lib/aios-gitops/jobs')))
).submit(request, payload=payload_bytes)
return self.sendj({
'job_id': receipt.job_id,
'state': receipt.state.value,
'side_effect_state': receipt.side_effect_state,
'retryable': receipt.http_status == 503,
}, receipt.http_status)
except ServiceUnavailable as exc:
return self.sendj({'error':str(exc),'layer':'durable_job','retryable':True,'side_effect_state':'UNKNOWN'},503)
except JobError as exc:
return self.sendj({'error':str(exc),'retryable':False},409)
def commit(self,data): def commit(self,data):
a=self.need('commit:create'); a=self.need('commit:create');
if not a: return if not a: return
@@ -680,7 +786,10 @@ class H(BaseHTTPRequestHandler):
a=self.need('tag:create'); a=self.need('tag:create');
if not a: return if not a: return
ver=data.get('version') or data.get('tag') ver=data.get('version') or data.get('tag')
if not re.match(r'^V\d+R\d+C\d+(-[A-Za-z0-9_.-]+)?$', ver or ''): return self.sendj({'error':'version must match VxRxCx'},400) try:
validate_version(ver)
except ValueError:
return self.sendj({'error':'version must match VxRxCx'},400)
if not data.get('target_oid') or not data.get('evidence_closure_oid') or not data.get('approval_plan_id'): if not data.get('target_oid') or not data.get('evidence_closure_oid') or not data.get('approval_plan_id'):
return self.sendj({ return self.sendj({
'error':'production_tag_evidence_required', 'error':'production_tag_evidence_required',
@@ -854,6 +963,8 @@ def api_spec():
return {'version':'V1R1C0-chatgpt-actions','openapi':'/api/openapi.yaml','read':['GET /api/base','GET /api/ver','GET /api/diff','GET /api/patch','GET /api/model','GET /api/run','GET /getReleaseCandidate','GET /getApprovalEnvelope','GET /getReleaseSaga','GET /admin/ai-evolution'],'write':['POST /api/branch','POST /api/file','POST /api/commit','POST /api/patch','POST /api/tag','POST /api/rollback','POST /api/model','PUT /api/model-priority','POST /api/nl','POST /createReleaseCandidate','POST /submitOwnerApproval','POST /startDeployment'],'policy':{'bearer_required_for_write':True,'direct_main_push':False,'delete_tag':False,'owner_approval_required_for_v_level':True,'api_keys_in_git':False,'git_secret_field':'secret_ref','online_git_sync':True,'silent_release_execution_enabled':SILENT_RELEASE_EXECUTION_ENABLED,'silent_release_shadow_default':True}} return {'version':'V1R1C0-chatgpt-actions','openapi':'/api/openapi.yaml','read':['GET /api/base','GET /api/ver','GET /api/diff','GET /api/patch','GET /api/model','GET /api/run','GET /getReleaseCandidate','GET /getApprovalEnvelope','GET /getReleaseSaga','GET /admin/ai-evolution'],'write':['POST /api/branch','POST /api/file','POST /api/commit','POST /api/patch','POST /api/tag','POST /api/rollback','POST /api/model','PUT /api/model-priority','POST /api/nl','POST /createReleaseCandidate','POST /submitOwnerApproval','POST /startDeployment'],'policy':{'bearer_required_for_write':True,'direct_main_push':False,'delete_tag':False,'owner_approval_required_for_v_level':True,'api_keys_in_git':False,'git_secret_field':'secret_ref','online_git_sync':True,'silent_release_execution_enabled':SILENT_RELEASE_EXECUTION_ENABLED,'silent_release_shadow_default':True}}
def main(): def main():
validate_gitea_api_url(os.getenv('AIOS_GITOPS_GITEA_API_URL', ''))
print(f'AIOS ChatGPT GitOps adapter listening on {HOST}:{PORT}') print(f'AIOS ChatGPT GitOps adapter listening on {HOST}:{PORT}')
ThreadingHTTPServer((HOST,PORT), H).serve_forever() ThreadingHTTPServer((HOST,PORT), H).serve_forever()
if __name__ == '__main__': main() if __name__ == '__main__': main()
@@ -0,0 +1,244 @@
"""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",
"readRepositoryFile",
"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": (),
"readRepositoryFile": ("repository", "ref", "path", "max_bytes"),
}
_OPERATION_PATHS = {
"createGovernedTask": "/v2/governed-task",
"acquireWriterLease": "/v2/writer-lease",
"commitFileGoverned": "/v2/commit-file",
"commitFilesGoverned": "/v2/commit-files",
"getGitOpsJob": "/v2/job",
"resumeGitOpsJob": "/v2/job/resume",
"reconcileGitOpsJob": "/v2/job/reconcile",
"getTagPublicationStatus": "/v2/tag/status",
"getReleaseConsistency": "/v2/release/consistency",
"runReleaseAcceptance": "/v2/release/acceptance",
"readRepositoryFile": "/v2/source/file",
}
def operation_contracts() -> dict[str, dict[str, object]]:
"""Return the runtime contract consumed by adapters and schema tooling."""
return {
operation: {
"method": "POST",
"path": _OPERATION_PATHS.get(operation),
"required_fields": list(fields),
}
for operation, fields in OPERATION_REQUIRED_FIELDS.items()
}
def render_openapi_contract_fragment() -> str:
required = "\n".join(
f" - {field}"
for field in OPERATION_REQUIRED_FIELDS["commitFilesGoverned"]
)
return (
" /v2/commit-files:\n"
" post:\n"
" operationId: commitFilesGoverned\n"
" summary: Submit one atomic governed commit containing multiple files\n"
" security: [{bearerAuth: []}]\n"
" requestBody:\n"
" required: true\n"
" content:\n"
" application/json:\n"
" schema:\n"
" type: object\n"
" additionalProperties: false\n"
" required:\n"
f"{required}\n"
" properties:\n"
" task_id: {type: string}\n"
" repository: {type: string, example: aios/aios}\n"
" expected_branch_oid: {type: string, pattern: '^[0-9a-f]{40}$'}\n"
" target_branch: {type: string, pattern: '^agent/[A-Za-z0-9._/-]+$'}\n"
" generation: {type: integer, minimum: 1}\n"
" idempotency_key: {type: string, minLength: 8, maxLength: 128, pattern: '^[A-Za-z0-9][A-Za-z0-9._:-]{7,127}$'}\n"
" files:\n"
" type: array\n"
" minItems: 1\n"
" maxItems: 50\n"
" items:\n"
" type: object\n"
" additionalProperties: false\n"
" required: [path, content_base64]\n"
" properties:\n"
" path: {type: string, minLength: 1, maxLength: 512}\n"
" content_base64: {type: string}\n"
" message: {type: string, minLength: 1, maxLength: 200}\n"
" responses:\n"
" '200': {description: Governed commit job and verified branch result}\n"
" '400': {description: Contract or policy rejection}\n"
" '409': {description: Branch CAS or idempotency conflict}\n"
" x-openai-isConsequential: true\n"
)
class RuntimeDriftError(RuntimeError):
pass
class BridgeV2Error(RuntimeError):
pass
@dataclass(frozen=True, slots=True)
class ControlPlaneReleaseManifest:
control_plane_release_version: str
control_plane_target_oid: str
artifact_sha256: str
runtime_path: Path
bridge_schema_version: str
repository_identity: str
authoritative_git_dir: Path
deployment_state: str
def as_dict(self) -> dict[str, object]:
return {
"control_plane_release_version": self.control_plane_release_version,
"control_plane_target_oid": self.control_plane_target_oid,
"artifact_sha256": self.artifact_sha256,
"runtime_path": str(self.runtime_path),
"bridge_schema_version": self.bridge_schema_version,
"repository_identity": self.repository_identity,
"authoritative_git_dir": str(self.authoritative_git_dir),
"deployment_state": self.deployment_state,
}
def validate(self, artifact: Path, *, observed_oid: str, approved_root: Path = Path("/opt/aios/gitops-control-plane")) -> dict[str, object]:
try:
runtime = self.runtime_path.resolve(strict=True)
root = approved_root.resolve(strict=True)
artifact_path = artifact.resolve(strict=True)
runtime.relative_to(root)
artifact_path.relative_to(runtime)
except (OSError, ValueError):
raise RuntimeDriftError("CONTROL_PLANE_RUNTIME_DRIFT") from None
mutable_markers = {"workspace", "workspaces", "worktree", "worktrees"}
if mutable_markers.intersection(part.lower() for part in runtime.parts):
raise RuntimeDriftError("CONTROL_PLANE_RUNTIME_DRIFT")
valid = (
len(self.control_plane_target_oid) == 40
and observed_oid == self.control_plane_target_oid
and len(self.artifact_sha256) == 64
and hashlib.sha256(artifact_path.read_bytes()).hexdigest() == self.artifact_sha256
and self.bridge_schema_version == BRIDGE_SCHEMA_VERSION
and self.repository_identity == "aios/aios"
and self.authoritative_git_dir == Path("/data/aios-gitops/repo.git")
and self.authoritative_git_dir.is_absolute()
and self.deployment_state == "STAGING"
)
if not valid:
raise RuntimeDriftError("CONTROL_PLANE_RUNTIME_DRIFT")
return {**self.as_dict(), "drift_status": "PASS"}
class BridgeV2Adapter:
"""Thin operation adapter; all mutating handlers are injected seams."""
def __init__(self, *, runtime_status: Mapping[str, Any], handlers: Mapping[str, Callable[[Mapping[str, Any]], Any]] | None = None) -> None:
self.runtime_status = dict(runtime_status)
self.handlers = dict(handlers or {})
def getRuntimeStatus(self) -> dict[str, Any]:
return {"bridge_schema_version": BRIDGE_SCHEMA_VERSION, **self.runtime_status}
def getGovernedWriteCapabilities(self) -> dict[str, Any]:
return {
"bridge_schema_version": BRIDGE_SCHEMA_VERSION,
"legacy_writes_enabled": False,
"governed_writes_enabled": True,
"operations": list(BRIDGE_OPERATIONS),
"operation_contracts": operation_contracts(),
"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 readRepositoryFile(self, request: Mapping[str, Any]) -> Any: return self._call("readRepositoryFile", 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,623 @@
"""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 hashlib
import hmac
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 gitea_tag_publication import validate_gitea_api_url
from aios_continuation_executor import AuthorityGitAdapter, ContinuationCommit, ContinuationExecutor, ContinuationPermissions, ContinuationRuntimeBinding, DetachedGitWorkspace, GiteaContinuationRemote, ServerSideCredentialProjection
from aios_continuation_lease import WriterLeaseRequestV2
from scoped_lease_store import ScopedLeaseError, ScopedLeaseStore
from aios_control_plane_snapshot import build_control_plane_snapshot
from aios_gitops_job_controller import GitOpsJobController, JobState, JobStore, _RECOVERABLE
from aios_governed_task_bootstrap import BootstrapError, GovernedTaskBootstrap
from governed_production_release import build_executor_config
MAX_BODY = 2 * 1024 * 1024
MAX_SOURCE_BYTES = 262_144
ROUTES = {
"/v2/runtime": "getRuntimeStatus",
"/v2/capabilities": "getGovernedWriteCapabilities",
"/v2/governed-task": "createGovernedTask",
"/v2/writer-lease": "acquireWriterLease",
"/v2/commit-file": "commitFileGoverned",
"/v2/commit-files": "commitFilesGoverned",
"/v2/source/file": "readRepositoryFile",
"/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 = ""
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.leases = ScopedLeaseStore(config.state_root / "scoped-leases.sqlite3")
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)))
original_branch_mode = lease.branch_mode
if lease.branch_mode == "CREATE_NEW":
try:
lease.validate(AuthorityGitAdapter(self.authority))
except Exception as exc:
# A request replay after the authority CAS may observe the
# already-created branch. Only the exact base identity is
# idempotent; all other validation failures remain fail-closed.
if str(exc) != "TARGET_BRANCH_ALREADY_EXISTS":
raise
# Bootstrap owns the durable binding; the runtime performs the one-time
# branch materialization through the authority CAS before continuation
# validation. No direct Git or legacy writer path is used.
self.authority.create_task_branch(lease.target_branch, lease.base_oid)
lease = WriterLeaseRequestV2(
task_id=lease.task_id,
repository=lease.repository,
branch_mode="CONTINUE_EXISTING",
base_oid=lease.base_oid,
expected_branch_oid=lease.expected_branch_oid,
target_branch=lease.target_branch,
generation=lease.generation,
lease_seconds=lease.lease_seconds,
)
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(), remote_branch_create_allowed=original_branch_mode == "CREATE_NEW")
result = (binding, controller, executor); self.runtimes[key] = result; self.controllers[lease.task_id] = controller; return result
def _validate_task_binding(self, request: Mapping[str, Any]) -> None:
stored = self.bindings.get(str(request.get("task_id")))
if not stored:
raise RuntimeError("TASK_BINDING_NOT_FOUND")
if (
str(request.get("repository")) != str(stored.get("repository"))
or str(request.get("target_branch")) != str(stored.get("target_branch"))
or str(request.get("base_oid")) != str(stored.get("base_oid"))
):
raise RuntimeError("TASK_LEASE_BINDING_CONFLICT")
if str(request.get("branch_mode")) != "CONTINUE_EXISTING":
raise RuntimeError("TASK_BRANCH_MODE_INVALID")
try:
generation = int(request.get("generation"))
stored_generation = int(stored.get("generation") or 1)
except (TypeError, ValueError) as exc:
raise RuntimeError("TASK_LEASE_BINDING_CONFLICT") from exc
if generation < stored_generation:
raise RuntimeError("TASK_GENERATION_STALE")
current = self.authority.read_ref("refs/heads/" + str(request["target_branch"]))
if current != str(request.get("expected_branch_oid")):
raise RuntimeError("LEASE_EXPECTED_BRANCH_OID_STALE")
if not self.authority.ancestry_check(str(stored.get("expected_branch_oid")), str(request["expected_branch_oid"])):
raise RuntimeError("TASK_LEASE_LINEAGE_INVALID")
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]:
if os.environ.get("AIOS_GITOPS_WRITES_ENABLED", "false").lower() not in {"1", "true", "yes", "on"}:
raise RuntimeError("GITOPS_WRITES_DISABLED")
self._validate_task_binding(request)
try:
record, created = self.leases.acquire(request)
except ScopedLeaseError:
raise
if not created:
if record["state"] == "EXPIRED":
raise RuntimeError("LEASE_EXPIRED")
record = {**record, "created": False}
return self._lease_response(record["binding"], record)
try:
binding, _, _ = self._runtime(request)
except Exception:
# The lease cannot be usable if its detached runtime could not be
# established. Leave no durable orphan behind.
self.leases.delete(record["lease_key"])
raise
req = binding.request
return self._lease_response(
{
"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,
},
{**record, "created": True},
)
@staticmethod
def _lease_response(request: Mapping[str, Any], record: Mapping[str, Any]) -> dict[str, Any]:
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": int(request["generation"]),
"lease_seconds": int(request["lease_seconds"]),
"lease_key": record["lease_key"],
"lease_state": record["state"],
"lease_expires_at": record["expires_at"],
"idempotent": not bool(record.get("created", True)),
"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,
}
def read_source_file(self, request: Mapping[str, Any]) -> dict[str, Any]:
repository = str(request.get("repository") or "")
if repository != self.config.repository:
raise RuntimeError("REPOSITORY_NOT_ALLOWED")
max_bytes = request.get("max_bytes")
if not isinstance(max_bytes, int) or isinstance(max_bytes, bool) or not 1 <= max_bytes <= MAX_SOURCE_BYTES:
raise RuntimeError("SOURCE_MAX_BYTES_INVALID")
return self.authority.read_file(str(request.get("ref") or ""), str(request.get("path") or ""), max_bytes)
@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]:
if os.environ.get("AIOS_GITOPS_WRITES_ENABLED", "false").lower() not in {"1", "true", "yes", "on"}:
raise RuntimeError("GITOPS_WRITES_DISABLED")
self._validate_task_binding(request)
record = self.leases.for_commit(request)
if record is None:
raise RuntimeError("LEASE_NOT_FOUND")
if record["state"] == "EXPIRED":
raise RuntimeError("LEASE_EXPIRED")
if record["state"] == "USED":
if record.get("idempotency_key") == str(request["idempotency_key"]):
return dict(record["result"])
raise RuntimeError("LEASE_ALREADY_USED")
runtime = self.runtimes.get((str(request["task_id"]), int(request["generation"])))
if runtime is None:
runtime = self._runtime(record["binding"])
binding, _, executor = runtime
if request["repository"] != binding.request.repository or 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)
result = executor.execute(binding, commit)
self.leases.mark_used(record["lease_key"], commit.idempotency_key, result)
return result
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()
if JobState(row["state"]) not in _RECOVERABLE:
return controller.store._receipt(row, duplicate=True).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,
"readRepositoryFile": self.read_source_file,
"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
action_token_path: Path | 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 _authorized(self, operation: str) -> bool:
if operation not in {"readRepositoryFile", "acquireWriterLease", "commitFilesGoverned", "commitFileGoverned"}:
return True
header = self.headers.get("Authorization", "")
token = header[7:].strip() if header.startswith("Bearer ") else self.headers.get("X-AIOS-Token", "")
if not token or self.action_token_path is None or not self.action_token_path.is_file():
self._json({"error": "UNAUTHORIZED", "hint": "Bearer token required"}, 401)
return False
digest = hashlib.sha256(token.encode()).hexdigest()
try:
records = self.action_token_path.read_text(encoding="utf-8").splitlines()
except OSError:
self._json({"error": "AUTHORITY_UNAVAILABLE"}, 503)
return False
for raw in records:
line = raw.strip()
if not line or line.startswith("#"):
continue
try:
record = json.loads(line) if line.startswith("{") else {"token_plain": line, "scopes": ["*"]}
except ValueError:
continue
if record.get("disabled"):
continue
valid = bool(record.get("token_sha256") and hmac.compare_digest(str(record["token_sha256"]), digest))
valid = valid or bool(record.get("token_plain") and hmac.compare_digest(str(record["token_plain"]), token))
scopes = set(record.get("scopes") or [])
required = {
"readRepositoryFile": {"repository:read", "file:write"},
"acquireWriterLease": {"agent-task:control"},
"commitFilesGoverned": {"file:write"},
"commitFileGoverned": {"file:write"},
}[operation]
if valid and ("*" in scopes or required.intersection(scopes)):
return True
self._json({"error": "FORBIDDEN", "required_scope": sorted(required)}, 403)
return False
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:
if not self._authorized(operation):
return
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:
code = str(exc) or type(exc).__name__
if code.endswith("NOT_FOUND") or code in {"SOURCE_FILE_NOT_FOUND", "SOURCE_REF_NOT_FOUND", "LEASE_NOT_FOUND"}:
status = 404
elif code in {"SOURCE_FILE_TOO_LARGE", "SOURCE_MAX_BYTES_INVALID", "SOURCE_PATH_INVALID", "SOURCE_REF_INVALID", "SOURCE_REF_SCOPE_DENIED", "REPOSITORY_NOT_ALLOWED", "GITOPS_WRITES_DISABLED"}:
status = 400 if code != "GITOPS_WRITES_DISABLED" else 423
elif any(token in code for token in ("CONFLICT", "EXPECTATION", "MONOTONIC", "EXPIRED", "ALREADY_USED", "STALE", "BINDING")):
status = 409
else:
status = 503
self._json({"error": code, "retryable": status >= 500}, status)
def serve(adapter: BridgeV2Adapter, host: str = "127.0.0.1", port: int = 18892) -> None:
BridgeHandler.adapter = adapter
BridgeHandler.action_token_path = Path(os.environ.get("AIOS_GITOPS_TOKEN_FILE", "/var/lib/aios-gitops/runtime/secrets/gitops.tokens"))
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=validate_gitea_api_url(os.environ.get("AIOS_GITEA_API_BASE", "")),
target_version=os.environ.get("AIOS_CONTROL_PLANE_TARGET_VERSION", "V2R2C1"),
baseline_path=Path(os.environ.get(
"AIOS_GITOPS_BASELINE_AUTHORITY",
"/var/lib/aios-gitops-runtime-anchor/baseline/current.json",
)),
)
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,600 @@
"""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, *, allow_absent: bool = False) -> 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, *, allow_absent: bool = False) -> None:
self._require_projected()
self.repository_identity()
current = self.read_git_ref(branch)
if current != expected and not (allow_absent and current is None):
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
remote_branch_create_allowed: bool = False
@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,
"remote_branch_create_allowed": binding.remote_branch_create_allowed,
}
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, allow_absent=binding.remote_branch_create_allowed)
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, allow_absent=bool(request.get("remote_branch_create_allowed")) or request.get("branch_mode") == "CREATE_NEW")
return self._finish_remote_verification(job_id, new_oid, new_oid)
allow_remote_create = bool(request.get("remote_branch_create_allowed")) or request.get("branch_mode") == "CREATE_NEW" or (git_oid is None and api_oid is None)
remote_matches_expected = git_oid == api_oid == expected_oid
remote_absent_for_create = git_oid is None and api_oid is None
if authority_oid == new_oid and (remote_matches_expected or remote_absent_for_create):
self.controller.advance(job_id, JobState.RECONCILING, side_effect_state="OBSERVATION_FIRST_RETRY")
self.remote.push_exact(row["branch"], expected_oid, new_oid, allow_absent=allow_remote_create)
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()
@@ -0,0 +1,154 @@
"""Campaign-aware continuation leases for an existing Git branch.
This module is deliberately transport-neutral. It validates branch lineage
and hands ref mutation to an injected compare-and-swap broker; it does not run
Git, inspect credentials, or create a branch itself.
"""
from __future__ import annotations
from dataclasses import dataclass, replace
from datetime import datetime, timedelta, timezone
import re
from typing import Protocol
OID_RE = re.compile(r"^[0-9a-f]{40}$")
TARGET_BRANCH = "agent/autonomous-release-campaign-v1-20260807"
LINEAGE_ANCHOR = "69b45e81dcd2c3707c367003b08c55fd361faae8"
CAMPAIGN_HEAD = "bba675fab3b7e0c5fba7066720252b3c91b9f20d"
class ContinuationError(RuntimeError):
"""Stable fail-closed error for lease or branch binding violations."""
class BranchProbe(Protocol):
def branch_exists(self, branch: str) -> bool: ...
def branch_head(self, branch: str) -> str: ...
def is_ancestor(self, older: str, newer: str) -> bool: ...
class RefCAS(Protocol):
def compare_and_swap_branch(self, branch: str, expected: str, new: str) -> str: ...
def _oid(value: str, field: str) -> str:
if not isinstance(value, str) or not OID_RE.fullmatch(value):
raise ContinuationError(f"{field}_invalid")
return value
@dataclass(frozen=True, slots=True)
class WriterLeaseRequestV2:
task_id: str
repository: str
branch_mode: str
base_oid: str
expected_branch_oid: str
target_branch: str
generation: int
lease_seconds: int = 900
schema = "aios.task-writer-lease.v2"
accepted_schemas = frozenset({"aios.task-writer-lease.v2", "aios.task-writer-lease-request.v2"})
def validate(self, probe: BranchProbe) -> None:
if not self.task_id or self.repository != "aios/aios":
raise ContinuationError("request_identity_invalid")
if self.branch_mode not in {"CREATE_NEW", "CONTINUE_EXISTING"}:
raise ContinuationError("branch_mode_invalid")
_oid(self.base_oid, "base_oid")
_oid(self.expected_branch_oid, "expected_branch_oid")
if self.target_branch in {"main", "master"} or self.target_branch.startswith(("refs/", "tags/")):
raise ContinuationError("protected_branch_rejected")
if not isinstance(self.generation, int) or isinstance(self.generation, bool) or self.generation < 1:
raise ContinuationError("generation_invalid")
if not isinstance(self.lease_seconds, int) or not 1 <= self.lease_seconds <= 86_400:
raise ContinuationError("lease_duration_invalid")
if self.branch_mode == "CONTINUE_EXISTING":
if not probe.branch_exists(self.target_branch):
raise ContinuationError("EXISTING_BRANCH_MISSING")
actual = _oid(probe.branch_head(self.target_branch), "actual_branch_oid")
if actual != self.expected_branch_oid:
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)
class CampaignWriterLease:
task_id: str
repository: str
branch: str
lineage_anchor_oid: str
current_head_oid: str
generation: int
lease_expires_at: datetime
append_only: bool = True
renewable: bool = True
multi_commit: bool = True
recoverable: bool = True
state: str = "ACTIVE"
branch_bound: bool = True
def __post_init__(self) -> None:
_oid(self.lineage_anchor_oid, "lineage_anchor_oid")
_oid(self.current_head_oid, "current_head_oid")
if not all((self.append_only, self.renewable, self.multi_commit, self.recoverable, self.branch_bound)):
raise ContinuationError("lease_capability_downgrade")
class ContinuationLeaseManager:
"""State machine for a multi-commit lease with CAS-only ref updates."""
def __init__(self, probe: BranchProbe, refs: RefCAS, *, now=datetime.now) -> None:
self.probe = probe
self.refs = refs
self.now = now
def open(self, request: WriterLeaseRequestV2) -> CampaignWriterLease:
request.validate(self.probe)
if request.branch_mode != "CONTINUE_EXISTING":
raise ContinuationError("CREATE_NEW_REQUIRES_SEPARATE_PROVISIONER")
return CampaignWriterLease(
task_id=request.task_id,
repository=request.repository,
branch=request.target_branch,
lineage_anchor_oid=request.base_oid,
current_head_oid=request.expected_branch_oid,
generation=request.generation,
lease_expires_at=self.now(timezone.utc) + timedelta(seconds=request.lease_seconds),
)
def renew(self, lease: CampaignWriterLease, seconds: int) -> CampaignWriterLease:
if lease.state != "ACTIVE" or not lease.renewable or seconds < 1:
raise ContinuationError("lease_not_renewable")
return replace(lease, lease_expires_at=self.now(timezone.utc) + timedelta(seconds=seconds))
def append_commit(self, lease: CampaignWriterLease, new_head_oid: str) -> CampaignWriterLease:
if lease.state != "ACTIVE":
raise ContinuationError("lease_not_active")
_oid(new_head_oid, "new_head_oid")
if not self.probe.is_ancestor(lease.current_head_oid, new_head_oid):
raise ContinuationError("NON_FAST_FORWARD_COMMIT")
try:
actual = self.refs.compare_and_swap_branch(
lease.branch, lease.current_head_oid, new_head_oid
)
except Exception as exc:
raise ContinuationError("BRANCH_LEASE_STALE") from exc
if actual != new_head_oid:
raise ContinuationError("BRANCH_LEASE_STALE")
return replace(lease, current_head_oid=new_head_oid)
def supersede(self, lease: CampaignWriterLease) -> CampaignWriterLease:
if lease.state not in {"ACTIVE", "RECOVERABLE_FAILURE", "RECONCILIATION_REQUIRED"}:
raise ContinuationError("lease_not_supersedable")
return replace(lease, generation=lease.generation + 1, state="SUPERSEDED")
@@ -0,0 +1,25 @@
"""Separate read, write, and release plane readiness evaluation."""
from __future__ import annotations
from dataclasses import dataclass
from typing import Mapping
@dataclass(frozen=True, slots=True)
class RuntimeStatus:
read_plane: Mapping[str, bool]
write_plane: Mapping[str, bool]
release_plane: Mapping[str, bool]
@property
def release_control_plane_ready(self) -> bool:
return all(self.read_plane.values()) and all(self.write_plane.values()) and all(self.release_plane.values())
def as_dict(self) -> dict[str, object]:
return {
"READ_PLANE": dict(self.read_plane),
"WRITE_PLANE": dict(self.write_plane),
"RELEASE_PLANE": dict(self.release_plane),
"release_control_plane_ready": self.release_control_plane_ready,
}
@@ -0,0 +1,58 @@
"""Workspace-independent authority/network/Gitea ref sentinel."""
from __future__ import annotations
from dataclasses import dataclass
from pathlib import Path
import subprocess
from typing import Callable, Mapping
class SentinelError(RuntimeError):
pass
@dataclass(frozen=True, slots=True)
class SentinelInputs:
authority_git_dir: Path
gitea_remote: str
gitea_api: str
ref: str
expected_oid: str
@dataclass(frozen=True, slots=True)
class SentinelResult:
authority_oid: str | None
network_oid: str | None
gitea_oid: str | None
equal: bool
error: str | None = None
def authority_oid(inputs: SentinelInputs) -> str:
result = subprocess.run(
["git", "--git-dir", str(inputs.authority_git_dir), "rev-parse", inputs.ref],
text=True,
capture_output=True,
check=False,
)
if result.returncode != 0:
raise SentinelError("AUTHORITY_REF_UNAVAILABLE")
return result.stdout.strip()
def evaluate(
inputs: SentinelInputs,
*,
network_reader: Callable[[str, str], str | None],
gitea_reader: Callable[[str, str], str | None],
) -> SentinelResult:
try:
local = authority_oid(inputs)
network = network_reader(inputs.gitea_remote, inputs.ref)
gitea = gitea_reader(inputs.gitea_api, inputs.ref)
equal = local == network == gitea == inputs.expected_oid
return SentinelResult(local, network, gitea, equal, None if equal else "OID_MISMATCH")
except SentinelError as exc:
return SentinelResult(None, None, None, False, str(exc))
@@ -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,125 @@
"""Non-Owner Gitea machine capability and branch reconciliation boundary."""
from __future__ import annotations
from dataclasses import dataclass
from typing import Any, Mapping, Protocol
class MachineBrokerError(RuntimeError):
pass
class GiteaTransport(Protocol):
def compare_and_swap_branch(self, branch: str, expected_oid: str, new_oid: str) -> str: ...
def create_or_update_pr(self, branch: str, target: str, title: str, body: str) -> Mapping[str, Any]: ...
def publish_status(self, branch: str, state: str, description: str) -> Mapping[str, Any]: ...
def protected_merge(self, pr_number: int) -> Mapping[str, Any]: ...
def read_branch(self, branch: str) -> str | None: ...
def whoami(self) -> Mapping[str, Any]: ...
def repository(self, owner: str, name: str) -> Mapping[str, Any]: ...
@dataclass(frozen=True, slots=True)
class MachineCredentialProjection:
"""Opaque systemd credential reference; the credential value is never accepted."""
reference: str
account: str
owner_account: str = "aiman"
@classmethod
def from_mapping(cls, value: Mapping[str, Any]) -> "MachineCredentialProjection":
if set(value) != {"scheme", "name", "account"}:
raise MachineBrokerError("CREDENTIAL_REFERENCE_INVALID")
if value["scheme"] != "systemd-credential" or value["name"] != "gitea-machine-token":
raise MachineBrokerError("CREDENTIAL_REFERENCE_INVALID")
return cls(f"{value['scheme']}:{value['name']}", str(value["account"]))
def __post_init__(self) -> None:
if self.reference != "systemd-credential:gitea-machine-token":
raise MachineBrokerError("CREDENTIAL_REFERENCE_INVALID")
if not self.account or self.account == self.owner_account:
raise MachineBrokerError("OWNER_MACHINE_IDENTITY_REJECTED")
@dataclass(frozen=True, slots=True)
class CampaignCapabilityReceipt:
campaign_id: str
task_id: str
repository: str
branch: str
expected_oid: str
capabilities: frozenset[str]
denied: frozenset[str]
def require(self, capability: str) -> None:
if capability in self.denied or capability not in self.capabilities:
raise MachineBrokerError("CAPABILITY_DENIED")
if self.branch in {"main", "master"}:
raise MachineBrokerError("PROTECTED_BRANCH_DENIED")
class GiteaMachineBroker:
def __init__(self, transport: GiteaTransport, credential: MachineCredentialProjection, receipt: CampaignCapabilityReceipt) -> None:
self.transport = transport
self.credential = credential
self.receipt = receipt
def verify_identity(self) -> Mapping[str, Any]:
identity = dict(self.transport.whoami())
login = identity.get("login") or identity.get("username")
if not isinstance(login, str) or not login or login == self.credential.owner_account or login != self.credential.account:
raise MachineBrokerError("GITEA_MACHINE_IDENTITY_INVALID")
owner, name = self.receipt.repository.split("/", 1)
repo = dict(self.transport.repository(owner, name))
if repo.get("full_name") != self.receipt.repository:
raise MachineBrokerError("GITEA_REPOSITORY_MISMATCH")
return {"login": login, "repository": self.receipt.repository, "owner_account": self.credential.owner_account}
def push_branch(self, new_oid: str) -> str:
self.receipt.require("exact_task_branch_push")
return self.transport.compare_and_swap_branch(self.receipt.branch, self.receipt.expected_oid, new_oid)
def create_pr(self, title: str, body: str) -> Mapping[str, Any]:
self.receipt.require("pr_create_update")
return self.transport.create_or_update_pr(self.receipt.branch, "main", title, body)
def status(self, state: str, description: str, context: str = "aios/durable-release") -> Mapping[str, Any]:
self.receipt.require("status_publish")
return self.transport.publish_status(self.receipt.branch, state, description, context)
def merge(self, pr_number: int) -> Mapping[str, Any]:
self.receipt.require("protected_merge")
return self.transport.protected_merge(pr_number)
def readback(self) -> str | None:
self.receipt.require("remote_readback")
return self.transport.read_branch(self.receipt.branch)
def reconcile_exact_branch(authority_oid: str, gitea_oid: str | None, expected_oid: str) -> str:
if authority_oid != expected_oid:
return "AUTHORITY_EXPECTATION_MISMATCH"
if gitea_oid is None:
return "CREATE_REQUIRED"
if gitea_oid == expected_oid:
return "ALREADY_IN_SYNC"
return "MANUAL_OWNER_REVIEW_REQUIRED"
def reconcile_fast_forward(
remote_oid: str | None,
expected_old_oid: str,
new_oid: str,
*,
old_is_ancestor_of_new: bool,
) -> str:
"""Return a bounded action for an active Job/lease branch scope."""
if remote_oid is None:
return "CREATE_REQUIRED"
if remote_oid == new_oid:
return "ALREADY_IN_SYNC"
if remote_oid == expected_old_oid and old_is_ancestor_of_new:
return "FAST_FORWARD_REQUIRED"
return "MANUAL_OWNER_REVIEW_REQUIRED"
@@ -0,0 +1,208 @@
"""Gitea HTTP/Git transport using only a projected systemd credential."""
from __future__ import annotations
from dataclasses import dataclass
import json
import os
from pathlib import Path
import subprocess
import tempfile
from typing import Any, Mapping
from urllib.error import HTTPError, URLError
from urllib.parse import quote, urlencode
from urllib.request import Request, urlopen
class GiteaTransportError(RuntimeError):
pass
def _safe_oid(value: str, field: str) -> str:
if not isinstance(value, str) or len(value) != 40 or any(ch not in "0123456789abcdef" for ch in value):
raise GiteaTransportError(f"{field}_INVALID")
return value
@dataclass(slots=True)
class GiteaMachineTransport:
api_base: str
repository_id: str
remote_url: str
authority_git_dir: Path
credential_name: str = "gitea-machine-token"
actor: str = "aios-autonomy-agent"
timeout: int = 20
expected_merge_head: str | None = None
expected_merge_base: str | None = None
def __post_init__(self) -> None:
if self.credential_name != "gitea-machine-token":
raise GiteaTransportError("CREDENTIAL_NAME_INVALID")
if "/" not in self.repository_id or self.repository_id.startswith("/"):
raise GiteaTransportError("REPOSITORY_INVALID")
if not self.api_base.startswith("https://") and not self.api_base.startswith("http://127.0.0.1"):
raise GiteaTransportError("GITEA_API_SCOPE_INVALID")
if not self.remote_url.startswith("https://"):
raise GiteaTransportError("GITEA_REMOTE_SCOPE_INVALID")
@property
def _credential_path(self) -> Path:
directory = os.environ.get("CREDENTIALS_DIRECTORY", "")
if not directory:
raise GiteaTransportError("CREDENTIAL_PROJECTION_MISSING")
path = Path(directory) / self.credential_name
if path.is_symlink() or not path.is_file():
raise GiteaTransportError("CREDENTIAL_PROJECTION_MISSING")
return path
def _token(self) -> str:
value = self._credential_path.read_text(encoding="utf-8").strip()
if not value:
raise GiteaTransportError("CREDENTIAL_EMPTY")
return value
def _request(self, method: str, path: str, body: Mapping[str, Any] | None = None) -> tuple[int, Any]:
data = None if body is None else json.dumps(dict(body), separators=(",", ":")).encode()
headers = {"Accept": "application/json", "Authorization": f"token {self._token()}"}
if data is not None:
headers["Content-Type"] = "application/json"
request = Request(self.api_base.rstrip("/") + path, data=data, headers=headers, method=method)
try:
with urlopen(request, timeout=self.timeout) as response:
raw = response.read()
return response.status, json.loads(raw.decode()) if raw else {}
except HTTPError as exc:
# Never include response bodies: Gitea may echo sensitive request context.
raise GiteaTransportError(f"GITEA_HTTP_{exc.code}") from exc
except (URLError, TimeoutError, OSError) as exc:
raise GiteaTransportError("GITEA_NETWORK_ERROR") from exc
def whoami(self) -> Mapping[str, Any]:
return self._request("GET", "/user")[1]
def repository_info(self) -> Mapping[str, Any]:
owner, name = self.repository_id.split("/", 1)
return self._request("GET", f"/repos/{quote(owner)}/{quote(name)}")[1]
def repository(self, owner: str, name: str) -> Mapping[str, Any]:
if f"{owner}/{name}" != self.repository_id:
raise GiteaTransportError("REPOSITORY_SCOPE_DENIED")
return self.repository_info()
def read_branch(self, branch: str) -> str | None:
owner, name = self.repository_id.split("/", 1)
try:
value = self._request("GET", f"/repos/{quote(owner)}/{quote(name)}/branches/{quote(branch, safe='/')}")[1]
except GiteaTransportError as exc:
if str(exc) == "GITEA_HTTP_404":
return None
raise
sha = value.get("commit", {}).get("id") if isinstance(value, dict) else None
return _safe_oid(sha, "remote_branch_oid")
def _askpass(self) -> tuple[Path, dict[str, str]]:
credential = self._credential_path
fd, name = tempfile.mkstemp(prefix="aios-gitea-askpass-", dir="/tmp")
path = Path(name)
os.fchmod(fd, 0o700)
script = "#!/bin/sh\ncase \"$1\" in *Username*) printf '%s\\n' \"$GITEA_MACHINE_ACTOR\" ;; *) cat \"$CREDENTIAL_FILE\" ;; esac\n"
with os.fdopen(fd, "w", encoding="utf-8") as stream:
stream.write(script)
stream.flush()
os.fsync(stream.fileno())
return path, {
"GIT_ASKPASS": str(path),
"GIT_TERMINAL_PROMPT": "0",
"GITEA_MACHINE_ACTOR": self.actor,
"CREDENTIAL_FILE": str(credential),
}
def _push_branch_fast_forward(self, branch: str, new_oid: str) -> None:
askpass, additions = self._askpass()
environment = os.environ.copy()
environment.update(additions)
try:
result = subprocess.run(
["git", f"--git-dir={self.authority_git_dir}", "push", self.remote_url, f"{new_oid}:refs/heads/{branch}"],
text=True,
capture_output=True,
check=False,
env=environment,
timeout=self.timeout,
)
if result.returncode != 0:
raise GiteaTransportError("GITEA_BRANCH_CREATE_FAILED")
finally:
askpass.unlink(missing_ok=True)
def compare_and_swap_branch(self, branch: str, expected_oid: str, new_oid: str) -> str:
_safe_oid(expected_oid, "expected_oid")
_safe_oid(new_oid, "new_oid")
current = self.read_branch(branch)
if current == new_oid:
return new_oid
if current is None:
self._push_branch_fast_forward(branch, new_oid)
readback = self.read_branch(branch)
if readback != new_oid:
raise GiteaTransportError("GITEA_BRANCH_CREATE_READBACK_MISMATCH")
return readback
if current != expected_oid:
raise GiteaTransportError("GITEA_BRANCH_EXPECTATION_MISMATCH")
# The preflight read binds the expected old tip. The Git receive-pack
# then enforces the actual fast-forward CAS while transferring the new
# objects; no force/lease override or arbitrary ref is permitted.
self._push_branch_fast_forward(branch, new_oid)
readback = self.read_branch(branch)
if readback != new_oid:
raise GiteaTransportError("GITEA_BRANCH_CAS_READBACK_MISMATCH")
return readback
def _pulls(self) -> list[Mapping[str, Any]]:
owner, name = self.repository_id.split("/", 1)
query = urlencode({"state": "all", "base_branch": "main", "limit": 100})
value = self._request("GET", f"/repos/{quote(owner)}/{quote(name)}/pulls?{query}")[1]
return value if isinstance(value, list) else []
def create_or_update_pr(self, branch: str, target: str, title: str, body: str) -> Mapping[str, Any]:
if target != "main":
raise GiteaTransportError("PR_BASE_SCOPE_DENIED")
head = self.read_branch(branch)
if head is None:
raise GiteaTransportError("PR_HEAD_MISSING")
matches = [p for p in self._pulls() if (p.get("head") or {}).get("ref") == branch and (p.get("base") or {}).get("ref") == target]
open_matches = [p for p in matches if p.get("state") == "open"]
if len(open_matches) > 1:
raise GiteaTransportError("MULTIPLE_MATCHING_PRS")
if open_matches:
if (open_matches[0].get("head") or {}).get("sha") != head:
raise GiteaTransportError("PR_HEAD_CONFLICT")
return open_matches[0]
owner, name = self.repository_id.split("/", 1)
return self._request("POST", f"/repos/{quote(owner)}/{quote(name)}/pulls", {"head": branch, "base": target, "title": title, "body": body, "allow_maintainer_edit": False})[1]
def publish_status(self, branch: str, state: str, description: str, context: str = "aios/durable-release") -> Mapping[str, Any]:
oid = self.read_branch(branch)
if oid is None:
raise GiteaTransportError("STATUS_BRANCH_MISSING")
owner, name = self.repository_id.split("/", 1)
return self._request("POST", f"/repos/{quote(owner)}/{quote(name)}/statuses/{oid}", {"state": state, "context": context, "description": description})[1]
def protected_merge(self, pr_number: int) -> Mapping[str, Any]:
if not isinstance(pr_number, int) or pr_number < 1:
raise GiteaTransportError("PR_NUMBER_INVALID")
owner, name = self.repository_id.split("/", 1)
pr = self._request("GET", f"/repos/{quote(owner)}/{quote(name)}/pulls/{pr_number}")[1]
if pr.get("state") != "open" or (pr.get("base") or {}).get("ref") != "main":
raise GiteaTransportError("PR_MERGE_BINDING_INVALID")
if pr.get("mergeable") is False:
raise GiteaTransportError("PR_NOT_MERGEABLE")
head_oid = (pr.get("head") or {}).get("sha")
base_oid = (pr.get("base") or {}).get("sha")
if self.expected_merge_head is not None and head_oid != self.expected_merge_head:
raise GiteaTransportError("PR_HEAD_EXPECTATION_MISMATCH")
if self.expected_merge_base is not None and base_oid != self.expected_merge_base:
raise GiteaTransportError("PR_BASE_EXPECTATION_MISMATCH")
body = {"do": "fast-forward-only", "force_merge": False, "merge_when_checks_succeed": False, "head_commit_id": head_oid}
return self._request("POST", f"/repos/{quote(owner)}/{quote(name)}/pulls/{pr_number}/merge", body)[1]
@@ -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)
@@ -0,0 +1,602 @@
"""SQLite-backed durable GitOps Job Controller.
The controller owns durable intent, idempotency, lease metadata, state
transitions, receipts, and payload references. A worker owns Git side effects
behind the campaign capability and branch-CAS boundaries.
"""
from __future__ import annotations
from dataclasses import dataclass
from enum import Enum
import hashlib
import json
import os
from pathlib import Path
import sqlite3
import tempfile
from datetime import datetime, timezone
from typing import Any, Callable, Mapping
class JobError(RuntimeError):
pass
class ServiceUnavailable(JobError):
pass
class BindingConflict(JobError):
pass
class JobState(str, Enum):
REQUESTED = "REQUESTED"
AUTHORIZED = "AUTHORIZED"
LEASED = "LEASED"
WORKTREE_READY = "WORKTREE_READY"
EXECUTING = "EXECUTING"
LOCAL_VERIFIED = "LOCAL_VERIFIED"
REMOTE_SYNCING = "REMOTE_SYNCING"
REMOTE_VERIFIED = "REMOTE_VERIFIED"
COMPLETED = "COMPLETED"
FAILED = "FAILED"
CANCELLED = "CANCELLED"
RECOVERABLE_FAILURE = "RECOVERABLE_FAILURE"
RECONCILING = "RECONCILING"
RECONCILIATION_REQUIRED = "RECONCILIATION_REQUIRED"
MANUAL_OWNER_REVIEW_REQUIRED = "MANUAL_OWNER_REVIEW_REQUIRED"
_ORDER = {
JobState.REQUESTED: 0,
JobState.AUTHORIZED: 1,
JobState.LEASED: 2,
JobState.WORKTREE_READY: 3,
JobState.EXECUTING: 4,
JobState.LOCAL_VERIFIED: 5,
JobState.REMOTE_SYNCING: 6,
JobState.REMOTE_VERIFIED: 7,
JobState.COMPLETED: 8,
}
_RECOVERABLE = {JobState.RECOVERABLE_FAILURE, JobState.RECONCILIATION_REQUIRED}
_TERMINAL = {
JobState.COMPLETED,
JobState.FAILED,
JobState.CANCELLED,
JobState.MANUAL_OWNER_REVIEW_REQUIRED,
}
def utc_now() -> str:
return datetime.now(timezone.utc).isoformat().replace("+00:00", "Z")
def canonical_json(value: Any) -> bytes:
return (
json.dumps(value, sort_keys=True, separators=(",", ":"), ensure_ascii=False) + "\n"
).encode("utf-8")
def sha256_json(value: Any) -> str:
return hashlib.sha256(canonical_json(value)).hexdigest()
class PayloadStore:
"""Content-addressed filesystem payloads; raw content never enters SQLite."""
def __init__(self, root: str | os.PathLike[str]) -> None:
self.root = Path(root)
if not self.root.is_absolute() or self.root.is_symlink():
raise JobError("PAYLOAD_ROOT_INVALID")
self.root.mkdir(parents=True, exist_ok=True, mode=0o700)
os.chmod(self.root, 0o700)
def put(self, content: bytes, expected_digest: str) -> tuple[str, int]:
digest = hashlib.sha256(content).hexdigest()
if digest != expected_digest:
raise JobError("PAYLOAD_DIGEST_MISMATCH")
if len(expected_digest) != 64 or any(ch not in "0123456789abcdef" for ch in expected_digest):
raise JobError("PAYLOAD_DIGEST_INVALID")
target = self.root / expected_digest
if target.is_symlink():
raise JobError("PAYLOAD_PATH_SYMLINK")
if target.is_file():
if target.stat().st_size != len(content) or hashlib.sha256(target.read_bytes()).hexdigest() != digest:
raise JobError("PAYLOAD_EXISTING_MISMATCH")
return f"sha256/{digest}", len(content)
fd, temporary = tempfile.mkstemp(prefix=".payload-", dir=self.root)
try:
os.fchmod(fd, 0o600)
with os.fdopen(fd, "wb") as stream:
stream.write(content)
stream.flush()
os.fsync(stream.fileno())
os.replace(temporary, target)
os.chmod(target, 0o600)
finally:
if os.path.exists(temporary):
os.unlink(temporary)
return f"sha256/{digest}", len(content)
def read_verified(self, reference: str, expected_digest: str, expected_size: int) -> bytes:
if reference != f"sha256/{expected_digest}":
raise JobError("PAYLOAD_REFERENCE_MISMATCH")
path = self.root / expected_digest
if path.is_symlink() or not path.is_file():
raise JobError("PAYLOAD_MISSING")
content = path.read_bytes()
if len(content) != expected_size or hashlib.sha256(content).hexdigest() != expected_digest:
raise JobError("PAYLOAD_VERIFY_FAILED")
return content
@dataclass(frozen=True, slots=True)
class JobReceipt:
job_id: str
idempotency_key: str
state: JobState
side_effect_state: str
http_status: int
revision: int
receipt: Mapping[str, Any] | None = None
def as_dict(self) -> dict[str, Any]:
return {
"job_id": self.job_id,
"idempotency_key": self.idempotency_key,
"state": self.state.value,
"side_effect_state": self.side_effect_state,
"http_status": self.http_status,
"revision": self.revision,
"receipt": dict(self.receipt) if self.receipt else None,
}
class JobStore:
SCHEMA_VERSION = 1
def __init__(self, root: str | os.PathLike[str], *, payload_root: str | os.PathLike[str] | None = None) -> None:
self.root = Path(root)
if not self.root.is_absolute() or self.root.is_symlink():
raise JobError("JOB_ROOT_INVALID")
self.root.mkdir(parents=True, exist_ok=True, mode=0o700)
os.chmod(self.root, 0o700)
self.db_path = self.root / "state.sqlite3"
self.payloads = PayloadStore(payload_root or self.root.parent / "payloads" / "sha256")
self._initialize()
def _connect(self) -> sqlite3.Connection:
if self.db_path.is_symlink():
raise ServiceUnavailable("JOB_DB_SYMLINK")
connection = sqlite3.connect(self.db_path, timeout=30.0)
connection.row_factory = sqlite3.Row
connection.execute("PRAGMA journal_mode=WAL")
connection.execute("PRAGMA synchronous=FULL")
connection.execute("PRAGMA foreign_keys=ON")
connection.execute("PRAGMA busy_timeout=30000")
return connection
def _initialize(self) -> None:
try:
with self._connect() as connection:
connection.executescript(
"""
CREATE TABLE IF NOT EXISTS metadata (key TEXT PRIMARY KEY, value TEXT NOT NULL);
CREATE TABLE IF NOT EXISTS jobs (
job_id TEXT PRIMARY KEY, idempotency_key TEXT NOT NULL UNIQUE,
campaign_id TEXT NOT NULL, repository TEXT NOT NULL, operation TEXT NOT NULL,
branch TEXT NOT NULL, expected_branch_oid TEXT NOT NULL,
payload_digest TEXT NOT NULL, payload_ref TEXT, payload_size INTEGER,
state TEXT NOT NULL, side_effect_state TEXT NOT NULL,
revision INTEGER NOT NULL, request_json TEXT NOT NULL,
receipt_json TEXT, created_at TEXT NOT NULL, updated_at TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS idempotency_keys (
key TEXT PRIMARY KEY, job_id TEXT NOT NULL UNIQUE,
binding_json TEXT NOT NULL, created_at TEXT NOT NULL,
FOREIGN KEY(job_id) REFERENCES jobs(job_id)
);
CREATE TABLE IF NOT EXISTS branch_leases (
lease_id TEXT PRIMARY KEY, job_id TEXT NOT NULL UNIQUE,
branch TEXT NOT NULL, lineage_anchor_oid TEXT NOT NULL,
current_head_oid TEXT NOT NULL, generation INTEGER NOT NULL,
lease_expires_at TEXT NOT NULL, state TEXT NOT NULL,
revision INTEGER NOT NULL, FOREIGN KEY(job_id) REFERENCES jobs(job_id)
);
CREATE TABLE IF NOT EXISTS state_transitions (
sequence INTEGER PRIMARY KEY AUTOINCREMENT, job_id TEXT NOT NULL,
from_state TEXT, to_state TEXT NOT NULL, revision INTEGER NOT NULL,
side_effect_state TEXT NOT NULL, created_at TEXT NOT NULL,
FOREIGN KEY(job_id) REFERENCES jobs(job_id)
);
CREATE TABLE IF NOT EXISTS receipts (
receipt_id TEXT PRIMARY KEY, job_id TEXT NOT NULL,
kind TEXT NOT NULL, digest TEXT NOT NULL, payload_json TEXT NOT NULL,
created_at TEXT NOT NULL, FOREIGN KEY(job_id) REFERENCES jobs(job_id)
);
INSERT OR REPLACE INTO metadata(key, value) VALUES('schema_version', '1');
"""
)
except sqlite3.Error as exc:
raise ServiceUnavailable("JOB_DB_UNAVAILABLE") from exc
def _mirror(self, job: Mapping[str, Any]) -> None:
directory = self.root / str(job["job_id"])
directory.mkdir(mode=0o700, exist_ok=True)
(directory / "receipts").mkdir(mode=0o700, exist_ok=True)
(directory / "logs-digest").mkdir(mode=0o700, exist_ok=True)
for name, value in (("request.json", job["request"]), ("state.json", job)):
target = directory / name
fd, temporary = tempfile.mkstemp(prefix=".mirror-", dir=directory)
try:
os.fchmod(fd, 0o600)
with os.fdopen(fd, "wb") as stream:
stream.write(canonical_json(value))
stream.flush()
os.fsync(stream.fileno())
os.replace(temporary, target)
finally:
if os.path.exists(temporary):
os.unlink(temporary)
def read(self, job_id: str) -> dict[str, Any]:
try:
with self._connect() as connection:
row = connection.execute("SELECT * FROM jobs WHERE job_id=?", (job_id,)).fetchone()
except sqlite3.Error as exc:
raise ServiceUnavailable("JOB_DB_UNAVAILABLE") from exc
if row is None:
raise JobError("JOB_NOT_FOUND")
return dict(row)
def _receipt(self, row: Mapping[str, Any], *, duplicate: bool) -> JobReceipt:
state = JobState(row["state"])
if state == JobState.COMPLETED:
status, side = 200, "COMPLETED"
elif state in _RECOVERABLE:
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:
status, side = 202, "SAME_JOB" if duplicate else "QUEUED"
receipt = json.loads(row["receipt_json"]) if row["receipt_json"] else None
return JobReceipt(row["job_id"], row["idempotency_key"], state, side, status, int(row["revision"]), receipt)
class GitOpsJobController:
def __init__(self, store: JobStore) -> None:
self.store = store
@staticmethod
def _binding(request: Mapping[str, Any]) -> dict[str, str]:
fields = ("campaign_id", "repository", "operation", "branch", "expected_branch_oid", "payload_digest")
if any(not isinstance(request.get(field), str) or not request[field].strip() for field in fields):
raise JobError("JOB_BINDING_INVALID")
oid = request["expected_branch_oid"]
digest = request["payload_digest"]
if len(oid) != 40 or any(ch not in "0123456789abcdef" for ch in oid):
raise JobError("EXPECTED_BRANCH_OID_INVALID")
if len(digest) != 64 or any(ch not in "0123456789abcdef" for ch in digest):
raise JobError("PAYLOAD_DIGEST_INVALID")
if request["branch"] in {"main", "master"} or request["branch"].startswith(("refs/", "tags/")):
raise JobError("PROTECTED_BRANCH_REJECTED")
binding = {field: str(request[field]) for field in fields}
# Operation-specific, non-secret binding inputs must survive the
# 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",
"target_branch",
"check_context",
"check_state",
"check_description",
)
for field in optional:
value = request.get(field)
if value is None:
continue
if isinstance(value, bool) or not isinstance(value, (str, int)):
raise JobError("JOB_BINDING_OPTION_INVALID")
text = str(value).strip()
if not text or len(text) > 512:
raise JobError("JOB_BINDING_OPTION_INVALID")
binding[field] = text
return binding
def submit(self, request: Mapping[str, Any], *, payload: bytes | None = None) -> JobReceipt:
binding = self._binding(request)
payload_ref = request.get("payload_ref")
payload_size = request.get("payload_size")
if payload is not None:
payload_ref, payload_size = self.store.payloads.put(payload, binding["payload_digest"])
if payload_ref is not None and payload_ref != f"sha256/{binding['payload_digest']}":
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")
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:
durable_request.update({"payload_ref": str(payload_ref), "payload_size": int(payload_size)})
now = utc_now()
try:
with self.store._connect() as connection:
connection.execute("BEGIN IMMEDIATE")
existing = connection.execute("SELECT * FROM jobs WHERE idempotency_key=?", (key,)).fetchone()
if existing is not None:
if json.loads(existing["request_json"]).get("binding") != binding:
raise BindingConflict("BINDING_CONFLICT")
row = dict(existing)
connection.commit()
return self.store._receipt(row, duplicate=True)
connection.execute(
"INSERT INTO jobs(job_id,idempotency_key,campaign_id,repository,operation,branch,expected_branch_oid,payload_digest,payload_ref,payload_size,state,side_effect_state,revision,request_json,receipt_json,created_at,updated_at) VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)",
(job_id, key, binding["campaign_id"], binding["repository"], binding["operation"], binding["branch"], binding["expected_branch_oid"], binding["payload_digest"], payload_ref, payload_size, JobState.REQUESTED.value, "NONE", 0, json.dumps({"binding": binding, **durable_request}, sort_keys=True), None, now, now),
)
connection.execute("INSERT INTO idempotency_keys(key,job_id,binding_json,created_at) VALUES(?,?,?,?)", (key, job_id, json.dumps(binding, sort_keys=True), now))
connection.execute("INSERT INTO state_transitions(job_id,from_state,to_state,revision,side_effect_state,created_at) VALUES(?,?,?,?,?,?)", (job_id, None, JobState.REQUESTED.value, 0, "NONE", now))
row = dict(connection.execute("SELECT * FROM jobs WHERE job_id=?", (job_id,)).fetchone())
connection.commit()
except BindingConflict:
raise
except sqlite3.IntegrityError:
row = self.store.read(job_id)
return self.store._receipt(row, duplicate=True)
except sqlite3.Error as exc:
raise ServiceUnavailable("JOB_DB_UNAVAILABLE") from exc
row["request"] = json.loads(row["request_json"])
self.store._mirror(row)
return self.store._receipt(row, duplicate=False)
def lease_job(
self,
job_id: str,
*,
lease_id: str,
lineage_anchor_oid: str,
current_head_oid: str,
generation: int,
lease_expires_at: str,
expected_revision: int,
) -> JobReceipt:
"""Atomically create a branch lease and move its Job to LEASED."""
if generation < 1 or any(len(oid) != 40 for oid in (lineage_anchor_oid, current_head_oid)):
raise JobError("LEASE_BINDING_INVALID")
try:
with self.store._connect() as connection:
connection.execute("BEGIN IMMEDIATE")
row = connection.execute("SELECT * FROM jobs WHERE job_id=?", (job_id,)).fetchone()
if row is None:
raise JobError("JOB_NOT_FOUND")
if int(row["revision"]) != expected_revision:
raise JobError("REVISION_CAS_CONFLICT")
if row["branch"] in {"main", "master"}:
raise JobError("PROTECTED_BRANCH_REJECTED")
now = utc_now()
connection.execute("INSERT INTO branch_leases(lease_id,job_id,branch,lineage_anchor_oid,current_head_oid,generation,lease_expires_at,state,revision) VALUES(?,?,?,?,?,?,?,?,?)", (lease_id, job_id, row["branch"], lineage_anchor_oid, current_head_oid, generation, lease_expires_at, "ACTIVE", 0))
next_revision = expected_revision + 1
updated = connection.execute("UPDATE jobs SET state=?,side_effect_state=?,revision=?,updated_at=? WHERE job_id=? AND revision=?", (JobState.LEASED.value, "LEASED", next_revision, now, job_id, expected_revision))
if updated.rowcount != 1:
raise JobError("REVISION_CAS_CONFLICT")
connection.execute("INSERT INTO state_transitions(job_id,from_state,to_state,revision,side_effect_state,created_at) VALUES(?,?,?,?,?,?)", (job_id, row["state"], JobState.LEASED.value, next_revision, "LEASED", now))
result = dict(connection.execute("SELECT * FROM jobs WHERE job_id=?", (job_id,)).fetchone())
connection.commit()
except sqlite3.IntegrityError as exc:
raise JobError("LEASE_CONFLICT") from exc
except (JobError, BindingConflict):
raise
except sqlite3.Error as exc:
raise ServiceUnavailable("JOB_DB_UNAVAILABLE") from exc
result["request"] = json.loads(result["request_json"])
self.store._mirror(result)
return self.store._receipt(result, duplicate=True)
def update_lease(self, lease_id: str, *, expected_revision: int, current_head_oid: str, generation: int, state: str = "ACTIVE") -> None:
if len(current_head_oid) != 40 or generation < 1:
raise JobError("LEASE_BINDING_INVALID")
try:
with self.store._connect() as connection:
connection.execute("BEGIN IMMEDIATE")
updated = connection.execute("UPDATE branch_leases SET current_head_oid=?,generation=?,state=?,revision=revision+1 WHERE lease_id=? AND revision=?", (current_head_oid, generation, state, lease_id, expected_revision))
if updated.rowcount != 1:
raise JobError("LEASE_REVISION_CAS_CONFLICT")
connection.commit()
except JobError:
raise
except sqlite3.Error as exc:
raise ServiceUnavailable("JOB_DB_UNAVAILABLE") from exc
def advance(self, job_id: str, state: JobState, *, side_effect_state: str, receipt: Mapping[str, Any] | None = None, expected_revision: int | None = None) -> JobReceipt:
return self._advance(
job_id,
state,
side_effect_state=side_effect_state,
receipt=receipt,
expected_revision=expected_revision,
)
def _advance(
self,
job_id: str,
state: JobState,
*,
side_effect_state: str,
receipt: Mapping[str, Any] | None = None,
expected_revision: int | None = None,
_allow_verified_external_recovery: bool = False,
) -> JobReceipt:
try:
with self.store._connect() as connection:
connection.execute("BEGIN IMMEDIATE")
row = connection.execute("SELECT * FROM jobs WHERE job_id=?", (job_id,)).fetchone()
if row is None:
raise JobError("JOB_NOT_FOUND")
previous = JobState(row["state"])
revision = int(row["revision"])
if expected_revision is not None and expected_revision != revision:
raise JobError("REVISION_CAS_CONFLICT")
recovery_transition = (
_allow_verified_external_recovery
and previous == JobState.MANUAL_OWNER_REVIEW_REQUIRED
and state == JobState.COMPLETED
)
if previous in _TERMINAL and state != previous and not recovery_transition:
raise JobError("JOB_TERMINAL")
if state not in _RECOVERABLE | {JobState.RECONCILING, JobState.MANUAL_OWNER_REVIEW_REQUIRED}:
if previous in _RECOVERABLE:
raise JobError("RECONCILE_REQUIRED")
if previous != JobState.RECONCILING and _ORDER.get(state, -1) < _ORDER.get(previous, -1):
raise JobError("JOB_STATE_REGRESSION")
next_revision = revision + 1
receipt_json = json.dumps(dict(receipt), sort_keys=True) if receipt is not None else row["receipt_json"]
now = utc_now()
updated = connection.execute("UPDATE jobs SET state=?,side_effect_state=?,revision=?,receipt_json=?,updated_at=? WHERE job_id=? AND revision=?", (state.value, side_effect_state, next_revision, receipt_json, now, job_id, revision))
if updated.rowcount != 1:
raise JobError("REVISION_CAS_CONFLICT")
connection.execute("INSERT INTO state_transitions(job_id,from_state,to_state,revision,side_effect_state,created_at) VALUES(?,?,?,?,?,?)", (job_id, previous.value, state.value, next_revision, side_effect_state, now))
if receipt is not None:
digest = hashlib.sha256(canonical_json(receipt)).hexdigest()
connection.execute("INSERT INTO receipts(receipt_id,job_id,kind,digest,payload_json,created_at) VALUES(?,?,?,?,?,?)", (f"receipt-{job_id}-{next_revision}", job_id, state.value, digest, json.dumps(dict(receipt), sort_keys=True), now))
result = dict(connection.execute("SELECT * FROM jobs WHERE job_id=?", (job_id,)).fetchone())
connection.commit()
except (JobError, BindingConflict):
raise
except sqlite3.Error as exc:
raise ServiceUnavailable("JOB_DB_UNAVAILABLE") from exc
result["request"] = json.loads(result["request_json"])
self.store._mirror(result)
return self.store._receipt(result, duplicate=True)
@staticmethod
def _validate_verified_external_merge_proof(
job_id: str,
binding: Mapping[str, Any],
proof: Mapping[str, Any],
) -> dict[str, Any]:
if not isinstance(proof, Mapping):
raise JobError("EXTERNAL_SIDE_EFFECT_PROOF_INVALID")
def require_oid(field: str) -> str:
value = proof.get(field)
if not isinstance(value, str) or len(value) != 40 or any(ch not in "0123456789abcdef" for ch in value):
raise JobError("EXTERNAL_SIDE_EFFECT_PROOF_INVALID")
return value
if proof.get("proof_schema") != "aios.job-recovery.v1":
raise JobError("EXTERNAL_SIDE_EFFECT_PROOF_INVALID")
if proof.get("job_id") != job_id:
raise JobError("EXTERNAL_SIDE_EFFECT_PROOF_MISMATCH")
if proof.get("operation") != "PROTECTED_MERGE" or binding.get("operation") != "PROTECTED_MERGE":
raise JobError("EXTERNAL_SIDE_EFFECT_OPERATION_REJECTED")
if proof.get("pr_number") != 53 or str(binding.get("pr_number")) != "53":
raise JobError("EXTERNAL_SIDE_EFFECT_PROOF_MISMATCH")
if proof.get("pr_merged") is not True or proof.get("protected_merge") is not True:
raise JobError("EXTERNAL_SIDE_EFFECT_PROOF_MISMATCH")
if proof.get("force_push") is not False:
raise JobError("EXTERNAL_SIDE_EFFECT_PROOF_MISMATCH")
if proof.get("machine_actor") != "aios-autonomy-agent":
raise JobError("EXTERNAL_SIDE_EFFECT_PROOF_MISMATCH")
expected_head = str(binding.get("expected_branch_oid", ""))
expected_base = str(binding.get("expected_main_oid", ""))
head_oid = require_oid("head_oid")
base_oid = require_oid("base_oid")
merged_main = require_oid("main_oid_after_protected_merge")
gitea_merged_main = require_oid("gitea_main_oid_after_protected_merge")
authority_merged_main = require_oid("authority_main_oid_after_protected_merge")
current_gitea = require_oid("current_gitea_main_oid")
current_authority = require_oid("current_authority_main_oid")
if head_oid != expected_head or base_oid != expected_base:
raise JobError("EXTERNAL_SIDE_EFFECT_PROOF_MISMATCH")
if not (merged_main == gitea_merged_main == authority_merged_main == head_oid):
raise JobError("EXTERNAL_SIDE_EFFECT_PROOF_MISMATCH")
if current_gitea != current_authority or proof.get("current_main_descends_from_protected_merge") is not True:
raise JobError("EXTERNAL_SIDE_EFFECT_PROOF_MISMATCH")
return dict(proof)
def complete_verified_external_side_effect(
self,
job_id: str,
proof: Mapping[str, Any],
*,
expected_revision: int | None = None,
) -> JobReceipt:
"""Close one historical manual job from independently verified proof.
This is deliberately narrower than advance: it is only valid for the
known protected PR53 recovery shape and preserves the old receipt.
The actual state change still uses the normal revision-CAS path.
"""
current = self.store.read(job_id)
revision = int(current["revision"])
if expected_revision is not None and expected_revision != revision:
raise JobError("REVISION_CAS_CONFLICT")
request = json.loads(current["request_json"])
binding = request.get("binding", request)
normalized_proof = self._validate_verified_external_merge_proof(job_id, binding, proof)
existing_receipt = json.loads(current["receipt_json"]) if current["receipt_json"] else None
if JobState(current["state"]) == JobState.COMPLETED:
if (
existing_receipt
and existing_receipt.get("recovered_from_historical_manual") is True
and existing_receipt.get("proof") == normalized_proof
):
return self.store._receipt(current, duplicate=True)
raise JobError("JOB_TERMINAL")
if JobState(current["state"]) != JobState.MANUAL_OWNER_REVIEW_REQUIRED:
raise JobError("EXTERNAL_SIDE_EFFECT_RECOVERY_STATE_INVALID")
recovery_receipt = {
"recovered_from_historical_manual": True,
"recovery_kind": "VERIFIED_EXTERNAL_SIDE_EFFECT",
"historical_receipt": existing_receipt,
"proof": normalized_proof,
}
return self._advance(
job_id,
JobState.COMPLETED,
side_effect_state="REMOTE_VERIFIED",
receipt=recovery_receipt,
expected_revision=revision,
_allow_verified_external_recovery=True,
)
def reconcile(self, job_id: str, observer: Callable[[Mapping[str, Any]], Mapping[str, Any]]) -> JobReceipt:
current = self.store.read(job_id)
if JobState(current["state"]) not in _RECOVERABLE:
return self.store._receipt(current, duplicate=True)
self.advance(job_id, JobState.RECONCILING, side_effect_state="IN_PROGRESS")
try:
observed = dict(observer(current))
stable = JobState(observed.get("state", JobState.REQUESTED.value))
if stable in {JobState.RECOVERABLE_FAILURE, JobState.RECONCILING}:
raise JobError("RECONCILIATION_UNSTABLE")
return self.advance(job_id, stable, side_effect_state=str(observed.get("side_effect_state", "NONE")), receipt=observed.get("receipt"))
except Exception as exc:
self.advance(job_id, JobState.RECONCILIATION_REQUIRED, side_effect_state="UNKNOWN", receipt={"error_code": type(exc).__name__})
raise JobError("RECONCILIATION_REQUIRED") from exc
def resume(self, job_id: str) -> JobReceipt:
current = self.store.read(job_id)
if JobState(current["state"]) in _TERMINAL:
raise JobError("JOB_TERMINAL")
return self.reconcile(job_id, lambda _current: {"state": JobState.REQUESTED.value, "side_effect_state": "RESUME_SAME_JOB"})
@@ -0,0 +1,279 @@
"""Durable Job side-effect executor for the AIOS campaign control plane."""
from __future__ import annotations
import argparse
import base64
from dataclasses import dataclass, replace
import json
from pathlib import Path
from typing import Any, Mapping
from aios_authority_git_executor import AuthorityGitExecutor, AuthorityGitError
from aios_campaign_recovery import RecoveryEvidenceError, load_recovered_authorization
from aios_gitea_machine_broker import CampaignCapabilityReceipt, GiteaMachineBroker, MachineBrokerError
from aios_gitops_job_controller import GitOpsJobController, JobError, JobState, JobStore
TASK_ID = "AIOS_AUTONOMOUS_RELEASE_CAMPAIGN_V1"
CAMPAIGN_ID = "AIOS-AUTONOMOUS-RELEASE-CAMPAIGN-20260807"
REPOSITORY = "aios/aios"
BRANCH = "agent/autonomous-release-campaign-v1-20260807"
EXPECTED_BRANCH_OID = "bba675fab3b7e0c5fba7066720252b3c91b9f20d"
AUTHORIZATION_DIGEST = "6b0bf550eb702126e967eeff2d561fa808444b924d8a71f5399accad508ed3b2"
LINEAGE_ANCHOR = "69b45e81dcd2c3707c367003b08c55fd361faae8"
class JobExecutionError(RuntimeError):
pass
@dataclass(slots=True)
class DurableJobExecutor:
controller: GitOpsJobController
authority: AuthorityGitExecutor
broker: GiteaMachineBroker
recovery_path: Path
def _request(self, job_id: str) -> tuple[dict[str, Any], dict[str, Any]]:
row = self.controller.store.read(job_id)
try:
request = json.loads(row["request_json"])
except (KeyError, TypeError, json.JSONDecodeError) as exc:
raise JobExecutionError("JOB_REQUEST_INVALID") from exc
return row, dict(request.get("binding") or {})
def _validate_campaign(self, request: Mapping[str, Any]) -> str:
operation = str(request.get("operation", ""))
expected_branch_oid = request.get("expected_branch_oid")
required = {
"task_id": TASK_ID,
"campaign_id": CAMPAIGN_ID,
"repository": REPOSITORY,
"branch": BRANCH,
"authorization_recovery_digest": AUTHORIZATION_DIGEST,
}
for key, expected in required.items():
if request.get(key) != expected:
raise JobExecutionError(f"JOB_{key.upper()}_MISMATCH")
new_oid = request.get("new_branch_oid")
if not isinstance(new_oid, str) or len(new_oid) != 40 or any(ch not in "0123456789abcdef" for ch in new_oid):
raise JobExecutionError("NEW_BRANCH_OID_INVALID")
if not isinstance(expected_branch_oid, str) or len(expected_branch_oid) != 40 or any(ch not in "0123456789abcdef" for ch in expected_branch_oid):
raise JobExecutionError("EXPECTED_BRANCH_OID_INVALID")
if operation == "ADVANCE_EXISTING_CAMPAIGN_BRANCH" and expected_branch_oid == new_oid:
raise JobExecutionError("EXPECTED_BRANCH_OID_INVALID")
if operation != "ADVANCE_EXISTING_CAMPAIGN_BRANCH" and expected_branch_oid != new_oid:
raise JobExecutionError("POST_ADVANCE_HEAD_BINDING_INVALID")
try:
recovery = load_recovered_authorization(self.recovery_path)
except RecoveryEvidenceError as exc:
raise JobExecutionError(str(exc)) from exc
if recovery["digest"] != AUTHORIZATION_DIGEST:
raise JobExecutionError("RECOVERY_DIGEST_MISMATCH")
for key, expected in required.items():
if key == "campaign_id":
actual = recovery.get(key)
elif key == "authorization_recovery_digest":
continue
else:
actual = recovery.get({"expected_branch_oid": "expected_branch_oid"}.get(key, key))
if actual is not None and actual != expected:
raise JobExecutionError("RECOVERY_BINDING_MISMATCH")
if recovery.get("expected_branch_oid") != EXPECTED_BRANCH_OID:
raise JobExecutionError("RECOVERY_BRANCH_OID_MISMATCH")
authority_head = self.authority.read_ref(f"refs/heads/{BRANCH}")
if authority_head not in {expected_branch_oid, new_oid}:
raise JobExecutionError("AUTHORITY_BRANCH_EXPECTATION_MISMATCH")
if not self.authority.ancestry_check(expected_branch_oid, new_oid):
raise JobExecutionError("NEW_OID_NOT_DESCENDANT")
if not self.authority.ancestry_check(EXPECTED_BRANCH_OID, new_oid):
raise JobExecutionError("NEW_OID_OUTSIDE_AUTHORIZED_LINEAGE")
identity = self.broker.verify_identity()
if identity.get("repository") != REPOSITORY or identity.get("login") == "aiman":
raise JobExecutionError("MACHINE_IDENTITY_SCOPE_INVALID")
return new_oid
def dry_run(self, job_id: str) -> dict[str, Any]:
_row, request = self._request(job_id)
new_oid = self._validate_campaign(request)
remote = self.broker.readback()
if remote is None:
action = "CREATE_REQUIRED"
elif remote == new_oid:
action = "ALREADY_IN_SYNC"
elif remote == request["expected_branch_oid"]:
action = "FAST_FORWARD_REQUIRED"
else:
action = "MANUAL_OWNER_REVIEW_REQUIRED"
if action == "MANUAL_OWNER_REVIEW_REQUIRED":
raise JobExecutionError("GITEA_BRANCH_CONFLICT")
return {
"decision": "SAFE_TO_ADVANCE",
"authority_action": "CAS_REQUIRED",
"gitea_action": action,
"previous_branch_oid": request["expected_branch_oid"],
"final_branch_oid": new_oid,
"force_push": False,
}
def _recover_requested(self, job_id: str) -> dict[str, Any]:
row = self.controller.store.read(job_id)
if JobState(row["state"]) in {JobState.RECOVERABLE_FAILURE, JobState.RECONCILIATION_REQUIRED}:
self.controller.resume(job_id)
row = self.controller.store.read(job_id)
return row
def execute(self, job_id: str, *, dry_run: bool = False) -> dict[str, Any]:
try:
return self._execute_unprotected(job_id, dry_run=dry_run)
except Exception as exc:
try:
row = self.controller.store.read(job_id)
state = JobState(row["state"])
if state not in {JobState.COMPLETED, JobState.MANUAL_OWNER_REVIEW_REQUIRED, JobState.RECOVERABLE_FAILURE, JobState.RECONCILIATION_REQUIRED}:
self.controller.advance(job_id, JobState.RECOVERABLE_FAILURE, side_effect_state="UNKNOWN", receipt={"error_code": type(exc).__name__})
except Exception:
pass
raise
def _execute_unprotected(self, job_id: str, *, dry_run: bool = False) -> dict[str, Any]:
row, request = self._request(job_id)
operation = request.get("operation")
if operation == "ADVANCE_EXISTING_CAMPAIGN_BRANCH":
if dry_run:
return self.dry_run(job_id)
return self._advance_campaign(job_id)
if operation == "COMMIT_FILE":
return self._commit_file(job_id, request)
if operation == "SYNC_TASK_BRANCH_TO_GITEA":
new_oid = self._validate_campaign(request)
final = self.broker.push_branch(new_oid)
return {"operation": operation, "remote_verified": final == new_oid, "final_network_oid": final}
if operation == "CREATE_PR":
self._validate_campaign(request)
return dict(self.broker.create_pr("AIOS autonomous release control plane", "Durable campaign continuation; exact head and recovery evidence are bound in the Job."))
if operation == "PUBLISH_CHECK":
self._validate_campaign(request)
context = request.get("check_context", "aios/durable-release")
state = request.get("check_state", "success")
description = request.get("check_description", "Durable campaign control-plane check passed")
return dict(self.broker.status(str(state), str(description), str(context)))
if operation == "PROTECTED_MERGE":
self._validate_campaign(request)
number = int(request.get("pr_number", "0"))
self.broker.transport.expected_merge_head = request["expected_branch_oid"]
expected_main = request.get("expected_main_oid")
if expected_main is not None:
self.broker.transport.expected_merge_base = expected_main
return dict(self.broker.merge(number))
if operation == "RECONCILE_AUTHORITY_MAIN":
self._validate_campaign(request)
old = request.get("expected_main_oid")
new = request.get("new_branch_oid")
if not isinstance(old, str) or not isinstance(new, str):
raise JobExecutionError("MAIN_RECONCILIATION_BINDING_INVALID")
gitea_main = self.broker.transport.read_branch("main")
if gitea_main != new:
raise JobExecutionError("GITEA_MAIN_EXPECTATION_MISMATCH")
if not self.authority.ancestry_check(old, new):
raise JobExecutionError("MAIN_OID_NOT_DESCENDANT")
return {"authority_main": self.authority.ff_CAS_main_after_verified_protected_merge(old, new)}
raise JobExecutionError("OPERATION_NOT_ALLOWED")
def _advance_campaign(self, job_id: str) -> dict[str, Any]:
self._recover_requested(job_id)
row, request = self._request(job_id)
new_oid = self._validate_campaign(request)
expected_oid = request["expected_branch_oid"]
state = JobState(row["state"])
if state == JobState.REQUESTED:
receipt = self.controller.advance(job_id, JobState.AUTHORIZED, side_effect_state="AUTHORIZED", receipt={"authorization_digest": AUTHORIZATION_DIGEST, "task_id": TASK_ID})
row = self.controller.store.read(job_id)
if JobState(row["state"]) == JobState.AUTHORIZED:
self.controller.lease_job(job_id, lease_id=f"lease-{job_id}", lineage_anchor_oid=LINEAGE_ANCHOR, current_head_oid=expected_oid, generation=1, lease_expires_at="2099-01-01T00:00:00Z", expected_revision=int(row["revision"]))
row = self.controller.store.read(job_id)
if JobState(row["state"]) == JobState.LEASED:
self.controller.advance(job_id, JobState.WORKTREE_READY, side_effect_state="WORKTREE_READY", receipt={"branch": BRANCH, "expected_oid": expected_oid})
row = self.controller.store.read(job_id)
if JobState(row["state"]) == JobState.WORKTREE_READY:
self.controller.advance(job_id, JobState.EXECUTING, side_effect_state="IN_PROGRESS")
row = self.controller.store.read(job_id)
authority_before = self.authority.read_ref(f"refs/heads/{BRANCH}")
if authority_before == expected_oid:
authority_oid = self.authority.advance_task_branch_CAS(f"refs/heads/{BRANCH}", expected_oid, new_oid)
elif authority_before == new_oid:
authority_oid = new_oid
else:
raise JobExecutionError("AUTHORITY_BRANCH_CONFLICT")
row = self.controller.store.read(job_id)
if JobState(row["state"]) == JobState.EXECUTING:
self.controller.advance(job_id, JobState.LOCAL_VERIFIED, side_effect_state="LOCAL_VERIFIED", receipt={"authority": {"branch": BRANCH, "previous_oid": expected_oid, "new_oid": new_oid, "CAS_success": True, "readback_oid": authority_oid}})
row = self.controller.store.read(job_id)
remote_before = self.broker.readback()
if remote_before != new_oid:
if remote_before not in {None, expected_oid}:
raise JobExecutionError("GITEA_BRANCH_CONFLICT")
if JobState(row["state"]) == JobState.LOCAL_VERIFIED:
if hasattr(self.broker, "receipt"):
self.broker.receipt = replace(self.broker.receipt, expected_oid=expected_oid)
self.controller.advance(job_id, JobState.REMOTE_SYNCING, side_effect_state="IN_PROGRESS", receipt={"gitea": {"previous_oid": remote_before, "desired_oid": new_oid, "force_push": False}})
final_network = self.broker.push_branch(new_oid)
else:
final_network = new_oid
final_api = self.broker.readback()
if final_api != new_oid:
raise JobExecutionError("GITEA_BRANCH_READBACK_MISMATCH")
row = self.controller.store.read(job_id)
if JobState(row["state"]) in {JobState.LOCAL_VERIFIED, JobState.REMOTE_SYNCING}:
self.controller.advance(job_id, JobState.REMOTE_VERIFIED, side_effect_state="REMOTE_VERIFIED", receipt={"gitea": {"machine_actor": "aios-autonomy-agent", "previous_oid": remote_before, "desired_oid": new_oid, "final_network_oid": final_network, "final_API_oid": final_api, "force_push": False}})
row = self.controller.store.read(job_id)
if JobState(row["state"]) == JobState.REMOTE_VERIFIED:
self.controller.advance(job_id, JobState.COMPLETED, side_effect_state="REMOTE_VERIFIED", receipt={"authority_oid": authority_oid, "network_oid": final_network, "gitea_api_oid": final_api, "force_push": False})
return self.controller.store._receipt(self.controller.store.read(job_id), duplicate=True).as_dict()
def _commit_file(self, job_id: str, request: Mapping[str, Any]) -> dict[str, Any]:
row = self.controller.store.read(job_id)
payload_ref = request.get("payload_ref")
if not payload_ref:
raise JobExecutionError("PAYLOAD_REQUIRED")
payload = self.controller.store.payloads.read_verified(payload_ref, row["payload_digest"], int(request["payload_size"]))
try:
spec = json.loads(payload.decode())
path = str(spec["path"])
content = base64.b64decode(spec["content_base64"], validate=True)
except Exception as exc:
raise JobExecutionError("COMMIT_FILE_PAYLOAD_INVALID") from exc
new_oid = self.authority.commit_file_in_isolated_worktree(base_oid=row["expected_branch_oid"], files={path: content}, message="chore(gitops): durable commit-file", worktree_name=job_id)
return {"operation": "COMMIT_FILE", "new_oid": new_oid}
def build_from_environment(job_root: Path) -> DurableJobExecutor:
from aios_gitea_machine_transport import GiteaMachineTransport
from aios_gitea_machine_broker import MachineCredentialProjection
account = __import__("os").environ.get("AIOS_GITEA_MACHINE_ACCOUNT", "aios-autonomy-agent")
api = __import__("os").environ.get("AIOS_GITEA_API", "https://g.cnnetmkt.top/git/api/v1")
remote = __import__("os").environ.get("AIOS_GITEA_REMOTE", "https://g.cnnetmkt.top/git/aios/aios.git")
authority_dir = Path(__import__("os").environ.get("AIOS_GITOPS_AUTHORITY_GIT_DIR", "/data/aios-gitops/repo.git"))
worktree_root = Path(__import__("os").environ.get("AIOS_GITOPS_WORKTREE_ROOT", "/var/lib/aios-gitops/worktrees"))
recovery = Path(__import__("os").environ.get("AIOS_RELEASE_RECOVERY_RECEIPT", "/var/lib/aios-gitops/release-campaigns/AIOS-AUTONOMOUS-RELEASE-CAMPAIGN-20260807/authorization-recovery.json"))
transport = GiteaMachineTransport(api, REPOSITORY, remote, authority_dir)
credential = MachineCredentialProjection.from_mapping({"scheme": "systemd-credential", "name": "gitea-machine-token", "account": account})
receipt = CampaignCapabilityReceipt(CAMPAIGN_ID, TASK_ID, REPOSITORY, BRANCH, EXPECTED_BRANCH_OID, frozenset({"exact_task_branch_push", "pr_create_update", "status_publish", "protected_merge", "remote_readback"}), frozenset({"direct_main_push", "force_push", "tag_update", "tag_delete", "baseline_write", "production_deploy"}))
return DurableJobExecutor(GitOpsJobController(JobStore(job_root)), AuthorityGitExecutor(authority_dir, worktree_root), GiteaMachineBroker(transport, credential, receipt), recovery)
def main(argv: list[str] | None = None) -> int:
parser = argparse.ArgumentParser()
parser.add_argument("--job-root", required=True, type=Path)
parser.add_argument("--job-id", required=True)
parser.add_argument("--dry-run", action="store_true")
args = parser.parse_args(argv)
result = build_from_environment(args.job_root).execute(args.job_id, dry_run=args.dry_run)
print(json.dumps(result, sort_keys=True))
return 0
if __name__ == "__main__":
raise SystemExit(main())
+7 -1
View File
@@ -5,6 +5,8 @@ from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
from pathlib import Path from pathlib import Path
from typing import Any from typing import Any
from release_version import validate_release_version
ROOT = Path(os.getenv('AIOS_GITOPS_ROOT', '/data/aios-gitops')) ROOT = Path(os.getenv('AIOS_GITOPS_ROOT', '/data/aios-gitops'))
WORKSPACE = Path(os.getenv('AIOS_GITOPS_WORKSPACE', str(ROOT / 'workspace'))) WORKSPACE = Path(os.getenv('AIOS_GITOPS_WORKSPACE', str(ROOT / 'workspace')))
DB_PATH = Path(os.getenv('AIOS_GITOPS_DB', str(ROOT / 'runtime' / 'gitops_model.sqlite3'))) DB_PATH = Path(os.getenv('AIOS_GITOPS_DB', str(ROOT / 'runtime' / 'gitops_model.sqlite3')))
@@ -218,7 +220,11 @@ class Handler(BaseHTTPRequestHandler):
a = self.require_write(['owner']); a = self.require_write(['owner']);
if not a: return if not a: return
ver=data.get('version') or data.get('tag') ver=data.get('version') or data.get('tag')
if not re.match(r'^V\d+R\d+C\d+(-[a-zA-Z0-9_.-]+)?$', ver or ''): self.send_json({'error':'version must match VxRxCx'},400); return try:
validate_release_version(ver)
except ValueError:
self.send_json({'error':'version must match VxRxCx'},400)
return
msg=data.get('message') or f'release {ver}'; out=git_cmd(['tag','-a',ver,'-m',msg]); msg=data.get('message') or f'release {ver}'; out=git_cmd(['tag','-a',ver,'-m',msg]);
if data.get('push',False): git_cmd(['push','origin',ver]) if data.get('push',False): git_cmd(['push','origin',ver])
level='V' if re.match(r'^V[1-9]\d*R0C0', ver) else ('R' if re.match(r'^V\d+R[1-9]\d*C0', ver) else 'C'); exec_sql('INSERT OR REPLACE INTO gitops_versions(version,tag,level,summary,status,created_by,created_at) VALUES(?,?,?,?,?,?,?)',(ver,ver,level,msg,'ready',a['username'],now())); exec_sql('UPDATE gitops_baseline SET active_version=?,active_tag=?,updated_at=? WHERE id=?',(ver,ver,now(),'current')); audit(a['username'],'git_tag',ver,{'level':level}); self.send_json({'status':'tagged','version':ver,'git':out}); return level='V' if re.match(r'^V[1-9]\d*R0C0', ver) else ('R' if re.match(r'^V\d+R[1-9]\d*C0', ver) else 'C'); exec_sql('INSERT OR REPLACE INTO gitops_versions(version,tag,level,summary,status,created_by,created_at) VALUES(?,?,?,?,?,?,?)',(ver,ver,level,msg,'ready',a['username'],now())); exec_sql('UPDATE gitops_baseline SET active_version=?,active_tag=?,updated_at=? WHERE id=?',(ver,ver,now(),'current')); audit(a['username'],'git_tag',ver,{'level':level}); self.send_json({'status':'tagged','version':ver,'git':out}); return
@@ -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))
+100
View File
@@ -0,0 +1,100 @@
"""Single supervised socket-activated worker for durable GitOps Jobs.
The worker consumes durable requests and reconciliation queries. Repository
mutation is a separate injected executor and remains impossible while the
legacy global write gate is false.
"""
from __future__ import annotations
import argparse
import json
import os
from pathlib import Path
import signal
import socket
import threading
from typing import Any, Mapping
from aios_gitops_job_controller import GitOpsJobController, JobError, JobState, JobStore, ServiceUnavailable
def runtime_policy(environment: Mapping[str, str] | None = None) -> dict[str, bool]:
env = environment or os.environ
legacy = env.get("AIOS_GITOPS_WRITES_ENABLED", "false").lower() in {"1", "true", "yes", "on"}
durable = env.get("AIOS_GITOPS_DURABLE_JOB_EXECUTION_ENABLED", "false").lower() in {"1", "true", "yes", "on"}
# Legacy direct Git mutation remains disabled. Authorized durable Jobs use
# their own campaign/task capability and may mutate only through the
# worker's lease/CAS path while the legacy gate stays false.
return {"legacy_global_write": legacy, "durable_job_execution": durable, "repository_mutation_allowed": durable}
def handle_request(controller: GitOpsJobController, request: Mapping[str, Any], executor: Any | None = None) -> dict[str, Any]:
job_id = str(request.get("job_id") or "")
if not job_id:
raise JobError("JOB_ID_REQUIRED")
if request.get("action") == "execute":
if executor is None:
raise JobError("JOB_EXECUTOR_UNAVAILABLE")
result = executor.execute(job_id, dry_run=bool(request.get("dry_run", False)))
receipt = controller.store._receipt(controller.store.read(job_id), duplicate=True)
return {"job": result, **receipt.as_dict()}
if request.get("action") == "resume":
receipt = controller.resume(job_id)
else:
row = controller.store.read(job_id)
receipt = controller.store._receipt(row, duplicate=True)
return receipt.as_dict()
def serve_socket(sock: socket.socket, controller: GitOpsJobController, stopping: threading.Event, executor: Any | None = None) -> None:
sock.listen(32)
sock.settimeout(1.0)
while not stopping.is_set():
try:
connection, _ = sock.accept()
except socket.timeout:
continue
with connection:
try:
raw = connection.makefile("rb").readline(1_048_576)
request = json.loads(raw.decode("utf-8"))
response = {"status": "ok", **handle_request(controller, request, executor)}
except ServiceUnavailable as exc:
response = {"status": "error", "error": str(exc), "retryable": True, "side_effect_state": "UNKNOWN"}
except Exception as exc:
response = {"status": "error", "error": type(exc).__name__, "retryable": False}
connection.sendall((json.dumps(response, sort_keys=True) + "\n").encode("utf-8"))
def socket_from_systemd() -> socket.socket:
if os.environ.get("LISTEN_FDS") != "1":
raise RuntimeError("SOCKET_ACTIVATION_REQUIRED")
return socket.fromfd(3, socket.AF_UNIX, socket.SOCK_STREAM)
def main(argv: list[str] | None = None) -> int:
parser = argparse.ArgumentParser()
parser.add_argument("--job-root", required=True, type=Path)
args = parser.parse_args(argv)
policy = runtime_policy()
if policy["legacy_global_write"]:
raise RuntimeError("LEGACY_GLOBAL_WRITES_MUST_REMAIN_FALSE")
if not policy["durable_job_execution"]:
raise RuntimeError("DURABLE_JOB_EXECUTION_GATE_CLOSED")
controller = GitOpsJobController(JobStore(args.job_root))
from aios_gitops_job_executor import build_from_environment
executor = build_from_environment(args.job_root)
stopping = threading.Event()
signal.signal(signal.SIGTERM, lambda *_: stopping.set())
signal.signal(signal.SIGINT, lambda *_: stopping.set())
sock = socket_from_systemd()
try:
serve_socket(sock, controller, stopping, executor)
finally:
sock.close()
return 0
if __name__ == "__main__":
raise SystemExit(main())
@@ -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
@@ -0,0 +1,229 @@
#!/usr/bin/env python3
"""Small staging-only response overlay for the legacy listPatches route.
The base staging module remains the source of truth for routing, auth, and
governed write behavior. This entrypoint only replaces the handler's JSON
serializer for the patch-read aliases, projecting the legacy payload into a
bounded, cursor-pageable summary while leaving every other route untouched.
"""
from __future__ import annotations
import base64
import json
import re
import urllib.parse
from typing import Any
import aios_model_api_staging as base
PATCH_PATHS = frozenset({"/api/patch", "/api/patches", "/api/branches", "/patch"})
PATCH_DEFAULT_LIMIT = 20
PATCH_MAX_LIMIT = 100
PATCH_RESPONSE_BUDGET = 128 * 1024
PATCH_FIELD_MAX_CHARS = 256
PATCH_SUMMARY_MAX_CHARS = 1024
PATCH_COMPAT_LIST_LIMIT = 20
_TOKEN_RE = re.compile(r"(?i)(?:sk-|ghp_|bearer\s+)[A-Za-z0-9._:/+=-]{12,}")
def _text(value: Any, limit: int = PATCH_FIELD_MAX_CHARS) -> str:
if value is None:
return ""
if isinstance(value, (dict, list)):
value = json.dumps(value, ensure_ascii=False, sort_keys=True, separators=(",", ":"))
value = _TOKEN_RE.sub("[REDACTED]", str(value))
return value if len(value) <= limit else value[: max(1, limit - 1)] + "…"
def _record_id(record: dict[str, Any], index: int) -> str:
return _text(
record.get("patch_id")
or record.get("id")
or record.get("patch_name")
or record.get("branch")
or f"patch-{index}",
)
def _record_key(record: dict[str, Any], index: int) -> tuple[bool, str, str, int]:
timestamp = _text(
record.get("updated_at")
or record.get("updatedAt")
or record.get("created_at")
or record.get("createdAt")
)
return bool(timestamp), timestamp, _record_id(record, index), index
def _summary(record: dict[str, Any], index: int, summary_limit: int) -> dict[str, Any]:
patch_id = _record_id(record, index)
raw_summary = next(
(
record.get(key)
for key in ("summary", "description", "text", "intent")
if record.get(key) not in (None, "")
),
"",
)
summary = _text(raw_summary, summary_limit)
item = {
"patch_id": patch_id,
"name": _text(record.get("name") or record.get("title") or record.get("patch_name") or patch_id),
"branch": _text(record.get("branch")) or None,
"ref": _text(record.get("ref") or record.get("branch")) or None,
"status": _text(record.get("status") or record.get("state") or "recorded"),
"risk": _text(record.get("risk")) or None,
"created_at": _text(record.get("created_at") or record.get("createdAt")) or None,
"updated_at": _text(record.get("updated_at") or record.get("updatedAt")) or None,
"commit_oid": _text(record.get("commit_oid") or record.get("commit") or record.get("oid"), 128) or None,
"summary": summary,
}
if len(_text(raw_summary, PATCH_SUMMARY_MAX_CHARS + 1)) > summary_limit:
item["summary_truncated"] = True
return item
def _branch_summary(record: Any, index: int) -> Any:
if not isinstance(record, dict):
return _text(record)
return {
"name": _text(record.get("name") or record.get("branch") or f"branch-{index}"),
"ref": _text(record.get("ref") or record.get("name") or record.get("branch")) or None,
"oid": _text(record.get("oid") or record.get("commit_oid") or record.get("commit"), 128) or None,
"updated_at": _text(record.get("updated_at") or record.get("updatedAt")) or None,
}
def _encode_cursor(key: tuple[bool, str, str, int]) -> str:
raw = json.dumps(
{"has_timestamp": key[0], "timestamp": key[1], "patch_id": key[2], "index": key[3]},
separators=(",", ":"),
).encode("utf-8")
return base64.urlsafe_b64encode(raw).decode("ascii").rstrip("=")
def _decode_cursor(value: Any) -> tuple[bool, str, str, int]:
if not isinstance(value, str) or not value or len(value) > 256 or re.fullmatch(r"[A-Za-z0-9_-]+", value) is None:
raise ValueError("invalid_cursor")
try:
padded = value + "=" * (-len(value) % 4)
decoded = json.loads(base64.urlsafe_b64decode(padded.encode("ascii")).decode("utf-8"))
except Exception as exc:
raise ValueError("invalid_cursor") from exc
if (
not isinstance(decoded, dict)
or set(decoded) != {"has_timestamp", "timestamp", "patch_id", "index"}
or not isinstance(decoded["has_timestamp"], bool)
or not isinstance(decoded["timestamp"], str)
or not isinstance(decoded["patch_id"], str)
or not isinstance(decoded["index"], int)
or decoded["index"] < 0
):
raise ValueError("invalid_cursor")
return decoded["has_timestamp"], decoded["timestamp"], decoded["patch_id"], decoded["index"]
def _limit(query: dict[str, list[str]]) -> int:
raw = (query.get("limit") or [str(PATCH_DEFAULT_LIMIT)])[0]
try:
result = int(raw)
except (TypeError, ValueError) as exc:
raise ValueError("invalid_limit") from exc
if not 1 <= result <= PATCH_MAX_LIMIT:
raise ValueError("invalid_limit")
return result
def _page(records: list[Any], query: dict[str, list[str]], summary_limit: int = PATCH_SUMMARY_MAX_CHARS) -> dict[str, Any]:
limit = _limit(query)
rows = [
(record, index, _record_key(record, index))
for index, record in enumerate(records)
if isinstance(record, dict)
]
rows.sort(key=lambda row: row[2], reverse=True)
cursor = (query.get("cursor") or [None])[0]
if cursor:
cursor_key = _decode_cursor(cursor)
if cursor_key not in {row[2] for row in rows}:
raise ValueError("invalid_cursor")
rows = [row for row in rows if row[2] < cursor_key]
selected = rows[:limit]
return {
"items": [_summary(record, index, summary_limit) for record, index, _ in selected],
"next_cursor": _encode_cursor(selected[-1][2]) if len(rows) > len(selected) and selected else None,
"has_more": len(rows) > len(selected),
"count": len(selected),
"_cursor_keys": [key for _, _, key in selected],
}
def _bounded_payload(data: dict[str, Any], query: dict[str, list[str]]) -> dict[str, Any]:
records = data.get("metadata_patches")
if not isinstance(records, list):
records = data.get("patches") if isinstance(data.get("patches"), list) else []
page = _page(records, query)
cursor_keys = page.pop("_cursor_keys")
payload = {
key: data[key]
for key in ("repository_available", "query_error")
if key in data
}
payload["metadata_patches"] = page["items"]
payload["patches"] = page["items"]
for key in ("orphan_metadata", "branches_without_metadata"):
values = data.get(key)
payload[key] = [
_summary(value, index, PATCH_SUMMARY_MAX_CHARS)
if isinstance(value, dict)
else _text(value)
for index, value in enumerate(values[:PATCH_COMPAT_LIST_LIMIT])
] if isinstance(values, list) else []
branches = data.get("authoritative_branches")
if isinstance(branches, list):
payload["authoritative_branches"] = [
_branch_summary(value, index) for index, value in enumerate(branches[:PATCH_COMPAT_LIST_LIMIT])
]
if isinstance(data.get("branches"), list):
payload["branches"] = [_text(value) for value in data["branches"][:PATCH_COMPAT_LIST_LIMIT]]
payload.update(page)
payload["list_patches_overlay"] = "bounded-summary-v1"
while len(json.dumps(payload, ensure_ascii=False, indent=2).encode("utf-8")) > PATCH_RESPONSE_BUDGET and payload["items"]:
payload["items"] = payload["items"][:-1]
cursor_keys = cursor_keys[:-1]
payload["metadata_patches"] = payload["items"]
payload["patches"] = payload["items"]
payload["count"] = len(payload["items"])
payload["has_more"] = True
payload["next_cursor"] = (
_encode_cursor(cursor_keys[-1])
if cursor_keys
else None
)
return payload
class ListPatchesOverlayHandler(base.H):
"""Delegate all behavior except bounded serialization of patch reads."""
def sendj(self, data: Any, code: int = 200):
path = urllib.parse.urlparse(self.path).path.rstrip("/")
if path in PATCH_PATHS and code in (200, 503) and isinstance(data, dict):
try:
data = _bounded_payload(data, urllib.parse.parse_qs(urllib.parse.urlparse(self.path).query))
except ValueError as exc:
return super().sendj(
{"error": str(exc), "min_limit": 1, "max_limit": PATCH_MAX_LIMIT},
400,
)
return super().sendj(data, code)
base.H = ListPatchesOverlayHandler
if __name__ == "__main__":
base.main()
+652
View File
@@ -0,0 +1,652 @@
#!/usr/bin/env python3
"""One-shot governed executor for an already approved exact release binding.
This module deliberately does not build release artifacts. The build/staging
evidence is completed before Owner production approval. Production execution
consumes that immutable binding, revalidates it, and performs narrow
side-effects through adapters in a durable fail-closed order.
Adapter protocol:
stdin: JSON request with operation = "probe" | "apply" | "rollback"
stdout: one JSON object on the final non-empty line
shell: never used
The executor never rewrites tags, never writes refs/heads/main, and records
Baseline last.
"""
from __future__ import annotations
import argparse
import dataclasses
import fcntl
import hashlib
import json
import os
import re
import stat
import subprocess
import sys
import tempfile
import time
from pathlib import Path
from typing import Any, Mapping, Sequence
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}$")
REQUIRED_V2R1C7_MEMBERS = {
"payload/VERSION",
"payload/active-runtime/app/deployment_state.py",
"payload/active-runtime/app/v2r1c6_privacy_tools.py",
"payload/active-runtime/app/v2r1c7_health_service.py",
"payload/deploy/aios-v2/systemd/aios-v2r1c7-production.service",
"payload/deploy/aios-v2/v2r1c7-runtime-config.json",
"payload/deploy/aios-v2/v2r1c7-rollback.json",
"payload/scripts/probe_v2r1c7_health.py",
"payload/scripts/rollback_v2r1c7_release.sh",
"payload/BUILD-METADATA.json",
"payload/ARTIFACT-MANIFEST.json",
"payload/ROLLBACK-METADATA.json",
}
class ReleaseBlocked(RuntimeError):
pass
class ManualOwnerReviewRequired(ReleaseBlocked):
pass
class ReconciliationRequired(ReleaseBlocked):
pass
@dataclasses.dataclass(frozen=True)
class Binding:
version: str
target_oid: str
evidence_closure_oid: str
artifact_sha256: str
approval_plan_id: str
artifact_path: str
artifact_size: int
production_service: str
production_port: int
repository_identity: str = "aios/aios"
def __post_init__(self) -> None:
if not self.version or not OID_RE.fullmatch(self.target_oid):
raise ValueError("invalid release target binding")
if not OID_RE.fullmatch(self.evidence_closure_oid):
raise ValueError("invalid evidence closure OID")
if not SHA_RE.fullmatch(self.artifact_sha256):
raise ValueError("invalid artifact SHA-256")
if not PLAN_RE.fullmatch(self.approval_plan_id):
raise ValueError("invalid approval plan id")
if self.artifact_size <= 0:
raise ValueError("invalid artifact size")
if not self.production_service:
raise ValueError("production service is required")
if not 1024 <= self.production_port <= 65535:
raise ValueError("invalid production port")
if self.repository_identity != "aios/aios":
raise ValueError("unexpected repository identity")
@property
def tag(self) -> str:
return self.version
@property
def fingerprint(self) -> str:
payload = {
"version": self.version,
"target_oid": self.target_oid,
"evidence_closure_oid": self.evidence_closure_oid,
"artifact_sha256": self.artifact_sha256,
"approval_plan_id": self.approval_plan_id,
"artifact_size": self.artifact_size,
"production_service": self.production_service,
"production_port": self.production_port,
"repository_identity": self.repository_identity,
}
return hashlib.sha256(canonical_json(payload)).hexdigest()
def request(self, operation: str) -> dict[str, Any]:
return {
"schema": "aios.production-executor-request.v1",
"operation": operation,
"binding_fingerprint": self.fingerprint,
"version": self.version,
"tag": self.tag,
"target_oid": self.target_oid,
"evidence_closure_oid": self.evidence_closure_oid,
"artifact_sha256": self.artifact_sha256,
"artifact_path": self.artifact_path,
"artifact_size": self.artifact_size,
"approval_plan_id": self.approval_plan_id,
"production_service": self.production_service,
"production_port": self.production_port,
"repository_identity": self.repository_identity,
}
def canonical_json(value: object) -> bytes:
return (json.dumps(value, sort_keys=True, separators=(",", ":")) + "\n").encode("utf-8")
def secure_json(path: Path) -> dict[str, Any]:
st = path.lstat()
if not stat.S_ISREG(st.st_mode) or path.is_symlink():
raise ReleaseBlocked(f"non-regular authorization file: {path}")
if st.st_uid != 0:
raise ReleaseBlocked(f"authorization file must be root-owned: {path}")
if st.st_mode & 0o022:
raise ReleaseBlocked(f"authorization file is group/world writable: {path}")
data = json.loads(path.read_text(encoding="utf-8"))
if not isinstance(data, dict):
raise ReleaseBlocked("authorization receipt must be a JSON object")
return data
def sha256_file(path: Path) -> str:
if path.is_symlink() or not path.is_file():
raise ReleaseBlocked("artifact must be a regular non-symlink file")
digest = hashlib.sha256()
with path.open("rb") as source:
for block in iter(lambda: source.read(1024 * 1024), b""):
digest.update(block)
return digest.hexdigest()
def run_git(git_dir: Path, *args: str, check: bool = True) -> subprocess.CompletedProcess[str]:
return subprocess.run(
["git", f"--git-dir={git_dir}", *args],
check=check,
capture_output=True,
text=True,
timeout=30,
env={"PATH": os.environ.get("PATH", "")},
)
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):
raise ReleaseBlocked("Authority main OID is invalid")
target_type = run_git(git_dir, "cat-file", "-t", binding.target_oid).stdout.strip()
if target_type != "commit":
raise ReleaseBlocked("target_oid is not a commit")
evidence_type = run_git(
git_dir, "cat-file", "-t", binding.evidence_closure_oid
).stdout.strip()
if evidence_type != "commit":
raise ReleaseBlocked("evidence_closure_oid is not a commit")
# A control-plane-only patch may advance main after release approval.
# The approved release target remains valid only while it is still on main.
target_on_main = run_git(
git_dir,
"merge-base",
"--is-ancestor",
binding.target_oid,
authority_main,
check=False,
).returncode == 0
if not target_on_main:
raise ManualOwnerReviewRequired(
"approved target is no longer an ancestor of Authority main"
)
target_in_evidence = run_git(
git_dir,
"merge-base",
"--is-ancestor",
binding.target_oid,
binding.evidence_closure_oid,
check=False,
).returncode == 0
if not target_in_evidence:
raise ReleaseBlocked("approved target is not an ancestor of evidence closure")
return {
"authority_main_oid": authority_main,
"target_oid": binding.target_oid,
"target_is_ancestor_of_main": True,
"evidence_closure_oid": binding.evidence_closure_oid,
"evidence_object_type": evidence_type,
"target_is_ancestor_of_evidence": True,
}
def verify_artifact(binding: Binding) -> dict[str, Any]:
path = Path(binding.artifact_path)
if path.stat().st_size != binding.artifact_size:
raise ReleaseBlocked("artifact size mismatch")
actual = sha256_file(path)
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)
)
return {
"artifact_path": str(path),
"artifact_sha256": actual,
"artifact_size": binding.artifact_size,
"verified": True,
}
def verify_authorization(receipt: Mapping[str, Any], binding: Binding) -> dict[str, Any]:
if receipt.get("accepted") is not True:
raise ReleaseBlocked("Owner production authorization is not accepted")
if receipt.get("binding_replaced") not in (None, False):
raise ManualOwnerReviewRequired("Owner binding was replaced")
expected = {
"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,
}
for key, value in expected.items():
if receipt.get(key) != value:
raise ManualOwnerReviewRequired(f"Owner authorization mismatch: {key}")
return {"accepted": True, **expected}
class DurableState:
def __init__(self, root: Path, binding: Binding):
self.root = root
self.root.mkdir(parents=True, exist_ok=True)
os.chmod(self.root, 0o700)
self.path = self.root / "execution.json"
self.binding = binding
self.value: dict[str, Any]
if self.path.exists():
existing = json.loads(self.path.read_text(encoding="utf-8"))
if existing.get("binding_fingerprint") != binding.fingerprint:
raise ManualOwnerReviewRequired(
"state root contains a different production binding"
)
self.value = existing
else:
self.value = {
"schema": "aios.production-execution-state.v1",
"binding_fingerprint": binding.fingerprint,
"version": binding.version,
"target_oid": binding.target_oid,
"state": "AUTHORIZED",
"completed": {},
"history": [],
}
self.save()
def save(self) -> None:
temporary = self.path.with_suffix(".tmp")
temporary.write_bytes(canonical_json(self.value))
os.chmod(temporary, 0o600)
os.replace(temporary, self.path)
def record(self, state: str, step: str, result: Mapping[str, Any]) -> None:
now = time.time()
sanitized = json.loads(json.dumps(result))
self.value["state"] = state
self.value["completed"][step] = sanitized
self.value["history"].append(
{"at": now, "state": state, "step": step, "result": sanitized}
)
self.save()
def fail(self, state: str, reason: str) -> None:
self.value["state"] = state
self.value["history"].append(
{"at": time.time(), "state": state, "reason": reason}
)
self.save()
class FileLock:
def __init__(self, path: Path):
self.path = path
self.handle = None
def __enter__(self) -> "FileLock":
self.path.parent.mkdir(parents=True, exist_ok=True)
self.handle = self.path.open("a+", encoding="utf-8")
os.chmod(self.path, 0o600)
try:
fcntl.flock(
self.handle.fileno(), fcntl.LOCK_EX | fcntl.LOCK_NB
)
except BlockingIOError as exc:
raise ReleaseBlocked("production executor is already running") from exc
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()
def parse_command(values: Sequence[str]) -> list[str]:
command = [str(x) for x in values if str(x)]
if not command:
raise ValueError("adapter command is required")
if not os.path.isabs(command[0]):
raise ValueError("adapter executable must be an absolute path")
return command
def run_adapter(command: Sequence[str], request: Mapping[str, Any]) -> dict[str, Any]:
completed = subprocess.run(
list(command),
input=canonical_json(request),
check=False,
capture_output=True,
timeout=180,
env={"PATH": os.environ.get("PATH", "")},
)
if completed.returncode != 0:
raise ReleaseBlocked(
f"adapter failed: {Path(command[0]).name} rc={completed.returncode}"
)
lines = [line for line in completed.stdout.decode("utf-8").splitlines() if line.strip()]
if not lines:
raise ReleaseBlocked("adapter returned no JSON result")
try:
value = json.loads(lines[-1])
except ValueError as exc:
raise ReleaseBlocked("adapter returned invalid JSON") from exc
if not isinstance(value, dict):
raise ReleaseBlocked("adapter result must be an object")
return value
def require_exact_response(result: Mapping[str, Any], binding: Binding) -> None:
for key, expected in {
"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,
}.items():
if key in result and result[key] != expected:
raise ManualOwnerReviewRequired(f"adapter binding mismatch: {key}")
def tag_step(command: Sequence[str], binding: Binding) -> dict[str, Any]:
request = binding.request("probe")
probe = run_adapter(command, request)
require_exact_response(probe, binding)
state = probe.get("status")
if state == "CONFLICT":
raise ManualOwnerReviewRequired("same-name tag conflicts with approved binding")
if state == "INDETERMINATE":
raise ReconciliationRequired("tag publication state is indeterminate")
if state not in {"APPLIED_VERIFIED", "NOT_APPLIED", "PARTIAL_LOCAL"}:
raise ReleaseBlocked(f"unsupported tag probe status: {state}")
if state != "APPLIED_VERIFIED":
applied = run_adapter(command, binding.request("apply"))
require_exact_response(applied, binding)
if applied.get("status") == "CONFLICT":
raise ManualOwnerReviewRequired("tag conflict during publication")
probe = run_adapter(command, request)
require_exact_response(probe, binding)
if probe.get("status") != "APPLIED_VERIFIED":
raise ReconciliationRequired("tag was not independently verified")
if probe.get("peeled_target_oid") != binding.target_oid:
raise ManualOwnerReviewRequired("published tag peeled target mismatch")
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")
return dict(probe)
def deploy_step(command: Sequence[str], binding: Binding) -> dict[str, Any]:
probe = run_adapter(command, binding.request("probe"))
require_exact_response(probe, binding)
if probe.get("status") == "ACTIVE_VERIFIED":
return dict(probe)
if probe.get("status") == "CONFLICT":
raise ManualOwnerReviewRequired("production runtime has conflicting binding")
applied = run_adapter(command, binding.request("apply"))
require_exact_response(applied, binding)
probe = run_adapter(command, binding.request("probe"))
require_exact_response(probe, binding)
if probe.get("status") != "ACTIVE_VERIFIED":
try:
run_adapter(command, binding.request("rollback"))
except Exception:
pass
raise ReconciliationRequired("production runtime did not verify after apply")
if (
probe.get("deployment_state") != "ACTIVE"
or probe.get("production_deployed") is not True
):
raise ReconciliationRequired("production health contract is not ACTIVE")
return dict(probe)
def finalize_step(
command: Sequence[str],
binding: Binding,
*,
name: str,
tag_oid: str,
) -> dict[str, Any]:
request = binding.request("probe")
request["tag_oid"] = tag_oid
probe = run_adapter(command, request)
require_exact_response(probe, binding)
if probe.get("status") == "CONFLICT":
raise ManualOwnerReviewRequired(f"{name} has conflicting release binding")
if probe.get("status") != "VERIFIED":
apply_request = binding.request("apply")
apply_request["tag_oid"] = tag_oid
applied = run_adapter(command, apply_request)
require_exact_response(applied, binding)
probe = run_adapter(command, request)
require_exact_response(probe, binding)
if probe.get("status") != "VERIFIED":
raise ReconciliationRequired(f"{name} did not verify after apply")
if probe.get("tag_oid") not in (None, tag_oid):
raise ManualOwnerReviewRequired(f"{name} tag object mismatch")
return dict(probe)
def execute(
*,
binding: Binding,
authorization_path: Path,
repository: Path,
state_root: Path,
tag_command: Sequence[str],
deploy_command: Sequence[str],
registry_command: Sequence[str],
baseline_command: Sequence[str],
) -> dict[str, Any]:
authorization = verify_authorization(secure_json(authorization_path), binding)
lineage = verify_git_lineage(repository, binding)
artifact = verify_artifact(binding)
state = DurableState(state_root, binding)
with FileLock(state_root / "executor.lock"):
state.record(
"PREFLIGHT_VERIFIED",
"preflight",
{"authorization": authorization, "lineage": lineage, "artifact": artifact},
)
tag: dict[str, Any] | None = None
runtime: dict[str, Any] | None = None
try:
tag = tag_step(tag_command, binding)
state.record("TAG_VERIFIED", "tag", tag)
runtime = deploy_step(deploy_command, binding)
state.record("RUNTIME_VERIFIED", "runtime", runtime)
registry = finalize_step(
registry_command,
binding,
name="registry",
tag_oid=tag["tag_oid"],
)
state.record("REGISTRY_VERIFIED", "registry", registry)
baseline = finalize_step(
baseline_command,
binding,
name="baseline",
tag_oid=tag["tag_oid"],
)
state.record("BASELINE_VERIFIED", "baseline", baseline)
# Final readback: no mutation, all four planes must still agree.
final_tag = tag_step(tag_command, binding)
final_runtime = run_adapter(deploy_command, binding.request("probe"))
final_registry_request = binding.request("probe")
final_registry_request["tag_oid"] = tag["tag_oid"]
final_registry = run_adapter(registry_command, final_registry_request)
final_baseline_request = binding.request("probe")
final_baseline_request["tag_oid"] = tag["tag_oid"]
final_baseline = run_adapter(baseline_command, final_baseline_request)
for value in (final_runtime, final_registry, final_baseline):
require_exact_response(value, binding)
if final_runtime.get("status") != "ACTIVE_VERIFIED":
raise ReconciliationRequired("final runtime 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 = {
"status": "PRODUCTION_ACCEPTED",
"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,
"tag_oid": tag["tag_oid"],
"tag": final_tag,
"runtime": final_runtime,
"registry": final_registry,
"baseline": final_baseline,
"force_push_used": False,
"tag_rewritten": False,
"direct_main_push": False,
"secrets_committed": False,
}
state.record("COMPLETED", "final_consistency", final)
return final
except ManualOwnerReviewRequired as exc:
state.fail("MANUAL_OWNER_REVIEW_REQUIRED", str(exc))
raise
except ReconciliationRequired as exc:
state.fail("RECONCILIATION_REQUIRED", str(exc))
raise
except Exception as exc:
# Once a formal tag has been verified, no later error is a clean
# pre-side-effect failure. Preserve the tag and require idempotent
# reconciliation rather than pretending the release never started.
if tag is not None:
reason = f"{type(exc).__name__}: {exc}"
state.fail("RECONCILIATION_REQUIRED", reason)
raise ReconciliationRequired(reason) from exc
state.fail("FAIL_CLOSED", f"{type(exc).__name__}: {exc}")
raise
def parser() -> argparse.ArgumentParser:
p = argparse.ArgumentParser()
p.add_argument("--authorization", required=True, type=Path)
p.add_argument("--repository", required=True, type=Path)
p.add_argument("--state-root", required=True, type=Path)
p.add_argument("--artifact", required=True, type=Path)
p.add_argument("--artifact-size", required=True, type=int)
p.add_argument("--version", required=True)
p.add_argument("--target-oid", required=True)
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("--production-service", required=True)
p.add_argument("--production-port", required=True, type=int)
p.add_argument("--repository-identity", default="aios/aios")
p.add_argument("--tag-adapter", required=True, nargs="+")
p.add_argument("--deploy-adapter", required=True, nargs="+")
p.add_argument("--registry-adapter", required=True, nargs="+")
p.add_argument("--baseline-adapter", required=True, nargs="+")
return p
def main() -> int:
args = parser().parse_args()
binding = Binding(
version=args.version,
target_oid=args.target_oid,
evidence_closure_oid=args.evidence_closure_oid,
artifact_sha256=args.artifact_sha256,
approval_plan_id=args.approval_plan_id,
artifact_path=str(args.artifact),
artifact_size=args.artifact_size,
production_service=args.production_service,
production_port=args.production_port,
repository_identity=args.repository_identity,
)
try:
result = execute(
binding=binding,
authorization_path=args.authorization,
repository=args.repository,
state_root=args.state_root,
tag_command=parse_command(args.tag_adapter),
deploy_command=parse_command(args.deploy_adapter),
registry_command=parse_command(args.registry_adapter),
baseline_command=parse_command(args.baseline_adapter),
)
print(json.dumps(result, sort_keys=True))
return 0
except ManualOwnerReviewRequired as exc:
print(json.dumps({"status": "MANUAL_OWNER_REVIEW_REQUIRED", "reason": str(exc)}, sort_keys=True))
return 20
except ReconciliationRequired as exc:
print(json.dumps({"status": "RECONCILIATION_REQUIRED", "reason": str(exc)}, sort_keys=True))
return 21
except Exception as exc:
print(json.dumps({"status": "FAIL_CLOSED", "error": type(exc).__name__, "reason": str(exc)}, sort_keys=True))
return 22
if __name__ == "__main__":
raise SystemExit(main())
+65 -2
View File
@@ -4,6 +4,7 @@ from __future__ import annotations
import os import os
import re import re
import subprocess import subprocess
import time
import urllib.parse import urllib.parse
from pathlib import Path from pathlib import Path
from typing import Iterable from typing import Iterable
@@ -306,14 +307,76 @@ def authoritative_diff(
from_oid = validate_git_revision(from_revision, "from", git_dir=path) from_oid = validate_git_revision(from_revision, "from", git_dir=path)
to_oid = validate_git_revision(to_revision, "to", git_dir=path) to_oid = validate_git_revision(to_revision, "to", git_dir=path)
args = ["diff", "--no-ext-diff", "--no-textconv", from_oid, to_oid] args = ["diff", "--no-ext-diff", "--no-textconv", from_oid, to_oid]
result = run_authoritative_git(args, git_dir=path) patch_limit = max(1, min(_MAX_OUTPUT, 256 * 1024))
process = subprocess.Popen(
["git", "--git-dir", str(path), *args],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
env={**os.environ, "GIT_OPTIONAL_LOCKS": "0"},
)
chunks: list[bytes] = []
stored_bytes = 0
total_bytes = 0
started = time.monotonic()
try:
while process.stdout is not None:
chunk = process.stdout.read(64 * 1024)
if not chunk:
break
total_bytes += len(chunk)
if stored_bytes < patch_limit:
remaining = patch_limit - stored_bytes
selected = chunk[:remaining]
chunks.append(selected)
stored_bytes += len(selected)
if time.monotonic() - started > _TIMEOUT:
process.kill()
raise AuthoritativeGitError("GIT_REF_QUERY_TIMEOUT")
stderr = (
process.stderr.read(8192).decode("utf-8", errors="replace")
if process.stderr is not None
else ""
)
returncode = process.wait(timeout=max(0.1, _TIMEOUT - (time.monotonic() - started)))
except (OSError, subprocess.TimeoutExpired) as exc:
process.kill()
process.wait()
raise AuthoritativeGitError("GIT_REF_QUERY_FAILED") from exc
if returncode != 0:
raise AuthoritativeGitError(_redact(stderr.strip() or "GIT_REF_QUERY_FAILED")[:200])
try:
names_result = run_authoritative_git(
["diff", "--no-ext-diff", "--no-textconv", "--name-status", from_oid, to_oid],
git_dir=path,
)
changed_paths = [
line.split("\t", 1)[-1]
for line in names_result.stdout.splitlines()
if line.strip()
]
except AuthoritativeGitOutputTooLarge:
changed_paths = []
try:
stats_result = run_authoritative_git(
["diff", "--no-ext-diff", "--no-textconv", "--stat", from_oid, to_oid],
git_dir=path,
)
stats = stats_result.stdout.strip()
except AuthoritativeGitOutputTooLarge:
stats = "GIT_STAT_OUTPUT_TRUNCATED"
patch = b"".join(chunks).decode("utf-8", errors="replace")
return { return {
"code": 0, "code": 0,
"stdout": result.stdout, "stdout": patch,
"stderr": "", "stderr": "",
"cmd": ["git", f"--git-dir={path}", *args], "cmd": ["git", f"--git-dir={path}", *args],
"from_oid": from_oid, "from_oid": from_oid,
"to_oid": to_oid, "to_oid": to_oid,
"changed_paths": changed_paths,
"stats": stats,
"total_size_bytes": total_bytes,
"truncated": total_bytes > patch_limit,
"patch_limit_bytes": patch_limit,
} }
@@ -951,3 +951,51 @@ class AuthorizedTaskWriterProvisioner:
return {**public_result, "prestart": prestart} return {**public_result, "prestart": prestart}
finally: finally:
shutil.rmtree(temporary_root, ignore_errors=True) shutil.rmtree(temporary_root, ignore_errors=True)
# v2 continuation validation is intentionally separate from the legacy Owner
# bootstrap path above. It consumes a recovered campaign authorization and
# never creates a branch or asks the Owner to authorize again.
from aios_continuation_lease import WriterLeaseRequestV2
def validate_writer_lease_request_v2(raw: Mapping[str, Any], probe: Any) -> dict[str, Any]:
"""Validate a CONTINUE_EXISTING request against the live exact branch head."""
required = {
"schema",
"task_id",
"repository",
"branch_mode",
"base_oid",
"expected_branch_oid",
"target_branch",
"generation",
"lease_seconds",
}
if set(raw) != required or raw.get("schema") not in WriterLeaseRequestV2.accepted_schemas:
raise ProvisioningError("LEASE_REQUEST_V2_FIELDS_REJECTED", 409)
request = WriterLeaseRequestV2(
task_id=str(raw["task_id"]),
repository=str(raw["repository"]),
branch_mode=str(raw["branch_mode"]),
base_oid=str(raw["base_oid"]),
expected_branch_oid=str(raw["expected_branch_oid"]),
target_branch=str(raw["target_branch"]),
generation=raw["generation"],
lease_seconds=raw["lease_seconds"],
)
request.validate(probe)
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,
"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,
}
+84
View File
@@ -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
+108
View File
@@ -0,0 +1,108 @@
"""Atomic, predicate-bound activation of the durable release campaign."""
from __future__ import annotations
import hashlib
import json
import os
from pathlib import Path
import re
import tempfile
from typing import Any, Mapping
from release_campaign_authorization import CampaignError, validate_policy
OID_RE = re.compile(r"^[0-9a-f]{40}$")
CAMPAIGN_ID = "AIOS-AUTONOMOUS-RELEASE-CAMPAIGN-20260807"
class CampaignActivationError(CampaignError):
pass
def canonical_json(value: Any) -> bytes:
return (json.dumps(value, sort_keys=True, separators=(",", ":"), ensure_ascii=False) + "\n").encode("utf-8")
def digest_bytes(value: bytes) -> str:
return hashlib.sha256(value).hexdigest()
def build_active_policy(current: Mapping[str, Any], facts: Mapping[str, Any]) -> dict[str, Any]:
policy = validate_policy(dict(current))
required = {
"campaign_id",
"repository",
"source_main_oid",
"gitea_main_oid",
"authority_main_oid",
"runtime_source_oid",
"durable_job_state",
"writer_ready",
"gitea_machine_authenticated",
"governance_executor_ready",
"preapproval_publisher_ready",
"runtime_exact_main",
"main_lineage_verified",
"campaign_revoked",
}
if set(facts) != required:
raise CampaignActivationError("ACTIVATION_FACTS_SCHEMA_INVALID")
if policy["activation_state"] != "PENDING_CONTROL_PLANE_MERGE" or policy["active"] is not False:
raise CampaignActivationError("CAMPAIGN_NOT_PENDING")
if policy["campaign_id"] != CAMPAIGN_ID or facts["campaign_id"] != CAMPAIGN_ID:
raise CampaignActivationError("CAMPAIGN_ID_MISMATCH")
if facts["repository"] != "aios/aios" or policy["repository"] != facts["repository"]:
raise CampaignActivationError("REPOSITORY_MISMATCH")
for name in ("source_main_oid", "gitea_main_oid", "authority_main_oid", "runtime_source_oid"):
if not isinstance(facts[name], str) or not OID_RE.fullmatch(facts[name]):
raise CampaignActivationError("ACTIVATION_OID_INVALID")
if not (
facts["source_main_oid"]
== facts["gitea_main_oid"]
== facts["authority_main_oid"]
== facts["runtime_source_oid"]
):
raise CampaignActivationError("ACTIVATION_MAIN_DIVERGENCE")
if facts["durable_job_state"] != "COMPLETED":
raise CampaignActivationError("DURABLE_JOB_NOT_COMPLETED")
boolean_fields = (
"writer_ready",
"gitea_machine_authenticated",
"governance_executor_ready",
"preapproval_publisher_ready",
"runtime_exact_main",
"main_lineage_verified",
)
if any(facts[name] is not True for name in boolean_fields) or facts["campaign_revoked"] is not False:
raise CampaignActivationError("ACTIVATION_PREDICATE_FAILED")
activated = dict(policy)
activated["activation_state"] = "ACTIVE"
activated["active"] = True
return validate_policy(activated)
def atomic_write_policy(path: Path, sidecar: Path, receipt_path: Path, policy: Mapping[str, Any], receipt: Mapping[str, Any]) -> str:
if path.is_symlink() or sidecar.is_symlink() or receipt_path.exists() or receipt_path.is_symlink():
raise CampaignActivationError("ACTIVATION_PATH_INVALID")
path.parent.mkdir(parents=True, exist_ok=True)
raw = canonical_json(dict(policy))
digest = digest_bytes(raw)
for target, data, mode in (
(path, raw, "wb"),
(sidecar, f"{digest} {path.name}\n".encode("ascii"), "wb"),
(receipt_path, canonical_json(dict(receipt)), "wb"),
):
fd, temporary = tempfile.mkstemp(prefix=f".{target.name}.", dir=target.parent)
try:
os.fchmod(fd, 0o600)
with os.fdopen(fd, mode) as stream:
stream.write(data)
stream.flush()
os.fsync(stream.fileno())
os.replace(temporary, target)
os.chmod(target, 0o600)
finally:
if os.path.exists(temporary):
os.unlink(temporary)
return digest
+106
View File
@@ -0,0 +1,106 @@
#!/usr/bin/env python3
"""Machine campaign approval gate for protected engineering PRs."""
from __future__ import annotations
import argparse
import json
from pathlib import Path
import subprocess
from typing import Any, Mapping
from release_campaign_authorization import CampaignAuthorization, CampaignError
class GateError(RuntimeError):
pass
def git_is_ancestor(repository: Path, ancestor: str, descendant: str) -> bool:
result = subprocess.run(
[
"git",
f"--git-dir={repository}",
"merge-base",
"--is-ancestor",
ancestor,
descendant,
],
check=False,
capture_output=True,
)
if result.returncode == 0:
return True
if result.returncode == 1:
return False
raise GateError("git_ancestor_check_failed")
def evaluate(
campaign: CampaignAuthorization,
facts: Mapping[str, Any],
repository: Path,
) -> dict[str, Any]:
required = {
"repository",
"pr_number",
"base_ref",
"base_oid",
"head_oid",
"required_checks_all_success",
"required_checks_same_head",
"unresolved_conversations",
"mergeable",
"protected_target",
}
if set(facts) != required:
raise GateError("pr_facts_fields_invalid")
if facts["repository"] != "aios/aios":
raise GateError("repository_mismatch")
if facts["base_ref"] != "refs/heads/main" or facts["protected_target"] != "main":
raise GateError("protected_main_required")
if facts["required_checks_all_success"] is not True:
raise GateError("required_checks_not_success")
if facts["required_checks_same_head"] is not True:
raise GateError("required_checks_stale")
if facts["unresolved_conversations"] != 0:
raise GateError("unresolved_conversations")
if facts["mergeable"] is not True:
raise GateError("pr_not_mergeable")
if not git_is_ancestor(repository, campaign.anchor_oid, str(facts["base_oid"])):
raise GateError("base_outside_campaign_lineage")
if not git_is_ancestor(repository, str(facts["base_oid"]), str(facts["head_oid"])):
raise GateError("head_outside_pr_lineage")
campaign.assert_operation("protected_merge")
return {
"schema": "aios.campaign-approval-gate-result.v1",
"status": "SUCCESS",
"campaign_id": campaign.campaign_id,
"pr_number": facts["pr_number"],
"head_oid": facts["head_oid"],
"owner_ui_interaction_required": False,
}
def main() -> int:
p = argparse.ArgumentParser()
p.add_argument("--campaign", type=Path, required=True)
p.add_argument("--revocation", type=Path)
p.add_argument("--pr-facts", type=Path, required=True)
p.add_argument("--repository", type=Path, required=True)
a = p.parse_args()
campaign = CampaignAuthorization.load(
a.campaign,
revocation=a.revocation,
require_active=True,
)
facts = json.loads(a.pr_facts.read_text(encoding="utf-8"))
print(json.dumps(evaluate(campaign, facts, a.repository), sort_keys=True))
return 0
if __name__ == "__main__":
try:
raise SystemExit(main())
except (CampaignError, GateError) as exc:
print(json.dumps({"status": "FAILURE", "reason": str(exc)}, sort_keys=True))
raise SystemExit(2)
+150
View File
@@ -0,0 +1,150 @@
#!/usr/bin/env python3
"""Automatically accept an exact release binding under an ACTIVE campaign."""
from __future__ import annotations
import argparse
import hashlib
import json
import os
from pathlib import Path
import stat
import tempfile
from typing import Any, Mapping
from release_campaign_authorization import CampaignAuthorization, CampaignError
class AcceptanceError(RuntimeError):
pass
def secure_json(path: Path) -> dict[str, Any]:
if path.is_symlink() or not path.is_file():
raise AcceptanceError(f"invalid_file:{path}")
st = path.lstat()
if st.st_uid != 0 or stat.S_IMODE(st.st_mode) & 0o022:
raise AcceptanceError(f"unsafe_file:{path}")
value = json.loads(path.read_text(encoding="utf-8"))
if not isinstance(value, dict):
raise AcceptanceError(f"json_object_required:{path}")
return value
def atomic_new(path: Path, value: Mapping[str, Any]) -> None:
if path.exists() or path.is_symlink():
raise AcceptanceError("acceptance_receipt_exists")
path.parent.mkdir(parents=True, exist_ok=True)
fd, name = tempfile.mkstemp(prefix=f".{path.name}.", dir=path.parent)
try:
os.fchmod(fd, 0o600)
if os.geteuid() == 0:
os.fchown(fd, 0, 0)
with os.fdopen(fd, "w", encoding="utf-8", closefd=True) as stream:
json.dump(dict(value), stream, sort_keys=True, indent=2)
stream.write("\n")
stream.flush()
os.fsync(stream.fileno())
os.replace(name, path)
os.chmod(path, 0o600)
finally:
try:
os.unlink(name)
except FileNotFoundError:
pass
def evaluate(
campaign: CampaignAuthorization,
binding: Mapping[str, Any],
evidence: Mapping[str, Any],
artifact: Mapping[str, Any],
staging: Mapping[str, Any],
main: Mapping[str, Any],
tag: Mapping[str, Any],
) -> dict[str, Any]:
required_binding = {
"version",
"target_oid",
"evidence_closure_oid",
"artifact_sha256",
"artifact_size",
"approval_plan_id",
}
if set(binding) != required_binding:
raise AcceptanceError("binding_fields_invalid")
if main.get("repository") != "aios/aios":
raise AcceptanceError("repository_identity_mismatch")
if main.get("target_in_main_lineage") is not True:
raise AcceptanceError("target_outside_main_lineage")
if evidence.get("object_type") != "commit":
raise AcceptanceError("evidence_not_commit")
if evidence.get("target_is_ancestor") is not True:
raise AcceptanceError("target_not_evidence_ancestor")
if evidence.get("closure_oid") != binding["evidence_closure_oid"]:
raise AcceptanceError("evidence_oid_mismatch")
if artifact.get("sha256") != binding["artifact_sha256"]:
raise AcceptanceError("artifact_digest_mismatch")
if artifact.get("size") != binding["artifact_size"]:
raise AcceptanceError("artifact_size_mismatch")
if artifact.get("retained") is not True:
raise AcceptanceError("artifact_not_retained")
if staging.get("install") != "PASS":
raise AcceptanceError("staging_install_failed")
if staging.get("health") != "PASS":
raise AcceptanceError("staging_health_failed")
if staging.get("restart") != "PASS":
raise AcceptanceError("staging_restart_failed")
if staging.get("rollback_readiness") != "PASS":
raise AcceptanceError("rollback_readiness_failed")
if tag.get("conflict") is not False:
raise AcceptanceError("tag_conflict")
campaign.assert_operation("derive_campaign_acceptance")
return {
"schema": "aios.campaign-release-acceptance.v1",
"accepted": True,
"binding_replaced": False,
"campaign_id": campaign.campaign_id,
"campaign_generation": campaign.data["generation"],
**dict(binding),
"owner_interaction_required": False,
}
def main() -> int:
p = argparse.ArgumentParser()
p.add_argument("--campaign", type=Path, required=True)
p.add_argument("--revocation", type=Path)
p.add_argument("--binding", type=Path, required=True)
p.add_argument("--evidence", type=Path, required=True)
p.add_argument("--artifact", type=Path, required=True)
p.add_argument("--staging", type=Path, required=True)
p.add_argument("--main-facts", type=Path, required=True)
p.add_argument("--tag-facts", type=Path, required=True)
p.add_argument("--output", type=Path, required=True)
a = p.parse_args()
campaign = CampaignAuthorization.load(
a.campaign,
revocation=a.revocation,
require_active=True,
)
receipt = evaluate(
campaign,
secure_json(a.binding),
secure_json(a.evidence),
secure_json(a.artifact),
secure_json(a.staging),
secure_json(a.main_facts),
secure_json(a.tag_facts),
)
atomic_new(a.output, receipt)
print(json.dumps(receipt, sort_keys=True))
return 0
if __name__ == "__main__":
try:
raise SystemExit(main())
except (CampaignError, AcceptanceError) as exc:
print(json.dumps({"status": "BLOCKED", "reason": str(exc)}, sort_keys=True))
raise SystemExit(2)
+16 -7
View File
@@ -8,6 +8,7 @@ import json
import os import os
import re import re
import subprocess import subprocess
import sys
import tempfile import tempfile
import time import time
import urllib.error import urllib.error
@@ -23,10 +24,15 @@ try:
from authoritative_git import AuthoritativeGitError, run_authoritative_git from authoritative_git import AuthoritativeGitError, run_authoritative_git
except ModuleNotFoundError: except ModuleNotFoundError:
from .authoritative_git import AuthoritativeGitError, run_authoritative_git from .authoritative_git import AuthoritativeGitError, run_authoritative_git
try:
from release_version import RELEASE_VERSION_RE, validate_release_version
except ModuleNotFoundError:
sys.path.insert(0, str(Path(__file__).resolve().parents[2]))
from release_version import RELEASE_VERSION_RE, validate_release_version
OID_RE = re.compile(r"^[0-9a-f]{40}$") OID_RE = re.compile(r"^[0-9a-f]{40}$")
VERSION_RE = re.compile(r"^V\d+R\d+C\d+(?:-[A-Za-z0-9_.-]+)?$") VERSION_RE = RELEASE_VERSION_RE
DEPLOYMENT_EVIDENCE_RE = re.compile(r"^(?:sha256:[0-9a-f]{64}|git:[0-9a-f]{40})$") DEPLOYMENT_EVIDENCE_RE = re.compile(r"^(?:sha256:[0-9a-f]{64}|git:[0-9a-f]{40})$")
FINGERPRINT_FIELDS = ( FINGERPRINT_FIELDS = (
"version", "version",
@@ -83,9 +89,14 @@ class PublicationManualOwnerReviewRequired(
def validate_version(version: str) -> str: def validate_version(version: str) -> str:
value = str(version or "") return validate_release_version(version)
if not VERSION_RE.fullmatch(value):
raise ValueError("invalid release version")
def validate_gitea_api_url(api_url: str) -> str:
value = str(api_url or "").strip().rstrip("/")
parsed = urllib.parse.urlsplit(value)
if parsed.scheme not in {"http", "https"} or not parsed.netloc:
raise ValueError("Gitea API URL must be HTTP(S)")
return value return value
@@ -411,13 +422,11 @@ class GiteaTagClient:
timeout: float = 15, timeout: float = 15,
token_line: int = 1, token_line: int = 1,
): ):
self.api_url = api_url.rstrip("/") self.api_url = validate_gitea_api_url(api_url)
self.repository = repository.strip("/") self.repository = repository.strip("/")
self.token_file = token_file self.token_file = token_file
self.timeout = timeout self.timeout = timeout
self.token_line = token_line self.token_line = token_line
if not self.api_url.startswith(("https://", "http://")):
raise ValueError("Gitea API URL must be HTTP(S)")
if self.repository.count("/") != 1: if self.repository.count("/") != 1:
raise ValueError("Gitea repository must be owner/name") raise ValueError("Gitea repository must be owner/name")
@@ -0,0 +1 @@
"""Permanent AIOS governance source components."""
@@ -0,0 +1,75 @@
#!/usr/bin/env python3
import json
import pathlib
import socket
import time
SOCKET = '/run/aios-governance-executor/executor.sock'
STATE = pathlib.Path('/var/lib/aios-gitops/governance-executor/drift-monitor.json')
APPROVED = pathlib.Path('/var/lib/aios-gitops/runtime/tasks/AIOS-PROTECTION-SNAPSHOT-DRIFT-FORENSICS-V1/protection-drift/semantic-analysis.json')
def call():
sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
sock.settimeout(20)
sock.connect(SOCKET)
request = {'operation': 'GET_BRANCH_PROTECTION', 'payload': {'repository': 'aios/aios', 'branch': 'main'}}
sock.sendall((json.dumps(request, separators=(',', ':')) + '\n').encode())
chunks = []
while True:
part = sock.recv(1024 * 1024)
if not part:
break
chunks.append(part)
if b'\n' in part:
break
sock.close()
return json.loads(b''.join(chunks).splitlines()[0])
def main():
try:
result = call()
if result.get('status') != 'PASS':
raise RuntimeError(str(result.get('code') or 'EXECUTOR_GET_FAILED'))
approved = json.loads(APPROVED.read_text(encoding='utf-8'))
approved_transport = approved['approved_transport_digest_from_evidence']
approved_semantic = approved['approved_semantic_digest']
transport = result['transport_digest']
semantic = result['semantic_digest']
if semantic != approved_semantic:
state = 'GOVERNANCE_POLICY_DRIFT'
elif transport != approved_transport:
state = 'GOVERNANCE_TRANSPORT_METADATA_CHANGED'
else:
state = 'NO_DRIFT'
payload = {
'schema': 'aios.governance-drift-monitor.v1',
'observed_at_unix': int(time.time()),
'status': state,
'release_blocking': False,
'approved_transport_digest': approved_transport,
'observed_transport_digest': transport,
'approved_semantic_digest': approved_semantic,
'observed_semantic_digest': semantic,
'semantic_schema': result.get('semantic_schema'),
'source': 'Dedicated Governance Executor GET_BRANCH_PROTECTION',
}
except Exception as exc:
payload = {
'schema': 'aios.governance-drift-monitor.v1',
'observed_at_unix': int(time.time()),
'status': 'MONITOR_UNAVAILABLE',
'release_blocking': False,
'error_code': str(exc),
}
STATE.parent.mkdir(parents=True, exist_ok=True)
temporary = STATE.with_suffix('.tmp')
temporary.write_text(json.dumps(payload, sort_keys=True, separators=(',', ':'), ensure_ascii=False) + '\n', encoding='utf-8')
temporary.replace(STATE)
STATE.chmod(0o640)
print(json.dumps(payload, sort_keys=True, separators=(',', ':'), ensure_ascii=False))
if __name__ == '__main__':
main()
+417
View File
@@ -0,0 +1,417 @@
#!/usr/bin/env python3
import base64
import copy
import hashlib
import json
import os
import pathlib
import socket
import time
import urllib.error
import urllib.parse
import urllib.request
SOURCE_GOVERNANCE = "/opt/aios/aios-control-plane/current/active-runtime/app/governance"
if SOURCE_GOVERNANCE not in __import__("sys").path:
__import__("sys").path.insert(0, SOURCE_GOVERNANCE)
from stage2_gate_migration import (
CAMPAIGN_GATE,
CAMPAIGN_ID,
OWNER_GATE,
STAGE1_SEMANTIC_SHA,
Stage2GateError,
build_target,
semantic_digest as stage2_semantic_digest,
validate_stage2_facts,
)
API = "http://127.0.0.1:3000"
REPO = "aios/aios"
SOCKET = "/run/aios-governance-executor/executor.sock"
JOURNAL = pathlib.Path("/var/lib/aios-gitops/governance-executor/executor-journal.jsonl")
RECONCILIATION_BINDING_PATH = pathlib.Path("/var/lib/aios-gitops/governance-executor/reconciliation-binding.json")
PR54_HEAD = "015185d7e5191d18c5c1596160807ac228782a0a"
PR54_REVIEW = 104
PR54_MANIFEST_SHA = "1b11ee42d53a9f6c1646e5f5ddd8ce5b7ecf301483b9f3d47a068290d541ba4b"
PR54_PREVIOUS_PROTECTION_SHA = "8b3370add21cd23c2b4f41d8d84d3eaaa09ddf0451549b72c7941bbd92d5d60a"
SEMANTIC_SCHEMA = "aios.semantic-protection.v1"
APPROVED_SEMANTIC_SHA = "ba15d512ed8c1f43e5f72c84125a3a71a7347ef59e18000bbfca24d6e69f1b19"
STAGE1_SEMANTIC_SHA = "d911e856f71de6105b8efa08e13338a448e0b6d37536332decd80c9f409ecf27"
USER_IDS = {"owner": 2, "aios-autonomy-agent": 153}
def canon(value):
return json.dumps(value, sort_keys=True, separators=(",", ":"), ensure_ascii=False).encode()
def sha(value):
return hashlib.sha256(canon(value)).hexdigest()
def token():
value = (pathlib.Path(os.environ["CREDENTIALS_DIRECTORY"]) / "gitea-governance-executor-token").read_text().strip()
if len(value) < 16:
raise RuntimeError("EXECUTOR_CREDENTIAL_INVALID")
return value
def api(method, path, value, body=None, expected=(200,)):
raw = None if body is None else json.dumps(body, separators=(",", ":")).encode()
req = urllib.request.Request(API + path, data=raw, method=method)
req.add_header("Authorization", "token " + value)
req.add_header("Accept", "application/json")
if raw is not None:
req.add_header("Content-Type", "application/json")
try:
with urllib.request.urlopen(req, timeout=20) as response:
status = response.status
payload = response.read()
except urllib.error.HTTPError as exc:
if exc.code in expected:
payload = exc.read()
return exc.code, json.loads(payload) if payload else None
raise RuntimeError("GITEA_HTTP_" + str(exc.code))
if status not in expected:
raise RuntimeError("GITEA_HTTP_" + str(status))
if not payload or not payload.strip():
raise RuntimeError("GITEA_EMPTY_JSON:" + path)
try:
decoded = json.loads(payload)
except (UnicodeDecodeError, json.JSONDecodeError) as exc:
raise RuntimeError("GITEA_INVALID_JSON:" + path) from exc
return status, decoded
def journal(event, **fields):
JOURNAL.parent.mkdir(parents=True, exist_ok=True)
record = {"schema": "aios.governance-executor-journal.v2", "event": event, "unix": int(time.time()), **fields}
with JOURNAL.open("a", encoding="utf-8") as stream:
stream.write(json.dumps(record, sort_keys=True, separators=(",", ":"), ensure_ascii=False) + "\n")
stream.flush()
os.fsync(stream.fileno())
def typed_json(value):
if value in (None, "null"):
return []
if isinstance(value, str):
return json.loads(value)
return value
def user_ids(names):
result = []
for name in names or []:
if name not in USER_IDS:
raise RuntimeError("UNKNOWN_GOVERNANCE_USER:" + str(name))
result.append(USER_IDS[name])
return sorted(result)
def semantic_policy(value):
policy = {
"schema_version": SEMANTIC_SCHEMA,
"repo_id": 1,
"protection_id": 2,
"branch_name": value.get("branch_name"),
"can_push": value.get("enable_push"),
"can_force_push": value.get("enable_force_push"),
"enable_merge_whitelist": value.get("enable_merge_whitelist"),
"merge_whitelist_user_ids": user_ids(value.get("merge_whitelist_usernames")),
"merge_whitelist_team_ids": [],
"required_approvals": value.get("required_approvals"),
"enable_status_check": value.get("enable_status_check"),
"status_check_contexts": sorted(value.get("status_check_contexts") or []),
"block_admin_merge_override": value.get("block_admin_merge_override"),
"block_on_official_review_requests": value.get("block_on_official_review_requests"),
"block_on_outdated_branch": value.get("block_on_outdated_branch"),
"block_on_rejected_reviews": value.get("block_on_rejected_reviews"),
"dismiss_stale_approvals": value.get("dismiss_stale_approvals"),
"ignore_stale_approvals": value.get("ignore_stale_approvals"),
"enable_approvals_whitelist": value.get("enable_approvals_whitelist"),
"approvals_whitelist_user_ids": user_ids(value.get("approvals_whitelist_username")),
"approvals_whitelist_team_ids": [],
"enable_bypass_allowlist": value.get("enable_bypass_allowlist"),
"bypass_allowlist_user_ids": user_ids(value.get("bypass_allowlist_usernames")),
"bypass_allowlist_team_ids": [],
"enable_force_push_allowlist": value.get("enable_force_push_allowlist"),
"force_push_allowlist_user_ids": user_ids(value.get("force_push_allowlist_usernames")),
"force_push_allowlist_team_ids": [],
"enable_whitelist": value.get("enable_push_whitelist"),
"whitelist_user_ids": user_ids(value.get("push_whitelist_usernames")),
"whitelist_team_ids": [],
"force_push_allowlist_deploy_keys": value.get("force_push_allowlist_deploy_keys"),
"whitelist_deploy_keys": value.get("push_whitelist_deploy_keys"),
"require_signed_commits": value.get("require_signed_commits"),
"priority": value.get("priority"),
"protected_file_patterns": value.get("protected_file_patterns"),
"unprotected_file_patterns": value.get("unprotected_file_patterns"),
}
return policy
def semantic_sha(value):
return sha(semantic_policy(value))
def verify_binding(binding, value):
required = {
"governance_request_PR", "exact_request_head", "Owner_review_id", "Owner_review_exact_head",
"manifest_sha256", "expected_previous_snapshot_sha256", "approved_delta",
"semantic_reconciliation_request_id", "semantic_reconciliation_head",
"semantic_reconciliation_review_id", "semantic_reconciliation_review_head",
"approved_semantic_digest",
}
if set(binding) != required:
raise RuntimeError("GOVERNANCE_BINDING_SCHEMA_INVALID")
if binding["governance_request_PR"] != 54 or binding["exact_request_head"] != PR54_HEAD:
raise RuntimeError("GOVERNANCE_BINDING_PR54_MISMATCH")
if binding["Owner_review_id"] != PR54_REVIEW or binding["Owner_review_exact_head"] != PR54_HEAD:
raise RuntimeError("GOVERNANCE_BINDING_REVIEW_MISMATCH")
if binding["manifest_sha256"] != PR54_MANIFEST_SHA:
raise RuntimeError("GOVERNANCE_BINDING_MANIFEST_MISMATCH")
if binding["expected_previous_snapshot_sha256"] != PR54_PREVIOUS_PROTECTION_SHA:
raise RuntimeError("GOVERNANCE_BINDING_SNAPSHOT_MISMATCH")
if binding["approved_delta"] != {"add_merge_whitelist_user_id": 153}:
raise RuntimeError("GOVERNANCE_BINDING_DELTA_DENIED")
if binding["approved_semantic_digest"] != APPROVED_SEMANTIC_SHA:
raise RuntimeError("GOVERNANCE_BINDING_SEMANTIC_DIGEST_MISMATCH")
if not RECONCILIATION_BINDING_PATH.is_file():
raise RuntimeError("SEMANTIC_RECONCILIATION_BINDING_REQUIRED")
try:
reconciliation = json.loads(RECONCILIATION_BINDING_PATH.read_text(encoding="utf-8"))
except (OSError, ValueError):
raise RuntimeError("SEMANTIC_RECONCILIATION_BINDING_INVALID") from None
required_reconciliation = {
"schema", "request_id", "pr_number", "request_head", "owner_review_id",
"owner_review_head", "approved_semantic_digest", "approved_delta",
}
if set(reconciliation) != required_reconciliation or reconciliation.get("schema") != "aios.semantic-reconciliation-binding.v1":
raise RuntimeError("SEMANTIC_RECONCILIATION_BINDING_INVALID")
if reconciliation["approved_semantic_digest"] != APPROVED_SEMANTIC_SHA or reconciliation["approved_delta"] != binding["approved_delta"]:
raise RuntimeError("SEMANTIC_RECONCILIATION_BINDING_MISMATCH")
if (
reconciliation["request_id"] != binding["semantic_reconciliation_request_id"]
or reconciliation["request_head"] != binding["semantic_reconciliation_head"]
or reconciliation["owner_review_id"] != binding["semantic_reconciliation_review_id"]
or reconciliation["owner_review_head"] != binding["semantic_reconciliation_review_head"]
):
raise RuntimeError("SEMANTIC_RECONCILIATION_BINDING_MISMATCH")
_, reconciliation_pr = api("GET", f"/api/v1/repos/{REPO}/pulls/{reconciliation['pr_number']}", value)
if (
reconciliation_pr.get("state") != "open"
or reconciliation_pr.get("merged")
or reconciliation_pr.get("head", {}).get("sha") != reconciliation["request_head"]
or reconciliation_pr.get("base", {}).get("ref") != "governance/approvals"
):
raise RuntimeError("SEMANTIC_RECONCILIATION_REQUEST_STALE")
_, reconciliation_reviews = api("GET", f"/api/v1/repos/{REPO}/pulls/{reconciliation['pr_number']}/reviews", value)
reconciliation_review = next((x for x in reconciliation_reviews if x.get("id") == reconciliation["owner_review_id"]), None)
if (
not reconciliation_review
or reconciliation_review.get("user", {}).get("id") != 2
or reconciliation_review.get("state") != "APPROVED"
or reconciliation_review.get("commit_id") != reconciliation["owner_review_head"]
or reconciliation_review.get("dismissed") is not False
):
raise RuntimeError("SEMANTIC_RECONCILIATION_OWNER_REVIEW_STALE")
_, pr = api("GET", f"/api/v1/repos/{REPO}/pulls/54", value)
if pr.get("state") != "open" or pr.get("merged") or pr.get("head", {}).get("sha") != PR54_HEAD:
raise RuntimeError("PR54_APPROVAL_STALE")
_, reviews = api("GET", f"/api/v1/repos/{REPO}/pulls/54/reviews", value)
review = next((x for x in reviews if x.get("id") == PR54_REVIEW), None)
if not review or review.get("user", {}).get("id") != 2 or review.get("state") != "APPROVED" or review.get("commit_id") != PR54_HEAD or review.get("dismissed") is not False:
raise RuntimeError("PR54_APPROVAL_STALE")
manifest_path = "approvals/governance-policy/AIOS-AUTONOMOUS-MERGE-GOVERNANCE-SCOPE-EXTENSION-V1.json"
_, content = api("GET", f"/api/v1/repos/{REPO}/contents/{urllib.parse.quote(manifest_path, safe='/')}?ref={PR54_HEAD}", value)
raw = base64.b64decode("".join(content.get("content", "").split()))
if hashlib.sha256(raw).hexdigest() != PR54_MANIFEST_SHA:
raise RuntimeError("PR54_MANIFEST_STALE")
def get_protection(value, branch):
if branch not in {"main", "governance/approvals"}:
raise RuntimeError("BRANCH_DENIED")
_, protection = api("GET", f"/api/v1/repos/{REPO}/branch_protections/{urllib.parse.quote(branch, safe='')}", value)
return protection
def mutation_receipt(before, after, binding, target_semantic):
return {
"before": {"transport_digest": sha(before), "semantic_digest": semantic_sha(before)},
"approved": {
"mutation_predicate_digest": sha({"operation": "ADD_MERGE_EXECUTOR", "subject_user_id": 153, "preserve_all_unmodified_security_fields": True}),
"Owner_review_id": binding["Owner_review_id"],
"semantic_policy_digest": APPROVED_SEMANTIC_SHA,
},
"after": {"transport_digest": sha(after), "semantic_digest": semantic_sha(after)},
"delta": {"add_merge_whitelist_user_id": 153},
"target_semantic_digest": sha(target_semantic),
"unmodified_security_fields": {"verified": True},
}
def apply_stage1(binding, value):
verify_binding(binding, value)
before = get_protection(value, "main")
before_semantic = semantic_policy(before)
before_semantic_sha = sha(before_semantic)
names = list(before.get("merge_whitelist_usernames") or [])
name_ids = user_ids(names)
if name_ids not in ([2], [2, 153]):
raise RuntimeError("PROTECTION_WHITELIST_PRESTATE_INVALID")
target_names = ["owner", "aios-autonomy-agent"]
target_semantic = copy.deepcopy(before_semantic)
target_semantic["merge_whitelist_user_ids"] = [2, 153]
if name_ids == [2, 153]:
if before_semantic_sha != STAGE1_SEMANTIC_SHA:
journal("APPLY_BLOCKED_STAGE1_IDEMPOTENCY_SEMANTIC_CONFLICT", transport_digest=sha(before), semantic_digest=before_semantic_sha, expected_semantic_digest=STAGE1_SEMANTIC_SHA)
raise RuntimeError("PROTECTION_STAGE1_SEMANTIC_CONFLICT")
receipt = mutation_receipt(before, before, binding, target_semantic)
journal("APPLY_APPROVED_PROTECTION_DELTA_IDEMPOTENT_SUCCESS", receipt=receipt)
return {"status": "IDEMPOTENT_SUCCESS", "protection": before, "transport_digest": sha(before), "semantic_digest": before_semantic_sha, "target_semantic_digest": sha(target_semantic)}
if before_semantic_sha != APPROVED_SEMANTIC_SHA:
journal("APPLY_BLOCKED_SEMANTIC_CAS", transport_digest=sha(before), semantic_digest=before_semantic_sha, expected_semantic_digest=APPROVED_SEMANTIC_SHA)
raise RuntimeError("PROTECTION_SEMANTIC_CAS_CONFLICT")
after_api = api("PATCH", f"/api/v1/repos/{REPO}/branch_protections/main", value, {"merge_whitelist_usernames": target_names})[1]
after = get_protection(value, "main")
for key in before:
if key in {"updated_at", "merge_whitelist_usernames"}:
continue
if after.get(key) != before.get(key):
raise RuntimeError("PROTECTION_UNAPPROVED_FIELD_CHANGED:" + key)
if user_ids(after.get("merge_whitelist_usernames")) != [2, 153]:
raise RuntimeError("PROTECTION_TARGET_READBACK_FAILED")
after_semantic = semantic_policy(after)
if after_semantic != target_semantic:
raise RuntimeError("PROTECTION_SEMANTIC_TARGET_READBACK_FAILED")
receipt = mutation_receipt(before, after, binding, target_semantic)
journal("APPLY_APPROVED_PROTECTION_DELTA_COMPLETED", receipt=receipt)
return {"status": "APPLIED", "protection": after, "previous_transport_digest": sha(before), "previous_semantic_digest": before_semantic_sha, "transport_digest": sha(after), "semantic_digest": sha(after_semantic), "receipt": receipt}
def apply_stage2(binding, facts, value):
verify_binding(binding, value)
validate_stage2_facts(facts)
before = get_protection(value, "main")
if OWNER_GATE in (before.get("status_check_contexts") or []) and stage2_semantic_digest(before) != STAGE1_SEMANTIC_SHA:
raise RuntimeError("STAGE2_SEMANTIC_PRECONDITION_INVALID")
try:
status, target = build_target(before)
except Stage2GateError as exc:
journal("MIGRATE_APPROVED_GATE_BLOCKED", reason=str(exc), semantic_digest=stage2_semantic_digest(before))
raise RuntimeError(str(exc)) from exc
target_digest = stage2_semantic_digest(target)
if status == "IDEMPOTENT_SUCCESS":
journal(
"MIGRATE_APPROVED_GATE_IDEMPOTENT_SUCCESS",
before_semantic_digest=stage2_semantic_digest(before),
target_semantic_digest=target_digest,
campaign_id=CAMPAIGN_ID,
)
return {
"status": status,
"protection": before,
"transport_digest": sha(before),
"semantic_digest": stage2_semantic_digest(before),
"target_semantic_digest": target_digest,
"gate_transition": {"removed": OWNER_GATE, "required": CAMPAIGN_GATE},
}
after_api = api(
"PATCH",
f"/api/v1/repos/{REPO}/branch_protections/main",
value,
{"status_check_contexts": target["status_check_contexts"]},
)[1]
after = get_protection(value, "main")
for key in before:
if key in {"updated_at", "status_check_contexts"}:
continue
if after.get(key) != before.get(key):
raise RuntimeError("PROTECTION_UNAPPROVED_FIELD_CHANGED:" + key)
if after.get("status_check_contexts") != target["status_check_contexts"]:
raise RuntimeError("STAGE2_GATE_TARGET_READBACK_FAILED")
if stage2_semantic_digest(after) != target_digest:
raise RuntimeError("STAGE2_SEMANTIC_TARGET_READBACK_FAILED")
receipt = {
"before_transport_digest": sha(before),
"before_semantic_digest": stage2_semantic_digest(before),
"after_transport_digest": sha(after),
"after_semantic_digest": stage2_semantic_digest(after),
"target_semantic_digest": target_digest,
"campaign_id": CAMPAIGN_ID,
"gate_transition": {"removed": OWNER_GATE, "required": CAMPAIGN_GATE},
"preserved_security_fields": True,
"facts_digest": sha(facts),
}
journal("MIGRATE_APPROVED_GATE_COMPLETED", receipt=receipt)
return {
"status": "APPLIED",
"protection": after,
"previous_transport_digest": sha(before),
"previous_semantic_digest": stage2_semantic_digest(before),
"transport_digest": sha(after),
"semantic_digest": stage2_semantic_digest(after),
"target_semantic_digest": target_digest,
"receipt": receipt,
}
def handle(request, value):
if not isinstance(request, dict) or set(request) != {"operation", "payload"}:
raise RuntimeError("REQUEST_SCHEMA_INVALID")
operation = request["operation"]
payload = request["payload"]
if not isinstance(payload, dict) or payload.get("repository") != REPO:
raise RuntimeError("REPOSITORY_DENIED")
if operation == "GET_BRANCH_PROTECTION":
protection = get_protection(value, payload.get("branch"))
journal("GET_BRANCH_PROTECTION", branch=payload.get("branch"), transport_digest=sha(protection), semantic_digest=semantic_sha(protection))
return {"status": "PASS", "protection": protection, "snapshot_sha256": sha(protection), "transport_digest": sha(protection), "semantic_digest": semantic_sha(protection), "semantic_schema": SEMANTIC_SCHEMA}
if operation == "READBACK_BRANCH_PROTECTION":
protection = get_protection(value, payload.get("branch"))
return {"status": "PASS", "protection": protection, "snapshot_sha256": sha(protection), "transport_digest": sha(protection), "semantic_digest": semantic_sha(protection), "semantic_schema": SEMANTIC_SCHEMA}
if operation == "APPLY_APPROVED_PROTECTION_DELTA":
binding = payload.get("GovernanceExecutionBinding")
if not isinstance(binding, dict):
raise RuntimeError("GOVERNANCE_BINDING_REQUIRED")
return apply_stage1(binding, value)
if operation == "MIGRATE_APPROVED_GATE":
binding = payload.get("GovernanceExecutionBinding")
facts = payload.get("Stage2Facts")
if not isinstance(binding, dict) or not isinstance(facts, dict):
raise RuntimeError("STAGE2_BINDING_OR_FACTS_REQUIRED")
return apply_stage2(binding, facts, value)
raise RuntimeError("OPERATION_DENIED")
def main():
value = token()
sock_path = pathlib.Path(SOCKET)
sock_path.unlink(missing_ok=True)
server = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
server.bind(SOCKET)
os.chmod(SOCKET, 0o660)
server.listen(16)
journal("EXECUTOR_STARTED", api_surface=["GET_BRANCH_PROTECTION", "APPLY_APPROVED_PROTECTION_DELTA", "MIGRATE_APPROVED_GATE", "READBACK_BRANCH_PROTECTION"], semantic_schema=SEMANTIC_SCHEMA, approved_semantic_digest=APPROVED_SEMANTIC_SHA, stage1_semantic_digest=STAGE1_SEMANTIC_SHA, campaign_gate=CAMPAIGN_GATE)
try:
while True:
conn, _ = server.accept()
with conn:
data = conn.recv(1024 * 1024 + 1)
if len(data) > 1024 * 1024:
result = {"status": "FAIL", "code": "REQUEST_TOO_LARGE"}
else:
try:
request = json.loads(data.decode())
result = handle(request, value)
except Exception as exc:
result = {"status": "FAIL", "code": str(exc)}
conn.sendall((json.dumps(result, sort_keys=True, separators=(",", ":"), ensure_ascii=False) + "\n").encode())
finally:
server.close()
sock_path.unlink(missing_ok=True)
if __name__ == "__main__":
main()
@@ -0,0 +1,347 @@
#!/usr/bin/env python3
import base64
import hashlib
import json
import os
import pathlib
import shutil
import socket
import subprocess
import tempfile
import time
import urllib.error
import urllib.parse
import urllib.request
LOCAL_API = 'http://127.0.0.1:3000/api/v1'
NETWORK_API = 'https://g.cnnetmkt.top/git/api/v1'
NETWORK_REMOTE = 'https://g.cnnetmkt.top/git/aios/aios.git'
REPO = 'aios/aios'
BASE_BRANCH = 'governance/approvals'
REQUEST_BRANCH = 'governance/request/protection-semantic-reconciliation-v1-20260808'
SOCKET_PATH = '/run/aios-governance-preapproval-publisher/publisher.sock'
CREDENTIAL_NAME = 'gitea-machine-token'
AUTHORITY_REPO = '/data/aios-gitops/repo.git'
TASK_ROOT = pathlib.Path('/var/lib/aios-gitops/runtime/tasks/AIOS-PROTECTION-SNAPSHOT-DRIFT-FORENSICS-V1')
INPUT_ROOT = TASK_ROOT / 'publication-input'
RECEIPT = TASK_ROOT / 'publication-receipt.json'
MANIFEST_REL = 'approvals/governance-policy/AIOS-PROTECTION-SEMANTIC-SNAPSHOT-RECONCILIATION-V1.json'
EVIDENCE_REL = 'approvals/governance-policy/AIOS-PROTECTION-SEMANTIC-SNAPSHOT-RECONCILIATION-V1.evidence.json'
def digest_bytes(data):
return hashlib.sha256(data).hexdigest()
def digest_json(value):
return digest_bytes(json.dumps(value, sort_keys=True, separators=(',', ':'), ensure_ascii=False).encode())
def load_token():
path = pathlib.Path(os.environ['CREDENTIALS_DIRECTORY']) / CREDENTIAL_NAME
value = path.read_text(encoding='utf-8').strip()
if len(value) < 16:
raise RuntimeError('MACHINE_CREDENTIAL_INVALID')
return value
def api(base, method, path, token, body=None, expected=(200,)):
raw = None if body is None else json.dumps(body, separators=(',', ':'), ensure_ascii=False).encode()
request = urllib.request.Request(base + path, data=raw, method=method)
request.add_header('Authorization', 'token ' + token)
request.add_header('Accept', 'application/json')
if raw is not None:
request.add_header('Content-Type', 'application/json')
try:
with urllib.request.urlopen(request, timeout=20) as response:
payload = response.read()
if response.status not in expected:
raise RuntimeError('GITEA_HTTP_' + str(response.status))
return response.status, json.loads(payload) if payload else None
except urllib.error.HTTPError as exc:
payload = exc.read()
if exc.code in expected:
return exc.code, json.loads(payload) if payload else None
raise RuntimeError('GITEA_HTTP_' + str(exc.code) + ':' + method + ':' + path) from None
def authority_head():
result = subprocess.run(
['git', '--git-dir=' + AUTHORITY_REPO, 'rev-parse', 'refs/heads/' + BASE_BRANCH],
text=True, capture_output=True, check=False,
)
if result.returncode != 0:
raise RuntimeError('AUTHORITY_BASE_UNAVAILABLE')
return result.stdout.strip()
def branch_api(base, token, branch, expected=(200,)):
return api(base, 'GET', '/repos/' + REPO + '/branches/' + urllib.parse.quote(branch, safe=''), token, expected=expected)
def verify_base(token):
authority = authority_head()
_, local = branch_api(LOCAL_API, token, BASE_BRANCH)
_, network = branch_api(NETWORK_API, token, BASE_BRANCH)
local_oid = local.get('commit', {}).get('id')
network_oid = network.get('commit', {}).get('id')
if not authority or authority != local_oid or authority != network_oid:
raise RuntimeError('RECONCILIATION_REQUIRED')
return authority
def verify_identity(token):
_, user = api(LOCAL_API, 'GET', '/user', token)
if user.get('login') != 'aios-autonomy-agent' or user.get('id') != 153 or user.get('is_admin') is not False:
raise RuntimeError('MACHINE_IDENTITY_REJECTED')
return {'login': user['login'], 'id': user['id'], 'owner': user.get('is_admin')}
def load_inputs():
manifest = json.dumps(json.loads((INPUT_ROOT / 'manifest.json').read_text(encoding='utf-8')), sort_keys=True, separators=(',', ':'), ensure_ascii=False).encode()
evidence = json.dumps(json.loads((INPUT_ROOT / 'evidence.json').read_text(encoding='utf-8')), sort_keys=True, separators=(',', ':'), ensure_ascii=False).encode()
for data in (manifest, evidence):
if any(secret in data.lower() for secret in (b'authorization:', b'ghp_', b'github_pat_', b'secret=', b'token=')):
raise RuntimeError('SECRET_CONTENT_REJECTED')
return manifest, evidence
def publication_key(base_oid, manifest_sha, evidence_sha):
payload = REPO + base_oid + REQUEST_BRANCH + manifest_sha + evidence_sha
return hashlib.sha256(payload.encode()).hexdigest()
def write_receipt(state, **fields):
record = {
'schema': 'aios.governance-preapproval-publication.v1',
'operation': 'SEMANTIC_RECONCILIATION_REQUEST',
'state': state,
'recorded_unix': int(time.time()),
'request_branch': REQUEST_BRANCH,
'manifest_path': MANIFEST_REL,
'evidence_path': EVIDENCE_REL,
'machine_actor': 'aios-autonomy-agent',
'owner': False,
'credential_ref': {'scheme': 'systemd-credential', 'name': CREDENTIAL_NAME},
'governance_capability_executed': False,
'protection_mutation': False,
'force_push': False,
**fields,
}
RECEIPT.parent.mkdir(parents=True, exist_ok=True)
temporary = RECEIPT.with_suffix('.tmp')
temporary.write_text(json.dumps(record, sort_keys=True, separators=(',', ':'), ensure_ascii=False) + '\n', encoding='utf-8')
temporary.replace(RECEIPT)
RECEIPT.chmod(0o640)
return record
def existing_prs(token):
_, prs = api(LOCAL_API, 'GET', '/repos/' + REPO + '/pulls?state=all&base=' + urllib.parse.quote(BASE_BRANCH, safe='') + '&limit=100', token)
return [pr for pr in prs if pr.get('head', {}).get('ref') == REQUEST_BRANCH and pr.get('base', {}).get('ref') == BASE_BRANCH]
def verify_existing_branch(token, branch_oid, manifest, evidence):
_, commit = api(LOCAL_API, 'GET', '/repos/' + REPO + '/git/commits/' + branch_oid, token)
if commit.get('parents', [{}])[0].get('sha') is None:
raise RuntimeError('REQUEST_BRANCH_COMMIT_INVALID')
for relative, expected in ((MANIFEST_REL, manifest), (EVIDENCE_REL, evidence)):
_, content = api(LOCAL_API, 'GET', '/repos/' + REPO + '/contents/' + urllib.parse.quote(relative, safe='/') + '?ref=' + branch_oid, token)
actual = base64.b64decode(''.join(content.get('content', '').split()))
if actual != expected:
raise RuntimeError('GOVERNANCE_REQUEST_BRANCH_CONFLICT')
def push_one_commit(token, base_oid, manifest, evidence):
work_root = TASK_ROOT / 'publisher-work'
work_root.mkdir(parents=True, exist_ok=True)
temporary = pathlib.Path(tempfile.mkdtemp(prefix='semantic-reconciliation-', dir=work_root))
askpass = temporary / 'askpass.sh'
clone = temporary / 'repo'
credential_path = pathlib.Path(os.environ['CREDENTIALS_DIRECTORY']) / CREDENTIAL_NAME
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)
env = os.environ.copy()
env.update({
'GIT_ASKPASS': str(askpass),
'GIT_TERMINAL_PROMPT': '0',
'GITEA_MACHINE_ACTOR': 'aios-autonomy-agent',
'CREDENTIAL_FILE': str(credential_path),
'GIT_CONFIG_COUNT': '1',
'GIT_CONFIG_KEY_0': 'safe.directory',
'GIT_CONFIG_VALUE_0': AUTHORITY_REPO,
})
try:
run = subprocess.run(
['git', 'clone', '--branch', BASE_BRANCH, '--single-branch', '--quiet', NETWORK_REMOTE, str(clone)],
text=True, capture_output=True, env=env, check=False,
)
if run.returncode != 0:
detail = ' '.join(run.stderr.strip().split())[:240]
raise RuntimeError('PUBLISHER_CLONE_FAILED:' + detail)
def git(*args, push_env=None):
result = subprocess.run(['git', *args], cwd=clone, text=True, capture_output=True, env=push_env or env, check=False)
if result.returncode != 0:
raise RuntimeError('PUBLISHER_GIT_OPERATION_FAILED')
return result.stdout.strip()
git('checkout', '--detach', base_oid)
git('switch', '--create', REQUEST_BRANCH)
manifest_path = clone / MANIFEST_REL
evidence_path = clone / EVIDENCE_REL
manifest_path.parent.mkdir(parents=True, exist_ok=True)
evidence_path.parent.mkdir(parents=True, exist_ok=True)
manifest_path.write_bytes(manifest)
evidence_path.write_bytes(evidence)
git('add', '--', MANIFEST_REL, EVIDENCE_REL)
staged = git('diff', '--cached', '--name-only').splitlines()
if staged != sorted([MANIFEST_REL, EVIDENCE_REL]):
raise RuntimeError('PUBLISHER_PATH_ALLOWLIST_FAILED')
git('-c', 'user.name=aios-autonomy-agent', '-c', 'user.email=aios-autonomy-agent@aios.invalid', 'commit', '--quiet', '-m', '[governance] publish semantic reconciliation evidence')
if git('rev-list', '--count', base_oid + '..HEAD') != '1':
raise RuntimeError('PUBLISHER_COMMIT_COUNT_FAILED')
git('push', '--porcelain', NETWORK_REMOTE, 'HEAD:refs/heads/' + REQUEST_BRANCH, push_env=env)
head = git('rev-parse', 'HEAD')
return head
finally:
shutil.rmtree(temporary, ignore_errors=True)
def publish():
token = load_token()
identity = verify_identity(token)
base_oid = verify_base(token)
manifest, evidence = load_inputs()
manifest_sha = digest_bytes(manifest)
evidence_sha = digest_bytes(evidence)
key = publication_key(base_oid, manifest_sha, evidence_sha)
write_receipt('REQUESTED', governance_base_oid=base_oid, manifest_sha256=manifest_sha, evidence_sha256=evidence_sha, idempotency_key=key, machine_identity=identity)
write_receipt('LOCAL_CANDIDATE_VERIFIED', governance_base_oid=base_oid, manifest_sha256=manifest_sha, evidence_sha256=evidence_sha, idempotency_key=key, machine_identity=identity)
branch_status, branch = branch_api(LOCAL_API, token, REQUEST_BRANCH, expected=(200, 404))
if branch_status == 200:
branch_oid = branch.get('commit', {}).get('id')
verify_existing_branch(token, branch_oid, manifest, evidence)
else:
branch_oid = push_one_commit(token, base_oid, manifest, evidence)
verify_existing_branch(token, branch_oid, manifest, evidence)
write_receipt('BRANCH_PUSHED', governance_base_oid=base_oid, manifest_sha256=manifest_sha, evidence_sha256=evidence_sha, idempotency_key=key, request_head_oid=branch_oid, machine_identity=identity)
prs = existing_prs(token)
if len(prs) > 1:
raise RuntimeError('MULTIPLE_MATCHING_APPROVAL_PRS')
if prs:
pr = prs[0]
if pr.get('head', {}).get('sha') != branch_oid:
raise RuntimeError('APPROVAL_PR_HEAD_MISMATCH')
pr_number = pr.get('number')
else:
_, pr = api(LOCAL_API, 'POST', '/repos/' + REPO + '/pulls', token, {
'title': '[GOVERNANCE RECONCILIATION] Semantic protection snapshot schema migration',
'head': REQUEST_BRANCH,
'base': BASE_BRANCH,
'body': 'Schema-only reconciliation for the existing PR54 Stage-1 and Stage-2 predicates. No new governance capability; Owner exact-head re-attestation required.',
}, expected=(201,))
pr_number = pr.get('number')
write_receipt('PR_CREATED', governance_base_oid=base_oid, manifest_sha256=manifest_sha, evidence_sha256=evidence_sha, idempotency_key=key, request_head_oid=branch_oid, pr_number=pr_number, machine_identity=identity)
_, remote_branch = branch_api(NETWORK_API, token, REQUEST_BRANCH)
if remote_branch.get('commit', {}).get('id') != branch_oid:
raise RuntimeError('NETWORK_BRANCH_READBACK_MISMATCH')
_, read_pr = api(LOCAL_API, 'GET', '/repos/' + REPO + '/pulls/' + str(pr_number), token)
if read_pr.get('head', {}).get('sha') != branch_oid or read_pr.get('base', {}).get('ref') != BASE_BRANCH:
raise RuntimeError('PR_READBACK_MISMATCH')
result = write_receipt('COMPLETED', governance_base_oid=base_oid, manifest_sha256=manifest_sha, evidence_sha256=evidence_sha, idempotency_key=key, request_head_oid=branch_oid, pr_number=pr_number, pr_head_oid=read_pr.get('head', {}).get('sha'), machine_identity=identity)
return result
def self_test():
token = load_token()
identity = verify_identity(token)
base_oid = verify_base(token)
branch_status, _ = branch_api(LOCAL_API, token, REQUEST_BRANCH, expected=(200, 404))
return {
'status': 'PASS',
'governance_base_oid': base_oid,
'machine_identity': identity,
'credential_ref': {'scheme': 'systemd-credential', 'name': CREDENTIAL_NAME},
'branch_namespace': 'refs/heads/governance/request/*',
'target_base': BASE_BRANCH,
'request_branch_status': 'EXISTS' if branch_status == 200 else 'ABSENT',
'operation_allowlist': ['READ_BASE', 'CREATE_ONE_REQUEST_BRANCH', 'WRITE_TWO_EVIDENCE_PATHS_ONE_COMMIT', 'CREATE_OR_READ_ONE_APPROVAL_PR', 'READBACK'],
'denied': ['MAIN_WRITE', 'DIRECT_APPROVALS_WRITE', 'BRANCH_PROTECTION', 'MERGE', 'FORCE_PUSH', 'TAG', 'PRODUCTION', 'CREDENTIAL_MUTATION'],
'governance_capability_executed': False,
}
def read_publication_status():
token = load_token()
receipt = json.loads(RECEIPT.read_text(encoding='utf-8'))
pr_number = receipt.get('pr_number')
if not pr_number:
raise RuntimeError('PUBLICATION_RECEIPT_PR_MISSING')
_, pr = api(LOCAL_API, 'GET', '/repos/' + REPO + '/pulls/' + str(pr_number), token)
_, reviews = api(LOCAL_API, 'GET', '/repos/' + REPO + '/pulls/' + str(pr_number) + '/reviews', token)
_, branch = branch_api(LOCAL_API, token, REQUEST_BRANCH)
_, network_branch = branch_api(NETWORK_API, token, REQUEST_BRANCH)
return {
'status': 'PASS',
'request_branch': REQUEST_BRANCH,
'branch_head': branch.get('commit', {}).get('id'),
'network_branch_head': network_branch.get('commit', {}).get('id'),
'pr_number': pr.get('number'),
'pr_state': pr.get('state'),
'pr_merged': pr.get('merged'),
'pr_head': pr.get('head', {}).get('sha'),
'pr_base': pr.get('base', {}).get('ref'),
'reviews': [
{
'id': item.get('id'),
'reviewer_id': item.get('user', {}).get('id'),
'state': item.get('state'),
'commit_id': item.get('commit_id'),
'dismissed': item.get('dismissed'),
}
for item in reviews
],
'governance_capability_executed': False,
'protection_mutation': False,
}
def handle(request):
if not isinstance(request, dict) or set(request) != {'operation'}:
raise RuntimeError('REQUEST_SCHEMA_INVALID')
if request['operation'] == 'DRY_RUN_SELF_TEST':
return self_test()
if request['operation'] == 'PUBLISH_SEMANTIC_RECONCILIATION':
return publish()
if request['operation'] == 'READ_SEMANTIC_RECONCILIATION_STATUS':
return read_publication_status()
raise RuntimeError('OPERATION_DENIED')
def serve(listener):
conn, _ = listener.accept()
with conn:
try:
data = conn.recv(1024 * 1024 + 1)
if len(data) > 1024 * 1024:
raise RuntimeError('REQUEST_TOO_LARGE')
result = handle(json.loads(data.decode()))
except Exception as exc:
result = {'status': 'FAIL', 'code': str(exc)}
conn.sendall((json.dumps(result, sort_keys=True, separators=(',', ':'), ensure_ascii=False) + '\n').encode())
def main():
if os.environ.get('LISTEN_FDS') != '1':
raise SystemExit('SOCKET_ACTIVATION_REQUIRED')
listener = socket.fromfd(3, socket.AF_UNIX, socket.SOCK_STREAM)
serve(listener)
if __name__ == '__main__':
main()
@@ -0,0 +1,73 @@
"""Fail-closed Authority/Gitea ref reconciliation after protected merge."""
from __future__ import annotations
from dataclasses import dataclass
from pathlib import Path
import subprocess
class RefReconciliationError(RuntimeError):
pass
def _oid(value: str, field: str) -> str:
if not isinstance(value, str) or len(value) != 40 or any(char not in "0123456789abcdef" for char in value):
raise RefReconciliationError(field + "_INVALID")
return value
@dataclass(frozen=True, slots=True)
class RefReconciler:
git_dir: Path
def read(self, ref: str) -> str:
result = subprocess.run(
["git", f"--git-dir={self.git_dir}", "rev-parse", "--verify", ref],
text=True,
capture_output=True,
check=False,
)
if result.returncode != 0:
raise RefReconciliationError("AUTHORITY_REF_UNAVAILABLE")
return result.stdout.strip()
def is_ancestor(self, ancestor: str, descendant: str) -> bool:
_oid(ancestor, "ancestor")
_oid(descendant, "descendant")
result = subprocess.run(
["git", f"--git-dir={self.git_dir}", "merge-base", "--is-ancestor", ancestor, descendant],
text=True,
capture_output=True,
check=False,
)
if result.returncode == 0:
return True
if result.returncode == 1:
return False
raise RefReconciliationError("AUTHORITY_ANCESTRY_CHECK_FAILED")
def reconcile_main(self, *, expected_oid: str, merged_oid: str, gitea_main_oid: str) -> str:
_oid(expected_oid, "expected_oid")
_oid(merged_oid, "merged_oid")
_oid(gitea_main_oid, "gitea_main_oid")
if gitea_main_oid != merged_oid:
raise RefReconciliationError("GITEA_MAIN_EXPECTATION_MISMATCH")
if not self.is_ancestor(expected_oid, merged_oid):
raise RefReconciliationError("MAIN_FF_REQUIRED")
current = self.read("refs/heads/main")
if current == merged_oid:
return merged_oid
if current != expected_oid:
raise RefReconciliationError("AUTHORITY_MAIN_EXPECTATION_MISMATCH")
result = subprocess.run(
["git", f"--git-dir={self.git_dir}", "update-ref", "refs/heads/main", merged_oid, expected_oid],
text=True,
capture_output=True,
check=False,
)
if result.returncode != 0:
raise RefReconciliationError("AUTHORITY_MAIN_CAS_FAILED")
if self.read("refs/heads/main") != merged_oid:
raise RefReconciliationError("AUTHORITY_MAIN_CAS_READBACK_MISMATCH")
return merged_oid
@@ -0,0 +1,81 @@
"""Canonical semantic branch-protection model used by AIOS governance gates."""
from __future__ import annotations
import hashlib
import json
from typing import Any, Mapping
SCHEMA = "aios.semantic-protection.v1"
USER_IDS = {"owner": 2, "aios-autonomy-agent": 153}
def canonical_json(value: Any) -> bytes:
return json.dumps(value, sort_keys=True, separators=(",", ":"), ensure_ascii=False).encode("utf-8")
def _typed_json(value: Any) -> list[Any]:
if value in (None, "null"):
return []
if isinstance(value, str):
return json.loads(value)
return list(value)
def _user_ids(names: Any) -> list[int]:
result = []
for name in names or []:
if name not in USER_IDS:
raise ValueError("UNKNOWN_GOVERNANCE_USER:" + str(name))
result.append(USER_IDS[name])
return sorted(result)
def semantic_policy(protection: Mapping[str, Any]) -> dict[str, Any]:
"""Drop transport metadata and canonicalize semantic security fields."""
return {
"schema_version": SCHEMA,
"repo_id": 1,
"protection_id": 2,
"branch_name": protection.get("branch_name"),
"can_push": protection.get("enable_push"),
"can_force_push": protection.get("enable_force_push"),
"enable_merge_whitelist": protection.get("enable_merge_whitelist"),
"merge_whitelist_user_ids": _user_ids(protection.get("merge_whitelist_usernames")),
"merge_whitelist_team_ids": [],
"required_approvals": protection.get("required_approvals"),
"enable_status_check": protection.get("enable_status_check"),
"status_check_contexts": sorted(protection.get("status_check_contexts") or []),
"block_admin_merge_override": protection.get("block_admin_merge_override"),
"block_on_official_review_requests": protection.get("block_on_official_review_requests"),
"block_on_outdated_branch": protection.get("block_on_outdated_branch"),
"block_on_rejected_reviews": protection.get("block_on_rejected_reviews"),
"dismiss_stale_approvals": protection.get("dismiss_stale_approvals"),
"ignore_stale_approvals": protection.get("ignore_stale_approvals"),
"enable_approvals_whitelist": protection.get("enable_approvals_whitelist"),
"approvals_whitelist_user_ids": _user_ids(protection.get("approvals_whitelist_username")),
"approvals_whitelist_team_ids": [],
"enable_bypass_allowlist": protection.get("enable_bypass_allowlist"),
"bypass_allowlist_user_ids": _user_ids(protection.get("bypass_allowlist_usernames")),
"bypass_allowlist_team_ids": [],
"enable_force_push_allowlist": protection.get("enable_force_push_allowlist"),
"force_push_allowlist_user_ids": _user_ids(protection.get("force_push_allowlist_usernames")),
"enable_whitelist": protection.get("enable_push_whitelist"),
"whitelist_user_ids": _user_ids(protection.get("push_whitelist_usernames")),
"whitelist_team_ids": [],
"force_push_allowlist_deploy_keys": protection.get("force_push_allowlist_deploy_keys"),
"whitelist_deploy_keys": protection.get("push_whitelist_deploy_keys"),
"require_signed_commits": protection.get("require_signed_commits"),
"priority": protection.get("priority"),
"protected_file_patterns": _typed_json(protection.get("protected_file_patterns")),
"unprotected_file_patterns": _typed_json(protection.get("unprotected_file_patterns")),
}
def semantic_digest(protection: Mapping[str, Any]) -> str:
return hashlib.sha256(canonical_json(semantic_policy(protection))).hexdigest()
def transport_digest(protection: Mapping[str, Any]) -> str:
return hashlib.sha256(canonical_json(protection)).hexdigest()
@@ -0,0 +1,141 @@
"""Fail-closed Stage-2 campaign gate migration predicates."""
from __future__ import annotations
import copy
import hashlib
import json
import re
from typing import Any, Mapping
SEMANTIC_SCHEMA = "aios.semantic-protection.v1"
STAGE1_SEMANTIC_SHA = "d911e856f71de6105b8efa08e13338a448e0b6d37536332decd80c9f409ecf27"
CAMPAIGN_ID = "AIOS-AUTONOMOUS-RELEASE-CAMPAIGN-20260807"
OWNER_GATE = "owner-approval-gate"
CAMPAIGN_GATE = "campaign-approval-gate"
OID_RE = re.compile(r"^[0-9a-f]{40}$")
class Stage2GateError(RuntimeError):
pass
def canonical_json(value: Any) -> bytes:
return json.dumps(value, sort_keys=True, separators=(",", ":"), ensure_ascii=False).encode("utf-8")
def _typed_json(value: Any) -> list[Any]:
if value in (None, "", "null"):
return []
if isinstance(value, str):
return json.loads(value)
return list(value)
def _user_ids(names: Any) -> list[int]:
mapping = {"owner": 2, "aios-autonomy-agent": 153}
result = []
for name in names or []:
if name not in mapping:
raise Stage2GateError("UNKNOWN_GOVERNANCE_USER")
result.append(mapping[name])
return sorted(result)
def semantic_policy(protection: Mapping[str, Any]) -> dict[str, Any]:
return {
"schema_version": SEMANTIC_SCHEMA,
"repo_id": 1,
"protection_id": 2,
"branch_name": protection.get("branch_name"),
"can_push": protection.get("enable_push"),
"can_force_push": protection.get("enable_force_push"),
"enable_merge_whitelist": protection.get("enable_merge_whitelist"),
"merge_whitelist_user_ids": _user_ids(protection.get("merge_whitelist_usernames")),
"merge_whitelist_team_ids": [],
"required_approvals": protection.get("required_approvals"),
"enable_status_check": protection.get("enable_status_check"),
"status_check_contexts": sorted(protection.get("status_check_contexts") or []),
"block_admin_merge_override": protection.get("block_admin_merge_override"),
"block_on_official_review_requests": protection.get("block_on_official_review_requests"),
"block_on_outdated_branch": protection.get("block_on_outdated_branch"),
"block_on_rejected_reviews": protection.get("block_on_rejected_reviews"),
"dismiss_stale_approvals": protection.get("dismiss_stale_approvals"),
"ignore_stale_approvals": protection.get("ignore_stale_approvals"),
"enable_approvals_whitelist": protection.get("enable_approvals_whitelist"),
"approvals_whitelist_user_ids": _user_ids(protection.get("approvals_whitelist_username")),
"approvals_whitelist_team_ids": [],
"enable_bypass_allowlist": protection.get("enable_bypass_allowlist"),
"bypass_allowlist_user_ids": _user_ids(protection.get("bypass_allowlist_usernames")),
"bypass_allowlist_team_ids": [],
"enable_force_push_allowlist": protection.get("enable_force_push_allowlist"),
"force_push_allowlist_user_ids": _user_ids(protection.get("force_push_allowlist_usernames")),
"force_push_allowlist_team_ids": [],
"enable_whitelist": protection.get("enable_push_whitelist"),
"whitelist_user_ids": _user_ids(protection.get("push_whitelist_usernames")),
"whitelist_team_ids": [],
"force_push_allowlist_deploy_keys": protection.get("force_push_allowlist_deploy_keys"),
"whitelist_deploy_keys": protection.get("push_whitelist_deploy_keys"),
"require_signed_commits": protection.get("require_signed_commits"),
"priority": protection.get("priority"),
"protected_file_patterns": protection.get("protected_file_patterns"),
"unprotected_file_patterns": protection.get("unprotected_file_patterns"),
}
def semantic_digest(protection: Mapping[str, Any]) -> str:
return hashlib.sha256(canonical_json(semantic_policy(protection))).hexdigest()
def build_target(protection: Mapping[str, Any]) -> tuple[str, dict[str, Any]]:
current = list(protection.get("status_check_contexts") or [])
has_owner = OWNER_GATE in current
has_campaign = CAMPAIGN_GATE in current
if has_owner and has_campaign:
raise Stage2GateError("GATE_PRESTATE_CONFLICT")
if not has_owner and not has_campaign:
raise Stage2GateError("OWNER_GATE_MISSING")
if has_campaign:
return "IDEMPOTENT_SUCCESS", copy.deepcopy(dict(protection))
if semantic_digest(protection) != STAGE1_SEMANTIC_SHA:
raise Stage2GateError("SEMANTIC_PRECONDITION_INVALID")
target = copy.deepcopy(dict(protection))
target["status_check_contexts"] = sorted(
[gate for gate in current if gate != OWNER_GATE] + [CAMPAIGN_GATE]
)
return "READY", target
def validate_stage2_facts(facts: Mapping[str, Any]) -> None:
required = {
"campaign_id",
"campaign_active",
"campaign_revoked",
"machine_actor",
"required_checks_all_success",
"required_checks_same_head",
"unresolved_conversations",
"mergeable",
"protected_target",
"current_main_oid",
"gitea_main_oid",
"current_main_lineage_verified",
}
if set(facts) != required:
raise Stage2GateError("STAGE2_FACTS_SCHEMA_INVALID")
if facts["campaign_id"] != CAMPAIGN_ID or facts["campaign_active"] is not True or facts["campaign_revoked"] is not False:
raise Stage2GateError("CAMPAIGN_NOT_ACTIVE")
if facts["machine_actor"] != "aios-autonomy-agent":
raise Stage2GateError("MACHINE_ACTOR_MISMATCH")
if facts["required_checks_all_success"] is not True or facts["required_checks_same_head"] is not True:
raise Stage2GateError("REQUIRED_CHECKS_NOT_CURRENT")
if facts["unresolved_conversations"] != 0 or facts["mergeable"] is not True:
raise Stage2GateError("CAMPAIGN_GATE_FACTS_INVALID")
if facts["protected_target"] != "main" or facts["current_main_lineage_verified"] is not True:
raise Stage2GateError("PROTECTED_TARGET_OR_LINEAGE_INVALID")
if (
not isinstance(facts["current_main_oid"], str)
or not OID_RE.fullmatch(facts["current_main_oid"])
or facts["gitea_main_oid"] != facts["current_main_oid"]
):
raise Stage2GateError("MAIN_READBACK_MISMATCH")
@@ -0,0 +1,588 @@
"""Governed V2R2C1 production release orchestration.
This module is deliberately scoped to the Owner-approved V2R2C1 binding. It
uses the existing TagPublisher and metadata adapters; it does not introduce a
second Git writer, a second durable job store, or an alternate baseline.
"""
from __future__ import annotations
import argparse
import fcntl
import hashlib
import json
import os
import re
import subprocess
import tarfile
import tempfile
import time
from dataclasses import dataclass
from pathlib import Path
from typing import Any, Mapping
from gitea_tag_publication import (
GiteaTagClient,
GitTagBackend,
PublicationManualOwnerReviewRequired,
PublicationPending,
PublicationReconciliationRequired,
PublicationRequest,
TagPublisher,
)
VERSION = "V2R2C1"
TARGET_OID = "998d30304787e31e5ae607c0bd86b5edb2b0e5ad"
EVIDENCE_CLOSURE_OID = "a8b906d1bc24088c19647e98de6a679268f6d0a2"
APPROVAL_PLAN_ID = "sha256:c0df52008ae7f64cec5ee2d28947aa7b892066c670ab8295e4549140b1a7500e"
REPOSITORY = "aios/aios"
SERVICE = "aios-v2r1c7-production.service"
PORT = 18927
OID_RE = re.compile(r"^[0-9a-f]{40}$")
SHA_RE = re.compile(r"^[0-9a-f]{64}$")
class ReleaseBlocked(RuntimeError):
"""A release gate failed and no unsafe recovery is permitted."""
class ManualOwnerReviewRequired(ReleaseBlocked):
pass
class ReconciliationRequired(ReleaseBlocked):
pass
def canonical(value: object) -> bytes:
return (json.dumps(value, sort_keys=True, separators=(",", ":")) + "\n").encode()
def sha256_file(path: Path) -> str:
if path.is_symlink() or not path.is_file():
raise ReleaseBlocked("artifact must be a regular file")
digest = hashlib.sha256()
with path.open("rb") as stream:
for block in iter(lambda: stream.read(1024 * 1024), b""):
digest.update(block)
return digest.hexdigest()
def atomic_json(path: Path, value: Mapping[str, Any], mode: int = 0o600) -> None:
path.parent.mkdir(parents=True, exist_ok=True)
temporary = path.with_name(path.name + ".tmp")
temporary.write_bytes(canonical(dict(value)))
os.chmod(temporary, mode)
os.replace(temporary, path)
@dataclass(frozen=True)
class ReleaseBinding:
version: str = VERSION
target_oid: str = TARGET_OID
evidence_closure_oid: str = EVIDENCE_CLOSURE_OID
approval_plan_id: str = APPROVAL_PLAN_ID
repository: str = REPOSITORY
production_service: str = SERVICE
production_port: int = PORT
def validate(self) -> None:
if self.version != VERSION or self.repository != REPOSITORY:
raise ManualOwnerReviewRequired("release binding is outside V2R2C1 scope")
if not OID_RE.fullmatch(self.target_oid) or self.target_oid != TARGET_OID:
raise ManualOwnerReviewRequired("V2R2C1 target_oid mismatch")
if not OID_RE.fullmatch(self.evidence_closure_oid) or self.evidence_closure_oid != EVIDENCE_CLOSURE_OID:
raise ManualOwnerReviewRequired("V2R2C1 evidence closure mismatch")
if self.approval_plan_id != APPROVAL_PLAN_ID:
raise ManualOwnerReviewRequired("V2R2C1 approval plan mismatch")
if self.production_service != SERVICE or int(self.production_port) != PORT:
raise ManualOwnerReviewRequired("production service binding mismatch")
def as_dict(self) -> dict[str, Any]:
return {
"version": self.version,
"target_oid": self.target_oid,
"evidence_closure_oid": self.evidence_closure_oid,
"approval_plan_id": self.approval_plan_id,
"repository": self.repository,
"production_service": self.production_service,
"production_port": self.production_port,
}
class StateLock:
def __init__(self, path: Path):
self.path = path
self.handle = None
def __enter__(self) -> "StateLock":
self.path.parent.mkdir(parents=True, exist_ok=True)
self.handle = self.path.open("a+", encoding="utf-8")
fcntl.flock(self.handle.fileno(), fcntl.LOCK_EX)
return self
def __exit__(self, exc_type, exc, tb) -> None:
if self.handle is not None:
fcntl.flock(self.handle.fileno(), fcntl.LOCK_UN)
self.handle.close()
class ReleaseState:
def __init__(self, root: Path, binding: ReleaseBinding):
self.root = root
self.path = root / "release-state.json"
self.binding = binding
root.mkdir(parents=True, exist_ok=True)
if self.path.exists():
value = json.loads(self.path.read_text(encoding="utf-8"))
if value.get("binding") != binding.as_dict():
raise ManualOwnerReviewRequired("persisted release binding conflict")
self.value = value
else:
self.value = {
"schema": "aios.v2r2c1.governed-production-release-state.v1",
"binding": binding.as_dict(),
"state": "INITIAL",
"completed": {},
"history": [],
}
self.save()
def save(self) -> None:
atomic_json(self.path, self.value)
def record(self, state: str, step: str, result: Mapping[str, Any]) -> None:
safe = json.loads(json.dumps(dict(result)))
self.value["state"] = state
self.value.setdefault("completed", {})[step] = safe
self.value.setdefault("history", []).append(
{"at": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()), "state": state, "step": step, "result": safe}
)
self.save()
def fail(self, state: str, reason: str) -> None:
self.value["state"] = state
self.value.setdefault("history", []).append(
{"at": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()), "state": state, "reason": str(reason)[:500]}
)
self.save()
def _git(git_dir: Path, *args: str, check: bool = True) -> str:
completed = subprocess.run(
["git", f"--git-dir={git_dir}", *args],
check=False,
capture_output=True,
text=True,
timeout=45,
env={"PATH": os.environ.get("PATH", "")},
)
if check and completed.returncode != 0:
raise ReconciliationRequired(completed.stderr.strip() or "authoritative git query failed")
return completed.stdout.strip()
def _ancestor(git_dir: Path, ancestor: str, descendant: str) -> bool:
return subprocess.run(
["git", f"--git-dir={git_dir}", "merge-base", "--is-ancestor", ancestor, descendant],
check=False,
capture_output=True,
timeout=45,
env={"PATH": os.environ.get("PATH", "")},
).returncode == 0
def verify_lineage(git_dir: Path, binding: ReleaseBinding) -> dict[str, Any]:
binding.validate()
if _git(git_dir, "cat-file", "-t", binding.target_oid) != "commit":
raise ReleaseBlocked("approved target_oid is not a commit")
if _git(git_dir, "cat-file", "-t", binding.evidence_closure_oid) != "commit":
raise ReleaseBlocked("evidence closure is not a commit")
if not _ancestor(git_dir, binding.target_oid, binding.evidence_closure_oid):
raise ManualOwnerReviewRequired("approved target is not an ancestor of evidence closure")
return {
"target_oid": binding.target_oid,
"evidence_closure_oid": binding.evidence_closure_oid,
"target_is_ancestor_of_evidence": True,
"main_oid": _git(git_dir, "rev-parse", "refs/heads/main"),
}
def verify_owner_approval(value: Mapping[str, Any], binding: ReleaseBinding) -> dict[str, Any]:
approval = value.get("owner_approval")
if not isinstance(approval, Mapping):
raise ReleaseBlocked("OWNER_APPROVAL_RECEIPT_REQUIRED")
if approval.get("approved") is not True or approval.get("status") != "APPROVED":
raise ReleaseBlocked("OWNER_APPROVAL_NOT_ACCEPTED")
if approval.get("approved_by") != "aiman":
raise ManualOwnerReviewRequired("OWNER_APPROVAL_ACTOR_MISMATCH")
if approval.get("approval_source") != "explicit_owner_approval_in_chat":
raise ManualOwnerReviewRequired("OWNER_APPROVAL_SOURCE_MISMATCH")
for key, expected in binding.as_dict().items():
if key in {"repository", "production_service", "production_port"}:
continue
if approval.get(key) != expected:
raise ManualOwnerReviewRequired("OWNER_APPROVAL_BINDING_MISMATCH:" + key)
return dict(approval)
def _payload_file(git_dir: Path, oid: str, path: str) -> bytes:
completed = subprocess.run(
["git", f"--git-dir={git_dir}", "show", f"{oid}:{path}"],
check=False,
capture_output=True,
timeout=45,
env={"PATH": os.environ.get("PATH", "")},
)
if completed.returncode != 0:
raise ReleaseBlocked("required release source missing:" + path)
return completed.stdout
def _commit_epoch(git_dir: Path, oid: str) -> int:
return int(_git(git_dir, "show", "-s", "--format=%ct", oid))
def build_production_artifact(
git_dir: Path,
binding: ReleaseBinding,
output_root: Path,
) -> dict[str, Any]:
"""Build a deterministic executable V2R2C1 health artifact from target_oid."""
binding.validate()
verify_lineage(git_dir, binding)
epoch = _commit_epoch(git_dir, binding.target_oid)
output_root.mkdir(parents=True, exist_ok=True)
stem = f"aios-{VERSION}-{binding.target_oid[:12]}"
output = output_root / (stem + ".tar.zst")
with tempfile.TemporaryDirectory(prefix="v2r2c1-artifact-") as temp:
root = Path(temp) / "payload"
root.mkdir(parents=True)
source_paths = {
"active-runtime/app/deployment_state.py": "active-runtime/app/deployment_state.py",
"active-runtime/app/v2r1c6_privacy_tools.py": "active-runtime/app/v2r1c6_privacy_tools.py",
}
for destination, source in source_paths.items():
target = root / destination
target.parent.mkdir(parents=True, exist_ok=True)
target.write_bytes(_payload_file(git_dir, binding.target_oid, source))
generated: dict[str, str] = {
"VERSION": VERSION + "\n",
"active-runtime/app/v2r2c1_health_service.py": (
"from v2r1c6_privacy_tools import main\n\n"
"if __name__ == '__main__':\n raise SystemExit(main())\n"
),
"deploy/aios-v2/systemd/aios-v2r1c7-production.service": (
"[Unit]\nDescription=AIOS V2R2C1 governed health contract service\n"
"After=network.target\n\n[Service]\nType=simple\n"
"User=aios-v2r1c7\nGroup=aios-v2r1c7\n"
"WorkingDirectory=/opt/aios/v2r2c1\n"
"EnvironmentFile=/etc/aios/aios-v2r2c1.env\n"
"ExecStart=/usr/bin/python3 /opt/aios/v2r2c1/active-runtime/app/v2r2c1_health_service.py --serve --host 127.0.0.1 --port ${AIOS_V2R1C7_PORT}\n"
"Restart=on-failure\nRestartSec=5\nNoNewPrivileges=true\nPrivateTmp=true\n"
"ProtectHome=true\nProtectSystem=strict\nProtectKernelLogs=true\n"
"ProtectKernelModules=true\nProtectKernelTunables=true\nProtectControlGroups=true\n"
"RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6\nIPAddressDeny=any\n"
"IPAddressAllow=localhost\nUMask=0077\n\n[Install]\nWantedBy=multi-user.target\n"
),
"deploy/aios-v2/v2r2c1-runtime-config.json": json.dumps({
"schema": "aios.v2r2c1.production-runtime.v1",
"version": VERSION,
"target_oid": binding.target_oid,
"service": SERVICE,
"port": PORT,
"health_path": "/health",
"deployment_state_source": "environment_and_state_file",
}, sort_keys=True, separators=(",", ":")) + "\n",
"deploy/aios-v2/v2r2c1-rollback.json": json.dumps({
"schema": "aios.v2r2c1.rollback-metadata.v1",
"rollback_version": "V2R1C7",
"rollback_service": SERVICE,
"rollback_target_is_previous_production": True,
"automatic_rollback": False,
}, sort_keys=True, separators=(",", ":")) + "\n",
"scripts/probe_v2r2c1_health.py": (
"import json,sys,urllib.request\n"
"url=sys.argv[1] if len(sys.argv)>1 else 'http://127.0.0.1:18927/health'\n"
"with urllib.request.urlopen(url, timeout=5) as r: print(json.dumps(json.load(r), sort_keys=True))\n"
),
"scripts/rollback_v2r2c1_release.sh": "#!/bin/sh\nset -eu\nprintf '%s\\n' 'rollback requires governed owner-approved executor'\nexit 2\n",
"release-manifests/V2R2C1.json": json.dumps({
"schema": "aios.release-source.v2",
"version": VERSION,
"repository": REPOSITORY,
"target_oid": binding.target_oid,
"evidence_closure_oid": binding.evidence_closure_oid,
"production_deployed": False,
"deployment_state": "STAGING",
"artifact_kind": "executable-production-health",
}, sort_keys=True, separators=(",", ":")) + "\n",
}
for rel, text in generated.items():
target = root / rel
target.parent.mkdir(parents=True, exist_ok=True)
target.write_text(text, encoding="utf-8")
if rel.startswith("scripts/"):
target.chmod(0o755)
metadata = {
"schema": "aios.release-artifact-build.v3",
"version": VERSION,
"repository": REPOSITORY,
"source_head_oid": binding.target_oid,
"evidence_closure_oid": binding.evidence_closure_oid,
"source_date_epoch": epoch,
"clean_source": True,
"executable_payload": True,
"production_deployed": False,
"tag_created": False,
"baseline_changed": False,
"secrets_included": False,
}
(root / "BUILD-METADATA.json").write_bytes(canonical(metadata))
(root / "ROLLBACK-METADATA.json").write_bytes(canonical(json.loads((root / "deploy/aios-v2/v2r2c1-rollback.json").read_text(encoding="utf-8"))))
names = sorted("payload/" + str(path.relative_to(root)).replace(os.sep, "/") for path in root.rglob("*") if path.is_file())
(root / "ARTIFACT-MANIFEST.json").write_bytes(canonical({
"schema": "aios.release-artifact-manifest.v3",
"version": VERSION,
"source_head_oid": binding.target_oid,
"evidence_closure_oid": binding.evidence_closure_oid,
"repository": REPOSITORY,
"files": names + ["payload/ARTIFACT-MANIFEST.json"],
"deployable": True,
"executable_payload": True,
"production_deployed": False,
"tag_created": False,
"baseline_changed": False,
}))
tar_path = Path(temp) / "release.tar"
with tarfile.open(tar_path, "w") as archive:
for path in sorted(root.rglob("*")):
relative = str(path.relative_to(root)).replace(os.sep, "/")
info = archive.gettarinfo(str(path), arcname="payload/" + relative)
info.uid = info.gid = 0
info.uname = "root"
info.gname = "root"
info.mtime = epoch
info.mode = 0o755 if path.is_dir() or path.suffix == ".sh" else 0o644
if path.is_file():
with path.open("rb") as stream:
archive.addfile(info, stream)
else:
archive.addfile(info)
subprocess.run(["zstd", "-T1", "-19", "--no-progress", "-q", "-f", str(tar_path), "-o", str(output)], check=True, timeout=120)
digest = sha256_file(output)
receipt = {
"schema": "aios.release-artifact-receipt.v3",
"version": VERSION,
"repository": REPOSITORY,
"source_head_oid": binding.target_oid,
"evidence_closure_oid": binding.evidence_closure_oid,
"artifact": str(output),
"artifact_sha256": digest,
"artifact_size": output.stat().st_size,
"deployable": True,
"executable_payload": True,
"production_deployed": False,
"tag_created": False,
"baseline_changed": False,
}
atomic_json(output.with_suffix(output.suffix + ".receipt.json"), receipt, mode=0o640)
return receipt
class GovernedProductionReleaseExecutor:
def __init__(self, config: Any):
self.config = config
self.binding = ReleaseBinding()
self.binding.validate()
self.root = Path(config.state_root) / "v2r2c1-governed-production-release"
self.state = ReleaseState(self.root, self.binding)
workspace = self.root / "publication-workspace"
workspace.mkdir(parents=True, exist_ok=True)
self.publisher = TagPublisher(
workspace,
GitTagBackend(workspace, authoritative_repo=Path(config.authoritative_git_dir)),
GiteaTagClient(config.api_base, REPOSITORY, Path(config.token_path)),
config.remote_name,
state_root=self.root / "tag-publication",
)
# Release checks must use the same explicitly configured staging baseline
# as the Action/model runtime; do not fall back to the publication workspace.
self.publisher.baseline_path = Path(
os.environ.get("AIOS_GITOPS_BASELINE_AUTHORITY", str(config.baseline_path))
)
# The immutable runtime package is the source of the version projection;
# the mutable state workspace only stores publication state.
self.publisher.versions_root = Path(__file__).resolve().parents[1] / "versions"
def _request(self, request: Mapping[str, Any]) -> PublicationRequest:
binding = self.binding
for key, expected in binding.as_dict().items():
if key in request and request[key] != expected:
raise ManualOwnerReviewRequired("release request binding mismatch:" + key)
return PublicationRequest(binding.version, binding.target_oid, binding.evidence_closure_oid, binding.approval_plan_id)
def approval_request(self, request: Mapping[str, Any]) -> dict[str, Any]:
if request.get("execute") is not False:
raise ReleaseBlocked("TAG_APPROVAL_REQUEST_MUST_BE_DRY_RUN")
owner = verify_owner_approval(request, self.binding)
with StateLock(self.root / "release.lock"):
current = self.state.value.get("approval")
approval_digest = hashlib.sha256(canonical(owner)).hexdigest()
record = {
"schema": "aios.v2r2c1.tag-approval-request.v1",
"binding": self.binding.as_dict(),
"owner_approval": owner,
"owner_approval_digest_sha256": approval_digest,
"execute": False,
"recorded_at": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()),
}
if current and current != record:
raise ManualOwnerReviewRequired("persisted Owner approval differs")
self.state.value["approval"] = record
self.state.record("APPROVAL_REQUEST_RECORDED", "tag_approval_request", record)
return {"status": "RECORDED", "execute": False, "binding": self.binding.as_dict(), "owner_approval_digest_sha256": approval_digest}
def _require_approval(self) -> None:
approval = self.state.value.get("approval")
if not isinstance(approval, Mapping) or approval.get("binding") != self.binding.as_dict():
raise ReleaseBlocked("OWNER_APPROVAL_REQUEST_NOT_RECORDED")
def publish_tag(self, request: Mapping[str, Any]) -> dict[str, Any]:
if request.get("execute") is not True:
raise ReleaseBlocked("TAG_PUBLICATION_REQUIRES_EXECUTE_TRUE")
self._require_approval()
publication = self._request(request)
with StateLock(self.root / "release.lock"):
try:
verified = self.publisher._verify_publication(publication, allow_push=True, previously_deployed=False)
except PublicationManualOwnerReviewRequired as exc:
self.state.fail("MANUAL_OWNER_REVIEW_REQUIRED", str(exc))
raise ManualOwnerReviewRequired(str(exc)) from exc
except PublicationPending as exc:
self.state.fail("REMOTE_PUSH_PENDING", str(exc))
raise ReconciliationRequired(str(exc)) from exc
except PublicationReconciliationRequired as exc:
self.state.fail("RECONCILIATION_REQUIRED", str(exc))
raise ReconciliationRequired(str(exc)) from exc
result = dict(verified)
self.state.record("TAG_VERIFIED", "tag", result)
return {"status": "REMOTE_VERIFIED", **result}
def tag_status(self, request: Mapping[str, Any]) -> dict[str, Any]:
self._request(request)
return self.publisher.get_tag_publication_status(VERSION)
def build_artifact(self, request: Mapping[str, Any]) -> dict[str, Any]:
self._require_approval()
self._request(request)
receipt = build_production_artifact(Path(self.config.authoritative_git_dir), self.binding, self.root / "artifacts")
self.state.record("ARTIFACT_VERIFIED", "production_artifact", receipt)
return {"status": "VERIFIED", **receipt}
def execute_request(self, request: Mapping[str, Any]) -> dict[str, Any]:
self._require_approval()
publication = self._request(request)
tag_state = self.state.value.get("completed", {}).get("tag")
if not isinstance(tag_state, Mapping) or tag_state.get("state") not in {"GITEA_API_CONFIRMED", "REMOTE_VERIFIED"}:
raise ReleaseBlocked("REMOTE_TAG_PROOF_REQUIRED")
artifact = self.state.value.get("completed", {}).get("production_artifact")
if not isinstance(artifact, Mapping):
artifact = self.build_artifact(request)
value = {
**self.binding.as_dict(),
"tag_oid": tag_state.get("tag_oid") or tag_state.get("gitea_tag_oid"),
"artifact_path": artifact["artifact"],
"artifact_size": int(artifact["artifact_size"]),
"artifact_sha256": artifact["artifact_sha256"],
}
# The production adapter is privileged and is run through the root
# approved-release service boundary by the caller. This handler is
# still useful for a root-owned staging invocation and remains
# fail-closed when the adapter is not available.
import v2r2c1_production_adapter
runtime = v2r2c1_production_adapter.execute_request({"schema": "aios.production-executor-request.v1", "operation": "apply", **value})
if runtime.get("status") != "ACTIVE_VERIFIED":
self.state.fail("RECONCILIATION_REQUIRED", "production health did not verify")
raise ReconciliationRequired("production health did not verify")
self.state.record("PRODUCTION_VERIFIED", "production", runtime)
return {"status": "PRODUCTION_VERIFIED", "runtime": runtime, "artifact": artifact, "tag_oid": value["tag_oid"]}
def consistency(self, request: Mapping[str, Any]) -> dict[str, Any]:
publication = self._request(request)
result = self.publisher.get_release_consistency(publication)
result["release_binding"] = self.binding.as_dict()
result["production_health"] = _health(self.config.health_url, self.binding, self.state.value.get("completed", {}).get("production_artifact", {}))
result["registry"] = _read_json(Path(self.config.registry_root) / f"{VERSION}.json")
result["baseline"] = _read_json(Path(self.config.baseline_path))
return result
def acceptance(self, request: Mapping[str, Any]) -> dict[str, Any]:
result = self.consistency(request)
result["accepted"] = (
result.get("status") == "PASS"
and result.get("production_health", {}).get("verification_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",
"verification_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 {
**body,
"http_status": status,
"verification_status": "ACTIVE_VERIFIED" if exact else "CONFLICT",
}
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())
+297
View File
@@ -0,0 +1,297 @@
#!/usr/bin/env python3
"""Thin production tag adapter over AIOS's existing mature TagPublisher.
This adapter deliberately does not call TagPublisher.publish(), because that
legacy method also mutates Registry/Baseline. The production executor requires:
Tag -> runtime ACTIVE -> Registry -> Baseline.
`apply` reuses TagPublisher._verify_publication() under the publisher's durable
per-version lock. That method performs the existing fail-closed annotated-tag,
network remote, Gitea API, peeled-target and listing verification, but stops
before Registry/Baseline mutation.
`probe` is read-only.
"""
from __future__ import annotations
import argparse
import json
import os
import re
import sys
from pathlib import Path
from typing import Any, Mapping
from gitea_tag_publication import (
GiteaTagClient,
GitTagBackend,
PublicationConflict,
PublicationManualOwnerReviewRequired,
PublicationPending,
PublicationReconciliationRequired,
PublicationRequest,
PublicationState,
TagPublisher,
)
from release_version import validate_release_version
OID_RE = re.compile(r"^[0-9a-f]{40}$")
PLAN_RE = re.compile(r"^sha256:[0-9a-f]{64}$")
def load_request() -> dict[str, Any]:
value = json.load(sys.stdin)
if not isinstance(value, dict):
raise ValueError("request_object_required")
if value.get("schema") != "aios.production-executor-request.v1":
raise ValueError("request_schema_invalid")
try:
validate_release_version(value.get("version", ""))
except ValueError as exc:
raise ValueError("version_invalid") from exc
for name in ("target_oid", "evidence_closure_oid"):
if not OID_RE.fullmatch(str(value.get(name, ""))):
raise ValueError(f"{name}_invalid")
if not PLAN_RE.fullmatch(str(value.get("approval_plan_id", ""))):
raise ValueError("approval_plan_id_invalid")
if value.get("repository_identity") != "aios/aios":
raise ValueError("repository_identity_invalid")
return value
def publication_request(value: Mapping[str, Any]) -> PublicationRequest:
return PublicationRequest(
str(value["version"]),
str(value["target_oid"]),
str(value["evidence_closure_oid"]),
str(value["approval_plan_id"]),
)
def response(value: Mapping[str, Any], status: str, **extra: Any) -> dict[str, Any]:
return {
"status": status,
"version": value["version"],
"target_oid": value["target_oid"],
"evidence_closure_oid": value["evidence_closure_oid"],
"artifact_sha256": value["artifact_sha256"],
"approval_plan_id": value["approval_plan_id"],
**extra,
}
def build(args: argparse.Namespace) -> tuple[TagPublisher, GitTagBackend, GiteaTagClient]:
git = GitTagBackend(args.workspace, authoritative_repo=args.repository)
gitea = GiteaTagClient(
args.gitea_api,
args.gitea_repo,
args.gitea_token_file,
token_line=args.gitea_token_line,
)
publisher = TagPublisher(
args.workspace,
git,
gitea,
args.remote,
state_root=args.state_root,
)
return publisher, git, gitea
def _gitea_lookup(gitea: GiteaTagClient, version: str) -> tuple[str, str] | None:
try:
return gitea.lookup_tag(version)
except RuntimeError as exc:
if "HTTP 404" in str(exc):
return None
raise
def probe(
args: argparse.Namespace,
value: Mapping[str, Any],
publisher: TagPublisher,
git: GitTagBackend,
gitea: GiteaTagClient,
) -> dict[str, Any]:
request = publication_request(value)
request.validate()
git.validate_evidence_lineage(request)
local_oid = git._rev(f"refs/tags/{request.version}")
local_target = git._rev(f"refs/tags/{request.version}^{{commit}}")
remote = git.remote_tag(args.remote, request.version)
api = _gitea_lookup(gitea, request.version)
listing = False
if api is not None:
listing = gitea.tag_visible_in_listing(request.version, request.target_oid)
if local_oid is None and remote is None and api is None:
return response(value, "NOT_APPLIED")
if local_oid is not None:
if local_target is None or local_oid == local_target:
return response(value, "CONFLICT", reason="local_tag_not_annotated")
if local_target != request.target_oid:
return response(
value,
"CONFLICT",
reason="local_peeled_target_mismatch",
tag_oid=local_oid,
peeled_target_oid=local_target,
)
# A remote/Gitea tag without the Authority tag object is not safe to
# recreate, because a new annotated tag would have a different object OID.
if local_oid is None and (remote is not None or api is not None):
return response(
value,
"INDETERMINATE",
reason="remote_or_gitea_tag_exists_without_authority_tag_object",
)
if remote is not None:
if remote[0] != local_oid or remote[1] != request.target_oid:
return response(
value,
"CONFLICT",
reason="network_remote_tag_mismatch",
tag_oid=local_oid,
peeled_target_oid=local_target,
remote_tag_oid=remote[0],
remote_peeled_target_oid=remote[1],
)
if api is not None:
if api[0] != local_oid or api[1] != request.target_oid:
return response(
value,
"CONFLICT",
reason="gitea_api_tag_mismatch",
tag_oid=local_oid,
peeled_target_oid=local_target,
gitea_tag_oid=api[0],
gitea_peeled_target_oid=api[1],
)
if (
local_oid is not None
and remote is not None
and api is not None
and listing
):
return response(
value,
"APPLIED_VERIFIED",
tag_oid=local_oid,
peeled_target_oid=local_target,
remote_tag_oid=remote[0],
remote_peeled_target_oid=remote[1],
gitea_tag_oid=api[0],
gitea_peeled_target_oid=api[1],
gitea_listing_visible=True,
)
if local_oid is not None and remote is None and api is None:
return response(
value,
"PARTIAL_LOCAL",
tag_oid=local_oid,
peeled_target_oid=local_target,
)
return response(
value,
"INDETERMINATE",
reason="tag_visibility_partial",
tag_oid=local_oid,
peeled_target_oid=local_target,
)
def apply(
args: argparse.Namespace,
value: Mapping[str, Any],
publisher: TagPublisher,
git: GitTagBackend,
gitea: GiteaTagClient,
) -> dict[str, Any]:
before = probe(args, value, publisher, git, gitea)
if before["status"] in {"APPLIED_VERIFIED", "CONFLICT", "INDETERMINATE"}:
return before
request = publication_request(value)
with publisher.store.lock(request.version):
request.validate()
publisher._assert_fingerprint(request)
publisher.store.record(request, PublicationState.PENDING)
publisher._verify_publication(
request,
allow_push=True,
previously_deployed=False,
)
return probe(args, value, publisher, git, gitea)
def main() -> int:
parser = argparse.ArgumentParser()
parser.add_argument("--workspace", type=Path, default=Path(os.environ["AIOS_GITOPS_CONTROLLER_WORKSPACE"]))
parser.add_argument("--repository", type=Path, default=Path(os.environ["AIOS_GITOPS_AUTHORITATIVE_GIT_DIR"]))
parser.add_argument("--remote", default=os.getenv("AIOS_GITOPS_GITEA_REMOTE", "gitea"))
parser.add_argument("--gitea-api", default=os.getenv("AIOS_GITOPS_GITEA_API_URL", ""))
parser.add_argument("--gitea-repo", default=os.getenv("AIOS_GITOPS_GITEA_REPOSITORY", ""))
parser.add_argument(
"--gitea-token-file",
type=Path,
default=Path(os.getenv("AIOS_GITOPS_GITEA_TOKEN_FILE", "")),
)
parser.add_argument(
"--gitea-token-line",
type=int,
default=int(os.getenv("AIOS_GITOPS_GITEA_TOKEN_LINE", "1")),
)
parser.add_argument(
"--state-root",
type=Path,
default=Path(
os.getenv(
"AIOS_GITOPS_PUBLICATION_STATE_DIR",
"/var/lib/aios-gitops/tag-publications",
)
),
)
args = parser.parse_args()
value = load_request()
publisher, git, gitea = build(args)
operation = value.get("operation")
if operation == "probe":
result = probe(args, value, publisher, git, gitea)
elif operation == "apply":
result = apply(args, value, publisher, git, gitea)
else:
raise ValueError("unsupported_operation")
print(json.dumps(result, sort_keys=True))
return 0
if __name__ == "__main__":
try:
raise SystemExit(main())
except PublicationManualOwnerReviewRequired as exc:
print(json.dumps({"status": "CONFLICT", "reason": str(exc)}, sort_keys=True))
raise SystemExit(20)
except PublicationConflict as exc:
print(json.dumps({"status": "CONFLICT", "reason": str(exc)}, sort_keys=True))
raise SystemExit(20)
except (PublicationPending, PublicationReconciliationRequired) as exc:
print(json.dumps({"status": "INDETERMINATE", "reason": str(exc)}, sort_keys=True))
raise SystemExit(21)
except Exception as exc:
print(
json.dumps(
{"status": "INDETERMINATE", "error": type(exc).__name__, "reason": str(exc)},
sort_keys=True,
)
)
raise SystemExit(22)
+642
View File
@@ -0,0 +1,642 @@
#!/usr/bin/env python3
"""One-shot local Owner authentication ingress for AIOS.
This module deliberately does not mint a reusable Owner token.
Authentication is based on a real local login session:
- a root-run one-shot server owns a Unix socket;
- the client must run as the configured Owner OS account;
- the server validates SO_PEERCRED, Linux audit loginuid and the controlling TTY;
- the Owner must interactively confirm the exact task/branch/base request;
- the server constructs a narrow OwnerPrincipal and invokes the existing
OwnerAuthorizationBroker;
- one successful authorization consumes the service lifetime.
The resulting principal is fixed to one non-wildcard intake scope
(`commit:create` for the current deployment) and cannot carry production,
tag, baseline, main or history-rewrite capability.
This is a trust-root ingress, not a Git writer and not a production executor.
"""
from __future__ import annotations
import argparse
import fcntl
import hashlib
import json
import os
from pathlib import Path
import pwd
import re
import secrets
import socket
import stat
import struct
import sys
import tempfile
import time
from typing import Any, Mapping
BINDING_SCHEMA = "aios.local-owner-auth-ingress.v1"
CLIENT_BEGIN_SCHEMA = "aios.local-owner-auth-client-begin.v1"
CHALLENGE_SCHEMA = "aios.local-owner-auth-challenge.v1"
CLIENT_CONFIRM_SCHEMA = "aios.local-owner-auth-client-confirm.v1"
RESULT_SCHEMA = "aios.local-owner-auth-result.v1"
ACTION = "AUTHORIZE_TASK_WRITER_PROVISIONING"
OID_RE = re.compile(r"^[0-9a-f]{40}$")
IDENTITY_RE = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._@:+/-]{1,254}$")
SCOPE_RE = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._:-]{1,127}$")
TASK_RE = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._:-]{1,255}$")
BRANCH_RE = re.compile(r"^agent/[A-Za-z0-9._/-]{1,220}$")
MAX_FRAME = 64 * 1024
CHALLENGE_TTL_SECONDS = 120
EXPECTED_ALLOWED_OPERATIONS = frozenset(
{
"create_worktree",
"create_target_branch_if_absent",
"commit_to_target_branch",
"push_target_branch",
}
)
EXPECTED_DENIED_OPERATIONS = frozenset(
{
"direct_main_push",
"force_push",
"tag_create",
"tag_update",
"tag_delete",
"branch_delete",
"repository_admin",
"baseline_write",
"production_deploy",
"production_marker_write",
"history_rewrite",
}
)
SENSITIVE_FRAGMENTS = (
"authorization_header",
"bearer",
"client_secret",
"password",
"private_key",
"raw_secret",
"raw_token",
"secret_value",
)
class BootstrapError(RuntimeError):
def __init__(self, code: str, status: int = 1) -> None:
super().__init__(code)
self.code = code
self.status = status
def canonical_json(value: Any) -> bytes:
return (
json.dumps(value, sort_keys=True, separators=(",", ":"), ensure_ascii=False)
+ "\n"
).encode("utf-8")
def sha256_json(value: Any) -> str:
return hashlib.sha256(canonical_json(value)).hexdigest()
def atomic_json(path: Path, value: Mapping[str, Any], mode: int = 0o600) -> None:
path.parent.mkdir(parents=True, exist_ok=True)
if path.parent.is_symlink() or path.is_symlink():
raise BootstrapError("STATE_SYMLINK_REJECTED")
fd, name = tempfile.mkstemp(prefix=f".{path.name}.", dir=str(path.parent))
try:
os.fchmod(fd, mode)
with os.fdopen(fd, "wb", closefd=True) as stream:
stream.write(canonical_json(dict(value)))
stream.flush()
os.fsync(stream.fileno())
os.replace(name, path)
os.chmod(path, mode)
dfd = os.open(path.parent, os.O_RDONLY | getattr(os, "O_DIRECTORY", 0))
try:
os.fsync(dfd)
finally:
os.close(dfd)
finally:
try:
os.unlink(name)
except FileNotFoundError:
pass
def secure_root_file(path: Path) -> dict[str, Any]:
if path.is_symlink() or not path.is_file():
raise BootstrapError("BINDING_FILE_INVALID")
st = path.lstat()
if st.st_uid != 0:
raise BootstrapError("BINDING_FILE_ROOT_OWNER_REQUIRED")
if stat.S_IMODE(st.st_mode) & 0o077:
raise BootstrapError("BINDING_FILE_PERMISSIONS_INVALID")
if st.st_size <= 0 or st.st_size > 1_048_576:
raise BootstrapError("BINDING_FILE_SIZE_INVALID")
try:
value = json.loads(path.read_text(encoding="utf-8"))
except (UnicodeDecodeError, json.JSONDecodeError) as exc:
raise BootstrapError("BINDING_JSON_INVALID") from exc
if not isinstance(value, dict):
raise BootstrapError("BINDING_JSON_INVALID")
return value
def validate_binding(value: Mapping[str, Any]) -> dict[str, Any]:
if set(value) != {
"schema",
"owner_identity",
"owner_role",
"authenticated_scope",
"request",
}:
raise BootstrapError("BINDING_FIELDS_REJECTED")
if value["schema"] != BINDING_SCHEMA:
raise BootstrapError("BINDING_SCHEMA_REJECTED")
identity = value["owner_identity"]
if not isinstance(identity, str) or not IDENTITY_RE.fullmatch(identity):
raise BootstrapError("OWNER_IDENTITY_INVALID")
if value["owner_role"] != "owner":
raise BootstrapError("OWNER_ROLE_INVALID")
scope = value["authenticated_scope"]
if (
not isinstance(scope, str)
or not SCOPE_RE.fullmatch(scope)
or scope == "writer:provision"
or scope == "*"
or scope.endswith(":*")
):
raise BootstrapError("OWNER_SCOPE_INVALID")
request = value["request"]
if not isinstance(request, dict):
raise BootstrapError("REQUEST_INVALID")
expected_fields = {
"action",
"task_id",
"repository",
"base_ref",
"base_oid",
"target_branch",
"one_time_use",
"expires_in_seconds",
"production_change_authorized",
"tag_change_authorized",
"baseline_change_authorized",
"main_write_authorized",
"allowed_operations",
"denied_operations",
}
if set(request) != expected_fields:
raise BootstrapError("REQUEST_FIELDS_REJECTED")
if request["action"] != ACTION:
raise BootstrapError("REQUEST_ACTION_REJECTED")
if request["repository"] != "aios/aios":
raise BootstrapError("REPOSITORY_REJECTED")
if request["base_ref"] != "refs/heads/main":
raise BootstrapError("BASE_REF_REJECTED")
if not isinstance(request["base_oid"], str) or not OID_RE.fullmatch(request["base_oid"]):
raise BootstrapError("BASE_OID_REJECTED")
if (
not isinstance(request["target_branch"], str)
or not BRANCH_RE.fullmatch(request["target_branch"])
):
raise BootstrapError("TARGET_BRANCH_REJECTED")
if (
not isinstance(request["task_id"], str)
or not TASK_RE.fullmatch(request["task_id"])
):
raise BootstrapError("TASK_ID_REJECTED")
if request["one_time_use"] is not True:
raise BootstrapError("ONE_TIME_USE_REQUIRED")
expiry = request["expires_in_seconds"]
if (
not isinstance(expiry, int)
or isinstance(expiry, bool)
or not 60 <= expiry <= 900
):
raise BootstrapError("EXPIRY_REJECTED")
for field in (
"production_change_authorized",
"tag_change_authorized",
"baseline_change_authorized",
"main_write_authorized",
):
if request[field] is not False:
raise BootstrapError("CONSEQUENTIAL_CAPABILITY_REJECTED")
if (
not isinstance(request["allowed_operations"], list)
or frozenset(request["allowed_operations"]) != EXPECTED_ALLOWED_OPERATIONS
):
raise BootstrapError("ALLOWED_OPERATIONS_MISMATCH")
if (
not isinstance(request["denied_operations"], list)
or frozenset(request["denied_operations"]) != EXPECTED_DENIED_OPERATIONS
):
raise BootstrapError("DENIED_OPERATIONS_MISMATCH")
return {
"schema": BINDING_SCHEMA,
"owner_identity": identity,
"owner_role": "owner",
"authenticated_scope": scope,
"request": dict(request),
}
def _status_uids(pid: int) -> tuple[int, int, int, int]:
try:
lines = Path(f"/proc/{pid}/status").read_text(encoding="utf-8").splitlines()
except OSError as exc:
raise BootstrapError("PEER_STATUS_UNAVAILABLE") from exc
for line in lines:
if line.startswith("Uid:"):
fields = line.split()[1:]
if len(fields) != 4:
break
return tuple(int(item) for item in fields) # type: ignore[return-value]
raise BootstrapError("PEER_UID_STATUS_INVALID")
def _loginuid(pid: int) -> int:
try:
raw = Path(f"/proc/{pid}/loginuid").read_text(encoding="ascii").strip()
value = int(raw)
except (OSError, ValueError) as exc:
raise BootstrapError("PEER_LOGINUID_UNAVAILABLE") from exc
if value == 4294967295:
raise BootstrapError("PEER_LOGIN_SESSION_REQUIRED")
return value
def _tty_path(pid: int) -> Path:
try:
value = os.readlink(f"/proc/{pid}/fd/0")
except OSError as exc:
raise BootstrapError("PEER_INTERACTIVE_TTY_REQUIRED") from exc
if not (value.startswith("/dev/pts/") or value.startswith("/dev/tty")):
raise BootstrapError("PEER_INTERACTIVE_TTY_REQUIRED")
path = Path(value)
try:
if not stat.S_ISCHR(path.stat().st_mode):
raise BootstrapError("PEER_INTERACTIVE_TTY_REQUIRED")
except OSError as exc:
raise BootstrapError("PEER_INTERACTIVE_TTY_REQUIRED") from exc
return path
def verify_peer_session(pid: int, uid: int, gid: int, owner_identity: str) -> dict[str, Any]:
try:
owner = pwd.getpwnam(owner_identity)
except KeyError as exc:
raise BootstrapError("OWNER_OS_ACCOUNT_MISSING") from exc
if uid != owner.pw_uid:
raise BootstrapError("PEER_OWNER_UID_MISMATCH")
uids = _status_uids(pid)
if any(item != owner.pw_uid for item in uids):
raise BootstrapError("PEER_UID_TRANSITION_REJECTED")
loginuid = _loginuid(pid)
if loginuid != owner.pw_uid:
# This deliberately rejects sudo/runuser from another authenticated
# account; the session itself must have logged in as the Owner.
raise BootstrapError("PEER_LOGINUID_OWNER_MISMATCH")
tty = _tty_path(pid)
if tty.stat().st_uid != owner.pw_uid:
raise BootstrapError("PEER_TTY_OWNER_MISMATCH")
return {
"pid": pid,
"uid": uid,
"gid": gid,
"loginuid": loginuid,
"tty": str(tty),
"owner_identity": owner_identity,
}
def read_frame(connection: socket.socket) -> dict[str, Any]:
data = bytearray()
while len(data) <= MAX_FRAME:
block = connection.recv(4096)
if not block:
break
data.extend(block)
if b"\n" in block:
break
if not data or len(data) > MAX_FRAME:
raise BootstrapError("PROTOCOL_FRAME_INVALID")
raw = bytes(data).split(b"\n", 1)[0]
try:
value = json.loads(raw)
except (UnicodeDecodeError, json.JSONDecodeError) as exc:
raise BootstrapError("PROTOCOL_JSON_INVALID") from exc
if not isinstance(value, dict):
raise BootstrapError("PROTOCOL_JSON_INVALID")
return value
def write_frame(connection: socket.socket, value: Mapping[str, Any]) -> None:
connection.sendall(canonical_json(dict(value)))
def safe_result(value: Mapping[str, Any]) -> dict[str, Any]:
for key in value:
lowered = str(key).lower()
if any(fragment in lowered for fragment in SENSITIVE_FRAGMENTS):
raise BootstrapError("BROKER_RESPONSE_SENSITIVE_DATA_REJECTED")
allowed = {
"status",
"approval_plan_id",
"grant_id",
"grant_digest_sha256",
"branch_oid",
"branch_ref_created",
"grant_consumed",
"owner_authorization_event_id",
"owner_authorization_reference",
"owner_authorization_digest_sha256",
"owner_authorization_consumed",
"owner_authorization_idempotent",
}
return {key: value.get(key) for key in sorted(allowed) if key in value}
def request_summary(binding: Mapping[str, Any]) -> dict[str, Any]:
request = binding["request"]
return {
"owner_identity": binding["owner_identity"],
"authenticated_scope": binding["authenticated_scope"],
"task_id": request["task_id"],
"repository": request["repository"],
"base_ref": request["base_ref"],
"base_oid": request["base_oid"],
"target_branch": request["target_branch"],
"expires_in_seconds": request["expires_in_seconds"],
"allowed_operations": sorted(request["allowed_operations"]),
"denied_operations": sorted(request["denied_operations"]),
"production_change_authorized": False,
"tag_change_authorized": False,
"baseline_change_authorized": False,
"main_write_authorized": False,
}
def _ensure_runtime_paths(socket_path: Path, state_root: Path, owner_identity: str) -> None:
if os.geteuid() != 0:
raise BootstrapError("SERVER_ROOT_REQUIRED")
owner = pwd.getpwnam(owner_identity)
for directory, mode, uid, gid in (
(socket_path.parent, 0o750, 0, owner.pw_gid),
(state_root, 0o700, 0, 0),
):
if directory.is_symlink():
raise BootstrapError("RUNTIME_DIRECTORY_SYMLINK_REJECTED")
directory.mkdir(parents=True, exist_ok=True)
os.chown(directory, uid, gid)
os.chmod(directory, mode)
if socket_path.exists() or socket_path.is_symlink():
raise BootstrapError("SOCKET_ALREADY_EXISTS")
def _lock(path: Path):
path.parent.mkdir(parents=True, exist_ok=True)
fd = os.open(path, os.O_RDWR | os.O_CREAT | getattr(os, "O_NOFOLLOW", 0), 0o600)
os.fchmod(fd, 0o600)
fcntl.flock(fd, fcntl.LOCK_EX | fcntl.LOCK_NB)
return fd
def invoke_broker(binding: Mapping[str, Any]) -> dict[str, Any]:
# Import only on the root server side. The bootstrap package does not carry
# a second broker/provisioner implementation; it reuses current main.
from owner_authorization_broker import ( # type: ignore
OwnerAuthorizationBroker,
OwnerAuthorizationBrokerConfig,
OwnerPrincipal,
)
config = OwnerAuthorizationBrokerConfig.from_environment()
identity = str(binding["owner_identity"])
scope = str(binding["authenticated_scope"])
if identity not in config.allowed_owner_identities:
raise BootstrapError("OWNER_IDENTITY_NOT_ALLOWED_BY_LIVE_BROKER")
if scope not in config.allowed_intake_scopes:
raise BootstrapError("OWNER_SCOPE_NOT_ALLOWED_BY_LIVE_BROKER")
if config.max_expiry_seconds < int(binding["request"]["expires_in_seconds"]):
raise BootstrapError("LIVE_BROKER_EXPIRY_TOO_SMALL")
principal = OwnerPrincipal(
identity=identity,
role="owner",
authenticated_scope=scope,
token_scopes=frozenset({scope}),
credential_id="",
)
return OwnerAuthorizationBroker(config=config).authorize_and_provision(
binding["request"], principal
)
def serve(binding_path: Path, socket_path: Path, state_root: Path, timeout: int) -> int:
binding = validate_binding(secure_root_file(binding_path))
identity = str(binding["owner_identity"])
_ensure_runtime_paths(socket_path, state_root, identity)
lock_fd = _lock(state_root / "bootstrap.lock")
listener = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
try:
listener.bind(str(socket_path))
owner = pwd.getpwnam(identity)
os.chown(socket_path, 0, owner.pw_gid)
os.chmod(socket_path, 0o660)
listener.listen(1)
listener.settimeout(timeout)
connection, _ = listener.accept()
with connection:
raw_peer = connection.getsockopt(
socket.SOL_SOCKET, socket.SO_PEERCRED, struct.calcsize("3i")
)
pid, uid, gid = struct.unpack("3i", raw_peer)
session = verify_peer_session(pid, uid, gid, identity)
begin = read_frame(connection)
if begin != {"schema": CLIENT_BEGIN_SCHEMA, "action": "BEGIN"}:
raise BootstrapError("CLIENT_BEGIN_REJECTED")
challenge = secrets.token_hex(12)
challenge_issued = time.monotonic()
summary = request_summary(binding)
write_frame(
connection,
{
"schema": CHALLENGE_SCHEMA,
"challenge": challenge,
"expires_in_seconds": CHALLENGE_TTL_SECONDS,
"request_digest_sha256": sha256_json(binding["request"]),
"summary": summary,
},
)
confirm = read_frame(connection)
expected_confirmation = f"AUTHORIZE {challenge}"
if (
confirm.get("schema") != CLIENT_CONFIRM_SCHEMA
or confirm.get("challenge") != challenge
or confirm.get("confirmation") != expected_confirmation
):
raise BootstrapError("OWNER_INTERACTIVE_CONFIRMATION_REJECTED")
if time.monotonic() - challenge_issued > CHALLENGE_TTL_SECONDS:
raise BootstrapError("OWNER_CONFIRMATION_EXPIRED")
result = safe_result(invoke_broker(binding))
receipt = {
"schema": "aios.local-owner-auth-ingress-receipt.v1",
"status": "CONSUMED",
"owner_identity": identity,
"owner_role": "owner",
"authenticated_scope": binding["authenticated_scope"],
"peer": session,
"request_digest_sha256": sha256_json(binding["request"]),
"binding_digest_sha256": sha256_json(binding),
"challenge_digest_sha256": hashlib.sha256(
challenge.encode("ascii")
).hexdigest(),
"broker": result,
"completed_at_unix": int(time.time()),
}
event_id = str(
result.get("owner_authorization_event_id")
or hashlib.sha256(
canonical_json(
{
"peer": session,
"request": sha256_json(binding["request"]),
}
)
).hexdigest()[:32]
)
atomic_json(state_root / "events" / f"{event_id}.json", receipt)
write_frame(
connection,
{
"schema": RESULT_SCHEMA,
"ok": True,
"result": result,
},
)
return 0
except (BootstrapError, TimeoutError, socket.timeout) as exc:
code = exc.code if isinstance(exc, BootstrapError) else "OWNER_BOOTSTRAP_TIMEOUT"
try:
if "connection" in locals():
write_frame(
connection,
{
"schema": RESULT_SCHEMA,
"ok": False,
"error": code,
},
)
except Exception:
pass
atomic_json(
state_root / "last-failure.json",
{
"schema": "aios.local-owner-auth-ingress-failure.v1",
"status": "FAILED",
"error": code,
"binding_digest_sha256": sha256_json(binding),
"failed_at_unix": int(time.time()),
},
)
return 2
finally:
listener.close()
try:
socket_path.unlink()
except FileNotFoundError:
pass
try:
fcntl.flock(lock_fd, fcntl.LOCK_UN)
finally:
os.close(lock_fd)
def client(socket_path: Path) -> int:
# The server performs the authoritative peer/session checks. The client
# also refuses non-interactive execution to make accidental automation hard.
if not sys.stdin.isatty() or not sys.stdout.isatty():
raise BootstrapError("CLIENT_INTERACTIVE_TTY_REQUIRED")
connection = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
connection.connect(str(socket_path))
with connection:
write_frame(connection, {"schema": CLIENT_BEGIN_SCHEMA, "action": "BEGIN"})
challenge = read_frame(connection)
if challenge.get("schema") != CHALLENGE_SCHEMA:
raise BootstrapError("CHALLENGE_INVALID")
summary = challenge.get("summary")
if not isinstance(summary, dict):
raise BootstrapError("CHALLENGE_INVALID")
print("\nAIOS one-shot Owner authorization request:")
print(json.dumps(summary, indent=2, sort_keys=True))
print("\nThis authorizes only the task writer shown above.")
phrase = f"AUTHORIZE {challenge['challenge']}"
typed = input(f"Type exactly:\n{phrase}\n> ").strip()
if typed != phrase:
raise BootstrapError("OWNER_CONFIRMATION_MISMATCH")
write_frame(
connection,
{
"schema": CLIENT_CONFIRM_SCHEMA,
"challenge": challenge["challenge"],
"confirmation": typed,
},
)
result = read_frame(connection)
print(json.dumps(result, indent=2, sort_keys=True))
return 0 if result.get("ok") is True else 3
def parser() -> argparse.ArgumentParser:
value = argparse.ArgumentParser()
mode = value.add_mutually_exclusive_group(required=True)
mode.add_argument("--serve", action="store_true")
mode.add_argument("--client", action="store_true")
value.add_argument("--binding", type=Path)
value.add_argument(
"--socket",
type=Path,
default=Path("/run/aios-owner-bootstrap/owner.sock"),
)
value.add_argument(
"--state-root",
type=Path,
default=Path("/var/lib/aios-gitops/owner-local-auth-ingress"),
)
value.add_argument("--timeout", type=int, default=300)
return value
def main(argv: list[str] | None = None) -> int:
args = parser().parse_args(argv)
if args.serve:
if args.binding is None:
raise SystemExit("--binding is required with --serve")
return serve(args.binding, args.socket, args.state_root, args.timeout)
return client(args.socket)
if __name__ == "__main__":
try:
raise SystemExit(main())
except BootstrapError as exc:
print(json.dumps({"status": "BLOCKED", "error": exc.code}), file=sys.stderr)
raise SystemExit(exc.status)
+225
View File
@@ -0,0 +1,225 @@
#!/usr/bin/env python3
"""AIOS durable autonomous release-campaign authorization.
The campaign receipt represents one explicit Owner policy delegation. It is not
a reusable Owner credential and it is not an unrestricted Git/production token.
Normal release operations are allowed only when all machine gates in the
campaign policy succeed. Formal tag conflicts, non-fast-forward main divergence,
history rewrite, private-signing-material exposure, repository identity conflict
or campaign revocation remain fail-closed/manual-review conditions.
"""
from __future__ import annotations
from dataclasses import dataclass
import hashlib
import json
import os
from pathlib import Path
import re
import stat
from typing import Any, Mapping
OID_RE = re.compile(r"^[0-9a-f]{40}$")
CAMPAIGN_RE = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._:-]{3,127}$")
SCHEMA = "aios.release-campaign-authorization.v1"
ALLOWED_ENGINEERING = frozenset({
"create_task_branch",
"create_worktree",
"append_commit",
"push_task_branch",
"repair_task_branch",
"create_pr",
"update_pr",
"run_required_checks",
"publish_truthful_status",
"protected_merge",
})
ALLOWED_RECONCILIATION = frozenset({
"fetch_gitea_main",
"verify_ancestor",
"authority_main_ff_cas",
"refresh_runtime_context",
})
ALLOWED_RELEASE = frozenset({
"deterministic_build",
"retain_artifact",
"materialize_approved_artifact",
"staging_install",
"health_check",
"restart_test",
"rollback_readiness_test",
"create_evidence_commit",
"generate_exact_binding",
"derive_campaign_acceptance",
})
ALLOWED_PRODUCTION = frozenset({
"create_annotated_tag",
"publish_tag",
"verify_tag",
"deploy_exact_artifact",
"verify_production_health",
"update_runtime_registry",
"update_production_baseline",
"update_baseline_sidecar",
})
DENIED = frozenset({
"force_push",
"non_fast_forward_main_write",
"direct_main_push",
"published_history_rewrite",
"formal_tag_rewrite",
"formal_tag_delete",
"overwrite_conflicting_same_name_tag",
"expose_private_signing_material",
"commit_plaintext_secret",
"repository_identity_change",
"weaken_branch_protection",
})
class CampaignError(RuntimeError):
pass
def canonical_json(value: Any) -> bytes:
return (
json.dumps(value, sort_keys=True, separators=(",", ":"), ensure_ascii=False)
+ "\n"
).encode("utf-8")
def sha256_bytes(value: bytes) -> str:
return hashlib.sha256(value).hexdigest()
def secure_json(path: Path) -> dict[str, Any]:
if path.is_symlink() or not path.is_file():
raise CampaignError(f"invalid_file:{path}")
st = path.lstat()
if st.st_uid != 0:
raise CampaignError(f"root_owner_required:{path}")
if stat.S_IMODE(st.st_mode) & 0o022:
raise CampaignError(f"group_world_writable_rejected:{path}")
try:
value = json.loads(path.read_text(encoding="utf-8"))
except (UnicodeDecodeError, json.JSONDecodeError) as exc:
raise CampaignError(f"invalid_json:{path}") from exc
if not isinstance(value, dict):
raise CampaignError(f"json_object_required:{path}")
return value
def _exact_set(value: Any, expected: frozenset[str], name: str) -> None:
if not isinstance(value, list) or frozenset(value) != expected:
raise CampaignError(f"{name}_mismatch")
def validate_policy(value: Mapping[str, Any]) -> dict[str, Any]:
required = {
"schema",
"campaign_id",
"generation",
"owner_identity",
"owner_role",
"repository",
"lineage_anchor_oid",
"control_plane_branch",
"activation_state",
"active",
"revoked",
"valid_until_revoked",
"allowed_engineering",
"allowed_reconciliation",
"allowed_release",
"allowed_production",
"denied",
"normal_owner_interaction_required",
}
if set(value) != required:
raise CampaignError("campaign_fields_invalid")
if value["schema"] != SCHEMA:
raise CampaignError("campaign_schema_invalid")
if not isinstance(value["campaign_id"], str) or not CAMPAIGN_RE.fullmatch(value["campaign_id"]):
raise CampaignError("campaign_id_invalid")
if value["generation"] != 1:
raise CampaignError("campaign_generation_invalid")
if value["owner_identity"] != "aiman" or value["owner_role"] != "owner":
raise CampaignError("campaign_owner_invalid")
if value["repository"] != "aios/aios":
raise CampaignError("campaign_repository_invalid")
if not isinstance(value["lineage_anchor_oid"], str) or not OID_RE.fullmatch(value["lineage_anchor_oid"]):
raise CampaignError("campaign_anchor_invalid")
branch = value["control_plane_branch"]
if branch != "agent/autonomous-release-campaign-v1-20260807":
raise CampaignError("campaign_branch_invalid")
if value["activation_state"] not in {"PENDING_CONTROL_PLANE_MERGE", "ACTIVE"}:
raise CampaignError("campaign_activation_state_invalid")
if value["revoked"] is not False or value["valid_until_revoked"] is not True:
raise CampaignError("campaign_lifecycle_invalid")
if value["normal_owner_interaction_required"] is not False:
raise CampaignError("campaign_owner_interaction_policy_invalid")
if value["activation_state"] == "ACTIVE" and value["active"] is not True:
raise CampaignError("campaign_active_state_invalid")
if value["activation_state"] == "PENDING_CONTROL_PLANE_MERGE" and value["active"] is not False:
raise CampaignError("campaign_pending_state_invalid")
_exact_set(value["allowed_engineering"], ALLOWED_ENGINEERING, "allowed_engineering")
_exact_set(value["allowed_reconciliation"], ALLOWED_RECONCILIATION, "allowed_reconciliation")
_exact_set(value["allowed_release"], ALLOWED_RELEASE, "allowed_release")
_exact_set(value["allowed_production"], ALLOWED_PRODUCTION, "allowed_production")
_exact_set(value["denied"], DENIED, "denied")
return dict(value)
@dataclass(frozen=True)
class CampaignAuthorization:
path: Path
data: dict[str, Any]
digest: str
@classmethod
def load(
cls,
path: Path,
*,
sidecar: Path | None = None,
revocation: Path | None = None,
require_active: bool = True,
) -> "CampaignAuthorization":
data = validate_policy(secure_json(path))
raw = path.read_bytes()
digest = sha256_bytes(raw)
sidecar = sidecar or path.with_suffix(path.suffix + ".sha256")
if not sidecar.is_file() or sidecar.is_symlink():
raise CampaignError("campaign_digest_sidecar_missing")
token = sidecar.read_text(encoding="ascii").strip().split()[0]
if token != digest:
raise CampaignError("campaign_digest_sidecar_mismatch")
if revocation and revocation.exists():
revoked = secure_json(revocation)
if revoked.get("campaign_id") == data["campaign_id"] and revoked.get("revoked") is True:
raise CampaignError("campaign_revoked")
if require_active and (data["activation_state"] != "ACTIVE" or data["active"] is not True):
raise CampaignError("campaign_not_active")
return cls(path=path, data=data, digest=digest)
@property
def campaign_id(self) -> str:
return str(self.data["campaign_id"])
@property
def anchor_oid(self) -> str:
return str(self.data["lineage_anchor_oid"])
def assert_operation(self, operation: str) -> None:
if operation in DENIED:
raise CampaignError(f"campaign_operation_denied:{operation}")
universe = (
ALLOWED_ENGINEERING
| ALLOWED_RECONCILIATION
| ALLOWED_RELEASE
| ALLOWED_PRODUCTION
)
if operation not in universe:
raise CampaignError(f"campaign_operation_out_of_scope:{operation}")
+427
View File
@@ -0,0 +1,427 @@
#!/usr/bin/env python3
"""One-interaction bootstrap for the AIOS autonomous release campaign.
A single real local `aiman` login confirmation does two things atomically at the
policy layer:
1. Writes a root-owned, digest-sidecar protected campaign authorization receipt
in PENDING_CONTROL_PLANE_MERGE state. It grants no immediate production side
effect; the campaign becomes ACTIVE only after the campaign control-plane PR
is merged and machine activation verifies lineage and implementation.
2. Invokes the existing OwnerAuthorizationBroker to issue only the narrow
engineering writer needed to implement that control-plane PR.
No reusable Owner token is minted.
"""
from __future__ import annotations
import argparse
import fcntl
import hashlib
import json
import os
from pathlib import Path
import secrets
import socket
import struct
import tempfile
import time
from typing import Any, Mapping
from owner_local_auth_ingress import (
BootstrapError,
canonical_json,
read_frame,
verify_peer_session,
write_frame,
)
from release_campaign_authorization import (
ALLOWED_ENGINEERING,
ALLOWED_PRODUCTION,
ALLOWED_RECONCILIATION,
ALLOWED_RELEASE,
DENIED,
SCHEMA,
validate_policy,
)
BEGIN = "aios.release-campaign-bootstrap.begin.v1"
CHALLENGE = "aios.release-campaign-bootstrap.challenge.v1"
CONFIRM = "aios.release-campaign-bootstrap.confirm.v1"
RESULT = "aios.release-campaign-bootstrap.result.v1"
TASK_ID = "AIOS_AUTONOMOUS_RELEASE_CAMPAIGN_V1"
TARGET_BRANCH = "agent/autonomous-release-campaign-v1-20260807"
REPOSITORY = "aios/aios"
class CampaignBootstrapError(RuntimeError):
pass
def sha256_file(path: Path) -> str:
h = hashlib.sha256()
with path.open("rb") as stream:
for block in iter(lambda: stream.read(1024 * 1024), b""):
h.update(block)
return h.hexdigest()
def atomic_json_new(path: Path, value: Mapping[str, Any]) -> None:
if path.exists() or path.is_symlink():
raise CampaignBootstrapError(f"target_exists:{path}")
path.parent.mkdir(parents=True, exist_ok=True)
fd, name = tempfile.mkstemp(prefix=f".{path.name}.", dir=path.parent)
try:
os.fchmod(fd, 0o600)
if os.geteuid() == 0:
os.fchown(fd, 0, 0)
with os.fdopen(fd, "wb", closefd=True) as stream:
stream.write(canonical_json(dict(value)))
stream.flush()
os.fsync(stream.fileno())
os.replace(name, path)
os.chmod(path, 0o600)
dfd = os.open(path.parent, os.O_RDONLY | os.O_DIRECTORY)
try:
os.fsync(dfd)
finally:
os.close(dfd)
finally:
try:
os.unlink(name)
except FileNotFoundError:
pass
def atomic_sidecar(path: Path, digest: str, target_name: str) -> None:
if path.exists() or path.is_symlink():
raise CampaignBootstrapError(f"sidecar_exists:{path}")
path.parent.mkdir(parents=True, exist_ok=True)
fd, name = tempfile.mkstemp(prefix=f".{path.name}.", dir=path.parent)
try:
os.fchmod(fd, 0o600)
if os.geteuid() == 0:
os.fchown(fd, 0, 0)
with os.fdopen(fd, "w", encoding="ascii", closefd=True) as stream:
stream.write(f"{digest} {target_name}\n")
stream.flush()
os.fsync(stream.fileno())
os.replace(name, path)
os.chmod(path, 0o600)
dfd = os.open(path.parent, os.O_RDONLY | os.O_DIRECTORY)
try:
os.fsync(dfd)
finally:
os.close(dfd)
finally:
try:
os.unlink(name)
except FileNotFoundError:
pass
def writer_request(base_oid: str) -> dict[str, Any]:
return {
"action": "AUTHORIZE_TASK_WRITER_PROVISIONING",
"task_id": TASK_ID,
"repository": REPOSITORY,
"base_ref": "refs/heads/main",
"base_oid": base_oid,
"target_branch": TARGET_BRANCH,
"one_time_use": True,
"expires_in_seconds": 300,
"production_change_authorized": False,
"tag_change_authorized": False,
"baseline_change_authorized": False,
"main_write_authorized": False,
"allowed_operations": [
"create_worktree",
"create_target_branch_if_absent",
"commit_to_target_branch",
"push_target_branch",
],
"denied_operations": [
"direct_main_push",
"force_push",
"tag_create",
"tag_update",
"tag_delete",
"branch_delete",
"repository_admin",
"baseline_write",
"production_deploy",
"production_marker_write",
"history_rewrite",
],
}
def campaign_policy(campaign_id: str, base_oid: str) -> dict[str, Any]:
value = {
"schema": SCHEMA,
"campaign_id": campaign_id,
"generation": 1,
"owner_identity": "aiman",
"owner_role": "owner",
"repository": REPOSITORY,
"lineage_anchor_oid": base_oid,
"control_plane_branch": TARGET_BRANCH,
"activation_state": "PENDING_CONTROL_PLANE_MERGE",
"active": False,
"revoked": False,
"valid_until_revoked": True,
"allowed_engineering": sorted(ALLOWED_ENGINEERING),
"allowed_reconciliation": sorted(ALLOWED_RECONCILIATION),
"allowed_release": sorted(ALLOWED_RELEASE),
"allowed_production": sorted(ALLOWED_PRODUCTION),
"denied": sorted(DENIED),
"normal_owner_interaction_required": False,
}
return validate_policy(value)
def invoke_writer_broker(base_oid: str) -> dict[str, Any]:
from owner_authorization_broker import (
OwnerAuthorizationBroker,
OwnerAuthorizationBrokerConfig,
OwnerPrincipal,
)
config = OwnerAuthorizationBrokerConfig.from_environment()
if "aiman" not in config.allowed_owner_identities:
raise CampaignBootstrapError("aiman_not_allowed_by_live_broker")
if "commit:create" not in config.allowed_intake_scopes:
raise CampaignBootstrapError("commit_create_not_allowed_by_live_broker")
principal = OwnerPrincipal(
identity="aiman",
role="owner",
authenticated_scope="commit:create",
token_scopes=frozenset({"commit:create"}),
credential_id="",
)
return OwnerAuthorizationBroker(config=config).authorize_and_provision(
writer_request(base_oid),
principal,
)
def safe_broker_result(value: Mapping[str, Any]) -> dict[str, Any]:
allowed = {
"status",
"approval_plan_id",
"grant_id",
"grant_digest_sha256",
"branch_oid",
"branch_ref_created",
"grant_consumed",
"owner_authorization_event_id",
"owner_authorization_reference",
"owner_authorization_digest_sha256",
"owner_authorization_consumed",
"owner_authorization_idempotent",
}
lowered = " ".join(str(k).lower() for k in value)
for secret_word in ("token", "password", "private_key", "secret_value", "bearer"):
if secret_word in lowered:
raise CampaignBootstrapError("broker_response_sensitive_field_rejected")
return {k: value[k] for k in sorted(allowed) if k in value}
def serve(
*,
socket_path: Path,
state_root: Path,
base_oid: str,
campaign_id: str,
timeout: int,
) -> int:
if os.geteuid() != 0:
raise CampaignBootstrapError("server_root_required")
if len(base_oid) != 40 or any(c not in "0123456789abcdef" for c in base_oid):
raise CampaignBootstrapError("base_oid_invalid")
policy = campaign_policy(campaign_id, base_oid)
campaign_dir = state_root / campaign_id
campaign_path = campaign_dir / "authorization.json"
sidecar_path = campaign_dir / "authorization.json.sha256"
socket_path.parent.mkdir(parents=True, exist_ok=True)
if socket_path.exists() or socket_path.is_symlink():
raise CampaignBootstrapError("socket_already_exists")
import pwd
owner = pwd.getpwnam("aiman")
os.chown(socket_path.parent, 0, owner.pw_gid)
os.chmod(socket_path.parent, 0o750)
listener = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
try:
listener.bind(str(socket_path))
os.chown(socket_path, 0, owner.pw_gid)
os.chmod(socket_path, 0o660)
listener.listen(1)
listener.settimeout(timeout)
conn, _ = listener.accept()
with conn:
raw = conn.getsockopt(socket.SOL_SOCKET, socket.SO_PEERCRED, struct.calcsize("3i"))
pid, uid, gid = struct.unpack("3i", raw)
peer = verify_peer_session(pid, uid, gid, "aiman")
if read_frame(conn) != {"schema": BEGIN, "action": "BEGIN"}:
raise CampaignBootstrapError("client_begin_invalid")
challenge = secrets.token_hex(12)
issued = time.monotonic()
summary = {
"campaign_id": campaign_id,
"repository": REPOSITORY,
"lineage_anchor_oid": base_oid,
"control_plane_branch": TARGET_BRANCH,
"normal_owner_interaction_after_activation": False,
"allowed_engineering": sorted(ALLOWED_ENGINEERING),
"allowed_reconciliation": sorted(ALLOWED_RECONCILIATION),
"allowed_release": sorted(ALLOWED_RELEASE),
"allowed_production": sorted(ALLOWED_PRODUCTION),
"denied": sorted(DENIED),
"writer_request": writer_request(base_oid),
}
write_frame(
conn,
{
"schema": CHALLENGE,
"challenge": challenge,
"expires_in_seconds": 120,
"summary": summary,
},
)
confirm = read_frame(conn)
phrase = f"AUTHORIZE CAMPAIGN {campaign_id} {challenge}"
if (
confirm.get("schema") != CONFIRM
or confirm.get("challenge") != challenge
or confirm.get("confirmation") != phrase
):
raise CampaignBootstrapError("campaign_confirmation_invalid")
if time.monotonic() - issued > 120:
raise CampaignBootstrapError("campaign_confirmation_expired")
# First issue the narrow engineering writer. If this fails, do not
# persist a campaign authorization that cannot be implemented.
broker = safe_broker_result(invoke_writer_broker(base_oid))
if broker.get("status") != "READY_FOR_CODE_AUTHORING":
raise CampaignBootstrapError("writer_not_ready")
atomic_json_new(campaign_path, policy)
digest = sha256_file(campaign_path)
atomic_sidecar(sidecar_path, digest, campaign_path.name)
receipt = {
"schema": "aios.release-campaign-bootstrap-receipt.v1",
"status": "CAMPAIGN_PENDING_CONTROL_PLANE_MERGE",
"campaign_id": campaign_id,
"campaign_authorization_path": str(campaign_path),
"campaign_authorization_sha256": digest,
"owner_identity": "aiman",
"owner_role": "owner",
"authentication": {
"method": "local_so_peercred_loginuid_tty",
"uid": peer["uid"],
"loginuid": peer["loginuid"],
"tty": peer["tty"],
},
"writer": broker,
"reusable_owner_token_created": False,
"production_side_effect_executed": False,
"completed_at_unix": int(time.time()),
}
receipt_path = campaign_dir / "bootstrap-receipt.json"
atomic_json_new(receipt_path, receipt)
write_frame(
conn,
{
"schema": RESULT,
"ok": True,
"campaign_id": campaign_id,
"campaign_authorization_path": str(campaign_path),
"campaign_authorization_sha256": digest,
"writer": broker,
},
)
return 0
finally:
listener.close()
try:
socket_path.unlink()
except FileNotFoundError:
pass
def client(socket_path: Path) -> int:
import sys
if not sys.stdin.isatty() or not sys.stdout.isatty():
raise CampaignBootstrapError("interactive_tty_required")
conn = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
conn.connect(str(socket_path))
with conn:
write_frame(conn, {"schema": BEGIN, "action": "BEGIN"})
challenge = read_frame(conn)
if challenge.get("schema") != CHALLENGE:
raise CampaignBootstrapError("challenge_invalid")
print("\nAIOS Autonomous Release Campaign authorization")
print(json.dumps(challenge["summary"], indent=2, sort_keys=True))
phrase = (
f"AUTHORIZE CAMPAIGN "
f"{challenge['summary']['campaign_id']} "
f"{challenge['challenge']}"
)
typed = input(f"\nType exactly:\n{phrase}\n> ").strip()
if typed != phrase:
raise CampaignBootstrapError("confirmation_mismatch")
write_frame(
conn,
{
"schema": CONFIRM,
"challenge": challenge["challenge"],
"confirmation": typed,
},
)
result = read_frame(conn)
print(json.dumps(result, indent=2, sort_keys=True))
return 0 if result.get("ok") is True else 3
def main() -> int:
p = argparse.ArgumentParser()
mode = p.add_mutually_exclusive_group(required=True)
mode.add_argument("--serve", action="store_true")
mode.add_argument("--client", action="store_true")
p.add_argument("--socket", type=Path, default=Path("/run/aios-release-campaign/owner.sock"))
p.add_argument(
"--state-root",
type=Path,
default=Path("/var/lib/aios-gitops/release-campaigns"),
)
p.add_argument("--base-oid")
p.add_argument("--campaign-id")
p.add_argument("--timeout", type=int, default=300)
a = p.parse_args()
if a.serve:
if not a.base_oid or not a.campaign_id:
raise CampaignBootstrapError("server_binding_required")
return serve(
socket_path=a.socket,
state_root=a.state_root,
base_oid=a.base_oid,
campaign_id=a.campaign_id,
timeout=a.timeout,
)
return client(a.socket)
if __name__ == "__main__":
try:
raise SystemExit(main())
except (CampaignBootstrapError, BootstrapError) as exc:
print(json.dumps({"status": "BLOCKED", "reason": str(exc)}, sort_keys=True))
raise SystemExit(2)
+113
View File
@@ -0,0 +1,113 @@
#!/usr/bin/env python3
"""Durable state model for the AIOS autonomous release campaign."""
from __future__ import annotations
from dataclasses import dataclass
import json
import os
from pathlib import Path
import tempfile
from typing import Any
STATES = (
"DEVELOPMENT",
"VALIDATING",
"PR_READY",
"MERGING",
"MAIN_RECONCILING",
"POSTMERGE_BUILDING",
"STAGING",
"EVIDENCE_CLOSING",
"BINDING_ACCEPTING",
"TAGGING",
"DEPLOYING",
"VERIFYING",
"REGISTRY_FINALIZING",
"BASELINE_FINALIZING",
"PRODUCTION_DEPLOYED",
)
RECOVERABLE = "RECOVERABLE_FAILURE"
RECONCILIATION = "RECONCILIATION_REQUIRED"
MANUAL = "MANUAL_OWNER_REVIEW_REQUIRED"
NEXT = {STATES[i]: STATES[i + 1] for i in range(len(STATES) - 1)}
MANUAL_REASONS = frozenset({
"formal_same_name_tag_conflict",
"force_push_required",
"published_history_rewrite_required",
"non_fast_forward_main_divergence",
"private_signing_material_exposure_required",
"repository_identity_conflict",
"campaign_revoked",
"operation_outside_campaign_scope",
})
class OrchestratorError(RuntimeError):
pass
@dataclass
class ReleaseState:
version: str
state: str = "DEVELOPMENT"
last_stable_state: str = "DEVELOPMENT"
recoverable_reason: str | None = None
def advance(self) -> None:
if self.state not in NEXT:
raise OrchestratorError(f"cannot_advance:{self.state}")
self.last_stable_state = self.state
self.state = NEXT[self.state]
self.recoverable_reason = None
def recoverable_failure(self, reason: str) -> None:
self.last_stable_state = self.state
self.state = RECOVERABLE
self.recoverable_reason = reason
def reconciliation_required(self, reason: str) -> None:
self.last_stable_state = self.state
self.state = RECONCILIATION
self.recoverable_reason = reason
def manual_review(self, reason: str) -> None:
if reason not in MANUAL_REASONS:
raise OrchestratorError("reason_not_manual_red_line")
self.last_stable_state = self.state
self.state = MANUAL
self.recoverable_reason = reason
def resume(self) -> None:
if self.state not in {RECOVERABLE, RECONCILIATION}:
raise OrchestratorError("state_not_resumable")
self.state = self.last_stable_state
def as_dict(self) -> dict[str, Any]:
return {
"schema": "aios.autonomous-release-state.v1",
"version": self.version,
"state": self.state,
"last_stable_state": self.last_stable_state,
"recoverable_reason": self.recoverable_reason,
"owner_interaction_required": self.state == MANUAL,
}
def atomic_state(path: Path, state: ReleaseState) -> None:
path.parent.mkdir(parents=True, exist_ok=True)
fd, name = tempfile.mkstemp(prefix=f".{path.name}.", dir=path.parent)
try:
os.fchmod(fd, 0o600)
with os.fdopen(fd, "w", encoding="utf-8", closefd=True) as stream:
json.dump(state.as_dict(), stream, sort_keys=True, indent=2)
stream.write("\n")
stream.flush()
os.fsync(stream.fileno())
os.replace(name, path)
finally:
try:
os.unlink(name)
except FileNotFoundError:
pass
+414
View File
@@ -0,0 +1,414 @@
#!/usr/bin/env python3
"""Atomic Registry/Baseline finalizer for an already ACTIVE exact release.
This adapter never writes Git refs. It is intended for the AIOS runtime
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 previous Baseline file SHA-256 (CAS)
"""
from __future__ import annotations
import argparse
import hashlib
import json
import os
import re
import stat
import subprocess
import sys
import tempfile
import time
import urllib.error
import urllib.request
from pathlib import Path
from typing import Any, Mapping
from release_version import validate_release_version
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}$")
def sha256_file(path: Path) -> str:
digest = hashlib.sha256()
with path.open("rb") as source:
for block in iter(lambda: source.read(1024 * 1024), b""):
digest.update(block)
return digest.hexdigest()
def atomic_json(path: Path, value: Mapping[str, Any], mode: int = 0o640) -> None:
path.parent.mkdir(parents=True, exist_ok=True)
# Existing runtime-authority files may be root:gitops and read-only to
# non-root readers. Preserve their owner/group/mode across os.replace().
if path.exists():
metadata = path.lstat()
if not stat.S_ISREG(metadata.st_mode) or path.is_symlink():
raise RuntimeError("ATOMIC_TARGET_NOT_REGULAR")
target_mode = stat.S_IMODE(metadata.st_mode)
target_uid = metadata.st_uid
target_gid = metadata.st_gid
else:
parent = path.parent.stat()
target_mode = mode
target_uid = 0 if os.geteuid() == 0 else os.geteuid()
target_gid = parent.st_gid
fd, name = tempfile.mkstemp(prefix=f".{path.name}.", dir=str(path.parent))
try:
with os.fdopen(fd, "w", encoding="utf-8") as output:
json.dump(dict(value), output, sort_keys=True, indent=2)
output.write("\n")
output.flush()
os.fsync(output.fileno())
os.chmod(name, target_mode)
if os.geteuid() == 0:
os.chown(name, target_uid, target_gid)
os.replace(name, path)
dfd = os.open(path.parent, os.O_RDONLY | os.O_DIRECTORY)
try:
os.fsync(dfd)
finally:
os.close(dfd)
finally:
try:
os.unlink(name)
except FileNotFoundError:
pass
def load_request() -> dict[str, Any]:
value = json.load(sys.stdin)
if not isinstance(value, dict) or value.get("schema") != "aios.production-executor-request.v1":
raise ValueError("request_schema_invalid")
if value.get("repository_identity") != "aios/aios":
raise ValueError("repository_identity_invalid")
try:
validate_release_version(value.get("version", ""))
except ValueError as exc:
raise ValueError("version_invalid") from exc
for name in ("target_oid", "evidence_closure_oid"):
if not OID_RE.fullmatch(str(value.get(name, ""))):
raise ValueError(f"{name}_invalid")
if not SHA_RE.fullmatch(str(value.get("artifact_sha256", ""))):
raise ValueError("artifact_sha256_invalid")
if not PLAN_RE.fullmatch(str(value.get("approval_plan_id", ""))):
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")
return value
def exact_response(value: Mapping[str, Any], status: str, **extra: Any) -> dict[str, Any]:
return {
"status": status,
"version": value["version"],
"target_oid": value["target_oid"],
"evidence_closure_oid": value["evidence_closure_oid"],
"artifact_sha256": value["artifact_sha256"],
"approval_plan_id": value["approval_plan_id"],
**extra,
}
def git(repository: Path, *args: str, check: bool = True) -> subprocess.CompletedProcess[str]:
return subprocess.run(
["git", f"--git-dir={repository}", *args],
check=check,
capture_output=True,
text=True,
timeout=30,
env={"PATH": os.environ.get("PATH", "")},
)
def verify_local_tag(repository: Path, value: Mapping[str, Any]) -> tuple[str, str]:
tag_oid = git(repository, "rev-parse", "--verify", f"refs/tags/{value['version']}").stdout.strip()
peeled = git(repository, "rev-parse", "--verify", f"refs/tags/{value['version']}^{{commit}}").stdout.strip()
if tag_oid == peeled or peeled != value["target_oid"]:
raise RuntimeError("FORMAL_TAG_BINDING_MISMATCH")
requested = value.get("tag_oid")
if requested and requested != tag_oid:
raise RuntimeError("FORMAL_TAG_OBJECT_MISMATCH")
return tag_oid, peeled
def health(url: str, value: Mapping[str, Any]) -> dict[str, Any]:
try:
with urllib.request.urlopen(url, timeout=5) as response:
body = json.loads(response.read().decode("utf-8"))
status = response.status
except (urllib.error.URLError, TimeoutError, ValueError) as exc:
raise RuntimeError("PRODUCTION_HEALTH_UNAVAILABLE") from exc
if not (
status == 200
and body.get("version") == value["version"]
and body.get("commit_oid") == value["target_oid"]
and body.get("artifact_sha256") == value["artifact_sha256"]
and body.get("deployment_state") == "ACTIVE"
and body.get("production_deployed") is True
):
raise RuntimeError("PRODUCTION_HEALTH_BINDING_MISMATCH")
return body
def registry_record(value: Mapping[str, Any], tag_oid: str, production_health: Mapping[str, Any]) -> dict[str, Any]:
return {
"schema": "aios.production-registry.v2",
"version": value["version"],
"repository": "aios/aios",
"target_oid": value["target_oid"],
"tag": value["version"],
"tag_oid": tag_oid,
"tag_object": tag_oid,
"peeled_target_oid": value["target_oid"],
"evidence_closure_oid": value["evidence_closure_oid"],
"artifact_sha256": value["artifact_sha256"],
"approval_plan_id": value["approval_plan_id"],
"production_service": value["production_service"],
"production_port": value["production_port"],
"registered": True,
"enabled": True,
"production_deployed": True,
"status": "PRODUCTION_DEPLOYED",
"deployment_state": production_health["deployment_state"],
"updated_at": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()),
}
def registry_matches(record: Mapping[str, Any], value: Mapping[str, Any], tag_oid: str) -> bool:
return all(
record.get(key) == expected
for key, expected in {
"version": value["version"],
"target_oid": value["target_oid"],
"tag_oid": tag_oid,
"evidence_closure_oid": value["evidence_closure_oid"],
"artifact_sha256": value["artifact_sha256"],
"approval_plan_id": value["approval_plan_id"],
"production_service": value["production_service"],
"production_port": value["production_port"],
"production_deployed": True,
}.items()
) and record.get("registered") is True and record.get("enabled") is True
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(
record.get(key) == expected
for key, expected in {
"target_oid": value["target_oid"],
"tag_oid": tag_oid,
"evidence_closure_oid": value["evidence_closure_oid"],
"artifact_sha256": value["artifact_sha256"],
"approval_plan_id": value["approval_plan_id"],
"production_service": value["production_service"],
"production_port": value["production_port"],
"status": "PRODUCTION_DEPLOYED",
}.items()
)
def registry_probe(args: argparse.Namespace, value: Mapping[str, Any]) -> dict[str, Any]:
tag_oid, _ = verify_local_tag(args.repository, value)
path = args.registry_root / f"{value['version']}.json"
if not path.exists():
return exact_response(value, "NOT_APPLIED", tag_oid=tag_oid)
record = json.loads(path.read_text(encoding="utf-8"))
if registry_matches(record, value, tag_oid):
return exact_response(
value,
"VERIFIED",
tag_oid=tag_oid,
registry_path=str(path),
registry_sha256=sha256_file(path),
)
return exact_response(value, "CONFLICT", tag_oid=tag_oid, reason="registry_binding_mismatch")
def registry_apply(args: argparse.Namespace, value: Mapping[str, Any]) -> dict[str, Any]:
existing = registry_probe(args, value)
if existing["status"] in {"VERIFIED", "CONFLICT"}:
return existing
tag_oid, _ = verify_local_tag(args.repository, value)
production_health = health(args.health_url, value)
path = args.registry_root / f"{value['version']}.json"
atomic_json(path, registry_record(value, tag_oid, production_health))
return registry_probe(args, value)
def secure_baseline_authority_file(path: Path) -> None:
if not path.is_file() or path.is_symlink():
raise RuntimeError("BASELINE_AUTHORITY_FILE_INVALID")
baseline_stat = path.lstat()
if baseline_stat.st_uid != 0 or baseline_stat.st_mode & 0o022:
raise RuntimeError("BASELINE_AUTHORITY_PERMISSIONS_INVALID")
def baseline_probe(args: argparse.Namespace, value: Mapping[str, Any]) -> dict[str, Any]:
tag_oid, _ = verify_local_tag(args.repository, value)
secure_baseline_authority_file(args.baseline_path)
record = json.loads(args.baseline_path.read_text(encoding="utf-8"))
if baseline_matches(record, value, tag_oid):
return exact_response(
value,
"VERIFIED",
tag_oid=tag_oid,
baseline_path=str(args.baseline_path),
baseline_sha256=sha256_file(args.baseline_path),
)
current = record.get("current_baseline") or record.get("version")
current_sha = sha256_file(args.baseline_path)
if (
current == args.expected_previous_baseline
and current_sha == args.expected_previous_baseline_sha256
):
return exact_response(
value,
"NOT_APPLIED",
tag_oid=tag_oid,
previous_baseline=current,
previous_baseline_sha256=current_sha,
)
return exact_response(
value,
"CONFLICT",
tag_oid=tag_oid,
reason="baseline_precondition_or_binding_mismatch",
observed_baseline=current,
observed_sha256=current_sha,
)
def baseline_apply(args: argparse.Namespace, value: Mapping[str, Any]) -> dict[str, Any]:
existing = baseline_probe(args, value)
if existing["status"] in {"VERIFIED", "CONFLICT"}:
return existing
tag_oid, _ = verify_local_tag(args.repository, value)
production_health = health(args.health_url, value)
registry_path = args.registry_root / f"{value['version']}.json"
if not registry_path.is_file():
raise RuntimeError("REGISTRY_NOT_FINALIZED")
registry = json.loads(registry_path.read_text(encoding="utf-8"))
if not registry_matches(registry, value, tag_oid):
raise RuntimeError("REGISTRY_BINDING_NOT_VERIFIED")
previous = json.loads(args.baseline_path.read_text(encoding="utf-8"))
if sha256_file(args.baseline_path) != args.expected_previous_baseline_sha256:
raise RuntimeError("BASELINE_CAS_MISMATCH")
generation = max(
int(previous.get("generation", 0) or 0),
int(previous.get("authority_generation", 0) or 0),
) + 1
record = {
"schema": "aios.production-baseline.v2",
"source_identity": "aios-gitops-runtime-anchor-baseline-authority-v1",
"current_baseline": value["version"],
"version": value["version"],
"previous_baseline": args.expected_previous_baseline,
"previous_version": args.expected_previous_baseline,
"status": "PRODUCTION_DEPLOYED",
"target_oid": value["target_oid"],
"peeled_target_oid": value["target_oid"],
"tag_oid": tag_oid,
"evidence_closure_oid": value["evidence_closure_oid"],
"approval_plan_id": value["approval_plan_id"],
"artifact_sha256": value["artifact_sha256"],
"production_artifact_sha256": value["artifact_sha256"],
"production_service": value["production_service"],
"production_port": value["production_port"],
"production_deployed": True,
"deployment_state": production_health["deployment_state"],
"registry_digest": sha256_file(registry_path),
"registry_source": str(registry_path),
"generation": generation,
"authority_generation": generation,
# The actual previously deployed production baseline is the rollback
# target. V2R1C6 has a formal tag but was never production-deployed.
"rollback_version": args.expected_previous_baseline,
"rollback_tag_oid": previous.get("tag_oid"),
"rollback_target_oid": previous.get("target_oid"),
"promoted_at": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()),
"promoted_by": "aios-approved-production-release-executor",
}
atomic_json(args.baseline_path, record)
return baseline_probe(args, value)
def main() -> int:
parser = argparse.ArgumentParser()
parser.add_argument(
"--mode",
choices=("registry", "baseline"),
default=os.environ.get("AIOS_RELEASE_METADATA_MODE"),
required=os.environ.get("AIOS_RELEASE_METADATA_MODE") is None,
)
parser.add_argument(
"--repository",
type=Path,
default=Path(os.environ["AIOS_GITOPS_AUTHORITATIVE_GIT_DIR"]),
)
parser.add_argument(
"--registry-root",
type=Path,
default=Path(os.environ["AIOS_RUNTIME_REGISTRY_ROOT"]),
)
parser.add_argument(
"--baseline-path",
type=Path,
default=Path(os.environ["AIOS_RUNTIME_BASELINE_PATH"]),
)
parser.add_argument(
"--expected-previous-baseline",
default=os.environ.get("AIOS_PREVIOUS_BASELINE", "V2R1C5"),
)
parser.add_argument(
"--expected-previous-baseline-sha256",
default=os.environ.get("AIOS_PREVIOUS_BASELINE_SHA256"),
required=os.environ.get("AIOS_PREVIOUS_BASELINE_SHA256") is None,
)
parser.add_argument(
"--health-url",
default=os.environ.get("AIOS_RELEASE_HEALTH_URL", "http://127.0.0.1:18927/health"),
)
args = parser.parse_args()
value = load_request()
operation = value.get("operation")
if args.mode == "registry":
if operation == "probe":
result = registry_probe(args, value)
elif operation == "apply":
result = registry_apply(args, value)
else:
raise ValueError("unsupported_operation")
else:
if operation == "probe":
result = baseline_probe(args, value)
elif operation == "apply":
result = baseline_apply(args, value)
else:
raise ValueError("unsupported_operation")
print(json.dumps(result, sort_keys=True))
return 0
if __name__ == "__main__":
try:
raise SystemExit(main())
except Exception as exc:
print(
json.dumps(
{"status": "FAIL_CLOSED", "error": type(exc).__name__, "reason": str(exc)},
sort_keys=True,
)
)
raise SystemExit(2)
+184
View File
@@ -0,0 +1,184 @@
"""Durable, generation-scoped leases for the staging governed writer."""
from __future__ import annotations
from datetime import datetime, timezone
import hashlib
import json
from pathlib import Path
import sqlite3
from typing import Any, Mapping
def _now() -> datetime:
return datetime.now(timezone.utc)
def _iso(value: datetime) -> str:
return value.isoformat()
def _expired(value: str) -> bool:
return datetime.fromisoformat(value).astimezone(timezone.utc) <= _now()
class ScopedLeaseError(RuntimeError):
pass
class ScopedLeaseStore:
"""A small SQLite CAS boundary; no repository or filesystem writes."""
def __init__(self, path: Path) -> None:
if not path.is_absolute() or path.is_symlink():
raise ScopedLeaseError("LEASE_STORE_INVALID")
path.parent.mkdir(parents=True, exist_ok=True)
self.path = path
with self._connect() as db:
db.executescript(
"""
CREATE TABLE IF NOT EXISTS scoped_leases (
lease_key TEXT PRIMARY KEY,
task_id TEXT NOT NULL,
repository TEXT NOT NULL,
target_branch TEXT NOT NULL,
expected_branch_oid TEXT NOT NULL,
generation INTEGER NOT NULL,
binding_json TEXT NOT NULL,
expires_at TEXT NOT NULL,
state TEXT NOT NULL,
idempotency_key TEXT,
result_json TEXT,
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL
);
CREATE INDEX IF NOT EXISTS scoped_leases_binding
ON scoped_leases(task_id, repository, target_branch, generation);
"""
)
def _connect(self) -> sqlite3.Connection:
db = sqlite3.connect(self.path, timeout=30.0)
db.row_factory = sqlite3.Row
db.execute("PRAGMA journal_mode=WAL")
db.execute("PRAGMA synchronous=FULL")
db.execute("PRAGMA busy_timeout=30000")
return db
@staticmethod
def key(request: Mapping[str, Any]) -> str:
binding = {
field: request[field]
for field in (
"task_id", "repository", "branch_mode", "base_oid",
"expected_branch_oid", "target_branch", "generation",
"lease_seconds",
)
}
return hashlib.sha256(
json.dumps(binding, sort_keys=True, separators=(",", ":")).encode()
).hexdigest()
@staticmethod
def _row(row: sqlite3.Row) -> dict[str, Any]:
value = dict(row)
value["binding"] = json.loads(value.pop("binding_json"))
if value.get("result_json"):
value["result"] = json.loads(value.pop("result_json"))
else:
value.pop("result_json", None)
return value
def acquire(self, request: Mapping[str, Any]) -> tuple[dict[str, Any], bool]:
key = self.key(request)
now = _now()
expires = now.timestamp() + int(request["lease_seconds"])
expires_at = _iso(datetime.fromtimestamp(expires, timezone.utc))
binding_json = json.dumps(dict(request), sort_keys=True, separators=(",", ":"))
try:
with self._connect() as db:
db.execute("BEGIN IMMEDIATE")
exact = db.execute(
"SELECT * FROM scoped_leases WHERE lease_key=?", (key,)
).fetchone()
if exact is not None:
if exact["state"] == "ACTIVE" and _expired(exact["expires_at"]):
db.execute(
"UPDATE scoped_leases SET state='EXPIRED',updated_at=? WHERE lease_key=?",
(_iso(now), key),
)
exact = db.execute(
"SELECT * FROM scoped_leases WHERE lease_key=?", (key,)
).fetchone()
db.commit()
return self._row(exact), False
active = db.execute(
"SELECT * FROM scoped_leases WHERE task_id=? AND repository=? AND target_branch=? AND state='ACTIVE'",
(str(request["task_id"]), str(request["repository"]), str(request["target_branch"])),
).fetchall()
for row in active:
if _expired(row["expires_at"]):
db.execute(
"UPDATE scoped_leases SET state='EXPIRED',updated_at=? WHERE lease_key=?",
(_iso(now), row["lease_key"]),
)
else:
raise ScopedLeaseError("LEASE_BINDING_CONFLICT")
db.execute(
"INSERT INTO scoped_leases(lease_key,task_id,repository,target_branch,expected_branch_oid,generation,binding_json,expires_at,state,created_at,updated_at) VALUES(?,?,?,?,?,?,?,?,?,?,?)",
(
key, request["task_id"], request["repository"],
request["target_branch"], request["expected_branch_oid"],
int(request["generation"]), binding_json, expires_at,
"ACTIVE", _iso(now), _iso(now),
),
)
row = db.execute(
"SELECT * FROM scoped_leases WHERE lease_key=?", (key,)
).fetchone()
db.commit()
return self._row(row), True
except ScopedLeaseError:
raise
except sqlite3.Error as exc:
raise ScopedLeaseError("LEASE_STORE_UNAVAILABLE") from exc
def for_commit(self, request: Mapping[str, Any]) -> dict[str, Any] | None:
try:
with self._connect() as db:
row = db.execute(
"SELECT * FROM scoped_leases WHERE task_id=? AND repository=? AND target_branch=? AND expected_branch_oid=? AND generation=? ORDER BY created_at DESC LIMIT 1",
(
request["task_id"], request["repository"],
request["target_branch"], request["expected_branch_oid"],
int(request["generation"]),
),
).fetchone()
if row is None:
return None
if row["state"] == "ACTIVE" and _expired(row["expires_at"]):
db.execute(
"UPDATE scoped_leases SET state='EXPIRED',updated_at=? WHERE lease_key=?",
(_iso(_now()), row["lease_key"]),
)
row = db.execute(
"SELECT * FROM scoped_leases WHERE lease_key=?", (row["lease_key"],)
).fetchone()
return self._row(row)
except sqlite3.Error as exc:
raise ScopedLeaseError("LEASE_STORE_UNAVAILABLE") from exc
def mark_used(self, lease_key: str, idempotency_key: str, result: Mapping[str, Any]) -> None:
with self._connect() as db:
updated = db.execute(
"UPDATE scoped_leases SET state='USED',idempotency_key=?,result_json=?,updated_at=? WHERE lease_key=? AND state='ACTIVE'",
(
idempotency_key, json.dumps(dict(result), sort_keys=True),
_iso(_now()), lease_key,
),
)
if updated.rowcount != 1:
raise ScopedLeaseError("LEASE_ALREADY_USED")
def delete(self, lease_key: str) -> None:
with self._connect() as db:
db.execute("DELETE FROM scoped_leases WHERE lease_key=? AND state='ACTIVE'", (lease_key,))
@@ -22,11 +22,12 @@ from enum import Enum
from pathlib import Path from pathlib import Path
from typing import Any, Protocol from typing import Any, Protocol
from release_version import validate_release_version
OID_RE = re.compile(r"^[0-9a-f]{40}$") OID_RE = re.compile(r"^[0-9a-f]{40}$")
SHA256_RE = re.compile(r"^[0-9a-f]{64}$") SHA256_RE = re.compile(r"^[0-9a-f]{64}$")
CANDIDATE_ID_RE = re.compile(r"^[A-Za-z0-9][A-Za-z0-9_.-]{2,127}$") CANDIDATE_ID_RE = re.compile(r"^[A-Za-z0-9][A-Za-z0-9_.-]{2,127}$")
VERSION_RE = re.compile(r"^V\d+R\d+C\d+(?:-[A-Za-z0-9_.-]+)?$")
SECRET_PATTERNS = ( SECRET_PATTERNS = (
re.compile(r"(?i)(authorization\s*[:=]\s*)(?:bearer|token)\s+\S+"), re.compile(r"(?i)(authorization\s*[:=]\s*)(?:bearer|token)\s+\S+"),
re.compile(r"(?i)((?:access_)?token\s*[:=]\s*)\S+"), re.compile(r"(?i)((?:access_)?token\s*[:=]\s*)\S+"),
@@ -769,8 +770,10 @@ class TagApprovalEnvelope:
def validate(self) -> None: def validate(self) -> None:
if self.action != "production_tag": if self.action != "production_tag":
raise ApprovalError("approval does not authorize production tag") raise ApprovalError("approval does not authorize production tag")
if not VERSION_RE.fullmatch(self.version): try:
raise ApprovalError("invalid release version") validate_release_version(self.version)
except ValueError as exc:
raise ApprovalError("invalid release version") from exc
_validate_oid("tag_oid", self.tag_oid) _validate_oid("tag_oid", self.tag_oid)
_validate_oid("target_oid", self.target_oid) _validate_oid("target_oid", self.target_oid)
if _parse_utc(self.expires_at) <= datetime.now(timezone.utc): if _parse_utc(self.expires_at) <= datetime.now(timezone.utc):
@@ -3457,6 +3457,19 @@ class SilentReleaseActionService:
} }
def get_release_saga(self, candidate_id: str) -> dict[str, Any]: def get_release_saga(self, candidate_id: str) -> dict[str, Any]:
# A read of a never-created candidate is a normal empty-state. Do
# not turn a missing journal into a durable manual-review snapshot.
saga_path = self.sagas._path(candidate_id)
journal_path = self.sagas._journal(candidate_id)
if not saga_path.exists() and not journal_path.exists():
return {
"candidate_id": candidate_id,
"status": "NOT_FOUND",
"state": "NOT_FOUND",
"execution_enabled": self.execution_enabled,
"shadow_mode": not self.execution_enabled,
"side_effects_executed": False,
}
return { return {
**self.sagas.load(candidate_id).to_dict(), **self.sagas.load(candidate_id).to_dict(),
"execution_enabled": self.execution_enabled, "execution_enabled": self.execution_enabled,
+455
View File
@@ -0,0 +1,455 @@
#!/usr/bin/env python3
"""Concrete V2R1C7 production deployment adapter.
The adapter consumes only an already approved immutable artifact. It never
writes Git refs, Registry, or Baseline. A rollback only tears down the V2R1C7
runtime and leaves V2R1C5/V2R1C6 untouched.
"""
from __future__ import annotations
import argparse
import grp
import hashlib
import json
import os
import pwd
import shutil
import stat
import subprocess
import sys
import tarfile
import tempfile
import time
import urllib.error
import urllib.request
from pathlib import Path
from typing import Any, Mapping
VERSION = "V2R1C7"
SERVICE = "aios-v2r1c7-production.service"
PORT = 18927
RUNTIME_USER = "aios-v2r1c7"
RUNTIME_GROUP = "aios-v2r1c7"
REQUIRED_MEMBERS = {
"payload/VERSION",
"payload/active-runtime/app/deployment_state.py",
"payload/active-runtime/app/v2r1c6_privacy_tools.py",
"payload/active-runtime/app/v2r1c7_health_service.py",
"payload/deploy/aios-v2/systemd/aios-v2r1c7-production.service",
"payload/deploy/aios-v2/v2r1c7-runtime-config.json",
"payload/deploy/aios-v2/v2r1c7-rollback.json",
"payload/scripts/probe_v2r1c7_health.py",
"payload/scripts/rollback_v2r1c7_release.sh",
"payload/BUILD-METADATA.json",
"payload/ARTIFACT-MANIFEST.json",
"payload/ROLLBACK-METADATA.json",
}
def load_request() -> dict[str, Any]:
value = json.load(sys.stdin)
if not isinstance(value, dict):
raise ValueError("request_object_required")
if value.get("schema") != "aios.production-executor-request.v1":
raise ValueError("request_schema_invalid")
if value.get("version") != VERSION:
raise ValueError("version_mismatch")
if value.get("production_service") != SERVICE:
raise ValueError("service_mismatch")
if int(value.get("production_port", -1)) != PORT:
raise ValueError("port_mismatch")
return value
def sha256_file(path: Path) -> str:
if path.is_symlink() or not path.is_file():
raise ValueError("artifact_regular_file_required")
digest = hashlib.sha256()
with path.open("rb") as stream:
for chunk in iter(lambda: stream.read(1024 * 1024), b""):
digest.update(chunk)
return digest.hexdigest()
def atomic_json(path: Path, value: Mapping[str, Any], *, mode: int, gid: int | None = None) -> None:
path.parent.mkdir(parents=True, exist_ok=True)
temporary = path.with_name(path.name + ".tmp")
temporary.write_text(
json.dumps(dict(value), sort_keys=True, separators=(",", ":")) + "\n",
encoding="utf-8",
)
os.chmod(temporary, mode)
if gid is not None:
os.chown(temporary, 0, gid)
os.replace(temporary, path)
def atomic_text(path: Path, text: str, *, mode: int, gid: int | None = None) -> None:
path.parent.mkdir(parents=True, exist_ok=True)
temporary = path.with_name(path.name + ".tmp")
temporary.write_text(text, encoding="utf-8")
os.chmod(temporary, mode)
if gid is not None:
os.chown(temporary, 0, gid)
os.replace(temporary, path)
def systemctl(*args: str, check: bool = True) -> subprocess.CompletedProcess[str]:
return subprocess.run(
["systemctl", *args],
check=check,
capture_output=True,
text=True,
timeout=45,
env={"PATH": os.environ.get("PATH", "")},
)
def identity() -> tuple[int, int]:
try:
user = pwd.getpwnam(RUNTIME_USER)
group = grp.getgrnam(RUNTIME_GROUP)
except KeyError as exc:
raise RuntimeError("RUNTIME_IDENTITY_MISSING") from exc
if user.pw_gid != group.gr_gid:
raise RuntimeError("RUNTIME_IDENTITY_GROUP_MISMATCH")
return user.pw_uid, group.gr_gid
def safe_extract(artifact: Path, destination: Path) -> set[str]:
with tempfile.TemporaryDirectory(prefix="aios-v2r1c7-unpack-") as temp:
tar_path = Path(temp) / "release.tar"
with tar_path.open("wb") as output:
completed = subprocess.run(
["zstd", "-dc", str(artifact)],
check=False,
stdout=output,
stderr=subprocess.PIPE,
timeout=60,
env={"PATH": os.environ.get("PATH", "")},
)
if completed.returncode != 0:
raise RuntimeError("ARTIFACT_DECOMPRESSION_FAILED")
with tarfile.open(tar_path, "r:") as archive:
members = archive.getmembers()
names = {m.name for m in members}
missing = REQUIRED_MEMBERS - names
if missing:
raise RuntimeError("ARTIFACT_MEMBERS_MISSING:" + ",".join(sorted(missing)))
for member in members:
relative = Path(member.name)
if (
relative.is_absolute()
or ".." in relative.parts
or member.issym()
or member.islnk()
or not (member.isfile() or member.isdir())
):
raise RuntimeError("UNSAFE_ARTIFACT_MEMBER:" + member.name)
if not member.name.startswith("payload/"):
raise RuntimeError("UNEXPECTED_ARTIFACT_ROOT:" + member.name)
payload = destination
payload.mkdir(parents=True, exist_ok=True)
for member in members:
rel = Path(member.name).relative_to("payload")
if str(rel) == ".":
continue
target = payload / rel
resolved_parent = target.parent.resolve()
resolved_parent.relative_to(payload.resolve())
if member.isdir():
target.mkdir(parents=True, exist_ok=True)
os.chmod(target, 0o755)
continue
target.parent.mkdir(parents=True, exist_ok=True)
source = archive.extractfile(member)
if source is None:
raise RuntimeError("ARTIFACT_MEMBER_READ_FAILED")
with target.open("wb") as output:
shutil.copyfileobj(source, output)
# Scripts must be executable; all other payload files are read-only.
mode = 0o755 if rel.parts and rel.parts[0] == "scripts" else 0o644
os.chmod(target, mode)
return names
def health(url: str, expected: Mapping[str, Any]) -> dict[str, Any]:
try:
with urllib.request.urlopen(url, timeout=5) as response:
body = json.loads(response.read().decode("utf-8"))
status = response.status
except (urllib.error.URLError, TimeoutError, ValueError):
return {"status": "UNAVAILABLE"}
exact = (
status == 200
and body.get("version") == expected["version"]
and body.get("commit_oid") == expected["target_oid"]
and body.get("artifact_sha256") == expected["artifact_sha256"]
and body.get("deployment_state") == "ACTIVE"
and body.get("production_deployed") is True
)
if not exact:
return {
"status": "CONFLICT",
"http_status": status,
"version": body.get("version"),
"target_oid": body.get("commit_oid"),
"artifact_sha256": body.get("artifact_sha256"),
"deployment_state": body.get("deployment_state"),
"production_deployed": body.get("production_deployed"),
}
return {
"status": "ACTIVE_VERIFIED",
"http_status": 200,
"version": body["version"],
"target_oid": body["commit_oid"],
"artifact_sha256": body["artifact_sha256"],
"deployment_state": body["deployment_state"],
"production_deployed": True,
}
def paths(args: argparse.Namespace, value: Mapping[str, Any]) -> dict[str, Path]:
release_id = (
f"{VERSION}-{str(value['target_oid'])[:12]}-"
f"{str(value['artifact_sha256'])[:12]}"
)
return {
"release_dir": args.release_root / release_id,
"current_link": args.current_link,
"env_file": args.env_file,
"state_file": args.state_file,
"unit_file": args.unit_file,
}
def probe(args: argparse.Namespace, value: Mapping[str, Any]) -> dict[str, Any]:
p = paths(args, value)
url = f"http://127.0.0.1:{PORT}/health"
active = systemctl("is-active", "--quiet", SERVICE, check=False).returncode == 0
if not active:
# A stale exact release directory is safe to reuse; it is not a deployment.
return {
"status": "NOT_APPLIED",
"version": VERSION,
"target_oid": value["target_oid"],
"artifact_sha256": value["artifact_sha256"],
"production_deployed": False,
"deployment_state": "ROLLED_BACK"
if p["state_file"].exists()
and "ROLLED_BACK" in p["state_file"].read_text(encoding="utf-8")
else "ABSENT",
}
result = health(url, value)
if result["status"] == "UNAVAILABLE":
return {
"status": "CONFLICT",
"version": VERSION,
"target_oid": value["target_oid"],
"artifact_sha256": value["artifact_sha256"],
"reason": "service_active_health_unavailable",
}
return result
def install_release(args: argparse.Namespace, value: Mapping[str, Any]) -> dict[str, Any]:
_, gid = identity()
artifact = Path(str(value["artifact_path"]))
if artifact.stat().st_size != int(value["artifact_size"]):
raise RuntimeError("ARTIFACT_SIZE_MISMATCH")
if sha256_file(artifact) != value["artifact_sha256"]:
raise RuntimeError("ARTIFACT_DIGEST_MISMATCH")
p = paths(args, value)
release_dir = p["release_dir"]
marker = release_dir / "DEPLOYMENT-BINDING.json"
marker_value = {
"schema": "aios.runtime-release-binding.v1",
"version": VERSION,
"target_oid": value["target_oid"],
"artifact_sha256": value["artifact_sha256"],
"approval_plan_id": value["approval_plan_id"],
}
if release_dir.exists():
if not marker.is_file():
raise RuntimeError("EXISTING_RELEASE_DIR_UNTRUSTED")
current_marker = json.loads(marker.read_text(encoding="utf-8"))
if current_marker != marker_value:
raise RuntimeError("EXISTING_RELEASE_DIR_BINDING_CONFLICT")
else:
release_dir.parent.mkdir(parents=True, exist_ok=True)
temp_dir = Path(
tempfile.mkdtemp(prefix=release_dir.name + ".", dir=str(release_dir.parent))
)
try:
safe_extract(artifact, temp_dir)
atomic_json(
temp_dir / "DEPLOYMENT-BINDING.json",
marker_value,
mode=0o644,
)
os.rename(temp_dir, release_dir)
except Exception:
shutil.rmtree(temp_dir, ignore_errors=True)
raise
if (release_dir / "VERSION").read_text(encoding="utf-8").strip() != VERSION:
raise RuntimeError("RELEASE_VERSION_FILE_MISMATCH")
# Only replace our dedicated V2R1C7 link. Never touch previous versions.
current_link = p["current_link"]
if current_link.exists() and not current_link.is_symlink():
raise RuntimeError("CURRENT_RELEASE_PATH_NOT_SYMLINK")
if current_link.is_symlink():
existing = current_link.resolve()
if existing != release_dir.resolve():
raise RuntimeError("CURRENT_RELEASE_LINK_CONFLICT")
else:
current_link.parent.mkdir(parents=True, exist_ok=True)
new_link = current_link.with_name(current_link.name + ".new")
try:
new_link.unlink()
except FileNotFoundError:
pass
new_link.symlink_to(release_dir)
os.replace(new_link, current_link)
state_value = {
"version": VERSION,
"commit_oid": value["target_oid"],
"artifact_sha256": value["artifact_sha256"],
"deployment_state": "ACTIVE",
}
atomic_json(p["state_file"], state_value, mode=0o640, gid=gid)
env = (
f"RELEASE_VERSION={VERSION}\n"
f"TARGET_OID={value['target_oid']}\n"
f"ARTIFACT_SHA256={value['artifact_sha256']}\n"
"DEPLOYMENT_STATE=ACTIVE\n"
f"AIOS_V2R1C7_PORT={PORT}\n"
f"DEPLOYMENT_STATE_FILE={p['state_file']}\n"
"AIOS_GITOPS_WRITES_ENABLED=false\n"
)
atomic_text(p["env_file"], env, mode=0o640, gid=gid)
unit_source = (
release_dir
/ "deploy"
/ "aios-v2"
/ "systemd"
/ "aios-v2r1c7-production.service"
)
unit_text = unit_source.read_text(encoding="utf-8")
if "aios-v2r1c7-production.service" in unit_text:
# Description text is irrelevant; service identity is the destination.
pass
atomic_text(p["unit_file"], unit_text, mode=0o644)
verify = subprocess.run(
["systemd-analyze", "verify", str(p["unit_file"])],
check=False,
capture_output=True,
text=True,
timeout=30,
env={"PATH": os.environ.get("PATH", "")},
)
if verify.returncode != 0:
raise RuntimeError("PRODUCTION_SYSTEMD_VERIFY_FAILED")
systemctl("daemon-reload")
systemctl("enable", SERVICE)
systemctl("restart", SERVICE)
deadline = time.time() + args.health_timeout
result = {"status": "UNAVAILABLE"}
while time.time() < deadline:
result = health(f"http://127.0.0.1:{PORT}/health", value)
if result["status"] == "ACTIVE_VERIFIED":
break
if result["status"] == "CONFLICT":
break
time.sleep(1)
if result["status"] != "ACTIVE_VERIFIED":
raise RuntimeError("PRODUCTION_HEALTH_VERIFY_FAILED")
# Restart recovery is part of production acceptance, not just staging.
systemctl("restart", SERVICE)
deadline = time.time() + args.health_timeout
while time.time() < deadline:
result = health(f"http://127.0.0.1:{PORT}/health", value)
if result["status"] == "ACTIVE_VERIFIED":
break
if result["status"] == "CONFLICT":
break
time.sleep(1)
if result["status"] != "ACTIVE_VERIFIED":
raise RuntimeError("PRODUCTION_RESTART_RECOVERY_FAILED")
return result
def rollback(args: argparse.Namespace, value: Mapping[str, Any]) -> dict[str, Any]:
_, gid = identity()
p = paths(args, value)
systemctl("stop", SERVICE, check=False)
systemctl("disable", SERVICE, check=False)
state = {
"version": VERSION,
"commit_oid": value["target_oid"],
"artifact_sha256": value["artifact_sha256"],
"deployment_state": "ROLLED_BACK",
}
atomic_json(p["state_file"], state, mode=0o640, gid=gid)
return {
"status": "ROLLED_BACK",
"version": VERSION,
"target_oid": value["target_oid"],
"artifact_sha256": value["artifact_sha256"],
"deployment_state": "ROLLED_BACK",
"production_deployed": False,
}
def main() -> int:
p = argparse.ArgumentParser()
p.add_argument("--release-root", type=Path, default=Path("/opt/aios/releases"))
p.add_argument("--current-link", type=Path, default=Path("/opt/aios/v2r1c7"))
p.add_argument("--env-file", type=Path, default=Path("/etc/aios/aios-v2r1c7.env"))
p.add_argument(
"--state-file",
type=Path,
default=Path("/var/lib/aios/v2r1c7/deployment-state.json"),
)
p.add_argument(
"--unit-file",
type=Path,
default=Path("/etc/systemd/system/aios-v2r1c7-production.service"),
)
p.add_argument("--health-timeout", type=int, default=30)
args = p.parse_args()
value = load_request()
operation = value.get("operation")
if operation == "probe":
result = probe(args, value)
elif operation == "apply":
result = install_release(args, value)
elif operation == "rollback":
result = rollback(args, value)
else:
raise ValueError("unsupported_operation")
print(json.dumps(result, sort_keys=True))
return 0
if __name__ == "__main__":
try:
raise SystemExit(main())
except Exception as exc:
print(
json.dumps(
{"status": "FAIL_CLOSED", "error": type(exc).__name__, "reason": str(exc)},
sort_keys=True,
)
)
raise SystemExit(2)
@@ -0,0 +1,135 @@
"""V2R2C1 adapter using the existing V2R1C7 deployment primitives.
The service identity and loopback port remain stable for a safe cutover. The
new release is installed under its own immutable directory and keeps the
previous V2R1C7 release available for an explicit, owner-approved rollback.
"""
from __future__ import annotations
import json
import os
import shutil
import sys
from argparse import Namespace
from pathlib import Path
from typing import Any, Mapping
import v2r1c7_production_adapter as base
VERSION = "V2R2C1"
SERVICE = "aios-v2r1c7-production.service"
PORT = 18927
RELEASE_ROOT = Path("/opt/aios/releases")
CURRENT_LINK = Path("/opt/aios/v2r2c1")
ENV_FILE = Path("/etc/aios/aios-v2r2c1.env")
STATE_FILE = Path("/var/lib/aios/v2r2c1/deployment-state.json")
UNIT_FILE = Path("/etc/systemd/system/aios-v2r1c7-production.service")
ROLLBACK_ROOT = Path("/var/lib/aios/v2r2c1/rollback")
REQUIRED_MEMBERS = {
"payload/VERSION",
"payload/active-runtime/app/deployment_state.py",
"payload/active-runtime/app/v2r1c6_privacy_tools.py",
"payload/active-runtime/app/v2r2c1_health_service.py",
"payload/deploy/aios-v2/systemd/aios-v2r1c7-production.service",
"payload/deploy/aios-v2/v2r2c1-runtime-config.json",
"payload/deploy/aios-v2/v2r2c1-rollback.json",
"payload/scripts/probe_v2r2c1_health.py",
"payload/scripts/rollback_v2r2c1_release.sh",
"payload/BUILD-METADATA.json",
"payload/ARTIFACT-MANIFEST.json",
"payload/ROLLBACK-METADATA.json",
}
def _configure_base() -> None:
base.VERSION = VERSION
base.SERVICE = SERVICE
base.PORT = PORT
base.REQUIRED_MEMBERS = REQUIRED_MEMBERS
def _args() -> Namespace:
return Namespace(
release_root=RELEASE_ROOT,
current_link=CURRENT_LINK,
env_file=ENV_FILE,
state_file=STATE_FILE,
unit_file=UNIT_FILE,
health_timeout=45,
)
def load_request() -> dict[str, Any]:
_configure_base()
value = base.load_request()
if value.get("version") != VERSION or value.get("production_service") != SERVICE or int(value.get("production_port", -1)) != PORT:
raise ValueError("V2R2C1 production binding mismatch")
return value
def _backup(path: Path, name: str) -> None:
if path.is_file() and not path.is_symlink():
ROLLBACK_ROOT.mkdir(parents=True, exist_ok=True)
destination = ROLLBACK_ROOT / name
if not destination.exists():
shutil.copy2(path, destination)
def execute_request(value: Mapping[str, Any]) -> dict[str, Any]:
_configure_base()
if value.get("schema") != "aios.production-executor-request.v1":
raise ValueError("request_schema_invalid")
if value.get("operation") == "apply":
_backup(UNIT_FILE, "aios-v2r1c7-production.service.previous")
_backup(Path("/etc/aios/aios-v2r1c7.env"), "aios-v2r1c7.env.previous")
_backup(Path("/var/lib/aios/v2r1c7/deployment-state.json"), "v2r1c7-deployment-state.previous")
return base.install_release(_args(), value)
if value.get("operation") == "probe":
return base.probe(_args(), value)
if value.get("operation") == "rollback":
return rollback(value)
raise ValueError("unsupported_operation")
def rollback(value: Mapping[str, Any]) -> dict[str, Any]:
"""Explicit rollback only; this adapter never invokes it automatically."""
import subprocess
subprocess.run(["systemctl", "stop", SERVICE], check=False, capture_output=True, text=True, timeout=45)
previous_unit = ROLLBACK_ROOT / "aios-v2r1c7-production.service.previous"
previous_env = ROLLBACK_ROOT / "aios-v2r1c7.env.previous"
previous_state = ROLLBACK_ROOT / "v2r1c7-deployment-state.previous"
if not previous_unit.is_file() or not previous_env.is_file():
raise RuntimeError("ROLLBACK_RECEIPT_MISSING")
UNIT_FILE.parent.mkdir(parents=True, exist_ok=True)
ENV_FILE.parent.mkdir(parents=True, exist_ok=True)
shutil.copy2(previous_unit, UNIT_FILE)
shutil.copy2(previous_env, Path("/etc/aios/aios-v2r1c7.env"))
if previous_state.is_file():
Path("/var/lib/aios/v2r1c7").mkdir(parents=True, exist_ok=True)
shutil.copy2(previous_state, Path("/var/lib/aios/v2r1c7/deployment-state.json"))
subprocess.run(["systemctl", "daemon-reload"], check=True, capture_output=True, text=True, timeout=45)
subprocess.run(["systemctl", "restart", SERVICE], check=True, capture_output=True, text=True, timeout=45)
result = base.health("http://127.0.0.1:18927/health", {
"version": "V2R1C7",
"target_oid": str(json.loads(Path("/var/lib/aios/v2r1c7/deployment-state.json").read_text(encoding="utf-8")).get("commit_oid")),
"artifact_sha256": str(json.loads(Path("/var/lib/aios/v2r1c7/deployment-state.json").read_text(encoding="utf-8")).get("artifact_sha256")),
})
return {"status": "ROLLED_BACK", "rollback_health": result, "automatic_rollback": False}
def main() -> int:
try:
value = load_request()
result = execute_request(value)
print(json.dumps(result, sort_keys=True))
return 0
except Exception as exc:
print(json.dumps({"status": "FAIL_CLOSED", "error": type(exc).__name__, "reason": str(exc)}, sort_keys=True))
return 2
if __name__ == "__main__":
raise SystemExit(main())
@@ -0,0 +1,38 @@
[Unit]
Description=AIOS governed approved production release executor
After=network-online.target
Wants=network-online.target
[Service]
Type=oneshot
User=root
Group=root
UMask=0077
Environment=PYTHONDONTWRITEBYTECODE=1
Environment=AIOS_GITOPS_WRITES_ENABLED=false
EnvironmentFile=/etc/aios-gitops/approved-release-executor.env
ExecStart=/usr/bin/python3 /usr/libexec/aios-approved-release-executor/current/active-runtime/app/approved_release_executor.py \
--authorization ${AIOS_RELEASE_AUTHORIZATION} \
--repository ${AIOS_GITOPS_AUTHORITATIVE_GIT_DIR} \
--state-root ${AIOS_RELEASE_EXECUTION_STATE_ROOT} \
--artifact ${AIOS_RELEASE_ARTIFACT} \
--artifact-size ${AIOS_RELEASE_ARTIFACT_SIZE} \
--version ${AIOS_RELEASE_VERSION} \
--target-oid ${AIOS_RELEASE_TARGET_OID} \
--evidence-closure-oid ${AIOS_RELEASE_EVIDENCE_CLOSURE_OID} \
--artifact-sha256 ${AIOS_RELEASE_ARTIFACT_SHA256} \
--approval-plan-id ${AIOS_RELEASE_APPROVAL_PLAN_ID} \
--production-service aios-v2r1c7-production.service \
--production-port 18927 \
--tag-adapter /usr/bin/python3 /usr/libexec/aios-approved-release-executor/current/active-runtime/app/governed_tag_adapter.py \
--deploy-adapter /usr/bin/python3 /usr/libexec/aios-approved-release-executor/current/active-runtime/app/v2r1c7_production_adapter.py \
--registry-adapter /usr/bin/env AIOS_RELEASE_METADATA_MODE=registry /usr/bin/python3 /usr/libexec/aios-approved-release-executor/current/active-runtime/app/release_metadata_adapter.py \
--baseline-adapter /usr/bin/env AIOS_RELEASE_METADATA_MODE=baseline /usr/bin/python3 /usr/libexec/aios-approved-release-executor/current/active-runtime/app/release_metadata_adapter.py
NoNewPrivileges=true
PrivateTmp=true
ProtectHome=true
ProtectSystem=full
ReadWritePaths=/etc/aios /etc/systemd/system /opt/aios /var/lib/aios /var/lib/aios-gitops /var/lib/aios-gitops-runtime-anchor /data/aios-gitops
[Install]
WantedBy=multi-user.target
@@ -0,0 +1,20 @@
[Service]
User=aiman
Group=gitops
WorkingDirectory=/opt/aios/gitops-model-api/@SOURCE_OID@-minimal-autonomous-loop/runtime
ExecStart=
ExecStart=/usr/bin/python3 -B /opt/aios/gitops-model-api/@SOURCE_OID@-minimal-autonomous-loop/runtime/app/aios_chatgpt_bridge_v2_server.py
Environment=AIOS_CONTROL_PLANE_TARGET_OID=@SOURCE_OID@
Environment=AIOS_CONTROL_PLANE_TARGET_VERSION=V2R2C1
Environment=AIOS_CONTROL_PLANE_RUNTIME_PATH=/opt/aios/gitops-model-api/@SOURCE_OID@-minimal-autonomous-loop/runtime
Environment=AIOS_CONTROL_PLANE_ARTIFACT=/opt/aios/gitops-model-api/@SOURCE_OID@-minimal-autonomous-loop/runtime/manifest.json
Environment=AIOS_CONTROL_PLANE_ARTIFACT_SHA256=@ARTIFACT_SHA256@
Environment=AIOS_GITOPS_WRITES_ENABLED=false
Environment=AIOS_GITOPS_DURABLE_JOB_EXECUTION_ENABLED=true
Environment=AIOS_GITOPS_TOKEN_FILE=%d/action_tokens
Environment=AIOS_GITEA_API_BASE=https://g.cnnetmkt.top/git/api/v1
Environment=AIOS_BRIDGE_V2_PORT=18892
Environment=PYTHONPATH=/opt/aios/gitops-model-api/@SOURCE_OID@-minimal-autonomous-loop/runtime:/opt/aios/gitops-model-api/@SOURCE_OID@-minimal-autonomous-loop/runtime/app:/opt/aios/gitops-model-api/@SOURCE_OID@-minimal-autonomous-loop/runtime/runtime-agent-os
LoadCredential=action_tokens:/data/aios-gitops/runtime/secrets/gitops.tokens
ReadOnlyPaths=/opt/aios/gitops-model-api/@SOURCE_OID@-minimal-autonomous-loop/runtime /data/aios-gitops/repo.git
ReadWritePaths=/var/lib/aios-gitops/bridge-v2-staging
@@ -0,0 +1,36 @@
[Unit]
Description=AIOS ChatGPT Bridge v2 staging control plane
After=network-online.target
[Service]
Type=simple
User=aiman
Group=gitops
PermissionsStartOnly=true
WorkingDirectory=@RUNTIME_ROOT@
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_GITOPS_WRITES_ENABLED=false
Environment=AIOS_GITEA_API_BASE=https://g.cnnetmkt.top/git/api/v1
Environment=AIOS_GITEA_TOKEN_FILE=/run/credentials/aios-chatgpt-bridge-v2-staging.service/gitea_token
Environment=AIOS_GITEA_REMOTE=gitea-http
Environment=AIOS_GITOPS_BASELINE_AUTHORITY=/var/lib/aios-gitops/model-api-staging/baseline/current.json
Environment=AIOS_CONTROL_PLANE_TARGET_VERSION=V2R2C1
Environment=AIOS_CONTROL_PLANE_TARGET_OID=@SOURCE_OID@
Environment=AIOS_CONTROL_PLANE_ARTIFACT_SHA256=@ARTIFACT_SHA256@
Environment=PYTHONPATH=@RUNTIME_ROOT@:@RUNTIME_ROOT@/app:@RUNTIME_ROOT@/runtime-agent-os
Environment=AIOS_DEPLOYMENT_STATE=STAGING
LoadCredential=gitea_token:/run/credentials/aios-governance-executor.service/gitea-governance-executor-token
ExecStartPre=/usr/bin/install -d -o aiman -g gitops -m 0750 /var/lib/aios-gitops/bridge-v2-staging
ExecStartPre=/usr/bin/setfacl -m u:aiman:--x /usr/local/libexec
ExecStart=/usr/bin/python3 -B @RUNTIME_ROOT@/app/aios_chatgpt_bridge_v2_server.py
NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=strict
ProtectHome=true
ReadWritePaths=/var/lib/aios-gitops/bridge-v2-staging /data/aios-gitops/repo.git
Restart=on-failure
[Install]
WantedBy=multi-user.target
@@ -0,0 +1,22 @@
[Service]
User=aiman
Group=gitops
WorkingDirectory=/opt/aios/gitops-model-api/@SOURCE_OID@-minimal-autonomous-loop/runtime
ExecStart=
ExecStart=/usr/bin/python3 -B /opt/aios/gitops-model-api/@SOURCE_OID@-minimal-autonomous-loop/runtime/app/aios_model_api_staging_list_patches_overlay.py
Environment=AIOS_GITOPS_WORKSPACE=/opt/aios/gitops-model-api/@SOURCE_OID@-minimal-autonomous-loop/runtime
Environment=AIOS_GITOPS_RUNTIME_OID=@SOURCE_OID@
Environment=AIOS_GITOPS_ARTIFACT=/opt/aios/gitops-model-api/@SOURCE_OID@-minimal-autonomous-loop/runtime/manifest.json
Environment=AIOS_GITOPS_ARTIFACT_SHA256=@ARTIFACT_SHA256@
Environment=AIOS_GITOPS_DEPLOYMENT_STATE=STAGING
Environment=AIOS_GITOPS_WRITES_ENABLED=false
Environment=AIOS_GITOPS_DURABLE_JOB_EXECUTION_ENABLED=true
Environment=AIOS_GITOPS_JOB_CONTROLLER_ENABLED=false
Environment=AIOS_GITOPS_TOKEN_FILE=%d/action_tokens
Environment=AIOS_GITOPS_GITEA_API_URL=https://g.cnnetmkt.top/git/api/v1
Environment=AIOS_BRIDGE_V2_URL=http://127.0.0.1:18892
Environment=AIOS_GITOPS_PORT=18893
Environment=PYTHONPATH=/opt/aios/gitops-model-api/@SOURCE_OID@-minimal-autonomous-loop/runtime:/opt/aios/gitops-model-api/@SOURCE_OID@-minimal-autonomous-loop/runtime/app:/opt/aios/gitops-model-api/@SOURCE_OID@-minimal-autonomous-loop/runtime/runtime-agent-os
LoadCredential=action_tokens:/data/aios-gitops/runtime/secrets/gitops.tokens
ReadOnlyPaths=/opt/aios/gitops-model-api/@SOURCE_OID@-minimal-autonomous-loop/runtime /data/aios-gitops/repo.git
ReadWritePaths=/var/lib/aios-gitops/model-api-staging
@@ -0,0 +1,67 @@
[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
PermissionsStartOnly=true
ExecStartPre=/usr/bin/install -d -o aiman -g gitops -m 0750 /var/lib/aios-gitops/model-api-staging
ExecStartPre=/usr/bin/install -d -o aiman -g gitops -m 0750 /var/lib/aios-gitops/model-api-staging/silent-release
ExecStartPre=/usr/bin/install -d -o root -g gitops -m 0750 /var/lib/aios-gitops/model-api-staging/baseline
ExecStartPre=/usr/bin/setfacl -m u:aiman:--x /usr/local/libexec
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/model-api-staging/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_GITOPS_GITEA_API_URL=https://g.cnnetmkt.top/git/api/v1
Environment=AIOS_GITOPS_GITEA_REPOSITORY=aios/aios
Environment=AIOS_GITOPS_GITEA_REMOTE=gitea-http
Environment=AIOS_GITOPS_GITEA_TOKEN_FILE=/run/credentials/aios-gitops-model-api-v2r2c1-staging.service/gitea_token
Environment=GIT_ASKPASS=/var/lib/aios-gitops/bridge-v2-staging/bridge-v2-askpass.sh
Environment=GIT_ASKPASS_REQUIRE=force
Environment=GIT_TERMINAL_PROMPT=0
Environment=GITEA_MACHINE_ACTOR=aios-autonomy-agent
Environment=CREDENTIAL_FILE=/run/credentials/aios-gitops-model-api-v2r2c1-staging.service/gitea_token
Environment=AIOS_GITOPS_GITEA_TOKEN_LINE=1
LoadCredential=gitea_token:/run/credentials/aios-governance-executor.service/gitea-governance-executor-token
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@:@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
+33
View File
@@ -0,0 +1,33 @@
[Unit]
Description=AIOS durable GitOps writer worker
Requires=aios-gitops-writer.socket
After=aios-gitops-writer.socket network-online.target
[Service]
Type=simple
User=root
Group=root
UMask=0077
Environment=AIOS_GITOPS_WRITES_ENABLED=false
Environment=AIOS_GITOPS_DURABLE_JOB_EXECUTION_ENABLED=true
Environment=AIOS_GITOPS_JOB_ROOT=/var/lib/aios-gitops/jobs
Environment=AIOS_GITEA_MACHINE_CREDENTIAL_REF=systemd-credential:gitea-machine-token
Environment=AIOS_GITOPS_AUTHORITY_GIT_DIR=/data/aios-gitops/repo.git
Environment=AIOS_GITOPS_WORKTREE_ROOT=/var/lib/aios-gitops/worktrees
Environment=AIOS_GITEA_API=https://g.cnnetmkt.top/git/api/v1
Environment=AIOS_GITEA_REMOTE=https://g.cnnetmkt.top/git/aios/aios.git
Environment=AIOS_GITEA_MACHINE_ACCOUNT=aios-autonomy-agent
Environment=AIOS_RELEASE_RECOVERY_RECEIPT=/var/lib/aios-gitops/release-campaigns/AIOS-AUTONOMOUS-RELEASE-CAMPAIGN-20260807/authorization-recovery.json
LoadCredentialEncrypted=gitea-machine-token:/etc/aios/credentials/gitea-machine.token.cred
Environment=PYTHONDONTWRITEBYTECODE=1
ExecStart=/usr/bin/python3 -B /opt/aios-gitops/active-runtime/app/aios_gitops_writer_worker.py --job-root ${AIOS_GITOPS_JOB_ROOT}
Restart=on-failure
RestartSec=2
NoNewPrivileges=no
PrivateTmp=true
ProtectSystem=strict
ProtectHome=true
ReadWritePaths=/var/lib/aios-gitops/jobs /var/lib/aios-gitops/payloads /var/lib/aios-gitops/worktrees /data/aios-gitops/repo.git
[Install]
WantedBy=multi-user.target
+14
View File
@@ -0,0 +1,14 @@
[Unit]
Description=AIOS durable GitOps writer socket
[Socket]
ListenStream=/run/aios-gitops/writer.sock
Accept=no
SocketUser=root
SocketGroup=gitops
SocketMode=0660
DirectoryMode=0750
RemoveOnStop=yes
[Install]
WantedBy=sockets.target
@@ -0,0 +1,26 @@
[Unit]
Description=AIOS Semantic Governance Drift Monitor
After=aios-governance-executor.service
[Service]
Type=oneshot
User=root
Group=gitops
ExecStart=/usr/bin/python3 /usr/local/libexec/aios-governance-drift-monitor
NoNewPrivileges=true
PrivateTmp=true
PrivateDevices=true
ProtectSystem=strict
ProtectHome=true
ProtectKernelTunables=true
ProtectKernelModules=true
ProtectControlGroups=true
LockPersonality=true
RestrictSUIDSGID=true
RestrictRealtime=true
MemoryDenyWriteExecute=true
RestrictAddressFamilies=AF_UNIX
IPAddressDeny=any
ReadOnlyPaths=/usr/local/libexec/aios-governance-drift-monitor
ReadOnlyPaths=/var/lib/aios-gitops/runtime/tasks/AIOS-PROTECTION-SNAPSHOT-DRIFT-FORENSICS-V1/protection-drift/semantic-analysis.json
ReadWritePaths=/var/lib/aios-gitops/governance-executor
@@ -0,0 +1,37 @@
[Unit]
Description=AIOS Dedicated Governance Executor
After=aios-gitea.service network-online.target
Wants=network-online.target
ConditionPathExists=/etc/credstore.encrypted/gitea-governance-executor-token
ConditionPathExists=/usr/local/libexec/aios-governance-executor
[Service]
Type=simple
User=root
Group=gitops
LoadCredentialEncrypted=gitea-governance-executor-token:/etc/credstore.encrypted/gitea-governance-executor-token
ExecStart=/usr/local/libexec/aios-governance-executor
Restart=on-failure
RestartSec=3
NoNewPrivileges=true
PrivateTmp=true
ProtectHome=true
ProtectSystem=strict
ProtectKernelLogs=true
ProtectKernelModules=true
ProtectKernelTunables=true
ProtectControlGroups=true
RestrictSUIDSGID=true
RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6
IPAddressDeny=any
IPAddressAllow=127.0.0.1
RuntimeDirectory=aios-governance-executor
RuntimeDirectoryMode=0750
StateDirectory=aios-gitops/governance-executor
StateDirectoryMode=0750
ReadOnlyPaths=/usr/local/libexec /etc/credstore.encrypted
ReadWritePaths=/var/lib/aios-gitops/governance-executor /run/aios-governance-executor
UMask=0007
[Install]
WantedBy=multi-user.target
@@ -0,0 +1,33 @@
[Unit]
Description=AIOS Governance Pre-Approval Publication Plane
Requires=aios-governance-preapproval-publisher.socket
After=network-online.target aios-gitea.service
Wants=network-online.target
[Service]
Type=simple
User=root
Group=gitops
UMask=0077
Environment=AIOS_GITOPS_WRITES_ENABLED=false
Environment=AIOS_GOVERNANCE_PUBLISHER_OPERATION_ALLOWLIST=semantic-reconciliation-only
LoadCredentialEncrypted=gitea-machine-token:/etc/aios/credentials/gitea-machine.token.cred
ExecStart=/usr/local/libexec/aios-governance-preapproval-publisher
NoNewPrivileges=true
PrivateTmp=true
PrivateDevices=true
ProtectSystem=strict
ProtectHome=true
ProtectKernelTunables=true
ProtectKernelModules=true
ProtectControlGroups=true
LockPersonality=true
RestrictSUIDSGID=true
RestrictRealtime=true
MemoryDenyWriteExecute=true
RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6
ReadOnlyPaths=/usr/local/libexec/aios-governance-preapproval-publisher
ReadOnlyPaths=/etc/aios/credentials
ReadOnlyPaths=/var/lib/aios-gitops/runtime/tasks/AIOS-PROTECTION-SNAPSHOT-DRIFT-FORENSICS-V1/publication-input
ReadWritePaths=/var/lib/aios-gitops/runtime/tasks/AIOS-PROTECTION-SNAPSHOT-DRIFT-FORENSICS-V1
ReadWritePaths=/run/aios-governance-preapproval-publisher
@@ -0,0 +1,13 @@
[Unit]
Description=AIOS Governance Pre-Approval Publication Plane Socket
[Socket]
ListenStream=/run/aios-governance-preapproval-publisher/publisher.sock
SocketUser=root
SocketGroup=gitops
SocketMode=0660
DirectoryMode=0750
RemoveOnStop=true
[Install]
WantedBy=sockets.target
@@ -0,0 +1,63 @@
# AIOS Approved Production Release Executor
Status: implementation artifact for governed protected-PR integration.
## Why this exists
The deployed `aios-silent-release-controller` is intentionally shadow-only.
It rejects production execution unless a trusted builder/orchestrator factory is
wired. V2R1C7 already completed post-merge deterministic build, staging,
rollback, evidence closure and exact Owner approval, so rebuilding inside the
production executor is both unnecessary and a deadlock source.
This patch adds a separate one-shot production executor. It **does not turn the
shadow controller into a production service**.
## Exact V2R1C7 binding
- target: `c7adece1cc8beffe7c718d53c775898b45d6c7f8`
- evidence: `8c2898baf07195b9861ed2ff74f03a71674ec9e9`
- artifact: `ce09a46f28b453634784abceda4597fd51c3aa0838e72a5ad5fefd03d58b949e`
- approval plan: `sha256:c8199d638c2bb2e4896ca9b219ca52db79713bee007df05653ab348905d6cf28`
The executor requires a root-owned accepted authorization receipt that matches
all five fields. It does not accept caller-supplied replacement evidence.
## Execution order
1. exact authorization + artifact + Git lineage preflight
2. annotated V2R1C7 Tag publication and Authority/network/Gitea verification
3. deploy exact artifact; health must report ACTIVE and exact target/digest
4. Registry atomic finalization
5. authoritative Production Baseline CAS finalization **last**
6. read back Tag/runtime/Registry/Baseline and require exact consistency
## Tag implementation
`governed_tag_adapter.py` is a thin adapter over the repository's existing
`gitea_tag_publication.TagPublisher`. It intentionally calls only the existing
tag verification/publication subroutine and never calls legacy
`TagPublisher.publish()` because that legacy operation finalizes Baseline before
the product runtime is ACTIVE.
## Runtime finalization
`release_metadata_adapter.py` writes no Git refs. Registry and Baseline are
runtime state only and are atomic/CAS guarded. Baseline apply independently
requires exact ACTIVE health, exact formal tag, exact Registry, and the exact
previous Baseline SHA-256.
For this release the previous **production** baseline is V2R1C5. V2R1C6 has a
formal tag but was never deployed, so rollback must return to V2R1C5.
## Governance
- force push: forbidden
- tag rewrite/delete: forbidden
- direct main push: forbidden
- V2R1C5/V2R1C6 mutation: forbidden
- plaintext secrets in Git: forbidden
- exact binding replacement: manual Owner review required
- after a verified Tag, uncertain errors become reconciliation-required
- global `AIOS_GITOPS_WRITES_ENABLED` stays false; this executor is a narrow,
separately installed production capability.
@@ -0,0 +1,30 @@
# AIOS Autonomous Release Campaign v1
This control plane changes the Owner interaction model from per-transaction
approval to one durable, revocable campaign policy delegation.
The single bootstrap confirmation creates:
1. a narrow task-writer grant for the control-plane implementation branch; and
2. a root-owned campaign authorization in PENDING_CONTROL_PLANE_MERGE state.
After the protected control-plane PR merges, machine activation may move that
same receipt to ACTIVE only when:
- the merged main is a descendant of the campaign lineage anchor;
- the campaign modules and tests are present in merged main;
- the campaign receipt digest is valid;
- no revocation exists;
- global AIOS_GITOPS_WRITES_ENABLED remains false.
Once ACTIVE, routine engineering, protected PRs, exact release binding
acceptance, tag publication, deployment, Registry/Baseline finalization and
recoverable failure handling no longer ask the Owner for another interaction.
Manual Owner review remains mandatory only for:
- formal same-name tag conflict;
- force-push requirement;
- published history rewrite;
- non-fast-forward main divergence;
- private signing material exposure;
- repository identity conflict;
- campaign revocation;
- operation outside campaign scope.
+53
View File
@@ -0,0 +1,53 @@
# AIOS Local Owner Authentication Ingress
This component closes the Owner authentication bootstrap gap without creating a
long-lived Owner PAT.
## Trust model
The ingress is deliberately one-shot and local-only.
1. A root-run server creates a Unix socket.
2. The peer must be the configured OS Owner account (`aiman`).
3. The server verifies Unix `SO_PEERCRED`, all four process UIDs, Linux audit
`loginuid`, and ownership of the controlling TTY.
4. A `sudo -u aiman` / `runuser` process from another authenticated login is
rejected because its audit `loginuid` does not equal the Owner UID.
5. The human Owner sees the exact task/base/branch/capability request and types
a random one-time challenge.
6. The server constructs only:
- identity: `aiman`
- role: `owner`
- authenticated scope: `commit:create`
- token scopes: [`commit:create`]
7. It calls the existing `OwnerAuthorizationBroker`; it does not call the
provisioner directly.
8. It exits and removes the socket after the single authorization attempt.
No reusable token is minted and no production/tag/baseline/main capability is
granted.
## Current V2R1C7 bootstrap binding
The bootstrap request is expected to authorize only:
- task: `V2R1C7_PRODUCTION_EXECUTOR_BOOTSTRAP`
- branch: `agent/v2r1c7-production-executor-bootstrap-20260807-r2`
- base: exact live `refs/heads/main`
- repository: `aios/aios`
- scope: `commit:create`
- TTL: <= 900 seconds
- production/tag/baseline/main write: false
## Installation model
For the current trust-root bootstrap, the exact SHA-verified script may be
staged under `/run/aios-owner-bootstrap/` and run by a transient root systemd
service. This staging step has no Git/Tag/Registry/Baseline side effect.
The Owner must connect from a real interactive login session as the OS account
`aiman`.
After the governed writer is issued, the same module and tests are committed
through the resulting protected engineering PR so future bootstrap uses code
from main rather than an out-of-band artifact.
+37
View File
@@ -0,0 +1,37 @@
# Durable GitOps Job Controller
The control-plane write boundary is a durable Job, not a synchronous checkout
or commit request. The source of truth is SQLite WAL at
`/var/lib/aios-gitops/jobs/state.sqlite3`, with `jobs`, `idempotency_keys`,
`branch_leases`, `state_transitions`, and `receipts` tables. Per-job
`request.json`, `state.json`, `receipts/`, and `logs-digest/` files are
durable mirrors/evidence, not a second state machine.
The controller owns only binding validation, idempotency, state transitions,
and recovery classification. A supervised worker owns an isolated detached
worktree and all Git side effects. `/api/file` and `/api/commit-file` adapters
must return `job_id` and `side_effect_state`; they must never perform
checkout, file write, `git add`, or `git commit` inline.
`/var/lib/aios-gitops/payloads/sha256/<digest>` is the content-addressed
payload store. SQLite records only `payload_digest`, `payload_ref`, and size;
the worker verifies both digest and size before consumption. The durable
execution gate is `AIOS_GITOPS_DURABLE_JOB_EXECUTION_ENABLED=true`, while
`AIOS_GITOPS_WRITES_ENABLED=false` remains the independent legacy repository
mutation gate.
The idempotency key is the SHA-256 of the canonical tuple:
`campaign_id`, `repository`, `operation`, `branch`, `expected_branch_oid`, and
`payload_digest`.
The writer lease is `CONTINUE_EXISTING` for the recovered campaign. It binds
to lineage anchor `69b45e81dcd2c3707c367003b08c55fd361faae8` and exact initial
head `bba675fab3b7e0c5fba7066720252b3c91b9f20d`. Every ref update is delegated
to a non-Owner machine broker using compare-and-swap; a stale head is a
`BRANCH_LEASE_STALE` condition and never a force push.
Production remains outside this worker contract. The fixed
`aios-gitops-writer.service` uses `Accept=no`, automatic restart, and durable
queue/reconciliation execution; it cannot mutate Git while the legacy global
write gate is false or while the campaign/machine capability is absent.
@@ -0,0 +1,5 @@
AIOS governed Action loop smoke
task=fix-action-contracts-20260825-1407
generation=5
expected_parent=f104e1244fcbc36d1debf1aa0c6720f734ea04cf
purpose=verify read-lease-commit-idempotency-cas
+97 -81
View File
@@ -1,7 +1,7 @@
openapi: 3.1.0 openapi: 3.1.0
info: info:
title: AIOS GitOps Controlled Access API title: AIOS GitOps Controlled Access API
version: V1R1C0-chatgpt-actions version: V2R2C1-chatgpt-actions-28ops
description: Controlled ChatGPT Actions API for AIOS online GitOps baseline. Read endpoints are public. Write endpoints require Bearer token and create Git commits on non-main branches. description: Controlled ChatGPT Actions API for AIOS online GitOps baseline. Read endpoints are public. Write endpoints require Bearer token and create Git commits on non-main branches.
servers: servers:
- url: https://cnnetmkt.top - url: https://cnnetmkt.top
@@ -241,6 +241,102 @@ components:
properties: properties:
candidate_id: {type: string} candidate_id: {type: string}
paths: paths:
/readRepositoryFile:
post:
operationId: readRepositoryFile
summary: Read one bounded file from an approved repository ref
security: [{bearerAuth: []}]
requestBody:
required: true
content:
application/json:
schema:
type: object
additionalProperties: false
required: [repository, ref, path, max_bytes]
properties:
repository: {type: string, enum: [aios/aios]}
ref: {type: string, description: agent branch, main, V2R2C1, or full commit OID}
path: {type: string, minLength: 1, maxLength: 512}
max_bytes: {type: integer, minimum: 1, maximum: 262144}
responses:
'200': {description: Bounded read-only source content}
'400': {description: Invalid repository, ref, path, or size}
'404': {description: Ref or file not found}
x-openai-isConsequential: false
/acquireWriterLease:
post:
operationId: acquireWriterLease
summary: Acquire a short-lived generation-scoped writer lease
security: [{bearerAuth: []}]
requestBody:
required: true
content:
application/json:
schema:
type: object
additionalProperties: false
required: [schema, task_id, repository, branch_mode, base_oid, expected_branch_oid, target_branch, generation, lease_seconds]
properties:
schema: {type: string, enum: [aios.task-writer-lease.v2]}
task_id: {type: string, minLength: 1, maxLength: 200}
repository: {type: string, enum: [aios/aios]}
branch_mode: {type: string, enum: [CONTINUE_EXISTING]}
base_oid: {type: string, pattern: '^[0-9a-f]{40}$'}
expected_branch_oid: {type: string, pattern: '^[0-9a-f]{40}$'}
target_branch: {type: string, pattern: '^agent/[A-Za-z0-9._/-]+$'}
generation: {type: integer, minimum: 1}
lease_seconds: {type: integer, minimum: 1, maximum: 3600}
responses:
'200': {description: Acquired or idempotently replayed scoped lease}
'409': {description: Binding, stale, expired, or already-used lease}
'423': {description: Governed writes disabled}
x-openai-isConsequential: true
/v2/commit-files:
post:
operationId: commitFilesGoverned
summary: Submit one atomic governed commit containing multiple files
security: [{bearerAuth: []}]
requestBody:
required: true
content:
application/json:
schema:
type: object
additionalProperties: false
required:
- task_id
- repository
- expected_branch_oid
- target_branch
- generation
- idempotency_key
- files
- message
properties:
task_id: {type: string}
repository: {type: string, example: aios/aios}
expected_branch_oid: {type: string, pattern: '^[0-9a-f]{40}$'}
target_branch: {type: string, pattern: '^agent/[A-Za-z0-9._/-]+$'}
generation: {type: integer, minimum: 1}
idempotency_key: {type: string, minLength: 8, maxLength: 128, pattern: '^[A-Za-z0-9][A-Za-z0-9._:-]{7,127}$'}
files:
type: array
minItems: 1
maxItems: 50
items:
type: object
additionalProperties: false
required: [path, content_base64]
properties:
path: {type: string, minLength: 1, maxLength: 512}
content_base64: {type: string}
message: {type: string, minLength: 1, maxLength: 200}
responses:
'200': {description: Governed commit job and verified branch result}
'400': {description: Contract or policy rejection}
'409': {description: Branch CAS or idempotency conflict}
x-openai-isConsequential: true
/createReleaseIntent: /createReleaseIntent:
post: post:
operationId: createReleaseIntent operationId: createReleaseIntent
@@ -320,20 +416,6 @@ paths:
'403': {description: Signature, key, expiry, or revocation failure} '403': {description: Signature, key, expiry, or revocation failure}
'409': {description: Conflicting release plan} '409': {description: Conflicting release plan}
x-openai-isConsequential: true x-openai-isConsequential: true
/submitSignedApproval:
post:
operationId: submitSignedApproval
summary: Alias for immutable signed owner approval submission
security: [{bearerAuth: []}]
requestBody:
required: true
content:
application/json:
schema: {$ref: '#/components/schemas/SignedReleasePlanApproval'}
responses:
'200': {description: Signature verified and approval stored}
'403': {description: Production approval backend unavailable or signature invalid}
x-openai-isConsequential: true
/startDeployment: /startDeployment:
post: post:
operationId: startDeployment operationId: startDeployment
@@ -397,17 +479,6 @@ paths:
operationId: listPatches operationId: listPatches
summary: List patch records summary: List patch records
responses: {'200': {description: OK}} responses: {'200': {description: OK}}
post:
operationId: createPatch
summary: Create a patch branch and commit metadata
security: [{bearerAuth: []}]
requestBody:
required: true
content:
application/json:
schema: {$ref: '#/components/schemas/PatchRequest'}
responses: {'200': {description: Patch branch committed}, '401': {description: Unauthorized}, '403': {description: Forbidden}}
x-openai-isConsequential: true
/api/model: /api/model:
get: get:
operationId: readModelRegistry operationId: readModelRegistry
@@ -429,61 +500,6 @@ paths:
operationId: getRuntimeStatus operationId: getRuntimeStatus
summary: Read runtime status summary: Read runtime status
responses: {'200': {description: OK}} responses: {'200': {description: OK}}
/api/branch:
post:
operationId: createBranch
summary: Create a non-main branch and commit branch metadata
security: [{bearerAuth: []}]
requestBody:
required: true
content:
application/json:
schema: {$ref: '#/components/schemas/BranchRequest'}
responses: {'200': {description: Branch created}, '401': {description: Unauthorized}, '403': {description: Forbidden}}
x-openai-isConsequential: true
/api/file:
post:
operationId: commitFile
summary: Write a file and create a Git commit on a non-main branch
security: [{bearerAuth: []}]
requestBody:
required: true
content:
application/json:
schema: {$ref: '#/components/schemas/FileCommitRequest'}
responses: {'200': {description: File committed}, '400': {description: Secret rejected or invalid path}, '401': {description: Unauthorized}}
x-openai-isConsequential: true
/api/commit-file:
post:
operationId: commitFileAtomic
summary: Atomically write one allowlisted file, commit, push, and verify using Token Policy V2
security: [{bearerAuth: []}]
requestBody:
required: true
content:
application/json:
schema: {$ref: '#/components/schemas/AtomicFileCommitRequest'}
responses:
'200': {description: Completed or idempotently replayed}
'400': {description: Invalid canonical request}
'401': {description: Missing, invalid, expired, or revoked Token Policy V2 grant}
'403': {description: Audience, source, branch, operation, path, or policy denied}
'409': {description: Idempotency conflict or manual review required}
'413': {description: Request too large}
'503': {description: Push or remote verification unavailable; recoverable}
x-openai-isConsequential: true
/api/commit:
post:
operationId: commitChanges
summary: Commit existing workspace changes on a non-main branch
security: [{bearerAuth: []}]
requestBody:
required: false
content:
application/json:
schema: {$ref: '#/components/schemas/CommitRequest'}
responses: {'200': {description: Commit result}, '401': {description: Unauthorized}}
x-openai-isConsequential: true
/api/tag-publication-status: /api/tag-publication-status:
get: get:
operationId: getTagPublicationStatus operationId: getTagPublicationStatus
@@ -0,0 +1,15 @@
# AIOS Mission Source Convergence
This directory contains the exact verified governance runtime sources migrated from the live control plane under `AIOS_AUTONOMOUS_RELEASE_MISSION_COMPLETION_AUTHORIZATION_V1`.
The executables remain capability-separated:
- `aios-governance-executor`: protected branch-policy GET, semantic CAS, and approved Stage-1 delta only.
- `aios-governance-drift-monitor`: read-only semantic/transport drift classification.
- `aios-governance-preapproval-publisher`: governance/request publication only; no protection, main, tag, production, Registry, Baseline, or credential mutation.
- `semantic_protection.py`: canonical `aios.semantic-protection.v1` model.
- `ref_reconciler.py`: Gitea-main to Authority FF-CAS reconciler with idempotent readback.
All credential values remain systemd-projected and are never part of source, logs, arguments, or receipts.
The live source hashes used for this migration are recorded in the mission receipt, while this Git tree is the permanent source of truth for future installs.
@@ -0,0 +1,7 @@
# AIOS Mission Zero-Interaction Self-Test
This marker is intentionally harmless and exists only to exercise the
machine-authored branch, exact-head status publication, campaign approval gate,
protected fast-forward merge, and Authority main CAS recovery path.
Production, tags, Registry, Baseline, and V2R1C7 remain unchanged.
@@ -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.
@@ -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,106 @@
#!/usr/bin/env bash
set -Eeuo pipefail
umask 027
ROOT="/data/aios-gitops"
NGINX_SITE="/etc/nginx/sites-enabled/aios-user-portal.conf"
STAMP="$(date -u +%Y%m%dT%H%M%SZ)"
BACKUP_DIR="$ROOT/backups/action-surface-final-$STAMP"
LOG="$ROOT/logs/action-surface-final-$STAMP.log"
mkdir -p "$BACKUP_DIR" "$ROOT/logs"
exec > >(tee -a "$LOG") 2>&1
sudo cp -a "$NGINX_SITE" "$BACKUP_DIR/aios-user-portal.conf"
SITE_BACKUP="$BACKUP_DIR/aios-user-portal.conf"
python3 - "$SITE_BACKUP" "$BACKUP_DIR/aios-user-portal.conf.next" <<'PY'
from pathlib import Path
import sys
source, output = map(Path, sys.argv[1:])
text = source.read_text(encoding="utf-8")
bridge_marker = " # AIOS external Action Bridge v2 staging route"
bridge_block = r''' # AIOS external Action Bridge v2 staging route
# The published Action schema uses the site-root /v2 paths and must reach
# the non-production Bridge v2 listener, never the production 18920 unit.
location ^~ /v2/ {
proxy_pass http://127.0.0.1:18892;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header Authorization $http_authorization;
proxy_set_header X-AIOS-Token $http_x_aios_token;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
client_max_body_size 1100k;
proxy_connect_timeout 5s;
proxy_send_timeout 120s;
proxy_read_timeout 120s;
}
'''
if bridge_marker not in text:
needle = " # BEGIN AIOS CHATGPT AUTONOMOUS RELEASE TASK ROUTE"
if text.count(needle) != 1:
raise SystemExit("expected exactly one legacy Action route marker")
text = text.replace(needle, bridge_block + "\n" + needle, 1)
elif "location ^~ /v2/" not in text or "proxy_pass http://127.0.0.1:18892;" not in text:
raise SystemExit("staging Bridge v2 route marker exists but route is incomplete")
marker = " # AIOS external Action governed-read ownership"
block = r''' # AIOS external Action governed-read ownership
# These reads are owned by the authenticated 18890 facade, not the 18893 model API.
location = /api/governed-write-capabilities {
proxy_pass http://127.0.0.1:18890/api/governed-write-capabilities;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header Authorization $http_authorization;
proxy_set_header X-AIOS-Token $http_x_aios_token;
}
location = /api/control-plane-snapshot {
proxy_pass http://127.0.0.1:18890/api/control-plane-snapshot;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header Authorization $http_authorization;
proxy_set_header X-AIOS-Token $http_x_aios_token;
}
location = /api/online-agent-task {
proxy_pass http://127.0.0.1:18890/api/online-agent-task;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header Authorization $http_authorization;
proxy_set_header X-AIOS-Token $http_x_aios_token;
}
location = /api/online-agent-task-events {
proxy_pass http://127.0.0.1:18890/api/online-agent-task-events;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header Authorization $http_authorization;
proxy_set_header X-AIOS-Token $http_x_aios_token;
}
'''
needle = " location /api/ {"
if text.count(needle) != 1:
raise SystemExit("expected exactly one generic /api/ location")
if marker not in text:
text = text.replace(needle, block + "\n" + needle, 1)
output.write_text(text, encoding="utf-8")
PY
sudo cp "$BACKUP_DIR/aios-user-portal.conf.next" "$NGINX_SITE"
sudo nginx -t
sudo systemctl reload nginx
printf 'ACTION_SURFACE_FINAL_OK backup=%s log=%s\n' "$BACKUP_DIR" "$LOG"
+4
View File
@@ -0,0 +1,4 @@
[pytest]
testpaths = tests
python_files = test_*.py
addopts = -ra --import-mode=importlib
+21
View File
@@ -0,0 +1,21 @@
"""Canonical AIOS release-version parser shared by runtime and release tools."""
from __future__ import annotations
import re
# The suffix preserves already-supported annotated release names while the
# canonical VnRnCn namespace remains accepted exactly.
RELEASE_VERSION_PATTERN = r"^V[0-9]+R[0-9]+C[0-9]+(?:-[A-Za-z0-9_.-]+)?$"
RELEASE_VERSION_RE = re.compile(RELEASE_VERSION_PATTERN)
def parse_release_version(version: object) -> str:
value = str(version or "").strip()
if not RELEASE_VERSION_RE.fullmatch(value):
raise ValueError("invalid release version")
return value
def validate_release_version(version: object) -> str:
return parse_release_version(version)
@@ -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,98 @@
# AIOS Action Connector Schema Final Closure — 2026-08-25
## Disposition
`NEEDS_REWORK`.
The staging runtime and published OpenAPI contract are repaired and the public `/v2` staging route is now reachable. The final P0-C acceptance cannot be claimed because this execution environment has no callable ChatGPT Action/connector inspection or registration tool, and the deployed repository intentionally has no connector acknowledgement/write endpoint. Therefore the required proof of the actual callable tool schema, a real public two-file batch commit, and its public terminal reconcile are not available. The task must not be marked `SUCCEEDED`.
No main, protected tag, baseline registry, or production write was performed.
## Scope and live identity
- Repository: `aios/aios`
- Task: `fix-action-contracts-20260825-1407`
- Branch: `agent/fix-action-contracts-20260825-1407`
- Source worktree: `/var/lib/aios-gitops/bridge-v2-staging/worktrees/fix-action-contracts-20260825-1407`
- Source HEAD before this closure patch: `eb72fcd8dce242abaef15f03a21b43ba2c5197fd`
- Deployed staging Bridge runtime commit: `2ec49005bde44f6981b14351500847d594e0276c`
- Deployed Bridge artifact SHA-256: `f67aed6cc0cce1f08197485c3eea204d3ef4d3989f44dc98e8ea2691e5886cbf`
- Bridge unit: `aios-chatgpt-bridge-v2-staging.service`, `aiman:gitops`, `127.0.0.1:18892`
- Model API unit: `aios-gitops-model-api-v2r2c1-staging.service`, `127.0.0.1:18893`
- Gitea API base projected by staging systemd: `https://g.cnnetmkt.top/git/api/v1`
- Runtime state: `STAGING`, `drift_status=PASS`, `legacy_global_writes_enabled=false`, `main_write=false`, `tag_write=false`, `baseline_write=false`, `production_write=false`
## Root causes and contract chain
1. Runtime source was already repaired to require `files` for `commitFilesGoverned`. `active-runtime/app/aios_chatgpt_bridge_v2.py` declares the canonical required tuple and `render_openapi_contract_fragment()` emits the operation fragment.
2. `scripts/build_v2r2c1_model_api_staging.py` copies the canonical `docs/gitops/openapi-chatgpt-actions.yaml` into the immutable staging artifact and rejects a build missing `/v2/commit-files`.
3. The deployed public OpenAPI is served from the model API workspace at `/api/openapi-chatgpt-actions.yaml`; its SHA-256 was `a0775b0719a76e08a95f3257a3d3b2a73b70e1c55c162914250f80dd28da022d`. It contains `/v2/commit-files`, `files`, and item fields `path` and `content_base64`.
4. The missing deployment link was Nginx: OpenAPI `servers.url` is `https://cnnetmkt.top` and its operation paths are `/v2/...`, but the live site had no `/v2/` location and the old `/api/v2/` location pointed at absent production port `18920`. The old `/api/file` route is an inactive legacy single-file router.
5. A repository-wide and host-side inspection found no formal external ChatGPT connector registration source, schema cache, re-registration command, or acknowledgement endpoint. The existing durable-owner design explicitly states that no connector acknowledgement/write endpoint is provided and the only valid state is `PENDING_EXTERNAL_CONNECTOR`. This is the remaining external blocker, not a reason to create a parallel local schema or reuse another credential.
## Changes made
- `ops/gitops-migration/bind_external_action_surface_final_staging.sh`
- added an idempotent exact `/v2/` route to staging Bridge `127.0.0.1:18892`;
- forwards `Authorization` and `X-AIOS-Token`;
- keeps the existing exact governed-read routes on `18890` and generic `/api/` on `18893`;
- refuses an incomplete existing staging route and never adds a route to production `18920`.
- `tests/test_action_contract_regressions.py`
- added a parity/deployment test tying published `/v2/commit-files` to the staging route and asserting no `18920` target in the staging installer.
- `runbooks/action-connector-schema-refresh-20260825.md`
- added the durable connector refresh and fail-closed acceptance procedure.
- `reports/action-connector-schema-final-closure-20260825.md`
- this evidence report.
The source/runtime contract parity test remains in place and still compares runtime required fields, generated fragment, and published schema. The live connector layer cannot be parity-tested because no connector layer is deployed or callable from this environment; the gate therefore remains fail-closed rather than accepting a fixture as proof.
## Deployment and restart evidence
The existing staging installer was run after `git diff --check` and focused tests. It created the Nginx backup `/data/aios-gitops/backups/action-surface-final-20260825T105656Z`, passed `nginx -t`, and reloaded Nginx. Public readback then returned HTTP 200 for:
- `https://cnnetmkt.top/v2/runtime`
- `https://cnnetmkt.top/v2/capabilities`
- `https://cnnetmkt.top/api/openapi-chatgpt-actions.yaml`
The staging Bridge and Model API were subsequently restarted once. Both `systemctl is-active` checks returned `active`. Post-restart `/v2/runtime` retained the staging identity and artifact SHA above; `/v2/capabilities` retained `commitFilesGoverned.required_fields` including `files`.
## Tests and live smoke
Passing results:
- `python3 -m pytest -q tests/test_action_contract_regressions.py tests/test_bridge_v2_governed_http_bootstrap.py` — `17 passed`.
- Mandatory suite — `264 passed, 28 subtests passed in 4.81s`.
- Full repository suite — `720 passed, 2 skipped, 2 failed, 154 subtests passed in 34.02s`. The two failures are unchanged known failures: stale `/tmp/aios-agent-events-test` fixture and deployed signer self-test return code `42`.
Post-restart live results:
- `getReleaseConsistency(V2R2C1, ...)`: HTTP 200 structured consistency response; no Gitea URL validation failure. Gitea listing and peeled target readback were visible.
- `runReleaseAcceptance(V2R2C1, ...)`: HTTP 200 structured policy result with `accepted=false`; it reached policy evaluation rather than failing URL validation.
- `getTagPublicationStatus("V2R2C1")`: HTTP 200 structured state (`REMOTE_PUSH_PENDING` on Bridge v2; the legacy model read surface returned structured `UNKNOWN`).
- `getReleaseSaga(nonexistent)`: HTTP 404, normal NOT_FOUND behavior.
- Existing terminal job `job-93138017f3afd7f709f691fd05c77923`: HTTP 200, `COMPLETED`, revision `8`; terminal `/v2/job/reconcile` returned HTTP 200 with the same `COMPLETED` receipt and Authority/API/Git remote OID `904e7ea0cd376d204bad19628c36a9dd90ca6621`.
- Empty diff: HTTP 200, zero paths/size, `truncated=false`.
- Non-empty diff: HTTP 200 with identities, changed paths, stats, total size, patch limit `262144`, and bounded response fields.
- Public `POST /v2/commit-files` probes returned `BRIDGE_REQUEST_FIELDS_MISSING` for incomplete bodies; the body containing `files` was not rejected as an unknown field. No write side effect occurred.
## Missing final acceptance
The following required evidence is not available and is intentionally not inferred:
- actual ChatGPT callable Action tool schema showing required `files`;
- formal connector re-register/cache invalidation acknowledgement;
- an authorized public `commitFilesGoverned` request containing two files;
- Authority/Gitea/API remote OID equality for that new batch commit;
- its batch Job `COMPLETED` readback and public terminal reconcile;
- post-batch connector callable smoke.
The prior single-file public commit `904e7ea0cd376d204bad19628c36a9dd90ca6621` and its terminal reconcile are preserved as baseline evidence only; they are not substituted for the required batch proof.
## Protected boundaries and rollback
No source or deployment operation wrote `main`, a protected tag, the baseline registry, or production. The production-facing `18920` route was not changed; the new `/v2/` location targets only the staging Bridge listener. No secret value was printed, rotated, or reused. Automatic rollback was not triggered because Nginx syntax, staging service health, runtime identity, Gitea readback, and protected-ref boundaries remained valid. If a future connector refresh breaks these checks, restore the Nginx backup and the preceding staging artifact through the existing governed installer, then re-run the same probes.
## Next required owner/platform action
Provide the supported staging external Action connector lifecycle (registration/re-registration/cache invalidation) and an authorized staging Action bearer, or have the legitimate external Agent claim the existing continuation task and produce its durable callable-schema and batch-commit evidence. Until then the correct disposition remains `NEEDS_REWORK`; do not mark `SUCCEEDED` based on the server-side OpenAPI alone.
@@ -0,0 +1,75 @@
# Action Contract Regression Remediation — 2026-08-25
## Scope
Governed task: `fix-action-contracts-20260825-1407`
Base: `3244f0f17f8845592ac94d585f5ebd65693c9f29`
Target branch: `agent/fix-action-contracts-20260825-1407`
## Verified regressions
1. `getReleaseConsistency` and `runReleaseAcceptance` fail before evaluation with:
`Gitea API URL must be HTTP(S)`.
V2R2C1 release evidence at `a8b906d1bc24088c19647e98de6a679268f6d0a2`
proves Gitea API readback passed for target `998d30304787e31e5ae607c0bd86b5edb2b0e5ad`.
Therefore current staging/contract overlay configuration has regressed.
2. `getReleaseSaga` fails with:
`Permission denied: '/var/lib/aios-gitops/silent-release'`.
Runtime service identity must have read/traverse access to the durable release state root.
Fix ownership/group/ACL at deployment time; do not broaden to world-writable permissions.
3. `getTagPublicationStatus` rejects the authoritative approved version `V2R2C1`
as `invalid release version`, while `getGovernedWriteCapabilities` returns the same
version as the approved release scope. The two Action contracts are internally inconsistent.
Release-version validation must accept the canonical AIOS `^V[0-9]+R[0-9]+C[0-9]+$`
namespace (or, preferably, use one shared validator for all release Actions).
4. `commitFilesGoverned` is unusable from the published Action schema.
Runtime capability metadata declares required fields:
`task_id, repository, expected_branch_oid, target_branch, generation, idempotency_key, files, message`,
but the exposed tool contract has no `files` input. Regenerate/publish the Action schema from
the same operation contract source and add a contract-parity test.
5. `reconcileGitOpsJob` returned `DURABLE_BINDING_INVALID` for a completed governed
single-file job that was otherwise read back as `COMPLETED`.
Reconciliation of a valid terminal job should be idempotent/read-only and must not require
a stale or absent active lease. Add a regression case for terminal completed jobs.
6. `getDiff` returns `ResponseTooLargeError` / `GIT_OUTPUT_LIMIT_EXCEEDED` for large diffs.
Add server-side bounded output: path/stat summary plus truncation metadata, or pagination.
The Action must never fail merely because a valid repository diff is large.
## Required implementation changes
- Centralize release-version parsing and reuse it in tag status, candidate, consistency, and acceptance.
- Validate/normalize Gitea API URL at service startup and fail health checks with a configuration-specific diagnostic.
- Ensure the deployment unit projects a valid HTTP(S) Gitea API base URL into the staging overlay.
- Correct durable release-state directory ownership/ACL for the runtime service account.
- Generate the external Action schema from the runtime operation contract; parity-test required fields.
- Make terminal reconciliation idempotent and independent of expired active writer leases.
- Bound diff output and return `truncated=true` with deterministic metadata rather than transport failure.
## Acceptance gates
All must pass on the same deployed overlay:
- `getReleaseConsistency(V2R2C1, 998d303..., a8b906d..., approved_plan)` returns a structured result, not URL error.
- `runReleaseAcceptance(...)` reaches policy evaluation, not URL error.
- `getTagPublicationStatus("V2R2C1")` returns structured publication state.
- `getReleaseSaga(nonexistent_candidate)` returns NOT_FOUND/empty-state semantics, never filesystem permission failure.
- Published `commitFilesGoverned` schema exposes `files` and a two-file isolated branch commit succeeds.
- Reconcile of a completed governed job is idempotent.
- Large diff returns bounded/truncated data rather than transport exception.
- Existing protected-ref controls remain unchanged: no direct main/tag/baseline/production write.
## Proven stable reference
V2R2C1 evidence closure (`a8b906d1bc24088c19647e98de6a679268f6d0a2`) records:
- Gitea Git/API readback PASS
- staging runtime PASS
- restart recovery PASS
- 59 mandatory tests passed / 0 failed
This evidence should be used as the compatibility baseline when repairing the current
`contract-overlay-v2` / `scheme-a-323b615b...` staging layer.
@@ -0,0 +1,119 @@
# AIOS Action contract repair evidence — 2026-08-25
## Disposition
Final disposition: `NEEDS_REWORK`.
The contract repair is deployed to staging and the focused acceptance checks are green. The task is not eligible for `SUCCEEDED`: `runReleaseAcceptance` returns structured policy output with `accepted=false` because the existing publication state for protected tag `V2R2C1` is `REMOTE_PUSH_PENDING`, and the repository-wide suite still has the two pre-existing signer/event-bus failures recorded below. The online Task API row is still `QUEUED`; it was not changed by direct database manipulation or an unauthorized synthetic claim/result.
No production release, `main`, release tag, baseline registry, or production service was directly modified.
## Scope and pre-change facts
- Repository: `aios/aios`; governed branch: `agent/fix-action-contracts-20260825-1407`.
- Initial task branch HEAD: `35eec9afe1432e8466d60040b2fb1049bb41242a`.
- Stable reference: `V2R2C1`; source commit `998d30304787e31e5ae607c0bd86b5edb2b0e5ad`; evidence closure `a8b906d1bc24088c19647e98de6a679268f6d0a2`.
- Pre-change refs: authority `main=3244f0f17f8845592ac94d585f5ebd65693c9f29`, tag `V2R2C1=07fc2746b688f5f243f29b39de39f32edc3dd9d2`; these remained unchanged.
- Staging services run as `User=aiman`, `Group=gitops`. The actual staging Gitea API base is `https://g.cnnetmkt.top/git/api/v1`, supplied by systemd environment rather than business-code host constants.
- Pre-change full test baseline: `707 passed, 2 skipped, 154 subtests passed, 2 failed`. The failures were the stale `/tmp/aios-agent-events-test` event-bus fixture and the real signer self-test.
## Root causes and repairs
1. Gitea URL validation occurred at release-operation time while the staging overlay's API environment was not consistently projected into all runtime paths. The canonical HTTP(S) validator is now called during model and Bridge startup; `getReleaseConsistency` and `runReleaseAcceptance` use the actual staging environment URL.
2. `silent-release` was not consistently writable by the real service identity, and the credential helper parent directory blocked `aiman` traversal. The persisted staging systemd overlay creates the state directory as `aiman:gitops` mode `0750` and grants only `u:aiman:--x` on `/usr/local/libexec`; no world-writable mode was used. The staging bridge also has the minimum repository write path required by the governed AuthorityGitExecutor/CAS flow.
3. Release version parsing was split across callers and rejected `V2R2C1`. A shared canonical parser now accepts `^V[0-9]+R[0-9]+C[0-9]+$` plus the existing bounded suffix form, and is used by capabilities, release candidate, consistency, acceptance, tag publication, and release/tag queries.
4. The runtime operation contract required `files` but the published Action schema omitted it. `commitFilesGoverned` now has one contract source and published schema projection, with parity tests. A real governed two-file commit completed on isolated branch `agent/action-contract-integration-20260825-1407`; authority, Git remote, and Gitea API all read back OID `0345cd8c70967940bc8e9132dff7d6ea11efa304`.
5. `reconcileGitOpsJob` reconstructed active lease state before checking terminal state. Terminal `COMPLETED`, `FAILED`, and `CANCELLED` jobs now short-circuit to idempotent read-back; active jobs retain strict lease validation. The two-file job reconciled as `COMPLETED` and a second reconcile returned the same receipt.
6. `getDiff` returned unbounded patch data and could fail the transport. It now returns identities, changed paths, stats, total bytes, bounded patch output, patch limit, and `truncated`. The large smoke returned `total_size_bytes=350222`, `stdout_bytes=262144`, `patch_limit_bytes=262144`, `truncated=true`; no `ResponseTooLargeError` or `GIT_OUTPUT_LIMIT_EXCEEDED` escaped.
During live integration, two additional staging-only contract defects were found and repaired: CREATE_NEW branch normalization/CAS recovery, and release executor projection of the configured staging baseline plus immutable runtime `versions/V2R2C1.json`. The latter removed the missing publication-workspace baseline/version errors without writing production state.
## Source, test, and operational changes
Source commits on the governed branch include the six contract repairs, focused regression tests, `versions/V2R2C1.json`, governed CREATE_NEW/reconcile recovery, and final runtime projection commits. The final source before this evidence commit is `2ec49005bde44f6981b14351500847d594e0276c`.
The runbook update is in `runbooks/agent-fix-action-contracts-20260825.md`. This report is the required durable evidence artifact. Existing untracked `runtime/agent-observability/` and `runtime/task-center/` directories were preserved and were not staged.
Tests run on the final source:
```text
PYTHONPATH=active-runtime/app python3 -m pytest -q tests/test_silent_release_execution_readiness.py tests/test_silent_release_controller.py tests/test_silent_release_hardening.py tests/test_silent_release_production_contract.py tests/test_chatgpt_actions_tag.py tests/test_gitea_tag_publication.py tests/test_authoritative_git_context.py
264 passed, 28 subtests passed in 4.78s
PYTHONPATH=active-runtime/app python3 -m pytest -q tests
717 passed, 2 skipped, 154 subtests passed, 2 failed in 33.98s
```
The full-suite failures remain the known event-bus fixture failure and the real signer adapter self-test (`returncode=42`); no new mandatory-suite regression was observed. Focused contract/reconcile tests and the exact mandatory suite passed.
## Staging deployment identity
- Final deployed source OID at the last completed runtime smoke: `2ec49005bde44f6981b14351500847d594e0276c`.
- Final deployed artifact: `/opt/aios/gitops-model-api/2ec49005bde44f6981b14351500847d594e0276c-action-contract-repair/runtime`.
- Artifact SHA256: `f67aed6cc0cce1f08197485c3eea204d3ef4d3989f44dc98e8ea2691e5886cbf`.
- Deployment state: `STAGING`; production-deployed flag: `false`.
- A lexically highest staging-only systemd drop-in was installed for each affected service, followed by `daemon-reload` and restart. `systemctl is-active` returned `active` for both services, and `systemctl show` confirmed the final OID, artifact SHA, API URL, and baseline authority.
## Runtime smoke evidence
- Model health returned `status=ok`, `version=V2R2C1`, final commit/artifact identity, `deployment_state=STAGING`, and `production_deployed=false`.
- Bridge/API readback was available; the bridge's `/health` path is not exposed by this runtime and returned `404`, while its governed endpoints were healthy.
- `getReleaseConsistency("V2R2C1", ...)` returned structured `status=PASS`, with Gitea listing/peeled target and tag object matching the stable reference.
- `runReleaseAcceptance(...)` entered policy evaluation and returned structured `accepted=false`; it did not fail URL validation. The independent pre-existing tag publication state is `REMOTE_PUSH_PENDING`, with the remote rejection explicitly reporting protected tag `V2R2C1`.
- `getTagPublicationStatus("V2R2C1")` returned structured `REMOTE_PUSH_PENDING`, not `invalid release version`.
- `getReleaseSaga` for a nonexistent candidate returned HTTP `404` with `status=NOT_FOUND`, `state=NOT_FOUND`, `execution_enabled=false`, and no filesystem permission error.
- `commitFilesGoverned` published schema includes required `files`, with per-file `path` and `content_base64`.
- Two-file governed commit: branch `agent/action-contract-integration-20260825-1407`, OID `0345cd8c70967940bc8e9132dff7d6ea11efa304` at authority/Git remote/API; protected-ref flags remained false.
- Terminal reconcile: `COMPLETED` read-back was idempotent across two calls; focused tests cover `FAILED` and `CANCELLED` terminal semantics.
- Small diff returned bounded non-truncated output; empty diff returned zero paths and zero bytes; large diff returned bounded/truncated output as recorded above.
- Main, protected tag, baseline registry, and production OIDs were re-read after deployment and remained unchanged.
## Restart persistence and remaining risk
The final staging services were restarted after deployment and health/runtime identity were re-read. The post-restart `systemctl is-active` result was `active` for both services; model health again reported OID `2ec49005bde44f6981b14351500847d594e0276c`, artifact SHA `f67aed6cc0cce1f08197485c3eea204d3ef4d3989f44dc98e8ea2691e5886cbf`, staging mode, and `production_deployed=false`. Post-restart consistency remained `PASS`, acceptance remained structured `accepted=false` for the pre-existing protected-tag publication state, saga remained `NOT_FOUND` for the nonexistent candidate, and the large diff remained bounded/truncated. The task cannot be promoted to `SUCCEEDED` until the protected-tag publication state and the two known repository-wide failures are resolved through their respective governed owners. No automatic rollback was performed because the staging services stayed healthy, mandatory tests passed, Gitea readback passed, and protected-ref boundaries remained intact.
Task disposition is `NEEDS_REWORK`, not `SUCCEEDED`: the staging contract repair is materially complete, but the deployed acceptance result is not all-green and the task row has not been legitimately claimed/verified by the online task worker.
## External Action Surface final closure evidence — 2026-08-25
### Disposition
NEEDS_REWORK. Staging is deployed and P0-A/P0-B have live closure evidence. P0-C is blocked: no formal staging external Action bearer issuance path was available, so the required real public two-file commit and terminal reconcile were not authorized or executed. Do not mark SUCCEEDED. No main, tag, baseline, or production write occurred; untracked runtime/agent-observability and runtime/task-center were preserved.
### Identity and before-change facts
Authority /data/aios-gitops/repo.git; worktree /var/lib/aios-gitops/bridge-v2-staging/worktrees/fix-action-contracts-20260825-1407; branch agent/fix-action-contracts-20260825-1407; initial HEAD 35eec9afe1432e8466d60040b2fb1049bb41242a1. Stable V2R2C1 source 998d30304787e31e5ae607c0bd86b5edb2b0e5ad; evidence closure a8b906d1bc24088c19647e98de6a679268f6d0a2. Repair commit 174e64dd0218cef6d49dd13d58bbd90c78fecadd; protected refs unchanged.
### P0-A governed-read ownership
Root cause: generic /api/ sent governed-write-capabilities to model API 18893 (404), while governed facade 18890 owned it. Fix: ops/gitops-migration/bind_external_action_surface_final_staging.sh installs exact locations for governed-write-capabilities, control-plane-snapshot, online-agent-task, and online-agent-task-events to 18890; generic /api/ remains 18893; auth headers forward and no business host is hard-coded. Backup /data/aios-gitops/backups/action-surface-final-20260825T101714Z; log /data/aios-gitops/logs/action-surface-final-20260825T101714Z.log. nginx -t passed, Nginx active, public GET /api/governed-write-capabilities HTTP 200. Contract reports governed writes enabled but legacy/main/tag/baseline/production writes false.
### P0-B release semantics
Root cause: public model GET used local TagPublisher/PublicationStore lacking tag object binding, so consistency was FAIL/tag_object=false; Bridge v2 had authoritative durable binding. Fix: active-runtime/app/aios_model_api_staging.py constructs one canonical publication request and routes release-consistency/release-acceptance through Bridge v2 getReleaseConsistency/runReleaseAcceptance; tests/test_action_contract_regressions.py covers this. Live V2R2C1 consistency with target 998d30304787e31e5ae607c0bd86b5edb2b0e5ad, closure a8b906d1bc24088c19647e98de6a679268f6d0a2, plan sha256:c0df52008ae7f64cec5ee2d28947aa7b892066c670ab8295e4549140b1a7500e returned HTTP 200 PASS, all checks true, tag object 07fc2746b688f5f243f29b39de39f32edc3dd9d2 and peeled target correct. Acceptance entered structured policy evaluation with accepted=false, not URL failure. Tag publication V2R2C1 returned structured UNKNOWN, not invalid version.
### P0-C credential and commit blocker
Hash-only Action record: account chatgpt-gitops-bot; id chatgpt-action-0a3e55b7659a4d4e8125feabce5e8bf5; kind gpt_action_api_key; consumer aios-gitops-model-api.service; issuer owner_ssh_breakglass_rotation; issued 2026-08-10T04:51:58.900488Z; expires 2026-09-09T04:51:58.900488Z; scopes file:write and agent-task:control; disabled false; owner approval required true. Plaintext was not printed/reused. agent_task_control_token is not an Action bearer. Inspected Writer grants, Gitea consumer, SSH, task broker and old token candidates provide no formal issuer; no unrelated credential substituted. Empty public POST /api/commitFilesGoverned reached staging and returned HTTP 423 gitops_writes_disabled with AIOS_GITOPS_WRITES_ENABLED=false. Writes were not enabled. No real public two-file commit or COMPLETED reconcile is claimed. Blocker: FORMAL_STAGING_ACTION_BEARER_ISSUANCE_NOT_AVAILABLE.
### Schema/deployment/runtime
Source 174e64dd0218cef6d49dd13d58bbd90c78fecadd. Artifact /data/aios-gitops/artifacts/action-contract-final-174e64d/aios-v2r2c1-model-api-174e64dd0218cef6d49dd13d58bbd90c78fecadd.tar.gz; SHA256 8fc811cfb9dbb22d6eb7ca418d58f9ada1d6fdd12e566e3e8f5a02bec47c4f37. Runtime /opt/aios/gitops-model-api/174e64dd0218cef6d49dd13d58bbd90c78fecadd-action-surface-final/runtime. Drop-in /etc/systemd/system/aios-gitops-model-api-v2r2c1-staging.service.d/zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz-action-contract-zzzz-final-999.conf; backup /data/aios-gitops/backups/action-contract-final-deploy-20260825T101959Z/drop-in-directory. User/Group aiman:gitops; API base from systemd https://g.cnnetmkt.top/git/api/v1; writes false; Bridge 18892; STAGING; production_deployed=false. /api/run proved active runtime/readable authority /data/aios-gitops/repo.git and main 3244f0f17f8845592ac94d585f5ebd65693c9f29. /api/openapi.yaml required commitFilesGoverned files, each with path/content_base64.
### Tests
Before mandatory: 264 passed, 28 subtests, 4.82s. After mandatory: 264 passed, 28 subtests, 4.80s. Focused action regression: 11 passed, 0.12s. Full: 719 passed, 2 skipped, 154 subtests, 2 failed, 34.03s. Same known failures: stale /tmp/aios-agent-events-test in test_unified_event_flow; signer adapter self-test return 42 in test_real_signer_help_and_self_test_are_available.
### Restart and smoke
After first deployment, model and Bridge staging services were restarted and remained active. Post-restart health retained source/artifact identity, V2R2C1, STAGING, production false. Capability HTTP 200; consistency HTTP 200/PASS; tag status structured UNKNOWN; nonexistent saga HTTP 404/NOT_FOUND with execution false, shadow true, side effects false. Empty diff HTTP 200, zero paths/size, truncated false. Bounded non-empty diff returned identities, paths/stats, bounded output and patch limit 262144; local pipe-to-head caused only expected curl broken-pipe. Existing large-diff behavior remains bounded/truncated with no transport exception. Gitea readback proven by consistency PASS; protected-ref boundary remained fail-closed.
### Task continuity and remaining work
Earlier execution observed TASK_NOT_FOUND and created continuation fix-action-contracts-20260825-1407-external-surface-closure with parent/source/OID and staging constraints. Later recheck returned parent HTTP 200 QUEUED; continuation also QUEUED, revision 1, claimed_by null, result null. Neither has claim, execution, terminal result, or success evidence. Required rework is formal bearer lifecycle, isolated real two-file public commit and OID/reconcile verification, disposition of the two pre-existing full-suite failures, then full smoke before SUCCEEDED.
### Health endpoint clarification
The service health contract is /api/health: local model port 18893 and public https://cnnetmkt.top/api/health both returned HTTP 200 with the deployed source/artifact identity, STAGING, and production_deployed=false. Public root /health returned HTTP 502 because the existing Nginx root location proxies the separate portal upstream on 127.0.0.1:3000; it is not the Action model health route. This unrelated portal-ingress 502 is recorded as an observed remaining staging risk, and was not changed because this task scope is the Action/Bridge/release surface.
\n
### Formal task-state boundary
The deployed Task API exposes verification decisions SUCCEEDED, NEEDS_REWORK, and FAILED only through POST /v1/tasks/{id}/verify while the task is VERIFYING. Both the parent and continuation were QUEUED with no claim, so a direct NEEDS_REWORK transition was not a legal API operation. No cancel, database edit, fake claim, or lifecycle bypass was performed. This report is the authoritative disposition for this run: NEEDS_REWORK.
\n
@@ -0,0 +1,92 @@
# Action 最小自治修复闭环证据(2026-08-25)
## 最终状态
`BLOCKED`(staging 代码/配置已部署并健康,但缺少本次可用的正式 Action bearer,未宣称真实 governed commit 或完整外部 smoke 成功)。
本轮没有写入 production、`main`、release tag 或 baseline。
## 修改前事实
- repository: `aios/aios`
- branch: `agent/fix-action-contracts-20260825-1407`
- 修改前 HEAD: `d7a0d0db09826d513cb19451a4f018c998c00191`
- production baseline: `V2R2C1`
- baseline source commit: `998d30304787e31e5ae607c0bd86b5edb2b0e5ad`
- Action schema 已限制为 26 operations,但没有外部 read-source/lease 入口;Bridge lease 主要是进程内 runtime binding;Bridge 未做 operation-specific bearer 校验。
- 真实 staging model-api source/artifact 在本轮前为 `7e5525cfcbbaa3898c2e5ed3dc9c13e321908e61` / `2163c2e0467f3bd0a97f8d2b95e3277391c47dcf3ebafb8fa372a01c3f530ac0`。
## 根因与修复
1. Autonomous Agent 没有安全 read seam:增加 `AuthorityGitExecutor.read_file` 和 `readRepositoryFile`,只读 authoritative bare Git object database,固定响应上限并返回 resolved commit/OID、size、SHA-256、base64。
2. Lease 没有跨重启 durable binding:增加 `ScopedLeaseStore` SQLite CAS 状态,绑定 task/repository/branch/expected OID/generation/TTL,支持同 binding 幂等、冲突、过期和 USED replay;commit 先校验 lease,再走既有 detached worktree/Job Controller/Authority CAS chain。
3. Lease 曾可由调用者声明治理身份:现在必须命中 durable task binding,公开路径只接受 `CONTINUE_EXISTING`,并要求 expected OID 等于当前 authority branch head。
4. Auth scope 曾混合:Bridge 按 read/lease/commit operation 分别检查 scope;model-api facade 认证后透传 bearer。
5. runtime contract 与 published schema:增加 read/lease operation,保留 `commitFilesGoverned.files[] = {path, content_base64}`,builder 将 `scoped_lease_store.py` 纳入 immutable artifact。
## 修改文件与配置
- `active-runtime/app/aios_authority_git_executor.py`:安全 ref/path/size/read helper。
- `active-runtime/app/scoped_lease_store.py`:SQLite durable lease state。
- `active-runtime/app/aios_chatgpt_bridge_v2.py`:read operation contract。
- `active-runtime/app/aios_chatgpt_bridge_v2_server.py`:read route、task binding、lease store、Bearer/scope matrix、structured errors。
- `active-runtime/app/aios_model_api_staging.py`:认证 facade 与 Bridge control proxy。
- `docs/gitops/openapi-chatgpt-actions.yaml`:新增 `readRepositoryFile`、`acquireWriterLease`,公开 operation 数 28。
- `scripts/build_v2r2c1_model_api_staging.py`:纳入 `scoped_lease_store.py`。
- `tests/test_minimum_autonomous_loop.py`:read/path/size、contract、lease 幂等/冲突测试。
- `runbooks/agent-minimum-autonomous-loop-20260825.md`:部署与验收手册。
- `deploy/*minimal-autonomous-loop.conf`:待随最终 source OID 渲染的 staging unit 配置模板;实际 drop-in 使用 systemd `LoadCredential` 投影 `/data/aios-gitops/runtime/secrets/gitops.tokens`,不复制 token。
## Git 与构建结果
- 第一实现提交:`f3a4485be7c803c3769c518578cf4bf86db32c6e`。
- 绑定与 scope 收紧提交:`04b69beb369365a1522268e16c3ac7cd1dd745ce`。
- 最终实现/部署 source OID:`9f32b26eba5bdaaa0e7c89042ce336426d465dbd`。
- build artifact:`/var/lib/aios-gitops/bridge-v2-staging/candidate-artifacts/min-autonomous-9f32b26/aios-v2r2c1-model-api-9f32b26eba5bdaaa0e7c89042ce336426d465dbd.tar.gz`
- artifact SHA-256:`ff2031b9ef8a39811c15086c2577d33cc6f187fd060136b7be7a8d6b4152e9f1`
- artifact source OID:`9f32b26eba5bdaaa0e7c89042ce336426d465dbd`
## 测试与真实 staging 结果
已通过:
```text
pytest -q tests/test_minimum_autonomous_loop.py tests/test_action_contract_regressions.py tests/test_bridge_v2_governed_http_bootstrap.py
21 passed
python3 -m py_compile ... # changed runtime modules
git diff --check
```
完整 mandatory suite(使用仓库需要的 `PYTHONPATH=.:services/aios-release-controller`)结果:
`724 passed, 2 skipped, 2 failed, 154 subtests passed`。
两个失败与本轮改动无关,和修改前已知基线一致:`tests/test_agent_event_bus.py::test_unified_event_flow` 受残留 `/tmp/aios-agent-events-test` 数据污染;`tests/test_signer_contract_compatibility.py::SignerContractCompatibilityTests::test_real_signer_help_and_self_test_are_available` 的已部署 signer self-test 返回码为 `42`。
已验证:
- `aios-chatgpt-bridge-v2-staging.service`: restart 后 active,ExecStart 指向 source `9f32b26...` runtime,PID `2061371`。
- `aios-gitops-model-api-v2r2c1-staging.service`: restart 后 active,ExecStart 指向 source `9f32b26...` runtime,PID `2061686`。
- `/api/health` 与 `/v2/runtime`: restart 后 HTTP 200,`STAGING`、`production_deployed=false`、runtime OID `9f32b26...`、artifact SHA `ff2031...`,`drift_status=PASS`。
- 实际 staging `/api/openapi-chatgpt-actions.yaml`: HTTP 200,版本 `V2R2C1-chatgpt-actions-28ops`,28 个 `operationId`,含 `readRepositoryFile`、`acquireWriterLease`、`commitFilesGoverned`;`commitFilesGoverned.files` required 且每项 required `path`、`content_base64`。
- 无 Bearer 的 model-api read 请求和 loopback Bridge read 请求均返回 HTTP 401;writes runtime flag 保持 false。
- systemd effective User/Group 为 `aiman:gitops`;Gitea API 来源为 staging service effective `AIOS_GITOPS_GITEA_API_URL=https://g.cnnetmkt.top/git/api/v1` 与 Bridge `AIOS_GITEA_API_BASE=https://g.cnnetmkt.top/git/api/v1`,不是业务代码硬编码。
- effective silent-release root 为 `/var/lib/aios-gitops/model-api-staging/silent-release`,ownership/mode 为 `aiman:gitops 750`,父目录同为 `aiman:gitops 750`;旧报错路径 `/var/lib/aios-gitops/silent-release` 不存在,未被错误地当作当前 runtime 路径。
- systemd 使用 `LoadCredential=action_tokens:/data/aios-gitops/runtime/secrets/gitops.tokens` 和 `%d/action_tokens` 投影;registry 文件 `root:root 600`,未打印或复制 bearer。
- 当前 token registry 是 hash-only 记录;本次没有打印、重用 Gitea/SSH/service token,也没有启用 dev auth bypass。
重启后的 service restart acceptance 已完成:URL、权限、Action schema、version parser 代码产物和 writes=false 均保持有效;production service 未重启。
未通过/未执行:
- 无法执行正式 bearer 下的 `read -> lease -> commitFilesGoverned`,因此没有伪造两文件 commit OID、lease USED、Gitea Git/API readback 或 idempotency replay 证据。
- 未执行临时 writes=true governed commit;staging 最终保持 `AIOS_GITOPS_WRITES_ENABLED=false`。
- 未宣称完整 Action smoke、restart 后 governed commit 或 `TASK_VERIFIED`。
## 保护边界
本轮只安装 staging artifact/drop-in;production service 未重启。`main`、`V2R2C1` tag、baseline target/evidence closure 未写入。两个既有 untracked 目录 `runtime/agent-observability/`、`runtime/task-center/` 原样保留。
## 剩余风险与下一步
主要闭环阻塞是:需要 Owner/正式 Action broker 提供当前有效 bearer(不在聊天中传递)。此外 mandatory suite 仍有上述两个已知环境/部署失败。拿到 bearer 后应从当前 branch HEAD 重新读取 source OID,按 runbook 执行一次真实两文件 governed commit、Gitea 三方 OID readback、idempotency/stale-OID 负例、staging restart 后复测;在此之前 Task 必须保持 `BLOCKED/NEEDS_REWORK`,不能标记 `SUCCEEDED`。
@@ -0,0 +1,224 @@
# AIOS Action OpenAPI 操作数上限修复证据
## 结论
staging 上的 `https://cnnetmkt.top/api/openapi-chatgpt-actions.yaml` 已从 32 个
实际 OpenAPI operations 收缩到 26 个,低于 ChatGPT Action 的 30 操作限制。
公网重新抓取返回 HTTP 200,schema version 为
`V2R2C1-chatgpt-actions-26ops`。`commitFilesGoverned` 仍是公开的批量 governed
writer,并且现在在 OpenAPI 中显式要求 `bearerAuth`、标记为
`x-openai-isConsequential: true`,required `files` 契约保持一致。
本次只修改 branch/staging overlay;没有写入 production service、main、release
tag 或 baseline registry。ChatGPT Action 编辑器仍需要重新导入或刷新这个 URL,
因为客户端可能缓存旧的 32-operation schema。
## 修改前事实
- Repository:`aios/aios`
- Branch:`agent/fix-action-contracts-20260825-1407`
- 修复前 branch tip:`767c7ff6a975fe18a701b95cd6dacca8600eddc0`
- 本轮最终 HEAD:`7e5525cfcbbaa3898c2e5ed3dc9c13e321908e61`
- staging model-api unit:`aios-gitops-model-api-v2r2c1-staging.service`
- staging model-api port:`127.0.0.1:18893`
- staging Bridge port:`127.0.0.1:18892`
- systemd Gitea API URL:`https://g.cnnetmkt.top/git/api/v1`
- 当前 service User/Group:`aiman:gitops`
- 修复前公网 schema:HTTP 200,24,721 bytes,SHA256
`a0775b0719a76e08a95f3257a3d3b2a73b70e1c55c162914250f80dd28da022d`;30 paths、
32 operations。两个 path 同时有多个 method,因此 path 数不是 operation 数。
- 修复前触发错误:`OpenAPI 规范最多可以包含 30 个操作`。
## 根因与决策
根因不是 URL host 或 YAML 传输错误,而是公开 Action projection 中混入了旧式
Git 写原语和同义 approval alias。公开 schema 同时暴露 legacy primitives 与
`commitFilesGoverned`,最终 operation 数为 32。
本次投影收缩为 26 个 operation:
- 先前修复中移除粗粒度旧别名 `POST /api/file` (`commitFile`);
- 先前修复中移除当前 isolated-writer policy 明确禁止的 `POST /api/commit`
(`commitChanges`);
- 本轮移除 `submitSignedApproval`,因为它是 `submitOwnerApproval` 的 alias;
- 本轮从 Actions projection 隐藏 `createPatch`、`createBranch`、
`commitFileAtomic` 三个旧式 Git 写原语。runtime 兼容处理没有删除,canonical
对外批量写入为 `/v2/commit-files`;
- 保留 release candidate、release consistency、release acceptance、tag
publication status、release saga、getDiff 和 recovery 相关既有能力,满足本任务
的既有 smoke contract。尚未实现的 `getReleaseSnapshot`/`getReleaseVerification`
聚合 API 不在本次冒险重构范围内。
此外,`commitFilesGoverned` 的 runtime fragment 生成器现在生成:
- `security: [{bearerAuth: []}]`;
- `x-openai-isConsequential: true`;
- `files.maxItems: 50`;
- `idempotency_key` 的长度和字符约束。
## 修改文件与提交
- `docs/gitops/openapi-chatgpt-actions.yaml`:canonical Action projection,26-op
allowlist、移除 legacy/alias、补齐 batch writer security。
- `active-runtime/app/aios_chatgpt_bridge_v2.py`:runtime contract fragment 的
单一生成来源与公开 security/batch constraints。
- `tests/test_action_contract_regressions.py`:26-operation gate、legacy operation
negative assertions、runtime/schema parity、bearer/consequential assertions。
- `runbooks/action-openapi-operation-limit-20260825.md`:操作上限、projection
allowlist、构建和刷新手册。
- `ops/gitops-migration/bind_external_action_surface_final_staging.sh` 及先前的
staging binding 测试/报告:本任务分支已将 `/v2/` 公开路由绑定到 staging Bridge
`127.0.0.1:18892`,没有使用 production `18920`。
提交:
- `4313f3e44a1e4be69d2608fb1fc18833aa0c82e8`
`fix(action): keep public schema within operation limit`
- `7e5525cfcbbaa3898c2e5ed3dc9c13e321908e61`
`fix(action): project governed core within 26 operations`
保留的未跟踪目录 `runtime/agent-observability/`、`runtime/task-center/` 未被加入
提交或删除。
## 测试结果
修改后 focused regression:
```text
13 passed in 0.15s
```
包含既有 Action/release/staging focused tests:
```text
32 passed, 11 subtests passed in 0.36s
```
mandatory suite:
```text
264 passed, 28 subtests passed in 4.79s
```
完整 suite:
```text
721 passed, 2 skipped, 2 failed, 154 subtests passed in 34.02s
```
两个失败与本次变更无关且在修改前已复现:
1. `tests/test_agent_event_bus.py::test_unified_event_flow` 使用了残留的
`/tmp/aios-agent-events-test`,已有 1 个旧 event,断言得到 2;
2. `tests/test_signer_contract_compatibility.py::test_real_signer_help_and_self_test_are_available`
的已部署 signer `self-test` 返回 42。`sign --help` 仍正常,live signer integration
另行 skip。
没有出现由本次 schema/runtime 改动引入的 mandatory regression。
## staging 构建与部署
使用仓库现有流程:
```text
scripts/build_v2r2c1_model_api_staging.py
source_oid: 7e5525cfcbbaa3898c2e5ed3dc9c13e321908e61
artifact: /data/aios-gitops/artifacts/action-openapi-limit-20260825-7e5525cf/aios-v2r2c1-model-api-7e5525cfcbbaa3898c2e5ed3dc9c13e321908e61.tar.gz
artifact_sha256: 2163c2e0467f3bd0a97f8d2b95e3277391c47dcf3ebafb8fa372a01c3f530ac0
deployment_state: STAGING
```
systemd 新 drop-in 持久化了 artifact/runtime identity、Gitea URL、`STAGING`、
`AIOS_GITOPS_WRITES_ENABLED=false`、`aiman:gitops` 和精确的 read/write paths。
当前生效 runtime:
```text
/opt/aios/gitops-model-api/7e5525cfcbbaa3898c2e5ed3dc9c13e321908e61-action-openapi-limit/runtime
```
silent-release 权限在两次重启后均为:
```text
/var/lib/aios-gitops/model-api-staging/silent-release aiman:gitops 750
ACL: user::rwx, group::r-x, other::---
```
部署前的原生 final drop-in 已备份至:
```text
/data/aios-gitops/backups/action-openapi-limit-deploy-20260825T112700Z/
```
第一次 restart 后 MainPID 为 `1775067`;第二次按要求 restart 后 MainPID 为
`1788253`,两次均 active/running。
## 公网 Action smoke
最终公网抓取:
```text
HTTP 200
Content-Type: application/yaml; charset=utf-8
bytes: 21553
SHA256: 798cac0ebd40097c63e5e01ab52c9fdbc255707c4f3820b77161c83349cd5199
info.version: V2R2C1-chatgpt-actions-26ops
paths: 25
operations: 26
```
关键 operation contract:
```text
commitFilesGoverned required:
task_id, repository, expected_branch_oid, target_branch, generation,
idempotency_key, files, message
files item required: path, content_base64
security: bearerAuth
x-openai-isConsequential: true
```
restart 后复测:
- `/api/health`:200,`status=ok`,`deployment_state=STAGING`,runtime OID 为
`7e5525cf…`,artifact SHA 与 receipt 一致,`production_deployed=false`;
- `/v2/runtime`:200,active,Bridge schema `aios.chatgpt-gitops-bridge.v2`;
- `/v2/capabilities`:200,governed writes enabled、legacy/main/baseline writes
false;
- `/api/release-consistency?version=V2R2C1...`:200,`status=PASS`,包含
Gitea listing/tag/OID readback;
- `/api/release-acceptance?version=V2R2C1...`:200,进入 policy evaluation,
`accepted=false` 是既有 acceptance policy 结果,不是 URL 校验错误;
- `/api/tag-publication-status?version=V2R2C1`:200,结构化 `UNKNOWN` 状态;
- `/getReleaseSaga?candidate_id=nonexistent-action-limit-20260825`:404,结构化
`NOT_FOUND`/empty-state,不是权限错误;
- empty diff:200,`total_size_bytes=0`、`truncated=false`;
- non-empty diff:200,31 changed paths、208,639 bytes;
- large diff:200,732 changed paths、3,577,251 bytes、`truncated=true`、patch
bounded at 262,144 bytes,无 transport exception。
保护边界复测:
```text
refs/heads/main = 3244f0f17f8845592ac94d585f5ebd65693c9f29
refs/tags/V2R2C1 = 07fc2746b688f5f243f29b39de39f32edc3dd9d2
service writes_enabled = false
service deployment_state = STAGING
```
main/tag/baseline registry/production 未被本次部署写入。
## 剩余风险与后续动作
- ChatGPT 客户端是否已刷新不能由 staging 服务端证明;请在 Action 编辑器重新导入
URL。若仍显示旧数量,删除旧 schema 后重新导入,确认 `V2R2C1-chatgpt-actions-26ops`。
- 完整 suite 的两个环境失败仍待独立清理 `/tmp` fixture 和修复 signer self-test;
它们不是本次 Action schema 修复的回归。
- 未来若实现正式 `getReleaseSnapshot`、`getReleaseVerification` 和 governed task
Action projection,应单独设计/测试,不应通过隐藏本任务仍要求的 release read paths
来制造假收缩。
## 状态
`STAGING_REPAIR_PASS`:公网 staging schema、runtime、release/Gitea readback、
restart 后 smoke 和保护边界均通过;production 未触碰。外部 ChatGPT Action UI
刷新仍是客户端侧动作,不作为服务端部署失败。
@@ -0,0 +1,21 @@
# Runbook Addendum — External Action Connector Schema Refresh
This addendum is staging-only and applies to the `fix-action-contracts-20260825-1407` closure.
## Contract source
Use `active-runtime/app/aios_chatgpt_bridge_v2.py` as the runtime source of required fields. Use `render_openapi_contract_fragment()` and `docs/gitops/openapi-chatgpt-actions.yaml` for the generated/published projection. `commitFilesGoverned` must require `files`, and each item must require `path` and `content_base64`.
The public OpenAPI server is the site root and its Bridge v2 paths are `/v2/...`. Install the route with `ops/gitops-migration/bind_external_action_surface_final_staging.sh`; it must target `127.0.0.1:18892`, preserve the Authorization headers, pass `nginx -t`, reload Nginx, and never target production `18920`.
## Connector lifecycle gate
The external Action connector must be refreshed through its supported registration or re-registration lifecycle. Record the connector identity, schema source URL, fetched schema digest, cache invalidation or registration acknowledgement, and a callable-tool inspection showing required `files`. Do not make a local fixture or a second hand-written schema a substitute for this evidence.
If the connector registration API, cache lifecycle, callable inspection, or formal staging Action bearer is unavailable, stop at `PENDING_EXTERNAL_CONNECTOR` and record `FORMAL_STAGING_ACTION_BEARER_ISSUANCE_NOT_AVAILABLE`. Never reuse Gitea, SSH, task-control, Writer, or old token credentials.
## Acceptance
After connector refresh, use the actual callable Action once to submit two files through `commitFilesGoverned` on an isolated `agent/*` branch. Verify the task/lease lineage, Authority ref, Git remote ref, and Gitea API branch OID are equal. Read the durable job, require `COMPLETED`, then call terminal reconcile and require an idempotent successful read-back. Restart only staging Action/Bridge/model services and repeat callable schema inspection and the batch/reconcile smoke.
Do not mark `SUCCEEDED` until the callable schema and batch evidence exist. If refresh breaks health, routes, auth, schema, or protected-ref boundaries, restore the recorded Nginx backup and previous staging artifact through the governed deployment procedure, then diagnose.
@@ -0,0 +1,58 @@
# ChatGPT Action OpenAPI 操作数上限运行手册
## 目的
`https://cnnetmkt.top/api/openapi-chatgpt-actions.yaml` 必须保持不超过 ChatGPT
Action 导入支持的 30 个实际操作;当前投影硬门槛收紧为 26,为未来治理、诊断
和任务协议保留余量。OpenAPI 中同一路径的多个 HTTP method 分别计数;路径数量
不等于操作数量。
## 单一来源与构建
规范唯一来源是 `docs/gitops/openapi-chatgpt-actions.yaml`。模型 API staging
构建脚本 `scripts/build_v2r2c1_model_api_staging.py` 将该文件复制到 immutable
artifact,并同时校验 `/v2/commit-files` runtime contract。禁止直接编辑已部署
artifact 或 production 文件。
变更后至少执行:
```bash
python3 - <<'PY'
import yaml
from pathlib import Path
doc = yaml.safe_load(Path('docs/gitops/openapi-chatgpt-actions.yaml').read_text())
methods = {'get', 'post', 'put', 'patch', 'delete', 'head', 'options'}
operations = [(p, m) for p, item in doc['paths'].items() for m in item if m in methods]
assert len(operations) <= 26, len(operations)
print(f'paths={len(doc["paths"])} operations={len(operations)}')
PY
PYTHONPATH=active-runtime/app python3 -m pytest -q tests/test_action_contract_regressions.py
```
## 公开面选择
公开 schema 保留受治理的 `/v2/commit-files` 以及 release、
tag publication、consistency、acceptance、saga 和 diff 操作。粗粒度旧别名
`/api/file`、`/api/commit`、`/api/branch`、`/api/patch` POST、`/api/commit-file`
以及 `/submitSignedApproval` 等旧别名/旧写原语不得重新加入公开 Action schema;
批量写入使用 `/v2/commit-files`。完整 runtime 兼容路由不因此删除。
`commitFilesGoverned` 必须显式声明 `security: [{bearerAuth: []}]` 和
`x-openai-isConsequential: true`;schema 与 Bridge runtime 的 required fields、
batch limits 必须由同一生成链维护。
## 部署与复验
只使用 `scripts/build_v2r2c1_model_api_staging.py` 生成 staging artifact,保留
当前 systemd drop-in 备份,更新 staging model-api 的 artifact 路径后执行
`daemon-reload` 和 staging unit restart。部署后从公网重新抓取 schema,验证:
- HTTP 200,Content-Type 为 YAML;
- 实际操作数为 30 或更少;
- `commitFilesGoverned` 仍包含 required `files`,每项包含 `path`、`content_base64`;
- `/v2/runtime`、capabilities、release consistency/acceptance、tag status、
saga empty-state、diff small/empty/large smoke 均正常;
- staging unit active,production/main/tag/baseline 未写入。
Action 客户端可能缓存旧规范。公网 schema 复验通过后,在 ChatGPT Action 编辑器
重新导入该 URL 或执行刷新;服务端无法替代客户端已保存的旧 schema cache。
@@ -0,0 +1,225 @@
# Agent Execution Contract — Action Contract Regression Repair
## Objective
Fully repair the current AIOS Action runtime/contract overlay regressions on the single server, without modifying protected refs directly and without introducing new infrastructure. The task is not complete until the deployed staging/action overlay passes all acceptance gates.
## Authority and scope
- Repository: `aios/aios`
- Base at task creation: `3244f0f17f8845592ac94d585f5ebd65693c9f29`
- Governed task: `fix-action-contracts-20260825-1407`
- Target branch: `agent/fix-action-contracts-20260825-1407`
- Protected refs must not be directly written: `main`, release tags, baseline registry, production runtime.
- No new SaaS, database, agent framework, or monitoring platform may be introduced.
## Non-interactive rules
The Agent must run the whole flow unnattended:
1. Collect facts.
2. Determine root causes.
3. Make the smallest correct changes.
4. Run unit/integration/contract tests.
5. Deploy to staging only.
6. Restart only the affected non-production services.
7. Run the full Action smoke suite.
8. On failure, automatically collect logs, diagnose, fix, and retry.
9. If a change causes a new regression, automatically roll back the staging change to the previous known-good configuration and record the failure.
10. Stop only if an owner decision is required for a high-risk, irreversible, or production-boundary action.
## Fact collection — required before editing
Record all of the following in the final evidence bundle:
- `id`, `hostname`, `uname -a`, `date --iso-seconds`
- `git rev-parse --show HEAD` for authority worktree and running overlay source
- `git status --porcelaine=v2`
- `git remote -v`
- current systemd service unit(s), drop-ins, Environment/EnvironmentFile sources for GitOps/Bredge/release services
- `systemctl show <affected-service>` including `User=`, `Group=`, `Environment=`, `EnvironmentFile=`, `ExecStart=`
- resolved Gitea API base URL after env/config expansion; redact secrets but not scheme/host
- `stat -c, `namei -l`, `getfacl`, als -ld on `/var/lib/aios-gitops/silent-release` and its parents
- find Action OpenAPI/schema generator and runtime operation definitions for `commitFilesGoverned`
- find the release version validator(s) and all callers of `getTagPublicationStatus`
- find `reconcileGitOpsJob` implementation and durable binding validation logic
- find `getDiff implementation and transport output limits
Do not edit any file until the actual implementation and configuration sources have been located.
## Repair 1 — Gitea API URL
Symptoms:
- `getReleaseConsistency` → `Gitea API URL must be HTTP(S)`
- `runReleaseAcceptance` → `Gitea API URL must be HTTP(ÊX
Required behavior:
- Determine which env/config key is projected into the current contract overlay.
- Compare the deployed overlay with V2R2C1 known-good configuration/deployment manifests.
- Restore a valid `HTTP://` or `HTTPS://` Gitea API base URL.
- Add startup config validation so invalid schemes fail at health/startup with a config-specific message, not during a release check.
- Never hard-code a different Gitea host if the existing deployment already has a single authoritative source.
## Repair 2 — durable release state permissions
Symptom:
- `getReleaseSaga` → `Permission denied: '/var/lib/aios-gitops/silent-release'`
Required behavior:
- Identify the actual runtime service User/Group.
- Grant only the required traverse/read/write permissions for the durable state directories the service must use.
- Prefer ownership + group mode or a specific ACL; keep the directory non-world-writable.
- Persist the permission in deployment/install logic so restart/reboot does not regress.
- Verify create/read/update of a staging-only saga state file under the service identity.
## Repair 3 — release version contract
Symptom:
- `getTagPublicationStatus("V2R2C1")` → `invalid release version`
- The same runtime exposes `V2R2C1` as `approved_release_version`.
Required behavior:
- Locate all version validators; consolidate onto one canonical parser.
- Accept the AIOS canonical namespace `^V[0-9]+R[0-9]+C[0-9]+$`.
- Preserve any additional already-supported legacy formats unless they are security-sensitive.
- Add contract tests proving that the version accepted by capabilities is accepted by status, candidate, consistency, acceptance, and tag publication.
## Repair 4 — `commitFilesGoverned` schema parity
Symptom:
- Runtime capability metadata requires `files`.
- Published Action tool schema does not expose `files`.
- Call reaches Bridge and fails `BRIDGE_REQUEST_FIELDS_MISSING`.
Required behavior:
- Find the canonical runtime operation contract.
- Generate the external tool schema from the same source of truth; do not manually copy a parallel schema.
- `files` must be required and must represent a list of file objects with at least `path` and `content_base64`.
- Add a parity test that compares runtime required fields with published Action required fields.
- Add a real governed commit test that writes two test files to an isolated `agent/` ibranch and verifies remote OID.
## Repair 5 — terminal reconciliation
Symptom:
- A governed job read back as `COMPLETED' but `reconcileGitOpsJob` failed `DURABLE_BINDING_INVALID`.
Required behavior:
- Reconciling a terminal successful job must be idempotent.
- An expired writer lease must not invalidate a durable binding for a job that is already terminal.
- Active-job reconciliation may require a live lease; terminal read-back must not.
- Add cases for COMPLETED, FAILED, CANCELLED / equivalent terminal states.
## Repair 6 — bounded `getDiff` output
Symptom:
- Large valid diffs fail with `ResponseTooLargeError` or `GIT_OUTPUT_LIMIT_EXCEEDED`.
Required behavior:
- Do not load unbounded diff text into the Action response.
- Return bounded summary data: commit identities, changed paths/stats, total bytes, and `truncated: true` when the full patch exceeds the safe limit.
- If pagination already exists in the contract use it; otherwise prefer deterministic truncation over a breaking schema change.
- Add tests for small, empty, and oversize diffs.
## Staging deployment procedure
1. Run the existing mandatory test suite before modification; record baseline.
2. Apply code and config changes only on the governed branch/worktree.
3. Run focused regression tests.
4. Run the full mandatory suite.
5. Build the staging contract overlay using the repository's existing build/deploy path.
6. Deploy to staging/non-production only.
7. If systemd units are changed: `systemctl daemon-reload`, then restart only the affected staging services.
8. Wait for the existing health endpoint/runtime status to become healthy.
9. Run the acceptance suite below.
If any step fails:
- collect `journalctl` logs and relevant app logs;
- diagnose root cause;
- automatically repair and retry when non-destructive;
- if a new change breaks staging, restore the previous staging artifact/config and restart the affected service;
- never silently continue after a failed gate.
## Acceptance suite
All of the following must pass on the same deployed staging overlay:
- Runtime status is `active` and healthy after restart.
- Gitea Git/API readback works from the runtime service identity.
- `getReleaseConsistency(V2R2C1, 998d30304787e31e5ae607c0bd86b5edb2b0e5ad, a8b906d1bc24088c19647e98de6a679268f6d0a2, approved_plan) returns a structured result and reaches evaluation.
- `runReleaseAcceptance` for the same identities reaches policy evaluation, not URL validation error.
- `getTagPublicationStatus("V2R2C1")` returns structured state, not `invalid release version`.
- `getReleaseSaga` for a nonexistent candidate returns NOT_FOUND/empty-state semantics, not filesystem permission failure.
- `commitFilesGoverned` exposes its required `files` parameter in the published Action schema.
- A real two-file governed commit to an isolated `agent/` branch succeeds and remote branch OID matches.
- Resuming a COMPLETED job returns the existing `JOB_TERMINAL` semantics or idempotent success as designed.
- Reconciling a COMPLETED governed job is successful/idempotent and does not require an active lease.
- Small/empty `getDiff still works.
- Large `getDiff` returns bounded output with truncation metadata instead of a transport exception.
- Existing protected-ref controls remain unchange: no direct main/tag/baseline/production write.
## Restart / reboot persistence
After the first full pass:
- Restart the staging Action/Bridge/release services one more time without changing files.
- Rerun the acceptance suite.
- Permissions, Gitea URL, and schema must still be correct after restart.
- If the repository already has a safe staging reboot test, run it as well.
## Evidence and completion
Create a single durable report in the repository, e.g. `reports/action-contract-repair-evidence-20260825.md`, containing:
- pre-change facts
- root cause for each regression
- files changed and why
- config/permission changes
- test commands and exact results
- staging deployment identity (commit/artifact/config digest)
- restart-persistence results
- final Action smoke results
- any remaining risks
Commit all source, test, deployment config, and runbook changes to the governed task branch. Do not mark the task SUCCEEDED until all acceptance gates pass on the actually deployed staging overlay.
## Automatic rollback conditions
Automatically roll back the staging deployment if any of the following occur:
- service fails to become healthy after restart
- protected-ref write boundary is broken
- Gitea readback that worked before the change stops working
- durable saga state cannot be read or recovered
- mandatory test suite regresses below the pre-change baseline
Do not automatically roll back an existing production release. Rollback here refers only to the staging overlay/service configuration changes made by this task.
## 2026-08-25 execution record
The repair branch was deployed through the existing immutable staging builder. The final source code before the evidence commit was `2ec49005bde44f6981b14351500847d594e0276c`; the deployed artifact was built from that source with SHA256 `f67aed6cc0cce1f08197485c3eea204d3ef4d3989f44dc98e8ea2691e5886cbf`. The final staging drop-ins use a lexically highest `zzzz...-999.conf` layer so they override earlier diagnostic overlays.
The runtime configuration is staging-only: `User=aiman`, `Group=gitops`, API base `https://g.cnnetmkt.top/git/api/v1`, baseline authority `/var/lib/aios-gitops/model-api-staging/baseline/current.json`, and read/write access to the staging state plus the authoritative bare repository required by the governed Writer/CAS path. No `main`, protected tag, baseline registry, or production write is enabled.
The release executor now binds both the configured staging baseline and the immutable runtime package's `versions/V2R2C1.json` projection. The bridge passes `AIOS_GITOPS_BASELINE_AUTHORITY` into `RuntimeConfig`; this prevents release consistency and acceptance from falling back to missing publication-workspace files.
The final acceptance result is recorded in `reports/action-contract-repair-evidence-20260825.md`. Contract smoke is green, and release consistency is `PASS`; release acceptance is structured policy output but remains `accepted=false` because the pre-existing publication state is `REMOTE_PUSH_PENDING` for the protected `V2R2C1` tag. The task must therefore remain `NEEDS_REWORK` until an authorized owner resolves that independent protected-tag state.
## External Action Surface final closure attempt (2026-08-25)
本轮仅部署 staging/non-production;没有直接写入 main�release tag�baseline registry 或 production。
Public route ownership: /api/governed-write-capabilities, /api/control-plane-snapshot, /api/online-agent-task, and /api/online-agent-task-events are exact-location proxied to 127.0.0.1:18890; generic /api/ remains on 127.0.0.1:18893. The durable installer is ops/gitops-migration/bind_external_action_surface_final_staging.sh; it backs up Nginx, runs nginx -t, reloads, and forwards Authorization/X-AIOS-Token.
A task-control token, local Writer grant, Gitea consumer, SSH credential, or old token file is not an external Action bearer. If formal issuance is unavailable, do not print or rotate secrets, enable disabled writes, or manufacture commit evidence; record FORMAL_STAGING_ACTION_BEARER_ISSUANCE_NOT_AVAILABLE. Public release-consistency and release-acceptance use the canonical Bridge v2 evaluator. A Task API continuation remains QUEUED until a legitimate Agent claims it and gives durable result evidence.
@@ -0,0 +1,81 @@
# AIOS 最小自治修复闭环运行手册
## 范围
本手册只覆盖 staging/non-production 的最小链路:
```text
readRepositoryFile -> acquireWriterLease -> commitFilesGoverned
-> immutable staging build/reload -> runtime/read-back
```
不覆盖 release/recovery/listPatches 语义,不得写入 `main`、release tag、baseline
或 production。
## Action contract
公开 Action schema 的 canonical URL 是:
`https://cnnetmkt.top/api/openapi-chatgpt-actions.yaml`。
本轮增加两个 operation,公开面共 28 个,保留 30 个上限余量:
- `readRepositoryFile`:`POST /readRepositoryFile`,必填 `repository`, `ref`, `path`, `max_bytes`;只从 authoritative bare Git object database 读取,禁止绝对路径、`..`、反斜线、NUL 和 `.git`,单次最多 262144 bytes。
- `acquireWriterLease`:`POST /acquireWriterLease`,只接受 `CONTINUE_EXISTING`,必须绑定 durable task、repository、target branch、base OID、当前 expected branch OID、generation 和短 TTL。
- `commitFilesGoverned`:既有 `POST /v2/commit-files`,必须先有同一 task/generation/expected OID 的 lease;`files` 为数组,每项必须含 `path` 与 `content_base64`。
model-api facade 对上述三个入口执行 Action bearer 认证,并把 Authorization/X-AIOS-Token 透传给 loopback Bridge。Bridge 再执行 operation-specific scope:read 允许 `repository:read` 或兼容的 `file:write`,lease 需要 `agent-task:control`,commit 需要 `file:write`。
## 构建与部署
从当前隔离 agent branch 的完整 source OID 构建:
```bash
python3 scripts/build_v2r2c1_model_api_staging.py \
--source-root . \
--output-root /var/lib/aios-gitops/bridge-v2-staging/candidate-artifacts/<id> \
--source-oid <40-hex-source-oid>
```
构建 receipt 必须声明 `STAGING`、`aios/aios`、`legacy_global_writes_enabled=false`,并记录 artifact SHA-256。使用 versioned systemd drop-in template,将 source OID、artifact SHA-256 和 immutable runtime path 渲染到 staging 两个 unit;同时使用:
```ini
LoadCredential=action_tokens:/data/aios-gitops/runtime/secrets/gitops.tokens
Environment=AIOS_GITOPS_TOKEN_FILE=%d/action_tokens
```
这样由 systemd 投影已有 Action token registry,不复制或打印 token,不使用 Gitea token 代替 Action bearer。部署后只执行:
```bash
systemctl daemon-reload
systemctl restart aios-chatgpt-bridge-v2-staging.service
systemctl restart aios-gitops-model-api-v2r2c1-staging.service
```
## 验收顺序
1. `systemctl is-active` 两个 staging unit;读取 `/api/health`、`/v2/runtime`。
2. 从公网 URL 重新抓取 YAML,解析 operation 数量,确认 `<=30`,并确认 `files`/`path`/`content_base64` required parity。
3. 使用正式 Action bearer:
```text
readRepositoryFile(repository, ref=agent/<branch> or exact OID, path, max_bytes)
acquireWriterLease(task_id, repository, CONTINUE_EXISTING, base_oid,
expected_branch_oid=<read resolved OID>, target_branch,
generation, lease_seconds)
commitFilesGoverned(task_id, repository, expected_branch_oid, target_branch,
generation, idempotency_key, two files, message)
```
4. 校验 authority branch OID、Gitea Git ref OID、Gitea API branch OID 和 `commit_visible` 一致;重复相同 idempotency key 不得生成第二个 commit,旧 expected OID 必须拒绝。
5. 验证 staging runtime source OID/artifact SHA-256 与 build receipt 一致,再重复完整 smoke。
6. 若 bearer 不可用,状态必须是 `BLOCKED`,不能用 SSH、root、Gitea、旧 token、service credential 或 dev bypass 代替。
完整 suite 使用:
```bash
PYTHONDONTWRITEBYTECODE=1 PYTHONPATH=.:services/aios-release-controller pytest -p no:cacheprovider -q
```
## 回滚边界
只回滚本轮创建的 staging artifact/drop-in;不得回滚 production release。若 health、Gitea readback、protected-ref boundary 或 mandatory suite 回归,恢复前一份 staging drop-in,采集 `journalctl -u` 日志,再继续诊断。
+169
View File
@@ -0,0 +1,169 @@
#!/usr/bin/env python3
from __future__ import annotations
import argparse
import hashlib
import json
import os
from pathlib import Path
import sys
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',
'aios_chatgpt_bridge_v2.py',
'aios_chatgpt_bridge_v2_server.py',
'governed_production_release.py',
'v2r2c1_production_adapter.py',
'v2r1c7_production_adapter.py',
'aios_governed_task_bootstrap.py',
'aios_continuation_executor.py',
'aios_continuation_lease.py',
'aios_authority_git_executor.py',
'scoped_lease_store.py',
'aios_gitops_reconciliation_core.py',
'authorized_task_writer_provisioning.py',
'signer_contract_compatibility.py',
)
OPTIONAL_APP = (
'aios_chatgpt_actions_api.py',
'aios_model_api_staging_list_patches_overlay.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'
sys.path.insert(0, str(app))
from aios_chatgpt_bridge_v2 import render_openapi_contract_fragment
runtime = source / 'runtime' / 'agent-os'
docs = source / 'docs' / 'gitops'
if not app.is_dir():
raise SystemExit('active-runtime/app missing')
paths = []
root_version = source / 'release_version.py'
if not root_version.is_file():
raise SystemExit('required source dependency missing: release_version.py')
paths.append((Path('release_version.py'), root_version))
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():
if ' /v2/commit-files:' not in openapi.read_text(encoding='utf-8'):
raise SystemExit('published OpenAPI is missing generated commitFilesGoverned contract')
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_list_patches_overlay.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())
+10 -4
View File
@@ -9,10 +9,16 @@ WORKSPACE="${AIOS_GITOPS_WORKSPACE:-/data/aios-gitops/workspace}"
TAG_REMOTE="${AIOS_GITOPS_GITEA_REMOTE:-gitea}" TAG_REMOTE="${AIOS_GITOPS_GITEA_REMOTE:-gitea}"
BRANCH_REMOTE="${AIOS_GITOPS_BRANCH_REMOTE:-gitea}" BRANCH_REMOTE="${AIOS_GITOPS_BRANCH_REMOTE:-gitea}"
case "$VERSION" in SCRIPT_ROOT="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd)"
V[0-9]*R[0-9]*C[0-9]*) ;; if ! PYTHONPATH="$SCRIPT_ROOT:${PYTHONPATH:-}" python3 - "$VERSION" <<'PY'
*) echo "ERROR: version must match VxRxCx" >&2; exit 64 ;; import sys
esac from release_version import validate_release_version
validate_release_version(sys.argv[1])
PY
then
echo "ERROR: version must match VxRxCx" >&2
exit 64
fi
for value in "$TARGET_OID" "$EVIDENCE_CLOSURE_OID"; do for value in "$TARGET_OID" "$EVIDENCE_CLOSURE_OID"; do
[[ "$value" =~ ^[0-9a-f]{40}$ ]] || { [[ "$value" =~ ^[0-9a-f]{40}$ ]] || {
@@ -1,8 +1,9 @@
from __future__ import annotations from __future__ import annotations
import hashlib,json,re,tarfile import hashlib,json,re,tarfile
from release_version import RELEASE_VERSION_RE
from pathlib import Path from pathlib import Path
from typing import Any from typing import Any
OID_RE=re.compile(r"^[0-9a-f]{40}$"); SHA_RE=re.compile(r"^[0-9a-f]{64}$"); VERSION_RE=re.compile(r"^V2R1C[0-9]+$") OID_RE=re.compile(r"^[0-9a-f]{40}$"); SHA_RE=re.compile(r"^[0-9a-f]{64}$"); VERSION_RE=RELEASE_VERSION_RE
REQUIRED_SOURCE={"schema","version","repository","base_ref","base_oid","binding_mode","release_readiness","production_deployed","tag_created","baseline_changed","production_service","production_port","rollback_metadata","controller_schema","required_checks"} REQUIRED_SOURCE={"schema","version","repository","base_ref","base_oid","binding_mode","release_readiness","production_deployed","tag_created","baseline_changed","production_service","production_port","rollback_metadata","controller_schema","required_checks"}
REQUEST_KEYS={"schema","version","repository","base_oid","target_oid","artifact_uri","artifact_sha256","artifact_size","evidence_closure_oid","approval_plan_id","execute","production_deployed","production_service","production_port","rollback_metadata","current_tag_oid","source_manifest_sha256"} REQUEST_KEYS={"schema","version","repository","base_oid","target_oid","artifact_uri","artifact_sha256","artifact_size","evidence_closure_oid","approval_plan_id","execute","production_deployed","production_service","production_port","rollback_metadata","current_tag_oid","source_manifest_sha256"}
class ManifestValidationError(ValueError): class ManifestValidationError(ValueError):
@@ -62,7 +63,7 @@ def validate_release_request(source_manifest:dict[str,Any],request_path:Path,exp
if not isinstance(request["approval_plan_id"],str) or not request["approval_plan_id"].startswith("sha256:"): raise ManifestValidationError("APPROVAL_PLAN","invalid approval plan") if not isinstance(request["approval_plan_id"],str) or not request["approval_plan_id"].startswith("sha256:"): raise ManifestValidationError("APPROVAL_PLAN","invalid approval plan")
if request["execute"] is not False or request["production_deployed"] is not False: raise ManifestValidationError("EXECUTION_NOT_ALLOWED","must be non-executing") if request["execute"] is not False or request["production_deployed"] is not False: raise ManifestValidationError("EXECUTION_NOT_ALLOWED","must be non-executing")
if request["production_service"]!=source_manifest["production_service"] or request["production_port"]!=source_manifest["production_port"]: raise ManifestValidationError("SERVICE_IDENTITY","request service mismatch") if request["production_service"]!=source_manifest["production_service"] or request["production_port"]!=source_manifest["production_port"]: raise ManifestValidationError("SERVICE_IDENTITY","request service mismatch")
if not isinstance(request["current_tag_oid"],str) or request["current_tag_oid"] not in {"V2R1C6","9e65bd59baea392294c0f253046d3ab116f63145"}: raise ManifestValidationError("PROTECTED_TAG","previous tag binding changed") if not isinstance(request["current_tag_oid"],str) or request["current_tag_oid"] not in {"V2R1C7","82792347937c0df59a8e02d6162775252bcad2e2"}: raise ManifestValidationError("PROTECTED_TAG","previous production tag binding changed")
_sha(request["source_manifest_sha256"],"source_manifest_sha256"); return request _sha(request["source_manifest_sha256"],"source_manifest_sha256"); return request
def build_approval_request(*,source_manifest_path:Path,target_oid:str,artifact_uri:str,artifact_sha256:str,artifact_size:int,evidence_closure_oid:str,rollback_metadata:str,current_tag_oid:str)->dict[str,Any]: def build_approval_request(*,source_manifest_path:Path,target_oid:str,artifact_uri:str,artifact_sha256:str,artifact_size:int,evidence_closure_oid:str,rollback_metadata:str,current_tag_oid:str)->dict[str,Any]:
source=_json_object(source_manifest_path); _oid(target_oid,"target_oid"); _sha(artifact_sha256,"artifact_sha256"); _oid(evidence_closure_oid,"evidence_closure_oid"); request={"schema":"aios.release-request.v2","version":source["version"],"repository":source["repository"],"base_oid":source["base_oid"],"target_oid":target_oid,"artifact_uri":artifact_uri,"artifact_sha256":artifact_sha256,"artifact_size":artifact_size,"evidence_closure_oid":evidence_closure_oid,"execute":False,"production_deployed":False,"production_service":source["production_service"],"production_port":source["production_port"],"rollback_metadata":rollback_metadata,"current_tag_oid":current_tag_oid,"source_manifest_sha256":sha256_file(source_manifest_path)}; request["approval_plan_id"]="sha256:"+sha256_bytes(_canonical(request)); return request source=_json_object(source_manifest_path); _oid(target_oid,"target_oid"); _sha(artifact_sha256,"artifact_sha256"); _oid(evidence_closure_oid,"evidence_closure_oid"); request={"schema":"aios.release-request.v2","version":source["version"],"repository":source["repository"],"base_oid":source["base_oid"],"target_oid":target_oid,"artifact_uri":artifact_uri,"artifact_sha256":artifact_sha256,"artifact_size":artifact_size,"evidence_closure_oid":evidence_closure_oid,"execute":False,"production_deployed":False,"production_service":source["production_service"],"production_port":source["production_port"],"rollback_metadata":rollback_metadata,"current_tag_oid":current_tag_oid,"source_manifest_sha256":sha256_file(source_manifest_path)}; request["approval_plan_id"]="sha256:"+sha256_bytes(_canonical(request)); return request
@@ -3,13 +3,13 @@ from __future__ import annotations
import json import json
from pathlib import Path from pathlib import Path
ROOT = Path(__file__).parents[2] ROOT = Path(__file__).parents[2]
def test_version_and_candidate_binding(): def test_version_and_candidate_binding():
assert (ROOT / "VERSION").read_text(encoding="utf-8").strip() == "V2R1C6" assert (ROOT / "VERSION").read_text(encoding="utf-8").strip() == "V2R1C7"
metadata = json.loads((ROOT / "versions/V2R1C6.json").read_text(encoding="utf-8")) metadata = json.loads((ROOT / "versions/V2R1C6.json").read_text(encoding="utf-8"))
assert metadata["version"] == "V2R1C6"
assert metadata["base_oid"] == "a33e2593967a180c89d44454350ccaeaf3f79b25" assert metadata["base_oid"] == "a33e2593967a180c89d44454350ccaeaf3f79b25"
assert metadata["candidate_branch"] == "agent/v2r1c6-mainline-reconciliation-20260806" assert metadata["candidate_branch"] == "agent/v2r1c6-mainline-reconciliation-20260806"
assert metadata["production_deployed"] is False assert metadata["production_deployed"] is False
@@ -7,17 +7,19 @@ import pytest
ROOT = Path(__file__).resolve().parents[2] ROOT = Path(__file__).resolve().parents[2]
sys.path.insert(0, str(ROOT / "active-runtime" / "app")) sys.path.insert(0, str(ROOT / "active-runtime" / "app"))
from deployment_state import RuntimeDeploymentState
from v2r1c6_privacy_tools import health_payload, serve # noqa: E402 from v2r1c6_privacy_tools import health_payload, serve # noqa: E402
def test_health_is_engineering_only() -> None: def test_health_is_engineering_only() -> None:
result = health_payload() result = health_payload(RuntimeDeploymentState("V2R1C7", "a" * 40, "b" * 64, "STAGED"))
assert result == { assert result["status"] == "ok"
"status": "ok", assert result["version"] == "V2R1C7"
"version": "V2R1C6", assert result["commit_oid"] == "a" * 40
"production_deployed": False, assert result["artifact_sha256"] == "b" * 64
"gitops_writes_enabled": False, assert result["deployment_state"] == "STAGED"
} assert result["production_deployed"] is False
assert result["health_timestamp"]
def test_listener_rejects_non_loopback() -> None: def test_listener_rejects_non_loopback() -> None:
+65
View File
@@ -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",
)
+118
View File
@@ -0,0 +1,118 @@
from __future__ import annotations
import hashlib
import json
import pytest
from aios_gitops_job_controller import GitOpsJobController, JobError, JobState, JobStore
HEAD = "a" * 40
BASE = "b" * 40
CURRENT = "c" * 40
def _controller(tmp_path):
store = JobStore(tmp_path / "jobs", payload_root=tmp_path / "payloads")
return GitOpsJobController(store), store
def _request():
payload = b"mission-recovery-test"
return {
"campaign_id": "AIOS-AUTONOMOUS-RELEASE-CAMPAIGN-20260807",
"repository": "aios-gitops",
"operation": "PROTECTED_MERGE",
"branch": "agent/recovery-test",
"expected_branch_oid": HEAD,
"expected_main_oid": BASE,
"payload_digest": hashlib.sha256(payload).hexdigest(),
"pr_number": 53,
"task_id": "AIOS_AUTONOMOUS_RELEASE_CAMPAIGN_V1",
}, payload
def _proof(proof_job_id: str, **overrides):
proof = {
"proof_schema": "aios.job-recovery.v1",
"job_id": proof_job_id,
"operation": "PROTECTED_MERGE",
"pr_number": 53,
"pr_merged": True,
"protected_merge": True,
"head_oid": HEAD,
"base_oid": BASE,
"main_oid_after_protected_merge": HEAD,
"gitea_main_oid_after_protected_merge": HEAD,
"authority_main_oid_after_protected_merge": HEAD,
"current_gitea_main_oid": CURRENT,
"current_authority_main_oid": CURRENT,
"current_main_descends_from_protected_merge": True,
"force_push": False,
"machine_actor": "aios-autonomy-agent",
}
proof.update(overrides)
return proof
def _manual_job(tmp_path):
controller, store = _controller(tmp_path)
request, payload = _request()
submitted = controller.submit(request, payload=payload)
manual = controller.advance(
submitted.job_id,
JobState.MANUAL_OWNER_REVIEW_REQUIRED,
side_effect_state="MANUAL_OWNER_REVIEW_REQUIRED",
receipt={"historical": "preserve-me"},
expected_revision=submitted.revision,
)
return controller, store, manual
def test_verified_external_recovery_preserves_history_and_is_idempotent(tmp_path):
controller, store, manual = _manual_job(tmp_path)
proof = _proof(manual.job_id)
completed = controller.complete_verified_external_side_effect(
manual.job_id, proof, expected_revision=manual.revision
)
assert completed.state is JobState.COMPLETED
assert completed.side_effect_state == "COMPLETED"
receipt = json.loads(store.read(manual.job_id)["receipt_json"])
assert receipt["recovered_from_historical_manual"] is True
assert receipt["historical_receipt"] == {"historical": "preserve-me"}
assert receipt["proof"] == proof
duplicate = controller.complete_verified_external_side_effect(manual.job_id, proof)
assert duplicate.state is JobState.COMPLETED
assert duplicate.revision == completed.revision
@pytest.mark.parametrize(
"field,value",
[
("job_id", "job-wrong"),
("head_oid", "d" * 40),
("main_oid_after_protected_merge", "d" * 40),
("force_push", True),
("operation", "PUSH"),
],
)
def test_verified_external_recovery_rejects_mismatched_proof(tmp_path, field, value):
controller, _store, manual = _manual_job(tmp_path)
proof_job_id = value if field == "job_id" else manual.job_id
overrides = {} if field == "job_id" else {field: value}
with pytest.raises(JobError):
controller.complete_verified_external_side_effect(
manual.job_id, _proof(proof_job_id, **overrides)
)
assert controller.store.read(manual.job_id)["state"] == JobState.MANUAL_OWNER_REVIEW_REQUIRED.value
def test_ordinary_terminal_advance_and_resume_remain_denied(tmp_path):
controller, _store, manual = _manual_job(tmp_path)
with pytest.raises(JobError, match="JOB_TERMINAL"):
controller.advance(manual.job_id, JobState.COMPLETED, side_effect_state="COMPLETED")
with pytest.raises(JobError, match="JOB_TERMINAL"):
controller.resume(manual.job_id)
@@ -0,0 +1,78 @@
import subprocess
from pathlib import Path
import pytest
from governance.ref_reconciler import RefReconciler, RefReconciliationError
from governance.semantic_protection import semantic_digest, semantic_policy, transport_digest
def protection(**overrides):
value = {
"branch_name": "main",
"enable_push": False,
"enable_force_push": False,
"enable_merge_whitelist": True,
"merge_whitelist_usernames": ["owner"],
"required_approvals": 0,
"enable_status_check": True,
"status_check_contexts": ["owner-approval-gate", "py-compile"],
"block_admin_merge_override": True,
"block_on_official_review_requests": True,
"block_on_outdated_branch": True,
"block_on_rejected_reviews": True,
"dismiss_stale_approvals": True,
"ignore_stale_approvals": False,
"enable_approvals_whitelist": True,
"approvals_whitelist_username": ["owner"],
"enable_bypass_allowlist": False,
"enable_force_push_allowlist": False,
"enable_push_whitelist": False,
"protected_file_patterns": "[]",
"unprotected_file_patterns": "[]",
}
value.update(overrides)
return value
def test_transport_metadata_does_not_change_semantic_digest():
before = protection(updated_at="old")
after = protection(updated_at="new")
assert transport_digest(before) != transport_digest(after)
assert semantic_digest(before) == semantic_digest(after)
def test_stage1_whitelist_is_a_semantic_change():
before = protection()
after = protection(merge_whitelist_usernames=["owner", "aios-autonomy-agent"])
assert semantic_policy(before)["merge_whitelist_user_ids"] == [2]
assert semantic_policy(after)["merge_whitelist_user_ids"] == [2, 153]
assert semantic_digest(before) != semantic_digest(after)
def test_ref_reconciler_rejects_gitea_mismatch(tmp_path: Path):
reconciler = RefReconciler(tmp_path / "missing.git")
with pytest.raises(RefReconciliationError, match="GITEA_MAIN_EXPECTATION_MISMATCH"):
reconciler.reconcile_main(expected_oid="a" * 40, merged_oid="b" * 40, gitea_main_oid="c" * 40)
def test_ref_reconciler_ff_cas_and_idempotency(tmp_path: Path):
repo = tmp_path / "repo.git"
subprocess.run(["git", "init", "--bare", str(repo)], check=True, capture_output=True)
worktree = tmp_path / "work"
subprocess.run(["git", "clone", str(repo), str(worktree)], check=True, capture_output=True)
subprocess.run(["git", "-C", str(worktree), "config", "user.email", "test@example.invalid"], check=True)
subprocess.run(["git", "-C", str(worktree), "config", "user.name", "test"], check=True)
(worktree / "one.txt").write_text("one\n", encoding="utf-8")
subprocess.run(["git", "-C", str(worktree), "add", "one.txt"], check=True)
subprocess.run(["git", "-C", str(worktree), "commit", "-m", "one"], check=True, capture_output=True)
old = subprocess.check_output(["git", "-C", str(worktree), "rev-parse", "HEAD"], text=True).strip()
subprocess.run(["git", "-C", str(worktree), "push", "origin", "HEAD:refs/heads/main"], check=True, capture_output=True)
(worktree / "two.txt").write_text("two\n", encoding="utf-8")
subprocess.run(["git", "-C", str(worktree), "add", "two.txt"], check=True)
subprocess.run(["git", "-C", str(worktree), "commit", "-m", "two"], check=True, capture_output=True)
new = subprocess.check_output(["git", "-C", str(worktree), "rev-parse", "HEAD"], text=True).strip()
subprocess.run(["git", "-C", str(worktree), "push", "origin", "HEAD:refs/heads/main"], check=True, capture_output=True)
reconciler = RefReconciler(repo)
assert reconciler.reconcile_main(expected_oid=old, merged_oid=new, gitea_main_oid=new) == new
assert reconciler.reconcile_main(expected_oid=old, merged_oid=new, gitea_main_oid=new) == new
+169
View File
@@ -0,0 +1,169 @@
from __future__ import annotations
import copy
import sys
from pathlib import Path
import pytest
ROOT = Path(__file__).resolve().parents[2]
sys.path.insert(0, str(ROOT / "active-runtime" / "app"))
from campaign_activation import CampaignActivationError, build_active_policy
from release_campaign_bootstrap import campaign_policy
import governance.stage2_gate_migration as stage2
ANCHOR = "69b45e81dcd2c3707c367003b08c55fd361faae8"
MAIN = "2a32e30c5cbd894cf007326917776893b3020231"
def protection(statuses=None):
return {
"branch_name": "main",
"enable_push": False,
"enable_force_push": False,
"enable_merge_whitelist": True,
"merge_whitelist_usernames": ["aios-autonomy-agent", "owner"],
"required_approvals": 0,
"enable_status_check": True,
"status_check_contexts": statuses or [
"autonomy-unit-tests", "owner-approval-gate", "py-compile"
],
"block_admin_merge_override": True,
"block_on_official_review_requests": True,
"block_on_outdated_branch": True,
"block_on_rejected_reviews": True,
"dismiss_stale_approvals": True,
"ignore_stale_approvals": False,
"enable_approvals_whitelist": True,
"approvals_whitelist_username": ["owner"],
"enable_bypass_allowlist": False,
"enable_force_push_allowlist": False,
"enable_push_whitelist": False,
"protected_file_patterns": "[]",
"unprotected_file_patterns": "[]",
"updated_at": "transport-only",
}
def facts(**overrides):
value = {
"campaign_id": stage2.CAMPAIGN_ID,
"campaign_active": True,
"campaign_revoked": False,
"machine_actor": "aios-autonomy-agent",
"required_checks_all_success": True,
"required_checks_same_head": True,
"unresolved_conversations": 0,
"mergeable": True,
"protected_target": "main",
"current_main_oid": MAIN,
"gitea_main_oid": MAIN,
"current_main_lineage_verified": True,
}
value.update(overrides)
return value
def test_stage2_replaces_only_gate_and_is_idempotent(monkeypatch):
before = protection()
monkeypatch.setattr(stage2, "STAGE1_SEMANTIC_SHA", stage2.semantic_digest(before))
status, target = stage2.build_target(before)
assert status == "READY"
assert "owner-approval-gate" not in target["status_check_contexts"]
assert "campaign-approval-gate" in target["status_check_contexts"]
for key in before:
if key != "status_check_contexts":
assert target[key] == before[key]
status2, target2 = stage2.build_target(target)
assert status2 == "IDEMPOTENT_SUCCESS"
assert target2 == target
@pytest.mark.parametrize(
"field,value",
[
("campaign_active", False),
("campaign_revoked", True),
("required_checks_all_success", False),
("required_checks_same_head", False),
("unresolved_conversations", 1),
("mergeable", False),
("current_main_oid", "f" * 40),
("machine_actor", "owner"),
],
)
def test_stage2_fails_closed_on_gate_fact_drift(field, value):
with pytest.raises(stage2.Stage2GateError):
stage2.validate_stage2_facts(facts(**{field: value}))
def test_stage2_canonicalizer_accepts_live_empty_pattern_fields():
before = protection()
before["protected_file_patterns"] = ""
before["unprotected_file_patterns"] = ""
monkeypatch = pytest.MonkeyPatch()
try:
monkeypatch.setattr(stage2, "STAGE1_SEMANTIC_SHA", stage2.semantic_digest(before))
status, target = stage2.build_target(before)
assert status == "READY"
assert target["protected_file_patterns"] == ""
assert target["unprotected_file_patterns"] == ""
finally:
monkeypatch.undo()
def test_stage2_fails_closed_on_semantic_drift(monkeypatch):
before = protection()
monkeypatch.setattr(stage2, "STAGE1_SEMANTIC_SHA", "f" * 64)
with pytest.raises(stage2.Stage2GateError, match="SEMANTIC_PRECONDITION_INVALID"):
stage2.build_target(before)
def activation_facts(**overrides):
value = {
"campaign_id": stage2.CAMPAIGN_ID,
"repository": "aios/aios",
"source_main_oid": MAIN,
"gitea_main_oid": MAIN,
"authority_main_oid": MAIN,
"runtime_source_oid": MAIN,
"durable_job_state": "COMPLETED",
"writer_ready": True,
"gitea_machine_authenticated": True,
"governance_executor_ready": True,
"preapproval_publisher_ready": True,
"runtime_exact_main": True,
"main_lineage_verified": True,
"campaign_revoked": False,
}
value.update(overrides)
return value
def test_campaign_activation_changes_only_active_state():
pending = campaign_policy(stage2.CAMPAIGN_ID, ANCHOR)
active = build_active_policy(pending, activation_facts())
assert active["activation_state"] == "ACTIVE"
assert active["active"] is True
for key in pending:
if key not in {"activation_state", "active"}:
assert active[key] == pending[key]
@pytest.mark.parametrize(
"field,value",
[
("source_main_oid", "e" * 40),
("gitea_main_oid", "e" * 40),
("durable_job_state", "MANUAL_OWNER_REVIEW_REQUIRED"),
("writer_ready", False),
("governance_executor_ready", False),
("campaign_revoked", True),
],
)
def test_campaign_activation_fails_closed(field, value):
pending = campaign_policy(stage2.CAMPAIGN_ID, ANCHOR)
with pytest.raises(CampaignActivationError):
build_active_policy(pending, activation_facts(**{field: value}))
+217
View File
@@ -0,0 +1,217 @@
from __future__ import annotations
import inspect
import json
import sys
from pathlib import Path
import pytest
import yaml
ROOT = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(ROOT))
sys.path.insert(0, str(ROOT / "runtime/agent-os"))
from aios_chatgpt_bridge_v2 import ( # noqa: E402
OPERATION_REQUIRED_FIELDS,
operation_contracts,
render_openapi_contract_fragment,
)
from aios_chatgpt_bridge_v2_server import GovernedRuntime # noqa: E402
from aios_gitops_job_controller import JobState # noqa: E402
from gitea_tag_publication import validate_gitea_api_url # noqa: E402
from silent_release_hardening import ReleaseSagaStore, SilentReleaseActionService # noqa: E402
from release_version import validate_release_version # noqa: E402
import aios_model_api_staging as model_api # noqa: E402
def test_canonical_release_version_is_shared_and_accepts_v2r2c1():
assert validate_release_version("V2R2C1") == "V2R2C1"
assert validate_release_version("V2R2C1-repair") == "V2R2C1-repair"
with pytest.raises(ValueError):
validate_release_version("v2r2c1")
def test_gitea_url_validation_is_startup_safe():
assert validate_gitea_api_url("https://g.cnnetmkt.top/git/api/v1") == (
"https://g.cnnetmkt.top/git/api/v1"
)
with pytest.raises(ValueError, match="HTTP"):
validate_gitea_api_url("g.cnnetmkt.top/git/api/v1")
def test_public_release_reads_use_the_bridge_canonical_evaluator():
query = {
"version": ["V2R2C1"],
"target_oid": ["998d30304787e31e5ae607c0bd86b5edb2b0e5ad"],
"evidence_closure_oid": ["a8b906d1bc24088c19647e98de6a679268f6d0a2"],
"approval_plan_id": ["sha256:plan"],
}
assert model_api.governed_release_query_payload(query) == {
"version": "V2R2C1",
"target_oid": "998d30304787e31e5ae607c0bd86b5edb2b0e5ad",
"evidence_closure_oid": "a8b906d1bc24088c19647e98de6a679268f6d0a2",
"approval_plan_id": "sha256:plan",
}
source = inspect.getsource(model_api.H.do_GET)
assert "self.bridge_release(" in source
assert "'/getReleaseConsistency'" in source
assert "'/runReleaseAcceptance'" in source
def test_governed_read_routes_are_bound_to_authenticated_facade():
route_script = (
ROOT / "ops/gitops-migration/bind_external_action_surface_final_staging.sh"
).read_text()
for path in (
"/api/governed-write-capabilities",
"/api/control-plane-snapshot",
"/api/online-agent-task",
"/api/online-agent-task-events",
):
assert f"location = {path}" in route_script
assert f"proxy_pass http://127.0.0.1:18890{path};" in route_script
assert "location /api/ {" in route_script
assert "Authorization $http_authorization" in route_script
def test_published_action_v2_paths_are_bound_to_staging_bridge():
route_script = (
ROOT / "ops/gitops-migration/bind_external_action_surface_final_staging.sh"
).read_text()
schema = (ROOT / "docs/gitops/openapi-chatgpt-actions.yaml").read_text()
assert "servers:" in schema
assert "- url: https://cnnetmkt.top" in schema
assert " /v2/commit-files:" in schema
assert "location ^~ /v2/" in route_script
assert "proxy_pass http://127.0.0.1:18892;" in route_script
assert "proxy_pass http://127.0.0.1:18920;" not in route_script
assert "Authorization $http_authorization" in route_script
def test_published_action_schema_stays_within_chatgpt_operation_limit():
schema = yaml.safe_load(
(ROOT / "docs/gitops/openapi-chatgpt-actions.yaml").read_text()
)
operations = [
(path, method, operation)
for path, item in schema["paths"].items()
for method, operation in item.items()
if method.lower() in {"get", "post", "put", "patch", "delete", "head", "options"}
]
assert len(operations) <= 30
assert len(operations) == 28
assert "commitFilesGoverned" in {
operation["operationId"] for _, _, operation in operations
}
assert {"readRepositoryFile", "acquireWriterLease"}.issubset(
{operation["operationId"] for _, _, operation in operations}
)
assert "/api/file" not in schema["paths"]
assert "/api/commit" not in schema["paths"]
operation_ids = {operation["operationId"] for _, _, operation in operations}
assert {
"submitSignedApproval",
"createPatch",
"createBranch",
"commitFileAtomic",
"commitFile",
"commitChanges",
}.isdisjoint(operation_ids)
governed = next(
operation for _, _, operation in operations
if operation["operationId"] == "commitFilesGoverned"
)
assert governed["security"] == [{"bearerAuth": []}]
assert governed["x-openai-isConsequential"] is True
def test_commit_files_runtime_and_published_schema_have_same_required_fields():
runtime = operation_contracts()["commitFilesGoverned"]["required_fields"]
assert runtime == list(OPERATION_REQUIRED_FIELDS["commitFilesGoverned"])
fragment = render_openapi_contract_fragment()
published = (ROOT / "docs/gitops/openapi-chatgpt-actions.yaml").read_text()
assert fragment in published
for field in runtime:
assert f" - {field}" in fragment
assert "required: [path, content_base64]" in fragment
class _Store:
def __init__(self, row):
self.row = row
self.receipt_calls = 0
def read(self, _job_id):
return self.row
def _receipt(self, row, duplicate=False):
self.receipt_calls += 1
return type("Receipt", (), {"as_dict": lambda _self: {
"job_id": row["job_id"],
"state": row["state"],
"duplicate": duplicate,
}})()
class _Controller:
def __init__(self, row):
self.store = _Store(row)
def _runtime_for(row):
runtime = object.__new__(GovernedRuntime)
controller = _Controller(row)
runtime._controller_for_job = lambda _job_id: controller
runtime._executor_from_row = lambda *_args: (_ for _ in ()).throw(
AssertionError("terminal reconcile must not reconstruct an executor")
)
return runtime, controller
@pytest.mark.parametrize(
"state",
[
JobState.COMPLETED.value,
JobState.FAILED.value,
JobState.CANCELLED.value,
JobState.MANUAL_OWNER_REVIEW_REQUIRED.value,
],
)
def test_terminal_reconcile_is_idempotent_without_durable_binding(state):
row = {
"job_id": "job-terminal",
"state": state,
"request_json": json.dumps({}),
}
runtime, controller = _runtime_for(row)
result = GovernedRuntime.job(runtime, {"job_id": row["job_id"]}, "reconcile")
assert result["duplicate"] is True
assert controller.store.receipt_calls == 1
def test_active_reconcile_still_uses_executor():
row = {
"job_id": "job-active",
"state": JobState.RECONCILIATION_REQUIRED.value,
"request_json": json.dumps({}),
}
runtime, controller = _runtime_for(row)
class Executor:
def reconcile(self, job_id):
return {"job_id": job_id, "state": "COMPLETED"}
runtime._executor_from_row = lambda *_args: Executor()
assert GovernedRuntime.job(runtime, {"job_id": row["job_id"]}, "reconcile") == {
"job_id": "job-active",
"state": "COMPLETED",
}
def test_missing_release_saga_is_a_not_found_empty_state(tmp_path):
service = object.__new__(SilentReleaseActionService)
service.sagas = ReleaseSagaStore(tmp_path)
service.execution_enabled = False
result = service.get_release_saga("never-created-action-contract")
assert result["status"] == "NOT_FOUND"
assert result["state"] == "NOT_FOUND"
assert not list(tmp_path.rglob("*"))
+245
View File
@@ -0,0 +1,245 @@
from __future__ import annotations
import json
import sys
from pathlib import Path
import pytest
ROOT = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(ROOT / "active-runtime" / "app"))
from approved_release_executor import (
Binding,
DurableState,
ManualOwnerReviewRequired,
ReconciliationRequired,
deploy_step,
execute,
tag_step,
verify_authorization,
)
TARGET = "c7adece1cc8beffe7c718d53c775898b45d6c7f8"
EVIDENCE = "8c2898baf07195b9861ed2ff74f03a71674ec9e9"
ARTIFACT = "ce09a46f28b453634784abceda4597fd51c3aa0838e72a5ad5fefd03d58b949e"
PLAN = "sha256:c8199d638c2bb2e4896ca9b219ca52db79713bee007df05653ab348905d6cf28"
TAG_OID = "1" * 40
def binding(tmp_path: Path) -> Binding:
artifact = tmp_path / "release.tar.zst"
artifact.write_bytes(b"x")
return Binding(
version="VTEST",
target_oid=TARGET,
evidence_closure_oid=EVIDENCE,
artifact_sha256=ARTIFACT,
approval_plan_id=PLAN,
artifact_path=str(artifact),
artifact_size=1,
production_service="test.service",
production_port=18927,
)
def authorization(value: Binding) -> dict:
return {
"accepted": True,
"binding_replaced": False,
"version": value.version,
"target_oid": value.target_oid,
"evidence_closure_oid": value.evidence_closure_oid,
"artifact_sha256": value.artifact_sha256,
"approval_plan_id": value.approval_plan_id,
}
def test_owner_authorization_is_exact(tmp_path):
value = binding(tmp_path)
assert verify_authorization(authorization(value), value)["accepted"] is True
wrong = authorization(value)
wrong["artifact_sha256"] = "0" * 64
with pytest.raises(ManualOwnerReviewRequired):
verify_authorization(wrong, value)
def test_durable_state_rejects_different_binding(tmp_path):
first = binding(tmp_path)
DurableState(tmp_path / "state", first)
second = Binding(
**{
**first.__dict__,
"approval_plan_id": "sha256:" + "2" * 64,
}
)
with pytest.raises(ManualOwnerReviewRequired):
DurableState(tmp_path / "state", second)
def test_tag_conflict_never_applies(monkeypatch, tmp_path):
value = binding(tmp_path)
calls = []
def fake(command, request):
calls.append(request["operation"])
return {
"status": "CONFLICT",
"version": value.version,
"target_oid": value.target_oid,
"artifact_sha256": value.artifact_sha256,
}
monkeypatch.setattr("approved_release_executor.run_adapter", fake)
with pytest.raises(ManualOwnerReviewRequired):
tag_step(["/tag"], value)
assert calls == ["probe"]
def test_runtime_failure_attempts_rollback_after_apply(monkeypatch, tmp_path):
value = binding(tmp_path)
calls = []
probes = 0
def fake(command, request):
nonlocal probes
calls.append(request["operation"])
if request["operation"] == "probe":
probes += 1
if probes == 1:
return {
"status": "NOT_APPLIED",
"version": value.version,
"target_oid": value.target_oid,
"artifact_sha256": value.artifact_sha256,
}
return {
"status": "NOT_APPLIED",
"version": value.version,
"target_oid": value.target_oid,
"artifact_sha256": value.artifact_sha256,
}
if request["operation"] == "rollback":
return {
"status": "ROLLED_BACK",
"version": value.version,
"target_oid": value.target_oid,
"artifact_sha256": value.artifact_sha256,
}
return {
"status": "APPLY_STARTED",
"version": value.version,
"target_oid": value.target_oid,
"artifact_sha256": value.artifact_sha256,
}
monkeypatch.setattr("approved_release_executor.run_adapter", fake)
with pytest.raises(ReconciliationRequired):
deploy_step(["/deploy"], value)
assert calls == ["probe", "apply", "probe", "rollback"]
def test_execute_orders_registry_before_baseline_and_baseline_last(
monkeypatch, tmp_path
):
value = binding(tmp_path)
auth_path = tmp_path / "authorization.json"
auth_path.write_text(json.dumps(authorization(value)))
auth_path.chmod(0o600)
monkeypatch.setattr(
"approved_release_executor.secure_json",
lambda path: json.loads(path.read_text(encoding="utf-8")),
)
monkeypatch.setattr(
"approved_release_executor.verify_git_lineage",
lambda repo, binding: {"verified": True},
)
monkeypatch.setattr(
"approved_release_executor.verify_artifact",
lambda binding: {"verified": True},
)
calls = []
tag_applied = False
runtime_applied = False
registry_applied = False
baseline_applied = False
def fake(command, request):
nonlocal tag_applied, runtime_applied, registry_applied, baseline_applied
name = command[0]
operation = request["operation"]
calls.append((name, operation))
common = {
"version": value.version,
"target_oid": value.target_oid,
"artifact_sha256": value.artifact_sha256,
}
if name == "/tag":
if operation == "apply":
tag_applied = True
if tag_applied:
return {
**common,
"status": "APPLIED_VERIFIED",
"tag_oid": TAG_OID,
"peeled_target_oid": value.target_oid,
}
return {**common, "status": "NOT_APPLIED"}
if name == "/deploy":
if operation == "apply":
runtime_applied = True
if operation == "rollback":
return {**common, "status": "ROLLED_BACK"}
if runtime_applied:
return {
**common,
"status": "ACTIVE_VERIFIED",
"deployment_state": "ACTIVE",
"production_deployed": True,
}
return {**common, "status": "NOT_APPLIED"}
if name == "/registry":
if operation == "apply":
registry_applied = True
return {
**common,
"status": "VERIFIED" if registry_applied else "NOT_APPLIED",
"tag_oid": TAG_OID if registry_applied else None,
}
if name == "/baseline":
if operation == "apply":
assert registry_applied is True
assert runtime_applied is True
baseline_applied = True
return {
**common,
"status": "VERIFIED" if baseline_applied else "NOT_APPLIED",
"tag_oid": TAG_OID if baseline_applied else None,
}
raise AssertionError(name)
monkeypatch.setattr("approved_release_executor.run_adapter", fake)
result = execute(
binding=value,
authorization_path=auth_path,
repository=tmp_path / "repo.git",
state_root=tmp_path / "state",
tag_command=["/tag"],
deploy_command=["/deploy"],
registry_command=["/registry"],
baseline_command=["/baseline"],
)
assert result["status"] == "PRODUCTION_ACCEPTED"
assert calls.index(("/registry", "apply")) < calls.index(("/baseline", "apply"))
assert calls.index(("/deploy", "apply")) < calls.index(("/registry", "apply"))
state = json.loads((tmp_path / "state" / "execution.json").read_text())
assert state["state"] == "COMPLETED"
+95
View File
@@ -0,0 +1,95 @@
from types import SimpleNamespace
import json
from pathlib import Path
import sys
import tempfile
import pytest
ROOT = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(ROOT / "active-runtime" / "app"))
import release_campaign_authorization as a
import release_campaign_bootstrap as b
import campaign_release_acceptance as c
import release_campaign_orchestrator as o
ANCHOR = "69b45e81dcd2c3707c367003b08c55fd361faae8"
def policy(state="ACTIVE", active=True):
value = b.campaign_policy("AIOS-AUTONOMOUS-RELEASE-V1", ANCHOR)
value["activation_state"] = state
value["active"] = active
return value
def test_campaign_policy_exact():
assert a.validate_policy(policy())["normal_owner_interaction_required"] is False
def test_denied_operation_fails():
obj = a.CampaignAuthorization(Path("/tmp/x"), policy(), "0"*64)
with pytest.raises(a.CampaignError):
obj.assert_operation("force_push")
def test_release_acceptance_has_no_second_owner_interaction():
obj = a.CampaignAuthorization(Path("/tmp/x"), policy(), "0"*64)
binding = {
"version": "V2R1C8",
"target_oid": "a"*40,
"evidence_closure_oid": "b"*40,
"artifact_sha256": "c"*64,
"artifact_size": 123,
"approval_plan_id": "sha256:" + "d"*64,
}
receipt = c.evaluate(
obj,
binding,
{"object_type": "commit", "target_is_ancestor": True, "closure_oid": "b"*40},
{"sha256": "c"*64, "size": 123, "retained": True},
{"install": "PASS", "health": "PASS", "restart": "PASS", "rollback_readiness": "PASS"},
{"repository": "aios/aios", "target_in_main_lineage": True},
{"conflict": False},
)
assert receipt["accepted"] is True
assert receipt["owner_interaction_required"] is False
def test_changed_binding_rejected():
obj = a.CampaignAuthorization(Path("/tmp/x"), policy(), "0"*64)
with pytest.raises(c.AcceptanceError):
c.evaluate(
obj,
{
"version": "V2R1C8",
"target_oid": "a"*40,
"evidence_closure_oid": "b"*40,
"artifact_sha256": "c"*64,
"artifact_size": 123,
"approval_plan_id": "sha256:" + "d"*64,
},
{"object_type": "commit", "target_is_ancestor": True, "closure_oid": "b"*40},
{"sha256": "e"*64, "size": 123, "retained": True},
{"install": "PASS", "health": "PASS", "restart": "PASS", "rollback_readiness": "PASS"},
{"repository": "aios/aios", "target_in_main_lineage": True},
{"conflict": False},
)
def test_orchestrator_recoverable_failure_resumes_without_owner():
state = o.ReleaseState("V2R1C8", state="STAGING", last_stable_state="STAGING")
state.recoverable_failure("transient_502")
assert state.as_dict()["owner_interaction_required"] is False
state.resume()
assert state.state == "STAGING"
def test_only_red_line_can_enter_manual_review():
state = o.ReleaseState("V2R1C8")
with pytest.raises(o.OrchestratorError):
state.manual_review("pytest_failed")
state.manual_review("formal_same_name_tag_conflict")
assert state.as_dict()["owner_interaction_required"] is True
@@ -0,0 +1,108 @@
import base64
import subprocess
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_authority_create_task_branch_is_agent_scoped_and_idempotent(tmp_path):
bare = tmp_path / "repo.git"
subprocess.run(["git", "init", "--bare", str(bare)], check=True, capture_output=True)
source = tmp_path / "source"
subprocess.run(["git", "init", str(source)], check=True, capture_output=True)
(source / "README.md").write_text("fixture\n", encoding="utf-8")
subprocess.run(["git", "-C", str(source), "add", "README.md"], check=True)
subprocess.run(["git", "-C", str(source), "-c", "user.name=test", "-c", "user.email=test@example.invalid", "commit", "-m", "fixture"], check=True, capture_output=True)
base = subprocess.check_output(["git", "-C", str(source), "rev-parse", "HEAD"], text=True).strip()
executor = AuthorityGitExecutor(bare, tmp_path / "worktrees")
subprocess.run(["git", "-C", str(source), "push", str(bare), "HEAD:refs/heads/main"], check=True, capture_output=True)
assert executor.create_task_branch("refs/heads/agent/contract-test", base) == base
assert executor.create_task_branch("refs/heads/agent/contract-test", base) == base
assert executor.read_ref("refs/heads/agent/contract-test") == base
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/source/file",
"/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

Some files were not shown because too many files have changed in this diff Show More