350 lines
18 KiB
Python
350 lines
18 KiB
Python
"""ChatGPT GitOps Bridge v2 contract and governed release operations."""
|
|
from __future__ import annotations
|
|
|
|
from dataclasses import dataclass
|
|
import hashlib
|
|
from pathlib import Path
|
|
from typing import Any, Callable, Mapping
|
|
|
|
from aios_task_admission_bridge_contract import (
|
|
TASK_ADMISSION_OPERATION_OPTIONAL_FIELDS,
|
|
TASK_ADMISSION_OPERATION_REQUIRED_FIELDS,
|
|
TASK_ADMISSION_ROUTES,
|
|
)
|
|
|
|
|
|
BRIDGE_SCHEMA_VERSION = "aios.chatgpt-gitops-bridge.v2"
|
|
BRIDGE_OPERATIONS = (
|
|
"getRuntimeStatus",
|
|
"getGovernedWriteCapabilities",
|
|
"createGovernedTask",
|
|
"projectOnlineAgentExecution",
|
|
"acquireWriterLease",
|
|
"commitFileGoverned",
|
|
"commitFilesGoverned",
|
|
"getGitOpsJob",
|
|
"resumeGitOpsJob",
|
|
"reconcileGitOpsJob",
|
|
"createTagApprovalRequest",
|
|
"requestOrCreateTag",
|
|
"getTagPublicationStatus",
|
|
"buildProductionArtifact",
|
|
"executeApprovedProductionRelease",
|
|
"getReleaseConsistency",
|
|
"runReleaseAcceptance",
|
|
"getControlPlaneSnapshot",
|
|
"listRepositoryTree",
|
|
"searchRepositorySource",
|
|
"getDiffIndex",
|
|
"readRepositoryFile",
|
|
)
|
|
|
|
TASK_ADMISSION_OPERATIONS = tuple(TASK_ADMISSION_OPERATION_REQUIRED_FIELDS)
|
|
|
|
OPERATION_REQUIRED_FIELDS = {
|
|
"createGovernedTask": ("task_id", "repository", "base_oid", "target_branch", "risk", "description", "idempotency_key"),
|
|
"projectOnlineAgentExecution": ("task_id", "required_capabilities"),
|
|
"acquireWriterLease": ("schema", "task_id", "job_id", "repository", "branch_mode", "base_oid", "expected_branch_oid", "target_branch", "generation", "idempotency_key", "lease_seconds"),
|
|
"commitFileGoverned": ("task_id", "job_id", "repository", "expected_branch_oid", "target_branch", "generation", "idempotency_key", "path", "content_base64", "message"),
|
|
"commitFilesGoverned": ("task_id", "job_id", "repository", "expected_branch_oid", "target_branch", "generation", "idempotency_key", "files", "message"),
|
|
"getGitOpsJob": ("task_id", "job_id"),
|
|
"resumeGitOpsJob": ("task_id", "job_id", "expected_revision"),
|
|
"reconcileGitOpsJob": ("task_id", "job_id", "expected_revision"),
|
|
"createTagApprovalRequest": ("version", "target_oid", "evidence_closure_oid", "approval_plan_id", "owner_approval", "execute"),
|
|
"requestOrCreateTag": ("version", "target_oid", "evidence_closure_oid", "approval_plan_id", "execute"),
|
|
"getTagPublicationStatus": ("version",),
|
|
"buildProductionArtifact": ("version", "target_oid", "evidence_closure_oid", "approval_plan_id"),
|
|
"executeApprovedProductionRelease": ("version", "target_oid", "evidence_closure_oid", "approval_plan_id", "execute"),
|
|
"getReleaseConsistency": ("version", "target_oid", "evidence_closure_oid", "approval_plan_id"),
|
|
"runReleaseAcceptance": ("version", "target_oid", "evidence_closure_oid", "approval_plan_id"),
|
|
"getControlPlaneSnapshot": (),
|
|
**TASK_ADMISSION_OPERATION_REQUIRED_FIELDS,
|
|
"listRepositoryTree": ("repository", "ref", "limit"),
|
|
"searchRepositorySource": ("repository", "ref", "query", "max_matches", "max_bytes"),
|
|
"getDiffIndex": ("repository", "from", "to", "limit"),
|
|
"readRepositoryFile": ("repository", "ref", "path"),
|
|
}
|
|
|
|
OPERATION_OPTIONAL_FIELDS = {
|
|
"listRepositoryTree": ("prefix", "depth", "cursor"),
|
|
"searchRepositorySource": ("path_globs", "cursor"),
|
|
"getDiffIndex": ("path_prefix", "cursor"),
|
|
"readRepositoryFile": ("max_bytes",),
|
|
}
|
|
|
|
READ_ONLY_OPERATIONS = frozenset(
|
|
{
|
|
"getRuntimeStatus",
|
|
"getGovernedWriteCapabilities",
|
|
"getControlPlaneSnapshot",
|
|
"listRepositoryTree",
|
|
"searchRepositorySource",
|
|
"getDiffIndex",
|
|
"readRepositoryFile",
|
|
}
|
|
)
|
|
|
|
_OPERATION_PATHS = {
|
|
"createGovernedTask": "/v2/governed-task",
|
|
"projectOnlineAgentExecution": "/v2/execution-envelope",
|
|
"acquireWriterLease": "/v2/writer-lease",
|
|
"commitFileGoverned": "/v2/commit-file",
|
|
"commitFilesGoverned": "/v2/commit-files",
|
|
"getGitOpsJob": "/v2/job",
|
|
"resumeGitOpsJob": "/v2/job/resume",
|
|
"reconcileGitOpsJob": "/v2/job/reconcile",
|
|
"getTagPublicationStatus": "/v2/tag/status",
|
|
"getReleaseConsistency": "/v2/release/consistency",
|
|
"runReleaseAcceptance": "/v2/release/acceptance",
|
|
**{operation: path for path, operation in TASK_ADMISSION_ROUTES.items()},
|
|
"listRepositoryTree": "/v2/repository/tree",
|
|
"searchRepositorySource": "/v2/repository/search",
|
|
"getDiffIndex": "/v2/repository/diff-index",
|
|
"readRepositoryFile": "/v2/repository/file",
|
|
}
|
|
|
|
|
|
def _has_complete_task_admission_handlers(
|
|
handlers: Mapping[str, Callable[[Mapping[str, Any]], Any]] | None,
|
|
) -> bool:
|
|
return handlers is not None and all(
|
|
callable(handlers.get(operation)) for operation in TASK_ADMISSION_OPERATIONS
|
|
)
|
|
|
|
|
|
def operation_contracts(
|
|
handlers: Mapping[str, Callable[[Mapping[str, Any]], Any]] | None = None,
|
|
) -> dict[str, dict[str, object]]:
|
|
"""Return the runtime contract consumed by adapters and schema tooling."""
|
|
contracts = {
|
|
operation: {
|
|
"method": "GET" if operation in OPERATION_OPTIONAL_FIELDS else "POST",
|
|
"path": _OPERATION_PATHS.get(operation),
|
|
"required_fields": list(fields),
|
|
"optional_fields": list(OPERATION_OPTIONAL_FIELDS.get(operation, ())),
|
|
"read_only": operation in READ_ONLY_OPERATIONS,
|
|
}
|
|
for operation, fields in OPERATION_REQUIRED_FIELDS.items()
|
|
if operation not in TASK_ADMISSION_OPERATIONS
|
|
}
|
|
if _has_complete_task_admission_handlers(handlers):
|
|
contracts.update(
|
|
{
|
|
operation: {
|
|
"method": "GET" if operation == "getTaskAdmissionStatus" else "POST",
|
|
"path": _OPERATION_PATHS[operation],
|
|
"required_fields": list(
|
|
TASK_ADMISSION_OPERATION_REQUIRED_FIELDS[operation]
|
|
),
|
|
"optional_fields": list(
|
|
TASK_ADMISSION_OPERATION_OPTIONAL_FIELDS[operation]
|
|
),
|
|
}
|
|
for operation in TASK_ADMISSION_OPERATIONS
|
|
}
|
|
)
|
|
return contracts
|
|
|
|
|
|
def render_openapi_contract_fragment() -> str:
|
|
required = "\n".join(
|
|
f" - {field}"
|
|
for field in OPERATION_REQUIRED_FIELDS["commitFilesGoverned"]
|
|
)
|
|
return (
|
|
" /v2/commit-files:\n"
|
|
" post:\n"
|
|
" operationId: commitFilesGoverned\n"
|
|
" summary: Submit one atomic governed commit containing multiple files\n"
|
|
" security: [{taskPrincipalAuth: []}]\n"
|
|
" x-aios-task-principal: Bridge resolves a fresh server-side TaskPrincipal from the authoritative admitted+claimed Task and re-verifies its Owner epoch, then checks exact task/job/repository/branch/generation/capability binding; caller JSON cannot provide a principal and missing verification is denied. An explicitly injected legacy bearer verifier may still be used by a trusted host.\n"
|
|
" requestBody:\n"
|
|
" required: true\n"
|
|
" content:\n"
|
|
" application/json:\n"
|
|
" schema:\n"
|
|
" type: object\n"
|
|
" additionalProperties: false\n"
|
|
" required:\n"
|
|
f"{required}\n"
|
|
" properties:\n"
|
|
" task_id: {type: string}\n"
|
|
" job_id: {type: string, description: External durable Job identity for exact completed-replay reconciliation}\n"
|
|
" source_lease_epoch: {type: integer, minimum: 1}\n"
|
|
" repository: {type: string, example: aios/aios}\n"
|
|
" expected_branch_oid: {type: string, pattern: '^[0-9a-f]{40}$'}\n"
|
|
" target_branch: {type: string, pattern: '^agent/[A-Za-z0-9._/-]+$'}\n"
|
|
" generation: {type: integer, minimum: 1}\n"
|
|
" idempotency_key: {type: string}\n"
|
|
" files:\n"
|
|
" type: array\n"
|
|
" minItems: 1\n"
|
|
" items:\n"
|
|
" type: object\n"
|
|
" additionalProperties: false\n"
|
|
" required: [path, content_base64]\n"
|
|
" properties:\n"
|
|
" path: {type: string, minLength: 1, maxLength: 512}\n"
|
|
" content_base64: {type: string}\n"
|
|
" message: {type: string, minLength: 1, maxLength: 200}\n"
|
|
" responses:\n"
|
|
" '200': {description: Governed commit job and verified branch result}\n"
|
|
" '202': {description: Existing external Job alias requires authoritative reconciliation; no replacement Job is created}\n"
|
|
" '401': {description: No verified Task principal is available}\n"
|
|
" '403': {description: Verified Task principal lacks exact task, external Job, or capability scope}\n"
|
|
" '400': {description: Contract or policy rejection}\n"
|
|
" '409': {description: Branch CAS or idempotency conflict}\n"
|
|
" '423': {description: Narrow P0 Task-principal mutation gate is frozen; assertion is not consumed}\n"
|
|
)
|
|
|
|
|
|
class RuntimeDriftError(RuntimeError):
|
|
pass
|
|
|
|
|
|
class BridgeV2Error(RuntimeError):
|
|
pass
|
|
|
|
|
|
@dataclass(frozen=True, slots=True)
|
|
class ControlPlaneReleaseManifest:
|
|
control_plane_release_version: str
|
|
control_plane_target_oid: str
|
|
artifact_sha256: str
|
|
runtime_path: Path
|
|
bridge_schema_version: str
|
|
repository_identity: str
|
|
authoritative_git_dir: Path
|
|
deployment_state: str
|
|
|
|
def as_dict(self) -> dict[str, object]:
|
|
return {
|
|
"control_plane_release_version": self.control_plane_release_version,
|
|
"control_plane_target_oid": self.control_plane_target_oid,
|
|
"artifact_sha256": self.artifact_sha256,
|
|
"runtime_path": str(self.runtime_path),
|
|
"bridge_schema_version": self.bridge_schema_version,
|
|
"repository_identity": self.repository_identity,
|
|
"authoritative_git_dir": str(self.authoritative_git_dir),
|
|
"deployment_state": self.deployment_state,
|
|
}
|
|
|
|
def validate(self, artifact: Path, *, observed_oid: str, approved_root: Path = Path("/opt/aios/gitops-control-plane")) -> dict[str, object]:
|
|
try:
|
|
runtime = self.runtime_path.resolve(strict=True)
|
|
root = approved_root.resolve(strict=True)
|
|
artifact_path = artifact.resolve(strict=True)
|
|
runtime.relative_to(root)
|
|
artifact_path.relative_to(runtime)
|
|
except (OSError, ValueError):
|
|
raise RuntimeDriftError("CONTROL_PLANE_RUNTIME_DRIFT") from None
|
|
mutable_markers = {"workspace", "workspaces", "worktree", "worktrees"}
|
|
if mutable_markers.intersection(part.lower() for part in runtime.parts):
|
|
raise RuntimeDriftError("CONTROL_PLANE_RUNTIME_DRIFT")
|
|
valid = (
|
|
len(self.control_plane_target_oid) == 40
|
|
and observed_oid == self.control_plane_target_oid
|
|
and len(self.artifact_sha256) == 64
|
|
and hashlib.sha256(artifact_path.read_bytes()).hexdigest() == self.artifact_sha256
|
|
and self.bridge_schema_version == BRIDGE_SCHEMA_VERSION
|
|
and self.repository_identity == "aios/aios"
|
|
and self.authoritative_git_dir == Path("/data/aios-gitops/repo.git")
|
|
and self.authoritative_git_dir.is_absolute()
|
|
and self.deployment_state == "STAGING"
|
|
)
|
|
if not valid:
|
|
raise RuntimeDriftError("CONTROL_PLANE_RUNTIME_DRIFT")
|
|
return {**self.as_dict(), "drift_status": "PASS"}
|
|
|
|
|
|
class BridgeV2Adapter:
|
|
"""Thin operation adapter; all mutating handlers are injected seams."""
|
|
|
|
def __init__(self, *, runtime_status: Mapping[str, Any] | Callable[[], Mapping[str, Any]], handlers: Mapping[str, Callable[[Mapping[str, Any]], Any]] | None = None) -> None:
|
|
self._runtime_status_source = runtime_status if callable(runtime_status) else dict(runtime_status)
|
|
self.handlers = dict(handlers or {})
|
|
|
|
def _runtime_status(self) -> dict[str, Any]:
|
|
value = self._runtime_status_source() if callable(self._runtime_status_source) else self._runtime_status_source
|
|
if not isinstance(value, Mapping):
|
|
raise BridgeV2Error("RUNTIME_STATUS_INVALID")
|
|
return dict(value)
|
|
|
|
def getRuntimeStatus(self) -> dict[str, Any]:
|
|
return {"bridge_schema_version": BRIDGE_SCHEMA_VERSION, **self._runtime_status()}
|
|
|
|
def getGovernedWriteCapabilities(self) -> dict[str, Any]:
|
|
runtime_status = self._runtime_status()
|
|
governed_writes_enabled = runtime_status.get("governed_writes_enabled") is True
|
|
return {
|
|
"bridge_schema_version": BRIDGE_SCHEMA_VERSION,
|
|
"legacy_writes_enabled": False,
|
|
"governed_writes_enabled": governed_writes_enabled,
|
|
"governed_write_blockers": list(
|
|
runtime_status.get("governed_write_blockers") or []
|
|
),
|
|
"operations": [
|
|
*BRIDGE_OPERATIONS,
|
|
*(
|
|
TASK_ADMISSION_OPERATIONS
|
|
if _has_complete_task_admission_handlers(self.handlers)
|
|
else ()
|
|
),
|
|
],
|
|
"operation_contracts": operation_contracts(self.handlers),
|
|
"force_push": False,
|
|
"main_write": False,
|
|
"tag_write": False,
|
|
"baseline_write": False,
|
|
"production_write": False,
|
|
"approved_release_operations": True,
|
|
"approved_release_scope": {
|
|
"version": "V2R2C1",
|
|
"target_oid": "998d30304787e31e5ae607c0bd86b5edb2b0e5ad",
|
|
"evidence_closure_oid": "a8b906d1bc24088c19647e98de6a679268f6d0a2",
|
|
"approval_plan_id": "sha256:c0df52008ae7f64cec5ee2d28947aa7b892066c670ab8295e4549140b1a7500e",
|
|
},
|
|
}
|
|
|
|
@staticmethod
|
|
def legacy_write_disabled() -> dict[str, object]:
|
|
return {"error": "LEGACY_WRITE_DISABLED", "replacement": "createGovernedTask", "governed_writes_available": False}
|
|
|
|
def _call(self, operation: str, request: Mapping[str, Any]) -> Any:
|
|
required = OPERATION_REQUIRED_FIELDS[operation]
|
|
if any(key not in request for key in required):
|
|
raise BridgeV2Error("BRIDGE_REQUEST_FIELDS_MISSING")
|
|
if operation in OPERATION_OPTIONAL_FIELDS:
|
|
allowed = set(required) | set(OPERATION_OPTIONAL_FIELDS[operation])
|
|
if set(request) - allowed:
|
|
raise BridgeV2Error("BRIDGE_REQUEST_FIELDS_UNKNOWN")
|
|
handler = self.handlers.get(operation)
|
|
if handler is None:
|
|
raise BridgeV2Error("GOVERNED_HANDLER_UNAVAILABLE")
|
|
return handler(dict(request))
|
|
|
|
def createGovernedTask(self, request: Mapping[str, Any]) -> Any: return self._call("createGovernedTask", request)
|
|
def projectOnlineAgentExecution(self, request: Mapping[str, Any]) -> Any: return self._call("projectOnlineAgentExecution", request)
|
|
def acquireWriterLease(self, request: Mapping[str, Any]) -> Any: return self._call("acquireWriterLease", request)
|
|
def commitFileGoverned(self, request: Mapping[str, Any]) -> Any: return self._call("commitFileGoverned", request)
|
|
def commitFilesGoverned(self, request: Mapping[str, Any]) -> Any: return self._call("commitFilesGoverned", request)
|
|
def getGitOpsJob(self, request: Mapping[str, Any]) -> Any: return self._call("getGitOpsJob", request)
|
|
def resumeGitOpsJob(self, request: Mapping[str, Any]) -> Any: return self._call("resumeGitOpsJob", request)
|
|
def reconcileGitOpsJob(self, request: Mapping[str, Any]) -> Any: return self._call("reconcileGitOpsJob", request)
|
|
def createTagApprovalRequest(self, request: Mapping[str, Any]) -> Any: return self._call("createTagApprovalRequest", request)
|
|
def requestOrCreateTag(self, request: Mapping[str, Any]) -> Any: return self._call("requestOrCreateTag", request)
|
|
def getTagPublicationStatus(self, request: Mapping[str, Any]) -> Any: return self._call("getTagPublicationStatus", request)
|
|
def buildProductionArtifact(self, request: Mapping[str, Any]) -> Any: return self._call("buildProductionArtifact", request)
|
|
def executeApprovedProductionRelease(self, request: Mapping[str, Any]) -> Any: return self._call("executeApprovedProductionRelease", request)
|
|
def getReleaseConsistency(self, request: Mapping[str, Any]) -> Any: return self._call("getReleaseConsistency", request)
|
|
def runReleaseAcceptance(self, request: Mapping[str, Any]) -> Any: return self._call("runReleaseAcceptance", request)
|
|
def getTaskAdmissionEnvelope(self, request: Mapping[str, Any]) -> Any: return self._call("getTaskAdmissionEnvelope", request)
|
|
def submitTaskAdmissionAuthorization(self, request: Mapping[str, Any]) -> Any: return self._call("submitTaskAdmissionAuthorization", request)
|
|
def getTaskAdmissionStatus(self, request: Mapping[str, Any]) -> Any: return self._call("getTaskAdmissionStatus", request)
|
|
def listRepositoryTree(self, request: Mapping[str, Any]) -> Any: return self._call("listRepositoryTree", request)
|
|
def searchRepositorySource(self, request: Mapping[str, Any]) -> Any: return self._call("searchRepositorySource", request)
|
|
def getDiffIndex(self, request: Mapping[str, Any]) -> Any: return self._call("getDiffIndex", request)
|
|
def readRepositoryFile(self, request: Mapping[str, Any]) -> Any: return self._call("readRepositoryFile", request)
|
|
|
|
def getControlPlaneSnapshot(self, request: Mapping[str, Any] | None = None) -> Any: return self._call("getControlPlaneSnapshot", request or {})
|