Compare commits
20
Commits
Regular → Executable
+22
-11
@@ -58,11 +58,6 @@ class AuthorityGitExecutor:
|
|||||||
value = self._git(["rev-parse", "--verify", ref], check=False)
|
value = self._git(["rev-parse", "--verify", ref], check=False)
|
||||||
return value if value and OID_RE.fullmatch(value) else None
|
return value if value and OID_RE.fullmatch(value) else None
|
||||||
|
|
||||||
def commit_exists(self, oid: str) -> bool:
|
|
||||||
_oid(oid, "commit_oid")
|
|
||||||
result = subprocess.run(["git", f"--git-dir={self.git_dir}", "cat-file", "-e", oid + "^{commit}"], capture_output=True, check=False)
|
|
||||||
return result.returncode == 0
|
|
||||||
|
|
||||||
def ancestry_check(self, older_oid: str, newer_oid: str) -> bool:
|
def ancestry_check(self, older_oid: str, newer_oid: str) -> bool:
|
||||||
_oid(older_oid, "older_oid")
|
_oid(older_oid, "older_oid")
|
||||||
_oid(newer_oid, "newer_oid")
|
_oid(newer_oid, "newer_oid")
|
||||||
@@ -75,12 +70,28 @@ class AuthorityGitExecutor:
|
|||||||
raise AuthorityGitError("ANCESTRY_CHECK_FAILED")
|
raise AuthorityGitError("ANCESTRY_CHECK_FAILED")
|
||||||
return result.returncode == 0
|
return result.returncode == 0
|
||||||
|
|
||||||
def create_detached_worktree(
|
def commit_exists(self, oid: str) -> bool:
|
||||||
self,
|
_oid(oid, "commit_oid")
|
||||||
base_oid: str,
|
result = subprocess.run(["git", f"--git-dir={self.git_dir}", "cat-file", "-e", f"{oid}^{{commit}}"], capture_output=True, check=False)
|
||||||
worktree_name: str,
|
return result.returncode == 0
|
||||||
root: Path | None = None,
|
|
||||||
) -> Path:
|
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")
|
_oid(base_oid, "base_oid")
|
||||||
if not worktree_name or "/" in worktree_name or "\\" in worktree_name or worktree_name in {".", ".."}:
|
if not worktree_name or "/" in worktree_name or "\\" in worktree_name or worktree_name in {".", ".."}:
|
||||||
raise AuthorityGitError("WORKTREE_NAME_INVALID")
|
raise AuthorityGitError("WORKTREE_NAME_INVALID")
|
||||||
|
|||||||
Regular → Executable
@@ -25,7 +25,6 @@ BRIDGE_OPERATIONS = (
|
|||||||
"executeApprovedProductionRelease",
|
"executeApprovedProductionRelease",
|
||||||
"getReleaseConsistency",
|
"getReleaseConsistency",
|
||||||
"runReleaseAcceptance",
|
"runReleaseAcceptance",
|
||||||
"getControlPlaneSnapshot",
|
|
||||||
)
|
)
|
||||||
|
|
||||||
OPERATION_REQUIRED_FIELDS = {
|
OPERATION_REQUIRED_FIELDS = {
|
||||||
@@ -43,7 +42,6 @@ OPERATION_REQUIRED_FIELDS = {
|
|||||||
"executeApprovedProductionRelease": ("version", "target_oid", "evidence_closure_oid", "approval_plan_id", "execute"),
|
"executeApprovedProductionRelease": ("version", "target_oid", "evidence_closure_oid", "approval_plan_id", "execute"),
|
||||||
"getReleaseConsistency": ("version", "target_oid", "evidence_closure_oid", "approval_plan_id"),
|
"getReleaseConsistency": ("version", "target_oid", "evidence_closure_oid", "approval_plan_id"),
|
||||||
"runReleaseAcceptance": ("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 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 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 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 __future__ import annotations
|
||||||
|
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from datetime import datetime, timedelta, timezone
|
from datetime import datetime, timezone, timedelta
|
||||||
import base64
|
import base64
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
|
import re
|
||||||
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
import re
|
|
||||||
import sqlite3
|
|
||||||
from typing import Any, Mapping
|
from typing import Any, Mapping
|
||||||
|
|
||||||
from aios_authority_git_executor import AuthorityGitExecutor
|
from aios_authority_git_executor import AuthorityGitExecutor
|
||||||
from aios_chatgpt_bridge_v2 import BridgeV2Adapter, BridgeV2Error, OPERATION_REQUIRED_FIELDS
|
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_continuation_lease import WriterLeaseRequestV2
|
||||||
from aios_control_plane_snapshot import build_control_plane_snapshot
|
|
||||||
from aios_gitops_job_controller import GitOpsJobController, JobStore
|
from aios_gitops_job_controller import GitOpsJobController, JobStore
|
||||||
from aios_governed_task_bootstrap import BootstrapError, GovernedTaskBootstrap
|
from aios_governed_task_bootstrap import BootstrapError, GovernedTaskBootstrap
|
||||||
from governed_production_release import build_executor_config
|
from governed_production_release import build_executor_config
|
||||||
@@ -61,6 +68,13 @@ class RuntimeConfig:
|
|||||||
health_url: str = "http://127.0.0.1:18927/health"
|
health_url: str = "http://127.0.0.1:18927/health"
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class _Runtime:
|
||||||
|
binding: ContinuationRuntimeBinding
|
||||||
|
controller: GitOpsJobController
|
||||||
|
executor: ContinuationExecutor
|
||||||
|
|
||||||
|
|
||||||
def _safe(value: str) -> str:
|
def _safe(value: str) -> str:
|
||||||
return re.sub(r"[^A-Za-z0-9_.-]+", "-", value).strip("-")[:120] or "runtime"
|
return re.sub(r"[^A-Za-z0-9_.-]+", "-", value).strip("-")[:120] or "runtime"
|
||||||
|
|
||||||
@@ -70,88 +84,160 @@ class GovernedRuntime:
|
|||||||
self.config = config
|
self.config = config
|
||||||
config.state_root.mkdir(parents=True, exist_ok=True)
|
config.state_root.mkdir(parents=True, exist_ok=True)
|
||||||
self.authority = AuthorityGitExecutor(config.authoritative_git_dir, config.state_root / "worktrees")
|
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.bootstrap = GovernedTaskBootstrap(
|
||||||
self.runtimes: dict[tuple[str, int], tuple[Any, GitOpsJobController, ContinuationExecutor]] = {}
|
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.controllers: dict[str, GitOpsJobController] = {}
|
||||||
self.bindings: dict[str, dict[str, Any]] = {}
|
|
||||||
self.release = build_executor_config(config)
|
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"
|
askpass = config.state_root / "bridge-v2-askpass.sh"
|
||||||
if not askpass.exists():
|
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)
|
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")
|
os.environ.setdefault("GIT_ASKPASS", str(askpass))
|
||||||
|
os.environ.setdefault("GIT_ASKPASS_REQUIRE", "force")
|
||||||
def runtime_status(self) -> dict[str, Any]:
|
os.environ.setdefault("GIT_TERMINAL_PROMPT", "0")
|
||||||
return {
|
os.environ.setdefault("GITEA_MACHINE_ACTOR", "aios-autonomy-agent")
|
||||||
"status": "active",
|
|
||||||
"health": 200,
|
|
||||||
"bridge_schema_version": "aios.chatgpt-gitops-bridge.v2",
|
|
||||||
"control_plane_release_version": self.config.target_version,
|
|
||||||
"control_plane_target_oid": self.config.target_oid,
|
|
||||||
"runtime_release_commit": self.config.target_oid,
|
|
||||||
"runtime_path": str(self.config.state_root),
|
|
||||||
"artifact_sha256": os.environ.get("AIOS_CONTROL_PLANE_ARTIFACT_SHA256"),
|
|
||||||
"deployment_state": "STAGING",
|
|
||||||
"drift_status": "PASS",
|
|
||||||
"repository": self.config.repository,
|
|
||||||
"repository_identity": self.config.repository,
|
|
||||||
"authoritative_git_dir": str(self.config.authoritative_git_dir),
|
|
||||||
"legacy_global_writes_enabled": False,
|
|
||||||
}
|
|
||||||
|
|
||||||
def _job_root(self, task_id: str) -> Path:
|
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)
|
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"]))
|
key = (str(request["task_id"]), int(request["generation"]))
|
||||||
if key in self.runtimes: return self.runtimes[key]
|
if key in self.runtimes:
|
||||||
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)))
|
return self.runtimes[key]
|
||||||
lease.validate(AuthorityGitAdapter(self.authority))
|
if create_new:
|
||||||
worktree_path = self.config.state_root / "worktrees" / _safe(lease.task_id)
|
binding, controller, executor = self.bootstrap.prepare_runtime(
|
||||||
if worktree_path.exists():
|
request, lease_seconds=int(request.get("lease_seconds", 900))
|
||||||
workspace = DetachedGitWorkspace(worktree_path)
|
)
|
||||||
else:
|
else:
|
||||||
self.authority.create_detached_worktree(lease.expected_branch_oid, _safe(lease.task_id), self.config.state_root / "worktrees")
|
request_v2 = WriterLeaseRequestV2(
|
||||||
workspace = DetachedGitWorkspace(worktree_path)
|
task_id=str(request["task_id"]),
|
||||||
if workspace.head_oid() != lease.expected_branch_oid or not workspace.is_detached(): raise RuntimeError("WORKTREE_HEAD_MISMATCH")
|
repository=str(request["repository"]),
|
||||||
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)
|
branch_mode=str(request["branch_mode"]),
|
||||||
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())
|
base_oid=str(request["base_oid"]),
|
||||||
result = (binding, controller, executor); self.runtimes[key] = result; self.controllers[lease.task_id] = controller; return result
|
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]:
|
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]:
|
def acquire_lease(self, request: Mapping[str, Any]) -> dict[str, Any]:
|
||||||
binding, _, _ = self._runtime(request)
|
create_new = str(request.get("branch_mode")) == "CREATE_NEW"
|
||||||
req = binding.request
|
runtime = self._runtime(request, create_new=create_new)
|
||||||
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}
|
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
|
@staticmethod
|
||||||
def _files(request: Mapping[str, Any]) -> dict[str, bytes]:
|
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}
|
raw = request["files"] if "files" in request else {request["path"]: request["content_base64"]}
|
||||||
if not files: raise ValueError("FILES_EMPTY")
|
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
|
return files
|
||||||
|
|
||||||
def commit_files(self, request: Mapping[str, Any]) -> dict[str, Any]:
|
def commit_files(self, request: Mapping[str, Any]) -> dict[str, Any]:
|
||||||
runtime = self.runtimes.get((str(request["task_id"]), int(request["generation"])))
|
key = (str(request["task_id"]), int(request["generation"]))
|
||||||
if runtime is None: raise RuntimeError("LEASE_NOT_FOUND")
|
runtime = self.runtimes.get(key)
|
||||||
binding, _, executor = runtime
|
if runtime is None:
|
||||||
if request["target_branch"] != binding.request.target_branch or request["expected_branch_oid"] != binding.request.expected_branch_oid: raise RuntimeError("LEASE_BINDING_CONFLICT")
|
raise RuntimeError("LEASE_NOT_FOUND")
|
||||||
commit = ContinuationCommit(idempotency_key=str(request["idempotency_key"]), files=self._files(request), message=str(request["message"]))
|
request_v2 = runtime.binding.request
|
||||||
executor.workspace.materialize_files(commit.files)
|
if (
|
||||||
return executor.execute(binding, commit)
|
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:
|
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:
|
try:
|
||||||
controller.store.read(job_id)
|
controller.store.read(job_id)
|
||||||
return controller
|
return controller
|
||||||
@@ -159,13 +245,13 @@ class GovernedRuntime:
|
|||||||
continue
|
continue
|
||||||
jobs_root = self.config.state_root / "jobs"
|
jobs_root = self.config.state_root / "jobs"
|
||||||
if jobs_root.is_dir():
|
if jobs_root.is_dir():
|
||||||
for root in jobs_root.iterdir():
|
for root in sorted(jobs_root.iterdir()):
|
||||||
if not root.is_dir():
|
if not root.is_dir() or not (root / "state.sqlite3").is_file():
|
||||||
continue
|
continue
|
||||||
controller = GitOpsJobController(JobStore(root, payload_root=root / "payloads"))
|
controller = GitOpsJobController(JobStore(root, payload_root=root / "payloads"))
|
||||||
try:
|
try:
|
||||||
controller.store.read(job_id)
|
controller.store.read(job_id)
|
||||||
self.controllers[str(job_id)] = controller
|
self.controllers["job:" + job_id] = controller
|
||||||
return controller
|
return controller
|
||||||
except Exception:
|
except Exception:
|
||||||
continue
|
continue
|
||||||
@@ -282,73 +368,17 @@ class GovernedRuntime:
|
|||||||
return controller.resume(job_id).as_dict()
|
return controller.resume(job_id).as_dict()
|
||||||
return self._executor_from_row(row, controller).reconcile(job_id)
|
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]:
|
def handlers(self) -> dict[str, Any]:
|
||||||
return {
|
return {
|
||||||
"createGovernedTask": self.create_task,
|
"createGovernedTask": self.create_task,
|
||||||
"acquireWriterLease": self.acquire_lease,
|
"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,
|
"commitFilesGoverned": self.commit_files,
|
||||||
"getGitOpsJob": lambda request: self.job(request, "get"),
|
"getGitOpsJob": lambda request: self.job(request, "get"),
|
||||||
"resumeGitOpsJob": lambda request: self.job(request, "resume"),
|
"resumeGitOpsJob": lambda request: self.job(request, "resume"),
|
||||||
"reconcileGitOpsJob": lambda request: self.job(request, "reconcile"),
|
"reconcileGitOpsJob": lambda request: self.job(request, "reconcile"),
|
||||||
"getControlPlaneSnapshot": lambda request: self.snapshot(),
|
|
||||||
"createTagApprovalRequest": self.release.approval_request,
|
"createTagApprovalRequest": self.release.approval_request,
|
||||||
"requestOrCreateTag": self.release.publish_tag,
|
"requestOrCreateTag": self.release.publish_tag,
|
||||||
"getTagPublicationStatus": self.release.tag_status,
|
"getTagPublicationStatus": self.release.tag_status,
|
||||||
@@ -358,50 +388,96 @@ class GovernedRuntime:
|
|||||||
"runReleaseAcceptance": self.release.acceptance,
|
"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]:
|
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()
|
return GovernedRuntime(runtime_config).handlers()
|
||||||
|
|
||||||
|
|
||||||
class _HTTPError(RuntimeError): pass
|
class _HTTPError(RuntimeError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
class BridgeHandler(BaseHTTPRequestHandler):
|
class BridgeHandler(BaseHTTPRequestHandler):
|
||||||
adapter: BridgeV2Adapter | None = None
|
adapter: BridgeV2Adapter
|
||||||
def log_message(self, *_args: Any) -> None: return
|
|
||||||
|
def log_message(self, *_args: Any) -> None:
|
||||||
|
return
|
||||||
|
|
||||||
def _json(self, value: Any, status: int = 200) -> None:
|
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]:
|
def _body(self) -> dict[str, Any]:
|
||||||
try: length = int(self.headers.get("Content-Length", "-1"))
|
try:
|
||||||
except ValueError as exc: raise _HTTPError("CONTENT_LENGTH_INVALID") from exc
|
length = int(self.headers.get("Content-Length", "-1"))
|
||||||
if length < 0 or length > MAX_BODY: raise _HTTPError("REQUEST_TOO_LARGE")
|
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"))
|
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
|
return value
|
||||||
|
|
||||||
def _allowed(self, operation: str, body: Mapping[str, Any]) -> None:
|
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"}
|
allowed = set(OPERATION_REQUIRED_FIELDS[operation]) | {
|
||||||
if operation == "commitFilesGoverned": allowed.add("files")
|
"schema", "lease_seconds", "create_target_branch", "worktree_mode", "cas_ref_update_required",
|
||||||
allowed.update({"owner_approval", "execute"})
|
"owner_approval", "execute"
|
||||||
if set(body) - allowed: raise _HTTPError("UNKNOWN_FIELDS")
|
}
|
||||||
|
if operation == "commitFilesGoverned":
|
||||||
|
allowed.add("files")
|
||||||
|
if set(body) - allowed:
|
||||||
|
raise _HTTPError("UNKNOWN_FIELDS")
|
||||||
|
|
||||||
def do_GET(self) -> None:
|
def do_GET(self) -> None:
|
||||||
if self.adapter is None: self._json({"error": "RUNTIME_UNAVAILABLE"}, 503); return
|
if self.path == "/v2/runtime":
|
||||||
if self.path == "/v2/runtime": self._json(self.adapter.getRuntimeStatus()); return
|
self._json(self.adapter.getRuntimeStatus())
|
||||||
if self.path == "/v2/capabilities": self._json(self.adapter.getGovernedWriteCapabilities()); return
|
return
|
||||||
if self.path == "/v2/control-plane-snapshot": self._json(self.adapter.getControlPlaneSnapshot({})); return
|
if self.path == "/v2/capabilities":
|
||||||
|
self._json(self.adapter.getGovernedWriteCapabilities())
|
||||||
|
return
|
||||||
self._json({"error": "NOT_FOUND"}, 404)
|
self._json({"error": "NOT_FOUND"}, 404)
|
||||||
|
|
||||||
def do_POST(self) -> None:
|
def do_POST(self) -> None:
|
||||||
operation = ROUTES.get(self.path)
|
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:
|
try:
|
||||||
body = self._body(); self._allowed(operation, body); self._json(getattr(self.adapter, operation)(body))
|
body = self._body()
|
||||||
except _HTTPError as exc: self._json({"error": str(exc)}, 413 if str(exc) == "REQUEST_TOO_LARGE" else 400)
|
self._allowed(operation, body)
|
||||||
except (BootstrapError, ValueError, BridgeV2Error) as exc: self._json({"error": str(exc)}, 409 if "CONFLICT" in str(exc) else 400)
|
self._json(getattr(self.adapter, operation)(body))
|
||||||
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)
|
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:
|
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:
|
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_authority_git_executor import AuthorityGitExecutor
|
||||||
from aios_continuation_lease import WriterLeaseRequestV2
|
from aios_continuation_lease import WriterLeaseRequestV2
|
||||||
from aios_gitops_job_controller import GitOpsJobController, JobState
|
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
|
from authorized_task_writer_provisioning import validate_writer_lease_request_v2
|
||||||
|
|
||||||
|
|
||||||
@@ -44,6 +43,7 @@ class ContinuationExecutionError(RuntimeError):
|
|||||||
class DetachedWorkspace(Protocol):
|
class DetachedWorkspace(Protocol):
|
||||||
def is_detached(self) -> bool: ...
|
def is_detached(self) -> bool: ...
|
||||||
def head_oid(self) -> str: ...
|
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: ...
|
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 branch_head(self, branch: str) -> str: ...
|
||||||
def is_ancestor(self, older: str, newer: str) -> bool: ...
|
def is_ancestor(self, older: str, newer: str) -> bool: ...
|
||||||
def compare_and_swap_branch(self, branch: str, expected: str, new: str) -> str: ...
|
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):
|
class RemotePort(Protocol):
|
||||||
def repository_identity(self) -> str: ...
|
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_git_ref(self, branch: str) -> str | None: ...
|
||||||
def read_api_branch(self, branch: str) -> str | None: ...
|
def read_api_branch(self, branch: str) -> str | None: ...
|
||||||
|
|
||||||
def commit_visible(self, oid: str) -> bool: ...
|
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:
|
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)
|
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)
|
@dataclass(slots=True)
|
||||||
class DetachedGitWorkspace:
|
class DetachedGitWorkspace:
|
||||||
@@ -150,20 +159,36 @@ class DetachedGitWorkspace:
|
|||||||
return result
|
return result
|
||||||
|
|
||||||
def materialize_files(self, files: Mapping[str, bytes]) -> None:
|
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)
|
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")
|
raise ContinuationExecutionError("WORKTREE_DIRTY_SCOPE_MISMATCH")
|
||||||
|
root = self.path.resolve()
|
||||||
for relative, content in files.items():
|
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()
|
target = (self.path / relative).resolve()
|
||||||
try:
|
try:
|
||||||
target.relative_to(self.path.resolve())
|
target.relative_to(root)
|
||||||
except ValueError as exc:
|
except ValueError as exc:
|
||||||
raise ContinuationExecutionError("COMMIT_PATH_REJECTED") from exc
|
raise ContinuationExecutionError("COMMIT_PATH_REJECTED") from exc
|
||||||
if target.is_symlink():
|
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.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:
|
def commit_files(self, files: Mapping[str, bytes], message: str) -> str:
|
||||||
expected_paths = set(files)
|
expected_paths = set(files)
|
||||||
@@ -258,10 +283,16 @@ class GiteaContinuationRemote:
|
|||||||
raise ContinuationExecutionError("GITEA_GIT_READBACK_INVALID")
|
raise ContinuationExecutionError("GITEA_GIT_READBACK_INVALID")
|
||||||
return oid
|
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._require_projected()
|
||||||
self.repository_identity()
|
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")
|
raise ContinuationExecutionError("GITEA_BRANCH_EXPECTATION_MISMATCH")
|
||||||
_run(["git", "push", self.remote_name, f"{new}:refs/heads/{branch}"], cwd=self.workspace)
|
_run(["git", "push", self.remote_name, f"{new}:refs/heads/{branch}"], cwd=self.workspace)
|
||||||
|
|
||||||
@@ -321,8 +352,10 @@ class ContinuationPermissions:
|
|||||||
production_write: bool = False
|
production_write: bool = False
|
||||||
main_write: bool = False
|
main_write: bool = False
|
||||||
|
|
||||||
def validate(self) -> None:
|
def validate(self, *, branch_mode: str = "CONTINUE_EXISTING") -> None:
|
||||||
if any(asdict(self).values()):
|
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")
|
raise ContinuationExecutionError("MUTATION_PERMISSION_DENIED")
|
||||||
|
|
||||||
|
|
||||||
@@ -413,20 +446,31 @@ class ContinuationExecutor:
|
|||||||
"lease_seconds": request.lease_seconds,
|
"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
|
request = binding.request
|
||||||
normalized = validate_writer_lease_request_v2(self._request_mapping(request), self.authority)
|
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")
|
raise ContinuationExecutionError("CONTINUATION_NORMALIZATION_INVALID")
|
||||||
if not binding.original_receipt_valid or not binding.original_grant_consumed:
|
if not binding.original_receipt_valid or not binding.original_grant_consumed:
|
||||||
raise ContinuationExecutionError("ORIGINAL_GRANT_LINEAGE_INVALID")
|
raise ContinuationExecutionError("ORIGINAL_GRANT_LINEAGE_INVALID")
|
||||||
binding.permissions.validate()
|
binding.permissions.validate(branch_mode=request.branch_mode)
|
||||||
now = self.now()
|
now = self.now()
|
||||||
if now < binding.lease_not_before:
|
if now < binding.lease_not_before:
|
||||||
raise ContinuationExecutionError("LEASE_NOT_ACTIVE")
|
raise ContinuationExecutionError("LEASE_NOT_ACTIVE")
|
||||||
if now >= binding.lease_expires_at:
|
if now >= binding.lease_expires_at:
|
||||||
raise ContinuationExecutionError("LEASE_EXPIRED")
|
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")
|
raise ContinuationExecutionError("GENERATION_NOT_MONOTONIC")
|
||||||
if not self.workspace.is_detached():
|
if not self.workspace.is_detached():
|
||||||
raise ContinuationExecutionError("WORKTREE_NOT_DETACHED")
|
raise ContinuationExecutionError("WORKTREE_NOT_DETACHED")
|
||||||
@@ -442,7 +486,7 @@ class ContinuationExecutor:
|
|||||||
"campaign_id": request.task_id,
|
"campaign_id": request.task_id,
|
||||||
"task_id": request.task_id,
|
"task_id": request.task_id,
|
||||||
"repository": request.repository,
|
"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,
|
"branch": request.target_branch,
|
||||||
"expected_branch_oid": request.expected_branch_oid,
|
"expected_branch_oid": request.expected_branch_oid,
|
||||||
"base_oid": request.base_oid,
|
"base_oid": request.base_oid,
|
||||||
@@ -458,7 +502,26 @@ class ContinuationExecutor:
|
|||||||
current = self.controller.store.read(submitted.job_id)
|
current = self.controller.store.read(submitted.job_id)
|
||||||
if JobState(current["state"]) == JobState.COMPLETED:
|
if JobState(current["state"]) == JobState.COMPLETED:
|
||||||
return self.controller.store._receipt(current, duplicate=True).as_dict()
|
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
|
request = binding.request
|
||||||
projected = False
|
projected = False
|
||||||
try:
|
try:
|
||||||
@@ -488,7 +551,10 @@ class ContinuationExecutor:
|
|||||||
new_oid = self.workspace.commit_files(commit.files, commit.message)
|
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):
|
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")
|
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:
|
if authority_oid != new_oid:
|
||||||
raise ContinuationExecutionError("AUTHORITY_CAS_READBACK_MISMATCH")
|
raise ContinuationExecutionError("AUTHORITY_CAS_READBACK_MISMATCH")
|
||||||
self.controller.advance(
|
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]:
|
def _finish_remote_verification(self, job_id: str, authority_oid: str, new_oid: str) -> dict[str, object]:
|
||||||
row = self.controller.store.read(job_id)
|
row = self.controller.store.read(job_id)
|
||||||
branch = row["branch"]
|
branch = row["branch"]
|
||||||
request = json.loads(row["request_json"])["binding"]
|
|
||||||
git_oid, api_oid, visible = self._observe_remote(branch, new_oid)
|
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 not (authority_oid == git_oid == api_oid == new_oid and visible):
|
||||||
if decision.action is not ReconciliationAction.MARK_REMOTE_VERIFIED:
|
|
||||||
return self._remote_pending(job_id, authority_oid, new_oid)
|
return self._remote_pending(job_id, authority_oid, new_oid)
|
||||||
receipt = {
|
receipt = {
|
||||||
"authority_oid": authority_oid,
|
"authority_oid": authority_oid,
|
||||||
@@ -539,7 +603,6 @@ class ContinuationExecutor:
|
|||||||
"api_branch_oid": api_oid,
|
"api_branch_oid": api_oid,
|
||||||
"commit_visible": True,
|
"commit_visible": True,
|
||||||
"remote_verified": True,
|
"remote_verified": True,
|
||||||
"reconciliation_decision": decision.action.value,
|
|
||||||
"force_push": False,
|
"force_push": False,
|
||||||
}
|
}
|
||||||
self.controller.advance(job_id, JobState.REMOTE_VERIFIED, side_effect_state="REMOTE_VERIFIED", receipt=receipt)
|
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"])
|
receipt = json.loads(row["receipt_json"])
|
||||||
new_oid = receipt["desired_remote_oid"]
|
new_oid = receipt["desired_remote_oid"]
|
||||||
authority_oid = self.authority.branch_head(row["branch"])
|
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()
|
self.credential.project()
|
||||||
try:
|
try:
|
||||||
git_oid, api_oid, visible = self._observe_remote(row["branch"], new_oid)
|
git_oid, api_oid, visible = self._observe_remote(row["branch"], new_oid)
|
||||||
if authority_oid == git_oid == api_oid == new_oid and visible:
|
if authority_oid == git_oid == api_oid == new_oid and visible:
|
||||||
self.controller.advance(job_id, JobState.RECONCILING, side_effect_state="OBSERVATION_FIRST")
|
self.controller.advance(job_id, JobState.RECONCILING, side_effect_state="OBSERVATION_FIRST")
|
||||||
return self._finish_remote_verification(job_id, new_oid, new_oid)
|
return self._finish_remote_verification(job_id, authority_oid, new_oid)
|
||||||
expected_oid = request["expected_branch_oid"]
|
expected_oid = None if row["operation"].endswith("_CREATE_NEW") else request["expected_branch_oid"]
|
||||||
if (
|
|
||||||
self.authority.is_ancestor(new_oid, authority_oid)
|
|
||||||
and git_oid is not None
|
|
||||||
and git_oid == api_oid
|
|
||||||
and self.authority.is_ancestor(git_oid, new_oid)
|
|
||||||
):
|
|
||||||
self.controller.advance(job_id, JobState.RECONCILING, side_effect_state="OBSERVATION_FIRST_CHAIN")
|
|
||||||
self.remote.push_exact(row["branch"], git_oid, new_oid)
|
|
||||||
return self._finish_remote_verification(job_id, new_oid, new_oid)
|
|
||||||
if authority_oid == new_oid and git_oid == api_oid == expected_oid:
|
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.controller.advance(job_id, JobState.RECONCILING, side_effect_state="OBSERVATION_FIRST_RETRY")
|
||||||
self.remote.push_exact(row["branch"], expected_oid, new_oid)
|
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()
|
return self.controller.store._receipt(row, duplicate=True).as_dict()
|
||||||
finally:
|
finally:
|
||||||
self.credential.revoke()
|
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 __future__ import annotations
|
||||||
|
|
||||||
from dataclasses import dataclass, asdict
|
from dataclasses import dataclass
|
||||||
|
from datetime import datetime, timezone, timedelta
|
||||||
import hashlib
|
import hashlib
|
||||||
import json
|
import json
|
||||||
from pathlib import Path
|
|
||||||
import re
|
import re
|
||||||
|
from pathlib import Path
|
||||||
from typing import Any, Mapping
|
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
|
from aios_gitops_job_controller import GitOpsJobController, JobStore
|
||||||
|
|
||||||
OID_RE = re.compile(r"^[0-9a-f]{40}$")
|
OID_RE = re.compile(r"^[0-9a-f]{40}$")
|
||||||
@@ -27,58 +39,218 @@ class GovernedTaskBinding:
|
|||||||
risk: str
|
risk: str
|
||||||
description: str
|
description: str
|
||||||
idempotency_key: str
|
idempotency_key: str
|
||||||
expected_branch_oid: str
|
|
||||||
branch_mode: str
|
|
||||||
generation: int
|
|
||||||
job_id: str | None = None
|
job_id: str | None = None
|
||||||
|
|
||||||
def response(self, *, idempotent: bool = False) -> dict[str, Any]:
|
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"
|
return re.sub(r"[^A-Za-z0-9_.-]+", "-", value).strip("-")[:120] or "task"
|
||||||
|
|
||||||
|
|
||||||
class GovernedTaskBootstrap:
|
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:
|
def __init__(
|
||||||
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)
|
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]:
|
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:
|
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:
|
def _read_existing(self, task_id: str) -> GovernedTaskBinding | None:
|
||||||
path = self._metadata(task_id)
|
path = self._metadata(task_id)
|
||||||
if not path.is_file() or path.is_symlink(): return None
|
if not path.is_file() or path.is_symlink():
|
||||||
return GovernedTaskBinding(**json.loads(path.read_text(encoding="utf-8")))
|
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:
|
def _write_metadata(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)
|
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]:
|
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")}
|
values = {key: str(request[key]) for key in (
|
||||||
if values["repository"] != "aios/aios" or not OID_RE.fullmatch(values["base_oid"]): raise BootstrapError("TASK_BINDING_INVALID")
|
"task_id",
|
||||||
if not BRANCH_RE.fullmatch(values["target_branch"]): raise BootstrapError("TARGET_BRANCH_INVALID")
|
"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"])
|
existing = self._read_existing(values["task_id"])
|
||||||
if existing is not None:
|
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)
|
return existing.response(idempotent=True)
|
||||||
if not self.authority.commit_exists(values["base_oid"]): raise BootstrapError("BASE_COMMIT_NOT_FOUND")
|
if not self.authority.commit_exists(values["base_oid"]):
|
||||||
current = self.authority.read_ref("refs/heads/" + values["target_branch"])
|
raise BootstrapError("BASE_COMMIT_NOT_FOUND")
|
||||||
expected = str(request.get("expected_branch_oid") or values["base_oid"])
|
if self.authority.read_ref("refs/heads/" + values["target_branch"]) is not None:
|
||||||
if current is None:
|
raise BootstrapError("CONFLICT")
|
||||||
mode, generation = "CREATE_NEW", 1
|
|
||||||
if expected != values["base_oid"]: raise BootstrapError("EXPECTED_BASE_CONFLICT")
|
|
||||||
else:
|
|
||||||
if expected != current: raise BootstrapError("EXISTING_BRANCH_OID_CONFLICT")
|
|
||||||
mode, generation = "CONTINUE_EXISTING", int(request.get("generation", 2))
|
|
||||||
if generation < 1: raise BootstrapError("GENERATION_INVALID")
|
|
||||||
payload = json.dumps(values, sort_keys=True, separators=(",", ":")).encode()
|
payload = json.dumps(values, sort_keys=True, separators=(",", ":")).encode()
|
||||||
_, controller = self._store(values["task_id"])
|
_, 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)
|
receipt = controller.submit(
|
||||||
binding = GovernedTaskBinding(**values, expected_branch_oid=expected, branch_mode=mode, generation=generation, job_id=receipt.job_id)
|
{
|
||||||
self._write(binding)
|
"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()
|
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.
|
# Merge this fragment into docs/gitops/openapi-chatgpt-actions.yaml.
|
||||||
components:
|
components:
|
||||||
schemas:
|
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:
|
AuthorizedTaskWriterProvisionRequest:
|
||||||
type: object
|
type: object
|
||||||
additionalProperties: false
|
additionalProperties: false
|
||||||
|
|||||||
@@ -13,6 +13,55 @@ components:
|
|||||||
scheme: bearer
|
scheme: bearer
|
||||||
bearerFormat: opaque-token
|
bearerFormat: opaque-token
|
||||||
schemas:
|
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:
|
PublicationState:
|
||||||
type: string
|
type: string
|
||||||
description: Canonical durable publication state. Legacy TAG_CREATED_LOCAL is normalized on read and is never emitted by new writes.
|
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}
|
message: {type: string}
|
||||||
TagRequest:
|
TagRequest:
|
||||||
type: object
|
type: object
|
||||||
required: [version, target_oid, evidence_closure_oid, approval_plan_id]
|
required: [version, target_oid, evidence_closure_oid, approval_plan_id, execute]
|
||||||
properties:
|
properties:
|
||||||
version: {type: string, example: V1R1C1}
|
version: {type: string, example: V1R1C1}
|
||||||
target_oid:
|
target_oid:
|
||||||
@@ -241,6 +290,122 @@ components:
|
|||||||
properties:
|
properties:
|
||||||
candidate_id: {type: string}
|
candidate_id: {type: string}
|
||||||
paths:
|
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:
|
/createReleaseIntent:
|
||||||
post:
|
post:
|
||||||
operationId: createReleaseIntent
|
operationId: createReleaseIntent
|
||||||
@@ -490,6 +655,14 @@ paths:
|
|||||||
summary: Read durable publication Saga status without mutation
|
summary: Read durable publication Saga status without mutation
|
||||||
parameters:
|
parameters:
|
||||||
- {in: query, name: version, required: true, schema: {type: string, pattern: '^V\d+R\d+C\d+(?:-[A-Za-z0-9_.-]+)?$'}}
|
- {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:
|
responses:
|
||||||
'200':
|
'200':
|
||||||
description: Canonical publication state or fail-closed UNKNOWN
|
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: 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: 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}}
|
- {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}}
|
responses: {'200': {description: PASS, FAIL, or fail-closed UNKNOWN}}
|
||||||
/api/release-acceptance:
|
/api/release-acceptance:
|
||||||
get:
|
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: 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: 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}}
|
- {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}}
|
responses: {'200': {description: Acceptance result with accepted boolean}}
|
||||||
/api/tag:
|
/api/tag:
|
||||||
post:
|
post:
|
||||||
@@ -526,7 +715,10 @@ paths:
|
|||||||
required: true
|
required: true
|
||||||
content:
|
content:
|
||||||
application/json:
|
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}}
|
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
|
x-openai-isConsequential: true
|
||||||
/api/rollback:
|
/api/rollback:
|
||||||
@@ -567,3 +759,48 @@ paths:
|
|||||||
'200': {description: Safe GitOps plan}
|
'200': {description: Safe GitOps plan}
|
||||||
'401': {description: Unauthorized}
|
'401': {description: Unauthorized}
|
||||||
x-openai-isConsequential: true
|
x-openai-isConsequential: true
|
||||||
|
/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
|
import json
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
ROOT = Path(__file__).parents[2]
|
ROOT = Path(__file__).parents[2]
|
||||||
|
|
||||||
|
|
||||||
def test_version_and_candidate_binding():
|
def test_version_and_candidate_binding():
|
||||||
assert (ROOT / "VERSION").read_text(encoding="utf-8").strip() == "V2R1C7"
|
assert (ROOT / "VERSION").read_text(encoding="utf-8").strip() == "V2R1C6"
|
||||||
metadata = json.loads((ROOT / "versions/V2R1C6.json").read_text(encoding="utf-8"))
|
metadata = json.loads((ROOT / "versions/V2R1C6.json").read_text(encoding="utf-8"))
|
||||||
assert metadata["version"] == "V2R1C6"
|
|
||||||
assert metadata["base_oid"] == "a33e2593967a180c89d44454350ccaeaf3f79b25"
|
assert metadata["base_oid"] == "a33e2593967a180c89d44454350ccaeaf3f79b25"
|
||||||
assert metadata["candidate_branch"] == "agent/v2r1c6-mainline-reconciliation-20260806"
|
assert metadata["candidate_branch"] == "agent/v2r1c6-mainline-reconciliation-20260806"
|
||||||
assert metadata["production_deployed"] is False
|
assert metadata["production_deployed"] is False
|
||||||
|
|||||||
@@ -7,19 +7,17 @@ import pytest
|
|||||||
|
|
||||||
ROOT = Path(__file__).resolve().parents[2]
|
ROOT = Path(__file__).resolve().parents[2]
|
||||||
sys.path.insert(0, str(ROOT / "active-runtime" / "app"))
|
sys.path.insert(0, str(ROOT / "active-runtime" / "app"))
|
||||||
from deployment_state import RuntimeDeploymentState
|
|
||||||
from v2r1c6_privacy_tools import health_payload, serve # noqa: E402
|
from v2r1c6_privacy_tools import health_payload, serve # noqa: E402
|
||||||
|
|
||||||
|
|
||||||
def test_health_is_engineering_only() -> None:
|
def test_health_is_engineering_only() -> None:
|
||||||
result = health_payload(RuntimeDeploymentState("V2R1C7", "a" * 40, "b" * 64, "STAGED"))
|
result = health_payload()
|
||||||
assert result["status"] == "ok"
|
assert result == {
|
||||||
assert result["version"] == "V2R1C7"
|
"status": "ok",
|
||||||
assert result["commit_oid"] == "a" * 40
|
"version": "V2R1C6",
|
||||||
assert result["artifact_sha256"] == "b" * 64
|
"production_deployed": False,
|
||||||
assert result["deployment_state"] == "STAGED"
|
"gitops_writes_enabled": False,
|
||||||
assert result["production_deployed"] is False
|
}
|
||||||
assert result["health_timestamp"]
|
|
||||||
|
|
||||||
|
|
||||||
def test_listener_rejects_non_loopback() -> None:
|
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"
|
ROOT / "docs" / "gitops" / "openapi-chatgpt-actions.yaml"
|
||||||
).read_text(encoding="utf-8")
|
).read_text(encoding="utf-8")
|
||||||
self.assertIn(
|
self.assertIn(
|
||||||
"required: [version, target_oid, evidence_closure_oid, approval_plan_id]",
|
"required: [version, target_oid, evidence_closure_oid, approval_plan_id, execute]",
|
||||||
schema,
|
schema,
|
||||||
)
|
)
|
||||||
self.assertIn("x-openai-isConsequential: true", 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")
|
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:
|
with tempfile.TemporaryDirectory() as root:
|
||||||
directory = Path(root)
|
directory = Path(root)
|
||||||
auth = self._load_auth(self._make_auth_payload(), directory)
|
auth = self._load_auth(self._make_auth_payload(), directory)
|
||||||
@@ -283,8 +283,8 @@ class GovernedLocalWriterTests(unittest.TestCase):
|
|||||||
|
|
||||||
self.assertEqual(service.process_request(create_request, peer)["status"], "ok")
|
self.assertEqual(service.process_request(create_request, peer)["status"], "ok")
|
||||||
self.assertEqual(service.process_request(append_request, peer)["status"], "ok")
|
self.assertEqual(service.process_request(append_request, peer)["status"], "ok")
|
||||||
self.assertEqual(service.process_request(append_request, peer)["status"], "receipt")
|
self.assertEqual(service.process_request(append_request, peer)["status"], "error")
|
||||||
self.assertEqual(len(sink.records), 2)
|
self.assertEqual(len(sink.records), 3)
|
||||||
self.assertEqual(len(handler.create_calls), 1)
|
self.assertEqual(len(handler.create_calls), 1)
|
||||||
self.assertEqual(len(handler.append_calls), 1)
|
self.assertEqual(len(handler.append_calls), 1)
|
||||||
|
|
||||||
|
|||||||
@@ -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