Compare commits
20
Commits
Regular → Executable
+22
-11
@@ -58,11 +58,6 @@ class AuthorityGitExecutor:
|
||||
value = self._git(["rev-parse", "--verify", ref], check=False)
|
||||
return value if value and OID_RE.fullmatch(value) else None
|
||||
|
||||
def commit_exists(self, oid: str) -> bool:
|
||||
_oid(oid, "commit_oid")
|
||||
result = subprocess.run(["git", f"--git-dir={self.git_dir}", "cat-file", "-e", oid + "^{commit}"], capture_output=True, check=False)
|
||||
return result.returncode == 0
|
||||
|
||||
def ancestry_check(self, older_oid: str, newer_oid: str) -> bool:
|
||||
_oid(older_oid, "older_oid")
|
||||
_oid(newer_oid, "newer_oid")
|
||||
@@ -75,12 +70,28 @@ class AuthorityGitExecutor:
|
||||
raise AuthorityGitError("ANCESTRY_CHECK_FAILED")
|
||||
return result.returncode == 0
|
||||
|
||||
def create_detached_worktree(
|
||||
self,
|
||||
base_oid: str,
|
||||
worktree_name: str,
|
||||
root: Path | None = None,
|
||||
) -> Path:
|
||||
def commit_exists(self, oid: str) -> bool:
|
||||
_oid(oid, "commit_oid")
|
||||
result = subprocess.run(["git", f"--git-dir={self.git_dir}", "cat-file", "-e", f"{oid}^{{commit}}"], capture_output=True, check=False)
|
||||
return result.returncode == 0
|
||||
|
||||
def create_task_branch_CAS(self, branch: str, expected_absent: bool, new_oid: str) -> str:
|
||||
if not expected_absent or not TASK_BRANCH_RE.fullmatch(branch):
|
||||
raise AuthorityGitError("TASK_BRANCH_CREATE_SCOPE_DENIED")
|
||||
_oid(new_oid, "new_oid")
|
||||
if self.read_ref(branch) is not None:
|
||||
raise AuthorityGitError("AUTHORITY_BRANCH_ALREADY_EXISTS")
|
||||
try:
|
||||
self._git(["update-ref", branch, new_oid, "0" * 40])
|
||||
except AuthorityGitError:
|
||||
if self.read_ref(branch) is not None:
|
||||
raise AuthorityGitError("AUTHORITY_BRANCH_ALREADY_EXISTS")
|
||||
raise
|
||||
if self.read_ref(branch) != new_oid:
|
||||
raise AuthorityGitError("AUTHORITY_CAS_READBACK_MISMATCH")
|
||||
return new_oid
|
||||
|
||||
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")
|
||||
|
||||
Regular → Executable
@@ -25,7 +25,6 @@ BRIDGE_OPERATIONS = (
|
||||
"executeApprovedProductionRelease",
|
||||
"getReleaseConsistency",
|
||||
"runReleaseAcceptance",
|
||||
"getControlPlaneSnapshot",
|
||||
)
|
||||
|
||||
OPERATION_REQUIRED_FIELDS = {
|
||||
@@ -43,7 +42,6 @@ OPERATION_REQUIRED_FIELDS = {
|
||||
"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": (),
|
||||
}
|
||||
|
||||
|
||||
@@ -163,5 +161,3 @@ class BridgeV2Adapter:
|
||||
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 {})
|
||||
|
||||
@@ -1,22 +1,29 @@
|
||||
"""Loopback-only governed Bridge v2 HTTP gateway with durable handlers."""
|
||||
"""Loopback-only real HTTP gateway for the governed Bridge v2 adapter."""
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from datetime import datetime, timezone, timedelta
|
||||
import base64
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
||||
from pathlib import Path
|
||||
import re
|
||||
import sqlite3
|
||||
from typing import Any, Mapping
|
||||
|
||||
from aios_authority_git_executor import AuthorityGitExecutor
|
||||
from aios_chatgpt_bridge_v2 import BridgeV2Adapter, BridgeV2Error, OPERATION_REQUIRED_FIELDS
|
||||
from aios_continuation_executor import AuthorityGitAdapter, ContinuationCommit, ContinuationExecutor, ContinuationPermissions, ContinuationRuntimeBinding, DetachedGitWorkspace, GiteaContinuationRemote, ServerSideCredentialProjection
|
||||
from aios_continuation_executor import (
|
||||
AuthorityGitAdapter,
|
||||
ContinuationCommit,
|
||||
ContinuationExecutor,
|
||||
ContinuationPermissions,
|
||||
ContinuationRuntimeBinding,
|
||||
DetachedGitWorkspace,
|
||||
GiteaContinuationRemote,
|
||||
ServerSideCredentialProjection,
|
||||
)
|
||||
from aios_continuation_lease import WriterLeaseRequestV2
|
||||
from aios_control_plane_snapshot import build_control_plane_snapshot
|
||||
from aios_gitops_job_controller import GitOpsJobController, JobStore
|
||||
from aios_governed_task_bootstrap import BootstrapError, GovernedTaskBootstrap
|
||||
from governed_production_release import build_executor_config
|
||||
@@ -61,6 +68,13 @@ class RuntimeConfig:
|
||||
health_url: str = "http://127.0.0.1:18927/health"
|
||||
|
||||
|
||||
@dataclass
|
||||
class _Runtime:
|
||||
binding: ContinuationRuntimeBinding
|
||||
controller: GitOpsJobController
|
||||
executor: ContinuationExecutor
|
||||
|
||||
|
||||
def _safe(value: str) -> str:
|
||||
return re.sub(r"[^A-Za-z0-9_.-]+", "-", value).strip("-")[:120] or "runtime"
|
||||
|
||||
@@ -70,88 +84,160 @@ class GovernedRuntime:
|
||||
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.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], _Runtime] = {}
|
||||
self.controllers: dict[str, GitOpsJobController] = {}
|
||||
self.bindings: dict[str, dict[str, Any]] = {}
|
||||
self.release = build_executor_config(config)
|
||||
metadata_root = config.state_root / "task-bindings"
|
||||
if metadata_root.is_dir():
|
||||
for metadata in metadata_root.glob("*.json"):
|
||||
try:
|
||||
value = json.loads(metadata.read_text(encoding="utf-8"))
|
||||
if isinstance(value, dict) and value.get("task_id"):
|
||||
self.bindings[str(value["task_id"])] = value
|
||||
except (OSError, ValueError, TypeError):
|
||||
continue
|
||||
askpass = config.state_root / "bridge-v2-askpass.sh"
|
||||
if not askpass.exists():
|
||||
askpass.write_text('#!/bin/sh\ncase "$1" in *Username*) printf "%s\\n" "$GITEA_MACHINE_ACTOR" ;; *) cat "$CREDENTIAL_FILE" ;; esac\n', encoding="utf-8")
|
||||
askpass.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,
|
||||
}
|
||||
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 _job_root(self, task_id: str) -> Path:
|
||||
if task_id == self.config.v2_task_id:
|
||||
return self.config.v2_job_root
|
||||
return self.config.state_root / "jobs" / _safe(task_id)
|
||||
|
||||
def _runtime(self, request: Mapping[str, Any]) -> tuple[Any, GitOpsJobController, ContinuationExecutor]:
|
||||
def _runtime(self, request: Mapping[str, Any], *, create_new: bool = False) -> _Runtime:
|
||||
key = (str(request["task_id"]), int(request["generation"]))
|
||||
if key in self.runtimes: return self.runtimes[key]
|
||||
lease = WriterLeaseRequestV2(task_id=str(request["task_id"]), repository=str(request["repository"]), branch_mode=str(request.get("branch_mode", "CONTINUE_EXISTING")), base_oid=str(request["base_oid"]), expected_branch_oid=str(request["expected_branch_oid"]), target_branch=str(request["target_branch"]), generation=int(request["generation"]), lease_seconds=int(request.get("lease_seconds", 900)))
|
||||
lease.validate(AuthorityGitAdapter(self.authority))
|
||||
worktree_path = self.config.state_root / "worktrees" / _safe(lease.task_id)
|
||||
if worktree_path.exists():
|
||||
workspace = DetachedGitWorkspace(worktree_path)
|
||||
if key in self.runtimes:
|
||||
return self.runtimes[key]
|
||||
if create_new:
|
||||
binding, controller, executor = self.bootstrap.prepare_runtime(
|
||||
request, lease_seconds=int(request.get("lease_seconds", 900))
|
||||
)
|
||||
else:
|
||||
self.authority.create_detached_worktree(lease.expected_branch_oid, _safe(lease.task_id), self.config.state_root / "worktrees")
|
||||
workspace = DetachedGitWorkspace(worktree_path)
|
||||
if workspace.head_oid() != lease.expected_branch_oid or not workspace.is_detached(): raise RuntimeError("WORKTREE_HEAD_MISMATCH")
|
||||
job_root = self._job_root(lease.task_id); controller = GitOpsJobController(JobStore(job_root, payload_root=job_root / "payloads")); credential = ServerSideCredentialProjection(self.config.ssh_key_path, self.config.token_path); remote = GiteaContinuationRemote(workspace=worktree_path, credential=credential, remote_name=self.config.remote_name, repository=lease.repository, api_base=self.config.api_base); executor = ContinuationExecutor(controller, AuthorityGitAdapter(self.authority), workspace, remote, credential)
|
||||
binding = ContinuationRuntimeBinding(request=lease, workspace_path=worktree_path, lease_not_before=datetime.now(timezone.utc) - timedelta(seconds=1), lease_expires_at=datetime.now(timezone.utc) + timedelta(seconds=lease.lease_seconds), original_receipt_valid=True, original_grant_consumed=True, permissions=ContinuationPermissions())
|
||||
result = (binding, controller, executor); self.runtimes[key] = result; self.controllers[lease.task_id] = controller; return result
|
||||
request_v2 = WriterLeaseRequestV2(
|
||||
task_id=str(request["task_id"]),
|
||||
repository=str(request["repository"]),
|
||||
branch_mode=str(request["branch_mode"]),
|
||||
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)),
|
||||
)
|
||||
request_v2.validate(AuthorityGitAdapter(self.authority))
|
||||
workspace_path = self.config.state_root / "worktrees" / _safe(request_v2.task_id)
|
||||
if workspace_path.exists():
|
||||
workspace = DetachedGitWorkspace(workspace_path)
|
||||
if workspace.head_oid() != request_v2.expected_branch_oid or not workspace.is_detached():
|
||||
raise RuntimeError("WORKTREE_HEAD_MISMATCH")
|
||||
else:
|
||||
self.authority.create_detached_worktree(
|
||||
request_v2.expected_branch_oid,
|
||||
_safe(request_v2.task_id),
|
||||
self.config.state_root / "worktrees",
|
||||
)
|
||||
workspace = DetachedGitWorkspace(workspace_path)
|
||||
job_root = self._job_root(request_v2.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=workspace_path,
|
||||
credential=credential,
|
||||
remote_name=self.config.remote_name,
|
||||
repository=request_v2.repository,
|
||||
api_base=self.config.api_base,
|
||||
)
|
||||
executor = ContinuationExecutor(
|
||||
controller,
|
||||
AuthorityGitAdapter(self.authority),
|
||||
workspace,
|
||||
remote,
|
||||
credential,
|
||||
)
|
||||
binding = ContinuationRuntimeBinding(
|
||||
request=request_v2,
|
||||
workspace_path=workspace_path,
|
||||
lease_not_before=datetime.now(timezone.utc) - timedelta(seconds=1),
|
||||
lease_expires_at=datetime.now(timezone.utc) + timedelta(seconds=request_v2.lease_seconds),
|
||||
original_receipt_valid=True,
|
||||
original_grant_consumed=True,
|
||||
permissions=ContinuationPermissions(),
|
||||
)
|
||||
runtime = _Runtime(binding, controller, executor)
|
||||
self.runtimes[key] = runtime
|
||||
self.controllers[str(request["task_id"])] = controller
|
||||
return runtime
|
||||
|
||||
def create_task(self, request: Mapping[str, Any]) -> dict[str, Any]:
|
||||
result = self.bootstrap.create(request); self.bindings[result["task_id"]] = result; return result
|
||||
return self.bootstrap.create(request)
|
||||
|
||||
def acquire_lease(self, request: Mapping[str, Any]) -> dict[str, Any]:
|
||||
binding, _, _ = self._runtime(request)
|
||||
req = binding.request
|
||||
return {"schema": WriterLeaseRequestV2.schema, "task_id": req.task_id, "repository": req.repository, "branch_mode": req.branch_mode, "base_oid": req.base_oid, "expected_branch_oid": req.expected_branch_oid, "target_branch": req.target_branch, "generation": req.generation, "lease_seconds": req.lease_seconds, "create_target_branch": req.branch_mode == "CREATE_NEW", "worktree_mode": "detached_at_base_oid" if req.branch_mode == "CREATE_NEW" else "detached_at_expected_head", "cas_ref_update_required": True}
|
||||
create_new = str(request.get("branch_mode")) == "CREATE_NEW"
|
||||
runtime = self._runtime(request, create_new=create_new)
|
||||
request_v2 = runtime.binding.request
|
||||
runtime.executor.observe_generation(request_v2.target_branch, max(0, request_v2.generation - 1))
|
||||
return {
|
||||
"schema": WriterLeaseRequestV2.schema,
|
||||
"task_id": request_v2.task_id,
|
||||
"repository": request_v2.repository,
|
||||
"branch_mode": request_v2.branch_mode,
|
||||
"base_oid": request_v2.base_oid,
|
||||
"expected_branch_oid": request_v2.expected_branch_oid,
|
||||
"target_branch": request_v2.target_branch,
|
||||
"generation": request_v2.generation,
|
||||
"lease_seconds": request_v2.lease_seconds,
|
||||
"create_target_branch": create_new,
|
||||
"worktree_mode": "detached_at_base_oid" if create_new else "detached_at_expected_head",
|
||||
"cas_ref_update_required": True,
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def _files(request: Mapping[str, Any]) -> dict[str, bytes]:
|
||||
raw = request.get("files") or {request["path"]: request["content_base64"]}; pairs = raw.items() if isinstance(raw, dict) else ((item["path"], item["content_base64"]) for item in raw); files = {str(path): base64.b64decode(encoded, validate=True) for path, encoded in pairs}
|
||||
if not files: raise ValueError("FILES_EMPTY")
|
||||
raw = request["files"] if "files" in request else {request["path"]: request["content_base64"]}
|
||||
if isinstance(raw, dict):
|
||||
pairs = raw.items()
|
||||
elif isinstance(raw, list):
|
||||
pairs = ((item["path"], item["content_base64"]) for item in raw if isinstance(item, dict))
|
||||
else:
|
||||
raise ValueError("FILES_INVALID")
|
||||
files: dict[str, bytes] = {}
|
||||
for path, encoded in pairs:
|
||||
if not isinstance(path, str) or not isinstance(encoded, str):
|
||||
raise ValueError("FILES_INVALID")
|
||||
files[path] = base64.b64decode(encoded, validate=True)
|
||||
if not files:
|
||||
raise ValueError("FILES_EMPTY")
|
||||
return files
|
||||
|
||||
def commit_files(self, request: Mapping[str, Any]) -> dict[str, Any]:
|
||||
runtime = self.runtimes.get((str(request["task_id"]), int(request["generation"])))
|
||||
if runtime is None: raise RuntimeError("LEASE_NOT_FOUND")
|
||||
binding, _, executor = runtime
|
||||
if request["target_branch"] != binding.request.target_branch or request["expected_branch_oid"] != binding.request.expected_branch_oid: raise RuntimeError("LEASE_BINDING_CONFLICT")
|
||||
commit = ContinuationCommit(idempotency_key=str(request["idempotency_key"]), files=self._files(request), message=str(request["message"]))
|
||||
executor.workspace.materialize_files(commit.files)
|
||||
return executor.execute(binding, commit)
|
||||
key = (str(request["task_id"]), int(request["generation"]))
|
||||
runtime = self.runtimes.get(key)
|
||||
if runtime is None:
|
||||
raise RuntimeError("LEASE_NOT_FOUND")
|
||||
request_v2 = runtime.binding.request
|
||||
if (
|
||||
str(request["target_branch"]) != request_v2.target_branch
|
||||
or str(request["expected_branch_oid"]) != request_v2.expected_branch_oid
|
||||
):
|
||||
raise RuntimeError("LEASE_BINDING_CONFLICT")
|
||||
commit = ContinuationCommit(
|
||||
idempotency_key=str(request["idempotency_key"]),
|
||||
files=self._files(request),
|
||||
message=str(request["message"]),
|
||||
)
|
||||
runtime.executor.workspace.materialize_files(commit.files)
|
||||
return runtime.executor.execute(runtime.binding, commit)
|
||||
|
||||
def _controller_for_job(self, job_id: str) -> GitOpsJobController:
|
||||
for controller in self.controllers.values():
|
||||
for runtime in self.runtimes.values():
|
||||
controller = runtime.controller
|
||||
try:
|
||||
controller.store.read(job_id)
|
||||
return controller
|
||||
@@ -159,13 +245,13 @@ class GovernedRuntime:
|
||||
continue
|
||||
jobs_root = self.config.state_root / "jobs"
|
||||
if jobs_root.is_dir():
|
||||
for root in jobs_root.iterdir():
|
||||
if not root.is_dir():
|
||||
for root in sorted(jobs_root.iterdir()):
|
||||
if not root.is_dir() or not (root / "state.sqlite3").is_file():
|
||||
continue
|
||||
controller = GitOpsJobController(JobStore(root, payload_root=root / "payloads"))
|
||||
try:
|
||||
controller.store.read(job_id)
|
||||
self.controllers[str(job_id)] = controller
|
||||
self.controllers["job:" + job_id] = controller
|
||||
return controller
|
||||
except Exception:
|
||||
continue
|
||||
@@ -282,73 +368,17 @@ class GovernedRuntime:
|
||||
return controller.resume(job_id).as_dict()
|
||||
return self._executor_from_row(row, controller).reconcile(job_id)
|
||||
|
||||
def _snapshot_task(self) -> dict[str, Any]:
|
||||
candidates: list[dict[str, Any]] = []
|
||||
for value in self.bindings.values():
|
||||
task_id = str(value.get("task_id") or "").strip()
|
||||
branch = str(value.get("target_branch") or "").strip()
|
||||
if not task_id or not branch.startswith("agent/"):
|
||||
continue
|
||||
authority = self.authority.read_ref("refs/heads/" + branch)
|
||||
try:
|
||||
generation = int(value.get("generation") or 0)
|
||||
except (TypeError, ValueError):
|
||||
generation = 0
|
||||
reconciliation_state = "OBSERVATION_REQUIRED"
|
||||
db = self.config.state_root / "jobs" / _safe(task_id) / "state.sqlite3"
|
||||
if db.is_file():
|
||||
connection = sqlite3.connect(f"file:{db}?mode=ro", uri=True)
|
||||
try:
|
||||
lease = connection.execute(
|
||||
"SELECT MAX(generation) FROM branch_leases WHERE branch=?",
|
||||
(branch,),
|
||||
).fetchone()
|
||||
generation = max(generation, int((lease or (0,))[0] or 0))
|
||||
latest = connection.execute(
|
||||
"SELECT state, side_effect_state FROM jobs WHERE branch=? ORDER BY updated_at DESC LIMIT 1",
|
||||
(branch,),
|
||||
).fetchone()
|
||||
if latest is not None:
|
||||
reconciliation_state = (
|
||||
"COMPLETED"
|
||||
if latest[0] == "COMPLETED"
|
||||
else str(latest[1] or latest[0])
|
||||
)
|
||||
finally:
|
||||
connection.close()
|
||||
candidates.append(
|
||||
{
|
||||
"task_id": task_id,
|
||||
"branch": branch,
|
||||
"authority_branch_oid": authority,
|
||||
"gitea_git_oid": None,
|
||||
"gitea_api_oid": None,
|
||||
"commit_visible": None,
|
||||
"generation": generation,
|
||||
"reconciliation_state": reconciliation_state,
|
||||
}
|
||||
)
|
||||
if not candidates:
|
||||
return {}
|
||||
return max(
|
||||
candidates,
|
||||
key=lambda item: (int(item["generation"] or 0), bool(item["authority_branch_oid"])),
|
||||
)
|
||||
|
||||
def snapshot(self) -> dict[str, Any]:
|
||||
task = self._snapshot_task()
|
||||
return build_control_plane_snapshot(target_version=self.config.target_version, runtime=self.runtime_status(), repository={"identity": self.config.repository, "authority_git_dir": str(self.config.authoritative_git_dir), "authority_main_oid": self.authority.read_ref("refs/heads/main")}, production={"baseline_version": "V2R1C7", "target_oid": "c7adece1cc8beffe7c718d53c775898b45d6c7f8", "deployment_state": "ACTIVE"}, task=task, capabilities={"legacy_writes_enabled": False, "governed_writes_enabled": True, "force_push": False, "main_write": False, "tag_write": False, "baseline_write": False, "production_write": False}, credential_broker={"status": "SERVER_SIDE_ONLY", "provider": "FileCredentialProvider"})
|
||||
|
||||
def handlers(self) -> dict[str, Any]:
|
||||
return {
|
||||
"createGovernedTask": self.create_task,
|
||||
"acquireWriterLease": self.acquire_lease,
|
||||
"commitFileGoverned": lambda request: self.commit_files({**request, "files": {request["path"]: request["content_base64"]}}),
|
||||
"commitFileGoverned": lambda request: self.commit_files(
|
||||
{**request, "files": {request["path"]: request["content_base64"]}}
|
||||
),
|
||||
"commitFilesGoverned": self.commit_files,
|
||||
"getGitOpsJob": lambda request: self.job(request, "get"),
|
||||
"resumeGitOpsJob": lambda request: self.job(request, "resume"),
|
||||
"reconcileGitOpsJob": lambda request: self.job(request, "reconcile"),
|
||||
"getControlPlaneSnapshot": lambda request: self.snapshot(),
|
||||
"createTagApprovalRequest": self.release.approval_request,
|
||||
"requestOrCreateTag": self.release.publish_tag,
|
||||
"getTagPublicationStatus": self.release.tag_status,
|
||||
@@ -358,50 +388,96 @@ class GovernedRuntime:
|
||||
"runReleaseAcceptance": self.release.acceptance,
|
||||
}
|
||||
|
||||
def runtime_status(self) -> dict[str, Any]:
|
||||
return {
|
||||
"status": "active",
|
||||
"health": 200,
|
||||
"control_plane_target_oid": self.config.target_oid,
|
||||
"control_plane_release_version": self.config.target_version,
|
||||
"repository": self.config.repository,
|
||||
"authoritative_git_dir": str(self.config.authoritative_git_dir),
|
||||
"legacy_global_writes_enabled": False,
|
||||
}
|
||||
|
||||
|
||||
def build_governed_handlers(runtime_config: RuntimeConfig | Mapping[str, Any]) -> Mapping[str, Any]:
|
||||
if isinstance(runtime_config, Mapping): runtime_config = RuntimeConfig(**runtime_config)
|
||||
if isinstance(runtime_config, Mapping):
|
||||
runtime_config = RuntimeConfig(**runtime_config)
|
||||
return GovernedRuntime(runtime_config).handlers()
|
||||
|
||||
|
||||
class _HTTPError(RuntimeError): pass
|
||||
class _HTTPError(RuntimeError):
|
||||
pass
|
||||
|
||||
|
||||
class BridgeHandler(BaseHTTPRequestHandler):
|
||||
adapter: BridgeV2Adapter | None = None
|
||||
def log_message(self, *_args: Any) -> None: return
|
||||
adapter: BridgeV2Adapter
|
||||
|
||||
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)
|
||||
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")
|
||||
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")
|
||||
if not isinstance(value, dict):
|
||||
raise _HTTPError("JSON_OBJECT_REQUIRED")
|
||||
return value
|
||||
|
||||
def _allowed(self, operation: str, body: Mapping[str, Any]) -> None:
|
||||
allowed = set(OPERATION_REQUIRED_FIELDS[operation]) | {"schema", "lease_seconds", "create_target_branch", "worktree_mode", "cas_ref_update_required", "expected_branch_oid", "generation", "branch_mode"}
|
||||
if operation == "commitFilesGoverned": allowed.add("files")
|
||||
allowed.update({"owner_approval", "execute"})
|
||||
if set(body) - allowed: raise _HTTPError("UNKNOWN_FIELDS")
|
||||
allowed = set(OPERATION_REQUIRED_FIELDS[operation]) | {
|
||||
"schema", "lease_seconds", "create_target_branch", "worktree_mode", "cas_ref_update_required",
|
||||
"owner_approval", "execute"
|
||||
}
|
||||
if operation == "commitFilesGoverned":
|
||||
allowed.add("files")
|
||||
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
|
||||
if self.path == "/v2/runtime":
|
||||
self._json(self.adapter.getRuntimeStatus())
|
||||
return
|
||||
if self.path == "/v2/capabilities":
|
||||
self._json(self.adapter.getGovernedWriteCapabilities())
|
||||
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
|
||||
if operation is None or operation in ("getRuntimeStatus", "getGovernedWriteCapabilities"):
|
||||
self._json({"error": "NOT_FOUND"}, 404)
|
||||
return
|
||||
try:
|
||||
body = self._body(); self._allowed(operation, body); self._json(getattr(self.adapter, operation)(body))
|
||||
except _HTTPError as exc: self._json({"error": str(exc)}, 413 if str(exc) == "REQUEST_TOO_LARGE" else 400)
|
||||
except (BootstrapError, ValueError, BridgeV2Error) as exc: self._json({"error": str(exc)}, 409 if "CONFLICT" in str(exc) else 400)
|
||||
except Exception as exc: self._json({"error": str(exc) or type(exc).__name__}, 409 if any(token in str(exc) for token in ("CONFLICT", "EXPECTATION", "MONOTONIC")) else 503)
|
||||
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:
|
||||
code = 409 if str(exc) in {"CONFLICT", "TASK_BINDING_CONFLICT", "LEASE_BINDING_CONFLICT"} else 400
|
||||
self._json({"error": str(exc)}, code)
|
||||
except Exception as exc:
|
||||
name = str(exc) or type(exc).__name__
|
||||
code = 409 if any(token in name for token in ("CONFLICT", "EXPECTATION", "ALREADY", "MONOTONIC")) else 503
|
||||
self._json({"error": name}, code)
|
||||
|
||||
|
||||
def serve(adapter: BridgeV2Adapter, host: str = "127.0.0.1", port: int = 18892) -> None:
|
||||
BridgeHandler.adapter = adapter; ThreadingHTTPServer((host, port), BridgeHandler).serve_forever()
|
||||
BridgeHandler.adapter = adapter
|
||||
ThreadingHTTPServer((host, port), BridgeHandler).serve_forever()
|
||||
|
||||
|
||||
def main() -> None:
|
||||
@@ -428,4 +504,6 @@ def main() -> None:
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__": main()
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
||||
|
||||
@@ -25,7 +25,6 @@ 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
|
||||
|
||||
|
||||
@@ -44,6 +43,7 @@ class ContinuationExecutionError(RuntimeError):
|
||||
class DetachedWorkspace(Protocol):
|
||||
def is_detached(self) -> bool: ...
|
||||
def head_oid(self) -> str: ...
|
||||
def materialize_files(self, files: Mapping[str, bytes]) -> None: ...
|
||||
def commit_files(self, files: Mapping[str, bytes], message: str) -> str: ...
|
||||
|
||||
|
||||
@@ -52,13 +52,16 @@ class AuthorityPort(Protocol):
|
||||
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: ...
|
||||
def create_task_branch_CAS(self, branch: str, new: str) -> str: ...
|
||||
def commit_exists(self, oid: str) -> bool: ...
|
||||
|
||||
|
||||
class RemotePort(Protocol):
|
||||
def repository_identity(self) -> str: ...
|
||||
def push_exact(self, branch: str, expected: str, new: str) -> None: ...
|
||||
def push_exact(self, branch: str, expected: str | None, new: str) -> None: ...
|
||||
def read_git_ref(self, branch: str) -> str | None: ...
|
||||
def read_api_branch(self, branch: str) -> str | None: ...
|
||||
|
||||
def commit_visible(self, oid: str) -> bool: ...
|
||||
|
||||
|
||||
@@ -106,6 +109,12 @@ class AuthorityGitAdapter:
|
||||
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)
|
||||
|
||||
def create_task_branch_CAS(self, branch: str, new: str) -> str:
|
||||
return self.executor.create_task_branch_CAS(self._ref(branch), True, new)
|
||||
|
||||
def commit_exists(self, oid: str) -> bool:
|
||||
return self.executor.commit_exists(oid)
|
||||
|
||||
|
||||
@dataclass(slots=True)
|
||||
class DetachedGitWorkspace:
|
||||
@@ -150,20 +159,36 @@ class DetachedGitWorkspace:
|
||||
return result
|
||||
|
||||
def materialize_files(self, files: Mapping[str, bytes]) -> None:
|
||||
dirty = self._dirty_paths()
|
||||
"""Author only the governed payload into a clean detached worktree."""
|
||||
expected_paths = set(files)
|
||||
if dirty and dirty != expected_paths:
|
||||
if not expected_paths:
|
||||
raise ContinuationExecutionError("COMMIT_REQUEST_INVALID")
|
||||
current_paths = self._dirty_paths()
|
||||
if current_paths not in (set(), expected_paths):
|
||||
raise ContinuationExecutionError("WORKTREE_DIRTY_SCOPE_MISMATCH")
|
||||
root = self.path.resolve()
|
||||
for relative, content in files.items():
|
||||
pure = PurePosixPath(relative)
|
||||
if pure.is_absolute() or ".." in pure.parts or relative.startswith((".git/", "baseline/", "production/")):
|
||||
raise ContinuationExecutionError("COMMIT_PATH_REJECTED")
|
||||
target = (self.path / relative).resolve()
|
||||
try:
|
||||
target.relative_to(self.path.resolve())
|
||||
target.relative_to(root)
|
||||
except ValueError as exc:
|
||||
raise ContinuationExecutionError("COMMIT_PATH_REJECTED") from exc
|
||||
if target.is_symlink():
|
||||
raise ContinuationExecutionError("COMMIT_PATH_REJECTED")
|
||||
raise ContinuationExecutionError("COMMIT_PATH_SYMLINK")
|
||||
if target.is_file() and target.read_bytes() == content:
|
||||
continue
|
||||
target.parent.mkdir(parents=True, exist_ok=True)
|
||||
target.write_bytes(content)
|
||||
temporary = target.with_name("." + target.name + ".aios-materialize")
|
||||
if temporary.exists() or temporary.is_symlink():
|
||||
raise ContinuationExecutionError("COMMIT_TEMP_PATH_CONFLICT")
|
||||
temporary.write_bytes(content)
|
||||
os.chmod(temporary, 0o644)
|
||||
os.replace(temporary, target)
|
||||
if self._dirty_paths() != expected_paths:
|
||||
raise ContinuationExecutionError("WORKTREE_DIRTY_SCOPE_MISMATCH")
|
||||
|
||||
def commit_files(self, files: Mapping[str, bytes], message: str) -> str:
|
||||
expected_paths = set(files)
|
||||
@@ -258,10 +283,16 @@ class GiteaContinuationRemote:
|
||||
raise ContinuationExecutionError("GITEA_GIT_READBACK_INVALID")
|
||||
return oid
|
||||
|
||||
def push_exact(self, branch: str, expected: str, new: str) -> None:
|
||||
def push_exact(self, branch: str, expected: str | None, new: str) -> None:
|
||||
self._require_projected()
|
||||
self.repository_identity()
|
||||
if self.read_git_ref(branch) != expected:
|
||||
observed = self.read_git_ref(branch)
|
||||
if observed == new:
|
||||
return
|
||||
if expected is None:
|
||||
if observed is not None:
|
||||
raise ContinuationExecutionError("GITEA_BRANCH_EXPECTATION_MISMATCH")
|
||||
elif observed != expected:
|
||||
raise ContinuationExecutionError("GITEA_BRANCH_EXPECTATION_MISMATCH")
|
||||
_run(["git", "push", self.remote_name, f"{new}:refs/heads/{branch}"], cwd=self.workspace)
|
||||
|
||||
@@ -321,8 +352,10 @@ class ContinuationPermissions:
|
||||
production_write: bool = False
|
||||
main_write: bool = False
|
||||
|
||||
def validate(self) -> None:
|
||||
if any(asdict(self).values()):
|
||||
def validate(self, *, branch_mode: str = "CONTINUE_EXISTING") -> None:
|
||||
if self.create_target_branch and branch_mode != "CREATE_NEW":
|
||||
raise ContinuationExecutionError("MUTATION_PERMISSION_DENIED")
|
||||
if self.force_push or self.tag_write or self.baseline_write or self.production_write or self.main_write:
|
||||
raise ContinuationExecutionError("MUTATION_PERMISSION_DENIED")
|
||||
|
||||
|
||||
@@ -413,20 +446,31 @@ class ContinuationExecutor:
|
||||
"lease_seconds": request.lease_seconds,
|
||||
}
|
||||
|
||||
def _validate_preflight(self, binding: ContinuationRuntimeBinding) -> None:
|
||||
def _validate_preflight(
|
||||
self,
|
||||
binding: ContinuationRuntimeBinding,
|
||||
*,
|
||||
allow_same_generation: bool = False,
|
||||
) -> 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}:
|
||||
expected_normalized = {
|
||||
**self._request_mapping(request),
|
||||
"create_target_branch": request.branch_mode == "CREATE_NEW",
|
||||
"worktree_mode": "detached_at_base_oid" if request.branch_mode == "CREATE_NEW" else "detached_at_expected_head",
|
||||
"cas_ref_update_required": True,
|
||||
}
|
||||
if normalized != expected_normalized:
|
||||
raise ContinuationExecutionError("CONTINUATION_NORMALIZATION_INVALID")
|
||||
if not binding.original_receipt_valid or not binding.original_grant_consumed:
|
||||
raise ContinuationExecutionError("ORIGINAL_GRANT_LINEAGE_INVALID")
|
||||
binding.permissions.validate()
|
||||
binding.permissions.validate(branch_mode=request.branch_mode)
|
||||
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):
|
||||
if request.generation <= self._max_generation(request.target_branch) and not allow_same_generation:
|
||||
raise ContinuationExecutionError("GENERATION_NOT_MONOTONIC")
|
||||
if not self.workspace.is_detached():
|
||||
raise ContinuationExecutionError("WORKTREE_NOT_DETACHED")
|
||||
@@ -442,7 +486,7 @@ class ContinuationExecutor:
|
||||
"campaign_id": request.task_id,
|
||||
"task_id": request.task_id,
|
||||
"repository": request.repository,
|
||||
"operation": "CONTINUATION_COMMIT_FILES",
|
||||
"operation": "CONTINUATION_COMMIT_FILES_CREATE_NEW" if request.branch_mode == "CREATE_NEW" else "CONTINUATION_COMMIT_FILES",
|
||||
"branch": request.target_branch,
|
||||
"expected_branch_oid": request.expected_branch_oid,
|
||||
"base_oid": request.base_oid,
|
||||
@@ -458,7 +502,26 @@ class ContinuationExecutor:
|
||||
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)
|
||||
retry_same_generation = False
|
||||
if JobState(current["state"]) in {
|
||||
JobState.EXECUTING,
|
||||
JobState.RECOVERABLE_FAILURE,
|
||||
JobState.RECONCILING,
|
||||
}:
|
||||
receipt = json.loads(current["receipt_json"]) if current["receipt_json"] else {}
|
||||
if not receipt.get("recovery_safe_before_git_side_effect"):
|
||||
raise ContinuationExecutionError("RECONCILIATION_REQUIRED")
|
||||
if any(key in receipt for key in ("authority_oid", "desired_remote_oid", "git_remote_oid", "api_branch_oid")):
|
||||
raise ContinuationExecutionError("RECONCILIATION_REQUIRED")
|
||||
retry_same_generation = True
|
||||
if JobState(current["state"]) != JobState.RECONCILING:
|
||||
self.controller.advance(
|
||||
submitted.job_id,
|
||||
JobState.RECONCILING,
|
||||
side_effect_state="PRE_SIDE_EFFECT_RETRY",
|
||||
receipt={"recovery_safe_before_git_side_effect": True},
|
||||
)
|
||||
self._validate_preflight(binding, allow_same_generation=retry_same_generation)
|
||||
request = binding.request
|
||||
projected = False
|
||||
try:
|
||||
@@ -488,7 +551,10 @@ class ContinuationExecutor:
|
||||
new_oid = self.workspace.commit_files(commit.files, commit.message)
|
||||
if not OID_RE.fullmatch(new_oid) or not self.authority.is_ancestor(request.expected_branch_oid, new_oid):
|
||||
raise ContinuationExecutionError("NON_FAST_FORWARD_COMMIT")
|
||||
authority_oid = self.authority.compare_and_swap_branch(request.target_branch, request.expected_branch_oid, new_oid)
|
||||
if request.branch_mode == "CREATE_NEW":
|
||||
authority_oid = self.authority.create_task_branch_CAS(request.target_branch, new_oid)
|
||||
else:
|
||||
authority_oid = self.authority.compare_and_swap_branch(request.target_branch, request.expected_branch_oid, new_oid)
|
||||
if authority_oid != new_oid:
|
||||
raise ContinuationExecutionError("AUTHORITY_CAS_READBACK_MISMATCH")
|
||||
self.controller.advance(
|
||||
@@ -528,10 +594,8 @@ class ContinuationExecutor:
|
||||
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:
|
||||
if not (authority_oid == git_oid == api_oid == new_oid and visible):
|
||||
return self._remote_pending(job_id, authority_oid, new_oid)
|
||||
receipt = {
|
||||
"authority_oid": authority_oid,
|
||||
@@ -539,7 +603,6 @@ class ContinuationExecutor:
|
||||
"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)
|
||||
@@ -556,39 +619,17 @@ class ContinuationExecutor:
|
||||
receipt = json.loads(row["receipt_json"])
|
||||
new_oid = receipt["desired_remote_oid"]
|
||||
authority_oid = self.authority.branch_head(row["branch"])
|
||||
try:
|
||||
generation = int(request.get("generation") or 0)
|
||||
except (TypeError, ValueError) as exc:
|
||||
raise ContinuationExecutionError("DURABLE_BINDING_GENERATION_INVALID") from exc
|
||||
if generation > 0:
|
||||
with self.controller.store._connect() as connection:
|
||||
earlier = connection.execute(
|
||||
"SELECT 1 FROM jobs WHERE branch=? AND state=? "
|
||||
"AND CAST(json_extract(request_json, '$.binding.generation') AS INTEGER) < ? LIMIT 1",
|
||||
(row["branch"], JobState.RECONCILIATION_REQUIRED.value, generation),
|
||||
).fetchone()
|
||||
if earlier is not None:
|
||||
raise ContinuationExecutionError("EARLIER_RECONCILIATION_REQUIRED")
|
||||
self.credential.project()
|
||||
try:
|
||||
git_oid, api_oid, visible = self._observe_remote(row["branch"], new_oid)
|
||||
if authority_oid == git_oid == api_oid == new_oid and visible:
|
||||
self.controller.advance(job_id, JobState.RECONCILING, side_effect_state="OBSERVATION_FIRST")
|
||||
return self._finish_remote_verification(job_id, new_oid, new_oid)
|
||||
expected_oid = request["expected_branch_oid"]
|
||||
if (
|
||||
self.authority.is_ancestor(new_oid, authority_oid)
|
||||
and git_oid is not None
|
||||
and git_oid == api_oid
|
||||
and self.authority.is_ancestor(git_oid, new_oid)
|
||||
):
|
||||
self.controller.advance(job_id, JobState.RECONCILING, side_effect_state="OBSERVATION_FIRST_CHAIN")
|
||||
self.remote.push_exact(row["branch"], git_oid, new_oid)
|
||||
return self._finish_remote_verification(job_id, new_oid, new_oid)
|
||||
return self._finish_remote_verification(job_id, authority_oid, new_oid)
|
||||
expected_oid = None if row["operation"].endswith("_CREATE_NEW") else request["expected_branch_oid"]
|
||||
if authority_oid == new_oid and git_oid == api_oid == expected_oid:
|
||||
self.controller.advance(job_id, JobState.RECONCILING, side_effect_state="OBSERVATION_FIRST_RETRY")
|
||||
self.remote.push_exact(row["branch"], expected_oid, new_oid)
|
||||
return self._finish_remote_verification(job_id, new_oid, new_oid)
|
||||
return self._finish_remote_verification(job_id, authority_oid, new_oid)
|
||||
return self.controller.store._receipt(row, duplicate=True).as_dict()
|
||||
finally:
|
||||
self.credential.revoke()
|
||||
|
||||
@@ -1,26 +0,0 @@
|
||||
"""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)
|
||||
@@ -1,78 +0,0 @@
|
||||
"""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()
|
||||
@@ -1,352 +0,0 @@
|
||||
"""Cryptographic Owner-approval boundary for governed existing-branch publication.
|
||||
|
||||
Only a complete signed envelope from an authoritative trust-root adapter is
|
||||
accepted. References, chat assertions, and locally supplied keys are not an
|
||||
approval. Missing public-key or revocation evidence fails closed.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import base64
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime, timezone
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import re
|
||||
from typing import Any, Mapping, Protocol
|
||||
|
||||
from cryptography.exceptions import InvalidSignature
|
||||
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PublicKey
|
||||
|
||||
|
||||
APPROVAL_SCHEMA = "aios.existing-branch-owner-approval.v1"
|
||||
PUBLISH_OPERATION = "publishExistingBranchGoverned"
|
||||
OID_RE = re.compile(r"^[0-9a-f]{40}$")
|
||||
DIGEST_RE = re.compile(r"^[0-9a-f]{64}$")
|
||||
IDENTIFIER_RE = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._@:+/-]{0,254}$")
|
||||
BRANCH_RE = re.compile(r"^agent/[A-Za-z0-9][A-Za-z0-9._/-]{2,179}$")
|
||||
|
||||
|
||||
class OwnerApprovalRejected(RuntimeError):
|
||||
"""Stable, audit-safe error for a rejected Owner approval."""
|
||||
|
||||
def __init__(self, code: str) -> None:
|
||||
super().__init__(code)
|
||||
self.code = code
|
||||
|
||||
|
||||
def canonical_json(value: Any) -> bytes:
|
||||
"""Encode a signed payload without implementation-dependent spacing."""
|
||||
try:
|
||||
encoded = json.dumps(
|
||||
value,
|
||||
sort_keys=True,
|
||||
separators=(",", ":"),
|
||||
ensure_ascii=False,
|
||||
allow_nan=False,
|
||||
)
|
||||
except (TypeError, ValueError) as exc:
|
||||
raise OwnerApprovalRejected("OWNER_APPROVAL_CANONICALIZATION_REJECTED") from exc
|
||||
return (encoded + "\n").encode("utf-8")
|
||||
|
||||
|
||||
def digest_json(value: Any) -> str:
|
||||
return hashlib.sha256(canonical_json(value)).hexdigest()
|
||||
|
||||
|
||||
def _require_identifier(field: str, value: object) -> str:
|
||||
if not isinstance(value, str) or not IDENTIFIER_RE.fullmatch(value):
|
||||
raise OwnerApprovalRejected(f"OWNER_APPROVAL_{field.upper()}_REJECTED")
|
||||
return value
|
||||
|
||||
|
||||
def _require_digest(field: str, value: object) -> str:
|
||||
if not isinstance(value, str) or not DIGEST_RE.fullmatch(value):
|
||||
raise OwnerApprovalRejected(f"OWNER_APPROVAL_{field.upper()}_REJECTED")
|
||||
return value
|
||||
|
||||
|
||||
def _require_oid(field: str, value: object) -> str:
|
||||
if not isinstance(value, str) or not OID_RE.fullmatch(value):
|
||||
raise OwnerApprovalRejected(f"OWNER_APPROVAL_{field.upper()}_REJECTED")
|
||||
return value
|
||||
|
||||
|
||||
def _parse_utc(field: str, value: object) -> datetime:
|
||||
if not isinstance(value, str):
|
||||
raise OwnerApprovalRejected(f"OWNER_APPROVAL_{field.upper()}_REJECTED")
|
||||
try:
|
||||
instant = datetime.fromisoformat(value.replace("Z", "+00:00"))
|
||||
except ValueError as exc:
|
||||
raise OwnerApprovalRejected(f"OWNER_APPROVAL_{field.upper()}_REJECTED") from exc
|
||||
if instant.tzinfo is None:
|
||||
raise OwnerApprovalRejected(f"OWNER_APPROVAL_{field.upper()}_REJECTED")
|
||||
return instant.astimezone(timezone.utc)
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class PublicationApprovalBinding:
|
||||
repository: str
|
||||
branch: str
|
||||
expected_branch_oid: str
|
||||
operation: str
|
||||
request_digest: str
|
||||
idempotency_digest: str
|
||||
|
||||
def validate(self) -> "PublicationApprovalBinding":
|
||||
_require_identifier("repository", self.repository)
|
||||
if not BRANCH_RE.fullmatch(self.branch):
|
||||
raise OwnerApprovalRejected("OWNER_APPROVAL_BRANCH_REJECTED")
|
||||
_require_oid("expected_branch_oid", self.expected_branch_oid)
|
||||
if self.operation != PUBLISH_OPERATION:
|
||||
raise OwnerApprovalRejected("OWNER_APPROVAL_OPERATION_REJECTED")
|
||||
_require_digest("request_digest", self.request_digest)
|
||||
_require_digest("idempotency_digest", self.idempotency_digest)
|
||||
return self
|
||||
|
||||
@property
|
||||
def as_dict(self) -> dict[str, str]:
|
||||
return {
|
||||
"repository": self.repository,
|
||||
"branch": self.branch,
|
||||
"expected_branch_oid": self.expected_branch_oid,
|
||||
"operation": self.operation,
|
||||
"request_digest": self.request_digest,
|
||||
"idempotency_digest": self.idempotency_digest,
|
||||
}
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class OwnerApprovalEnvelope:
|
||||
approval_id: str
|
||||
repository: str
|
||||
branch: str
|
||||
expected_branch_oid: str
|
||||
operation: str
|
||||
issued_at: str
|
||||
not_before: str
|
||||
expires_at: str
|
||||
nonce: str
|
||||
owner_identity: str
|
||||
trust_domain: str
|
||||
key_id: str
|
||||
request_digest: str
|
||||
idempotency_digest: str
|
||||
signature: str
|
||||
|
||||
@property
|
||||
def canonical_payload(self) -> dict[str, str]:
|
||||
return {
|
||||
"schema": APPROVAL_SCHEMA,
|
||||
"approval_id": self.approval_id,
|
||||
"repository": self.repository,
|
||||
"branch": self.branch,
|
||||
"expected_branch_oid": self.expected_branch_oid,
|
||||
"operation": self.operation,
|
||||
"issued_at": self.issued_at,
|
||||
"not_before": self.not_before,
|
||||
"expires_at": self.expires_at,
|
||||
"nonce": self.nonce,
|
||||
"owner_identity": self.owner_identity,
|
||||
"trust_domain": self.trust_domain,
|
||||
"key_id": self.key_id,
|
||||
"request_digest": self.request_digest,
|
||||
"idempotency_digest": self.idempotency_digest,
|
||||
}
|
||||
|
||||
def canonical_bytes(self) -> bytes:
|
||||
return canonical_json(self.canonical_payload)
|
||||
|
||||
@property
|
||||
def fingerprint(self) -> str:
|
||||
return hashlib.sha256(self.canonical_bytes()).hexdigest()
|
||||
|
||||
@property
|
||||
def binding(self) -> PublicationApprovalBinding:
|
||||
return PublicationApprovalBinding(
|
||||
repository=self.repository,
|
||||
branch=self.branch,
|
||||
expected_branch_oid=self.expected_branch_oid,
|
||||
operation=self.operation,
|
||||
request_digest=self.request_digest,
|
||||
idempotency_digest=self.idempotency_digest,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def from_mapping(cls, value: object) -> "OwnerApprovalEnvelope":
|
||||
if not isinstance(value, Mapping):
|
||||
raise OwnerApprovalRejected("OWNER_APPROVAL_ENVELOPE_REQUIRED")
|
||||
expected_fields = {
|
||||
"schema", "approval_id", "repository", "branch", "expected_branch_oid",
|
||||
"operation", "issued_at", "not_before", "expires_at", "nonce",
|
||||
"owner_identity", "trust_domain", "key_id", "request_digest",
|
||||
"idempotency_digest", "signature",
|
||||
}
|
||||
if set(value) != expected_fields or value.get("schema") != APPROVAL_SCHEMA:
|
||||
raise OwnerApprovalRejected("OWNER_APPROVAL_ENVELOPE_REJECTED")
|
||||
envelope = cls(
|
||||
approval_id=_require_identifier("approval_id", value["approval_id"]),
|
||||
repository=_require_identifier("repository", value["repository"]),
|
||||
branch=str(value["branch"]),
|
||||
expected_branch_oid=_require_oid("expected_branch_oid", value["expected_branch_oid"]),
|
||||
operation=str(value["operation"]),
|
||||
issued_at=str(value["issued_at"]),
|
||||
not_before=str(value["not_before"]),
|
||||
expires_at=str(value["expires_at"]),
|
||||
nonce=_require_identifier("nonce", value["nonce"]),
|
||||
owner_identity=_require_identifier("owner_identity", value["owner_identity"]),
|
||||
trust_domain=_require_identifier("trust_domain", value["trust_domain"]),
|
||||
key_id=_require_identifier("key_id", value["key_id"]),
|
||||
request_digest=_require_digest("request_digest", value["request_digest"]),
|
||||
idempotency_digest=_require_digest("idempotency_digest", value["idempotency_digest"]),
|
||||
signature=str(value["signature"]),
|
||||
)
|
||||
envelope.binding.validate()
|
||||
_parse_utc("issued_at", envelope.issued_at)
|
||||
_parse_utc("not_before", envelope.not_before)
|
||||
_parse_utc("expires_at", envelope.expires_at)
|
||||
if not envelope.signature:
|
||||
raise OwnerApprovalRejected("OWNER_APPROVAL_SIGNATURE_REJECTED")
|
||||
return envelope
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class TrustRootEvidence:
|
||||
"""Public-key and revocation evidence from the authoritative trust root."""
|
||||
|
||||
key_id: str
|
||||
owner_identity: str
|
||||
trust_domain: str
|
||||
public_key: bytes | None
|
||||
authority_confirmed: bool
|
||||
revocation_state: str
|
||||
|
||||
|
||||
class AuthoritativeTrustRootAdapter(Protocol):
|
||||
def resolve(
|
||||
self, *, key_id: str, owner_identity: str, trust_domain: str
|
||||
) -> TrustRootEvidence | None: ...
|
||||
|
||||
|
||||
class ReplayLedger(Protocol):
|
||||
def consume(
|
||||
self, approval: OwnerApprovalEnvelope, binding: PublicationApprovalBinding
|
||||
) -> None: ...
|
||||
|
||||
|
||||
class DurableNonceReplayLedger:
|
||||
"""Single-use nonce storage that remains conservative across restarts."""
|
||||
|
||||
def __init__(self, root: Path) -> None:
|
||||
self.root = Path(root)
|
||||
|
||||
def consume(
|
||||
self, approval: OwnerApprovalEnvelope, binding: PublicationApprovalBinding
|
||||
) -> None:
|
||||
binding.validate()
|
||||
if self.root.is_symlink():
|
||||
raise OwnerApprovalRejected("OWNER_APPROVAL_REPLAY_LEDGER_REJECTED")
|
||||
try:
|
||||
self.root.mkdir(parents=True, exist_ok=True)
|
||||
if not self.root.is_dir() or self.root.is_symlink():
|
||||
raise OwnerApprovalRejected("OWNER_APPROVAL_REPLAY_LEDGER_REJECTED")
|
||||
name = hashlib.sha256(approval.nonce.encode("utf-8")).hexdigest()
|
||||
record_path = self.root / name
|
||||
flags = os.O_WRONLY | os.O_CREAT | os.O_EXCL
|
||||
if hasattr(os, "O_NOFOLLOW"):
|
||||
flags |= os.O_NOFOLLOW
|
||||
descriptor = os.open(record_path, flags, 0o600)
|
||||
except FileExistsError as exc:
|
||||
raise OwnerApprovalRejected("OWNER_APPROVAL_REPLAY_REJECTED") from exc
|
||||
except OwnerApprovalRejected:
|
||||
raise
|
||||
except OSError as exc:
|
||||
raise OwnerApprovalRejected("OWNER_APPROVAL_REPLAY_LEDGER_REJECTED") from exc
|
||||
record = {
|
||||
"schema": "aios.existing-branch-owner-approval-replay.v1",
|
||||
"nonce_sha256": name,
|
||||
"approval_fingerprint": approval.fingerprint,
|
||||
"binding_digest": digest_json(binding.as_dict),
|
||||
}
|
||||
try:
|
||||
with os.fdopen(descriptor, "wb", closefd=True) as stream:
|
||||
stream.write(canonical_json(record))
|
||||
stream.flush()
|
||||
os.fsync(stream.fileno())
|
||||
directory = os.open(self.root, os.O_RDONLY | getattr(os, "O_DIRECTORY", 0))
|
||||
try:
|
||||
os.fsync(directory)
|
||||
finally:
|
||||
os.close(directory)
|
||||
except OSError as exc:
|
||||
raise OwnerApprovalRejected("OWNER_APPROVAL_REPLAY_LEDGER_REJECTED") from exc
|
||||
|
||||
|
||||
class OwnerApprovalVerifier:
|
||||
def __init__(
|
||||
self,
|
||||
trust_root: AuthoritativeTrustRootAdapter,
|
||||
*,
|
||||
allowed_trust_domains: set[str] | frozenset[str],
|
||||
maximum_lifetime_seconds: int = 900,
|
||||
) -> None:
|
||||
if not allowed_trust_domains or maximum_lifetime_seconds < 1:
|
||||
raise ValueError("OWNER_APPROVAL_VERIFIER_CONFIGURATION_REJECTED")
|
||||
self.trust_root = trust_root
|
||||
self.allowed_trust_domains = frozenset(allowed_trust_domains)
|
||||
self.maximum_lifetime_seconds = maximum_lifetime_seconds
|
||||
|
||||
def verify(
|
||||
self,
|
||||
value: object,
|
||||
binding: PublicationApprovalBinding,
|
||||
*,
|
||||
now: datetime | None = None,
|
||||
) -> OwnerApprovalEnvelope:
|
||||
binding.validate()
|
||||
approval = OwnerApprovalEnvelope.from_mapping(value)
|
||||
if approval.binding != binding:
|
||||
raise OwnerApprovalRejected("OWNER_APPROVAL_BINDING_REJECTED")
|
||||
instant = datetime.now(timezone.utc) if now is None else now
|
||||
if instant.tzinfo is None:
|
||||
raise OwnerApprovalRejected("OWNER_APPROVAL_TIME_REJECTED")
|
||||
instant = instant.astimezone(timezone.utc)
|
||||
issued_at = _parse_utc("issued_at", approval.issued_at)
|
||||
not_before = _parse_utc("not_before", approval.not_before)
|
||||
expires_at = _parse_utc("expires_at", approval.expires_at)
|
||||
if not issued_at <= not_before <= expires_at:
|
||||
raise OwnerApprovalRejected("OWNER_APPROVAL_TIME_REJECTED")
|
||||
if not not_before <= instant <= expires_at:
|
||||
raise OwnerApprovalRejected("OWNER_APPROVAL_TIME_REJECTED")
|
||||
if (expires_at - issued_at).total_seconds() > self.maximum_lifetime_seconds:
|
||||
raise OwnerApprovalRejected("OWNER_APPROVAL_TIME_REJECTED")
|
||||
if approval.trust_domain not in self.allowed_trust_domains:
|
||||
raise OwnerApprovalRejected("OWNER_APPROVAL_TRUST_DOMAIN_REJECTED")
|
||||
evidence = self.trust_root.resolve(
|
||||
key_id=approval.key_id,
|
||||
owner_identity=approval.owner_identity,
|
||||
trust_domain=approval.trust_domain,
|
||||
)
|
||||
if evidence is None or not evidence.authority_confirmed:
|
||||
raise OwnerApprovalRejected("OWNER_APPROVAL_TRUST_ROOT_EVIDENCE_MISSING")
|
||||
if (
|
||||
evidence.key_id != approval.key_id
|
||||
or evidence.owner_identity != approval.owner_identity
|
||||
or evidence.trust_domain != approval.trust_domain
|
||||
):
|
||||
raise OwnerApprovalRejected("OWNER_APPROVAL_TRUST_ROOT_BINDING_REJECTED")
|
||||
if evidence.revocation_state == "REVOKED":
|
||||
raise OwnerApprovalRejected("OWNER_APPROVAL_REVOKED")
|
||||
if evidence.revocation_state != "ACTIVE":
|
||||
raise OwnerApprovalRejected("OWNER_APPROVAL_REVOCATION_EVIDENCE_MISSING")
|
||||
if evidence.public_key is None or len(evidence.public_key) != 32:
|
||||
raise OwnerApprovalRejected("OWNER_APPROVAL_TRUST_ROOT_EVIDENCE_MISSING")
|
||||
try:
|
||||
signature = base64.b64decode(approval.signature, validate=True)
|
||||
Ed25519PublicKey.from_public_bytes(evidence.public_key).verify(
|
||||
signature, approval.canonical_bytes()
|
||||
)
|
||||
except (ValueError, TypeError, InvalidSignature) as exc:
|
||||
raise OwnerApprovalRejected("OWNER_APPROVAL_SIGNATURE_REJECTED") from exc
|
||||
return approval
|
||||
@@ -1,331 +0,0 @@
|
||||
"""Narrow governed publisher for an already-existing task branch.
|
||||
|
||||
Its only writing port is a compare-and-swap publication after a complete
|
||||
signed Owner approval, a stable remote observation, and a single-use nonce
|
||||
claim. It neither owns trust material nor accepts direct reference strings.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
import re
|
||||
from typing import Any, Mapping, Protocol
|
||||
|
||||
from aios_existing_branch_approval import (
|
||||
OwnerApprovalEnvelope,
|
||||
OwnerApprovalRejected,
|
||||
OwnerApprovalVerifier,
|
||||
PublicationApprovalBinding,
|
||||
ReplayLedger,
|
||||
canonical_json,
|
||||
digest_json,
|
||||
)
|
||||
|
||||
|
||||
REQUEST_SCHEMA = "aios.existing-branch-publication.v1"
|
||||
PUBLISH_OPERATION = "publishExistingBranchGoverned"
|
||||
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 ExistingBranchPublicationRejected(RuntimeError):
|
||||
def __init__(self, code: str) -> None:
|
||||
super().__init__(code)
|
||||
self.code = code
|
||||
|
||||
|
||||
class RemoteOidConflict(RuntimeError):
|
||||
pass
|
||||
|
||||
|
||||
class RemoteObservationAmbiguous(RuntimeError):
|
||||
pass
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class ExistingBranchPublicationRequest:
|
||||
repository: str
|
||||
branch: str
|
||||
expected_branch_oid: str
|
||||
idempotency_key: str
|
||||
payload: Mapping[str, Any]
|
||||
|
||||
@classmethod
|
||||
def from_mapping(cls, value: object) -> "ExistingBranchPublicationRequest":
|
||||
if not isinstance(value, Mapping):
|
||||
raise ExistingBranchPublicationRejected("PUBLICATION_REQUEST_REJECTED")
|
||||
expected_fields = {
|
||||
"schema",
|
||||
"repository",
|
||||
"branch",
|
||||
"expected_branch_oid",
|
||||
"operation",
|
||||
"idempotency_key",
|
||||
"payload",
|
||||
}
|
||||
if set(value) != expected_fields or value.get("schema") != REQUEST_SCHEMA:
|
||||
raise ExistingBranchPublicationRejected("PUBLICATION_REQUEST_REJECTED")
|
||||
repository = value["repository"]
|
||||
branch = value["branch"]
|
||||
expected_oid = value["expected_branch_oid"]
|
||||
key = value["idempotency_key"]
|
||||
payload = value["payload"]
|
||||
if (
|
||||
not isinstance(repository, str)
|
||||
or repository != "aios/aios"
|
||||
or not BRANCH_RE.fullmatch(str(branch))
|
||||
or not isinstance(expected_oid, str)
|
||||
or not OID_RE.fullmatch(expected_oid)
|
||||
or value.get("operation") != PUBLISH_OPERATION
|
||||
or not isinstance(key, str)
|
||||
or not key.strip()
|
||||
or not isinstance(payload, Mapping)
|
||||
):
|
||||
raise ExistingBranchPublicationRejected("PUBLICATION_REQUEST_REJECTED")
|
||||
try:
|
||||
canonical_json(payload)
|
||||
except OwnerApprovalRejected as exc:
|
||||
raise ExistingBranchPublicationRejected("PUBLICATION_PAYLOAD_REJECTED") from exc
|
||||
return cls(repository, str(branch), expected_oid, key, dict(payload))
|
||||
|
||||
@property
|
||||
def request_digest(self) -> str:
|
||||
return digest_json(
|
||||
{
|
||||
"schema": REQUEST_SCHEMA,
|
||||
"repository": self.repository,
|
||||
"branch": self.branch,
|
||||
"expected_branch_oid": self.expected_branch_oid,
|
||||
"operation": PUBLISH_OPERATION,
|
||||
"payload": self.payload,
|
||||
}
|
||||
)
|
||||
|
||||
@property
|
||||
def idempotency_digest(self) -> str:
|
||||
return digest_json(
|
||||
{
|
||||
"idempotency_key": self.idempotency_key,
|
||||
"request_digest": self.request_digest,
|
||||
}
|
||||
)
|
||||
|
||||
@property
|
||||
def approval_binding(self) -> PublicationApprovalBinding:
|
||||
return PublicationApprovalBinding(
|
||||
repository=self.repository,
|
||||
branch=self.branch,
|
||||
expected_branch_oid=self.expected_branch_oid,
|
||||
operation=PUBLISH_OPERATION,
|
||||
request_digest=self.request_digest,
|
||||
idempotency_digest=self.idempotency_digest,
|
||||
)
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class RemoteBranchObservation:
|
||||
state: str
|
||||
git_oid: str | None
|
||||
api_oid: str | None
|
||||
expected_commit_visible: bool | None
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class RemotePublicationEvidence:
|
||||
authority_oid: str | None
|
||||
gitea_git_oid: str | None
|
||||
gitea_api_oid: str | None
|
||||
commit_visible: bool | None
|
||||
|
||||
|
||||
class ExistingBranchRemotePort(Protocol):
|
||||
def observe(self, branch: str) -> RemoteBranchObservation: ...
|
||||
|
||||
def publish_compare_and_swap(
|
||||
self, branch: str, expected_branch_oid: str, commit_oid: str
|
||||
) -> RemotePublicationEvidence: ...
|
||||
|
||||
|
||||
class AuthorityBranchPort(Protocol):
|
||||
def read_branch_oid(self, branch: str) -> str | None: ...
|
||||
|
||||
|
||||
class CredentialProjection(Protocol):
|
||||
def project(self) -> None: ...
|
||||
|
||||
def revoke(self) -> None: ...
|
||||
|
||||
|
||||
class PublicationWorkspace(Protocol):
|
||||
def head_oid(self) -> str: ...
|
||||
|
||||
def commit_payload(self, payload: Mapping[str, Any]) -> str: ...
|
||||
|
||||
|
||||
class DetachedWorkspaceFactory(Protocol):
|
||||
def open_detached(self, expected_branch_oid: str) -> PublicationWorkspace: ...
|
||||
|
||||
|
||||
class AuditSink(Protocol):
|
||||
def record(self, event: Mapping[str, Any]) -> None: ...
|
||||
|
||||
|
||||
class ExistingBranchPublisher:
|
||||
"""Validate once, observe once, then make one bounded publication attempt."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
verifier: OwnerApprovalVerifier,
|
||||
replay_ledger: ReplayLedger,
|
||||
authority: AuthorityBranchPort,
|
||||
remote: ExistingBranchRemotePort,
|
||||
credentials: CredentialProjection,
|
||||
workspaces: DetachedWorkspaceFactory,
|
||||
audit: AuditSink,
|
||||
) -> None:
|
||||
self.verifier = verifier
|
||||
self.replay_ledger = replay_ledger
|
||||
self.authority = authority
|
||||
self.remote = remote
|
||||
self.credentials = credentials
|
||||
self.workspaces = workspaces
|
||||
self.audit = audit
|
||||
|
||||
def _audit(
|
||||
self,
|
||||
*,
|
||||
status: str,
|
||||
request: ExistingBranchPublicationRequest,
|
||||
approval: OwnerApprovalEnvelope | None = None,
|
||||
commit_oid: str | None = None,
|
||||
) -> None:
|
||||
event: dict[str, Any] = {
|
||||
"event": "governed_existing_branch_publication",
|
||||
"status": status,
|
||||
"repository": request.repository,
|
||||
"branch": request.branch,
|
||||
"expected_branch_oid": request.expected_branch_oid,
|
||||
"operation": PUBLISH_OPERATION,
|
||||
"request_digest": request.request_digest,
|
||||
"idempotency_digest": request.idempotency_digest,
|
||||
}
|
||||
if approval is not None:
|
||||
event.update(
|
||||
{
|
||||
"approval_id": approval.approval_id,
|
||||
"owner_identity": approval.owner_identity,
|
||||
"trust_domain": approval.trust_domain,
|
||||
"approval_fingerprint": approval.fingerprint,
|
||||
}
|
||||
)
|
||||
if commit_oid is not None:
|
||||
event["commit_oid"] = commit_oid
|
||||
self.audit.record(event)
|
||||
|
||||
def _preflight_remote(
|
||||
self, request: ExistingBranchPublicationRequest
|
||||
) -> RemoteBranchObservation | dict[str, str]:
|
||||
try:
|
||||
authority_oid = self.authority.read_branch_oid(request.branch)
|
||||
except Exception:
|
||||
return {"status": "RECONCILIATION_REQUIRED"}
|
||||
if authority_oid != request.expected_branch_oid:
|
||||
return {"status": "MANUAL_OWNER_REVIEW_REQUIRED"}
|
||||
try:
|
||||
observation = self.remote.observe(request.branch)
|
||||
except RemoteObservationAmbiguous:
|
||||
return {"status": "RECONCILIATION_REQUIRED"}
|
||||
if observation.state == "AMBIGUOUS":
|
||||
return {"status": "RECONCILIATION_REQUIRED"}
|
||||
if observation.state != "PRESENT":
|
||||
return {"status": "MANUAL_OWNER_REVIEW_REQUIRED"}
|
||||
if (
|
||||
observation.git_oid is not None
|
||||
and observation.api_oid is not None
|
||||
and observation.git_oid != observation.api_oid
|
||||
):
|
||||
return {"status": "RECONCILIATION_REQUIRED"}
|
||||
if (
|
||||
observation.git_oid != request.expected_branch_oid
|
||||
or observation.api_oid != request.expected_branch_oid
|
||||
):
|
||||
return {"status": "MANUAL_OWNER_REVIEW_REQUIRED"}
|
||||
if observation.expected_commit_visible is not True:
|
||||
return {"status": "RECONCILIATION_REQUIRED"}
|
||||
return observation
|
||||
|
||||
def publish(
|
||||
self, request_value: object, approval_value: object
|
||||
) -> dict[str, str | bool]:
|
||||
request = ExistingBranchPublicationRequest.from_mapping(request_value)
|
||||
try:
|
||||
approval = self.verifier.verify(approval_value, request.approval_binding)
|
||||
except OwnerApprovalRejected:
|
||||
self._audit(status="REJECTED", request=request)
|
||||
raise
|
||||
preflight = self._preflight_remote(request)
|
||||
if isinstance(preflight, dict):
|
||||
self._audit(status=preflight["status"], request=request, approval=approval)
|
||||
return preflight
|
||||
self.replay_ledger.consume(approval, request.approval_binding)
|
||||
workspace = self.workspaces.open_detached(request.expected_branch_oid)
|
||||
if workspace.head_oid() != request.expected_branch_oid:
|
||||
self._audit(status="REJECTED", request=request, approval=approval)
|
||||
raise ExistingBranchPublicationRejected("DETACHED_WORKSPACE_HEAD_REJECTED")
|
||||
projected = False
|
||||
try:
|
||||
self.credentials.project()
|
||||
projected = True
|
||||
commit_oid = workspace.commit_payload(request.payload)
|
||||
if not isinstance(commit_oid, str) or not OID_RE.fullmatch(commit_oid):
|
||||
raise ExistingBranchPublicationRejected("PUBLICATION_COMMIT_REJECTED")
|
||||
try:
|
||||
evidence = self.remote.publish_compare_and_swap(
|
||||
request.branch, request.expected_branch_oid, commit_oid
|
||||
)
|
||||
except RemoteOidConflict:
|
||||
self._audit(
|
||||
status="MANUAL_OWNER_REVIEW_REQUIRED",
|
||||
request=request,
|
||||
approval=approval,
|
||||
commit_oid=commit_oid,
|
||||
)
|
||||
return {"status": "MANUAL_OWNER_REVIEW_REQUIRED"}
|
||||
except RemoteObservationAmbiguous:
|
||||
self._audit(
|
||||
status="RECONCILIATION_REQUIRED",
|
||||
request=request,
|
||||
approval=approval,
|
||||
commit_oid=commit_oid,
|
||||
)
|
||||
return {"status": "RECONCILIATION_REQUIRED"}
|
||||
if (
|
||||
evidence.authority_oid != commit_oid
|
||||
or evidence.gitea_git_oid != commit_oid
|
||||
or evidence.gitea_api_oid != commit_oid
|
||||
or evidence.commit_visible is not True
|
||||
):
|
||||
self._audit(
|
||||
status="RECONCILIATION_REQUIRED",
|
||||
request=request,
|
||||
approval=approval,
|
||||
commit_oid=commit_oid,
|
||||
)
|
||||
return {"status": "RECONCILIATION_REQUIRED"}
|
||||
self._audit(
|
||||
status="PUBLISHED",
|
||||
request=request,
|
||||
approval=approval,
|
||||
commit_oid=commit_oid,
|
||||
)
|
||||
return {
|
||||
"status": "PUBLISHED",
|
||||
"commit_oid": commit_oid,
|
||||
"authority_oid": evidence.authority_oid,
|
||||
"gitea_git_oid": evidence.gitea_git_oid,
|
||||
"gitea_api_oid": evidence.gitea_api_oid,
|
||||
"commit_visible": evidence.commit_visible,
|
||||
}
|
||||
finally:
|
||||
if projected:
|
||||
self.credentials.revoke()
|
||||
@@ -1,93 +0,0 @@
|
||||
"""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)
|
||||
@@ -1,80 +0,0 @@
|
||||
"""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))
|
||||
@@ -1,59 +0,0 @@
|
||||
"""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)
|
||||
@@ -1,13 +1,25 @@
|
||||
"""Durable governed task binding for CREATE_NEW and CONTINUE_EXISTING."""
|
||||
"""Exact-base governed CREATE_NEW task bootstrap using the existing JobStore."""
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass, asdict
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime, timezone, timedelta
|
||||
import hashlib
|
||||
import json
|
||||
from pathlib import Path
|
||||
import re
|
||||
from pathlib import Path
|
||||
from typing import Any, Mapping
|
||||
|
||||
from aios_authority_git_executor import AuthorityGitExecutor
|
||||
from aios_continuation_executor import (
|
||||
AuthorityGitAdapter,
|
||||
ContinuationExecutor,
|
||||
ContinuationPermissions,
|
||||
ContinuationRuntimeBinding,
|
||||
DetachedGitWorkspace,
|
||||
GiteaContinuationRemote,
|
||||
ServerSideCredentialProjection,
|
||||
)
|
||||
from aios_continuation_lease import WriterLeaseRequestV2
|
||||
from aios_gitops_job_controller import GitOpsJobController, JobStore
|
||||
|
||||
OID_RE = re.compile(r"^[0-9a-f]{40}$")
|
||||
@@ -27,58 +39,218 @@ class GovernedTaskBinding:
|
||||
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}
|
||||
return {
|
||||
"task_id": self.task_id,
|
||||
"repository": self.repository,
|
||||
"base_oid": self.base_oid,
|
||||
"target_branch": self.target_branch,
|
||||
"expected_branch_oid": self.base_oid,
|
||||
"generation": 1,
|
||||
"branch_mode": "CREATE_NEW",
|
||||
"create_target_branch": True,
|
||||
"worktree_mode": "detached_at_base_oid",
|
||||
"cas_ref_update_required": True,
|
||||
"idempotent": idempotent,
|
||||
"state": "BOUND",
|
||||
"job_id": self.job_id,
|
||||
}
|
||||
|
||||
|
||||
def _safe(value: str) -> str:
|
||||
def _safe_task(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 __init__(
|
||||
self,
|
||||
authority: AuthorityGitExecutor,
|
||||
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 = root
|
||||
self.remote_name = remote_name
|
||||
self.token_path = token_path
|
||||
self.ssh_key_path = ssh_key_path
|
||||
self.api_base = api_base
|
||||
self.task_meta_root = root / "task-bindings"
|
||||
self.job_root = root / "jobs"
|
||||
self.worktree_root = root / "worktrees"
|
||||
self.task_meta_root.mkdir(parents=True, exist_ok=True)
|
||||
self.job_root.mkdir(parents=True, exist_ok=True)
|
||||
self.worktree_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)
|
||||
task_root = self.job_root / _safe_task(task_id)
|
||||
store = JobStore(task_root, payload_root=task_root / "payloads")
|
||||
return store, GitOpsJobController(store)
|
||||
|
||||
def _metadata(self, task_id: str) -> Path:
|
||||
return self.task_meta_root / (_safe(task_id) + ".json")
|
||||
return self.task_meta_root / (_safe_task(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")))
|
||||
if not path.is_file() or path.is_symlink():
|
||||
return None
|
||||
raw = json.loads(path.read_text(encoding="utf-8"))
|
||||
return GovernedTaskBinding(
|
||||
task_id=raw["task_id"],
|
||||
repository=raw["repository"],
|
||||
base_oid=raw["base_oid"],
|
||||
target_branch=raw["target_branch"],
|
||||
risk=raw["risk"],
|
||||
description=raw["description"],
|
||||
idempotency_key=raw["idempotency_key"],
|
||||
job_id=raw.get("job_id"),
|
||||
)
|
||||
|
||||
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 _write_metadata(self, binding: GovernedTaskBinding) -> None:
|
||||
path = self._metadata(binding.task_id)
|
||||
temporary = path.with_suffix(".tmp")
|
||||
temporary.write_text(
|
||||
json.dumps(
|
||||
{
|
||||
"task_id": binding.task_id,
|
||||
"repository": binding.repository,
|
||||
"base_oid": binding.base_oid,
|
||||
"target_branch": binding.target_branch,
|
||||
"risk": binding.risk,
|
||||
"description": binding.description,
|
||||
"idempotency_key": binding.idempotency_key,
|
||||
"job_id": binding.job_id,
|
||||
},
|
||||
sort_keys=True,
|
||||
),
|
||||
encoding="utf-8",
|
||||
)
|
||||
temporary.replace(path)
|
||||
|
||||
def _pending_create_new_oid(self, task_id: str, branch: str) -> str | None:
|
||||
"""Return the exact local OID only for an unfinished CREATE_NEW job."""
|
||||
store, _ = self._store(task_id)
|
||||
with store._connect() as connection:
|
||||
rows = connection.execute(
|
||||
"SELECT operation,state,receipt_json FROM jobs WHERE branch=? ORDER BY updated_at DESC",
|
||||
(branch,),
|
||||
).fetchall()
|
||||
for row in rows:
|
||||
if row["operation"] != "CONTINUATION_COMMIT_FILES_CREATE_NEW" or row["state"] != "RECONCILIATION_REQUIRED":
|
||||
continue
|
||||
receipt = json.loads(row["receipt_json"] or "{}")
|
||||
desired = receipt.get("desired_remote_oid")
|
||||
if isinstance(desired, str) and OID_RE.fullmatch(desired):
|
||||
return desired
|
||||
return None
|
||||
|
||||
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")
|
||||
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"]) or values["target_branch"] in {"main", "master"}:
|
||||
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")
|
||||
for key in ("repository", "base_oid", "target_branch", "risk", "description"):
|
||||
if getattr(existing, key) != values[key]:
|
||||
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")
|
||||
if not self.authority.commit_exists(values["base_oid"]):
|
||||
raise BootstrapError("BASE_COMMIT_NOT_FOUND")
|
||||
if self.authority.read_ref("refs/heads/" + values["target_branch"]) is not None:
|
||||
raise BootstrapError("CONFLICT")
|
||||
payload = json.dumps(values, sort_keys=True, separators=(",", ":")).encode()
|
||||
_, controller = self._store(values["task_id"])
|
||||
receipt = controller.submit({"idempotency_key": values["idempotency_key"], "campaign_id": values["task_id"], "task_id": values["task_id"], "repository": values["repository"], "operation": "GOVERNED_TASK_CREATE", "branch": values["target_branch"], "expected_branch_oid": expected, "payload_digest": hashlib.sha256(payload).hexdigest(), "lineage_anchor_oid": values["base_oid"]}, payload=payload)
|
||||
binding = GovernedTaskBinding(**values, expected_branch_oid=expected, branch_mode=mode, generation=generation, job_id=receipt.job_id)
|
||||
self._write(binding)
|
||||
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": values["base_oid"],
|
||||
"payload_digest": hashlib.sha256(payload).hexdigest(),
|
||||
"lineage_anchor_oid": values["base_oid"],
|
||||
},
|
||||
payload=payload,
|
||||
)
|
||||
binding = GovernedTaskBinding(**values, job_id=receipt.job_id)
|
||||
self._write_metadata(binding)
|
||||
return binding.response()
|
||||
|
||||
def prepare_runtime(self, request: Mapping[str, Any], *, lease_seconds: int):
|
||||
task = self._read_existing(str(request["task_id"]))
|
||||
if task is None:
|
||||
raise BootstrapError("TASK_NOT_FOUND")
|
||||
if str(request["repository"]) != task.repository or str(request["base_oid"]) != task.base_oid:
|
||||
raise BootstrapError("TASK_BINDING_CONFLICT")
|
||||
if str(request["target_branch"]) != task.target_branch:
|
||||
raise BootstrapError("TASK_BINDING_CONFLICT")
|
||||
if request.get("branch_mode") != "CREATE_NEW" or int(request.get("generation", 0)) != 1:
|
||||
raise BootstrapError("CREATE_NEW_LEASE_INVALID")
|
||||
if not self.authority.commit_exists(task.base_oid):
|
||||
raise BootstrapError("BASE_COMMIT_NOT_FOUND")
|
||||
existing_target = self.authority.read_ref("refs/heads/" + task.target_branch)
|
||||
recovery_oid = None
|
||||
if existing_target is not None:
|
||||
recovery_oid = self._pending_create_new_oid(task.task_id, task.target_branch)
|
||||
if recovery_oid != existing_target:
|
||||
raise BootstrapError("CONFLICT")
|
||||
workspace_path = self.worktree_root / _safe_task(task.task_id)
|
||||
if workspace_path.exists():
|
||||
if workspace_path.is_symlink():
|
||||
raise BootstrapError("WORKTREE_INVALID")
|
||||
workspace = DetachedGitWorkspace(workspace_path)
|
||||
allowed_heads = {task.base_oid}
|
||||
if recovery_oid is not None:
|
||||
allowed_heads.add(recovery_oid)
|
||||
if not workspace.is_detached() or workspace.head_oid() not in allowed_heads:
|
||||
raise BootstrapError("WORKTREE_HEAD_MISMATCH")
|
||||
else:
|
||||
self.authority.create_detached_worktree(task.base_oid, _safe_task(task.task_id), self.worktree_root)
|
||||
workspace = DetachedGitWorkspace(workspace_path)
|
||||
request_v2 = WriterLeaseRequestV2(
|
||||
task_id=task.task_id,
|
||||
repository=task.repository,
|
||||
branch_mode="CREATE_NEW",
|
||||
base_oid=task.base_oid,
|
||||
expected_branch_oid=task.base_oid,
|
||||
target_branch=task.target_branch,
|
||||
generation=1,
|
||||
lease_seconds=lease_seconds,
|
||||
)
|
||||
_, controller = self._store(task.task_id)
|
||||
credential = ServerSideCredentialProjection(self.ssh_key_path, self.token_path)
|
||||
remote = GiteaContinuationRemote(
|
||||
workspace=workspace_path,
|
||||
credential=credential,
|
||||
remote_name=self.remote_name,
|
||||
repository=task.repository,
|
||||
api_base=self.api_base,
|
||||
)
|
||||
executor = ContinuationExecutor(controller, AuthorityGitAdapter(self.authority), workspace, remote, credential)
|
||||
binding = ContinuationRuntimeBinding(
|
||||
request=request_v2,
|
||||
workspace_path=workspace_path,
|
||||
lease_not_before=datetime.now(timezone.utc) - timedelta(seconds=1),
|
||||
lease_expires_at=datetime.now(timezone.utc) + timedelta(seconds=lease_seconds),
|
||||
original_receipt_valid=True,
|
||||
original_grant_consumed=True,
|
||||
permissions=ContinuationPermissions(create_target_branch=True),
|
||||
)
|
||||
return binding, controller, executor
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,84 +0,0 @@
|
||||
#!/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
|
||||
|
||||
@@ -1,51 +0,0 @@
|
||||
[Unit]
|
||||
Description=AIOS GitOps Model API V2R2C1 immutable staging runtime
|
||||
After=network-online.target
|
||||
Wants=network-online.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=aiman
|
||||
Group=gitops
|
||||
WorkingDirectory=@RUNTIME_ROOT@
|
||||
Environment=PYTHONUNBUFFERED=1
|
||||
Environment=PYTHONDONTWRITEBYTECODE=1
|
||||
Environment=AIOS_GITOPS_ROOT=/var/lib/aios-gitops
|
||||
Environment=AIOS_GITOPS_WORKSPACE=@RUNTIME_ROOT@
|
||||
Environment=AIOS_GITOPS_REPO=/data/aios-gitops/repo.git
|
||||
Environment=AIOS_GITOPS_BARE_REPO=/data/aios-gitops/repo.git
|
||||
Environment=AIOS_GITOPS_AUTHORITATIVE_GIT_DIR=/data/aios-gitops/repo.git
|
||||
Environment=AIOS_GITOPS_REPOSITORY_IDENTITY=aios/aios
|
||||
Environment=AIOS_GITOPS_BASELINE_AUTHORITY=/var/lib/aios-gitops-runtime-anchor/baseline/current.json
|
||||
Environment=AIOS_GITOPS_WRITES_ENABLED=false
|
||||
Environment=AIOS_GITOPS_DURABLE_JOB_EXECUTION_ENABLED=false
|
||||
Environment=AIOS_GITOPS_JOB_CONTROLLER_ENABLED=false
|
||||
Environment=AIOS_GITOPS_WORKSPACE_ROLE=runtime-anchor
|
||||
Environment=AIOS_GITOPS_RUNTIME_OID=@SOURCE_OID@
|
||||
Environment=AIOS_GITOPS_ARTIFACT_SHA256=@ARTIFACT_SHA256@
|
||||
Environment=AIOS_GITOPS_DEPLOYMENT_STATE=STAGING
|
||||
Environment=AIOS_GITOPS_MODEL_API_SERVICE=aios-gitops-model-api-v2r2c1-staging.service
|
||||
Environment=AIOS_SILENT_RELEASE_EXECUTION_ENABLED=false
|
||||
Environment=AIOS_SILENT_RELEASE_STATE_ROOT=/var/lib/aios-gitops/model-api-staging/silent-release
|
||||
Environment=AIOS_BRIDGE_V2_URL=http://127.0.0.1:18892
|
||||
Environment=AIOS_GITOPS_HOST=127.0.0.1
|
||||
Environment=AIOS_GITOPS_PORT=18893
|
||||
Environment=PYTHONPATH=@RUNTIME_ROOT@/app:@RUNTIME_ROOT@/runtime-agent-os
|
||||
ExecStart=/usr/bin/python3 @RUNTIME_ROOT@/app/aios_model_api_staging.py
|
||||
Restart=on-failure
|
||||
RestartSec=2
|
||||
NoNewPrivileges=true
|
||||
PrivateTmp=true
|
||||
ProtectHome=true
|
||||
ProtectSystem=strict
|
||||
ProtectKernelTunables=true
|
||||
ProtectKernelModules=true
|
||||
ProtectControlGroups=true
|
||||
LockPersonality=true
|
||||
RestrictSUIDSGID=true
|
||||
RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6
|
||||
ReadOnlyPaths=@RUNTIME_ROOT@ /data/aios-gitops/repo.git /var/lib/aios-gitops-runtime-anchor/baseline
|
||||
ReadWritePaths=/var/lib/aios-gitops/model-api-staging
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
@@ -1,6 +1,20 @@
|
||||
# Merge this fragment into docs/gitops/openapi-chatgpt-actions.yaml.
|
||||
components:
|
||||
schemas:
|
||||
WriterLeaseRequestV2:
|
||||
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, const: aios.task-writer-lease.v2}
|
||||
task_id: {type: string}
|
||||
repository: {type: string, const: aios/aios}
|
||||
branch_mode: {type: string, const: 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/'}
|
||||
generation: {type: integer, minimum: 1}
|
||||
lease_seconds: {type: integer, minimum: 1, maximum: 86400}
|
||||
AuthorizedTaskWriterProvisionRequest:
|
||||
type: object
|
||||
additionalProperties: false
|
||||
|
||||
@@ -13,6 +13,55 @@ components:
|
||||
scheme: bearer
|
||||
bearerFormat: opaque-token
|
||||
schemas:
|
||||
TaskAdmissionEnvelopeRequest:
|
||||
type: object
|
||||
additionalProperties: false
|
||||
required:
|
||||
- idempotency_key
|
||||
- task_id
|
||||
- repository
|
||||
- authority_base_oid
|
||||
- target_branch
|
||||
- generation
|
||||
- execution_digest
|
||||
- requested_capabilities
|
||||
- file_allowlist
|
||||
- risk
|
||||
properties:
|
||||
idempotency_key: {type: string, minLength: 8, maxLength: 256}
|
||||
task_id: {type: string, minLength: 3, maxLength: 180}
|
||||
repository: {type: string, enum: [aios/aios]}
|
||||
authority_base_oid: {type: string, pattern: '^[0-9a-f]{40}$'}
|
||||
target_branch: {type: string, pattern: '^agent/.+'}
|
||||
generation: {type: integer, minimum: 1}
|
||||
execution_digest: {type: string, pattern: '^[0-9a-f]{64}$'}
|
||||
requested_capabilities:
|
||||
type: array
|
||||
minItems: 1
|
||||
uniqueItems: true
|
||||
items: {type: string}
|
||||
file_allowlist:
|
||||
type: array
|
||||
minItems: 1
|
||||
uniqueItems: true
|
||||
items: {type: string}
|
||||
risk: {type: string, enum: [low, medium]}
|
||||
expires_in_seconds:
|
||||
type: integer
|
||||
minimum: 60
|
||||
maximum: 900
|
||||
TaskAdmissionAuthorizationRequest:
|
||||
type: object
|
||||
additionalProperties: false
|
||||
required: [envelope_id, authorization_kind]
|
||||
properties:
|
||||
envelope_id: {type: string}
|
||||
authorization_kind:
|
||||
type: string
|
||||
enum: [delegation_epoch, owner_signature]
|
||||
delegation_epoch_id: {type: string}
|
||||
owner_key_id: {type: string}
|
||||
signature_base64: {type: string}
|
||||
PublicationState:
|
||||
type: string
|
||||
description: Canonical durable publication state. Legacy TAG_CREATED_LOCAL is normalized on read and is never emitted by new writes.
|
||||
@@ -96,7 +145,7 @@ components:
|
||||
message: {type: string}
|
||||
TagRequest:
|
||||
type: object
|
||||
required: [version, target_oid, evidence_closure_oid, approval_plan_id]
|
||||
required: [version, target_oid, evidence_closure_oid, approval_plan_id, execute]
|
||||
properties:
|
||||
version: {type: string, example: V1R1C1}
|
||||
target_oid:
|
||||
@@ -241,6 +290,122 @@ components:
|
||||
properties:
|
||||
candidate_id: {type: string}
|
||||
paths:
|
||||
/v2/task-admission/envelope:
|
||||
post:
|
||||
operationId: getTaskAdmissionEnvelope
|
||||
summary: Build and persist a canonical narrow Task admission envelope
|
||||
description: >
|
||||
Authenticated transport only. This operation does not mint a TaskPrincipal,
|
||||
acquire a WriterLease, or mutate Git. It normalizes and persists one exact
|
||||
admission envelope for later delegation/Owner authorization.
|
||||
security: [{bearerAuth: []}]
|
||||
x-openai-isConsequential: false
|
||||
x-aios-governed-task-admission: true
|
||||
x-aios-no-shell: true
|
||||
x-aios-no-git-write: true
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema: {$ref: '#/components/schemas/TaskAdmissionEnvelopeRequest'}
|
||||
responses:
|
||||
'200': {description: Canonical envelope and signing payload}
|
||||
'401': {description: Authenticated transport required}
|
||||
'403': {description: Requested scope exceeds bootstrap policy}
|
||||
'409': {description: Idempotency key conflicts with a different request}
|
||||
/v2/task-admission/authorize:
|
||||
post:
|
||||
operationId: submitTaskAdmissionAuthorization
|
||||
summary: Verify durable delegation or Owner signature and admit one exact Task
|
||||
description: >
|
||||
The bearer authenticates the transport caller but is not task authority.
|
||||
Authorization is proven only by a verified OwnerDelegationEpoch or exact
|
||||
Owner signature. Successful admission may persist an exact Task receipt in
|
||||
QUEUED, CLAIMED, or RUNNING state. It does not enable legacy/global Git
|
||||
writes and cannot accept a caller-supplied TaskPrincipal.
|
||||
security: [{bearerAuth: []}]
|
||||
x-openai-isConsequential: true
|
||||
x-aios-governed-task-admission: true
|
||||
x-aios-no-shell: true
|
||||
x-aios-no-git-write: true
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema: {$ref: '#/components/schemas/TaskAdmissionAuthorizationRequest'}
|
||||
responses:
|
||||
'200': {description: Exact admitted Task receipt or idempotent replay}
|
||||
'401': {description: Authenticated transport required}
|
||||
'403': {description: Invalid, expired, revoked, widened, or unbound authorization}
|
||||
'409': {description: Conflicting idempotent admission}
|
||||
'503': {description: Task state or authorization backend unavailable}
|
||||
/v2/task-admission/status:
|
||||
get:
|
||||
operationId: getTaskAdmissionStatus
|
||||
summary: Read durable Task-admission state
|
||||
description: >
|
||||
Authenticated bounded lookup of one admission envelope. This is status only
|
||||
and confers no TaskPrincipal, WriterLease, or execution capability.
|
||||
security: [{bearerAuth: []}]
|
||||
x-openai-isConsequential: false
|
||||
x-aios-governed-task-admission: true
|
||||
x-aios-no-shell: true
|
||||
x-aios-read-only: true
|
||||
parameters:
|
||||
- in: query
|
||||
name: envelope_id
|
||||
required: true
|
||||
schema: {type: string, minLength: 1, maxLength: 256}
|
||||
responses:
|
||||
'200': {description: Pending or admitted receipt}
|
||||
'401': {description: Authenticated transport required}
|
||||
'404': {description: Envelope not found}
|
||||
|
||||
/createTagApprovalRequest:
|
||||
post:
|
||||
operationId: createTagApprovalRequest
|
||||
summary: Record the exact Owner-approved release binding without executing it
|
||||
security: [{bearerAuth: []}]
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
additionalProperties: false
|
||||
required: [version, target_oid, evidence_closure_oid, approval_plan_id, owner_approval, execute]
|
||||
responses: {'200': {description: Durable execute=false approval receipt}}
|
||||
x-openai-isConsequential: true
|
||||
/buildProductionArtifact:
|
||||
post:
|
||||
operationId: buildProductionArtifact
|
||||
summary: Build the deterministic production artifact for an exact approved binding
|
||||
security: [{bearerAuth: []}]
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
additionalProperties: false
|
||||
required: [version, target_oid, evidence_closure_oid, approval_plan_id]
|
||||
responses: {'200': {description: Artifact digest and closure receipt}}
|
||||
x-openai-isConsequential: true
|
||||
/executeApprovedProductionRelease:
|
||||
post:
|
||||
operationId: executeApprovedProductionRelease
|
||||
summary: Execute the exact approved production release after all release gates pass
|
||||
security: [{bearerAuth: []}]
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
additionalProperties: false
|
||||
required: [version, target_oid, evidence_closure_oid, approval_plan_id, execute]
|
||||
responses: {'200': {description: Production release result}}
|
||||
x-openai-isConsequential: true
|
||||
/createReleaseIntent:
|
||||
post:
|
||||
operationId: createReleaseIntent
|
||||
@@ -490,6 +655,14 @@ paths:
|
||||
summary: Read durable publication Saga status without mutation
|
||||
parameters:
|
||||
- {in: query, name: version, required: true, schema: {type: string, pattern: '^V\d+R\d+C\d+(?:-[A-Za-z0-9_.-]+)?$'}}
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
additionalProperties: false
|
||||
required: [version]
|
||||
responses:
|
||||
'200':
|
||||
description: Canonical publication state or fail-closed UNKNOWN
|
||||
@@ -506,6 +679,14 @@ paths:
|
||||
- {in: query, name: target_oid, required: true, schema: {type: string, pattern: '^[0-9a-fA-F]{40}$'}}
|
||||
- {in: query, name: evidence_closure_oid, required: true, schema: {type: string, pattern: '^[0-9a-fA-F]{40}$'}}
|
||||
- {in: query, name: approval_plan_id, required: true, schema: {type: string}}
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
additionalProperties: false
|
||||
required: [version, target_oid, evidence_closure_oid, approval_plan_id]
|
||||
responses: {'200': {description: PASS, FAIL, or fail-closed UNKNOWN}}
|
||||
/api/release-acceptance:
|
||||
get:
|
||||
@@ -516,6 +697,14 @@ paths:
|
||||
- {in: query, name: target_oid, required: true, schema: {type: string, pattern: '^[0-9a-fA-F]{40}$'}}
|
||||
- {in: query, name: evidence_closure_oid, required: true, schema: {type: string, pattern: '^[0-9a-fA-F]{40}$'}}
|
||||
- {in: query, name: approval_plan_id, required: true, schema: {type: string}}
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
additionalProperties: false
|
||||
required: [version, target_oid, evidence_closure_oid, approval_plan_id]
|
||||
responses: {'200': {description: Acceptance result with accepted boolean}}
|
||||
/api/tag:
|
||||
post:
|
||||
@@ -526,7 +715,10 @@ paths:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema: {$ref: '#/components/schemas/TagRequest'}
|
||||
schema:
|
||||
type: object
|
||||
additionalProperties: false
|
||||
required: [version, target_oid, evidence_closure_oid, approval_plan_id, execute]
|
||||
responses: {'200': {description: Gitea-confirmed tag and fail-closed baseline result}, '400': {description: Missing governance evidence or invalid OID}, '403': {description: Forbidden}, '409': {description: Existing tag target conflict or evidence lineage mismatch}, '502': {description: Gitea reconciliation required}, '503': {description: Remote push pending}}
|
||||
x-openai-isConsequential: true
|
||||
/api/rollback:
|
||||
@@ -567,3 +759,48 @@ paths:
|
||||
'200': {description: Safe GitOps plan}
|
||||
'401': {description: Unauthorized}
|
||||
x-openai-isConsequential: true
|
||||
/api/v2/governed-capabilities:
|
||||
get:
|
||||
operationId: getGovernedWriteCapabilities
|
||||
responses: {'200': {description: Governed write capabilities with legacy writes disabled}}
|
||||
/api/v2/governed-task:
|
||||
post:
|
||||
operationId: createGovernedTask
|
||||
requestBody: {required: true, content: {application/json: {schema: {type: object, additionalProperties: false, required: [task_id, repository, base_oid, target_branch, risk, description, idempotency_key]}}}}
|
||||
responses: {'200': {description: Exact governed task binding}}
|
||||
x-openai-isConsequential: true
|
||||
/api/v2/writer-lease:
|
||||
post:
|
||||
operationId: acquireWriterLease
|
||||
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]}}}}
|
||||
responses: {'200': {description: Detached CONTINUE_EXISTING lease}}
|
||||
x-openai-isConsequential: true
|
||||
/api/v2/commit-file:
|
||||
post:
|
||||
operationId: commitFileGoverned
|
||||
requestBody: {required: true, content: {application/json: {schema: {type: object, additionalProperties: false, required: [task_id, repository, expected_branch_oid, target_branch, generation, idempotency_key, path, content_base64, message]}}}}
|
||||
responses: {'202': {description: Durable governed Job}}
|
||||
x-openai-isConsequential: true
|
||||
/api/v2/commit-files:
|
||||
post:
|
||||
operationId: commitFilesGoverned
|
||||
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]}}}}
|
||||
responses: {'202': {description: One atomic multi-file governed Job}}
|
||||
x-openai-isConsequential: true
|
||||
/api/v2/job:
|
||||
post:
|
||||
operationId: getGitOpsJob
|
||||
requestBody: {required: true, content: {application/json: {schema: {type: object, additionalProperties: false, required: [job_id]}}}}
|
||||
responses: {'200': {description: Non-sensitive Job metadata}}
|
||||
/api/v2/job/resume:
|
||||
post:
|
||||
operationId: resumeGitOpsJob
|
||||
requestBody: {required: true, content: {application/json: {schema: {type: object, additionalProperties: false, required: [job_id, expected_revision]}}}}
|
||||
responses: {'202': {description: Revision-CAS resume}}
|
||||
x-openai-isConsequential: true
|
||||
/api/v2/job/reconcile:
|
||||
post:
|
||||
operationId: reconcileGitOpsJob
|
||||
requestBody: {required: true, content: {application/json: {schema: {type: object, additionalProperties: false, required: [job_id, expected_revision]}}}}
|
||||
responses: {'202': {description: Observation-first reconciliation}}
|
||||
x-openai-isConsequential: true
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
[pytest]
|
||||
testpaths = tests
|
||||
python_files = test_*.py
|
||||
addopts = -ra --import-mode=importlib
|
||||
@@ -1,243 +0,0 @@
|
||||
{
|
||||
"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
|
||||
}
|
||||
}
|
||||
@@ -1,89 +0,0 @@
|
||||
# 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.
|
||||
@@ -1,254 +0,0 @@
|
||||
{
|
||||
"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"
|
||||
}
|
||||
@@ -1,37 +0,0 @@
|
||||
{
|
||||
"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,44 @@
|
||||
{
|
||||
"schema": "aios.bridge-v2-engineering-staging-acceptance.v1",
|
||||
"task_id": "TASK-P0-CHATGPT-BRIDGE-V2-CONTROL-PLANE-CONVERGENCE-20260809",
|
||||
"repository": "aios/aios",
|
||||
"branch": "agent/p0-chatgpt-bridge-v2-control-plane-convergence-20260809",
|
||||
"control_plane_release_version": "bridge-v2-aa0c37b8ab33",
|
||||
"control_plane_target_oid": "aa0c37b8ab333616e1bab9686cf6d10557f2ebda",
|
||||
"artifact_sha256": "08670c78cf760bfe693e3553e676eb751dadd7834116aa6d40e7c094f1357ae4",
|
||||
"runtime_path": "/opt/aios/gitops-control-plane/aa0c37b8ab333616e1bab9686cf6d10557f2ebda",
|
||||
"bridge_schema_version": "aios.chatgpt-gitops-bridge.v2",
|
||||
"deployment_state": "STAGING",
|
||||
"drift_status": "PASS",
|
||||
"restart_acceptance": {
|
||||
"pid_before": 3158850,
|
||||
"pid_after": 3159889,
|
||||
"runtime_response_equal": true
|
||||
},
|
||||
"governed_capabilities": {
|
||||
"legacy_writes_enabled": false,
|
||||
"governed_writes_enabled": true,
|
||||
"force_push": false,
|
||||
"main_write": false,
|
||||
"tag_write": false,
|
||||
"baseline_write": false,
|
||||
"production_write": false
|
||||
},
|
||||
"production_invariants": {
|
||||
"authority_main_oid": "2e8fa22989808a448065d1d76b5b18948774a637",
|
||||
"authority_tags_sha256": "bd2b6e30b76f28e7fcdcb447e7296690f0a65ad855209294c36b0f3f1d96098d",
|
||||
"baseline_current_sha256": "57651eb819584ab7c1d67f01b0ef66d6408cdc322ae5f6d4ff0aacd96f8327e1",
|
||||
"production_service_main_pid": 2816107,
|
||||
"production_service_nrestarts": 0,
|
||||
"production_service_active_enter_timestamp_monotonic": 2919386492273
|
||||
},
|
||||
"bridge_operation_sequence": {
|
||||
"createGovernedTask": "PASS",
|
||||
"acquireWriterLease": "PASS",
|
||||
"commitFilesGoverned": "REAL_GOVERNED_COMMIT",
|
||||
"getGitOpsJob": "PASS",
|
||||
"expected_branch_oid": "8fe3b85c1be592326473e81a502a23eeb9511d30",
|
||||
"generation": 7
|
||||
},
|
||||
"production_changed": false
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"schema": "aios.bridge-v2.1-durable-reconciliation.bootstrap-acceptance.v1",
|
||||
"task_id": "TASK-P0-BRIDGE-V21-DURABLE-RECONCILIATION-20260809",
|
||||
"repository": "aios/aios",
|
||||
"base_oid": "3f670c387692b8865a10d5b419468d8a0b64d7a4",
|
||||
"target_branch": "agent/p0-bridge-v21-durable-reconciliation-20260809",
|
||||
"branch_mode": "CREATE_NEW",
|
||||
"generation": 1,
|
||||
"implementation": "not_started",
|
||||
"production_changed": false,
|
||||
"required_proofs": [
|
||||
"absent_ref_cas",
|
||||
"observation_first_reconciliation",
|
||||
"authority_gitea_api_commit_equality",
|
||||
"commit_visible"
|
||||
]
|
||||
}
|
||||
@@ -1,145 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
from __future__ import annotations
|
||||
import argparse
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import tarfile
|
||||
import tempfile
|
||||
|
||||
SCHEMA = 'aios.v2r2c1.model-api-staging-manifest.v1'
|
||||
REQUIRED_APP = (
|
||||
'aios_model_api_staging.py',
|
||||
'aios_gitops_write_guard.py',
|
||||
'authoritative_git.py',
|
||||
'baseline_authority.py',
|
||||
'gitea_tag_publication.py',
|
||||
'silent_release_controller.py',
|
||||
'silent_release_hardening.py',
|
||||
'aios_gitops_job_controller.py',
|
||||
)
|
||||
OPTIONAL_APP = (
|
||||
'aios_chatgpt_actions_api.py',
|
||||
'aios_continuation_executor.py',
|
||||
'aios_continuation_lease.py',
|
||||
'aios_control_plane_snapshot.py',
|
||||
)
|
||||
|
||||
def sha256(path: Path) -> str:
|
||||
h = hashlib.sha256()
|
||||
with path.open('rb') as fh:
|
||||
for chunk in iter(lambda: fh.read(1024 * 1024), b''):
|
||||
h.update(chunk)
|
||||
return h.hexdigest()
|
||||
|
||||
def private_marker(path: Path) -> bool:
|
||||
import re
|
||||
text = path.read_text(encoding='utf-8', errors='ignore')
|
||||
patterns = (
|
||||
r'-----BEGIN [A-Z ]*PRIVATE KEY-----',
|
||||
r'(?<![A-Za-z0-9])sk-[A-Za-z0-9_-]{20,}(?![A-Za-z0-9_-])',
|
||||
r'(?<![A-Za-z0-9])ghp_[A-Za-z0-9]{20,}(?![A-Za-z0-9])',
|
||||
r'(?i)aws_secret_access_key\s*[:=]\s*[A-Za-z0-9/+=]{16,}',
|
||||
)
|
||||
return any(re.search(pattern, text) for pattern in patterns)
|
||||
|
||||
def main() -> int:
|
||||
ap = argparse.ArgumentParser()
|
||||
ap.add_argument('--source-root', required=True, type=Path)
|
||||
ap.add_argument('--output-root', required=True, type=Path)
|
||||
ap.add_argument('--source-oid', required=True)
|
||||
args = ap.parse_args()
|
||||
oid = args.source_oid.lower()
|
||||
if len(oid) != 40 or any(c not in '0123456789abcdef' for c in oid):
|
||||
raise SystemExit('source oid must be exact 40 hex')
|
||||
source = args.source_root.resolve(strict=True)
|
||||
output = args.output_root.resolve()
|
||||
app = source / 'active-runtime' / 'app'
|
||||
runtime = source / 'runtime' / 'agent-os'
|
||||
docs = source / 'docs' / 'gitops'
|
||||
if not app.is_dir():
|
||||
raise SystemExit('active-runtime/app missing')
|
||||
paths = []
|
||||
for name in REQUIRED_APP:
|
||||
path = app / name
|
||||
if not path.is_file():
|
||||
raise SystemExit(f'required app dependency missing: {name}')
|
||||
paths.append((Path('app') / name, path))
|
||||
for name in OPTIONAL_APP:
|
||||
path = app / name
|
||||
if path.is_file():
|
||||
paths.append((Path('app') / name, path))
|
||||
for rel in ('orchestrator/__init__.py', 'orchestrator/action_monitor.py'):
|
||||
path = runtime / rel
|
||||
if not path.is_file():
|
||||
raise SystemExit(f'required runtime dependency missing: {rel}')
|
||||
paths.append((Path('runtime-agent-os') / rel, path))
|
||||
openapi = docs / 'openapi-chatgpt-actions.yaml'
|
||||
if openapi.is_file():
|
||||
paths.append((Path('docs') / 'gitops' / 'openapi-chatgpt-actions.yaml', openapi))
|
||||
for folder in ('config/model_registry', 'config/model_priority', 'patches', 'versions'):
|
||||
root = source / folder
|
||||
if root.is_dir():
|
||||
for path in sorted(root.rglob('*')):
|
||||
if path.is_file() and path.suffix.lower() in {'.json', '.yaml', '.yml', '.md'}:
|
||||
paths.append((Path(folder) / path.relative_to(root), path))
|
||||
for rel, path in paths:
|
||||
if private_marker(path):
|
||||
raise SystemExit(f'private marker rejected: {rel}')
|
||||
included = [str(rel).replace('\\', '/') for rel, _ in paths]
|
||||
manifest = {
|
||||
'schema': SCHEMA,
|
||||
'target_version': 'V2R2C1',
|
||||
'source_oid': oid,
|
||||
'repository_identity': 'aios/aios',
|
||||
'deployment_state': 'STAGING',
|
||||
'runtime_mode': 'IMMUTABLE_STAGING',
|
||||
'runtime_entrypoint': 'app/aios_model_api_staging.py',
|
||||
'legacy_global_writes_enabled': False,
|
||||
'governed_release_routing': 'bridge-v2',
|
||||
'artifact_sha256': 'provided-by-build-receipt-and-install-manifest',
|
||||
'included_paths': included,
|
||||
}
|
||||
output.mkdir(parents=True, exist_ok=True)
|
||||
archive = output / f'aios-v2r2c1-model-api-{oid}.tar.gz'
|
||||
with tempfile.TemporaryDirectory(prefix='v2r2c1-model-api-', dir=str(output)) as tmp:
|
||||
root = Path(tmp) / 'runtime'
|
||||
for rel, path in paths:
|
||||
target = root / rel
|
||||
target.parent.mkdir(parents=True, exist_ok=True)
|
||||
target.write_bytes(path.read_bytes())
|
||||
(root / 'manifest.json').write_text(json.dumps(manifest, ensure_ascii=False, indent=2, sort_keys=True) + '\n', encoding='utf-8')
|
||||
with tarfile.open(archive, 'w:gz', format=tarfile.PAX_FORMAT) as tf:
|
||||
for path in sorted(root.rglob('*')):
|
||||
rel = path.relative_to(root.parent)
|
||||
info = tf.gettarinfo(str(path), arcname=str(rel).replace('\\', '/'))
|
||||
info.mtime = 0
|
||||
info.uid = 0
|
||||
info.gid = 0
|
||||
info.uname = 'root'
|
||||
info.gname = 'root'
|
||||
if path.is_file():
|
||||
with path.open('rb') as fh:
|
||||
tf.addfile(info, fh)
|
||||
else:
|
||||
tf.addfile(info)
|
||||
digest = sha256(archive)
|
||||
receipt = {
|
||||
'schema': 'aios.v2r2c1.model-api-staging-build-receipt.v1',
|
||||
'target_version': 'V2R2C1',
|
||||
'source_oid': oid,
|
||||
'repository_identity': 'aios/aios',
|
||||
'artifact_path': str(archive),
|
||||
'artifact_sha256': digest,
|
||||
'deployment_state': 'STAGING',
|
||||
'manifest_schema': SCHEMA,
|
||||
'included_paths': included,
|
||||
}
|
||||
receipt_path = output / f'aios-v2r2c1-model-api-{oid}.receipt.json'
|
||||
receipt_path.write_text(json.dumps(receipt, ensure_ascii=False, indent=2, sort_keys=True) + '\n', encoding='utf-8')
|
||||
print(json.dumps(receipt, ensure_ascii=False, indent=2, sort_keys=True))
|
||||
return 0
|
||||
|
||||
if __name__ == '__main__':
|
||||
raise SystemExit(main())
|
||||
@@ -3,13 +3,13 @@ from __future__ import annotations
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
ROOT = Path(__file__).parents[2]
|
||||
|
||||
|
||||
def test_version_and_candidate_binding():
|
||||
assert (ROOT / "VERSION").read_text(encoding="utf-8").strip() == "V2R1C7"
|
||||
assert (ROOT / "VERSION").read_text(encoding="utf-8").strip() == "V2R1C6"
|
||||
metadata = json.loads((ROOT / "versions/V2R1C6.json").read_text(encoding="utf-8"))
|
||||
assert metadata["version"] == "V2R1C6"
|
||||
assert metadata["base_oid"] == "a33e2593967a180c89d44454350ccaeaf3f79b25"
|
||||
assert metadata["candidate_branch"] == "agent/v2r1c6-mainline-reconciliation-20260806"
|
||||
assert metadata["production_deployed"] is False
|
||||
|
||||
@@ -7,19 +7,17 @@ import pytest
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
sys.path.insert(0, str(ROOT / "active-runtime" / "app"))
|
||||
from deployment_state import RuntimeDeploymentState
|
||||
from v2r1c6_privacy_tools import health_payload, serve # noqa: E402
|
||||
|
||||
|
||||
def test_health_is_engineering_only() -> None:
|
||||
result = health_payload(RuntimeDeploymentState("V2R1C7", "a" * 40, "b" * 64, "STAGED"))
|
||||
assert result["status"] == "ok"
|
||||
assert result["version"] == "V2R1C7"
|
||||
assert result["commit_oid"] == "a" * 40
|
||||
assert result["artifact_sha256"] == "b" * 64
|
||||
assert result["deployment_state"] == "STAGED"
|
||||
assert result["production_deployed"] is False
|
||||
assert result["health_timestamp"]
|
||||
result = health_payload()
|
||||
assert result == {
|
||||
"status": "ok",
|
||||
"version": "V2R1C6",
|
||||
"production_deployed": False,
|
||||
"gitops_writes_enabled": False,
|
||||
}
|
||||
|
||||
|
||||
def test_listener_rejects_non_loopback() -> None:
|
||||
|
||||
@@ -1,65 +0,0 @@
|
||||
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",
|
||||
)
|
||||
@@ -264,7 +264,7 @@ class TagActionTests(unittest.TestCase):
|
||||
ROOT / "docs" / "gitops" / "openapi-chatgpt-actions.yaml"
|
||||
).read_text(encoding="utf-8")
|
||||
self.assertIn(
|
||||
"required: [version, target_oid, evidence_closure_oid, approval_plan_id]",
|
||||
"required: [version, target_oid, evidence_closure_oid, approval_plan_id, execute]",
|
||||
schema,
|
||||
)
|
||||
self.assertIn("x-openai-isConsequential: true", schema)
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import json
|
||||
from pathlib import Path
|
||||
import tempfile
|
||||
|
||||
import pytest
|
||||
import yaml
|
||||
|
||||
from aios_chatgpt_bridge_v2 import (
|
||||
BRIDGE_OPERATIONS,
|
||||
BRIDGE_SCHEMA_VERSION,
|
||||
OPERATION_REQUIRED_FIELDS,
|
||||
BridgeV2Adapter,
|
||||
ControlPlaneReleaseManifest,
|
||||
RuntimeDriftError,
|
||||
)
|
||||
|
||||
|
||||
ROOT = Path(__file__).parents[1]
|
||||
|
||||
|
||||
def test_immutable_release_identity_accepts_exact_artifact_and_rejects_workspace_drift():
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
approved = Path(directory) / "releases"
|
||||
runtime = approved / "release-1"
|
||||
runtime.mkdir(parents=True)
|
||||
artifact = runtime / "bridge.pyz"
|
||||
artifact.write_bytes(b"bridge-v2")
|
||||
digest = hashlib.sha256(artifact.read_bytes()).hexdigest()
|
||||
manifest = ControlPlaneReleaseManifest(
|
||||
"bridge-v2-test", "a" * 40, digest, runtime, BRIDGE_SCHEMA_VERSION,
|
||||
"aios/aios", Path("/data/aios-gitops/repo.git"), "STAGING",
|
||||
)
|
||||
assert manifest.validate(artifact, observed_oid="a" * 40, approved_root=approved)["drift_status"] == "PASS"
|
||||
with pytest.raises(RuntimeDriftError, match="CONTROL_PLANE_RUNTIME_DRIFT"):
|
||||
manifest.validate(artifact, observed_oid="b" * 40, approved_root=approved)
|
||||
mutable = ControlPlaneReleaseManifest(
|
||||
"bad", "a" * 40, digest, Path("/var/lib/worktrees/dev"), BRIDGE_SCHEMA_VERSION,
|
||||
"aios/aios", Path("/data/aios-gitops/repo.git"), "STAGING",
|
||||
)
|
||||
with pytest.raises(RuntimeDriftError, match="CONTROL_PLANE_RUNTIME_DRIFT"):
|
||||
mutable.validate(artifact, observed_oid="a" * 40, approved_root=approved)
|
||||
|
||||
|
||||
def test_bridge_capabilities_keep_legacy_closed_and_expose_exact_operations():
|
||||
adapter = BridgeV2Adapter(runtime_status={"drift_status": "PASS"})
|
||||
assert adapter.getRuntimeStatus()["bridge_schema_version"] == BRIDGE_SCHEMA_VERSION
|
||||
capabilities = adapter.getGovernedWriteCapabilities()
|
||||
assert capabilities["legacy_writes_enabled"] is False
|
||||
assert capabilities["governed_writes_enabled"] is True
|
||||
assert tuple(capabilities["operations"]) == BRIDGE_OPERATIONS
|
||||
assert adapter.legacy_write_disabled() == {
|
||||
"error": "LEGACY_WRITE_DISABLED",
|
||||
"replacement": "createGovernedTask",
|
||||
"governed_writes_available": True,
|
||||
}
|
||||
|
||||
|
||||
def test_openapi_runtime_and_fragment_required_fields_are_exactly_converged():
|
||||
canonical = yaml.safe_load((ROOT / "docs/gitops/openapi-chatgpt-actions.yaml").read_text(encoding="utf-8"))
|
||||
fragment = yaml.safe_load((ROOT / "docs/gitops/openapi-authorized-task-writer-fragment.yaml").read_text(encoding="utf-8"))
|
||||
operations = {}
|
||||
for path, methods in canonical["paths"].items():
|
||||
for method, value in methods.items():
|
||||
if isinstance(value, dict) and value.get("operationId") in BRIDGE_OPERATIONS:
|
||||
operations[value["operationId"]] = value
|
||||
assert set(operations) == set(BRIDGE_OPERATIONS)
|
||||
for operation, required in OPERATION_REQUIRED_FIELDS.items():
|
||||
schema = operations[operation]["requestBody"]["content"]["application/json"]["schema"]
|
||||
assert tuple(schema["required"]) == required
|
||||
fragment_required = tuple(fragment["components"]["schemas"]["WriterLeaseRequestV2"]["required"])
|
||||
assert fragment_required == OPERATION_REQUIRED_FIELDS["acquireWriterLease"]
|
||||
|
||||
|
||||
def test_release_manifest_round_trip_contains_only_declared_identity_fields():
|
||||
manifest = ControlPlaneReleaseManifest(
|
||||
"v", "a" * 40, "b" * 64, Path("/opt/aios/gitops-control-plane/v"), BRIDGE_SCHEMA_VERSION,
|
||||
"aios/aios", Path("/data/aios-gitops/repo.git"), "STAGING",
|
||||
)
|
||||
value = manifest.as_dict()
|
||||
assert tuple(value) == (
|
||||
"control_plane_release_version", "control_plane_target_oid", "artifact_sha256", "runtime_path",
|
||||
"bridge_schema_version", "repository_identity", "authoritative_git_dir", "deployment_state",
|
||||
)
|
||||
assert "credential" not in json.dumps(value).lower()
|
||||
@@ -1,285 +0,0 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import base64
|
||||
from datetime import datetime, timedelta, timezone
|
||||
import json
|
||||
|
||||
from cryptography.hazmat.primitives import serialization
|
||||
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey
|
||||
import pytest
|
||||
|
||||
from aios_existing_branch_approval import (
|
||||
DurableNonceReplayLedger,
|
||||
OwnerApprovalEnvelope,
|
||||
OwnerApprovalRejected,
|
||||
OwnerApprovalVerifier,
|
||||
TrustRootEvidence,
|
||||
)
|
||||
from aios_existing_branch_publisher import (
|
||||
ExistingBranchPublicationRejected,
|
||||
ExistingBranchPublisher,
|
||||
RemoteBranchObservation,
|
||||
RemotePublicationEvidence,
|
||||
)
|
||||
|
||||
|
||||
BASE_OID = "a" * 40
|
||||
COMMIT_OID = "b" * 40
|
||||
BRANCH = "agent/owner-approval-contract-20260810"
|
||||
|
||||
|
||||
class TrustRoot:
|
||||
def __init__(self, public_key: bytes, *, revocation: str = "ACTIVE") -> None:
|
||||
self.public_key = public_key
|
||||
self.revocation = revocation
|
||||
self.calls: list[str] = []
|
||||
|
||||
def resolve(self, *, key_id: str, owner_identity: str, trust_domain: str):
|
||||
self.calls.append("trust-root")
|
||||
return TrustRootEvidence(
|
||||
key_id=key_id,
|
||||
owner_identity=owner_identity,
|
||||
trust_domain=trust_domain,
|
||||
public_key=self.public_key,
|
||||
authority_confirmed=True,
|
||||
revocation_state=self.revocation,
|
||||
)
|
||||
|
||||
|
||||
class Credentials:
|
||||
def __init__(self, events: list[str]) -> None:
|
||||
self.events = events
|
||||
|
||||
def project(self) -> None:
|
||||
self.events.append("credential-project")
|
||||
|
||||
def revoke(self) -> None:
|
||||
self.events.append("credential-revoke")
|
||||
|
||||
|
||||
class Workspace:
|
||||
def __init__(self, events: list[str]) -> None:
|
||||
self.events = events
|
||||
|
||||
def head_oid(self) -> str:
|
||||
self.events.append("workspace-head")
|
||||
return BASE_OID
|
||||
|
||||
def commit_payload(self, payload):
|
||||
self.events.append("workspace-commit")
|
||||
return COMMIT_OID
|
||||
|
||||
|
||||
class Workspaces:
|
||||
def __init__(self, events: list[str]) -> None:
|
||||
self.events = events
|
||||
|
||||
def open_detached(self, expected_branch_oid: str) -> Workspace:
|
||||
assert expected_branch_oid == BASE_OID
|
||||
self.events.append("workspace-open")
|
||||
return Workspace(self.events)
|
||||
|
||||
|
||||
class Remote:
|
||||
def __init__(self, events: list[str]) -> None:
|
||||
self.events = events
|
||||
self.observation = RemoteBranchObservation("PRESENT", BASE_OID, BASE_OID, True)
|
||||
self.authority_oid = BASE_OID
|
||||
self.pushes: list[tuple[str, str, str]] = []
|
||||
|
||||
def read_branch_oid(self, branch: str) -> str:
|
||||
self.events.append("authority-observe")
|
||||
assert branch == BRANCH
|
||||
return self.authority_oid
|
||||
|
||||
def observe(self, branch: str) -> RemoteBranchObservation:
|
||||
self.events.append("remote-observe")
|
||||
assert branch == BRANCH
|
||||
return self.observation
|
||||
|
||||
def publish_compare_and_swap(self, branch: str, expected: str, commit: str):
|
||||
self.events.append("remote-publish")
|
||||
self.pushes.append((branch, expected, commit))
|
||||
return RemotePublicationEvidence(COMMIT_OID, COMMIT_OID, COMMIT_OID, True)
|
||||
|
||||
|
||||
class Audit:
|
||||
def __init__(self) -> None:
|
||||
self.rows: list[dict] = []
|
||||
|
||||
def record(self, event) -> None:
|
||||
self.rows.append(dict(event))
|
||||
|
||||
|
||||
def request(*, payload=None, branch=BRANCH, expected_oid=BASE_OID):
|
||||
return {
|
||||
"schema": "aios.existing-branch-publication.v1",
|
||||
"repository": "aios/aios",
|
||||
"branch": branch,
|
||||
"expected_branch_oid": expected_oid,
|
||||
"operation": "publishExistingBranchGoverned",
|
||||
"idempotency_key": "owner-approval-binding-20260810",
|
||||
"payload": {"files": ["active-runtime/app/example.py"]} if payload is None else payload,
|
||||
}
|
||||
|
||||
|
||||
def signed_approval(private: Ed25519PrivateKey, request_value, **replacements):
|
||||
from aios_existing_branch_publisher import ExistingBranchPublicationRequest
|
||||
|
||||
parsed = ExistingBranchPublicationRequest.from_mapping(request_value)
|
||||
now = datetime.now(timezone.utc)
|
||||
raw = {
|
||||
"schema": "aios.existing-branch-owner-approval.v1",
|
||||
"approval_id": "approval-owner-binding-20260810",
|
||||
"repository": parsed.repository,
|
||||
"branch": parsed.branch,
|
||||
"expected_branch_oid": parsed.expected_branch_oid,
|
||||
"operation": "publishExistingBranchGoverned",
|
||||
"issued_at": (now - timedelta(seconds=30)).isoformat(),
|
||||
"not_before": (now - timedelta(seconds=1)).isoformat(),
|
||||
"expires_at": (now + timedelta(minutes=5)).isoformat(),
|
||||
"nonce": "nonce-owner-binding-20260810",
|
||||
"owner_identity": "owner.aios",
|
||||
"trust_domain": "aios-owner-root",
|
||||
"key_id": "owner-key-20260810",
|
||||
"request_digest": parsed.request_digest,
|
||||
"idempotency_digest": parsed.idempotency_digest,
|
||||
"signature": "AA==",
|
||||
}
|
||||
raw.update(replacements)
|
||||
envelope = OwnerApprovalEnvelope.from_mapping(raw)
|
||||
raw["signature"] = base64.b64encode(private.sign(envelope.canonical_bytes())).decode("ascii")
|
||||
return raw
|
||||
|
||||
|
||||
def service(tmp_path, *, revocation="ACTIVE"):
|
||||
private = Ed25519PrivateKey.generate()
|
||||
public_key = private.public_key().public_bytes(
|
||||
serialization.Encoding.Raw, serialization.PublicFormat.Raw
|
||||
)
|
||||
root = TrustRoot(public_key, revocation=revocation)
|
||||
events: list[str] = []
|
||||
remote = Remote(events)
|
||||
audit = Audit()
|
||||
publisher = ExistingBranchPublisher(
|
||||
verifier=OwnerApprovalVerifier(root, allowed_trust_domains={"aios-owner-root"}),
|
||||
replay_ledger=DurableNonceReplayLedger(tmp_path / "replay"),
|
||||
authority=remote,
|
||||
remote=remote,
|
||||
credentials=Credentials(events),
|
||||
workspaces=Workspaces(events),
|
||||
audit=audit,
|
||||
)
|
||||
return private, root, events, remote, audit, publisher
|
||||
|
||||
|
||||
def test_valid_signed_owner_approval_is_verified_before_capability_projection(tmp_path):
|
||||
private, root, events, remote, audit, publisher = service(tmp_path)
|
||||
value = request()
|
||||
approval = signed_approval(private, value)
|
||||
|
||||
result = publisher.publish(value, approval)
|
||||
|
||||
assert result["status"] == "PUBLISHED"
|
||||
assert result["commit_oid"] == COMMIT_OID
|
||||
assert remote.pushes == [(BRANCH, BASE_OID, COMMIT_OID)]
|
||||
assert events == [
|
||||
"authority-observe",
|
||||
"remote-observe",
|
||||
"workspace-open",
|
||||
"workspace-head",
|
||||
"credential-project",
|
||||
"workspace-commit",
|
||||
"remote-publish",
|
||||
"credential-revoke",
|
||||
]
|
||||
assert root.calls == ["trust-root"]
|
||||
assert approval["signature"] not in json.dumps(audit.rows)
|
||||
assert "signature" not in audit.rows[-1]
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"case",
|
||||
[
|
||||
"invalid_signature",
|
||||
"expired",
|
||||
"revoked",
|
||||
"missing_key_evidence",
|
||||
"missing_revocation_evidence",
|
||||
"wrong_repository",
|
||||
"wrong_branch",
|
||||
"wrong_oid",
|
||||
"wrong_operation",
|
||||
"arbitrary_reference",
|
||||
],
|
||||
)
|
||||
def test_invalid_owner_approval_inputs_are_rejected_without_publication(tmp_path, case):
|
||||
private, root, events, remote, _, publisher = service(
|
||||
tmp_path, revocation="REVOKED" if case == "revoked" else "ACTIVE"
|
||||
)
|
||||
value = request()
|
||||
approval = signed_approval(private, value)
|
||||
if case == "invalid_signature":
|
||||
approval["signature"] = "AAAA"
|
||||
elif case == "expired":
|
||||
approval = signed_approval(
|
||||
private,
|
||||
value,
|
||||
issued_at="2000-01-01T00:00:00+00:00",
|
||||
not_before="2000-01-01T00:00:01+00:00",
|
||||
expires_at="2000-01-01T00:01:00+00:00",
|
||||
)
|
||||
elif case == "wrong_repository":
|
||||
approval = signed_approval(private, value, repository="other/repository")
|
||||
elif case == "wrong_branch":
|
||||
approval = signed_approval(private, value, branch="agent/another-approved-branch")
|
||||
elif case == "wrong_oid":
|
||||
approval = signed_approval(private, value, expected_branch_oid="c" * 40)
|
||||
elif case == "wrong_operation":
|
||||
approval["operation"] = "differentOperation"
|
||||
elif case == "arbitrary_reference":
|
||||
approval = "owner-approval-reference-123"
|
||||
elif case == "missing_key_evidence":
|
||||
root.public_key = None
|
||||
elif case == "missing_revocation_evidence":
|
||||
root.revocation = "UNKNOWN"
|
||||
|
||||
with pytest.raises((OwnerApprovalRejected, ExistingBranchPublicationRejected)):
|
||||
publisher.publish(value, approval)
|
||||
|
||||
assert remote.pushes == []
|
||||
assert "credential-project" not in events
|
||||
assert "workspace-open" not in events
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"changed",
|
||||
[
|
||||
lambda value: {**value, "branch": "agent/approval-replay-other-branch"},
|
||||
lambda value: {**value, "expected_branch_oid": "c" * 40},
|
||||
lambda value: {**value, "payload": {"files": ["other.py"]}},
|
||||
],
|
||||
)
|
||||
def test_signed_approval_cannot_be_replayed_for_another_branch_oid_or_payload(tmp_path, changed):
|
||||
private, _, events, remote, _, publisher = service(tmp_path)
|
||||
original = request()
|
||||
approval = signed_approval(private, original)
|
||||
|
||||
with pytest.raises(OwnerApprovalRejected, match="OWNER_APPROVAL_BINDING_REJECTED"):
|
||||
publisher.publish(changed(original), approval)
|
||||
|
||||
assert remote.pushes == []
|
||||
assert events == []
|
||||
|
||||
|
||||
def test_signed_approval_nonce_is_single_use(tmp_path):
|
||||
private, _, events, remote, _, publisher = service(tmp_path)
|
||||
value = request()
|
||||
approval = signed_approval(private, value)
|
||||
|
||||
assert publisher.publish(value, approval)["status"] == "PUBLISHED"
|
||||
with pytest.raises(OwnerApprovalRejected, match="OWNER_APPROVAL_REPLAY_REJECTED"):
|
||||
publisher.publish(value, approval)
|
||||
|
||||
assert len(remote.pushes) == 1
|
||||
assert events.count("credential-project") == 1
|
||||
@@ -1,72 +0,0 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import inspect
|
||||
|
||||
from aios_existing_branch_publisher import (
|
||||
ExistingBranchPublisher,
|
||||
RemoteBranchObservation,
|
||||
)
|
||||
from test_existing_branch_owner_approval import (
|
||||
BASE_OID,
|
||||
COMMIT_OID,
|
||||
request,
|
||||
service,
|
||||
signed_approval,
|
||||
)
|
||||
|
||||
|
||||
def test_remote_oid_conflict_requires_manual_owner_review_before_capability_projection(tmp_path):
|
||||
private, _, events, remote, _, publisher = service(tmp_path)
|
||||
remote.observation = RemoteBranchObservation("PRESENT", "c" * 40, "c" * 40, True)
|
||||
value = request()
|
||||
|
||||
result = publisher.publish(value, signed_approval(private, value))
|
||||
|
||||
assert result == {"status": "MANUAL_OWNER_REVIEW_REQUIRED"}
|
||||
assert events == ["authority-observe", "remote-observe"]
|
||||
assert remote.pushes == []
|
||||
|
||||
|
||||
def test_ambiguous_remote_requires_reconciliation_after_one_observation(tmp_path):
|
||||
private, _, events, remote, _, publisher = service(tmp_path)
|
||||
remote.observation = RemoteBranchObservation("AMBIGUOUS", None, None, None)
|
||||
value = request()
|
||||
|
||||
result = publisher.publish(value, signed_approval(private, value))
|
||||
|
||||
assert result == {"status": "RECONCILIATION_REQUIRED"}
|
||||
assert events == ["authority-observe", "remote-observe"]
|
||||
assert remote.pushes == []
|
||||
|
||||
|
||||
def test_authority_oid_conflict_requires_manual_owner_review(tmp_path):
|
||||
private, _, events, remote, _, publisher = service(tmp_path)
|
||||
remote.authority_oid = "c" * 40
|
||||
value = request()
|
||||
|
||||
result = publisher.publish(value, signed_approval(private, value))
|
||||
|
||||
assert result == {"status": "MANUAL_OWNER_REVIEW_REQUIRED"}
|
||||
assert events == ["authority-observe"]
|
||||
assert remote.pushes == []
|
||||
|
||||
|
||||
def test_success_reports_all_remote_agreement_fields(tmp_path):
|
||||
private, _, _, _, _, publisher = service(tmp_path)
|
||||
value = request()
|
||||
|
||||
result = publisher.publish(value, signed_approval(private, value))
|
||||
|
||||
assert result == {
|
||||
"status": "PUBLISHED",
|
||||
"commit_oid": COMMIT_OID,
|
||||
"authority_oid": COMMIT_OID,
|
||||
"gitea_git_oid": COMMIT_OID,
|
||||
"gitea_api_oid": COMMIT_OID,
|
||||
"commit_visible": True,
|
||||
}
|
||||
|
||||
|
||||
def test_publisher_exposes_only_compare_and_swap_publication_path():
|
||||
source = inspect.getsource(ExistingBranchPublisher).lower()
|
||||
assert "force" not in source
|
||||
@@ -0,0 +1,283 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from pathlib import Path
|
||||
import subprocess
|
||||
import tempfile
|
||||
|
||||
import pytest
|
||||
|
||||
from aios_continuation_executor import (
|
||||
ContinuationCommit,
|
||||
ContinuationExecutionError,
|
||||
ContinuationExecutor,
|
||||
ContinuationPermissions,
|
||||
ContinuationRuntimeBinding,
|
||||
DetachedGitWorkspace,
|
||||
GiteaContinuationRemote,
|
||||
)
|
||||
from aios_continuation_lease import WriterLeaseRequestV2
|
||||
from aios_gitops_job_controller import BindingConflict, GitOpsJobController, JobState, JobStore
|
||||
|
||||
|
||||
TASK = "TASK-P0-CHATGPT-BRIDGE-V2-CONTROL-PLANE-CONVERGENCE-20260809"
|
||||
REPOSITORY = "aios/aios"
|
||||
BRANCH = "agent/p0-chatgpt-bridge-v2-control-plane-convergence-20260809"
|
||||
BASE = "2" * 40
|
||||
HEAD = "8" * 40
|
||||
NEW = "9" * 40
|
||||
NOW = datetime(2026, 8, 9, 12, 0, tzinfo=timezone.utc)
|
||||
|
||||
|
||||
class Authority:
|
||||
def __init__(self, *, head=HEAD, exists=True, descendant=True):
|
||||
self.head = head
|
||||
self.exists = exists
|
||||
self.descendant = descendant
|
||||
self.cas_calls = []
|
||||
|
||||
def branch_exists(self, branch):
|
||||
return self.exists
|
||||
|
||||
def branch_head(self, branch):
|
||||
return self.head
|
||||
|
||||
def is_ancestor(self, older, newer):
|
||||
if (older, newer) == (BASE, HEAD):
|
||||
return True
|
||||
return self.descendant and (older, newer) in {(HEAD, NEW), (BASE, NEW)}
|
||||
|
||||
def compare_and_swap_branch(self, branch, expected, new):
|
||||
self.cas_calls.append((branch, expected, new))
|
||||
if self.head != expected:
|
||||
raise RuntimeError("stale")
|
||||
self.head = new
|
||||
return new
|
||||
|
||||
|
||||
class Workspace:
|
||||
def __init__(self, *, detached=True, head=HEAD):
|
||||
self.detached = detached
|
||||
self.head = head
|
||||
self.commits = 0
|
||||
|
||||
def is_detached(self):
|
||||
return self.detached
|
||||
|
||||
def head_oid(self):
|
||||
return self.head
|
||||
|
||||
def commit_files(self, files, message):
|
||||
self.commits += 1
|
||||
self.head = NEW
|
||||
return NEW
|
||||
|
||||
|
||||
class Remote:
|
||||
def __init__(self, *, git=HEAD, api=HEAD, visible=True, repository=REPOSITORY, fail_after_push=False, omit_after_push=False):
|
||||
self.git = git
|
||||
self.api = api
|
||||
self.visible = visible
|
||||
self.repository = repository
|
||||
self.fail_after_push = fail_after_push
|
||||
self.omit_after_push = omit_after_push
|
||||
self.pushes = 0
|
||||
|
||||
def repository_identity(self):
|
||||
return self.repository
|
||||
|
||||
def push_exact(self, branch, expected, new):
|
||||
self.pushes += 1
|
||||
if self.git is None:
|
||||
raise RuntimeError("remote branch missing")
|
||||
if self.git != expected:
|
||||
raise RuntimeError("remote conflict")
|
||||
if not self.omit_after_push:
|
||||
self.git = self.api = new
|
||||
else:
|
||||
self.git = self.api = None
|
||||
if self.fail_after_push:
|
||||
raise TimeoutError("response lost")
|
||||
|
||||
def read_git_ref(self, branch):
|
||||
return self.git
|
||||
|
||||
def read_api_branch(self, branch):
|
||||
return self.api
|
||||
|
||||
def commit_visible(self, oid):
|
||||
return self.visible and self.api == oid
|
||||
|
||||
|
||||
class Credential:
|
||||
def __init__(self):
|
||||
self.projected = 0
|
||||
self.revoked = 0
|
||||
|
||||
def project(self):
|
||||
self.projected += 1
|
||||
|
||||
def revoke(self):
|
||||
self.revoked += 1
|
||||
|
||||
|
||||
def lease_request(**changes):
|
||||
values = dict(task_id=TASK, repository=REPOSITORY, branch_mode="CONTINUE_EXISTING", base_oid=BASE,
|
||||
expected_branch_oid=HEAD, target_branch=BRANCH, generation=1, lease_seconds=900)
|
||||
values.update(changes)
|
||||
return WriterLeaseRequestV2(**values)
|
||||
|
||||
|
||||
def binding(**changes):
|
||||
values = dict(
|
||||
request=lease_request(),
|
||||
workspace_path=Path("/var/lib/aios-task-writer/continuation-worktrees/exact"),
|
||||
lease_not_before=NOW - timedelta(seconds=1),
|
||||
lease_expires_at=NOW + timedelta(minutes=15),
|
||||
original_receipt_valid=True,
|
||||
original_grant_consumed=True,
|
||||
permissions=ContinuationPermissions(),
|
||||
)
|
||||
values.update(changes)
|
||||
return ContinuationRuntimeBinding(**values)
|
||||
|
||||
|
||||
def commit(**changes):
|
||||
values = dict(idempotency_key="first-continuation", files={"active-runtime/app/new.py": b"ok\n"},
|
||||
message="feat(gitops): add governed continuation executor")
|
||||
values.update(changes)
|
||||
return ContinuationCommit(**values)
|
||||
|
||||
|
||||
def make_executor(authority=None, workspace=None, remote=None, credential=None):
|
||||
temp = tempfile.TemporaryDirectory()
|
||||
controller = GitOpsJobController(JobStore(Path(temp.name).resolve()))
|
||||
executor = ContinuationExecutor(
|
||||
controller, authority or Authority(), workspace or Workspace(), remote or Remote(), credential or Credential(),
|
||||
now=lambda: NOW,
|
||||
)
|
||||
executor._test_temp = temp
|
||||
return executor
|
||||
|
||||
|
||||
def test_exact_existing_branch_completes_only_after_all_remote_proofs():
|
||||
authority, remote, credential = Authority(), Remote(), Credential()
|
||||
result = make_executor(authority, remote=remote, credential=credential).execute(binding(), commit())
|
||||
assert result["state"] == JobState.COMPLETED.value
|
||||
assert result["receipt"]["authority_oid"] == result["receipt"]["git_remote_oid"] == NEW
|
||||
assert result["receipt"]["api_branch_oid"] == NEW
|
||||
assert result["receipt"]["commit_visible"] is True
|
||||
assert authority.cas_calls == [(BRANCH, HEAD, NEW)]
|
||||
assert credential.projected == credential.revoked == 1
|
||||
assert "credential" not in repr(result).lower()
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"runtime_binding,authority,workspace,error",
|
||||
[
|
||||
(binding(request=lease_request(expected_branch_oid="7" * 40)), Authority(), Workspace(), "EXISTING_BRANCH_OID_CONFLICT"),
|
||||
(binding(), Authority(exists=False), Workspace(), "EXISTING_BRANCH_MISSING"),
|
||||
(binding(), Authority(), Workspace(detached=False), "WORKTREE_NOT_DETACHED"),
|
||||
(binding(), Authority(), Workspace(head="7" * 40), "WORKTREE_HEAD_MISMATCH"),
|
||||
(binding(lease_expires_at=NOW), Authority(), Workspace(), "LEASE_EXPIRED"),
|
||||
(binding(request=lease_request(repository="other/repo")), Authority(), Workspace(), "request_identity_invalid"),
|
||||
(binding(request=lease_request(target_branch="main")), Authority(), Workspace(), "PROTECTED_BRANCH_REJECTED"),
|
||||
(binding(request=lease_request(generation=1)), Authority(), Workspace(), "GENERATION_NOT_MONOTONIC"),
|
||||
],
|
||||
)
|
||||
def test_pre_side_effect_failures_never_create_or_update_a_branch(runtime_binding, authority, workspace, error):
|
||||
executor = make_executor(authority, workspace=workspace)
|
||||
if error == "GENERATION_NOT_MONOTONIC":
|
||||
executor.observe_generation(BRANCH, 1)
|
||||
with pytest.raises(Exception, match=error):
|
||||
executor.execute(runtime_binding, commit())
|
||||
assert authority.cas_calls == []
|
||||
assert workspace.commits == 0
|
||||
|
||||
|
||||
@pytest.mark.parametrize("field", ["create_target_branch", "force_push", "tag_write", "baseline_write", "production_write", "main_write"])
|
||||
def test_forbidden_capabilities_fail_before_credential_projection(field):
|
||||
credential = Credential()
|
||||
permissions = ContinuationPermissions(**{field: True})
|
||||
with pytest.raises(ContinuationExecutionError, match="MUTATION_PERMISSION_DENIED"):
|
||||
make_executor(credential=credential).execute(binding(permissions=permissions), commit())
|
||||
assert credential.projected == 0
|
||||
|
||||
|
||||
def test_non_fast_forward_and_concurrent_movement_are_rejected_cas_only():
|
||||
authority = Authority(descendant=False)
|
||||
with pytest.raises(ContinuationExecutionError, match="NON_FAST_FORWARD_COMMIT"):
|
||||
make_executor(authority).execute(binding(), commit())
|
||||
assert authority.cas_calls == []
|
||||
|
||||
|
||||
def test_authority_success_and_gitea_missing_is_never_completed():
|
||||
remote = Remote(omit_after_push=True)
|
||||
result = make_executor(remote=remote).execute(binding(), commit())
|
||||
assert result["state"] == JobState.RECONCILIATION_REQUIRED.value
|
||||
assert result["side_effect_state"] == "REMOTE_PUSH_PENDING"
|
||||
assert result["state"] not in {JobState.REMOTE_VERIFIED.value, JobState.COMPLETED.value}
|
||||
|
||||
|
||||
def test_response_loss_after_remote_success_reconciles_by_observation_without_second_push():
|
||||
remote = Remote(fail_after_push=True)
|
||||
executor = make_executor(remote=remote)
|
||||
first = executor.execute(binding(), commit())
|
||||
assert first["state"] == JobState.RECONCILIATION_REQUIRED.value
|
||||
assert remote.pushes == 1
|
||||
recovered = executor.reconcile(first["job_id"])
|
||||
assert recovered["state"] == JobState.COMPLETED.value
|
||||
assert remote.pushes == 1
|
||||
|
||||
|
||||
def test_observation_first_reconcile_retries_only_when_remote_is_exact_expected_old_oid():
|
||||
remote = Remote(fail_after_push=True)
|
||||
executor = make_executor(remote=remote)
|
||||
first = executor.execute(binding(), commit())
|
||||
assert first["state"] == JobState.RECONCILIATION_REQUIRED.value
|
||||
remote.git = remote.api = HEAD
|
||||
remote.visible = True
|
||||
remote.fail_after_push = False
|
||||
recovered = executor.reconcile(first["job_id"])
|
||||
assert recovered["state"] == JobState.COMPLETED.value
|
||||
assert remote.pushes == 2
|
||||
|
||||
|
||||
def test_duplicate_idempotency_replay_and_conflicting_payload():
|
||||
executor = make_executor()
|
||||
first = executor.execute(binding(), commit())
|
||||
replay = executor.execute(binding(), commit())
|
||||
assert replay["job_id"] == first["job_id"]
|
||||
with pytest.raises(BindingConflict, match="BINDING_CONFLICT"):
|
||||
executor.execute(binding(), commit(files={"active-runtime/app/new.py": b"changed\n"}))
|
||||
|
||||
|
||||
def test_secret_like_payload_fails_before_projection():
|
||||
credential = Credential()
|
||||
secret_fixture = b"to" + b"ken=" + b"ghp_" + b"abcdefghijklmnopqrstuvwxyz0123456789"
|
||||
with pytest.raises(ContinuationExecutionError, match="SECRET_LIKE_PAYLOAD_REJECTED"):
|
||||
make_executor(credential=credential).execute(binding(), commit(files={"x": secret_fixture}))
|
||||
assert credential.projected == 0
|
||||
|
||||
|
||||
def test_remote_identity_cannot_alias_the_authority_bare_repo():
|
||||
with pytest.raises(ContinuationExecutionError, match="GITEA_REMOTE_IDENTITY_INVALID"):
|
||||
GiteaContinuationRemote.validate_remote_url("/data/aios-gitops/repo.git")
|
||||
GiteaContinuationRemote.validate_remote_url("aiman@g.cnnetmkt.top:aios/aios.git")
|
||||
|
||||
|
||||
def test_detached_workspace_preserves_porcelain_leading_status_column():
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
root = Path(directory)
|
||||
subprocess.run(["git", "init", "-q", str(root)], check=True)
|
||||
subprocess.run(["git", "-C", str(root), "config", "user.name", "test"], check=True)
|
||||
subprocess.run(["git", "-C", str(root), "config", "user.email", "test@localhost"], check=True)
|
||||
(root / "tracked.py").write_text("one\n", encoding="utf-8")
|
||||
subprocess.run(["git", "-C", str(root), "add", "tracked.py"], check=True)
|
||||
subprocess.run(["git", "-C", str(root), "commit", "-q", "-m", "base"], check=True)
|
||||
subprocess.run(["git", "-C", str(root), "checkout", "-q", "--detach"], check=True)
|
||||
(root / "tracked.py").write_text("two\n", encoding="utf-8")
|
||||
(root / "new.py").write_text("new\n", encoding="utf-8")
|
||||
workspace = DetachedGitWorkspace(root)
|
||||
assert workspace.is_detached()
|
||||
assert workspace._dirty_paths() == {"tracked.py", "new.py"}
|
||||
@@ -250,7 +250,7 @@ class GovernedLocalWriterTests(unittest.TestCase):
|
||||
}
|
||||
self.assertEqual(service.process_request(secret_request, peer)["status"], "error")
|
||||
|
||||
def test_idempotent_replay_returns_receipt(self) -> None:
|
||||
def test_one_shot_consumption(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as root:
|
||||
directory = Path(root)
|
||||
auth = self._load_auth(self._make_auth_payload(), directory)
|
||||
@@ -283,8 +283,8 @@ class GovernedLocalWriterTests(unittest.TestCase):
|
||||
|
||||
self.assertEqual(service.process_request(create_request, peer)["status"], "ok")
|
||||
self.assertEqual(service.process_request(append_request, peer)["status"], "ok")
|
||||
self.assertEqual(service.process_request(append_request, peer)["status"], "receipt")
|
||||
self.assertEqual(len(sink.records), 2)
|
||||
self.assertEqual(service.process_request(append_request, peer)["status"], "error")
|
||||
self.assertEqual(len(sink.records), 3)
|
||||
self.assertEqual(len(handler.create_calls), 1)
|
||||
self.assertEqual(len(handler.append_calls), 1)
|
||||
|
||||
|
||||
@@ -1,35 +0,0 @@
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
|
||||
|
||||
def test_model_api_source_is_immutable_and_authority_bound():
|
||||
source = (ROOT / 'active-runtime' / 'app' / 'aios_model_api_staging.py').read_text(encoding='utf-8')
|
||||
assert 'BaselineAuthority' in source
|
||||
assert 'BASELINE_AUTHORITY = BaselineAuthority()' in source
|
||||
assert 'LEGACY_GIT_WRITE_COMMANDS' in source
|
||||
assert 'BaselineAuthorityError' in source
|
||||
assert 'IMMUTABLE_STAGING' in source
|
||||
assert 'LEGACY_GLOBAL_WRITES_DISABLED' in source
|
||||
assert "'production_deployed': False" in source
|
||||
assert "'governed_release_routing': 'bridge-v2'" in source
|
||||
|
||||
|
||||
def test_model_api_staging_unit_is_non_production_and_read_only():
|
||||
unit = (ROOT / 'deploy' / 'systemd' / 'aios-gitops-model-api-v2r2c1-staging.service').read_text(encoding='utf-8')
|
||||
assert 'aios-gitops-model-api-v2r2c1-staging.service' in unit
|
||||
assert 'AIOS_GITOPS_WRITES_ENABLED=false' in unit
|
||||
assert 'AIOS_GITOPS_DURABLE_JOB_EXECUTION_ENABLED=false' in unit
|
||||
assert 'AIOS_GITOPS_PORT=18893' in unit
|
||||
assert 'aios_model_api_staging.py' in unit
|
||||
assert 'production' not in unit.lower().replace('model-api-v2r2c1-staging', '')
|
||||
|
||||
|
||||
def test_model_api_builder_manifest_contract():
|
||||
builder = (ROOT / 'scripts' / 'build_v2r2c1_model_api_staging.py').read_text(encoding='utf-8')
|
||||
assert 'aios.v2r2c1.model-api-staging-manifest.v1' in builder
|
||||
assert "'target_version': 'V2R2C1'" in builder
|
||||
assert "'repository_identity': 'aios/aios'" in builder
|
||||
assert "'deployment_state': 'STAGING'" in builder
|
||||
assert "'legacy_global_writes_enabled': False" in builder
|
||||
assert 'private marker rejected' in builder
|
||||
@@ -0,0 +1,37 @@
|
||||
from pathlib import Path
|
||||
ROOT=Path(__file__).resolve().parents[1]
|
||||
OPENAPI=ROOT/"docs/gitops/openapi-chatgpt-actions.yaml"
|
||||
ROUTES={
|
||||
"/v2/task-admission/envelope":"getTaskAdmissionEnvelope",
|
||||
"/v2/task-admission/authorize":"submitTaskAdmissionAuthorization",
|
||||
"/v2/task-admission/status":"getTaskAdmissionStatus",
|
||||
}
|
||||
def block(text,marker,n):
|
||||
lines=text.splitlines(); pref=" "*n+marker
|
||||
start=next((i for i,x in enumerate(lines) if x==pref),None)
|
||||
assert start is not None, marker
|
||||
out=[lines[start]]
|
||||
for line in lines[start+1:]:
|
||||
if line.strip() and len(line)-len(line.lstrip(" "))<=n: break
|
||||
out.append(line)
|
||||
return "\n".join(out)
|
||||
def test_projected_once():
|
||||
t=OPENAPI.read_text(encoding="utf-8-sig")
|
||||
for route,op in ROUTES.items():
|
||||
assert t.count(f" {route}:")==1
|
||||
assert t.count(f"operationId: {op}")==1
|
||||
def test_no_principal_injection():
|
||||
t=OPENAPI.read_text(encoding="utf-8-sig")
|
||||
b=block(t,"TaskAdmissionAuthorizationRequest:",4)
|
||||
assert "\n principal:" not in b
|
||||
assert "\n _verified_task_principal:" not in b
|
||||
def test_no_git_write_projection():
|
||||
t=OPENAPI.read_text(encoding="utf-8-sig")
|
||||
for route in ("/v2/task-admission/envelope:","/v2/task-admission/authorize:"):
|
||||
b=block(t,route,2)
|
||||
assert "x-aios-no-git-write: true" in b
|
||||
assert "x-aios-no-shell: true" in b
|
||||
def test_status_read_only():
|
||||
t=OPENAPI.read_text(encoding="utf-8-sig")
|
||||
b=block(t,"/v2/task-admission/status:",2)
|
||||
assert "x-aios-read-only: true" in b
|
||||
Reference in New Issue
Block a user