Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0aa45ee2f9 |
@@ -1,748 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Server-local governed immutable Host seed executor.
|
||||
|
||||
This executor is deliberately not a general-purpose privileged command runner.
|
||||
It polls one fixed Git ref/path, verifies a high-risk governed-task binding as
|
||||
the durable authorization evidence, materializes one exact candidate from the
|
||||
authoritative Git object database plus a declared patch/manifest, and atomically
|
||||
publishes the immutable release tree.
|
||||
|
||||
It never starts/restarts services, never creates the continuation Task/Job,
|
||||
never writes Git refs, and never switches Production.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime, timezone
|
||||
import argparse
|
||||
import fcntl
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
import pwd
|
||||
from pathlib import Path, PurePosixPath
|
||||
import re
|
||||
import shutil
|
||||
import stat
|
||||
import subprocess
|
||||
import tempfile
|
||||
from typing import Any, Mapping
|
||||
|
||||
REQUEST_SCHEMA = "aios.control-plane-host-seed-request.v1"
|
||||
MANIFEST_SCHEMAS = {
|
||||
"aios.control-plane-immutable-candidate.v5",
|
||||
"aios.control-plane-immutable-candidate.v6",
|
||||
}
|
||||
OPERATION = "HOST_SEED_IMMUTABLE_CANDIDATE"
|
||||
REPOSITORY = "aios/aios"
|
||||
REQUEST_REF = "refs/heads/agent/host-seed-authorization-requests"
|
||||
REQUEST_PATH = "control-plane/host-seed/request.json"
|
||||
OID_RE = re.compile(r"^[0-9a-f]{40}$")
|
||||
SHA_RE = re.compile(r"^[0-9a-f]{64}$")
|
||||
TASK_RE = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._:-]{2,179}$")
|
||||
RELEASE_RE = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._-]{3,191}$")
|
||||
CANDIDATE_RE = re.compile(r"^V[0-9]+$")
|
||||
MAX_REQUEST_BYTES = 128 * 1024
|
||||
MAX_MANIFEST_BYTES = 4 * 1024 * 1024
|
||||
MAX_PATCH_BYTES = 2 * 1024 * 1024
|
||||
MAX_MACHINE_GOAL_BYTES = 512 * 1024
|
||||
MAX_BOOTSTRAP_BYTES = 512 * 1024
|
||||
|
||||
|
||||
class HostSeedError(RuntimeError):
|
||||
pass
|
||||
|
||||
|
||||
def canonical(value: object) -> bytes:
|
||||
return (json.dumps(value, ensure_ascii=False, sort_keys=True, separators=(",", ":")) + "\n").encode("utf-8")
|
||||
|
||||
|
||||
def sha256(data: bytes) -> str:
|
||||
return hashlib.sha256(data).hexdigest()
|
||||
|
||||
|
||||
def canonical_compact(value: object) -> bytes:
|
||||
return json.dumps(
|
||||
value, ensure_ascii=False, sort_keys=True, separators=(",", ":")
|
||||
).encode("utf-8")
|
||||
|
||||
|
||||
def _git(git_dir: Path, *args: str, input_bytes: bytes | None = None) -> bytes:
|
||||
env = {"PATH": os.environ.get("PATH", "")}
|
||||
proc = subprocess.run(
|
||||
["git", f"--git-dir={git_dir}", *args],
|
||||
input=input_bytes,
|
||||
capture_output=True,
|
||||
timeout=120,
|
||||
env=env,
|
||||
)
|
||||
if proc.returncode:
|
||||
raise HostSeedError("GIT_" + (args[0] if args else "COMMAND").upper() + "_FAILED")
|
||||
return proc.stdout
|
||||
|
||||
|
||||
def _git_text(git_dir: Path, *args: str) -> str:
|
||||
return _git(git_dir, *args).decode("utf-8", "strict").strip()
|
||||
|
||||
|
||||
def _safe_repo_path(value: object, *, field: str) -> str:
|
||||
if not isinstance(value, str) or not value or len(value) > 512:
|
||||
raise HostSeedError(field + "_INVALID")
|
||||
p = PurePosixPath(value)
|
||||
if p.is_absolute() or any(part in {"", ".", ".."} for part in p.parts):
|
||||
raise HostSeedError(field + "_INVALID")
|
||||
normalized = p.as_posix()
|
||||
if normalized.startswith(".git/") or normalized == ".git":
|
||||
raise HostSeedError(field + "_INVALID")
|
||||
return normalized
|
||||
|
||||
|
||||
def _read_git_blob(git_dir: Path, oid: str, path: str, *, maximum: int) -> bytes:
|
||||
if not OID_RE.fullmatch(oid):
|
||||
raise HostSeedError("PAYLOAD_COMMIT_INVALID")
|
||||
raw = _git(git_dir, "show", f"{oid}:{path}")
|
||||
if len(raw) <= 0 or len(raw) > maximum:
|
||||
raise HostSeedError("GIT_BLOB_SIZE_INVALID")
|
||||
return raw
|
||||
|
||||
|
||||
def _parse_json(raw: bytes, *, code: str) -> dict[str, Any]:
|
||||
try:
|
||||
value = json.loads(raw.decode("utf-8"))
|
||||
except (UnicodeDecodeError, json.JSONDecodeError) as exc:
|
||||
raise HostSeedError(code) from exc
|
||||
if not isinstance(value, dict):
|
||||
raise HostSeedError(code)
|
||||
return value
|
||||
|
||||
|
||||
def _parse_time(value: object, *, field: str) -> datetime:
|
||||
if not isinstance(value, str) or not value:
|
||||
raise HostSeedError(field + "_INVALID")
|
||||
text = value[:-1] + "+00:00" if value.endswith("Z") else value
|
||||
try:
|
||||
parsed = datetime.fromisoformat(text)
|
||||
except ValueError as exc:
|
||||
raise HostSeedError(field + "_INVALID") from exc
|
||||
if parsed.tzinfo is None:
|
||||
raise HostSeedError(field + "_INVALID")
|
||||
return parsed.astimezone(timezone.utc)
|
||||
|
||||
|
||||
def _regular_file_bytes(
|
||||
path: Path, *, maximum: int, expected_uid: int | None = None
|
||||
) -> bytes:
|
||||
if path.parent.is_symlink():
|
||||
raise HostSeedError("AUTHORIZATION_BINDING_PARENT_SYMLINK_REJECTED")
|
||||
flags = os.O_RDONLY | getattr(os, "O_CLOEXEC", 0) | getattr(os, "O_NOFOLLOW", 0)
|
||||
try:
|
||||
fd = os.open(path, flags)
|
||||
except OSError as exc:
|
||||
raise HostSeedError("AUTHORIZATION_BINDING_UNAVAILABLE") from exc
|
||||
try:
|
||||
info = os.fstat(fd)
|
||||
if not stat.S_ISREG(info.st_mode) or info.st_size <= 0 or info.st_size > maximum:
|
||||
raise HostSeedError("AUTHORIZATION_BINDING_INVALID")
|
||||
if info.st_mode & 0o022:
|
||||
raise HostSeedError("AUTHORIZATION_BINDING_WRITABLE_BY_GROUP_OR_OTHER")
|
||||
if expected_uid is not None and info.st_uid != expected_uid:
|
||||
raise HostSeedError("AUTHORIZATION_BINDING_OWNER_MISMATCH")
|
||||
raw = os.read(fd, maximum + 1)
|
||||
if len(raw) > maximum:
|
||||
raise HostSeedError("AUTHORIZATION_BINDING_INVALID")
|
||||
return raw
|
||||
finally:
|
||||
os.close(fd)
|
||||
|
||||
|
||||
def _atomic_json(path: Path, value: Mapping[str, Any], *, mode: int = 0o600) -> None:
|
||||
path.parent.mkdir(parents=True, exist_ok=True, mode=0o700)
|
||||
if path.parent.is_symlink() or path.is_symlink():
|
||||
raise HostSeedError("STATE_SYMLINK_REJECTED")
|
||||
fd, temporary = tempfile.mkstemp(prefix=".tmp-", dir=path.parent)
|
||||
try:
|
||||
os.fchmod(fd, mode)
|
||||
with os.fdopen(fd, "wb", closefd=True) as stream:
|
||||
stream.write(canonical(value))
|
||||
stream.flush()
|
||||
os.fsync(stream.fileno())
|
||||
os.replace(temporary, path)
|
||||
os.chmod(path, mode)
|
||||
parent_fd = os.open(path.parent, os.O_RDONLY | getattr(os, "O_DIRECTORY", 0))
|
||||
try:
|
||||
os.fsync(parent_fd)
|
||||
finally:
|
||||
os.close(parent_fd)
|
||||
finally:
|
||||
if os.path.exists(temporary):
|
||||
os.unlink(temporary)
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class Intent:
|
||||
candidate: str
|
||||
release_id: str
|
||||
base_oid: str
|
||||
payload_commit: str
|
||||
patch_path: str
|
||||
manifest_path: str
|
||||
machine_goal_path: str
|
||||
seed_bootstrap_path: str
|
||||
artifact_sha256: str
|
||||
manifest_sha256: str
|
||||
machine_goal_sha256: str
|
||||
release_tree_sha256: str
|
||||
seed_bootstrap_sha256: str
|
||||
target_host: str
|
||||
|
||||
def as_dict(self) -> dict[str, Any]:
|
||||
return {
|
||||
"candidate": self.candidate,
|
||||
"release_id": self.release_id,
|
||||
"base_oid": self.base_oid,
|
||||
"payload_commit": self.payload_commit,
|
||||
"patch_path": self.patch_path,
|
||||
"manifest_path": self.manifest_path,
|
||||
"machine_goal_path": self.machine_goal_path,
|
||||
"seed_bootstrap_path": self.seed_bootstrap_path,
|
||||
"artifact_sha256": self.artifact_sha256,
|
||||
"manifest_sha256": self.manifest_sha256,
|
||||
"machine_goal_sha256": self.machine_goal_sha256,
|
||||
"release_tree_sha256": self.release_tree_sha256,
|
||||
"seed_bootstrap_sha256": self.seed_bootstrap_sha256,
|
||||
"target_host": self.target_host,
|
||||
}
|
||||
|
||||
@property
|
||||
def digest(self) -> str:
|
||||
return sha256(canonical(self.as_dict()))
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class Request:
|
||||
intent: Intent
|
||||
authorization_task_id: str
|
||||
authorization_binding_sha256: str
|
||||
authorization_sha256: str
|
||||
expires_at: datetime
|
||||
|
||||
@classmethod
|
||||
def load(cls, raw: Mapping[str, Any], *, host_identity: str, now: datetime) -> "Request":
|
||||
if set(raw) != {"schema", "operation", "repository", "intent", "authorization"}:
|
||||
raise HostSeedError("REQUEST_FIELDS_INVALID")
|
||||
if raw.get("schema") != REQUEST_SCHEMA or raw.get("operation") != OPERATION:
|
||||
raise HostSeedError("REQUEST_CONTRACT_INVALID")
|
||||
if raw.get("repository") != REPOSITORY:
|
||||
raise HostSeedError("REQUEST_REPOSITORY_MISMATCH")
|
||||
intent_raw = raw.get("intent")
|
||||
if not isinstance(intent_raw, Mapping) or set(intent_raw) != {
|
||||
"candidate", "release_id", "base_oid", "payload_commit", "patch_path",
|
||||
"manifest_path", "machine_goal_path", "seed_bootstrap_path",
|
||||
"artifact_sha256", "manifest_sha256", "machine_goal_sha256",
|
||||
"release_tree_sha256", "seed_bootstrap_sha256", "target_host",
|
||||
}:
|
||||
raise HostSeedError("INTENT_FIELDS_INVALID")
|
||||
candidate = str(intent_raw.get("candidate") or "")
|
||||
release_id = str(intent_raw.get("release_id") or "")
|
||||
base_oid = str(intent_raw.get("base_oid") or "")
|
||||
payload_commit = str(intent_raw.get("payload_commit") or "")
|
||||
artifact_sha = str(intent_raw.get("artifact_sha256") or "")
|
||||
manifest_sha = str(intent_raw.get("manifest_sha256") or "")
|
||||
machine_goal_sha = str(intent_raw.get("machine_goal_sha256") or "")
|
||||
release_tree_sha = str(intent_raw.get("release_tree_sha256") or "")
|
||||
seed_bootstrap_sha = str(intent_raw.get("seed_bootstrap_sha256") or "")
|
||||
target_host = str(intent_raw.get("target_host") or "")
|
||||
if not CANDIDATE_RE.fullmatch(candidate):
|
||||
raise HostSeedError("CANDIDATE_INVALID")
|
||||
if not RELEASE_RE.fullmatch(release_id):
|
||||
raise HostSeedError("RELEASE_ID_INVALID")
|
||||
if not OID_RE.fullmatch(base_oid) or not OID_RE.fullmatch(payload_commit):
|
||||
raise HostSeedError("INTENT_OID_INVALID")
|
||||
if not all(SHA_RE.fullmatch(x) for x in (
|
||||
artifact_sha, manifest_sha, machine_goal_sha, release_tree_sha, seed_bootstrap_sha
|
||||
)):
|
||||
raise HostSeedError("INTENT_SHA_INVALID")
|
||||
if target_host != host_identity:
|
||||
raise HostSeedError("TARGET_HOST_MISMATCH")
|
||||
intent = Intent(
|
||||
candidate=candidate,
|
||||
release_id=release_id,
|
||||
base_oid=base_oid,
|
||||
payload_commit=payload_commit,
|
||||
patch_path=_safe_repo_path(intent_raw.get("patch_path"), field="PATCH_PATH"),
|
||||
manifest_path=_safe_repo_path(intent_raw.get("manifest_path"), field="MANIFEST_PATH"),
|
||||
machine_goal_path=_safe_repo_path(intent_raw.get("machine_goal_path"), field="MACHINE_GOAL_PATH"),
|
||||
seed_bootstrap_path=_safe_repo_path(intent_raw.get("seed_bootstrap_path"), field="SEED_BOOTSTRAP_PATH"),
|
||||
artifact_sha256=artifact_sha,
|
||||
manifest_sha256=manifest_sha,
|
||||
machine_goal_sha256=machine_goal_sha,
|
||||
release_tree_sha256=release_tree_sha,
|
||||
seed_bootstrap_sha256=seed_bootstrap_sha,
|
||||
target_host=target_host,
|
||||
)
|
||||
auth = raw.get("authorization")
|
||||
if not isinstance(auth, Mapping) or set(auth) != {
|
||||
"task_id", "binding_sha256", "expires_at", "intent_sha256",
|
||||
"authorization_sha256",
|
||||
}:
|
||||
raise HostSeedError("AUTHORIZATION_FIELDS_INVALID")
|
||||
task_id = str(auth.get("task_id") or "")
|
||||
binding_sha = str(auth.get("binding_sha256") or "")
|
||||
authorization_sha = str(auth.get("authorization_sha256") or "")
|
||||
if (
|
||||
not TASK_RE.fullmatch(task_id)
|
||||
or not SHA_RE.fullmatch(binding_sha)
|
||||
or not SHA_RE.fullmatch(authorization_sha)
|
||||
):
|
||||
raise HostSeedError("AUTHORIZATION_IDENTITY_INVALID")
|
||||
if auth.get("intent_sha256") != intent.digest:
|
||||
raise HostSeedError("AUTHORIZATION_INTENT_MISMATCH")
|
||||
expires_text = str(auth.get("expires_at") or "")
|
||||
expires_at = _parse_time(expires_text, field="AUTHORIZATION_EXPIRES_AT")
|
||||
expected_authorization_sha = sha256(canonical({
|
||||
"task_id": task_id,
|
||||
"intent_sha256": intent.digest,
|
||||
"expires_at": expires_text,
|
||||
}))
|
||||
if authorization_sha != expected_authorization_sha:
|
||||
raise HostSeedError("AUTHORIZATION_DIGEST_MISMATCH")
|
||||
if now >= expires_at:
|
||||
raise HostSeedError("AUTHORIZATION_EXPIRED")
|
||||
if (expires_at - now).total_seconds() > 3600:
|
||||
raise HostSeedError("AUTHORIZATION_WINDOW_TOO_LARGE")
|
||||
return cls(
|
||||
intent=intent,
|
||||
authorization_task_id=task_id,
|
||||
authorization_binding_sha256=binding_sha,
|
||||
authorization_sha256=authorization_sha,
|
||||
expires_at=expires_at,
|
||||
)
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class Config:
|
||||
git_dir: Path
|
||||
task_binding_root: Path
|
||||
state_root: Path
|
||||
release_root: Path
|
||||
host_identity: str
|
||||
task_binding_owner: str | None = "aios-bridge-staging"
|
||||
request_ref: str = REQUEST_REF
|
||||
request_path: str = REQUEST_PATH
|
||||
|
||||
|
||||
def _load_task_binding(config: Config, request: Request) -> dict[str, Any]:
|
||||
safe = re.sub(r"[^A-Za-z0-9_.-]+", "-", request.authorization_task_id).strip("-")[:120] or "task"
|
||||
path = config.task_binding_root / f"{safe}.json"
|
||||
expected_uid = None
|
||||
if config.task_binding_owner:
|
||||
try:
|
||||
expected_uid = pwd.getpwnam(config.task_binding_owner).pw_uid
|
||||
except KeyError as exc:
|
||||
raise HostSeedError("AUTHORIZATION_BINDING_OWNER_UNKNOWN") from exc
|
||||
raw = _regular_file_bytes(path, maximum=256 * 1024, expected_uid=expected_uid)
|
||||
if sha256(raw) != request.authorization_binding_sha256:
|
||||
raise HostSeedError("AUTHORIZATION_BINDING_HASH_MISMATCH")
|
||||
binding = _parse_json(raw, code="AUTHORIZATION_BINDING_JSON_INVALID")
|
||||
required = {
|
||||
"task_id", "repository", "base_oid", "target_branch", "risk",
|
||||
"description", "idempotency_key", "expected_branch_oid",
|
||||
"branch_mode", "generation", "job_id",
|
||||
}
|
||||
if set(binding) != required:
|
||||
raise HostSeedError("AUTHORIZATION_BINDING_FIELDS_INVALID")
|
||||
if binding.get("task_id") != request.authorization_task_id:
|
||||
raise HostSeedError("AUTHORIZATION_TASK_MISMATCH")
|
||||
if binding.get("repository") != REPOSITORY:
|
||||
raise HostSeedError("AUTHORIZATION_REPOSITORY_MISMATCH")
|
||||
if binding.get("risk") != "high":
|
||||
raise HostSeedError("AUTHORIZATION_RISK_MISMATCH")
|
||||
if binding.get("description") != "HOST_SEED_AUTHORIZATION:" + request.authorization_sha256:
|
||||
raise HostSeedError("AUTHORIZATION_DESCRIPTION_MISMATCH")
|
||||
if not str(binding.get("target_branch") or "").startswith("agent/host-seed-authorization-"):
|
||||
raise HostSeedError("AUTHORIZATION_BRANCH_MISMATCH")
|
||||
if not isinstance(binding.get("generation"), int) or binding["generation"] < 1:
|
||||
raise HostSeedError("AUTHORIZATION_GENERATION_INVALID")
|
||||
return binding
|
||||
|
||||
|
||||
def _load_manifest(config: Config, request: Request) -> dict[str, Any]:
|
||||
raw = _read_git_blob(
|
||||
config.git_dir, request.intent.payload_commit, request.intent.manifest_path,
|
||||
maximum=MAX_MANIFEST_BYTES,
|
||||
)
|
||||
if sha256(raw) != request.intent.manifest_sha256:
|
||||
raise HostSeedError("MANIFEST_HASH_MISMATCH")
|
||||
manifest = _parse_json(raw, code="MANIFEST_JSON_INVALID")
|
||||
if manifest.get("schema") not in MANIFEST_SCHEMAS:
|
||||
raise HostSeedError("MANIFEST_SCHEMA_REJECTED")
|
||||
if manifest.get("candidate") != request.intent.candidate:
|
||||
raise HostSeedError("MANIFEST_CANDIDATE_MISMATCH")
|
||||
if manifest.get("repository") != REPOSITORY:
|
||||
raise HostSeedError("MANIFEST_REPOSITORY_MISMATCH")
|
||||
if manifest.get("base_oid") != request.intent.base_oid:
|
||||
raise HostSeedError("MANIFEST_BASE_MISMATCH")
|
||||
if manifest.get("release_id") != request.intent.release_id:
|
||||
raise HostSeedError("MANIFEST_RELEASE_ID_MISMATCH")
|
||||
members = manifest.get("package_members")
|
||||
changes = manifest.get("changed_files")
|
||||
if not isinstance(members, Mapping) or not isinstance(changes, list):
|
||||
raise HostSeedError("MANIFEST_MEMBERS_INVALID")
|
||||
return manifest
|
||||
|
||||
|
||||
def _load_identity_evidence(config: Config, request: Request) -> dict[str, Any]:
|
||||
goal_raw = _read_git_blob(
|
||||
config.git_dir, request.intent.payload_commit, request.intent.machine_goal_path,
|
||||
maximum=MAX_MACHINE_GOAL_BYTES,
|
||||
)
|
||||
if sha256(goal_raw) != request.intent.machine_goal_sha256:
|
||||
raise HostSeedError("MACHINE_GOAL_HASH_MISMATCH")
|
||||
goal = _parse_json(goal_raw, code="MACHINE_GOAL_JSON_INVALID")
|
||||
required_pairs = {
|
||||
"candidate": request.intent.candidate,
|
||||
"repository": REPOSITORY,
|
||||
"base_oid": request.intent.base_oid,
|
||||
"release_id": request.intent.release_id,
|
||||
"artifact_sha256": request.intent.artifact_sha256,
|
||||
"manifest_sha256": request.intent.manifest_sha256,
|
||||
"release_tree_sha256": request.intent.release_tree_sha256,
|
||||
"seed_bootstrap_sha256": request.intent.seed_bootstrap_sha256,
|
||||
}
|
||||
for key, expected in required_pairs.items():
|
||||
if goal.get(key) != expected:
|
||||
raise HostSeedError("MACHINE_GOAL_BINDING_MISMATCH:" + key)
|
||||
if goal.get("host_seed_authorized") is not False:
|
||||
raise HostSeedError("MACHINE_GOAL_HOST_SEED_FLAG_INVALID")
|
||||
bootstrap = _read_git_blob(
|
||||
config.git_dir, request.intent.payload_commit, request.intent.seed_bootstrap_path,
|
||||
maximum=MAX_BOOTSTRAP_BYTES,
|
||||
)
|
||||
if sha256(bootstrap) != request.intent.seed_bootstrap_sha256:
|
||||
raise HostSeedError("SEED_BOOTSTRAP_HASH_MISMATCH")
|
||||
return goal
|
||||
|
||||
|
||||
def _load_patch(config: Config, request: Request) -> bytes:
|
||||
patch = _read_git_blob(
|
||||
config.git_dir, request.intent.payload_commit, request.intent.patch_path,
|
||||
maximum=MAX_PATCH_BYTES,
|
||||
)
|
||||
if b"\x00" in patch:
|
||||
raise HostSeedError("PATCH_BINARY_NUL_REJECTED")
|
||||
return patch
|
||||
|
||||
|
||||
def _manifest_release_map(manifest: Mapping[str, Any]) -> dict[str, str]:
|
||||
members = manifest["package_members"]
|
||||
result: dict[str, str] = {}
|
||||
prefix = "payload/repo/after/"
|
||||
for name, digest in members.items():
|
||||
if not isinstance(name, str) or not name.startswith(prefix) or not isinstance(digest, str) or not SHA_RE.fullmatch(digest):
|
||||
continue
|
||||
rel = _safe_repo_path(name[len(prefix):], field="RELEASE_MEMBER")
|
||||
if rel in result:
|
||||
raise HostSeedError("DUPLICATE_RELEASE_MEMBER")
|
||||
result[rel] = digest
|
||||
declared_count = manifest.get("release_member_count")
|
||||
if not isinstance(declared_count, int) or declared_count != len(result):
|
||||
raise HostSeedError("RELEASE_MEMBER_COUNT_MISMATCH")
|
||||
return result
|
||||
|
||||
|
||||
def _expected_changes(manifest: Mapping[str, Any]) -> dict[str, tuple[str, str]]:
|
||||
result: dict[str, tuple[str, str]] = {}
|
||||
for entry in manifest["changed_files"]:
|
||||
if not isinstance(entry, Mapping):
|
||||
raise HostSeedError("CHANGED_FILE_ENTRY_INVALID")
|
||||
rel = _safe_repo_path(entry.get("path"), field="CHANGED_PATH")
|
||||
digest = str(entry.get("after_sha256") or "")
|
||||
status = str(entry.get("status") or "")
|
||||
if not SHA_RE.fullmatch(digest) or status not in {"modified", "added"} or rel in result:
|
||||
raise HostSeedError("CHANGED_FILE_ENTRY_INVALID")
|
||||
result[rel] = (digest, status)
|
||||
if manifest.get("changed_file_count") != len(result):
|
||||
raise HostSeedError("CHANGED_FILE_COUNT_MISMATCH")
|
||||
return result
|
||||
|
||||
|
||||
def _regular_member(root: Path, relative: str) -> Path:
|
||||
current = root
|
||||
for part in PurePosixPath(relative).parts:
|
||||
current = current / part
|
||||
try:
|
||||
info = current.lstat()
|
||||
except FileNotFoundError as exc:
|
||||
raise HostSeedError("RELEASE_MEMBER_MISSING:" + relative) from exc
|
||||
if stat.S_ISLNK(info.st_mode):
|
||||
raise HostSeedError("RELEASE_MEMBER_SYMLINK_REJECTED:" + relative)
|
||||
if not current.is_file():
|
||||
raise HostSeedError("RELEASE_MEMBER_NOT_REGULAR:" + relative)
|
||||
return current
|
||||
|
||||
|
||||
def _materialize_and_verify(config: Config, request: Request, manifest: Mapping[str, Any], patch: bytes, workspace: Path) -> dict[str, str]:
|
||||
if _git_text(config.git_dir, "cat-file", "-t", request.intent.base_oid) != "commit":
|
||||
raise HostSeedError("BASE_COMMIT_NOT_FOUND")
|
||||
if _git_text(config.git_dir, "cat-file", "-t", request.intent.payload_commit) != "commit":
|
||||
raise HostSeedError("PAYLOAD_COMMIT_NOT_FOUND")
|
||||
workspace.parent.mkdir(parents=True, exist_ok=True)
|
||||
clone = subprocess.run(
|
||||
["git", "clone", "--no-checkout", "--shared", str(config.git_dir), str(workspace)],
|
||||
capture_output=True, timeout=120, env={"PATH": os.environ.get("PATH", "")},
|
||||
)
|
||||
if clone.returncode:
|
||||
raise HostSeedError("WORKSPACE_CLONE_FAILED")
|
||||
checkout = subprocess.run(
|
||||
["git", "-C", str(workspace), "checkout", "--detach", request.intent.base_oid],
|
||||
capture_output=True, timeout=120, env={"PATH": os.environ.get("PATH", "")},
|
||||
)
|
||||
if checkout.returncode:
|
||||
raise HostSeedError("WORKSPACE_CHECKOUT_FAILED")
|
||||
try:
|
||||
proc = subprocess.run(
|
||||
["git", "-C", str(workspace), "apply", "--whitespace=nowarn", "-"],
|
||||
input=patch, capture_output=True, timeout=120, env={"PATH": os.environ.get("PATH", "")},
|
||||
)
|
||||
if proc.returncode:
|
||||
raise HostSeedError("PATCH_APPLY_FAILED")
|
||||
changes = _expected_changes(manifest)
|
||||
status_raw = subprocess.run(
|
||||
["git", "-C", str(workspace), "status", "--porcelain", "-z"],
|
||||
check=True, capture_output=True, timeout=120, env={"PATH": os.environ.get("PATH", "")},
|
||||
).stdout
|
||||
actual: set[str] = set()
|
||||
chunks = status_raw.split(b"\0")
|
||||
for chunk in chunks:
|
||||
if not chunk:
|
||||
continue
|
||||
text = chunk.decode("utf-8", "surrogateescape")
|
||||
if len(text) < 4:
|
||||
raise HostSeedError("WORKTREE_STATUS_INVALID")
|
||||
actual.add(text[3:].replace("\\", "/"))
|
||||
if actual != set(changes):
|
||||
raise HostSeedError("PATCH_CHANGESET_MISMATCH")
|
||||
base_paths = {
|
||||
_safe_repo_path(path, field="TRACKED_PATH")
|
||||
for path in _git_text(config.git_dir, "ls-tree", "-r", "--name-only", request.intent.base_oid).splitlines()
|
||||
if path
|
||||
}
|
||||
for rel, (digest, status) in changes.items():
|
||||
if status == "modified" and rel not in base_paths:
|
||||
raise HostSeedError("MODIFIED_PATH_NOT_IN_BASE:" + rel)
|
||||
if status == "added" and rel in base_paths:
|
||||
raise HostSeedError("ADDED_PATH_ALREADY_IN_BASE:" + rel)
|
||||
p = _regular_member(workspace, rel)
|
||||
if sha256(p.read_bytes()) != digest:
|
||||
raise HostSeedError("CHANGED_FILE_HASH_MISMATCH:" + rel)
|
||||
release_map = _manifest_release_map(manifest)
|
||||
expected_release_paths = base_paths | {
|
||||
rel for rel, (_digest, status) in changes.items() if status == "added"
|
||||
}
|
||||
if set(release_map) != expected_release_paths:
|
||||
raise HostSeedError("RELEASE_MEMBER_SET_MISMATCH")
|
||||
for rel, digest in release_map.items():
|
||||
p = _regular_member(workspace, rel)
|
||||
if sha256(p.read_bytes()) != digest:
|
||||
raise HostSeedError("RELEASE_MEMBER_HASH_MISMATCH:" + rel)
|
||||
tree_digest = sha256(canonical_compact({f"payload/repo/after/{k}": release_map[k] for k in sorted(release_map)}))
|
||||
if tree_digest != request.intent.release_tree_sha256:
|
||||
raise HostSeedError("RELEASE_TREE_HASH_MISMATCH")
|
||||
if manifest.get("release_tree_sha256") not in {None, request.intent.release_tree_sha256}:
|
||||
raise HostSeedError("MANIFEST_RELEASE_TREE_HASH_MISMATCH")
|
||||
return release_map
|
||||
except Exception:
|
||||
raise
|
||||
|
||||
|
||||
def _publish_release(config: Config, request: Request, workspace: Path, release_map: Mapping[str, str]) -> Path:
|
||||
if config.release_root.exists() and config.release_root.is_symlink():
|
||||
raise HostSeedError("RELEASE_ROOT_SYMLINK_REJECTED")
|
||||
final = config.release_root / request.intent.release_id
|
||||
if final.exists():
|
||||
if final.is_symlink() or not final.is_dir():
|
||||
raise HostSeedError("RELEASE_DESTINATION_CONFLICT")
|
||||
for rel, digest in release_map.items():
|
||||
p = final / PurePosixPath(rel)
|
||||
if p.is_symlink() or not p.is_file() or sha256(p.read_bytes()) != digest:
|
||||
raise HostSeedError("EXISTING_RELEASE_HASH_MISMATCH")
|
||||
return final
|
||||
|
||||
config.release_root.mkdir(parents=True, exist_ok=True)
|
||||
os.chmod(config.release_root, 0o755)
|
||||
staging = Path(tempfile.mkdtemp(prefix=".host-seed-", dir=config.release_root))
|
||||
os.chmod(staging, 0o755)
|
||||
try:
|
||||
for rel, digest in release_map.items():
|
||||
source = _regular_member(workspace, rel)
|
||||
target = staging / PurePosixPath(rel)
|
||||
target.parent.mkdir(parents=True, exist_ok=True)
|
||||
current = target.parent
|
||||
while current != staging:
|
||||
os.chmod(current, 0o755)
|
||||
current = current.parent
|
||||
data = source.read_bytes()
|
||||
if sha256(data) != digest:
|
||||
raise HostSeedError("STAGING_SOURCE_DRIFT")
|
||||
fd = os.open(target, os.O_WRONLY | os.O_CREAT | os.O_EXCL | getattr(os, "O_NOFOLLOW", 0), 0o600)
|
||||
try:
|
||||
with os.fdopen(fd, "wb", closefd=True) as out:
|
||||
out.write(data)
|
||||
out.flush()
|
||||
os.fsync(out.fileno())
|
||||
except Exception:
|
||||
raise
|
||||
source_mode = stat.S_IMODE(source.stat(follow_symlinks=False).st_mode)
|
||||
os.chmod(target, source_mode & 0o755 or 0o644)
|
||||
for rel, digest in release_map.items():
|
||||
p = staging / PurePosixPath(rel)
|
||||
if p.is_symlink() or sha256(p.read_bytes()) != digest:
|
||||
raise HostSeedError("STAGING_VERIFY_FAILED")
|
||||
os.replace(staging, final)
|
||||
parent_fd = os.open(config.release_root, os.O_RDONLY | getattr(os, "O_DIRECTORY", 0))
|
||||
try:
|
||||
os.fsync(parent_fd)
|
||||
finally:
|
||||
os.close(parent_fd)
|
||||
return final
|
||||
finally:
|
||||
if staging.exists():
|
||||
shutil.rmtree(staging)
|
||||
|
||||
|
||||
def execute(config: Config, *, now: datetime | None = None, require_root: bool = True) -> dict[str, Any]:
|
||||
if require_root and os.geteuid() != 0:
|
||||
raise HostSeedError("ROOT_REQUIRED")
|
||||
current = now or datetime.now(timezone.utc)
|
||||
config.state_root.mkdir(parents=True, exist_ok=True, mode=0o700)
|
||||
lock_path = config.state_root / "executor.lock"
|
||||
lock_fd = os.open(lock_path, os.O_RDWR | os.O_CREAT, 0o600)
|
||||
try:
|
||||
fcntl.flock(lock_fd, fcntl.LOCK_EX)
|
||||
ref_probe = subprocess.run(
|
||||
["git", f"--git-dir={config.git_dir}", "rev-parse", "--verify", config.request_ref],
|
||||
capture_output=True, timeout=30, env={"PATH": os.environ.get("PATH", "")},
|
||||
)
|
||||
if ref_probe.returncode:
|
||||
return {
|
||||
"schema": "aios.control-plane-host-seed-poll.v1",
|
||||
"state": "NO_REQUEST",
|
||||
"request_ref": config.request_ref,
|
||||
"service_mutation": False,
|
||||
"task_creation": False,
|
||||
}
|
||||
request_oid = ref_probe.stdout.decode("utf-8", "strict").strip()
|
||||
if not OID_RE.fullmatch(request_oid):
|
||||
raise HostSeedError("REQUEST_REF_INVALID")
|
||||
raw = _read_git_blob(config.git_dir, request_oid, config.request_path, maximum=MAX_REQUEST_BYTES)
|
||||
request_value = _parse_json(raw, code="REQUEST_JSON_INVALID")
|
||||
request = Request.load(request_value, host_identity=config.host_identity, now=current)
|
||||
binding = _load_task_binding(config, request)
|
||||
manifest = _load_manifest(config, request)
|
||||
_load_identity_evidence(config, request)
|
||||
patch = _load_patch(config, request)
|
||||
|
||||
op_id = request.intent.digest
|
||||
receipt_path = config.state_root / "receipts" / f"{op_id}.json"
|
||||
if receipt_path.exists():
|
||||
receipt = _parse_json(_regular_file_bytes(receipt_path, maximum=256 * 1024), code="RECEIPT_INVALID")
|
||||
if receipt.get("state") == "EXECUTED" and receipt.get("intent_sha256") == op_id:
|
||||
return receipt
|
||||
raise HostSeedError("OPERATION_STATE_CONFLICT")
|
||||
|
||||
_atomic_json(config.state_root / "operations" / f"{op_id}.json", {
|
||||
"schema": "aios.control-plane-host-seed-operation.v1",
|
||||
"state": "PENDING",
|
||||
"intent_sha256": op_id,
|
||||
"request_commit": request_oid,
|
||||
"authorization_task_id": request.authorization_task_id,
|
||||
"authorization_binding_sha256": request.authorization_binding_sha256,
|
||||
"started_at": current.isoformat().replace("+00:00", "Z"),
|
||||
})
|
||||
|
||||
workspace_root = config.state_root / "workspaces"
|
||||
workspace_root.mkdir(parents=True, exist_ok=True)
|
||||
workspace = workspace_root / f"seed-{op_id[:20]}"
|
||||
if workspace.exists():
|
||||
raise HostSeedError("WORKSPACE_ALREADY_EXISTS")
|
||||
release_map: dict[str, str] | None = None
|
||||
try:
|
||||
release_map = _materialize_and_verify(config, request, manifest, patch, workspace)
|
||||
destination = _publish_release(config, request, workspace, release_map)
|
||||
finally:
|
||||
if workspace.exists():
|
||||
shutil.rmtree(workspace, ignore_errors=True)
|
||||
|
||||
receipt = {
|
||||
"schema": "aios.control-plane-host-seed-receipt.v1",
|
||||
"state": "EXECUTED",
|
||||
"operation": OPERATION,
|
||||
"intent_sha256": op_id,
|
||||
"request_commit": request_oid,
|
||||
"repository": REPOSITORY,
|
||||
"target_host": request.intent.target_host,
|
||||
"candidate": request.intent.candidate,
|
||||
"release_id": request.intent.release_id,
|
||||
"base_oid": request.intent.base_oid,
|
||||
"payload_commit": request.intent.payload_commit,
|
||||
"validation_artifact_sha256": request.intent.artifact_sha256,
|
||||
"artifact_delivery_used": False,
|
||||
"manifest_sha256": request.intent.manifest_sha256,
|
||||
"machine_goal_sha256": request.intent.machine_goal_sha256,
|
||||
"release_tree_sha256": request.intent.release_tree_sha256,
|
||||
"seed_bootstrap_sha256": request.intent.seed_bootstrap_sha256,
|
||||
"authorization_task_id": request.authorization_task_id,
|
||||
"authorization_binding_sha256": request.authorization_binding_sha256,
|
||||
"release_path": str(destination),
|
||||
"release_member_count": len(release_map or {}),
|
||||
"service_mutation": False,
|
||||
"service_restart": False,
|
||||
"projector_started": False,
|
||||
"task_creation": False,
|
||||
"production_write": False,
|
||||
"main_write": False,
|
||||
"tag_write": False,
|
||||
"baseline_write": False,
|
||||
"executed_at": datetime.now(timezone.utc).isoformat().replace("+00:00", "Z"),
|
||||
}
|
||||
_atomic_json(receipt_path, receipt)
|
||||
_atomic_json(config.state_root / "operations" / f"{op_id}.json", receipt)
|
||||
return receipt
|
||||
finally:
|
||||
fcntl.flock(lock_fd, fcntl.LOCK_UN)
|
||||
os.close(lock_fd)
|
||||
|
||||
|
||||
def config_from_environment() -> Config:
|
||||
return Config(
|
||||
git_dir=Path(os.environ.get("AIOS_HOST_SEED_GIT_DIR", "/data/aios-gitops/repo.git")),
|
||||
task_binding_root=Path(os.environ.get(
|
||||
"AIOS_HOST_SEED_TASK_BINDING_ROOT",
|
||||
"/var/lib/aios-gitops/bridge-v2-staging/task-bindings",
|
||||
)),
|
||||
state_root=Path(os.environ.get(
|
||||
"AIOS_HOST_SEED_STATE_ROOT",
|
||||
"/var/lib/aios-control-plane-host-seed-executor",
|
||||
)),
|
||||
release_root=Path(os.environ.get("AIOS_HOST_SEED_RELEASE_ROOT", "/opt/aios/releases")),
|
||||
host_identity=os.environ.get("AIOS_HOST_IDENTITY", "cn-network-mkt"),
|
||||
task_binding_owner=os.environ.get(
|
||||
"AIOS_HOST_SEED_TASK_BINDING_OWNER", "aios-bridge-staging"
|
||||
) or None,
|
||||
request_ref=os.environ.get("AIOS_HOST_SEED_REQUEST_REF", REQUEST_REF),
|
||||
request_path=os.environ.get("AIOS_HOST_SEED_REQUEST_PATH", REQUEST_PATH),
|
||||
)
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("command", choices=("execute",))
|
||||
args = parser.parse_args()
|
||||
if args.command == "execute":
|
||||
result = execute(config_from_environment())
|
||||
print(json.dumps(result, sort_keys=True, separators=(",", ":")))
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -1,52 +0,0 @@
|
||||
#!/bin/sh
|
||||
set -eu
|
||||
|
||||
if [ "$(id -u)" -ne 0 ]; then
|
||||
echo "ROOT_REQUIRED" >&2
|
||||
exit 20
|
||||
fi
|
||||
|
||||
SOURCE_ROOT=${1:-}
|
||||
if [ -z "$SOURCE_ROOT" ]; then
|
||||
echo "SOURCE_ROOT_REQUIRED" >&2
|
||||
exit 21
|
||||
fi
|
||||
|
||||
EXECUTOR="$SOURCE_ROOT/active-runtime/app/aios_control_plane_host_seed_executor.py"
|
||||
SERVICE="$SOURCE_ROOT/deploy/systemd/aios-control-plane-host-seed-executor.service"
|
||||
TIMER="$SOURCE_ROOT/deploy/systemd/aios-control-plane-host-seed-executor.timer"
|
||||
|
||||
for file in "$EXECUTOR" "$SERVICE" "$TIMER"; do
|
||||
if [ ! -f "$file" ] || [ -L "$file" ]; then
|
||||
echo "SOURCE_FILE_INVALID:$file" >&2
|
||||
exit 22
|
||||
fi
|
||||
done
|
||||
|
||||
AUTHORITY_GIT=/data/aios-gitops/repo.git
|
||||
REQUEST_REF=refs/heads/agent/host-seed-authorization-requests
|
||||
if [ ! -d "$AUTHORITY_GIT" ]; then
|
||||
echo "AUTHORITATIVE_GIT_UNAVAILABLE" >&2
|
||||
exit 23
|
||||
fi
|
||||
if ! id -u aios-bridge-staging >/dev/null 2>&1; then
|
||||
echo "BRIDGE_SERVICE_ACCOUNT_UNAVAILABLE" >&2
|
||||
exit 24
|
||||
fi
|
||||
if git --git-dir="$AUTHORITY_GIT" show-ref --verify --quiet "$REQUEST_REF"; then
|
||||
echo "HOST_SEED_REQUEST_REF_ALREADY_PRESENT" >&2
|
||||
exit 25
|
||||
fi
|
||||
|
||||
install -d -m 0755 /usr/local/libexec
|
||||
install -m 0755 "$EXECUTOR" /usr/local/libexec/aios-control-plane-host-seed-executor
|
||||
install -m 0644 "$SERVICE" /etc/systemd/system/aios-control-plane-host-seed-executor.service
|
||||
install -m 0644 "$TIMER" /etc/systemd/system/aios-control-plane-host-seed-executor.timer
|
||||
install -d -m 0700 /var/lib/aios-control-plane-host-seed-executor
|
||||
|
||||
systemctl daemon-reload
|
||||
systemctl enable --now aios-control-plane-host-seed-executor.timer
|
||||
systemctl is-enabled --quiet aios-control-plane-host-seed-executor.timer
|
||||
systemctl is-active --quiet aios-control-plane-host-seed-executor.timer
|
||||
|
||||
echo "HOST_SEED_EXECUTOR_INSTALL=PASS"
|
||||
@@ -1,15 +0,0 @@
|
||||
#!/bin/sh
|
||||
set -eu
|
||||
|
||||
if [ "$(id -u)" -ne 0 ]; then
|
||||
echo "ROOT_REQUIRED" >&2
|
||||
exit 20
|
||||
fi
|
||||
|
||||
systemctl disable --now aios-control-plane-host-seed-executor.timer 2>/dev/null || true
|
||||
rm -f /etc/systemd/system/aios-control-plane-host-seed-executor.timer
|
||||
rm -f /etc/systemd/system/aios-control-plane-host-seed-executor.service
|
||||
rm -f /usr/local/libexec/aios-control-plane-host-seed-executor
|
||||
systemctl daemon-reload
|
||||
|
||||
echo "HOST_SEED_EXECUTOR_ROLLBACK=PASS"
|
||||
@@ -1,41 +0,0 @@
|
||||
[Unit]
|
||||
Description=AIOS governed immutable Host seed executor
|
||||
After=local-fs.target
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
User=root
|
||||
Group=root
|
||||
UMask=0077
|
||||
Environment=PYTHONDONTWRITEBYTECODE=1
|
||||
Environment=AIOS_HOST_IDENTITY=cn-network-mkt
|
||||
Environment=AIOS_HOST_SEED_GIT_DIR=/data/aios-gitops/repo.git
|
||||
Environment=AIOS_HOST_SEED_TASK_BINDING_ROOT=/var/lib/aios-gitops/bridge-v2-staging/task-bindings
|
||||
Environment=AIOS_HOST_SEED_TASK_BINDING_OWNER=aios-bridge-staging
|
||||
Environment=AIOS_HOST_SEED_STATE_ROOT=/var/lib/aios-control-plane-host-seed-executor
|
||||
Environment=AIOS_HOST_SEED_RELEASE_ROOT=/opt/aios/releases
|
||||
Environment=AIOS_HOST_SEED_REQUEST_REF=refs/heads/agent/host-seed-authorization-requests
|
||||
Environment=AIOS_HOST_SEED_REQUEST_PATH=control-plane/host-seed/request.json
|
||||
ExecStart=/usr/bin/python3 -B /usr/local/libexec/aios-control-plane-host-seed-executor execute
|
||||
NoNewPrivileges=true
|
||||
PrivateTmp=true
|
||||
PrivateDevices=true
|
||||
ProtectSystem=strict
|
||||
ProtectHome=true
|
||||
ProtectKernelTunables=true
|
||||
ProtectKernelModules=true
|
||||
ProtectControlGroups=true
|
||||
ProtectKernelLogs=true
|
||||
LockPersonality=true
|
||||
RestrictSUIDSGID=true
|
||||
RestrictRealtime=true
|
||||
RestrictNamespaces=true
|
||||
MemoryDenyWriteExecute=true
|
||||
RestrictAddressFamilies=AF_UNIX
|
||||
IPAddressDeny=any
|
||||
CapabilityBoundingSet=CAP_DAC_OVERRIDE CAP_FOWNER CAP_CHOWN
|
||||
ReadOnlyPaths=/usr/local/libexec/aios-control-plane-host-seed-executor
|
||||
ReadOnlyPaths=/data/aios-gitops/repo.git
|
||||
ReadOnlyPaths=/var/lib/aios-gitops/bridge-v2-staging/task-bindings
|
||||
ReadWritePaths=/var/lib/aios-control-plane-host-seed-executor
|
||||
ReadWritePaths=/opt/aios/releases
|
||||
@@ -1,12 +0,0 @@
|
||||
[Unit]
|
||||
Description=Poll governed immutable Host seed requests
|
||||
|
||||
[Timer]
|
||||
OnBootSec=2min
|
||||
OnUnitActiveSec=30s
|
||||
AccuracySec=1s
|
||||
Persistent=true
|
||||
Unit=aios-control-plane-host-seed-executor.service
|
||||
|
||||
[Install]
|
||||
WantedBy=timers.target
|
||||
@@ -0,0 +1,721 @@
|
||||
{
|
||||
"schema_version": "aios.chatgpt-action-schema-capability-manifest.v2",
|
||||
"source_schema": {
|
||||
"path": "docs/gitops/openapi-chatgpt-actions.yaml",
|
||||
"operation_count": 31,
|
||||
"sha256": "d6129421085c9516ba88796d8727bc4ba1c4b4b48bac74f146e68ebf7cc9cd96"
|
||||
},
|
||||
"public_runtime_schema": {
|
||||
"path": "docs/gitops/openapi-chatgpt-actions-public-runtime-2e599646.yaml",
|
||||
"immutable_runtime_oid": "2e5996463de564793df881df5c5f684359733627",
|
||||
"operation_count": 30,
|
||||
"sha256": "4cee48511e9435cb147b8925c37aaa135c384a468ede34d6828bad42c97c5fcc"
|
||||
},
|
||||
"authoritative_server_urls": [
|
||||
"https://cnnetmkt.top"
|
||||
],
|
||||
"union_operation_count": 46,
|
||||
"schemas": [
|
||||
{
|
||||
"path": "docs/gitops/openapi-chatgpt-actions-governed-release.yaml",
|
||||
"trust_domain": "governed-release",
|
||||
"operation_count": 26,
|
||||
"public_action_import": true
|
||||
},
|
||||
{
|
||||
"path": "docs/gitops/openapi-chatgpt-actions-online-gitops-task.yaml",
|
||||
"trust_domain": "online-gitops-task",
|
||||
"operation_count": 14,
|
||||
"public_action_import": true
|
||||
},
|
||||
{
|
||||
"path": "docs/gitops/openapi-chatgpt-actions-online-agent-task.yaml",
|
||||
"trust_domain": "online-agent-task",
|
||||
"operation_count": 6,
|
||||
"public_action_import": true
|
||||
}
|
||||
],
|
||||
"operations": [
|
||||
{
|
||||
"operation_id": "acquireWriterLease",
|
||||
"schema": "docs/gitops/openapi-chatgpt-actions-governed-release.yaml",
|
||||
"trust_domain": "governed-release",
|
||||
"server_url": "https://cnnetmkt.top",
|
||||
"server_route": "/acquireWriterLease",
|
||||
"method": "POST",
|
||||
"declared_openapi_security": [],
|
||||
"required_scope": "file:write",
|
||||
"exposure": "control-plane",
|
||||
"source_origin": null,
|
||||
"public_origin": "docs/gitops/openapi-chatgpt-actions-public-runtime-2e599646.yaml"
|
||||
},
|
||||
{
|
||||
"operation_id": "cancelOnlineAgentTask",
|
||||
"schema": "docs/gitops/openapi-chatgpt-actions-online-agent-task.yaml",
|
||||
"trust_domain": "online-agent-task",
|
||||
"server_url": "https://cnnetmkt.top",
|
||||
"server_route": "/cancelOnlineAgentTask",
|
||||
"method": "POST",
|
||||
"declared_openapi_security": [],
|
||||
"required_scope": "agent-task:control",
|
||||
"exposure": "control-plane",
|
||||
"source_origin": null,
|
||||
"public_origin": "docs/gitops/openapi-chatgpt-actions-public-runtime-2e599646.yaml"
|
||||
},
|
||||
{
|
||||
"operation_id": "commitChanges",
|
||||
"schema": "docs/gitops/openapi-chatgpt-actions-online-gitops-task.yaml",
|
||||
"trust_domain": "online-gitops-task",
|
||||
"server_url": "https://cnnetmkt.top",
|
||||
"server_route": "/api/commit",
|
||||
"method": "POST",
|
||||
"declared_openapi_security": [
|
||||
{
|
||||
"bearerAuth": []
|
||||
}
|
||||
],
|
||||
"required_scope": "commit:create",
|
||||
"exposure": "control-plane",
|
||||
"source_origin": "docs/gitops/openapi-chatgpt-actions.yaml",
|
||||
"public_origin": null
|
||||
},
|
||||
{
|
||||
"operation_id": "commitFile",
|
||||
"schema": "docs/gitops/openapi-chatgpt-actions-online-gitops-task.yaml",
|
||||
"trust_domain": "online-gitops-task",
|
||||
"server_url": "https://cnnetmkt.top",
|
||||
"server_route": "/api/file",
|
||||
"method": "POST",
|
||||
"declared_openapi_security": [
|
||||
{
|
||||
"bearerAuth": []
|
||||
}
|
||||
],
|
||||
"required_scope": "file:write",
|
||||
"exposure": "control-plane",
|
||||
"source_origin": "docs/gitops/openapi-chatgpt-actions.yaml",
|
||||
"public_origin": null
|
||||
},
|
||||
{
|
||||
"operation_id": "commitFileAtomic",
|
||||
"schema": "docs/gitops/openapi-chatgpt-actions-online-gitops-task.yaml",
|
||||
"trust_domain": "online-gitops-task",
|
||||
"server_url": "https://cnnetmkt.top",
|
||||
"server_route": "/api/commit-file",
|
||||
"method": "POST",
|
||||
"declared_openapi_security": [
|
||||
{
|
||||
"bearerAuth": []
|
||||
}
|
||||
],
|
||||
"required_scope": "file:write",
|
||||
"exposure": "control-plane",
|
||||
"source_origin": "docs/gitops/openapi-chatgpt-actions.yaml",
|
||||
"public_origin": null
|
||||
},
|
||||
{
|
||||
"operation_id": "commitFileGoverned",
|
||||
"schema": "docs/gitops/openapi-chatgpt-actions-governed-release.yaml",
|
||||
"trust_domain": "governed-release",
|
||||
"server_url": "https://cnnetmkt.top",
|
||||
"server_route": "/commitFileGoverned",
|
||||
"method": "POST",
|
||||
"declared_openapi_security": [],
|
||||
"required_scope": "file:write",
|
||||
"exposure": "control-plane",
|
||||
"source_origin": null,
|
||||
"public_origin": "docs/gitops/openapi-chatgpt-actions-public-runtime-2e599646.yaml"
|
||||
},
|
||||
{
|
||||
"operation_id": "commitFilesGoverned",
|
||||
"schema": "docs/gitops/openapi-chatgpt-actions-governed-release.yaml",
|
||||
"trust_domain": "governed-release",
|
||||
"server_url": "https://cnnetmkt.top",
|
||||
"server_route": "/commitFilesGoverned",
|
||||
"method": "POST",
|
||||
"declared_openapi_security": [],
|
||||
"required_scope": "file:write",
|
||||
"exposure": "control-plane",
|
||||
"source_origin": null,
|
||||
"public_origin": "docs/gitops/openapi-chatgpt-actions-public-runtime-2e599646.yaml"
|
||||
},
|
||||
{
|
||||
"operation_id": "createBranch",
|
||||
"schema": "docs/gitops/openapi-chatgpt-actions-online-gitops-task.yaml",
|
||||
"trust_domain": "online-gitops-task",
|
||||
"server_url": "https://cnnetmkt.top",
|
||||
"server_route": "/api/branch",
|
||||
"method": "POST",
|
||||
"declared_openapi_security": [
|
||||
{
|
||||
"bearerAuth": []
|
||||
}
|
||||
],
|
||||
"required_scope": "branch:create",
|
||||
"exposure": "control-plane",
|
||||
"source_origin": "docs/gitops/openapi-chatgpt-actions.yaml",
|
||||
"public_origin": null
|
||||
},
|
||||
{
|
||||
"operation_id": "createGovernedTask",
|
||||
"schema": "docs/gitops/openapi-chatgpt-actions-governed-release.yaml",
|
||||
"trust_domain": "governed-release",
|
||||
"server_url": "https://cnnetmkt.top",
|
||||
"server_route": "/createGovernedTask",
|
||||
"method": "POST",
|
||||
"declared_openapi_security": [],
|
||||
"required_scope": "branch:create",
|
||||
"exposure": "control-plane",
|
||||
"source_origin": null,
|
||||
"public_origin": "docs/gitops/openapi-chatgpt-actions-public-runtime-2e599646.yaml"
|
||||
},
|
||||
{
|
||||
"operation_id": "createOnlineAgentTask",
|
||||
"schema": "docs/gitops/openapi-chatgpt-actions-online-agent-task.yaml",
|
||||
"trust_domain": "online-agent-task",
|
||||
"server_url": "https://cnnetmkt.top",
|
||||
"server_route": "/createOnlineAgentTask",
|
||||
"method": "POST",
|
||||
"declared_openapi_security": [],
|
||||
"required_scope": "agent-task:control",
|
||||
"exposure": "control-plane",
|
||||
"source_origin": null,
|
||||
"public_origin": "docs/gitops/openapi-chatgpt-actions-public-runtime-2e599646.yaml"
|
||||
},
|
||||
{
|
||||
"operation_id": "createPatch",
|
||||
"schema": "docs/gitops/openapi-chatgpt-actions-online-gitops-task.yaml",
|
||||
"trust_domain": "online-gitops-task",
|
||||
"server_url": "https://cnnetmkt.top",
|
||||
"server_route": "/api/patch",
|
||||
"method": "POST",
|
||||
"declared_openapi_security": [
|
||||
{
|
||||
"bearerAuth": []
|
||||
}
|
||||
],
|
||||
"required_scope": "patch:create",
|
||||
"exposure": "control-plane",
|
||||
"source_origin": "docs/gitops/openapi-chatgpt-actions.yaml",
|
||||
"public_origin": null
|
||||
},
|
||||
{
|
||||
"operation_id": "createReleaseCandidate",
|
||||
"schema": "docs/gitops/openapi-chatgpt-actions-governed-release.yaml",
|
||||
"trust_domain": "governed-release",
|
||||
"server_url": "https://cnnetmkt.top",
|
||||
"server_route": "/createReleaseCandidate",
|
||||
"method": "POST",
|
||||
"declared_openapi_security": [
|
||||
{
|
||||
"bearerAuth": []
|
||||
}
|
||||
],
|
||||
"required_scope": "release:candidate",
|
||||
"exposure": "control-plane",
|
||||
"source_origin": "docs/gitops/openapi-chatgpt-actions.yaml",
|
||||
"public_origin": "docs/gitops/openapi-chatgpt-actions-public-runtime-2e599646.yaml"
|
||||
},
|
||||
{
|
||||
"operation_id": "createReleaseIntent",
|
||||
"schema": "docs/gitops/openapi-chatgpt-actions-governed-release.yaml",
|
||||
"trust_domain": "governed-release",
|
||||
"server_url": "https://cnnetmkt.top",
|
||||
"server_route": "/createReleaseIntent",
|
||||
"method": "POST",
|
||||
"declared_openapi_security": [
|
||||
{
|
||||
"bearerAuth": []
|
||||
}
|
||||
],
|
||||
"required_scope": "release:candidate",
|
||||
"exposure": "control-plane",
|
||||
"source_origin": "docs/gitops/openapi-chatgpt-actions.yaml",
|
||||
"public_origin": null
|
||||
},
|
||||
{
|
||||
"operation_id": "getApprovalEnvelope",
|
||||
"schema": "docs/gitops/openapi-chatgpt-actions-governed-release.yaml",
|
||||
"trust_domain": "governed-release",
|
||||
"server_url": "https://cnnetmkt.top",
|
||||
"server_route": "/getApprovalEnvelope",
|
||||
"method": "GET",
|
||||
"declared_openapi_security": [],
|
||||
"required_scope": null,
|
||||
"exposure": "public",
|
||||
"source_origin": "docs/gitops/openapi-chatgpt-actions.yaml",
|
||||
"public_origin": "docs/gitops/openapi-chatgpt-actions-public-runtime-2e599646.yaml"
|
||||
},
|
||||
{
|
||||
"operation_id": "getBaseline",
|
||||
"schema": "docs/gitops/openapi-chatgpt-actions-online-gitops-task.yaml",
|
||||
"trust_domain": "online-gitops-task",
|
||||
"server_url": "https://cnnetmkt.top",
|
||||
"server_route": "/api/base",
|
||||
"method": "GET",
|
||||
"declared_openapi_security": [],
|
||||
"required_scope": null,
|
||||
"exposure": "public",
|
||||
"source_origin": "docs/gitops/openapi-chatgpt-actions.yaml",
|
||||
"public_origin": "docs/gitops/openapi-chatgpt-actions-public-runtime-2e599646.yaml"
|
||||
},
|
||||
{
|
||||
"operation_id": "getCapabilityStatus",
|
||||
"schema": "docs/gitops/openapi-chatgpt-actions-governed-release.yaml",
|
||||
"trust_domain": "governed-release",
|
||||
"server_url": "https://cnnetmkt.top",
|
||||
"server_route": "/getCapabilityStatus",
|
||||
"method": "GET",
|
||||
"declared_openapi_security": [],
|
||||
"required_scope": null,
|
||||
"exposure": "public",
|
||||
"source_origin": "docs/gitops/openapi-chatgpt-actions.yaml",
|
||||
"public_origin": null
|
||||
},
|
||||
{
|
||||
"operation_id": "getControlPlaneSnapshot",
|
||||
"schema": "docs/gitops/openapi-chatgpt-actions-governed-release.yaml",
|
||||
"trust_domain": "governed-release",
|
||||
"server_url": "https://cnnetmkt.top",
|
||||
"server_route": "/api/control-plane-snapshot",
|
||||
"method": "GET",
|
||||
"declared_openapi_security": [],
|
||||
"required_scope": null,
|
||||
"exposure": "public",
|
||||
"source_origin": null,
|
||||
"public_origin": "docs/gitops/openapi-chatgpt-actions-public-runtime-2e599646.yaml"
|
||||
},
|
||||
{
|
||||
"operation_id": "getDiff",
|
||||
"schema": "docs/gitops/openapi-chatgpt-actions-online-gitops-task.yaml",
|
||||
"trust_domain": "online-gitops-task",
|
||||
"server_url": "https://cnnetmkt.top",
|
||||
"server_route": "/api/diff",
|
||||
"method": "GET",
|
||||
"declared_openapi_security": [],
|
||||
"required_scope": null,
|
||||
"exposure": "public",
|
||||
"source_origin": "docs/gitops/openapi-chatgpt-actions.yaml",
|
||||
"public_origin": "docs/gitops/openapi-chatgpt-actions-public-runtime-2e599646.yaml"
|
||||
},
|
||||
{
|
||||
"operation_id": "getGitOpsJob",
|
||||
"schema": "docs/gitops/openapi-chatgpt-actions-governed-release.yaml",
|
||||
"trust_domain": "governed-release",
|
||||
"server_url": "https://cnnetmkt.top",
|
||||
"server_route": "/getGitOpsJob",
|
||||
"method": "POST",
|
||||
"declared_openapi_security": [],
|
||||
"required_scope": "file:write",
|
||||
"exposure": "control-plane",
|
||||
"source_origin": null,
|
||||
"public_origin": "docs/gitops/openapi-chatgpt-actions-public-runtime-2e599646.yaml"
|
||||
},
|
||||
{
|
||||
"operation_id": "getGovernedWriteCapabilities",
|
||||
"schema": "docs/gitops/openapi-chatgpt-actions-governed-release.yaml",
|
||||
"trust_domain": "governed-release",
|
||||
"server_url": "https://cnnetmkt.top",
|
||||
"server_route": "/api/governed-write-capabilities",
|
||||
"method": "GET",
|
||||
"declared_openapi_security": [],
|
||||
"required_scope": null,
|
||||
"exposure": "public",
|
||||
"source_origin": null,
|
||||
"public_origin": "docs/gitops/openapi-chatgpt-actions-public-runtime-2e599646.yaml"
|
||||
},
|
||||
{
|
||||
"operation_id": "getOnlineAgentTask",
|
||||
"schema": "docs/gitops/openapi-chatgpt-actions-online-agent-task.yaml",
|
||||
"trust_domain": "online-agent-task",
|
||||
"server_url": "https://cnnetmkt.top",
|
||||
"server_route": "/api/online-agent-task",
|
||||
"method": "GET",
|
||||
"declared_openapi_security": [],
|
||||
"required_scope": "agent-task:control",
|
||||
"exposure": "control-plane",
|
||||
"source_origin": null,
|
||||
"public_origin": "docs/gitops/openapi-chatgpt-actions-public-runtime-2e599646.yaml"
|
||||
},
|
||||
{
|
||||
"operation_id": "getOnlineAgentTaskEvents",
|
||||
"schema": "docs/gitops/openapi-chatgpt-actions-online-agent-task.yaml",
|
||||
"trust_domain": "online-agent-task",
|
||||
"server_url": "https://cnnetmkt.top",
|
||||
"server_route": "/api/online-agent-task-events",
|
||||
"method": "GET",
|
||||
"declared_openapi_security": [],
|
||||
"required_scope": "agent-task:control",
|
||||
"exposure": "control-plane",
|
||||
"source_origin": null,
|
||||
"public_origin": "docs/gitops/openapi-chatgpt-actions-public-runtime-2e599646.yaml"
|
||||
},
|
||||
{
|
||||
"operation_id": "getRecoveryStatus",
|
||||
"schema": "docs/gitops/openapi-chatgpt-actions-governed-release.yaml",
|
||||
"trust_domain": "governed-release",
|
||||
"server_url": "https://cnnetmkt.top",
|
||||
"server_route": "/getRecoveryStatus",
|
||||
"method": "GET",
|
||||
"declared_openapi_security": [],
|
||||
"required_scope": null,
|
||||
"exposure": "public",
|
||||
"source_origin": "docs/gitops/openapi-chatgpt-actions.yaml",
|
||||
"public_origin": null
|
||||
},
|
||||
{
|
||||
"operation_id": "getReleaseAcceptance",
|
||||
"schema": "docs/gitops/openapi-chatgpt-actions-governed-release.yaml",
|
||||
"trust_domain": "governed-release",
|
||||
"server_url": "https://cnnetmkt.top",
|
||||
"server_route": "/getReleaseAcceptance",
|
||||
"method": "GET",
|
||||
"declared_openapi_security": [],
|
||||
"required_scope": null,
|
||||
"exposure": "public",
|
||||
"source_origin": "docs/gitops/openapi-chatgpt-actions.yaml",
|
||||
"public_origin": null
|
||||
},
|
||||
{
|
||||
"operation_id": "getReleaseCandidate",
|
||||
"schema": "docs/gitops/openapi-chatgpt-actions-governed-release.yaml",
|
||||
"trust_domain": "governed-release",
|
||||
"server_url": "https://cnnetmkt.top",
|
||||
"server_route": "/getReleaseCandidate",
|
||||
"method": "GET",
|
||||
"declared_openapi_security": [],
|
||||
"required_scope": null,
|
||||
"exposure": "public",
|
||||
"source_origin": "docs/gitops/openapi-chatgpt-actions.yaml",
|
||||
"public_origin": "docs/gitops/openapi-chatgpt-actions-public-runtime-2e599646.yaml"
|
||||
},
|
||||
{
|
||||
"operation_id": "getReleaseConsistency",
|
||||
"schema": "docs/gitops/openapi-chatgpt-actions-governed-release.yaml",
|
||||
"trust_domain": "governed-release",
|
||||
"server_url": "https://cnnetmkt.top",
|
||||
"server_route": "/api/release-consistency",
|
||||
"method": "GET",
|
||||
"declared_openapi_security": [],
|
||||
"required_scope": null,
|
||||
"exposure": "public",
|
||||
"source_origin": "docs/gitops/openapi-chatgpt-actions.yaml",
|
||||
"public_origin": "docs/gitops/openapi-chatgpt-actions-public-runtime-2e599646.yaml"
|
||||
},
|
||||
{
|
||||
"operation_id": "getReleasePlan",
|
||||
"schema": "docs/gitops/openapi-chatgpt-actions-governed-release.yaml",
|
||||
"trust_domain": "governed-release",
|
||||
"server_url": "https://cnnetmkt.top",
|
||||
"server_route": "/getReleasePlan",
|
||||
"method": "GET",
|
||||
"declared_openapi_security": [],
|
||||
"required_scope": null,
|
||||
"exposure": "public",
|
||||
"source_origin": "docs/gitops/openapi-chatgpt-actions.yaml",
|
||||
"public_origin": null
|
||||
},
|
||||
{
|
||||
"operation_id": "getReleaseSaga",
|
||||
"schema": "docs/gitops/openapi-chatgpt-actions-governed-release.yaml",
|
||||
"trust_domain": "governed-release",
|
||||
"server_url": "https://cnnetmkt.top",
|
||||
"server_route": "/getReleaseSaga",
|
||||
"method": "GET",
|
||||
"declared_openapi_security": [],
|
||||
"required_scope": null,
|
||||
"exposure": "public",
|
||||
"source_origin": "docs/gitops/openapi-chatgpt-actions.yaml",
|
||||
"public_origin": "docs/gitops/openapi-chatgpt-actions-public-runtime-2e599646.yaml"
|
||||
},
|
||||
{
|
||||
"operation_id": "getRuntimeStatus",
|
||||
"schema": "docs/gitops/openapi-chatgpt-actions-online-gitops-task.yaml",
|
||||
"trust_domain": "online-gitops-task",
|
||||
"server_url": "https://cnnetmkt.top",
|
||||
"server_route": "/api/run",
|
||||
"method": "GET",
|
||||
"declared_openapi_security": [],
|
||||
"required_scope": null,
|
||||
"exposure": "public",
|
||||
"source_origin": "docs/gitops/openapi-chatgpt-actions.yaml",
|
||||
"public_origin": "docs/gitops/openapi-chatgpt-actions-public-runtime-2e599646.yaml"
|
||||
},
|
||||
{
|
||||
"operation_id": "getTagPublicationStatus",
|
||||
"schema": "docs/gitops/openapi-chatgpt-actions-governed-release.yaml",
|
||||
"trust_domain": "governed-release",
|
||||
"server_url": "https://cnnetmkt.top",
|
||||
"server_route": "/api/tag-publication-status",
|
||||
"method": "GET",
|
||||
"declared_openapi_security": [],
|
||||
"required_scope": null,
|
||||
"exposure": "public",
|
||||
"source_origin": "docs/gitops/openapi-chatgpt-actions.yaml",
|
||||
"public_origin": "docs/gitops/openapi-chatgpt-actions-public-runtime-2e599646.yaml"
|
||||
},
|
||||
{
|
||||
"operation_id": "listPatches",
|
||||
"schema": "docs/gitops/openapi-chatgpt-actions-online-gitops-task.yaml",
|
||||
"trust_domain": "online-gitops-task",
|
||||
"server_url": "https://cnnetmkt.top",
|
||||
"server_route": "/api/patch",
|
||||
"method": "GET",
|
||||
"declared_openapi_security": [],
|
||||
"required_scope": null,
|
||||
"exposure": "public",
|
||||
"source_origin": "docs/gitops/openapi-chatgpt-actions.yaml",
|
||||
"public_origin": null
|
||||
},
|
||||
{
|
||||
"operation_id": "listVersions",
|
||||
"schema": "docs/gitops/openapi-chatgpt-actions-online-gitops-task.yaml",
|
||||
"trust_domain": "online-gitops-task",
|
||||
"server_url": "https://cnnetmkt.top",
|
||||
"server_route": "/api/ver",
|
||||
"method": "GET",
|
||||
"declared_openapi_security": [],
|
||||
"required_scope": null,
|
||||
"exposure": "public",
|
||||
"source_origin": "docs/gitops/openapi-chatgpt-actions.yaml",
|
||||
"public_origin": "docs/gitops/openapi-chatgpt-actions-public-runtime-2e599646.yaml"
|
||||
},
|
||||
{
|
||||
"operation_id": "planNaturalLanguageGitOpsAction",
|
||||
"schema": "docs/gitops/openapi-chatgpt-actions-online-gitops-task.yaml",
|
||||
"trust_domain": "online-gitops-task",
|
||||
"server_url": "https://cnnetmkt.top",
|
||||
"server_route": "/api/nl",
|
||||
"method": "POST",
|
||||
"declared_openapi_security": [
|
||||
{
|
||||
"bearerAuth": []
|
||||
}
|
||||
],
|
||||
"required_scope": "nl:plan",
|
||||
"exposure": "control-plane",
|
||||
"source_origin": "docs/gitops/openapi-chatgpt-actions.yaml",
|
||||
"public_origin": null
|
||||
},
|
||||
{
|
||||
"operation_id": "readModelRegistry",
|
||||
"schema": "docs/gitops/openapi-chatgpt-actions-online-gitops-task.yaml",
|
||||
"trust_domain": "online-gitops-task",
|
||||
"server_url": "https://cnnetmkt.top",
|
||||
"server_route": "/api/model",
|
||||
"method": "GET",
|
||||
"declared_openapi_security": [],
|
||||
"required_scope": null,
|
||||
"exposure": "public",
|
||||
"source_origin": "docs/gitops/openapi-chatgpt-actions.yaml",
|
||||
"public_origin": null
|
||||
},
|
||||
{
|
||||
"operation_id": "reconcileGitOpsJob",
|
||||
"schema": "docs/gitops/openapi-chatgpt-actions-governed-release.yaml",
|
||||
"trust_domain": "governed-release",
|
||||
"server_url": "https://cnnetmkt.top",
|
||||
"server_route": "/reconcileGitOpsJob",
|
||||
"method": "POST",
|
||||
"declared_openapi_security": [],
|
||||
"required_scope": "file:write",
|
||||
"exposure": "control-plane",
|
||||
"source_origin": null,
|
||||
"public_origin": "docs/gitops/openapi-chatgpt-actions-public-runtime-2e599646.yaml"
|
||||
},
|
||||
{
|
||||
"operation_id": "registerModel",
|
||||
"schema": "docs/gitops/openapi-chatgpt-actions-online-gitops-task.yaml",
|
||||
"trust_domain": "online-gitops-task",
|
||||
"server_url": "https://cnnetmkt.top",
|
||||
"server_route": "/api/model",
|
||||
"method": "POST",
|
||||
"declared_openapi_security": [
|
||||
{
|
||||
"bearerAuth": []
|
||||
}
|
||||
],
|
||||
"required_scope": "model:write",
|
||||
"exposure": "control-plane",
|
||||
"source_origin": "docs/gitops/openapi-chatgpt-actions.yaml",
|
||||
"public_origin": null
|
||||
},
|
||||
{
|
||||
"operation_id": "registerOnlineAgent",
|
||||
"schema": "docs/gitops/openapi-chatgpt-actions-online-agent-task.yaml",
|
||||
"trust_domain": "online-agent-task",
|
||||
"server_url": "https://cnnetmkt.top",
|
||||
"server_route": "/registerOnlineAgent",
|
||||
"method": "POST",
|
||||
"declared_openapi_security": [],
|
||||
"required_scope": "agent-task:control",
|
||||
"exposure": "control-plane",
|
||||
"source_origin": null,
|
||||
"public_origin": "docs/gitops/openapi-chatgpt-actions-public-runtime-2e599646.yaml"
|
||||
},
|
||||
{
|
||||
"operation_id": "requestOrCreateTag",
|
||||
"schema": "docs/gitops/openapi-chatgpt-actions-governed-release.yaml",
|
||||
"trust_domain": "governed-release",
|
||||
"server_url": "https://cnnetmkt.top",
|
||||
"server_route": "/api/tag",
|
||||
"method": "POST",
|
||||
"declared_openapi_security": [
|
||||
{
|
||||
"bearerAuth": []
|
||||
}
|
||||
],
|
||||
"required_scope": "tag:create",
|
||||
"exposure": "control-plane",
|
||||
"source_origin": "docs/gitops/openapi-chatgpt-actions.yaml",
|
||||
"public_origin": "docs/gitops/openapi-chatgpt-actions-public-runtime-2e599646.yaml"
|
||||
},
|
||||
{
|
||||
"operation_id": "requestRollback",
|
||||
"schema": "docs/gitops/openapi-chatgpt-actions-governed-release.yaml",
|
||||
"trust_domain": "governed-release",
|
||||
"server_url": "https://cnnetmkt.top",
|
||||
"server_route": "/api/rollback",
|
||||
"method": "POST",
|
||||
"declared_openapi_security": [
|
||||
{
|
||||
"bearerAuth": []
|
||||
}
|
||||
],
|
||||
"required_scope": "rollback:request",
|
||||
"exposure": "control-plane",
|
||||
"source_origin": "docs/gitops/openapi-chatgpt-actions.yaml",
|
||||
"public_origin": "docs/gitops/openapi-chatgpt-actions-public-runtime-2e599646.yaml"
|
||||
},
|
||||
{
|
||||
"operation_id": "resumeGitOpsJob",
|
||||
"schema": "docs/gitops/openapi-chatgpt-actions-governed-release.yaml",
|
||||
"trust_domain": "governed-release",
|
||||
"server_url": "https://cnnetmkt.top",
|
||||
"server_route": "/resumeGitOpsJob",
|
||||
"method": "POST",
|
||||
"declared_openapi_security": [],
|
||||
"required_scope": "file:write",
|
||||
"exposure": "control-plane",
|
||||
"source_origin": null,
|
||||
"public_origin": "docs/gitops/openapi-chatgpt-actions-public-runtime-2e599646.yaml"
|
||||
},
|
||||
{
|
||||
"operation_id": "runReleaseAcceptance",
|
||||
"schema": "docs/gitops/openapi-chatgpt-actions-governed-release.yaml",
|
||||
"trust_domain": "governed-release",
|
||||
"server_url": "https://cnnetmkt.top",
|
||||
"server_route": "/api/release-acceptance",
|
||||
"method": "GET",
|
||||
"declared_openapi_security": [],
|
||||
"required_scope": null,
|
||||
"exposure": "public",
|
||||
"source_origin": "docs/gitops/openapi-chatgpt-actions.yaml",
|
||||
"public_origin": "docs/gitops/openapi-chatgpt-actions-public-runtime-2e599646.yaml"
|
||||
},
|
||||
{
|
||||
"operation_id": "setModelPriority",
|
||||
"schema": "docs/gitops/openapi-chatgpt-actions-online-gitops-task.yaml",
|
||||
"trust_domain": "online-gitops-task",
|
||||
"server_url": "https://cnnetmkt.top",
|
||||
"server_route": "/api/model-priority",
|
||||
"method": "PUT",
|
||||
"declared_openapi_security": [
|
||||
{
|
||||
"bearerAuth": []
|
||||
}
|
||||
],
|
||||
"required_scope": "model:priority",
|
||||
"exposure": "control-plane",
|
||||
"source_origin": "docs/gitops/openapi-chatgpt-actions.yaml",
|
||||
"public_origin": null
|
||||
},
|
||||
{
|
||||
"operation_id": "startDeployment",
|
||||
"schema": "docs/gitops/openapi-chatgpt-actions-governed-release.yaml",
|
||||
"trust_domain": "governed-release",
|
||||
"server_url": "https://cnnetmkt.top",
|
||||
"server_route": "/startDeployment",
|
||||
"method": "POST",
|
||||
"declared_openapi_security": [
|
||||
{
|
||||
"bearerAuth": []
|
||||
}
|
||||
],
|
||||
"required_scope": "release:deploy",
|
||||
"exposure": "control-plane",
|
||||
"source_origin": "docs/gitops/openapi-chatgpt-actions.yaml",
|
||||
"public_origin": "docs/gitops/openapi-chatgpt-actions-public-runtime-2e599646.yaml"
|
||||
},
|
||||
{
|
||||
"operation_id": "submitOwnerApproval",
|
||||
"schema": "docs/gitops/openapi-chatgpt-actions-governed-release.yaml",
|
||||
"trust_domain": "governed-release",
|
||||
"server_url": "https://cnnetmkt.top",
|
||||
"server_route": "/submitOwnerApproval",
|
||||
"method": "POST",
|
||||
"declared_openapi_security": [
|
||||
{
|
||||
"bearerAuth": []
|
||||
}
|
||||
],
|
||||
"required_scope": "release:approve",
|
||||
"exposure": "control-plane",
|
||||
"source_origin": "docs/gitops/openapi-chatgpt-actions.yaml",
|
||||
"public_origin": "docs/gitops/openapi-chatgpt-actions-public-runtime-2e599646.yaml"
|
||||
},
|
||||
{
|
||||
"operation_id": "submitSignedApproval",
|
||||
"schema": "docs/gitops/openapi-chatgpt-actions-governed-release.yaml",
|
||||
"trust_domain": "governed-release",
|
||||
"server_url": "https://cnnetmkt.top",
|
||||
"server_route": "/submitSignedApproval",
|
||||
"method": "POST",
|
||||
"declared_openapi_security": [
|
||||
{
|
||||
"bearerAuth": []
|
||||
}
|
||||
],
|
||||
"required_scope": "release:approve",
|
||||
"exposure": "control-plane",
|
||||
"source_origin": "docs/gitops/openapi-chatgpt-actions.yaml",
|
||||
"public_origin": null
|
||||
},
|
||||
{
|
||||
"operation_id": "verifyOnlineAgentTask",
|
||||
"schema": "docs/gitops/openapi-chatgpt-actions-online-agent-task.yaml",
|
||||
"trust_domain": "online-agent-task",
|
||||
"server_url": "https://cnnetmkt.top",
|
||||
"server_route": "/verifyOnlineAgentTask",
|
||||
"method": "POST",
|
||||
"declared_openapi_security": [],
|
||||
"required_scope": "agent-task:control",
|
||||
"exposure": "control-plane",
|
||||
"source_origin": null,
|
||||
"public_origin": "docs/gitops/openapi-chatgpt-actions-public-runtime-2e599646.yaml"
|
||||
}
|
||||
],
|
||||
"scope_provenance": {
|
||||
"release_and_gitops": "immutable staging runtime aios_model_api_staging.py handler scope checks",
|
||||
"agent_task_control": {
|
||||
"required_scope": "agent-task:control",
|
||||
"immutable_runtime_oid": "2e5996463de564793df881df5c5f684359733627",
|
||||
"proxy_routes": [
|
||||
"/api/online-agent-task",
|
||||
"/api/online-agent-task-events",
|
||||
"/createOnlineAgentTask",
|
||||
"/registerOnlineAgent",
|
||||
"/verifyOnlineAgentTask",
|
||||
"/cancelOnlineAgentTask"
|
||||
],
|
||||
"operation_ids": [
|
||||
"cancelOnlineAgentTask",
|
||||
"createOnlineAgentTask",
|
||||
"getOnlineAgentTask",
|
||||
"getOnlineAgentTaskEvents",
|
||||
"registerOnlineAgent",
|
||||
"verifyOnlineAgentTask"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
# ChatGPT Action schema trust-domain reconciliation
|
||||
|
||||
The main source has **31** operations while immutable public runtime `2e5996463de564793df881df5c5f684359733627` serves a different **30**. Both are retained as non-import parity fixtures. Their exact compatible union is **46** operations.
|
||||
|
||||
Import only these bounded specs:
|
||||
|
||||
- `openapi-chatgpt-actions-governed-release.yaml` — 26 Governed GitOps and Release operations.
|
||||
- `openapi-chatgpt-actions-online-gitops-task.yaml` — 14 Online GitOps Task operations.
|
||||
- `openapi-chatgpt-actions-online-agent-task.yaml` — 6 actual public Online Agent Task operations.
|
||||
|
||||
The machine-readable capability manifest records split schema, server route, declared OpenAPI security, required runtime scope, exposure, and `source_origin` / `public_origin` for every union operation. The dedicated Online Agent Task document includes `createOnlineAgentTask`, `registerOnlineAgent`, `getOnlineAgentTask`, `getOnlineAgentTaskEvents`, `verifyOnlineAgentTask`, and `cancelOnlineAgentTask`; each requires runtime scope `agent-task:control`. Its declared OpenAPI security is copied from the public snapshot without rewrite, while the manifest records the runtime scope separately.
|
||||
|
||||
Run `python3 scripts/validate_chatgpt_action_schema_split.py` and `python3 -m unittest discover -s tests -p 'test_chatgpt_action_schema_split.py' -v`. The validator reports `formal_validator=NOT_INSTALLED` rather than a false formal pass if `openapi-spec-validator` is absent.
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,782 @@
|
||||
openapi: 3.1.0
|
||||
info:
|
||||
title: AIOS ChatGPT Actions - Online Agent Task
|
||||
version: V1R1C0-chatgpt-actions-online-agent-task
|
||||
description: Trust-domain import from the exact union of the 31-operation main source and 30-operation
|
||||
immutable public runtime; operation, server route, and declared security are preserved.
|
||||
servers:
|
||||
- url: https://cnnetmkt.top
|
||||
security: []
|
||||
components:
|
||||
securitySchemes:
|
||||
bearerAuth:
|
||||
type: http
|
||||
scheme: bearer
|
||||
bearerFormat: opaque-token
|
||||
schemas:
|
||||
PublicationState:
|
||||
type: string
|
||||
description: Canonical durable publication state. Legacy TAG_CREATED_LOCAL is normalized on read
|
||||
and is never emitted by new writes.
|
||||
enum:
|
||||
- PENDING
|
||||
- LOCAL_TAG_CREATED
|
||||
- REMOTE_PUSH_PENDING
|
||||
- REMOTE_PUSH_CONFIRMED
|
||||
- GITEA_API_CONFIRMED
|
||||
- BASELINE_UPDATE_PENDING
|
||||
- BASELINE_UPDATED
|
||||
- PRODUCTION_DEPLOYED
|
||||
- RECONCILIATION_REQUIRED
|
||||
- MANUAL_OWNER_REVIEW_REQUIRED
|
||||
PublicationStatus:
|
||||
type: object
|
||||
required:
|
||||
- version
|
||||
- state
|
||||
properties:
|
||||
version:
|
||||
type: string
|
||||
state:
|
||||
$ref: '#/components/schemas/PublicationState'
|
||||
fail_closed:
|
||||
type: boolean
|
||||
BranchRequest:
|
||||
type: object
|
||||
required:
|
||||
- branch
|
||||
properties:
|
||||
branch:
|
||||
type: string
|
||||
example: patch/chatgpt-actions-demo
|
||||
base:
|
||||
type: string
|
||||
default: main
|
||||
message:
|
||||
type: string
|
||||
FileCommitRequest:
|
||||
type: object
|
||||
required:
|
||||
- branch
|
||||
- path
|
||||
- content
|
||||
properties:
|
||||
branch:
|
||||
type: string
|
||||
base:
|
||||
type: string
|
||||
default: main
|
||||
create_branch:
|
||||
type: boolean
|
||||
default: false
|
||||
path:
|
||||
type: string
|
||||
example: docs/gitops/chatgpt-change.md
|
||||
content:
|
||||
type: string
|
||||
message:
|
||||
type: string
|
||||
AtomicFileCommitRequest:
|
||||
type: object
|
||||
additionalProperties: false
|
||||
required:
|
||||
- branch
|
||||
- path
|
||||
- content
|
||||
- message
|
||||
- expected_head_oid
|
||||
- idempotency_key
|
||||
properties:
|
||||
branch:
|
||||
type: string
|
||||
pattern: ^patch/[A-Za-z0-9._/-]+$
|
||||
path:
|
||||
type: string
|
||||
minLength: 1
|
||||
maxLength: 512
|
||||
content:
|
||||
type: string
|
||||
maxLength: 1048576
|
||||
message:
|
||||
type: string
|
||||
minLength: 1
|
||||
maxLength: 200
|
||||
expected_head_oid:
|
||||
type: string
|
||||
pattern: ^[0-9a-f]{40}$
|
||||
idempotency_key:
|
||||
type: string
|
||||
pattern: ^[A-Za-z0-9][A-Za-z0-9._:-]{7,127}$
|
||||
CommitRequest:
|
||||
type: object
|
||||
properties:
|
||||
branch:
|
||||
type: string
|
||||
paths:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
message:
|
||||
type: string
|
||||
PatchRequest:
|
||||
type: object
|
||||
required:
|
||||
- patch_name
|
||||
properties:
|
||||
patch_name:
|
||||
type: string
|
||||
branch:
|
||||
type: string
|
||||
base:
|
||||
type: string
|
||||
default: main
|
||||
risk:
|
||||
type: string
|
||||
enum:
|
||||
- low
|
||||
- medium
|
||||
- high
|
||||
description:
|
||||
type: string
|
||||
message:
|
||||
type: string
|
||||
TagRequest:
|
||||
type: object
|
||||
required:
|
||||
- version
|
||||
- target_oid
|
||||
- evidence_closure_oid
|
||||
- approval_plan_id
|
||||
properties:
|
||||
version:
|
||||
type: string
|
||||
example: V1R1C1
|
||||
target_oid:
|
||||
type: string
|
||||
pattern: ^[0-9a-fA-F]{40}$
|
||||
description: Full approved commit OID. The tag is created explicitly at this commit, never implicitly
|
||||
at workspace HEAD.
|
||||
evidence_closure_oid:
|
||||
type: string
|
||||
pattern: ^[0-9a-fA-F]{40}$
|
||||
description: Required evidence closure commit. target_oid must be its ancestor.
|
||||
approval_plan_id:
|
||||
type: string
|
||||
description: Audited owner-approved release or tag plan identifier.
|
||||
message:
|
||||
type: string
|
||||
execute:
|
||||
type: boolean
|
||||
description: Owner-approved calls only. Otherwise an approval request commit is created.
|
||||
RollbackRequest:
|
||||
type: object
|
||||
required:
|
||||
- target_version
|
||||
properties:
|
||||
target_version:
|
||||
type: string
|
||||
reason:
|
||||
type: string
|
||||
branch:
|
||||
type: string
|
||||
ModelRequest:
|
||||
type: object
|
||||
required:
|
||||
- provider_name
|
||||
- base_url
|
||||
- model_name
|
||||
properties:
|
||||
username:
|
||||
type: string
|
||||
provider_name:
|
||||
type: string
|
||||
base_url:
|
||||
type: string
|
||||
model_name:
|
||||
type: string
|
||||
display_name:
|
||||
type: string
|
||||
api_key_secret_ref:
|
||||
type: string
|
||||
description: Preferred. Git stores this secret_ref only.
|
||||
api_key:
|
||||
type: string
|
||||
writeOnly: true
|
||||
description: Accepted only for runtime secret storage; never committed to Git or logs.
|
||||
branch:
|
||||
type: string
|
||||
message:
|
||||
type: string
|
||||
ModelPriorityRequest:
|
||||
type: object
|
||||
required:
|
||||
- items
|
||||
properties:
|
||||
username:
|
||||
type: string
|
||||
branch:
|
||||
type: string
|
||||
items:
|
||||
type: array
|
||||
items:
|
||||
type: object
|
||||
properties:
|
||||
model_id:
|
||||
type: string
|
||||
priority:
|
||||
type: integer
|
||||
enabled:
|
||||
type: boolean
|
||||
fallback_policy:
|
||||
type: string
|
||||
NLRequest:
|
||||
type: object
|
||||
required:
|
||||
- text
|
||||
properties:
|
||||
text:
|
||||
type: string
|
||||
branch:
|
||||
type: string
|
||||
DeploymentPlan:
|
||||
type: object
|
||||
required:
|
||||
- service
|
||||
- release_path
|
||||
- release_root
|
||||
- cutover_link
|
||||
- dropin_path
|
||||
- dropin_sha256
|
||||
- systemd_unit_sha256
|
||||
- rollback_path
|
||||
- rollback_sha256
|
||||
- source_commit
|
||||
- tree_oid
|
||||
- artifact_sha256
|
||||
- manifest_sha256
|
||||
- package_closure_sha256
|
||||
- authoritative_git_dir
|
||||
- repository_identity
|
||||
- writer_expected
|
||||
- baseline_before_sha256
|
||||
- registry_before_sha256
|
||||
- production_tag_refset_before_sha256
|
||||
- health_contract_version
|
||||
- timeout_policy
|
||||
- retry_policy
|
||||
- policy_sha256
|
||||
- health_contract
|
||||
properties:
|
||||
service:
|
||||
type: string
|
||||
release_path:
|
||||
type: string
|
||||
release_root:
|
||||
type: string
|
||||
cutover_link:
|
||||
type: string
|
||||
dropin_path:
|
||||
type: string
|
||||
dropin_sha256:
|
||||
type: string
|
||||
pattern: ^[0-9a-f]{64}$
|
||||
systemd_unit_sha256:
|
||||
type: string
|
||||
pattern: ^[0-9a-f]{64}$
|
||||
rollback_path:
|
||||
type: string
|
||||
rollback_sha256:
|
||||
type: string
|
||||
pattern: ^[0-9a-f]{64}$
|
||||
source_commit:
|
||||
type: string
|
||||
pattern: ^[0-9a-f]{40}$
|
||||
tree_oid:
|
||||
type: string
|
||||
pattern: ^[0-9a-f]{40}$
|
||||
artifact_sha256:
|
||||
type: string
|
||||
pattern: ^[0-9a-f]{64}$
|
||||
manifest_sha256:
|
||||
type: string
|
||||
pattern: ^[0-9a-f]{64}$
|
||||
package_closure_sha256:
|
||||
type: string
|
||||
pattern: ^[0-9a-f]{64}$
|
||||
authoritative_git_dir:
|
||||
type: string
|
||||
repository_identity:
|
||||
type: string
|
||||
writer_expected:
|
||||
type: boolean
|
||||
enum:
|
||||
- false
|
||||
baseline_before_sha256:
|
||||
type: string
|
||||
pattern: ^[0-9a-f]{64}$
|
||||
registry_before_sha256:
|
||||
type: string
|
||||
pattern: ^[0-9a-f]{64}$
|
||||
production_tag_refset_before_sha256:
|
||||
type: string
|
||||
pattern: ^[0-9a-f]{64}$
|
||||
health_contract_version:
|
||||
type: string
|
||||
timeout_policy:
|
||||
type: object
|
||||
retry_policy:
|
||||
type: object
|
||||
policy_sha256:
|
||||
type: string
|
||||
pattern: ^[0-9a-f]{64}$
|
||||
health_contract:
|
||||
type: object
|
||||
additionalProperties: true
|
||||
ReleaseCandidateInput:
|
||||
type: object
|
||||
required:
|
||||
- source_commit
|
||||
- version
|
||||
additionalProperties: false
|
||||
properties:
|
||||
candidate_id:
|
||||
type: string
|
||||
pattern: ^[A-Za-z0-9][A-Za-z0-9_.-]{2,127}$
|
||||
source_commit:
|
||||
type: string
|
||||
pattern: ^[0-9a-f]{40}$
|
||||
version:
|
||||
type: string
|
||||
pattern: ^V[0-9]+R[0-9]+C[0-9]+(?:[-.][A-Za-z0-9][A-Za-z0-9.-]*)?$
|
||||
ReleaseIntentInput:
|
||||
type: object
|
||||
required:
|
||||
- event_id
|
||||
- source_commit
|
||||
additionalProperties: false
|
||||
properties:
|
||||
event_id:
|
||||
type: string
|
||||
pattern: ^[A-Za-z0-9][A-Za-z0-9_.:-]{2,255}$
|
||||
source_commit:
|
||||
type: string
|
||||
pattern: ^[0-9a-f]{40}$
|
||||
SignedReleasePlanApproval:
|
||||
type: object
|
||||
required:
|
||||
- release_plan_id
|
||||
- candidate_id
|
||||
- source_commit
|
||||
- tree_oid
|
||||
- artifact_sha256
|
||||
- manifest_sha256
|
||||
- deployment_plan_sha256
|
||||
- release_plan_sha256
|
||||
- policy_sha256
|
||||
- approval_plan_id
|
||||
- evidence_closure_oid
|
||||
- owner_approval_record_oid
|
||||
- capabilities
|
||||
- owner_identity
|
||||
- trust_domain
|
||||
- issued_at
|
||||
- not_before
|
||||
- expires_at
|
||||
- nonce
|
||||
- key_id
|
||||
- signature
|
||||
properties:
|
||||
release_plan_id:
|
||||
type: string
|
||||
candidate_id:
|
||||
type: string
|
||||
source_commit:
|
||||
type: string
|
||||
pattern: ^[0-9a-f]{40}$
|
||||
tree_oid:
|
||||
type: string
|
||||
pattern: ^[0-9a-f]{40}$
|
||||
artifact_sha256:
|
||||
type: string
|
||||
pattern: ^[0-9a-f]{64}$
|
||||
manifest_sha256:
|
||||
type: string
|
||||
pattern: ^[0-9a-f]{64}$
|
||||
deployment_plan_sha256:
|
||||
type: string
|
||||
pattern: ^[0-9a-f]{64}$
|
||||
release_plan_sha256:
|
||||
type: string
|
||||
pattern: ^[0-9a-f]{64}$
|
||||
policy_sha256:
|
||||
type: string
|
||||
pattern: ^[0-9a-f]{64}$
|
||||
approval_plan_id:
|
||||
type: string
|
||||
evidence_closure_oid:
|
||||
type: string
|
||||
pattern: ^[0-9a-f]{40}$
|
||||
owner_approval_record_oid:
|
||||
type: string
|
||||
pattern: ^[0-9a-f]{40}$
|
||||
owner_identity:
|
||||
type: string
|
||||
trust_domain:
|
||||
type: string
|
||||
issued_at:
|
||||
type: string
|
||||
format: date-time
|
||||
not_before:
|
||||
type: string
|
||||
format: date-time
|
||||
capabilities:
|
||||
type: array
|
||||
uniqueItems: true
|
||||
items:
|
||||
type: string
|
||||
enum:
|
||||
- runtime_switch
|
||||
- tag_publish
|
||||
- baseline_registry_finalize
|
||||
expires_at:
|
||||
type: string
|
||||
format: date-time
|
||||
nonce:
|
||||
type: string
|
||||
minLength: 16
|
||||
key_id:
|
||||
type: string
|
||||
signature:
|
||||
type: string
|
||||
format: byte
|
||||
writeOnly: true
|
||||
StartDeploymentRequest:
|
||||
type: object
|
||||
required:
|
||||
- candidate_id
|
||||
additionalProperties: false
|
||||
properties:
|
||||
candidate_id:
|
||||
type: string
|
||||
Oid40:
|
||||
type: string
|
||||
pattern: ^[0-9a-f]{40}$
|
||||
CreateGovernedTaskRequest:
|
||||
type: object
|
||||
additionalProperties: false
|
||||
required:
|
||||
- task_id
|
||||
- repository
|
||||
- base_oid
|
||||
- target_branch
|
||||
- risk
|
||||
- description
|
||||
- idempotency_key
|
||||
properties:
|
||||
task_id:
|
||||
type: string
|
||||
repository:
|
||||
type: string
|
||||
const: aios/aios
|
||||
base_oid:
|
||||
$ref: '#/components/schemas/Oid40'
|
||||
target_branch:
|
||||
type: string
|
||||
pattern: ^agent/
|
||||
risk:
|
||||
type: string
|
||||
enum:
|
||||
- low
|
||||
- medium
|
||||
- high
|
||||
- engineering
|
||||
description:
|
||||
type: string
|
||||
idempotency_key:
|
||||
type: string
|
||||
expected_branch_oid:
|
||||
$ref: '#/components/schemas/Oid40'
|
||||
generation:
|
||||
type: integer
|
||||
minimum: 1
|
||||
WriterLeaseRequest:
|
||||
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
|
||||
task_id:
|
||||
type: string
|
||||
repository:
|
||||
type: string
|
||||
const: aios/aios
|
||||
branch_mode:
|
||||
type: string
|
||||
enum:
|
||||
- CREATE_NEW
|
||||
- CONTINUE_EXISTING
|
||||
base_oid:
|
||||
$ref: '#/components/schemas/Oid40'
|
||||
expected_branch_oid:
|
||||
$ref: '#/components/schemas/Oid40'
|
||||
target_branch:
|
||||
type: string
|
||||
pattern: ^agent/
|
||||
generation:
|
||||
type: integer
|
||||
minimum: 1
|
||||
lease_seconds:
|
||||
type: integer
|
||||
minimum: 1
|
||||
maximum: 86400
|
||||
CommitFileGovernedRequest:
|
||||
type: object
|
||||
additionalProperties: false
|
||||
required:
|
||||
- task_id
|
||||
- repository
|
||||
- expected_branch_oid
|
||||
- target_branch
|
||||
- generation
|
||||
- idempotency_key
|
||||
- path
|
||||
- content_base64
|
||||
- message
|
||||
properties:
|
||||
task_id:
|
||||
type: string
|
||||
repository:
|
||||
type: string
|
||||
const: aios/aios
|
||||
expected_branch_oid:
|
||||
$ref: '#/components/schemas/Oid40'
|
||||
target_branch:
|
||||
type: string
|
||||
pattern: ^agent/
|
||||
generation:
|
||||
type: integer
|
||||
minimum: 1
|
||||
idempotency_key:
|
||||
type: string
|
||||
path:
|
||||
type: string
|
||||
content_base64:
|
||||
type: string
|
||||
message:
|
||||
type: string
|
||||
CommitFilesGovernedRequest:
|
||||
type: object
|
||||
additionalProperties: false
|
||||
required:
|
||||
- task_id
|
||||
- repository
|
||||
- expected_branch_oid
|
||||
- target_branch
|
||||
- generation
|
||||
- idempotency_key
|
||||
- files
|
||||
- message
|
||||
properties:
|
||||
task_id:
|
||||
type: string
|
||||
repository:
|
||||
type: string
|
||||
const: aios/aios
|
||||
expected_branch_oid:
|
||||
$ref: '#/components/schemas/Oid40'
|
||||
target_branch:
|
||||
type: string
|
||||
pattern: ^agent/
|
||||
generation:
|
||||
type: integer
|
||||
minimum: 1
|
||||
idempotency_key:
|
||||
type: string
|
||||
files:
|
||||
type: object
|
||||
additionalProperties:
|
||||
type: string
|
||||
description: Base64 encoded file bytes
|
||||
message:
|
||||
type: string
|
||||
CreateOnlineAgentTaskRequest:
|
||||
type: object
|
||||
additionalProperties: false
|
||||
required:
|
||||
- goal
|
||||
- instructions
|
||||
- success_criteria
|
||||
- idempotency_key
|
||||
properties:
|
||||
task_id:
|
||||
type: string
|
||||
goal:
|
||||
type: string
|
||||
instructions:
|
||||
type: string
|
||||
success_criteria:
|
||||
type: array
|
||||
items: {}
|
||||
required_capabilities:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
constraints:
|
||||
type: object
|
||||
additionalProperties: true
|
||||
metadata:
|
||||
type: object
|
||||
additionalProperties: true
|
||||
deadline:
|
||||
type: string
|
||||
format: date-time
|
||||
priority:
|
||||
type: integer
|
||||
minimum: -100
|
||||
maximum: 100
|
||||
max_attempts:
|
||||
type: integer
|
||||
minimum: 1
|
||||
maximum: 20
|
||||
idempotency_key:
|
||||
type: string
|
||||
paths:
|
||||
/createOnlineAgentTask:
|
||||
post:
|
||||
operationId: createOnlineAgentTask
|
||||
x-openai-isConsequential: true
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/CreateOnlineAgentTaskRequest'
|
||||
responses:
|
||||
'200':
|
||||
description: Online task created
|
||||
'201':
|
||||
description: Online task created
|
||||
/registerOnlineAgent:
|
||||
post:
|
||||
operationId: registerOnlineAgent
|
||||
x-openai-isConsequential: true
|
||||
description: 'Control operation. The returned agent token is secret material and must be delivered
|
||||
through runtime secret storage, never Git.
|
||||
|
||||
'
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
required:
|
||||
- agent_id
|
||||
- agent_type
|
||||
- agent_version
|
||||
- capabilities
|
||||
properties:
|
||||
agent_id:
|
||||
type: string
|
||||
agent_type:
|
||||
type: string
|
||||
agent_version:
|
||||
type: string
|
||||
capabilities:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
metadata:
|
||||
type: object
|
||||
additionalProperties: true
|
||||
responses:
|
||||
'201':
|
||||
description: Agent registered
|
||||
/api/online-agent-task:
|
||||
get:
|
||||
operationId: getOnlineAgentTask
|
||||
parameters:
|
||||
- name: task_id
|
||||
in: query
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
responses:
|
||||
'200':
|
||||
description: Task status
|
||||
/api/online-agent-task-events:
|
||||
get:
|
||||
operationId: getOnlineAgentTaskEvents
|
||||
parameters:
|
||||
- name: task_id
|
||||
in: query
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
responses:
|
||||
'200':
|
||||
description: Task events
|
||||
/verifyOnlineAgentTask:
|
||||
post:
|
||||
operationId: verifyOnlineAgentTask
|
||||
x-openai-isConsequential: true
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
required:
|
||||
- task_id
|
||||
- decision
|
||||
properties:
|
||||
task_id:
|
||||
type: string
|
||||
decision:
|
||||
type: string
|
||||
enum:
|
||||
- SUCCEEDED
|
||||
- NEEDS_REWORK
|
||||
- FAILED
|
||||
verification:
|
||||
type: object
|
||||
additionalProperties: true
|
||||
responses:
|
||||
'200':
|
||||
description: Verification applied
|
||||
/cancelOnlineAgentTask:
|
||||
post:
|
||||
operationId: cancelOnlineAgentTask
|
||||
x-openai-isConsequential: true
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
required:
|
||||
- task_id
|
||||
properties:
|
||||
task_id:
|
||||
type: string
|
||||
reason:
|
||||
type: string
|
||||
responses:
|
||||
'200':
|
||||
description: Task cancelled
|
||||
x-aios-schema-reconciliation:
|
||||
source_schema: docs/gitops/openapi-chatgpt-actions.yaml
|
||||
public_runtime_schema: docs/gitops/openapi-chatgpt-actions-public-runtime-2e599646.yaml
|
||||
trust_domain: online-agent-task
|
||||
operation_count: 6
|
||||
public_action_import: true
|
||||
@@ -0,0 +1,881 @@
|
||||
openapi: 3.1.0
|
||||
info:
|
||||
title: AIOS ChatGPT Actions - Online GitOps Task
|
||||
version: V1R1C0-chatgpt-actions-online-gitops-task
|
||||
description: Trust-domain import from the exact union of the 31-operation main source and 30-operation
|
||||
immutable public runtime; operation, server route, and declared security are preserved.
|
||||
servers:
|
||||
- url: https://cnnetmkt.top
|
||||
security: []
|
||||
components:
|
||||
securitySchemes:
|
||||
bearerAuth:
|
||||
type: http
|
||||
scheme: bearer
|
||||
bearerFormat: opaque-token
|
||||
schemas:
|
||||
PublicationState:
|
||||
type: string
|
||||
description: Canonical durable publication state. Legacy TAG_CREATED_LOCAL is normalized on read
|
||||
and is never emitted by new writes.
|
||||
enum:
|
||||
- PENDING
|
||||
- LOCAL_TAG_CREATED
|
||||
- REMOTE_PUSH_PENDING
|
||||
- REMOTE_PUSH_CONFIRMED
|
||||
- GITEA_API_CONFIRMED
|
||||
- BASELINE_UPDATE_PENDING
|
||||
- BASELINE_UPDATED
|
||||
- PRODUCTION_DEPLOYED
|
||||
- RECONCILIATION_REQUIRED
|
||||
- MANUAL_OWNER_REVIEW_REQUIRED
|
||||
PublicationStatus:
|
||||
type: object
|
||||
required:
|
||||
- version
|
||||
- state
|
||||
properties:
|
||||
version:
|
||||
type: string
|
||||
state:
|
||||
$ref: '#/components/schemas/PublicationState'
|
||||
fail_closed:
|
||||
type: boolean
|
||||
BranchRequest:
|
||||
type: object
|
||||
required:
|
||||
- branch
|
||||
properties:
|
||||
branch:
|
||||
type: string
|
||||
example: patch/chatgpt-actions-demo
|
||||
base:
|
||||
type: string
|
||||
default: main
|
||||
message:
|
||||
type: string
|
||||
FileCommitRequest:
|
||||
type: object
|
||||
required:
|
||||
- branch
|
||||
- path
|
||||
- content
|
||||
properties:
|
||||
branch:
|
||||
type: string
|
||||
base:
|
||||
type: string
|
||||
default: main
|
||||
create_branch:
|
||||
type: boolean
|
||||
default: false
|
||||
path:
|
||||
type: string
|
||||
example: docs/gitops/chatgpt-change.md
|
||||
content:
|
||||
type: string
|
||||
message:
|
||||
type: string
|
||||
AtomicFileCommitRequest:
|
||||
type: object
|
||||
additionalProperties: false
|
||||
required:
|
||||
- branch
|
||||
- path
|
||||
- content
|
||||
- message
|
||||
- expected_head_oid
|
||||
- idempotency_key
|
||||
properties:
|
||||
branch:
|
||||
type: string
|
||||
pattern: ^patch/[A-Za-z0-9._/-]+$
|
||||
path:
|
||||
type: string
|
||||
minLength: 1
|
||||
maxLength: 512
|
||||
content:
|
||||
type: string
|
||||
maxLength: 1048576
|
||||
message:
|
||||
type: string
|
||||
minLength: 1
|
||||
maxLength: 200
|
||||
expected_head_oid:
|
||||
type: string
|
||||
pattern: ^[0-9a-f]{40}$
|
||||
idempotency_key:
|
||||
type: string
|
||||
pattern: ^[A-Za-z0-9][A-Za-z0-9._:-]{7,127}$
|
||||
CommitRequest:
|
||||
type: object
|
||||
properties:
|
||||
branch:
|
||||
type: string
|
||||
paths:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
message:
|
||||
type: string
|
||||
PatchRequest:
|
||||
type: object
|
||||
required:
|
||||
- patch_name
|
||||
properties:
|
||||
patch_name:
|
||||
type: string
|
||||
branch:
|
||||
type: string
|
||||
base:
|
||||
type: string
|
||||
default: main
|
||||
risk:
|
||||
type: string
|
||||
enum:
|
||||
- low
|
||||
- medium
|
||||
- high
|
||||
description:
|
||||
type: string
|
||||
message:
|
||||
type: string
|
||||
TagRequest:
|
||||
type: object
|
||||
required:
|
||||
- version
|
||||
- target_oid
|
||||
- evidence_closure_oid
|
||||
- approval_plan_id
|
||||
properties:
|
||||
version:
|
||||
type: string
|
||||
example: V1R1C1
|
||||
target_oid:
|
||||
type: string
|
||||
pattern: ^[0-9a-fA-F]{40}$
|
||||
description: Full approved commit OID. The tag is created explicitly at this commit, never implicitly
|
||||
at workspace HEAD.
|
||||
evidence_closure_oid:
|
||||
type: string
|
||||
pattern: ^[0-9a-fA-F]{40}$
|
||||
description: Required evidence closure commit. target_oid must be its ancestor.
|
||||
approval_plan_id:
|
||||
type: string
|
||||
description: Audited owner-approved release or tag plan identifier.
|
||||
message:
|
||||
type: string
|
||||
execute:
|
||||
type: boolean
|
||||
description: Owner-approved calls only. Otherwise an approval request commit is created.
|
||||
RollbackRequest:
|
||||
type: object
|
||||
required:
|
||||
- target_version
|
||||
properties:
|
||||
target_version:
|
||||
type: string
|
||||
reason:
|
||||
type: string
|
||||
branch:
|
||||
type: string
|
||||
ModelRequest:
|
||||
type: object
|
||||
required:
|
||||
- provider_name
|
||||
- base_url
|
||||
- model_name
|
||||
properties:
|
||||
username:
|
||||
type: string
|
||||
provider_name:
|
||||
type: string
|
||||
base_url:
|
||||
type: string
|
||||
model_name:
|
||||
type: string
|
||||
display_name:
|
||||
type: string
|
||||
api_key_secret_ref:
|
||||
type: string
|
||||
description: Preferred. Git stores this secret_ref only.
|
||||
api_key:
|
||||
type: string
|
||||
writeOnly: true
|
||||
description: Accepted only for runtime secret storage; never committed to Git or logs.
|
||||
branch:
|
||||
type: string
|
||||
message:
|
||||
type: string
|
||||
ModelPriorityRequest:
|
||||
type: object
|
||||
required:
|
||||
- items
|
||||
properties:
|
||||
username:
|
||||
type: string
|
||||
branch:
|
||||
type: string
|
||||
items:
|
||||
type: array
|
||||
items:
|
||||
type: object
|
||||
properties:
|
||||
model_id:
|
||||
type: string
|
||||
priority:
|
||||
type: integer
|
||||
enabled:
|
||||
type: boolean
|
||||
fallback_policy:
|
||||
type: string
|
||||
NLRequest:
|
||||
type: object
|
||||
required:
|
||||
- text
|
||||
properties:
|
||||
text:
|
||||
type: string
|
||||
branch:
|
||||
type: string
|
||||
DeploymentPlan:
|
||||
type: object
|
||||
required:
|
||||
- service
|
||||
- release_path
|
||||
- release_root
|
||||
- cutover_link
|
||||
- dropin_path
|
||||
- dropin_sha256
|
||||
- systemd_unit_sha256
|
||||
- rollback_path
|
||||
- rollback_sha256
|
||||
- source_commit
|
||||
- tree_oid
|
||||
- artifact_sha256
|
||||
- manifest_sha256
|
||||
- package_closure_sha256
|
||||
- authoritative_git_dir
|
||||
- repository_identity
|
||||
- writer_expected
|
||||
- baseline_before_sha256
|
||||
- registry_before_sha256
|
||||
- production_tag_refset_before_sha256
|
||||
- health_contract_version
|
||||
- timeout_policy
|
||||
- retry_policy
|
||||
- policy_sha256
|
||||
- health_contract
|
||||
properties:
|
||||
service:
|
||||
type: string
|
||||
release_path:
|
||||
type: string
|
||||
release_root:
|
||||
type: string
|
||||
cutover_link:
|
||||
type: string
|
||||
dropin_path:
|
||||
type: string
|
||||
dropin_sha256:
|
||||
type: string
|
||||
pattern: ^[0-9a-f]{64}$
|
||||
systemd_unit_sha256:
|
||||
type: string
|
||||
pattern: ^[0-9a-f]{64}$
|
||||
rollback_path:
|
||||
type: string
|
||||
rollback_sha256:
|
||||
type: string
|
||||
pattern: ^[0-9a-f]{64}$
|
||||
source_commit:
|
||||
type: string
|
||||
pattern: ^[0-9a-f]{40}$
|
||||
tree_oid:
|
||||
type: string
|
||||
pattern: ^[0-9a-f]{40}$
|
||||
artifact_sha256:
|
||||
type: string
|
||||
pattern: ^[0-9a-f]{64}$
|
||||
manifest_sha256:
|
||||
type: string
|
||||
pattern: ^[0-9a-f]{64}$
|
||||
package_closure_sha256:
|
||||
type: string
|
||||
pattern: ^[0-9a-f]{64}$
|
||||
authoritative_git_dir:
|
||||
type: string
|
||||
repository_identity:
|
||||
type: string
|
||||
writer_expected:
|
||||
type: boolean
|
||||
enum:
|
||||
- false
|
||||
baseline_before_sha256:
|
||||
type: string
|
||||
pattern: ^[0-9a-f]{64}$
|
||||
registry_before_sha256:
|
||||
type: string
|
||||
pattern: ^[0-9a-f]{64}$
|
||||
production_tag_refset_before_sha256:
|
||||
type: string
|
||||
pattern: ^[0-9a-f]{64}$
|
||||
health_contract_version:
|
||||
type: string
|
||||
timeout_policy:
|
||||
type: object
|
||||
retry_policy:
|
||||
type: object
|
||||
policy_sha256:
|
||||
type: string
|
||||
pattern: ^[0-9a-f]{64}$
|
||||
health_contract:
|
||||
type: object
|
||||
additionalProperties: true
|
||||
ReleaseCandidateInput:
|
||||
type: object
|
||||
required:
|
||||
- source_commit
|
||||
- version
|
||||
additionalProperties: false
|
||||
properties:
|
||||
candidate_id:
|
||||
type: string
|
||||
pattern: ^[A-Za-z0-9][A-Za-z0-9_.-]{2,127}$
|
||||
source_commit:
|
||||
type: string
|
||||
pattern: ^[0-9a-f]{40}$
|
||||
version:
|
||||
type: string
|
||||
pattern: ^V[0-9]+R[0-9]+C[0-9]+(?:[-.][A-Za-z0-9][A-Za-z0-9.-]*)?$
|
||||
ReleaseIntentInput:
|
||||
type: object
|
||||
required:
|
||||
- event_id
|
||||
- source_commit
|
||||
additionalProperties: false
|
||||
properties:
|
||||
event_id:
|
||||
type: string
|
||||
pattern: ^[A-Za-z0-9][A-Za-z0-9_.:-]{2,255}$
|
||||
source_commit:
|
||||
type: string
|
||||
pattern: ^[0-9a-f]{40}$
|
||||
SignedReleasePlanApproval:
|
||||
type: object
|
||||
required:
|
||||
- release_plan_id
|
||||
- candidate_id
|
||||
- source_commit
|
||||
- tree_oid
|
||||
- artifact_sha256
|
||||
- manifest_sha256
|
||||
- deployment_plan_sha256
|
||||
- release_plan_sha256
|
||||
- policy_sha256
|
||||
- approval_plan_id
|
||||
- evidence_closure_oid
|
||||
- owner_approval_record_oid
|
||||
- capabilities
|
||||
- owner_identity
|
||||
- trust_domain
|
||||
- issued_at
|
||||
- not_before
|
||||
- expires_at
|
||||
- nonce
|
||||
- key_id
|
||||
- signature
|
||||
properties:
|
||||
release_plan_id:
|
||||
type: string
|
||||
candidate_id:
|
||||
type: string
|
||||
source_commit:
|
||||
type: string
|
||||
pattern: ^[0-9a-f]{40}$
|
||||
tree_oid:
|
||||
type: string
|
||||
pattern: ^[0-9a-f]{40}$
|
||||
artifact_sha256:
|
||||
type: string
|
||||
pattern: ^[0-9a-f]{64}$
|
||||
manifest_sha256:
|
||||
type: string
|
||||
pattern: ^[0-9a-f]{64}$
|
||||
deployment_plan_sha256:
|
||||
type: string
|
||||
pattern: ^[0-9a-f]{64}$
|
||||
release_plan_sha256:
|
||||
type: string
|
||||
pattern: ^[0-9a-f]{64}$
|
||||
policy_sha256:
|
||||
type: string
|
||||
pattern: ^[0-9a-f]{64}$
|
||||
approval_plan_id:
|
||||
type: string
|
||||
evidence_closure_oid:
|
||||
type: string
|
||||
pattern: ^[0-9a-f]{40}$
|
||||
owner_approval_record_oid:
|
||||
type: string
|
||||
pattern: ^[0-9a-f]{40}$
|
||||
owner_identity:
|
||||
type: string
|
||||
trust_domain:
|
||||
type: string
|
||||
issued_at:
|
||||
type: string
|
||||
format: date-time
|
||||
not_before:
|
||||
type: string
|
||||
format: date-time
|
||||
capabilities:
|
||||
type: array
|
||||
uniqueItems: true
|
||||
items:
|
||||
type: string
|
||||
enum:
|
||||
- runtime_switch
|
||||
- tag_publish
|
||||
- baseline_registry_finalize
|
||||
expires_at:
|
||||
type: string
|
||||
format: date-time
|
||||
nonce:
|
||||
type: string
|
||||
minLength: 16
|
||||
key_id:
|
||||
type: string
|
||||
signature:
|
||||
type: string
|
||||
format: byte
|
||||
writeOnly: true
|
||||
StartDeploymentRequest:
|
||||
type: object
|
||||
required:
|
||||
- candidate_id
|
||||
additionalProperties: false
|
||||
properties:
|
||||
candidate_id:
|
||||
type: string
|
||||
Oid40:
|
||||
type: string
|
||||
pattern: ^[0-9a-f]{40}$
|
||||
CreateGovernedTaskRequest:
|
||||
type: object
|
||||
additionalProperties: false
|
||||
required:
|
||||
- task_id
|
||||
- repository
|
||||
- base_oid
|
||||
- target_branch
|
||||
- risk
|
||||
- description
|
||||
- idempotency_key
|
||||
properties:
|
||||
task_id:
|
||||
type: string
|
||||
repository:
|
||||
type: string
|
||||
const: aios/aios
|
||||
base_oid:
|
||||
$ref: '#/components/schemas/Oid40'
|
||||
target_branch:
|
||||
type: string
|
||||
pattern: ^agent/
|
||||
risk:
|
||||
type: string
|
||||
enum:
|
||||
- low
|
||||
- medium
|
||||
- high
|
||||
- engineering
|
||||
description:
|
||||
type: string
|
||||
idempotency_key:
|
||||
type: string
|
||||
expected_branch_oid:
|
||||
$ref: '#/components/schemas/Oid40'
|
||||
generation:
|
||||
type: integer
|
||||
minimum: 1
|
||||
WriterLeaseRequest:
|
||||
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
|
||||
task_id:
|
||||
type: string
|
||||
repository:
|
||||
type: string
|
||||
const: aios/aios
|
||||
branch_mode:
|
||||
type: string
|
||||
enum:
|
||||
- CREATE_NEW
|
||||
- CONTINUE_EXISTING
|
||||
base_oid:
|
||||
$ref: '#/components/schemas/Oid40'
|
||||
expected_branch_oid:
|
||||
$ref: '#/components/schemas/Oid40'
|
||||
target_branch:
|
||||
type: string
|
||||
pattern: ^agent/
|
||||
generation:
|
||||
type: integer
|
||||
minimum: 1
|
||||
lease_seconds:
|
||||
type: integer
|
||||
minimum: 1
|
||||
maximum: 86400
|
||||
CommitFileGovernedRequest:
|
||||
type: object
|
||||
additionalProperties: false
|
||||
required:
|
||||
- task_id
|
||||
- repository
|
||||
- expected_branch_oid
|
||||
- target_branch
|
||||
- generation
|
||||
- idempotency_key
|
||||
- path
|
||||
- content_base64
|
||||
- message
|
||||
properties:
|
||||
task_id:
|
||||
type: string
|
||||
repository:
|
||||
type: string
|
||||
const: aios/aios
|
||||
expected_branch_oid:
|
||||
$ref: '#/components/schemas/Oid40'
|
||||
target_branch:
|
||||
type: string
|
||||
pattern: ^agent/
|
||||
generation:
|
||||
type: integer
|
||||
minimum: 1
|
||||
idempotency_key:
|
||||
type: string
|
||||
path:
|
||||
type: string
|
||||
content_base64:
|
||||
type: string
|
||||
message:
|
||||
type: string
|
||||
CommitFilesGovernedRequest:
|
||||
type: object
|
||||
additionalProperties: false
|
||||
required:
|
||||
- task_id
|
||||
- repository
|
||||
- expected_branch_oid
|
||||
- target_branch
|
||||
- generation
|
||||
- idempotency_key
|
||||
- files
|
||||
- message
|
||||
properties:
|
||||
task_id:
|
||||
type: string
|
||||
repository:
|
||||
type: string
|
||||
const: aios/aios
|
||||
expected_branch_oid:
|
||||
$ref: '#/components/schemas/Oid40'
|
||||
target_branch:
|
||||
type: string
|
||||
pattern: ^agent/
|
||||
generation:
|
||||
type: integer
|
||||
minimum: 1
|
||||
idempotency_key:
|
||||
type: string
|
||||
files:
|
||||
type: object
|
||||
additionalProperties:
|
||||
type: string
|
||||
description: Base64 encoded file bytes
|
||||
message:
|
||||
type: string
|
||||
CreateOnlineAgentTaskRequest:
|
||||
type: object
|
||||
additionalProperties: false
|
||||
required:
|
||||
- goal
|
||||
- instructions
|
||||
- success_criteria
|
||||
- idempotency_key
|
||||
properties:
|
||||
task_id:
|
||||
type: string
|
||||
goal:
|
||||
type: string
|
||||
instructions:
|
||||
type: string
|
||||
success_criteria:
|
||||
type: array
|
||||
items: {}
|
||||
required_capabilities:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
constraints:
|
||||
type: object
|
||||
additionalProperties: true
|
||||
metadata:
|
||||
type: object
|
||||
additionalProperties: true
|
||||
deadline:
|
||||
type: string
|
||||
format: date-time
|
||||
priority:
|
||||
type: integer
|
||||
minimum: -100
|
||||
maximum: 100
|
||||
max_attempts:
|
||||
type: integer
|
||||
minimum: 1
|
||||
maximum: 20
|
||||
idempotency_key:
|
||||
type: string
|
||||
paths:
|
||||
/api/base:
|
||||
get:
|
||||
operationId: getBaseline
|
||||
summary: Read current AIOS baseline
|
||||
responses:
|
||||
'200':
|
||||
description: OK
|
||||
/api/ver:
|
||||
get:
|
||||
operationId: listVersions
|
||||
summary: List GitOps versions
|
||||
responses:
|
||||
'200':
|
||||
description: OK
|
||||
/api/diff:
|
||||
get:
|
||||
operationId: getDiff
|
||||
summary: Read Git diff between refs
|
||||
parameters:
|
||||
- in: query
|
||||
name: from
|
||||
schema:
|
||||
type: string
|
||||
default: HEAD~1
|
||||
- in: query
|
||||
name: to
|
||||
schema:
|
||||
type: string
|
||||
default: HEAD
|
||||
responses:
|
||||
'200':
|
||||
description: OK
|
||||
/api/patch:
|
||||
get:
|
||||
operationId: listPatches
|
||||
summary: List patch records
|
||||
responses:
|
||||
'200':
|
||||
description: OK
|
||||
post:
|
||||
operationId: createPatch
|
||||
summary: Create a patch branch and commit metadata
|
||||
security:
|
||||
- bearerAuth: []
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/PatchRequest'
|
||||
responses:
|
||||
'200':
|
||||
description: Patch branch committed
|
||||
'401':
|
||||
description: Unauthorized
|
||||
'403':
|
||||
description: Forbidden
|
||||
x-openai-isConsequential: true
|
||||
/api/model:
|
||||
get:
|
||||
operationId: readModelRegistry
|
||||
summary: Read visible model registry and priority
|
||||
responses:
|
||||
'200':
|
||||
description: OK
|
||||
post:
|
||||
operationId: registerModel
|
||||
summary: Register a custom model; API keys are stored as runtime secrets and Git receives secret_ref
|
||||
only
|
||||
security:
|
||||
- bearerAuth: []
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ModelRequest'
|
||||
responses:
|
||||
'200':
|
||||
description: Model registered and committed
|
||||
'401':
|
||||
description: Unauthorized
|
||||
'403':
|
||||
description: Forbidden
|
||||
x-openai-isConsequential: true
|
||||
/api/run:
|
||||
get:
|
||||
operationId: getRuntimeStatus
|
||||
summary: Read runtime status
|
||||
responses:
|
||||
'200':
|
||||
description: OK
|
||||
/api/branch:
|
||||
post:
|
||||
operationId: createBranch
|
||||
summary: Create a non-main branch and commit branch metadata
|
||||
security:
|
||||
- bearerAuth: []
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/BranchRequest'
|
||||
responses:
|
||||
'200':
|
||||
description: Branch created
|
||||
'401':
|
||||
description: Unauthorized
|
||||
'403':
|
||||
description: Forbidden
|
||||
x-openai-isConsequential: true
|
||||
/api/file:
|
||||
post:
|
||||
operationId: commitFile
|
||||
summary: Write a file and create a Git commit on a non-main branch
|
||||
security:
|
||||
- bearerAuth: []
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/FileCommitRequest'
|
||||
responses:
|
||||
'200':
|
||||
description: File committed
|
||||
'400':
|
||||
description: Secret rejected or invalid path
|
||||
'401':
|
||||
description: Unauthorized
|
||||
x-openai-isConsequential: true
|
||||
/api/commit-file:
|
||||
post:
|
||||
operationId: commitFileAtomic
|
||||
summary: Atomically write one allowlisted file, commit, push, and verify using Token Policy V2
|
||||
security:
|
||||
- bearerAuth: []
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/AtomicFileCommitRequest'
|
||||
responses:
|
||||
'200':
|
||||
description: Completed or idempotently replayed
|
||||
'400':
|
||||
description: Invalid canonical request
|
||||
'401':
|
||||
description: Missing
|
||||
invalid: null
|
||||
expired: null
|
||||
or revoked Token Policy V2 grant: null
|
||||
'403':
|
||||
description: Audience
|
||||
source: null
|
||||
branch: null
|
||||
operation: null
|
||||
path: null
|
||||
or policy denied: null
|
||||
'409':
|
||||
description: Idempotency conflict or manual review required
|
||||
'413':
|
||||
description: Request too large
|
||||
'503':
|
||||
description: Push or remote verification unavailable; recoverable
|
||||
x-openai-isConsequential: true
|
||||
/api/commit:
|
||||
post:
|
||||
operationId: commitChanges
|
||||
summary: Commit existing workspace changes on a non-main branch
|
||||
security:
|
||||
- bearerAuth: []
|
||||
requestBody:
|
||||
required: false
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/CommitRequest'
|
||||
responses:
|
||||
'200':
|
||||
description: Commit result
|
||||
'401':
|
||||
description: Unauthorized
|
||||
x-openai-isConsequential: true
|
||||
/api/model-priority:
|
||||
put:
|
||||
operationId: setModelPriority
|
||||
summary: Set model priority and commit policy file
|
||||
security:
|
||||
- bearerAuth: []
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ModelPriorityRequest'
|
||||
responses:
|
||||
'200':
|
||||
description: Priority updated and committed
|
||||
'401':
|
||||
description: Unauthorized
|
||||
x-openai-isConsequential: true
|
||||
/api/nl:
|
||||
post:
|
||||
operationId: planNaturalLanguageGitOpsAction
|
||||
summary: Convert natural language to a safe committed plan without executing V-level changes
|
||||
security:
|
||||
- bearerAuth: []
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/NLRequest'
|
||||
responses:
|
||||
'200':
|
||||
description: Safe GitOps plan
|
||||
'401':
|
||||
description: Unauthorized
|
||||
x-openai-isConsequential: true
|
||||
x-aios-schema-reconciliation:
|
||||
source_schema: docs/gitops/openapi-chatgpt-actions.yaml
|
||||
public_runtime_schema: docs/gitops/openapi-chatgpt-actions-public-runtime-2e599646.yaml
|
||||
trust_domain: online-gitops-task
|
||||
operation_count: 14
|
||||
public_action_import: true
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,117 +0,0 @@
|
||||
# Server-local governed immutable Host seed executor
|
||||
|
||||
Date: 2026-08-22
|
||||
|
||||
## Goal
|
||||
|
||||
Provide a server-local, fail-closed way to publish an already validated
|
||||
immutable control-plane candidate without depending on Windows, an external
|
||||
Agent worker, SSH, or an arbitrary root command channel.
|
||||
|
||||
The executor is deliberately narrower than deployment:
|
||||
|
||||
- it reads only the authoritative local Git object database;
|
||||
- it consumes one fixed request ref and path;
|
||||
- it accepts only `HOST_SEED_IMMUTABLE_CANDIDATE`;
|
||||
- it requires a high-risk governed-task binding whose description binds the
|
||||
exact candidate intent *and* authorization expiry;
|
||||
- it materializes `base_oid + candidate.patch`, verifies every release-member
|
||||
SHA against the candidate manifest, and verifies the candidate Machine Goal;
|
||||
- it atomically publishes one immutable directory under `/opt/aios/releases`;
|
||||
- it never starts or restarts services, starts the Projector, creates the fixed
|
||||
continuation Task/Job, switches Production, or writes `main`, tags, or the
|
||||
baseline.
|
||||
|
||||
## Canonical server path
|
||||
|
||||
```text
|
||||
explicit Owner HOST_SEED authorization
|
||||
-> high-risk governed authorization task binding
|
||||
-> governed request commit on refs/heads/agent/host-seed-authorization-requests
|
||||
-> root timer polls local authoritative Git
|
||||
-> validate exact intent, expiry, binding SHA, candidate evidence
|
||||
-> read payload commit from local Git
|
||||
-> shared disposable clone at exact base
|
||||
-> apply declared patch
|
||||
-> verify exact changed set + all release-member bytes
|
||||
-> atomic immutable publish
|
||||
-> durable sanitized receipt
|
||||
```
|
||||
|
||||
The Host seed request is not the fixed continuation Task. The fixed
|
||||
continuation Task remains a separate later gate.
|
||||
|
||||
## Trust and authorization binding
|
||||
|
||||
The request contains an immutable `intent` with:
|
||||
|
||||
- candidate / release id;
|
||||
- base OID and payload commit;
|
||||
- fixed patch, manifest, Machine Goal and bootstrap paths;
|
||||
- validation artifact SHA;
|
||||
- Manifest, Machine Goal, release-tree and bootstrap SHAs;
|
||||
- exact target Host identity.
|
||||
|
||||
`intent_sha256` is the canonical SHA of that object. Authorization adds
|
||||
`task_id` and `expires_at`; those are hashed into `authorization_sha256`.
|
||||
The server-side governed-task binding must contain:
|
||||
|
||||
```text
|
||||
risk=high
|
||||
description=HOST_SEED_AUTHORIZATION:<authorization_sha256>
|
||||
target_branch=agent/host-seed-authorization-...
|
||||
```
|
||||
|
||||
The request also binds the exact byte SHA of that server-side binding. This
|
||||
prevents extending the authorization window or changing the candidate without
|
||||
creating a new governed authorization binding.
|
||||
|
||||
The executor additionally requires the binding file to be a non-symlink
|
||||
regular file, not group/world writable, and owned by the configured Bridge
|
||||
service account in production.
|
||||
|
||||
## Delivery model
|
||||
|
||||
The live delivery payload is server-native Git data, not the external ZIP.
|
||||
The payload commit carries:
|
||||
|
||||
```text
|
||||
control-plane/candidates/<candidate>/candidate.patch
|
||||
control-plane/candidates/<candidate>/implementation_manifest_*.json
|
||||
control-plane/candidates/<candidate>/machine_goal_*.json
|
||||
control-plane/candidates/<candidate>/aios_control_plane_immutable_seed_*.py
|
||||
```
|
||||
|
||||
The external ZIP SHA remains bound as validation evidence through Machine
|
||||
Goal, but the executor records `artifact_delivery_used=false`: it does not
|
||||
claim that ZIP bytes were copied to the Host.
|
||||
|
||||
The release tree itself is reconstructed and verified from authoritative Git
|
||||
objects plus the declared patch.
|
||||
|
||||
## Idempotency and recovery
|
||||
|
||||
Before publishing, the executor writes a durable `PENDING` operation record
|
||||
keyed by the exact intent SHA. The final receipt is `EXECUTED`.
|
||||
|
||||
If the immutable release directory already exists, every member must match the
|
||||
manifest before the operation is accepted as idempotent. Any drift fails
|
||||
closed.
|
||||
|
||||
A missing request ref is a normal `NO_REQUEST` poll and has no side effects.
|
||||
|
||||
## Systemd confinement
|
||||
|
||||
The service:
|
||||
|
||||
- runs as root only because immutable release publication requires it;
|
||||
- has no network access;
|
||||
- reads authoritative Git and Bridge task bindings read-only;
|
||||
- can write only its private state root and `/opt/aios/releases`;
|
||||
- has no service-control, Projector, Task, Git-ref, Production, tag or baseline
|
||||
operation in its code path.
|
||||
|
||||
The timer polls every 30 seconds. Installing/enabling this executor is itself a
|
||||
separate governed control-plane deployment. Installing it does **not** grant a
|
||||
Host seed authorization and does not seed any candidate while the fixed
|
||||
request ref is absent.
|
||||
@@ -0,0 +1,67 @@
|
||||
#!/usr/bin/env python3
|
||||
from pathlib import Path
|
||||
import copy,hashlib,importlib.util,json,sys,yaml
|
||||
R=Path(__file__).resolve().parents[1];D=R/'docs/gitops';M={'get','post','put','patch','delete','head','options','trace'}
|
||||
N=['openapi-chatgpt-actions-governed-release.yaml','openapi-chatgpt-actions-online-gitops-task.yaml','openapi-chatgpt-actions-online-agent-task.yaml']
|
||||
def die(s):raise AssertionError(s)
|
||||
def load(p):
|
||||
x=yaml.safe_load(Path(p).read_text());
|
||||
if not isinstance(x,dict):die('yaml mapping required:'+str(p))
|
||||
return x
|
||||
def op(d,label):
|
||||
r={}
|
||||
for route,ms in d.get('paths',{}).items():
|
||||
if not isinstance(ms,dict) or not isinstance(route,str) or not route.startswith('/'):die(label+': paths')
|
||||
for method,x in ms.items():
|
||||
if method.lower() in M:
|
||||
if not isinstance(x,dict) or not x.get('operationId'):die(label+': operationId')
|
||||
i=x['operationId'];
|
||||
if i in r:die(label+': duplicate '+i)
|
||||
r[i]={'route':route,'method':method.upper(),'security':copy.deepcopy(x.get('security',d.get('security',[])))}
|
||||
return r
|
||||
def st(x):return json.dumps(x,sort_keys=True,separators=(',',':'))
|
||||
def refs(x,c,label):
|
||||
if isinstance(x,dict):
|
||||
if isinstance(x.get('$ref'),str) and x['$ref'].startswith('#/components/'):
|
||||
z={'components':c}
|
||||
for k in x['$ref'][2:].split('/'):
|
||||
if not isinstance(z,dict) or k not in z:die(label+': unresolved '+x['$ref'])
|
||||
z=z[k]
|
||||
for v in x.values():refs(v,c,label)
|
||||
elif isinstance(x,list):
|
||||
for v in x:refs(v,c,label)
|
||||
def main():
|
||||
s,p=load(D/'openapi-chatgpt-actions.yaml'),load(D/'openapi-chatgpt-actions-public-runtime-2e599646.yaml');so,po=op(s,'source'),op(p,'public')
|
||||
if (len(so),len(po))!=(31,30) or s.get('servers')!=p.get('servers'):die('input count/server drift')
|
||||
for i in so.keys()&po.keys():
|
||||
if any(st(so[i][k])!=st(po[i][k]) for k in ('route','method','security')):die('common drift '+i)
|
||||
u={**so,**po}
|
||||
if len(u)!=46:die('union count')
|
||||
z={}
|
||||
for n in N:
|
||||
d=load(D/n)
|
||||
if not str(d.get('openapi','')).startswith('3.') or d.get('servers')!=s['servers'] or not isinstance(d.get('components'),dict):die('openapi structure '+n)
|
||||
refs(d.get('paths'),d['components'],n); q=op(d,n)
|
||||
if not 1<=len(q)<=30 or z.keys()&q.keys():die('limit/duplicate '+n)
|
||||
z.update(q)
|
||||
if set(z)!=set(u):die('split coverage')
|
||||
for i in u:
|
||||
if any(st(z[i][k])!=st(u[i][k]) for k in ('route','method','security')):die('route/auth '+i)
|
||||
m=json.loads((D/'chatgpt-action-schema-capability-manifest.json').read_text());r={x.get('operation_id'):x for x in m.get('operations',[]) if isinstance(x,dict)}
|
||||
if m.get('union_operation_count')!=46 or set(r)!=set(u):die('manifest coverage')
|
||||
for i,x in u.items():
|
||||
a=r[i]
|
||||
if a.get('server_route')!=x['route'] or a.get('method')!=x['method'] or st(a.get('declared_openapi_security'))!=st(x['security']):die('manifest route/auth '+i)
|
||||
if bool(a.get('source_origin'))!=(i in so) or bool(a.get('public_origin'))!=(i in po):die('manifest origins '+i)
|
||||
A={'createOnlineAgentTask','registerOnlineAgent','getOnlineAgentTask','getOnlineAgentTaskEvents','verifyOnlineAgentTask','cancelOnlineAgentTask'}
|
||||
if set(m.get('scope_provenance',{}).get('agent_task_control',{}).get('operation_ids',[]))!=A:die('agent ids')
|
||||
for i in A:
|
||||
if r[i].get('required_scope')!='agent-task:control' or r[i].get('schema')!='docs/gitops/openapi-chatgpt-actions-online-agent-task.yaml':die('agent scope '+i)
|
||||
if importlib.util.find_spec('openapi_spec_validator'):
|
||||
from openapi_spec_validator import validate_spec
|
||||
for n in N:validate_spec(load(D/n))
|
||||
print('formal_validator=PASS')
|
||||
else:print('formal_validator=NOT_INSTALLED')
|
||||
print('yaml_parse=PASS');print('openapi_structure=PASS');print('source_coverage=PASS');print('public_runtime_coverage=PASS');print('union_route_auth_reconciliation=PASS');print('capability_manifest=PASS');print('schema_split_validation=PASS')
|
||||
try:main()
|
||||
except (AssertionError,OSError,ValueError,yaml.YAMLError,json.JSONDecodeError) as e:print('schema_split_validation=FAIL: '+str(e),file=sys.stderr);sys.exit(1)
|
||||
@@ -0,0 +1,9 @@
|
||||
import subprocess,sys,unittest
|
||||
from pathlib import Path
|
||||
R=Path(__file__).resolve().parents[1]
|
||||
class SchemaSplitTest(unittest.TestCase):
|
||||
def test_reconciliation(self):
|
||||
x=subprocess.run([sys.executable,str(R/'scripts/validate_chatgpt_action_schema_split.py')],cwd=R,text=True,capture_output=True)
|
||||
self.assertEqual(x.returncode,0,x.stdout+x.stderr)
|
||||
for s in ('source_coverage=PASS','public_runtime_coverage=PASS','union_route_auth_reconciliation=PASS','schema_split_validation=PASS'):self.assertIn(s,x.stdout)
|
||||
if __name__=='__main__':unittest.main()
|
||||
@@ -1,328 +0,0 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime, timedelta, timezone
|
||||
import hashlib
|
||||
import importlib.util
|
||||
import json
|
||||
from pathlib import Path
|
||||
import shutil
|
||||
import subprocess
|
||||
|
||||
import pytest
|
||||
|
||||
|
||||
MODULE_PATH = Path(__file__).parents[1] / "active-runtime" / "app" / "aios_control_plane_host_seed_executor.py"
|
||||
spec = importlib.util.spec_from_file_location("host_seed_executor", MODULE_PATH)
|
||||
m = importlib.util.module_from_spec(spec)
|
||||
assert spec and spec.loader
|
||||
import sys
|
||||
sys.modules[spec.name] = m
|
||||
spec.loader.exec_module(m)
|
||||
|
||||
|
||||
def run(*args: str, cwd: Path) -> str:
|
||||
return subprocess.check_output(args, cwd=cwd, text=True).strip()
|
||||
|
||||
|
||||
def sha(data: bytes) -> str:
|
||||
return hashlib.sha256(data).hexdigest()
|
||||
|
||||
|
||||
def canonical(value: object) -> bytes:
|
||||
return (json.dumps(value, sort_keys=True, separators=(",", ":")) + "\n").encode()
|
||||
|
||||
|
||||
def setup_case(tmp_path: Path, *, risk: str = "high", tamper_bootstrap: bool = False):
|
||||
repo = tmp_path / "src"
|
||||
bare = tmp_path / "repo.git"
|
||||
repo.mkdir()
|
||||
run("git", "init", "-b", "main", cwd=repo)
|
||||
run("git", "config", "user.email", "test@example.invalid", cwd=repo)
|
||||
run("git", "config", "user.name", "test", cwd=repo)
|
||||
(repo / "a.txt").write_text("base\n", encoding="utf-8")
|
||||
(repo / "bin.sh").write_text("#!/bin/sh\nexit 0\n", encoding="utf-8")
|
||||
(repo / "bin.sh").chmod(0o755)
|
||||
run("git", "add", "-A", cwd=repo)
|
||||
run("git", "commit", "-m", "base", cwd=repo)
|
||||
base = run("git", "rev-parse", "HEAD", cwd=repo)
|
||||
run("git", "clone", "--bare", str(repo), str(bare), cwd=tmp_path)
|
||||
|
||||
(repo / "a.txt").write_text("changed\n", encoding="utf-8")
|
||||
(repo / "c.txt").write_text("added\n", encoding="utf-8")
|
||||
run("git", "add", "-N", "c.txt", cwd=repo)
|
||||
patch = subprocess.check_output(["git", "diff", "--binary", base], cwd=repo)
|
||||
after = {
|
||||
"a.txt": sha((repo / "a.txt").read_bytes()),
|
||||
"bin.sh": sha((repo / "bin.sh").read_bytes()),
|
||||
"c.txt": sha((repo / "c.txt").read_bytes()),
|
||||
}
|
||||
package_members = {f"payload/repo/after/{k}": v for k, v in after.items()}
|
||||
release_tree = sha(json.dumps({k: package_members[k] for k in sorted(package_members)}, sort_keys=True, separators=(",", ":")).encode())
|
||||
manifest = {
|
||||
"schema": "aios.control-plane-immutable-candidate.v6",
|
||||
"candidate": "V9",
|
||||
"repository": "aios/aios",
|
||||
"base_oid": base,
|
||||
"release_id": "control-plane-test-v9",
|
||||
"changed_file_count": 2,
|
||||
"changed_files": [
|
||||
{"path": "a.txt", "status": "modified", "after_sha256": after["a.txt"]},
|
||||
{"path": "c.txt", "status": "added", "after_sha256": after["c.txt"]},
|
||||
],
|
||||
"release_member_count": 3,
|
||||
"package_members": package_members,
|
||||
"release_tree_sha256": release_tree,
|
||||
}
|
||||
manifest_raw = canonical(manifest)
|
||||
bootstrap = b"#!/usr/bin/env python3\nprint('seed helper evidence only')\n"
|
||||
bootstrap_sha = sha(bootstrap)
|
||||
artifact_sha = "a" * 64
|
||||
goal = {
|
||||
"schema": "aios.control-plane-immutable-candidate-goal.v6",
|
||||
"candidate": "V9",
|
||||
"repository": "aios/aios",
|
||||
"base_oid": base,
|
||||
"release_id": "control-plane-test-v9",
|
||||
"artifact_sha256": artifact_sha,
|
||||
"manifest_sha256": sha(manifest_raw),
|
||||
"release_tree_sha256": release_tree,
|
||||
"seed_bootstrap_sha256": bootstrap_sha,
|
||||
"host_seed_authorized": False,
|
||||
}
|
||||
goal_raw = canonical(goal)
|
||||
|
||||
# Reset the source worktree to base; delivery files live in a separate commit.
|
||||
run("git", "reset", "--hard", base, cwd=repo)
|
||||
delivery = repo / "control-plane" / "candidates" / "v9"
|
||||
delivery.mkdir(parents=True)
|
||||
(delivery / "candidate.patch").write_bytes(patch)
|
||||
(delivery / "implementation_manifest_v9.json").write_bytes(manifest_raw)
|
||||
(delivery / "machine_goal_v9.json").write_bytes(goal_raw)
|
||||
(delivery / "aios_control_plane_immutable_seed_v9.py").write_bytes(
|
||||
b"tampered\n" if tamper_bootstrap else bootstrap
|
||||
)
|
||||
run("git", "add", "-A", cwd=repo)
|
||||
run("git", "commit", "-m", "candidate payload", cwd=repo)
|
||||
payload_commit = run("git", "rev-parse", "HEAD", cwd=repo)
|
||||
run("git", "push", str(bare), f"{payload_commit}:refs/heads/agent/candidate-payload", cwd=repo)
|
||||
|
||||
intent = {
|
||||
"candidate": "V9",
|
||||
"release_id": "control-plane-test-v9",
|
||||
"base_oid": base,
|
||||
"payload_commit": payload_commit,
|
||||
"patch_path": "control-plane/candidates/v9/candidate.patch",
|
||||
"manifest_path": "control-plane/candidates/v9/implementation_manifest_v9.json",
|
||||
"machine_goal_path": "control-plane/candidates/v9/machine_goal_v9.json",
|
||||
"seed_bootstrap_path": "control-plane/candidates/v9/aios_control_plane_immutable_seed_v9.py",
|
||||
"artifact_sha256": artifact_sha,
|
||||
"manifest_sha256": sha(manifest_raw),
|
||||
"machine_goal_sha256": sha(goal_raw),
|
||||
"release_tree_sha256": release_tree,
|
||||
"seed_bootstrap_sha256": bootstrap_sha,
|
||||
"target_host": "cn-network-mkt",
|
||||
}
|
||||
intent_digest = sha(canonical(intent))
|
||||
task_id = "host-seed-authz-v9-cn-network-mkt"
|
||||
now = datetime(2026, 8, 22, 12, 0, tzinfo=timezone.utc)
|
||||
expires_text = (now + timedelta(minutes=30)).isoformat().replace("+00:00", "Z")
|
||||
authorization_digest = sha(canonical({
|
||||
"task_id": task_id,
|
||||
"intent_sha256": intent_digest,
|
||||
"expires_at": expires_text,
|
||||
}))
|
||||
binding = {
|
||||
"task_id": task_id,
|
||||
"repository": "aios/aios",
|
||||
"base_oid": payload_commit,
|
||||
"target_branch": "agent/host-seed-authorization-v9-cn-network-mkt",
|
||||
"risk": risk,
|
||||
"description": "HOST_SEED_AUTHORIZATION:" + authorization_digest,
|
||||
"idempotency_key": "host-seed-authz-v9-1",
|
||||
"expected_branch_oid": payload_commit,
|
||||
"branch_mode": "CREATE_NEW",
|
||||
"generation": 1,
|
||||
"job_id": "job-" + "1" * 32,
|
||||
}
|
||||
binding_root = tmp_path / "bindings"
|
||||
binding_root.mkdir()
|
||||
binding_raw = json.dumps(binding, sort_keys=True).encode()
|
||||
(binding_root / f"{task_id}.json").write_bytes(binding_raw)
|
||||
(binding_root / f"{task_id}.json").chmod(0o600)
|
||||
|
||||
request = {
|
||||
"schema": "aios.control-plane-host-seed-request.v1",
|
||||
"operation": "HOST_SEED_IMMUTABLE_CANDIDATE",
|
||||
"repository": "aios/aios",
|
||||
"intent": intent,
|
||||
"authorization": {
|
||||
"task_id": task_id,
|
||||
"binding_sha256": sha(binding_raw),
|
||||
"expires_at": expires_text,
|
||||
"intent_sha256": intent_digest,
|
||||
"authorization_sha256": authorization_digest,
|
||||
},
|
||||
}
|
||||
request_path = repo / "control-plane" / "host-seed" / "request.json"
|
||||
request_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
request_path.write_bytes(canonical(request))
|
||||
run("git", "add", str(request_path.relative_to(repo)), cwd=repo)
|
||||
run("git", "commit", "-m", "host seed request", cwd=repo)
|
||||
request_commit = run("git", "rev-parse", "HEAD", cwd=repo)
|
||||
run("git", "push", str(bare), f"{request_commit}:{m.REQUEST_REF}", cwd=repo)
|
||||
|
||||
config = m.Config(
|
||||
git_dir=bare,
|
||||
task_binding_root=binding_root,
|
||||
state_root=tmp_path / "state",
|
||||
release_root=tmp_path / "releases",
|
||||
host_identity="cn-network-mkt",
|
||||
task_binding_owner=None,
|
||||
)
|
||||
return config, now, after, request
|
||||
|
||||
|
||||
def test_execute_publishes_exact_release_and_is_idempotent(tmp_path: Path):
|
||||
config, now, after, request = setup_case(tmp_path)
|
||||
receipt = m.execute(config, now=now, require_root=False)
|
||||
assert receipt["state"] == "EXECUTED"
|
||||
assert receipt["service_mutation"] is False
|
||||
assert receipt["service_restart"] is False
|
||||
assert receipt["projector_started"] is False
|
||||
assert receipt["task_creation"] is False
|
||||
assert receipt["production_write"] is False
|
||||
final = config.release_root / "control-plane-test-v9"
|
||||
assert final.is_dir() and not final.is_symlink()
|
||||
assert final.stat().st_mode & 0o777 == 0o755
|
||||
assert sha((final / "a.txt").read_bytes()) == after["a.txt"]
|
||||
assert sha((final / "bin.sh").read_bytes()) == after["bin.sh"]
|
||||
assert sha((final / "c.txt").read_bytes()) == after["c.txt"]
|
||||
assert (final / "bin.sh").stat().st_mode & 0o111
|
||||
again = m.execute(config, now=now, require_root=False)
|
||||
assert again == receipt
|
||||
|
||||
|
||||
def test_execute_rejects_non_high_risk_authorization(tmp_path: Path):
|
||||
config, now, _, _ = setup_case(tmp_path, risk="engineering")
|
||||
with pytest.raises(m.HostSeedError, match="AUTHORIZATION_RISK_MISMATCH"):
|
||||
m.execute(config, now=now, require_root=False)
|
||||
assert not config.release_root.exists()
|
||||
|
||||
|
||||
def test_execute_rejects_tampered_seed_bootstrap(tmp_path: Path):
|
||||
config, now, _, _ = setup_case(tmp_path, tamper_bootstrap=True)
|
||||
with pytest.raises(m.HostSeedError, match="SEED_BOOTSTRAP_HASH_MISMATCH"):
|
||||
m.execute(config, now=now, require_root=False)
|
||||
assert not config.release_root.exists()
|
||||
|
||||
|
||||
def test_request_rejects_wrong_host(tmp_path: Path):
|
||||
config, now, _, _ = setup_case(tmp_path)
|
||||
config = m.Config(
|
||||
git_dir=config.git_dir,
|
||||
task_binding_root=config.task_binding_root,
|
||||
state_root=config.state_root,
|
||||
release_root=config.release_root,
|
||||
host_identity="other-host",
|
||||
task_binding_owner=None,
|
||||
)
|
||||
with pytest.raises(m.HostSeedError, match="TARGET_HOST_MISMATCH"):
|
||||
m.execute(config, now=now, require_root=False)
|
||||
|
||||
|
||||
def test_existing_release_drift_is_fail_closed(tmp_path: Path):
|
||||
config, now, _, _ = setup_case(tmp_path)
|
||||
first = m.execute(config, now=now, require_root=False)
|
||||
assert first["state"] == "EXECUTED"
|
||||
(config.release_root / "control-plane-test-v9" / "a.txt").write_text("drift\n")
|
||||
# remove durable receipt so existing-release verification path is exercised.
|
||||
for p in (config.state_root / "receipts").glob("*.json"):
|
||||
p.unlink()
|
||||
with pytest.raises(m.HostSeedError, match="EXISTING_RELEASE_HASH_MISMATCH"):
|
||||
m.execute(config, now=now, require_root=False)
|
||||
|
||||
|
||||
def test_no_request_ref_is_safe_noop(tmp_path: Path):
|
||||
repo = tmp_path / "src"
|
||||
bare = tmp_path / "repo.git"
|
||||
repo.mkdir()
|
||||
run("git", "init", "-b", "main", cwd=repo)
|
||||
run("git", "config", "user.email", "test@example.invalid", cwd=repo)
|
||||
run("git", "config", "user.name", "test", cwd=repo)
|
||||
(repo / "a").write_text("x\n")
|
||||
run("git", "add", "a", cwd=repo)
|
||||
run("git", "commit", "-m", "base", cwd=repo)
|
||||
run("git", "clone", "--bare", str(repo), str(bare), cwd=tmp_path)
|
||||
cfg = m.Config(
|
||||
git_dir=bare,
|
||||
task_binding_root=tmp_path / "bindings",
|
||||
state_root=tmp_path / "state",
|
||||
release_root=tmp_path / "releases",
|
||||
host_identity="cn-network-mkt",
|
||||
task_binding_owner=None,
|
||||
)
|
||||
result = m.execute(cfg, now=datetime(2026, 8, 22, tzinfo=timezone.utc), require_root=False)
|
||||
assert result["state"] == "NO_REQUEST"
|
||||
assert result["service_mutation"] is False
|
||||
assert result["task_creation"] is False
|
||||
assert not cfg.release_root.exists()
|
||||
|
||||
|
||||
def test_authorization_expiry_cannot_be_extended_without_new_binding(tmp_path: Path):
|
||||
config, now, _, _ = setup_case(tmp_path)
|
||||
# Rewrite only the request expiry and recompute the request commit, but keep
|
||||
# the original governed-task binding description. The authorization digest
|
||||
# therefore no longer matches the binding and must fail closed.
|
||||
repo = tmp_path / "src"
|
||||
req_path = repo / "control-plane" / "host-seed" / "request.json"
|
||||
value = json.loads(req_path.read_text())
|
||||
value["authorization"]["expires_at"] = (now + timedelta(minutes=45)).isoformat().replace("+00:00", "Z")
|
||||
value["authorization"]["authorization_sha256"] = sha(canonical({
|
||||
"task_id": value["authorization"]["task_id"],
|
||||
"intent_sha256": value["authorization"]["intent_sha256"],
|
||||
"expires_at": value["authorization"]["expires_at"],
|
||||
}))
|
||||
req_path.write_bytes(canonical(value))
|
||||
run("git", "add", str(req_path.relative_to(repo)), cwd=repo)
|
||||
run("git", "commit", "-m", "tamper expiry", cwd=repo)
|
||||
new_commit = run("git", "rev-parse", "HEAD", cwd=repo)
|
||||
run("git", "push", "--force", str(config.git_dir), f"{new_commit}:{m.REQUEST_REF}", cwd=repo)
|
||||
with pytest.raises(m.HostSeedError, match="AUTHORIZATION_DESCRIPTION_MISMATCH"):
|
||||
m.execute(config, now=now, require_root=False)
|
||||
|
||||
|
||||
def test_systemd_contract_has_no_network_or_implicit_activation():
|
||||
root = Path(__file__).parents[1]
|
||||
service = (root / "deploy/systemd/aios-control-plane-host-seed-executor.service").read_text()
|
||||
timer = (root / "deploy/systemd/aios-control-plane-host-seed-executor.timer").read_text()
|
||||
assert "User=root" in service
|
||||
assert "IPAddressDeny=any" in service
|
||||
assert "RestrictAddressFamilies=AF_UNIX" in service
|
||||
assert "ReadOnlyPaths=/data/aios-gitops/repo.git" in service
|
||||
assert "ReadWritePaths=/opt/aios/releases" in service
|
||||
assert "systemctl start" not in service
|
||||
assert "projector" not in service.lower()
|
||||
assert "OnUnitActiveSec=30s" in timer
|
||||
|
||||
|
||||
def test_release_root_symlink_is_rejected(tmp_path: Path):
|
||||
config, now, _, _ = setup_case(tmp_path)
|
||||
real = tmp_path / "real-releases"
|
||||
real.mkdir()
|
||||
config.release_root.symlink_to(real, target_is_directory=True)
|
||||
with pytest.raises(m.HostSeedError, match="RELEASE_ROOT_SYMLINK_REJECTED"):
|
||||
m.execute(config, now=now, require_root=False)
|
||||
|
||||
def test_install_and_rollback_scripts_have_valid_bash_syntax():
|
||||
root = Path(__file__).parents[1]
|
||||
for relative in (
|
||||
"deploy/install/install_control_plane_host_seed_executor.sh",
|
||||
"deploy/install/rollback_control_plane_host_seed_executor.sh",
|
||||
):
|
||||
subprocess.run(
|
||||
["/usr/bin/bash", "-n", str(root / relative)],
|
||||
check=True,
|
||||
text=True,
|
||||
capture_output=True,
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user