Compare commits
24
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4e6ef70029 | ||
|
|
80feef309f | ||
|
|
a5489f1b67 | ||
|
|
312d3b4830 | ||
|
|
ac2dc5c30e | ||
|
|
5ef261b265 | ||
|
|
3a22c3c7d8 | ||
|
|
1c9f28a613 | ||
|
|
9514062cf3 | ||
|
|
a701a32cb4 | ||
|
|
45f3226662 | ||
|
|
975d239d40 | ||
|
|
3bc44accb5 | ||
|
|
3304d87872 | ||
|
|
875d41c310 | ||
|
|
870ac61dc2 | ||
|
|
009f276699 | ||
|
|
600c717e17 | ||
|
|
02be6d28ed | ||
|
|
b1e240dda3 | ||
|
|
2dfbe837f4 | ||
|
|
80df1e0d87 | ||
|
|
7ecf7cbfce | ||
|
|
bbada7c863 |
@@ -5,13 +5,148 @@ import json
|
||||
import os
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
import urllib.parse
|
||||
|
||||
import aios_chatgpt_actions_api as api
|
||||
import aios_isolated_writer_policy as policy
|
||||
from isolated_writer_authz import AuthorizationError, authorize
|
||||
from isolated_writer_commit_file import AtomicCommitFile, CommitFileError, OperationLedger
|
||||
from isolated_writer_token_registry import TokenRegistry
|
||||
|
||||
_reject = api.reject_non_read_http
|
||||
_write_file = api.write_file
|
||||
_sync_branch = api.sync_branch
|
||||
_original_do_get = api.H.do_GET
|
||||
api.OPENAPI = Path(os.getenv("AIOS_GITOPS_ISOLATED_WRITER_OPENAPI", str(api.OPENAPI)))
|
||||
|
||||
|
||||
def _registry() -> TokenRegistry:
|
||||
return TokenRegistry(Path(os.environ["AIOS_GITOPS_TOKEN_REGISTRY_V2"]))
|
||||
|
||||
|
||||
def _atomic_writer() -> AtomicCommitFile:
|
||||
return AtomicCommitFile(
|
||||
workspace=api.WS,
|
||||
bare_repo=api.BARE_REPO,
|
||||
ledger=OperationLedger(Path(os.environ["AIOS_GITOPS_WRITER_OPERATION_LEDGER"])),
|
||||
remote=os.getenv("AIOS_GITOPS_GITEA_PUSH_REMOTE", ""),
|
||||
audit=policy.append_audit_record,
|
||||
)
|
||||
|
||||
|
||||
def _bearer(handler) -> str:
|
||||
value = handler.headers.get("Authorization", "")
|
||||
return value[7:].strip() if value.startswith("Bearer ") else ""
|
||||
|
||||
|
||||
def _authorize(handler, *, method: str, path: str, branch=None, target_path=None):
|
||||
grant = authorize(
|
||||
_registry(),
|
||||
_bearer(handler),
|
||||
method=method,
|
||||
path=path,
|
||||
source_ip=str(handler.client_address[0]),
|
||||
workspace=api.WS,
|
||||
branch=branch,
|
||||
target_path=target_path,
|
||||
)
|
||||
grant.record["_source_ip"] = str(handler.client_address[0])
|
||||
return grant
|
||||
|
||||
|
||||
def _authorization_error(handler, exc: AuthorizationError):
|
||||
return handler.sendj({"error": exc.code}, exc.status)
|
||||
|
||||
|
||||
def _isolated_runtime_status(handler):
|
||||
branch = api.sh(["rev-parse", "--abbrev-ref", "HEAD"], True)["stdout"]
|
||||
head = api.sh(["rev-parse", "HEAD"], True)["stdout"]
|
||||
return handler.sendj({
|
||||
"service": os.getenv(
|
||||
"AIOS_GITOPS_SERVICE_AUDIENCE",
|
||||
"aios-isolated-writer.service",
|
||||
),
|
||||
"status": "active",
|
||||
"runtime_role": os.getenv("AIOS_GITOPS_RUNTIME_ROLE"),
|
||||
"workspace_role": os.getenv("AIOS_GITOPS_WORKSPACE_ROLE"),
|
||||
"writes_enabled": os.getenv("AIOS_GITOPS_WRITES_ENABLED", "").lower()
|
||||
in {"1", "true", "yes", "on"},
|
||||
"allowed_branch": policy.allowed_branch(),
|
||||
"approved_base_oid": policy.allowed_base_oid(),
|
||||
"workspace_branch": branch,
|
||||
"workspace_head_oid": head,
|
||||
"repository_identity": os.getenv("AIOS_GITOPS_REPOSITORY_IDENTITY"),
|
||||
"token_policy_version": 2,
|
||||
"atomic_commit_file": True,
|
||||
"path_policy_sha256": policy.configured_path_policy_sha256(),
|
||||
})
|
||||
|
||||
|
||||
def _isolated_do_get(self):
|
||||
path = urllib.parse.urlparse(self.path).path.rstrip("/") or "/"
|
||||
if path in {"/health", "/api/health"}:
|
||||
return self.sendj({
|
||||
"status": "ok",
|
||||
"service": os.getenv(
|
||||
"AIOS_GITOPS_SERVICE_AUDIENCE",
|
||||
"aios-isolated-writer.service",
|
||||
),
|
||||
})
|
||||
if path not in {"/api/runtime-status", "/api/baseline", "/api/patches"}:
|
||||
return self.sendj({"error": "isolated_writer_operation_forbidden"}, 403)
|
||||
try:
|
||||
_authorize(self, method="GET", path=path)
|
||||
except AuthorizationError as exc:
|
||||
return _authorization_error(self, exc)
|
||||
if path == "/api/runtime-status":
|
||||
return _isolated_runtime_status(self)
|
||||
return _original_do_get(self)
|
||||
|
||||
|
||||
def _read_commit_file_body(handler):
|
||||
try:
|
||||
length = int(handler.headers.get("Content-Length", "0"))
|
||||
except ValueError:
|
||||
raise CommitFileError("writer_content_length_rejected") from None
|
||||
if length <= 0:
|
||||
raise CommitFileError("writer_request_object_required")
|
||||
if length > 1_100_000:
|
||||
raise CommitFileError("writer_request_too_large", 413)
|
||||
raw = handler.rfile.read(length)
|
||||
try:
|
||||
value = json.loads(raw.decode("utf-8"))
|
||||
except (UnicodeDecodeError, json.JSONDecodeError):
|
||||
raise CommitFileError("writer_request_json_rejected") from None
|
||||
if not isinstance(value, dict):
|
||||
raise CommitFileError("writer_request_object_required")
|
||||
return value
|
||||
|
||||
|
||||
def _isolated_do_post(self):
|
||||
path = urllib.parse.urlparse(self.path).path.rstrip("/") or "/"
|
||||
if path != "/api/commit-file":
|
||||
return self.sendj({"error": "isolated_writer_operation_forbidden"}, 403)
|
||||
try:
|
||||
data = _read_commit_file_body(self)
|
||||
branch = str(data.get("branch") or "")
|
||||
target_path = str(data.get("path") or "")
|
||||
grant = _authorize(
|
||||
self,
|
||||
method="POST",
|
||||
path=path,
|
||||
branch=branch,
|
||||
target_path=target_path,
|
||||
)
|
||||
if api.content_has_secret(str(data.get("content") or "")):
|
||||
raise CommitFileError("writer_secret_content_rejected", 403)
|
||||
result = _atomic_writer().execute(grant, data)
|
||||
return self.sendj(result)
|
||||
except AuthorizationError as exc:
|
||||
return _authorization_error(self, exc)
|
||||
except CommitFileError as exc:
|
||||
return self.sendj({"error": exc.code}, exc.status)
|
||||
except PermissionError:
|
||||
return self.sendj({"error": "isolated_writer_policy_rejected"}, 403)
|
||||
|
||||
|
||||
def reject_non_read_http(handler, method: str, path: str) -> bool:
|
||||
@@ -173,6 +308,8 @@ api.H.branch, api.H.file, api.H.commit, api.H.patch = (
|
||||
_checked_commit,
|
||||
_checked_patch,
|
||||
)
|
||||
api.H.do_GET = _isolated_do_get
|
||||
api.H.do_POST = _isolated_do_post
|
||||
|
||||
if __name__ == "__main__":
|
||||
policy.require_service_invariants(api.WS, api.BARE_REPO)
|
||||
|
||||
@@ -9,10 +9,9 @@ import re
|
||||
from pathlib import Path, PurePosixPath, PureWindowsPath
|
||||
from urllib.parse import unquote
|
||||
|
||||
WRITE_ENDPOINTS = frozenset({
|
||||
"/api/branch", "/api/create-branch", "/api/file", "/api/commit-file",
|
||||
"/api/commit", "/api/patch", "/api/create-patch",
|
||||
})
|
||||
import isolated_writer_path_policy as path_policy
|
||||
|
||||
WRITE_ENDPOINTS = frozenset({"/api/commit-file"})
|
||||
ALLOWED_ROOTS = (
|
||||
"active-runtime",
|
||||
"docs",
|
||||
@@ -69,6 +68,15 @@ def require_service_invariants(workspace: Path, bare_repo: Path) -> None:
|
||||
raise PermissionError("isolated_writer_role_required")
|
||||
if os.getenv("AIOS_GITOPS_WORKSPACE_ROLE", "") != "isolated-writer":
|
||||
raise PermissionError("isolated_writer_workspace_role_required")
|
||||
if os.getenv("AIOS_GITOPS_TOKEN_POLICY_VERSION_REQUIRED", "") != "2":
|
||||
raise PermissionError("isolated_writer_token_policy_v2_required")
|
||||
if not os.getenv("AIOS_GITOPS_SERVICE_AUDIENCE", "").strip():
|
||||
raise PermissionError("isolated_writer_service_audience_required")
|
||||
if not os.getenv("AIOS_GITOPS_TOKEN_REGISTRY_V2", "").strip():
|
||||
raise PermissionError("isolated_writer_token_registry_required")
|
||||
if not os.getenv("AIOS_GITOPS_WRITER_OPERATION_LEDGER", "").strip():
|
||||
raise PermissionError("isolated_writer_operation_ledger_required")
|
||||
path_policy.configured_path_policy()
|
||||
for name in _DENY_FLAGS:
|
||||
if os.getenv(name, "").strip().lower() not in _FALSE:
|
||||
raise PermissionError("isolated_writer_forbidden_capability_flag")
|
||||
@@ -182,6 +190,7 @@ def _audit_sanitize(value):
|
||||
"private_key",
|
||||
)
|
||||
)
|
||||
and str(key).lower() != "token_id"
|
||||
else _audit_sanitize(inner)
|
||||
)
|
||||
for key, inner in value.items()
|
||||
@@ -239,9 +248,14 @@ def validate_path(path: str, workspace: Path | None = None) -> str:
|
||||
):
|
||||
raise PermissionError("isolated_writer_path_rejected")
|
||||
first = parts[0]
|
||||
service_exact_path = False
|
||||
if first in DENIED_ROOTS:
|
||||
raise PermissionError("isolated_writer_protected_path_rejected")
|
||||
if value != "README.md" and first not in ALLOWED_ROOTS:
|
||||
try:
|
||||
path_policy.require_service_path("commit_file.atomic", value)
|
||||
service_exact_path = True
|
||||
except path_policy.PathPolicyError as exc:
|
||||
raise PermissionError(str(exc)) from None
|
||||
if value != "README.md" and first not in ALLOWED_ROOTS and not service_exact_path:
|
||||
raise PermissionError("isolated_writer_path_outside_allowed_roots")
|
||||
if workspace is not None:
|
||||
lexical_root = Path(os.path.abspath(str(workspace)))
|
||||
@@ -261,6 +275,22 @@ def validate_path(path: str, workspace: Path | None = None) -> str:
|
||||
return value
|
||||
|
||||
|
||||
def validate_content_for_path(path: str, content: str) -> None:
|
||||
if path.split("/", 1)[0] not in DENIED_ROOTS:
|
||||
return
|
||||
try:
|
||||
path_policy.validate_content("commit_file.atomic", path, content)
|
||||
except path_policy.PathPolicyError as exc:
|
||||
raise PermissionError(str(exc)) from None
|
||||
|
||||
|
||||
def configured_path_policy_sha256() -> str | None:
|
||||
try:
|
||||
return path_policy.configured_path_policy_sha256()
|
||||
except path_policy.PathPolicyError as exc:
|
||||
raise PermissionError(str(exc)) from None
|
||||
|
||||
|
||||
def validate_commit_paths(paths: list[str], workspace: Path | None = None) -> None:
|
||||
if enforcement_required():
|
||||
for path in paths:
|
||||
|
||||
@@ -0,0 +1,167 @@
|
||||
"""Fail-closed operation authorization for an isolated Git writer."""
|
||||
from __future__ import annotations
|
||||
|
||||
import ipaddress
|
||||
import os
|
||||
import re
|
||||
import subprocess
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime, timezone
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from isolated_writer_token_registry import TokenRegistry
|
||||
|
||||
|
||||
ROUTE_OPERATIONS = {
|
||||
("GET", "/api/runtime-status"): "runtime.status.read",
|
||||
("GET", "/api/baseline"): "baseline.read",
|
||||
("GET", "/api/patches"): "patches.read",
|
||||
("POST", "/api/commit-file"): "commit_file.atomic",
|
||||
}
|
||||
OID_RE = re.compile(r"[0-9a-f]{40}")
|
||||
|
||||
|
||||
class AuthorizationError(PermissionError):
|
||||
def __init__(self, code: str, status: int = 403):
|
||||
super().__init__(code)
|
||||
self.code = code
|
||||
self.status = status
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class AuthorizedGrant:
|
||||
record: dict[str, Any]
|
||||
operation: str
|
||||
use_count: int
|
||||
|
||||
@property
|
||||
def token_id(self) -> str:
|
||||
return str(self.record["token_id"])
|
||||
|
||||
@property
|
||||
def task_id(self) -> str:
|
||||
return str(self.record["task_id"])
|
||||
|
||||
|
||||
def operation_for(method: str, path: str) -> str:
|
||||
operation = ROUTE_OPERATIONS.get((method.upper(), path))
|
||||
if not operation:
|
||||
raise AuthorizationError("isolated_writer_operation_forbidden")
|
||||
return operation
|
||||
|
||||
|
||||
def _parse_time(value: Any) -> datetime:
|
||||
try:
|
||||
parsed = datetime.fromisoformat(str(value).replace("Z", "+00:00"))
|
||||
except (TypeError, ValueError):
|
||||
raise AuthorizationError("writer_token_time_invalid") from None
|
||||
if parsed.tzinfo is None:
|
||||
raise AuthorizationError("writer_token_time_invalid")
|
||||
return parsed.astimezone(timezone.utc)
|
||||
|
||||
|
||||
def _git_is_ancestor(workspace: Path, base: str, branch: str) -> bool:
|
||||
result = subprocess.run(
|
||||
["git", "merge-base", "--is-ancestor", base, branch],
|
||||
cwd=workspace,
|
||||
stdout=subprocess.DEVNULL,
|
||||
stderr=subprocess.DEVNULL,
|
||||
)
|
||||
return result.returncode == 0
|
||||
|
||||
|
||||
def _validate_grant(
|
||||
grant: dict[str, Any],
|
||||
*,
|
||||
audience: str,
|
||||
source_ip: str,
|
||||
operation: str,
|
||||
branch: str | None,
|
||||
path: str | None,
|
||||
workspace: Path,
|
||||
current_time: datetime,
|
||||
) -> None:
|
||||
if int(grant.get("schema_version", 0)) != 2:
|
||||
raise AuthorizationError("writer_token_policy_v2_required", 401)
|
||||
if str(grant.get("audience") or "") != audience:
|
||||
raise AuthorizationError("writer_token_audience_rejected")
|
||||
try:
|
||||
peer = ipaddress.ip_address(source_ip)
|
||||
networks = [ipaddress.ip_network(str(value), strict=True) for value in grant.get("source_cidrs", [])]
|
||||
except ValueError:
|
||||
raise AuthorizationError("writer_token_source_rejected") from None
|
||||
if not networks or not any(peer in network for network in networks):
|
||||
raise AuthorizationError("writer_token_source_rejected")
|
||||
created = _parse_time(grant.get("created_at"))
|
||||
not_before = _parse_time(grant.get("not_before"))
|
||||
expires = _parse_time(grant.get("expires_at"))
|
||||
if expires <= not_before or (expires - created).total_seconds() > 1800:
|
||||
raise AuthorizationError("writer_token_lifetime_rejected")
|
||||
if current_time < not_before:
|
||||
raise AuthorizationError("writer_token_not_yet_valid", 401)
|
||||
if current_time >= expires:
|
||||
raise AuthorizationError("writer_token_expired", 401)
|
||||
if bool(grant.get("revoked")):
|
||||
raise AuthorizationError("writer_token_revoked", 401)
|
||||
operations = grant.get("allowed_operations")
|
||||
if not isinstance(operations, list) or operation not in operations:
|
||||
raise AuthorizationError("writer_token_operation_rejected")
|
||||
expected_branch = str(grant.get("allowed_branch") or "")
|
||||
configured_branch = os.getenv("AIOS_GITOPS_ALLOWED_BRANCH", "")
|
||||
if expected_branch != configured_branch:
|
||||
raise AuthorizationError("writer_token_branch_rejected")
|
||||
if branch is not None and branch != expected_branch:
|
||||
raise AuthorizationError("writer_token_branch_rejected")
|
||||
if path is not None:
|
||||
paths = grant.get("allowed_paths")
|
||||
if not isinstance(paths, list) or path not in paths:
|
||||
raise AuthorizationError("writer_token_path_rejected")
|
||||
base = str(grant.get("approved_base_oid") or "").lower()
|
||||
if not OID_RE.fullmatch(base):
|
||||
raise AuthorizationError("writer_token_base_rejected")
|
||||
if not _git_is_ancestor(workspace, base, expected_branch):
|
||||
raise AuthorizationError("writer_token_base_not_ancestor")
|
||||
|
||||
|
||||
def authorize(
|
||||
registry: TokenRegistry,
|
||||
token: str,
|
||||
*,
|
||||
method: str,
|
||||
path: str,
|
||||
source_ip: str,
|
||||
workspace: Path,
|
||||
branch: str | None = None,
|
||||
target_path: str | None = None,
|
||||
current_time: datetime | None = None,
|
||||
) -> AuthorizedGrant:
|
||||
if os.getenv("AIOS_GITOPS_TOKEN_POLICY_VERSION_REQUIRED", "") != "2":
|
||||
raise AuthorizationError("writer_token_policy_v2_not_required")
|
||||
if not token:
|
||||
raise AuthorizationError("unauthorized", 401)
|
||||
grant = registry.find(token)
|
||||
if grant is None:
|
||||
raise AuthorizationError("unauthorized", 401)
|
||||
operation = operation_for(method, path)
|
||||
_validate_grant(
|
||||
grant,
|
||||
audience=os.getenv("AIOS_GITOPS_SERVICE_AUDIENCE", ""),
|
||||
source_ip=source_ip,
|
||||
operation=operation,
|
||||
branch=branch,
|
||||
path=target_path,
|
||||
workspace=workspace,
|
||||
current_time=current_time or datetime.now(timezone.utc),
|
||||
)
|
||||
limits = grant.get("max_uses")
|
||||
if not isinstance(limits, dict):
|
||||
raise AuthorizationError("writer_token_use_limit_missing")
|
||||
try:
|
||||
used = registry.consume(str(grant["token_id"]), operation, int(limits[operation]))
|
||||
except (KeyError, TypeError, ValueError):
|
||||
raise AuthorizationError("writer_token_use_limit_missing") from None
|
||||
except PermissionError as exc:
|
||||
code = str(exc)
|
||||
raise AuthorizationError(code, 401 if code == "writer_token_revoked" else 403) from None
|
||||
return AuthorizedGrant(grant, operation, used)
|
||||
@@ -0,0 +1,403 @@
|
||||
"""Atomic, idempotent commit-file Saga for the isolated writer."""
|
||||
from __future__ import annotations
|
||||
|
||||
import fcntl
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import sqlite3
|
||||
import subprocess
|
||||
import tempfile
|
||||
import threading
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
import aios_isolated_writer_policy as policy
|
||||
|
||||
|
||||
OID_RE = re.compile(r"[0-9a-f]{40}")
|
||||
IDEMPOTENCY_RE = re.compile(r"[A-Za-z0-9][A-Za-z0-9._:-]{7,127}")
|
||||
STATES = frozenset({
|
||||
"PENDING", "FILE_WRITTEN", "COMMITTED", "PUSHED", "VERIFIED", "COMPLETED",
|
||||
"FAILED_RECOVERABLE", "MANUAL_REVIEW_REQUIRED",
|
||||
})
|
||||
_LOCKS: dict[str, threading.Lock] = {}
|
||||
_LOCKS_GUARD = threading.Lock()
|
||||
_LEDGER_INIT_LOCK = threading.Lock()
|
||||
|
||||
|
||||
class CommitFileError(RuntimeError):
|
||||
def __init__(self, code: str, status: int = 400):
|
||||
super().__init__(code)
|
||||
self.code = code
|
||||
self.status = status
|
||||
|
||||
|
||||
class SimulatedCrash(RuntimeError):
|
||||
pass
|
||||
|
||||
|
||||
def canonical_digest(data: dict[str, Any]) -> str:
|
||||
payload = {
|
||||
"branch": data["branch"],
|
||||
"path": data["path"],
|
||||
"content_sha256": hashlib.sha256(data["content"].encode("utf-8")).hexdigest(),
|
||||
"message": data["message"],
|
||||
"expected_head_oid": data["expected_head_oid"],
|
||||
}
|
||||
encoded = json.dumps(payload, sort_keys=True, separators=(",", ":")).encode("utf-8")
|
||||
return hashlib.sha256(encoded).hexdigest()
|
||||
|
||||
|
||||
class OperationLedger:
|
||||
def __init__(self, path: Path | str):
|
||||
self.path = Path(path)
|
||||
self.path.parent.mkdir(parents=True, exist_ok=True)
|
||||
if self.path.parent.is_symlink() or self.path.is_symlink():
|
||||
raise PermissionError("writer_operation_ledger_symlink_rejected")
|
||||
with _LEDGER_INIT_LOCK:
|
||||
with self.connect() as db:
|
||||
db.execute(
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS writer_operations (
|
||||
operation_id TEXT PRIMARY KEY,
|
||||
token_id TEXT NOT NULL,
|
||||
task_id TEXT NOT NULL,
|
||||
idempotency_key TEXT NOT NULL UNIQUE,
|
||||
operation_digest TEXT NOT NULL,
|
||||
branch TEXT NOT NULL,
|
||||
path TEXT NOT NULL,
|
||||
content_sha256 TEXT NOT NULL,
|
||||
pre_head_oid TEXT NOT NULL,
|
||||
commit_oid TEXT,
|
||||
push_status TEXT,
|
||||
verification_status TEXT,
|
||||
state TEXT NOT NULL,
|
||||
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
error_code TEXT
|
||||
)
|
||||
"""
|
||||
)
|
||||
os.chmod(self.path, 0o600)
|
||||
|
||||
def connect(self) -> sqlite3.Connection:
|
||||
db = sqlite3.connect(self.path, timeout=30)
|
||||
db.row_factory = sqlite3.Row
|
||||
db.execute("PRAGMA journal_mode=WAL")
|
||||
db.execute("PRAGMA synchronous=FULL")
|
||||
return db
|
||||
|
||||
def get_or_create(self, grant: Any, data: dict[str, Any], digest: str) -> dict[str, Any]:
|
||||
key = data["idempotency_key"]
|
||||
with self.connect() as db:
|
||||
db.execute("BEGIN IMMEDIATE")
|
||||
row = db.execute(
|
||||
"SELECT * FROM writer_operations WHERE idempotency_key=?", (key,)
|
||||
).fetchone()
|
||||
if row:
|
||||
db.commit()
|
||||
record = dict(row)
|
||||
if record["operation_digest"] != digest:
|
||||
raise CommitFileError("writer_idempotency_conflict", 409)
|
||||
if (
|
||||
record["token_id"] != grant.token_id
|
||||
or record["task_id"] != grant.task_id
|
||||
):
|
||||
raise CommitFileError("writer_idempotency_owner_conflict", 409)
|
||||
return record
|
||||
operation_id = hashlib.sha256(
|
||||
f"{grant.task_id}\0{key}".encode("utf-8")
|
||||
).hexdigest()
|
||||
content_sha = hashlib.sha256(data["content"].encode("utf-8")).hexdigest()
|
||||
db.execute(
|
||||
"""
|
||||
INSERT INTO writer_operations(
|
||||
operation_id,token_id,task_id,idempotency_key,operation_digest,
|
||||
branch,path,content_sha256,pre_head_oid,state
|
||||
) VALUES(?,?,?,?,?,?,?,?,?,?)
|
||||
""",
|
||||
(
|
||||
operation_id, grant.token_id, grant.task_id, key, digest,
|
||||
data["branch"], data["path"], content_sha,
|
||||
data["expected_head_oid"], "PENDING",
|
||||
),
|
||||
)
|
||||
db.commit()
|
||||
return self.get(key)
|
||||
|
||||
def get(self, key: str) -> dict[str, Any]:
|
||||
with self.connect() as db:
|
||||
row = db.execute(
|
||||
"SELECT * FROM writer_operations WHERE idempotency_key=?", (key,)
|
||||
).fetchone()
|
||||
if row is None:
|
||||
raise KeyError(key)
|
||||
return dict(row)
|
||||
|
||||
def update(self, key: str, state: str, **fields: Any) -> dict[str, Any]:
|
||||
if state not in STATES:
|
||||
raise ValueError("invalid writer operation state")
|
||||
allowed = {"commit_oid", "push_status", "verification_status", "error_code"}
|
||||
assignments = ["state=?", "updated_at=CURRENT_TIMESTAMP"]
|
||||
values: list[Any] = [state]
|
||||
for name, value in fields.items():
|
||||
if name not in allowed:
|
||||
raise ValueError("invalid ledger field")
|
||||
assignments.append(f"{name}=?")
|
||||
values.append(value)
|
||||
values.append(key)
|
||||
with self.connect() as db:
|
||||
db.execute("BEGIN IMMEDIATE")
|
||||
db.execute(
|
||||
f"UPDATE writer_operations SET {','.join(assignments)} WHERE idempotency_key=?",
|
||||
values,
|
||||
)
|
||||
db.commit()
|
||||
return self.get(key)
|
||||
|
||||
|
||||
class AtomicCommitFile:
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
workspace: Path,
|
||||
bare_repo: Path,
|
||||
ledger: OperationLedger,
|
||||
remote: str,
|
||||
audit,
|
||||
crash_after_state: str | None = None,
|
||||
):
|
||||
self.workspace = workspace
|
||||
self.bare_repo = bare_repo
|
||||
self.ledger = ledger
|
||||
self.remote = remote
|
||||
self.audit = audit
|
||||
self.crash_after_state = crash_after_state
|
||||
|
||||
def _git(self, *args: str, check: bool = True) -> str:
|
||||
result = subprocess.run(
|
||||
["git", *args], cwd=self.workspace, text=True, capture_output=True
|
||||
)
|
||||
if check and result.returncode != 0:
|
||||
raise CommitFileError("writer_git_operation_failed", 500)
|
||||
return result.stdout.strip()
|
||||
|
||||
def _transition(self, key: str, state: str, **fields: Any) -> dict[str, Any]:
|
||||
record = self.ledger.update(key, state, **fields)
|
||||
if self.crash_after_state == state:
|
||||
raise SimulatedCrash(state)
|
||||
return record
|
||||
|
||||
def _validate(self, data: Any) -> dict[str, Any]:
|
||||
if not isinstance(data, dict):
|
||||
raise CommitFileError("writer_request_object_required")
|
||||
required = ("branch", "path", "content", "message", "expected_head_oid", "idempotency_key")
|
||||
if any(field not in data for field in required):
|
||||
raise CommitFileError("writer_request_incomplete")
|
||||
if data.get("force") or data.get("force_push"):
|
||||
raise CommitFileError("isolated_writer_force_rejected", 403)
|
||||
if set(data) != set(required):
|
||||
raise CommitFileError("writer_request_fields_rejected")
|
||||
item = {field: data[field] for field in required}
|
||||
if item["branch"] != policy.allowed_branch():
|
||||
raise CommitFileError("isolated_writer_branch_rejected", 403)
|
||||
try:
|
||||
item["path"] = policy.validate_path(item["path"], self.workspace)
|
||||
except PermissionError as exc:
|
||||
raise CommitFileError(str(exc), 403) from None
|
||||
if not isinstance(item["content"], str) or "\x00" in item["content"]:
|
||||
raise CommitFileError("writer_content_rejected")
|
||||
try:
|
||||
policy.validate_content_for_path(item["path"], item["content"])
|
||||
except PermissionError as exc:
|
||||
raise CommitFileError(str(exc), 403) from None
|
||||
if len(item["content"].encode("utf-8")) > 1_048_576:
|
||||
raise CommitFileError("writer_content_too_large", 413)
|
||||
message = item["message"]
|
||||
if (
|
||||
not isinstance(message, str)
|
||||
or not 1 <= len(message) <= 200
|
||||
or "\x00" in message
|
||||
or "\n" in message
|
||||
or "\r" in message
|
||||
):
|
||||
raise CommitFileError("writer_message_rejected")
|
||||
if not isinstance(item["expected_head_oid"], str) or not OID_RE.fullmatch(item["expected_head_oid"]):
|
||||
raise CommitFileError("writer_expected_head_rejected")
|
||||
if not isinstance(item["idempotency_key"], str) or not IDEMPOTENCY_RE.fullmatch(item["idempotency_key"]):
|
||||
raise CommitFileError("writer_idempotency_key_rejected")
|
||||
return item
|
||||
|
||||
def _atomic_write(self, relative: str, content: str) -> None:
|
||||
target = self.workspace / relative
|
||||
target.parent.mkdir(parents=True, exist_ok=True)
|
||||
fd, temporary = tempfile.mkstemp(prefix=".writer-", dir=target.parent)
|
||||
try:
|
||||
with os.fdopen(fd, "wb", closefd=True) as stream:
|
||||
stream.write(content.encode("utf-8"))
|
||||
stream.flush()
|
||||
os.fsync(stream.fileno())
|
||||
os.replace(temporary, target)
|
||||
parent_fd = os.open(target.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)
|
||||
|
||||
def _verify_remote(self, branch: str, oid: str) -> None:
|
||||
bare = subprocess.run(
|
||||
["git", "--git-dir", str(self.bare_repo), "rev-parse", f"refs/heads/{branch}"],
|
||||
text=True, capture_output=True,
|
||||
)
|
||||
if bare.returncode != 0 or bare.stdout.strip() != oid:
|
||||
raise CommitFileError("writer_authoritative_bare_oid_mismatch", 500)
|
||||
remote = subprocess.run(
|
||||
["git", "ls-remote", "--heads", self.remote, f"refs/heads/{branch}"],
|
||||
cwd=self.workspace, text=True, capture_output=True,
|
||||
)
|
||||
if remote.returncode != 0:
|
||||
raise CommitFileError("writer_gitea_verification_failed", 503)
|
||||
fields = remote.stdout.strip().split()
|
||||
if len(fields) != 2:
|
||||
raise CommitFileError("writer_gitea_ref_missing", 503)
|
||||
if fields[0] != oid:
|
||||
raise CommitFileError("writer_gitea_oid_conflict", 409)
|
||||
|
||||
def execute(self, grant: Any, raw_data: Any) -> dict[str, Any]:
|
||||
data = self._validate(raw_data)
|
||||
digest = canonical_digest(data)
|
||||
branch = data["branch"]
|
||||
with _LOCKS_GUARD:
|
||||
lock = _LOCKS.setdefault(branch, threading.Lock())
|
||||
lock_path = self.ledger.path.parent / f"{hashlib.sha256(branch.encode()).hexdigest()}.lock"
|
||||
with lock, open(lock_path, "a+b") as branch_lock:
|
||||
fcntl.flock(branch_lock.fileno(), fcntl.LOCK_EX)
|
||||
return self._execute_locked(grant, data, digest)
|
||||
|
||||
def _execute_locked(self, grant: Any, data: dict[str, Any], digest: str) -> dict[str, Any]:
|
||||
key = data["idempotency_key"]
|
||||
record = self.ledger.get_or_create(grant, data, digest)
|
||||
if record["state"] == "COMPLETED":
|
||||
return self._result(record, idempotent=True)
|
||||
if self._git("rev-parse", "--abbrev-ref", "HEAD") != data["branch"]:
|
||||
record = self._transition(
|
||||
key, "MANUAL_REVIEW_REQUIRED", error_code="writer_current_branch_conflict"
|
||||
)
|
||||
self._audit(grant, data, digest, record, "manual_review_required")
|
||||
raise CommitFileError("writer_current_branch_conflict", 409)
|
||||
head = self._git("rev-parse", "HEAD")
|
||||
commit_oid = record.get("commit_oid")
|
||||
if head != record["pre_head_oid"] and head != commit_oid:
|
||||
record = self._transition(
|
||||
key, "MANUAL_REVIEW_REQUIRED", error_code="writer_branch_head_conflict"
|
||||
)
|
||||
self._audit(grant, data, digest, record, "manual_review_required")
|
||||
raise CommitFileError("writer_branch_head_conflict", 409)
|
||||
try:
|
||||
state = record["state"]
|
||||
if state == "MANUAL_REVIEW_REQUIRED":
|
||||
raise CommitFileError("writer_manual_review_required", 409)
|
||||
if state == "FAILED_RECOVERABLE":
|
||||
if record.get("push_status") == "confirmed":
|
||||
state = "PUSHED"
|
||||
else:
|
||||
state = "COMMITTED" if commit_oid else "FILE_WRITTEN"
|
||||
if state == "PENDING":
|
||||
self._atomic_write(data["path"], data["content"])
|
||||
record = self._transition(key, "FILE_WRITTEN")
|
||||
state = record["state"]
|
||||
if state == "FILE_WRITTEN":
|
||||
actual_sha = hashlib.sha256(
|
||||
(self.workspace / data["path"]).read_bytes()
|
||||
).hexdigest()
|
||||
if actual_sha != record["content_sha256"]:
|
||||
raise CommitFileError("writer_file_content_conflict", 409)
|
||||
staged_before = self._git("diff", "--cached", "--name-only", check=False).splitlines()
|
||||
if staged_before:
|
||||
raise CommitFileError("writer_extra_staged_files_rejected", 409)
|
||||
self._git("add", "--", data["path"])
|
||||
staged = self._git("diff", "--cached", "--name-only").splitlines()
|
||||
if staged != [data["path"]]:
|
||||
raise CommitFileError("writer_extra_staged_files_rejected", 409)
|
||||
self._git("commit", "-m", data["message"])
|
||||
commit_oid = self._git("rev-parse", "HEAD")
|
||||
record = self._transition(key, "COMMITTED", commit_oid=commit_oid)
|
||||
state = record["state"]
|
||||
if state == "COMMITTED":
|
||||
commit_oid = str(record["commit_oid"])
|
||||
pushed = subprocess.run(
|
||||
["git", "push", "--porcelain", self.remote, f"HEAD:refs/heads/{data['branch']}"],
|
||||
cwd=self.workspace, text=True, capture_output=True,
|
||||
)
|
||||
if pushed.returncode != 0:
|
||||
self._transition(
|
||||
key, "FAILED_RECOVERABLE",
|
||||
push_status="failed", error_code="writer_gitea_push_failed",
|
||||
)
|
||||
raise CommitFileError("writer_gitea_push_failed", 503)
|
||||
record = self._transition(key, "PUSHED", push_status="confirmed")
|
||||
state = record["state"]
|
||||
if state == "PUSHED":
|
||||
commit_oid = str(record["commit_oid"])
|
||||
self._verify_remote(data["branch"], commit_oid)
|
||||
record = self._transition(
|
||||
key, "VERIFIED", verification_status="bare+gitea"
|
||||
)
|
||||
state = record["state"]
|
||||
if state == "VERIFIED":
|
||||
record = self._transition(key, "COMPLETED", error_code=None)
|
||||
self._audit(grant, data, digest, record, "completed")
|
||||
return self._result(record, idempotent=False)
|
||||
except SimulatedCrash:
|
||||
raise
|
||||
except CommitFileError as exc:
|
||||
latest = self.ledger.get(key)
|
||||
if latest["state"] not in {"FAILED_RECOVERABLE", "MANUAL_REVIEW_REQUIRED"}:
|
||||
failure_state = (
|
||||
"MANUAL_REVIEW_REQUIRED"
|
||||
if exc.code in {
|
||||
"writer_gitea_oid_conflict",
|
||||
"writer_file_content_conflict",
|
||||
"writer_current_branch_conflict",
|
||||
}
|
||||
else "FAILED_RECOVERABLE"
|
||||
)
|
||||
latest = self._transition(key, failure_state, error_code=exc.code)
|
||||
self._audit(grant, data, digest, latest, "failed")
|
||||
raise
|
||||
|
||||
def _audit(self, grant: Any, data: dict[str, Any], digest: str, record: dict[str, Any], result: str) -> None:
|
||||
self.audit({
|
||||
"token_id": grant.token_id,
|
||||
"task_id": grant.task_id,
|
||||
"audience": grant.record["audience"],
|
||||
"source_ip": grant.record.get("_source_ip", "socket-peer"),
|
||||
"operation": grant.operation,
|
||||
"branch": data["branch"],
|
||||
"path": data["path"],
|
||||
"idempotency_key": data["idempotency_key"],
|
||||
"operation_digest": digest,
|
||||
"path_policy_sha256": policy.configured_path_policy_sha256(),
|
||||
"pre_head_oid": record["pre_head_oid"],
|
||||
"commit_oid": record.get("commit_oid"),
|
||||
"result": result,
|
||||
})
|
||||
|
||||
@staticmethod
|
||||
def _result(record: dict[str, Any], *, idempotent: bool) -> dict[str, Any]:
|
||||
return {
|
||||
"status": "completed",
|
||||
"state": record["state"],
|
||||
"operation_id": record["operation_id"],
|
||||
"branch": record["branch"],
|
||||
"path": record["path"],
|
||||
"commit_oid": record.get("commit_oid"),
|
||||
"push_status": record.get("push_status"),
|
||||
"verification_status": record.get("verification_status"),
|
||||
"idempotent": idempotent,
|
||||
}
|
||||
@@ -0,0 +1,422 @@
|
||||
"""Strict service-level exact-path policy for isolated Git writers."""
|
||||
from __future__ import annotations
|
||||
|
||||
import grp
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import stat
|
||||
from dataclasses import dataclass
|
||||
from functools import lru_cache
|
||||
from pathlib import Path, PurePosixPath, PureWindowsPath
|
||||
from typing import Any
|
||||
from urllib.parse import unquote
|
||||
|
||||
|
||||
MAX_POLICY_BYTES = 65_536
|
||||
POLICY_FIELDS = frozenset(
|
||||
{
|
||||
"schema_version",
|
||||
"policy_id",
|
||||
"audience",
|
||||
"repository_identity",
|
||||
"allowed_branch",
|
||||
"allowed_operations",
|
||||
}
|
||||
)
|
||||
OPERATION_FIELDS = frozenset({"exact_paths", "max_content_bytes", "content_profile"})
|
||||
SUPPORTED_OPERATIONS = frozenset({"commit_file.atomic"})
|
||||
SUPPORTED_CONTENT_PROFILES = frozenset({"systemd-codex-review-worker-v1"})
|
||||
_SECRET_RE = re.compile(
|
||||
r"(?i)(authorization\s*:|bearer\s+|(?:token|password|secret)\s*[:=])"
|
||||
)
|
||||
_SYSTEMD_NAME_RE = re.compile(r"[A-Za-z][A-Za-z0-9]*\Z")
|
||||
_SYSTEMD_MAX_LINE_BYTES = 4096
|
||||
_SYSTEMD_ALLOWED_DIRECTIVES = {
|
||||
"Unit": frozenset({"Description", "After", "Wants"}),
|
||||
"Service": frozenset(
|
||||
{
|
||||
"Type",
|
||||
"User",
|
||||
"Group",
|
||||
"WorkingDirectory",
|
||||
"Environment",
|
||||
"ExecStart",
|
||||
"Restart",
|
||||
"RestartSec",
|
||||
"NoNewPrivileges",
|
||||
"PrivateTmp",
|
||||
"PrivateDevices",
|
||||
"ProtectSystem",
|
||||
"ProtectHome",
|
||||
"ProtectKernelTunables",
|
||||
"ProtectKernelModules",
|
||||
"ProtectControlGroups",
|
||||
"LockPersonality",
|
||||
"RestrictSUIDSGID",
|
||||
"RestrictRealtime",
|
||||
"MemoryDenyWriteExecute",
|
||||
"RuntimeDirectory",
|
||||
"StateDirectory",
|
||||
"CacheDirectory",
|
||||
"ReadOnlyPaths",
|
||||
"ReadWritePaths",
|
||||
"UMask",
|
||||
}
|
||||
),
|
||||
"Install": frozenset({"WantedBy"}),
|
||||
}
|
||||
_SYSTEMD_MULTI_DIRECTIVES = frozenset(
|
||||
{
|
||||
("Service", "Environment"),
|
||||
("Service", "ReadOnlyPaths"),
|
||||
("Service", "ReadWritePaths"),
|
||||
}
|
||||
)
|
||||
_SYSTEMD_EXACT_SINGLE_VALUES = {
|
||||
("Unit", "Description"): "AIOS Codex Review/Test Worker",
|
||||
("Unit", "After"): "network-online.target",
|
||||
("Unit", "Wants"): "network-online.target",
|
||||
("Service", "Type"): "simple",
|
||||
("Service", "User"): "aiman",
|
||||
("Service", "Group"): "gitops",
|
||||
(
|
||||
"Service",
|
||||
"WorkingDirectory",
|
||||
): "/usr/libexec/aios-codex-review-test-worker/current",
|
||||
(
|
||||
"Service",
|
||||
"ExecStart",
|
||||
): "/usr/bin/python3 -m task_plane.workers.codex_review_test",
|
||||
("Service", "Restart"): "on-failure",
|
||||
("Service", "RestartSec"): "2",
|
||||
("Service", "NoNewPrivileges"): "true",
|
||||
("Service", "PrivateTmp"): "true",
|
||||
("Service", "PrivateDevices"): "true",
|
||||
("Service", "ProtectSystem"): "strict",
|
||||
("Service", "ProtectHome"): "true",
|
||||
("Service", "ProtectKernelTunables"): "true",
|
||||
("Service", "ProtectKernelModules"): "true",
|
||||
("Service", "ProtectControlGroups"): "true",
|
||||
("Service", "LockPersonality"): "true",
|
||||
("Service", "RestrictSUIDSGID"): "true",
|
||||
("Service", "RestrictRealtime"): "true",
|
||||
("Service", "MemoryDenyWriteExecute"): "true",
|
||||
("Service", "RuntimeDirectory"): "aios-codex-review-test-worker",
|
||||
("Service", "StateDirectory"): "aios-codex-review-test-worker",
|
||||
("Service", "CacheDirectory"): "aios-codex-review-test-worker",
|
||||
("Service", "UMask"): "0077",
|
||||
("Install", "WantedBy"): "multi-user.target",
|
||||
}
|
||||
_SYSTEMD_EXACT_MULTI_VALUES = {
|
||||
("Service", "Environment"): (
|
||||
"AIOS_CODEX_WORKER_MODE=shadow",
|
||||
"AIOS_TASK_PLANE_EXECUTION_ENABLED=false",
|
||||
"PYTHONDONTWRITEBYTECODE=1",
|
||||
),
|
||||
("Service", "ReadOnlyPaths"): (
|
||||
"/data/aios-gitops/repo.git",
|
||||
"/usr/libexec/aios-codex-review-test-worker",
|
||||
),
|
||||
("Service", "ReadWritePaths"): (
|
||||
"/run/aios-codex-review-test-worker",
|
||||
"/var/lib/aios-codex-review-test-worker",
|
||||
"/var/cache/aios-codex-review-test-worker",
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
class PathPolicyError(PermissionError):
|
||||
pass
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class OperationPolicy:
|
||||
exact_paths: tuple[str, ...]
|
||||
max_content_bytes: int
|
||||
content_profile: str
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class ServicePathPolicy:
|
||||
schema_version: int
|
||||
policy_id: str
|
||||
audience: str
|
||||
repository_identity: str
|
||||
allowed_branch: str
|
||||
allowed_operations: dict[str, OperationPolicy]
|
||||
sha256: str
|
||||
|
||||
def operation(self, name: str) -> OperationPolicy:
|
||||
try:
|
||||
return self.allowed_operations[name]
|
||||
except KeyError:
|
||||
raise PathPolicyError("isolated_writer_service_operation_rejected") from None
|
||||
|
||||
def require_exact_path(self, operation: str, path: str) -> OperationPolicy:
|
||||
selected = self.operation(operation)
|
||||
if path not in selected.exact_paths:
|
||||
raise PathPolicyError("isolated_writer_service_path_rejected")
|
||||
return selected
|
||||
|
||||
|
||||
def _canonical_json(value: Any) -> bytes:
|
||||
return (
|
||||
json.dumps(value, ensure_ascii=False, sort_keys=True, separators=(",", ":"))
|
||||
+ "\n"
|
||||
).encode("utf-8")
|
||||
|
||||
|
||||
def validate_relative_path_syntax(path: str) -> str:
|
||||
if not isinstance(path, str):
|
||||
raise PathPolicyError("isolated_writer_path_rejected")
|
||||
if not path or path != path.strip() or "\x00" in path or "\\" in path:
|
||||
raise PathPolicyError("isolated_writer_path_rejected")
|
||||
try:
|
||||
decoded = unquote(path, errors="strict")
|
||||
except (UnicodeDecodeError, ValueError):
|
||||
raise PathPolicyError("isolated_writer_path_rejected") from None
|
||||
if decoded != path:
|
||||
raise PathPolicyError("isolated_writer_path_rejected")
|
||||
posix = PurePosixPath(path)
|
||||
windows = PureWindowsPath(path)
|
||||
parts = path.split("/")
|
||||
if (
|
||||
posix.is_absolute()
|
||||
or windows.is_absolute()
|
||||
or bool(windows.drive)
|
||||
or any(part in {"", ".", ".."} for part in parts)
|
||||
):
|
||||
raise PathPolicyError("isolated_writer_path_rejected")
|
||||
return path
|
||||
|
||||
|
||||
def _require_fields(value: dict[str, Any], allowed: frozenset[str], code: str) -> None:
|
||||
if set(value) != allowed:
|
||||
raise PathPolicyError(code)
|
||||
|
||||
|
||||
def _parse_policy(document: Any, canonical: bytes) -> ServicePathPolicy:
|
||||
if not isinstance(document, dict):
|
||||
raise PathPolicyError("isolated_writer_path_policy_object_required")
|
||||
_require_fields(document, POLICY_FIELDS, "isolated_writer_path_policy_fields_rejected")
|
||||
if document["schema_version"] != 1:
|
||||
raise PathPolicyError("isolated_writer_path_policy_schema_rejected")
|
||||
text_fields = (
|
||||
"policy_id",
|
||||
"audience",
|
||||
"repository_identity",
|
||||
"allowed_branch",
|
||||
)
|
||||
if any(
|
||||
not isinstance(document[field], str) or not document[field].strip()
|
||||
for field in text_fields
|
||||
):
|
||||
raise PathPolicyError("isolated_writer_path_policy_value_rejected")
|
||||
raw_operations = document["allowed_operations"]
|
||||
if not isinstance(raw_operations, dict) or not raw_operations:
|
||||
raise PathPolicyError("isolated_writer_path_policy_operations_rejected")
|
||||
if not set(raw_operations).issubset(SUPPORTED_OPERATIONS):
|
||||
raise PathPolicyError("isolated_writer_path_policy_operations_rejected")
|
||||
operations: dict[str, OperationPolicy] = {}
|
||||
for name, raw in raw_operations.items():
|
||||
if not isinstance(raw, dict):
|
||||
raise PathPolicyError("isolated_writer_path_policy_operation_rejected")
|
||||
_require_fields(
|
||||
raw,
|
||||
OPERATION_FIELDS,
|
||||
"isolated_writer_path_policy_operation_fields_rejected",
|
||||
)
|
||||
paths = raw["exact_paths"]
|
||||
if (
|
||||
not isinstance(paths, list)
|
||||
or not paths
|
||||
or any(not isinstance(item, str) for item in paths)
|
||||
):
|
||||
raise PathPolicyError("isolated_writer_path_policy_paths_rejected")
|
||||
validated = tuple(validate_relative_path_syntax(item) for item in paths)
|
||||
if len(set(validated)) != len(validated):
|
||||
raise PathPolicyError("isolated_writer_path_policy_paths_rejected")
|
||||
maximum = raw["max_content_bytes"]
|
||||
if not isinstance(maximum, int) or isinstance(maximum, bool) or not 1 <= maximum <= 1_048_576:
|
||||
raise PathPolicyError("isolated_writer_path_policy_size_rejected")
|
||||
profile = raw["content_profile"]
|
||||
if profile not in SUPPORTED_CONTENT_PROFILES:
|
||||
raise PathPolicyError("isolated_writer_path_policy_profile_rejected")
|
||||
operations[name] = OperationPolicy(validated, maximum, profile)
|
||||
return ServicePathPolicy(
|
||||
schema_version=1,
|
||||
policy_id=document["policy_id"],
|
||||
audience=document["audience"],
|
||||
repository_identity=document["repository_identity"],
|
||||
allowed_branch=document["allowed_branch"],
|
||||
allowed_operations=operations,
|
||||
sha256=hashlib.sha256(canonical).hexdigest(),
|
||||
)
|
||||
|
||||
|
||||
def load_path_policy(
|
||||
path: Path,
|
||||
*,
|
||||
expected_owner_uid: int = 0,
|
||||
expected_group_gid: int | None = None,
|
||||
expected_parent_mode: int = 0o750,
|
||||
expected_file_mode: int = 0o640,
|
||||
) -> ServicePathPolicy:
|
||||
lexical = Path(os.path.abspath(str(path)))
|
||||
if lexical.is_symlink():
|
||||
raise PathPolicyError("isolated_writer_path_policy_symlink_rejected")
|
||||
try:
|
||||
parent_info = lexical.parent.stat(follow_symlinks=False)
|
||||
file_info = lexical.stat(follow_symlinks=False)
|
||||
except FileNotFoundError:
|
||||
raise PathPolicyError("isolated_writer_path_policy_missing") from None
|
||||
if stat.S_ISLNK(parent_info.st_mode) or not stat.S_ISDIR(parent_info.st_mode):
|
||||
raise PathPolicyError("isolated_writer_path_policy_parent_rejected")
|
||||
if stat.S_ISLNK(file_info.st_mode) or not stat.S_ISREG(file_info.st_mode):
|
||||
raise PathPolicyError("isolated_writer_path_policy_file_rejected")
|
||||
if lexical.resolve(strict=True) != lexical:
|
||||
raise PathPolicyError("isolated_writer_path_policy_symlink_rejected")
|
||||
if (
|
||||
parent_info.st_uid != expected_owner_uid
|
||||
or stat.S_IMODE(parent_info.st_mode) != expected_parent_mode
|
||||
):
|
||||
raise PathPolicyError("isolated_writer_path_policy_parent_permissions_rejected")
|
||||
if (
|
||||
file_info.st_uid != expected_owner_uid
|
||||
or stat.S_IMODE(file_info.st_mode) != expected_file_mode
|
||||
):
|
||||
raise PathPolicyError("isolated_writer_path_policy_permissions_rejected")
|
||||
if expected_group_gid is not None and (
|
||||
parent_info.st_gid != expected_group_gid or file_info.st_gid != expected_group_gid
|
||||
):
|
||||
raise PathPolicyError("isolated_writer_path_policy_group_rejected")
|
||||
if file_info.st_size <= 0 or file_info.st_size > MAX_POLICY_BYTES:
|
||||
raise PathPolicyError("isolated_writer_path_policy_file_size_rejected")
|
||||
raw = lexical.read_bytes()
|
||||
try:
|
||||
document = json.loads(raw.decode("utf-8"))
|
||||
except (UnicodeDecodeError, json.JSONDecodeError):
|
||||
raise PathPolicyError("isolated_writer_path_policy_json_rejected") from None
|
||||
canonical = _canonical_json(document)
|
||||
if raw != canonical:
|
||||
raise PathPolicyError("isolated_writer_path_policy_not_canonical")
|
||||
return _parse_policy(document, canonical)
|
||||
|
||||
|
||||
@lru_cache(maxsize=1)
|
||||
def configured_path_policy() -> ServicePathPolicy | None:
|
||||
configured = os.getenv("AIOS_GITOPS_PATH_POLICY_FILE", "").strip()
|
||||
if not configured:
|
||||
return None
|
||||
if not Path(configured).is_absolute():
|
||||
raise PathPolicyError("isolated_writer_path_policy_path_rejected")
|
||||
try:
|
||||
gitops_gid = grp.getgrnam("gitops").gr_gid
|
||||
except KeyError:
|
||||
raise PathPolicyError("isolated_writer_path_policy_group_rejected") from None
|
||||
loaded = load_path_policy(Path(configured), expected_group_gid=gitops_gid)
|
||||
if loaded.audience != os.getenv("AIOS_GITOPS_SERVICE_AUDIENCE", ""):
|
||||
raise PathPolicyError("isolated_writer_path_policy_audience_rejected")
|
||||
if loaded.repository_identity != os.getenv("AIOS_GITOPS_REPOSITORY_IDENTITY", ""):
|
||||
raise PathPolicyError("isolated_writer_path_policy_repository_rejected")
|
||||
if loaded.allowed_branch != os.getenv("AIOS_GITOPS_ALLOWED_BRANCH", ""):
|
||||
raise PathPolicyError("isolated_writer_path_policy_branch_rejected")
|
||||
return loaded
|
||||
|
||||
|
||||
def clear_path_policy_cache() -> None:
|
||||
configured_path_policy.cache_clear()
|
||||
|
||||
|
||||
def require_service_path(operation: str, path: str) -> OperationPolicy:
|
||||
loaded = configured_path_policy()
|
||||
if loaded is None:
|
||||
raise PathPolicyError("isolated_writer_service_path_policy_required")
|
||||
return loaded.require_exact_path(operation, path)
|
||||
|
||||
|
||||
def configured_path_policy_sha256() -> str | None:
|
||||
loaded = configured_path_policy()
|
||||
return None if loaded is None else loaded.sha256
|
||||
|
||||
|
||||
def validate_content(operation: str, path: str, content: str) -> None:
|
||||
selected = require_service_path(operation, path)
|
||||
try:
|
||||
encoded = content.encode("utf-8", errors="strict")
|
||||
except (AttributeError, UnicodeEncodeError):
|
||||
raise PathPolicyError("isolated_writer_systemd_profile_rejected") from None
|
||||
if len(encoded) > selected.max_content_bytes:
|
||||
raise PathPolicyError("isolated_writer_service_content_too_large")
|
||||
if selected.content_profile == "systemd-codex-review-worker-v1":
|
||||
_validate_systemd_codex_review_worker(content)
|
||||
return
|
||||
raise PathPolicyError("isolated_writer_path_policy_profile_rejected")
|
||||
|
||||
|
||||
def _validate_systemd_codex_review_worker(content: str) -> None:
|
||||
if not isinstance(content, str):
|
||||
raise PathPolicyError("isolated_writer_systemd_profile_rejected")
|
||||
try:
|
||||
content.encode("utf-8", errors="strict")
|
||||
except UnicodeEncodeError:
|
||||
raise PathPolicyError("isolated_writer_systemd_profile_rejected") from None
|
||||
if (
|
||||
"\x00" in content
|
||||
or "\r" in content
|
||||
or _SECRET_RE.search(content)
|
||||
or not content.endswith("\n")
|
||||
):
|
||||
raise PathPolicyError("isolated_writer_systemd_profile_rejected")
|
||||
|
||||
sections: set[str] = set()
|
||||
values: dict[tuple[str, str], list[str]] = {}
|
||||
section: str | None = None
|
||||
for line in content.splitlines():
|
||||
if len(line.encode("utf-8")) > _SYSTEMD_MAX_LINE_BYTES:
|
||||
raise PathPolicyError("isolated_writer_systemd_profile_rejected")
|
||||
if not line:
|
||||
continue
|
||||
if line[0].isspace() or line.startswith(("#", ";")):
|
||||
raise PathPolicyError("isolated_writer_systemd_profile_rejected")
|
||||
if line.startswith("[") and line.endswith("]"):
|
||||
candidate = line[1:-1]
|
||||
if (
|
||||
not _SYSTEMD_NAME_RE.fullmatch(candidate)
|
||||
or candidate not in _SYSTEMD_ALLOWED_DIRECTIVES
|
||||
or candidate in sections
|
||||
):
|
||||
raise PathPolicyError("isolated_writer_systemd_profile_rejected")
|
||||
sections.add(candidate)
|
||||
section = candidate
|
||||
continue
|
||||
if section is None or "=" not in line:
|
||||
raise PathPolicyError("isolated_writer_systemd_profile_rejected")
|
||||
directive, value = line.split("=", 1)
|
||||
if (
|
||||
not _SYSTEMD_NAME_RE.fullmatch(directive)
|
||||
or directive not in _SYSTEMD_ALLOWED_DIRECTIVES[section]
|
||||
or not value
|
||||
or value != value.strip()
|
||||
):
|
||||
raise PathPolicyError("isolated_writer_systemd_profile_rejected")
|
||||
key = (section, directive)
|
||||
if key in values and key not in _SYSTEMD_MULTI_DIRECTIVES:
|
||||
raise PathPolicyError("isolated_writer_systemd_profile_rejected")
|
||||
values.setdefault(key, []).append(value)
|
||||
|
||||
if sections != set(_SYSTEMD_ALLOWED_DIRECTIVES):
|
||||
raise PathPolicyError("isolated_writer_systemd_profile_rejected")
|
||||
if set(values) != set(_SYSTEMD_EXACT_SINGLE_VALUES) | set(
|
||||
_SYSTEMD_EXACT_MULTI_VALUES
|
||||
):
|
||||
raise PathPolicyError("isolated_writer_systemd_profile_rejected")
|
||||
for key, expected in _SYSTEMD_EXACT_SINGLE_VALUES.items():
|
||||
if values.get(key) != [expected]:
|
||||
raise PathPolicyError("isolated_writer_systemd_profile_rejected")
|
||||
for key, expected in _SYSTEMD_EXACT_MULTI_VALUES.items():
|
||||
observed = values.get(key, [])
|
||||
if len(observed) != len(set(observed)) or tuple(observed) != expected:
|
||||
raise PathPolicyError("isolated_writer_systemd_profile_rejected")
|
||||
@@ -0,0 +1,241 @@
|
||||
"""Digest-only, task-scoped token registry for isolated writers."""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import hashlib
|
||||
import hmac
|
||||
import json
|
||||
import os
|
||||
import grp
|
||||
import pwd
|
||||
import secrets
|
||||
import sqlite3
|
||||
import stat
|
||||
import threading
|
||||
from datetime import datetime, timezone
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
_REGISTRY_INIT_LOCK = threading.Lock()
|
||||
|
||||
|
||||
def utc_now() -> str:
|
||||
return datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
|
||||
|
||||
|
||||
def token_digest(token: str) -> str:
|
||||
return hashlib.sha256(token.encode("utf-8")).hexdigest()
|
||||
|
||||
|
||||
class TokenRegistry:
|
||||
def __init__(self, path: Path | str):
|
||||
self.path = Path(path)
|
||||
self._prepare()
|
||||
|
||||
def _prepare(self) -> None:
|
||||
self.path.parent.mkdir(parents=True, exist_ok=True)
|
||||
if self.path.parent.is_symlink() or self.path.is_symlink():
|
||||
raise PermissionError("writer_token_registry_symlink_rejected")
|
||||
with _REGISTRY_INIT_LOCK:
|
||||
with self._connect() as db:
|
||||
db.executescript(
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS token_grants (
|
||||
token_id TEXT PRIMARY KEY,
|
||||
token_digest TEXT NOT NULL UNIQUE,
|
||||
grant_json TEXT NOT NULL,
|
||||
revoked INTEGER NOT NULL DEFAULT 0,
|
||||
created_at TEXT NOT NULL
|
||||
);
|
||||
CREATE TABLE IF NOT EXISTS token_uses (
|
||||
token_id TEXT NOT NULL,
|
||||
operation TEXT NOT NULL,
|
||||
use_count INTEGER NOT NULL DEFAULT 0,
|
||||
PRIMARY KEY(token_id, operation),
|
||||
FOREIGN KEY(token_id) REFERENCES token_grants(token_id)
|
||||
);
|
||||
"""
|
||||
)
|
||||
os.chmod(self.path, 0o600)
|
||||
|
||||
def _connect(self) -> sqlite3.Connection:
|
||||
db = sqlite3.connect(self.path, timeout=30, isolation_level=None)
|
||||
db.row_factory = sqlite3.Row
|
||||
db.execute("PRAGMA journal_mode=WAL")
|
||||
db.execute("PRAGMA synchronous=FULL")
|
||||
db.execute("PRAGMA foreign_keys=ON")
|
||||
return db
|
||||
|
||||
def issue(self, grant: dict[str, Any], token: str | None = None) -> tuple[str, dict[str, Any]]:
|
||||
value = token or secrets.token_urlsafe(32)
|
||||
if len(value.encode("utf-8")) < 32:
|
||||
raise ValueError("writer_token_entropy_insufficient")
|
||||
record = dict(grant)
|
||||
record["schema_version"] = 2
|
||||
record["token_digest"] = token_digest(value)
|
||||
record["revoked"] = False
|
||||
record.setdefault("created_at", utc_now())
|
||||
required = (
|
||||
"token_id",
|
||||
"audience",
|
||||
"task_id",
|
||||
"source_cidrs",
|
||||
"allowed_branch",
|
||||
"approved_base_oid",
|
||||
"allowed_operations",
|
||||
"allowed_paths",
|
||||
"not_before",
|
||||
"expires_at",
|
||||
"max_uses",
|
||||
"issued_by",
|
||||
)
|
||||
if any(not record.get(field) for field in required):
|
||||
raise ValueError("writer_token_grant_incomplete")
|
||||
encoded = json.dumps(record, sort_keys=True, separators=(",", ":"))
|
||||
with self._connect() as db:
|
||||
db.execute("BEGIN IMMEDIATE")
|
||||
try:
|
||||
db.execute(
|
||||
"""
|
||||
INSERT INTO token_grants(token_id, token_digest, grant_json, revoked, created_at)
|
||||
VALUES(?,?,?,?,?)
|
||||
""",
|
||||
(
|
||||
str(record["token_id"]),
|
||||
str(record["token_digest"]),
|
||||
encoded,
|
||||
0,
|
||||
str(record["created_at"]),
|
||||
),
|
||||
)
|
||||
db.execute("COMMIT")
|
||||
except Exception:
|
||||
db.execute("ROLLBACK")
|
||||
raise
|
||||
return value, record
|
||||
|
||||
def find(self, token: str) -> dict[str, Any] | None:
|
||||
candidate = token_digest(token)
|
||||
with self._connect() as db:
|
||||
rows = db.execute(
|
||||
"SELECT token_digest, grant_json, revoked FROM token_grants"
|
||||
).fetchall()
|
||||
match = None
|
||||
for row in rows:
|
||||
if hmac.compare_digest(str(row["token_digest"]), candidate):
|
||||
match = dict(json.loads(str(row["grant_json"])))
|
||||
match["revoked"] = bool(row["revoked"])
|
||||
return match
|
||||
|
||||
def consume(self, token_id: str, operation: str, maximum: int) -> int:
|
||||
if maximum < 1:
|
||||
raise PermissionError("writer_token_operation_exhausted")
|
||||
with self._connect() as db:
|
||||
db.execute("BEGIN IMMEDIATE")
|
||||
try:
|
||||
row = db.execute(
|
||||
"SELECT revoked FROM token_grants WHERE token_id=?",
|
||||
(token_id,),
|
||||
).fetchone()
|
||||
if row is None or bool(row["revoked"]):
|
||||
raise PermissionError("writer_token_revoked")
|
||||
used_row = db.execute(
|
||||
"SELECT use_count FROM token_uses WHERE token_id=? AND operation=?",
|
||||
(token_id, operation),
|
||||
).fetchone()
|
||||
used = int(used_row["use_count"]) if used_row else 0
|
||||
if used >= maximum:
|
||||
raise PermissionError("writer_token_operation_exhausted")
|
||||
current = used + 1
|
||||
db.execute(
|
||||
"""
|
||||
INSERT INTO token_uses(token_id, operation, use_count)
|
||||
VALUES(?,?,?)
|
||||
ON CONFLICT(token_id, operation)
|
||||
DO UPDATE SET use_count=excluded.use_count
|
||||
""",
|
||||
(token_id, operation, current),
|
||||
)
|
||||
db.execute("COMMIT")
|
||||
return current
|
||||
except Exception:
|
||||
db.execute("ROLLBACK")
|
||||
raise
|
||||
|
||||
def revoke(self, token_id: str) -> bool:
|
||||
with self._connect() as db:
|
||||
db.execute("BEGIN IMMEDIATE")
|
||||
changed = db.execute(
|
||||
"UPDATE token_grants SET revoked=1 WHERE token_id=? AND revoked=0",
|
||||
(token_id,),
|
||||
).rowcount
|
||||
db.execute("COMMIT")
|
||||
return bool(changed)
|
||||
|
||||
def usage(self, token_id: str, operation: str) -> int:
|
||||
with self._connect() as db:
|
||||
row = db.execute(
|
||||
"SELECT use_count FROM token_uses WHERE token_id=? AND operation=?",
|
||||
(token_id, operation),
|
||||
).fetchone()
|
||||
return int(row["use_count"]) if row else 0
|
||||
|
||||
|
||||
def _write_secret_file(
|
||||
path: Path,
|
||||
token: str,
|
||||
*,
|
||||
owner: str | None = None,
|
||||
group: str | None = None,
|
||||
) -> None:
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
if path.parent.is_symlink() or path.is_symlink():
|
||||
raise PermissionError("writer_token_file_symlink_rejected")
|
||||
flags = os.O_WRONLY | os.O_CREAT | os.O_EXCL
|
||||
if hasattr(os, "O_NOFOLLOW"):
|
||||
flags |= os.O_NOFOLLOW
|
||||
fd = os.open(path, flags, 0o600)
|
||||
try:
|
||||
if not stat.S_ISREG(os.fstat(fd).st_mode):
|
||||
raise PermissionError("writer_token_file_not_regular")
|
||||
if owner is not None or group is not None:
|
||||
uid = pwd.getpwnam(owner).pw_uid if owner is not None else -1
|
||||
gid = grp.getgrnam(group).gr_gid if group is not None else -1
|
||||
os.fchown(fd, uid, gid)
|
||||
os.write(fd, token.encode("utf-8"))
|
||||
os.fsync(fd)
|
||||
finally:
|
||||
os.close(fd)
|
||||
|
||||
|
||||
def main(argv: list[str] | None = None) -> int:
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--registry", required=True)
|
||||
sub = parser.add_subparsers(dest="command", required=True)
|
||||
issue = sub.add_parser("issue")
|
||||
issue.add_argument("--grant-file", required=True)
|
||||
issue.add_argument("--token-file", required=True)
|
||||
issue.add_argument("--owner")
|
||||
issue.add_argument("--group")
|
||||
revoke = sub.add_parser("revoke")
|
||||
revoke.add_argument("--token-id", required=True)
|
||||
args = parser.parse_args(argv)
|
||||
registry = TokenRegistry(args.registry)
|
||||
if args.command == "issue":
|
||||
grant = json.loads(Path(args.grant_file).read_text(encoding="utf-8"))
|
||||
token, record = registry.issue(grant)
|
||||
_write_secret_file(
|
||||
Path(args.token_file),
|
||||
token,
|
||||
owner=args.owner,
|
||||
group=args.group,
|
||||
)
|
||||
print(json.dumps({"token_id": record["token_id"], "created": True}, sort_keys=True))
|
||||
return 0
|
||||
changed = registry.revoke(args.token_id)
|
||||
print(json.dumps({"token_id": args.token_id, "revoked": changed}, sort_keys=True))
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
+83
@@ -0,0 +1,83 @@
|
||||
{
|
||||
"approval_request_branch": "governance/request/integration-refresh-control-plane-deployment-20260728T093428Z",
|
||||
"approval_type": "CONTROL_PLANE_DEPLOYMENT_APPROVAL_REQUEST",
|
||||
"authorization_lineage": {
|
||||
"execution_renewal_merge_oid": "3a22c3c7d86930efd4bd93ac05d80ad322050cb9",
|
||||
"grant_id": "grant-11354281aafdd074df5899a3925dd2a0370cffa9edb06f5c7d7227ce8de00d91",
|
||||
"source_mapping_approval_merge_oid": "9514062cf33e45941abe4a8c98377e0713b0b3f6"
|
||||
},
|
||||
"deployment_status": "NOT_EXECUTED",
|
||||
"evidence": {
|
||||
"control_plane_artifact": {
|
||||
"path": "/var/lib/aios-integration-refresh-source-writer-v2/artifacts/control-plane-patch-112d995545e3e79126a062b2643a9f0c17ae7bd4.tar",
|
||||
"sha256": "bf101ade1aaf956f40c0bba74ed4b347f5f38d0de6a0c1b7694ae107c4b43c14",
|
||||
"size_bytes": 13363200
|
||||
},
|
||||
"rendered_systemd_config_sha256": "182a3cba48e752798cf24323a8c0249c53d7386371388a8db0435a29c43dc969",
|
||||
"rollback_artifact": {
|
||||
"path": "/var/lib/aios-gitops/runtime/tasks/AIOS-INTEGRATION-REFRESH-PROVISIONER-IMPLEMENTATION-20260726/implementation/rollback-to-85426f4f570e4d40b212318d47e52e4aed5cb06a.tar",
|
||||
"sha256": "f12a2a95e475da5d02eaff99e93615cb0186019ca30edae013ab054a2c6418ce"
|
||||
},
|
||||
"source_map": {
|
||||
"path": "docs/autonomous-release/INTEGRATION_REFRESH_SOURCE_MAP_20260728.json",
|
||||
"sha256": "01ddd56cfa2a525559e2d174cf7df117a9465ffce44ef25bc7a15b8e74d06bf5"
|
||||
},
|
||||
"test_evidence": {
|
||||
"collection_errors": 0,
|
||||
"path": "/var/lib/aios-gitops/runtime/tasks/AIOS-INTEGRATION-REFRESH-PROVISIONER-IMPLEMENTATION-20260726/implementation/test-evidence.json",
|
||||
"sha256": "d8933553f56462a3f1b3040dec989ff0adf717a17728d55ba0fe9e127d93c206",
|
||||
"tests_failed": 0,
|
||||
"tests_passed": 41
|
||||
}
|
||||
},
|
||||
"execution_conditions": {
|
||||
"candidate_only": true,
|
||||
"integration_unchanged": true,
|
||||
"main_unchanged": true,
|
||||
"no_credentials_or_secrets_in_request": true,
|
||||
"production_baseline_unchanged": true,
|
||||
"registry_unchanged": true
|
||||
},
|
||||
"expected_governance_base_oid": "3a22c3c7d86930efd4bd93ac05d80ad322050cb9",
|
||||
"expires_at": "2026-07-28T21:34:28Z",
|
||||
"implementation": {
|
||||
"commit_oid": "112d995545e3e79126a062b2643a9f0c17ae7bd4",
|
||||
"tree_oid": "4ed88399fb8e7e4c235413ebbd783c9b6ad3c624"
|
||||
},
|
||||
"implementation_branch": "patch/integration-refresh-grant-provisioner-20260726",
|
||||
"issued_at": "2026-07-28T09:34:28Z",
|
||||
"prohibited_operations": [
|
||||
"production-baseline.write",
|
||||
"production-tag.write",
|
||||
"main.write",
|
||||
"integration.write",
|
||||
"registry.write",
|
||||
"force-push",
|
||||
"direct-main-push",
|
||||
"direct-tag-write"
|
||||
],
|
||||
"repository": "aios/aios",
|
||||
"request_status": "PENDING_HUMAN_APPROVAL",
|
||||
"requested_operations": [
|
||||
"control-plane.candidate.deploy",
|
||||
"control-plane.candidate.verify",
|
||||
"control-plane.candidate.rollback"
|
||||
],
|
||||
"required_controls": [
|
||||
"Two distinct human approvals, one Codex and one GPT, both APPROVED against the same current approval-request PR head.",
|
||||
"Re-verify artifact, test evidence, source map, rendered systemd configuration, and rollback artifact paths and SHA-256 digests immediately before execution.",
|
||||
"AIOS_GITOPS_WRITES_ENABLED=false throughout candidate deployment, verification, and rollback.",
|
||||
"No force push, direct main write, or direct tag write."
|
||||
],
|
||||
"required_reviewers": [
|
||||
"codex",
|
||||
"gpt"
|
||||
],
|
||||
"schema": "aios.control-plane-deployment-approval-request/v1",
|
||||
"target_governance": {
|
||||
"approved_base_oid": "3a22c3c7d86930efd4bd93ac05d80ad322050cb9",
|
||||
"binding_rule": "Human approvals must bind to the same approval-request PR head; its first parent must equal the active governance base."
|
||||
},
|
||||
"task_id": "AIOS-INTEGRATION-REFRESH-PROVISIONER-IMPLEMENTATION-20260726",
|
||||
"validity_hours": 12
|
||||
}
|
||||
+268
@@ -0,0 +1,268 @@
|
||||
{
|
||||
"acknowledges_pr26_owner_approval_was_missing": true,
|
||||
"approval_request_branch": "governance/request/single-owner-control-plane-deployment-20260728T170740Z-ea7a102dac417c23",
|
||||
"approval_type": "SINGLE_OWNER_GOVERNANCE_CONTROL_PLANE_DEPLOYMENT",
|
||||
"control_plane_targets": {
|
||||
"allowed_human_approvers": [
|
||||
"owner"
|
||||
],
|
||||
"branch_deletion": false,
|
||||
"direct_protected_branch_push": false,
|
||||
"dismiss_stale_approvals": true,
|
||||
"executor_must_not_approve": true,
|
||||
"force_push": false,
|
||||
"governance_model": "SINGLE_OWNER_WITH_MACHINE_GATES_V1",
|
||||
"historical_records_immutable": true,
|
||||
"machine_gates_required": true,
|
||||
"policy_effective_at_set_only_after_activation_tests_pass": true,
|
||||
"protected_branches": [
|
||||
"governance/approvals",
|
||||
"main",
|
||||
"release/**",
|
||||
"integration/**"
|
||||
],
|
||||
"requester_must_not_approve": true,
|
||||
"require_approval_for_latest_commit": true,
|
||||
"required_human_approvals": 1,
|
||||
"required_owner_approvals": 1,
|
||||
"required_status_contexts": [
|
||||
"aios/machine-gates",
|
||||
"aios/secret-scan",
|
||||
"aios/evidence-closure"
|
||||
],
|
||||
"tag_rewrite": false,
|
||||
"wip_blocks_merge": true
|
||||
},
|
||||
"does_not_claim_pr26_met_old_two_natural_person_policy": true,
|
||||
"expected_authoritative_governance_base_oid": "ac2dc5c30eb35748ef11c98297812729d0fd5828",
|
||||
"expected_gitea_governance_base_oid": "a5489f1b67870622162b2daa781bd0237735831a",
|
||||
"expires_at": "2026-07-29T05:07:40Z",
|
||||
"explicitly_allowed_operations_after_exact_head_approvals_and_merge": [
|
||||
"approval_mirror.fast_forward_governance_only",
|
||||
"approval_validator.update",
|
||||
"state_machine.update",
|
||||
"gitea_branch_protection.update_via_authenticated_api_compare_and_swap",
|
||||
"governance_policy.activate",
|
||||
"rollback_configuration.install",
|
||||
"governance_acceptance_tests.execute",
|
||||
"single_owner_governance_activation_receipt.create"
|
||||
],
|
||||
"explicitly_denied_operations": [
|
||||
"force_push",
|
||||
"plus_refspec",
|
||||
"tag_fetch",
|
||||
"prune",
|
||||
"direct_git_database_write",
|
||||
"direct_protected_ref_push",
|
||||
"main_write",
|
||||
"integration_write",
|
||||
"tag_write",
|
||||
"registry_write",
|
||||
"baseline_write",
|
||||
"production_write",
|
||||
"history_rewrite",
|
||||
"branch_delete",
|
||||
"secret_read",
|
||||
"approval_impersonation"
|
||||
],
|
||||
"global_constraints": {
|
||||
"AIOS_GITOPS_WRITES_ENABLED": false,
|
||||
"direct_main_push": false,
|
||||
"force_push_used": false,
|
||||
"owner_approval_respected": true,
|
||||
"secrets_committed": false,
|
||||
"tag_rewritten": false
|
||||
},
|
||||
"implementation_surface": {
|
||||
"approval_validator": [
|
||||
"services/aios-autonomous-release-candidate/aios_autonomous_release_candidate/approval.py",
|
||||
"services/aios-autonomous-release-candidate/aios_autonomous_release_candidate/models.py",
|
||||
"services/aios-autonomous-release-candidate/aios_autonomous_release_candidate/policy.py"
|
||||
],
|
||||
"runtime": [
|
||||
"active-runtime/app/silent_release_controller.py",
|
||||
"active-runtime/app/silent_release_hardening.py"
|
||||
],
|
||||
"state_machine": [
|
||||
"services/aios-autonomous-release-candidate/aios_autonomous_release_candidate/store.py",
|
||||
"services/aios-autonomous-release-candidate/aios_autonomous_release_candidate/workflow.py",
|
||||
"services/aios-autonomous-release-candidate/aios_autonomous_release_candidate/release.py"
|
||||
],
|
||||
"systemd": [
|
||||
"deploy/systemd/aios-autonomous-release-candidate.service",
|
||||
"deploy/systemd/aios-silent-release-controller.service"
|
||||
]
|
||||
},
|
||||
"issued_at": "2026-07-28T17:07:40Z",
|
||||
"legacy_gitea_protection_gate": {
|
||||
"account_reviews_do_not_prove_distinct_natural_persons": true,
|
||||
"both_must_approve_this_exact_head": true,
|
||||
"current_approval_whitelist": [
|
||||
"codex",
|
||||
"gpt"
|
||||
],
|
||||
"current_required_approvals": 2
|
||||
},
|
||||
"migration_mode": "OWNER_BREAK_GLASS_GOVERNANCE_RESET_RATIFICATION",
|
||||
"mirror_contract": {
|
||||
"expected_chain_contains_policy_merge_oid": "a5489f1b67870622162b2daa781bd0237735831a",
|
||||
"expected_pre_sync_authoritative_oid": "ac2dc5c30eb35748ef11c98297812729d0fd5828",
|
||||
"fast_forward_only": true,
|
||||
"force": false,
|
||||
"fsck_connectivity_only_required": true,
|
||||
"only_ref": "refs/heads/governance/approvals",
|
||||
"plus_refspec": false,
|
||||
"prune": false,
|
||||
"quarantine_required": true,
|
||||
"source": "gitea_repository_file_url_only",
|
||||
"tags": false
|
||||
},
|
||||
"owner_break_glass_ratification": {
|
||||
"attests_reason": "single-maintainer-system",
|
||||
"attests_scope": "governance-policy-migration-only",
|
||||
"created_before_expires_at": true,
|
||||
"dismissed": false,
|
||||
"required": true,
|
||||
"review_state": "APPROVED",
|
||||
"reviewed_head_oid_must_equal_current_pr_head_oid": true,
|
||||
"reviewer_user_id": 2,
|
||||
"reviewer_username": "owner",
|
||||
"stale": false
|
||||
},
|
||||
"pre_approval_non_authorizations": {
|
||||
"approval_validator_update_allowed": false,
|
||||
"development_allowed": false,
|
||||
"gitea_branch_protection_update_allowed": false,
|
||||
"governance_policy_activation_allowed": false,
|
||||
"merge_allowed": false,
|
||||
"mirror_allowed": false,
|
||||
"production_change_allowed": false,
|
||||
"rollback_install_allowed": false,
|
||||
"standing_writer_policy_allowed": false,
|
||||
"state_machine_update_allowed": false
|
||||
},
|
||||
"pre_cutover_protection_snapshot": {
|
||||
"protected_branch": [
|
||||
{
|
||||
"approvals_whitelist_team_i_ds": "null",
|
||||
"approvals_whitelist_user_i_ds": "[42,43]",
|
||||
"block_on_official_review_requests": 1,
|
||||
"block_on_outdated_branch": 1,
|
||||
"block_on_rejected_reviews": 1,
|
||||
"branch_name": "governance/approvals",
|
||||
"can_push": 0,
|
||||
"created_unix": 1785093201,
|
||||
"dismiss_stale_approvals": 1,
|
||||
"enable_approvals_whitelist": 1,
|
||||
"enable_merge_whitelist": 1,
|
||||
"enable_status_check": 0,
|
||||
"enable_whitelist": 0,
|
||||
"id": 1,
|
||||
"ignore_stale_approvals": 0,
|
||||
"merge_whitelist_team_i_ds": "null",
|
||||
"merge_whitelist_user_i_ds": "[42,43]",
|
||||
"protected_file_patterns": "",
|
||||
"repo_id": 1,
|
||||
"require_signed_commits": 0,
|
||||
"required_approvals": 2,
|
||||
"status_check_contexts": "[]",
|
||||
"unprotected_file_patterns": "",
|
||||
"updated_unix": 1785093422,
|
||||
"whitelist_deploy_keys": 0,
|
||||
"whitelist_team_i_ds": "null",
|
||||
"whitelist_user_i_ds": "null"
|
||||
},
|
||||
{
|
||||
"approvals_whitelist_team_i_ds": "null",
|
||||
"approvals_whitelist_user_i_ds": "[42,43]",
|
||||
"block_on_official_review_requests": 1,
|
||||
"block_on_outdated_branch": 1,
|
||||
"block_on_rejected_reviews": 1,
|
||||
"branch_name": "integration/chatgpt-autonomous-completion-20260726",
|
||||
"can_push": 0,
|
||||
"created_unix": 1785093201,
|
||||
"dismiss_stale_approvals": 1,
|
||||
"enable_approvals_whitelist": 1,
|
||||
"enable_merge_whitelist": 1,
|
||||
"enable_status_check": 0,
|
||||
"enable_whitelist": 0,
|
||||
"id": 3,
|
||||
"ignore_stale_approvals": 0,
|
||||
"merge_whitelist_team_i_ds": "null",
|
||||
"merge_whitelist_user_i_ds": "[42,43]",
|
||||
"protected_file_patterns": "",
|
||||
"repo_id": 1,
|
||||
"require_signed_commits": 0,
|
||||
"required_approvals": 2,
|
||||
"status_check_contexts": "[]",
|
||||
"unprotected_file_patterns": "",
|
||||
"updated_unix": 1785093422,
|
||||
"whitelist_deploy_keys": 0,
|
||||
"whitelist_team_i_ds": "null",
|
||||
"whitelist_user_i_ds": "null"
|
||||
},
|
||||
{
|
||||
"approvals_whitelist_team_i_ds": "null",
|
||||
"approvals_whitelist_user_i_ds": "[42,43]",
|
||||
"block_on_official_review_requests": 1,
|
||||
"block_on_outdated_branch": 1,
|
||||
"block_on_rejected_reviews": 1,
|
||||
"branch_name": "main",
|
||||
"can_push": 0,
|
||||
"created_unix": 1785093201,
|
||||
"dismiss_stale_approvals": 1,
|
||||
"enable_approvals_whitelist": 1,
|
||||
"enable_merge_whitelist": 1,
|
||||
"enable_status_check": 0,
|
||||
"enable_whitelist": 0,
|
||||
"id": 2,
|
||||
"ignore_stale_approvals": 0,
|
||||
"merge_whitelist_team_i_ds": "null",
|
||||
"merge_whitelist_user_i_ds": "[42,43]",
|
||||
"protected_file_patterns": "",
|
||||
"repo_id": 1,
|
||||
"require_signed_commits": 0,
|
||||
"required_approvals": 2,
|
||||
"status_check_contexts": "[]",
|
||||
"unprotected_file_patterns": "",
|
||||
"updated_unix": 1785093422,
|
||||
"whitelist_deploy_keys": 0,
|
||||
"whitelist_team_i_ds": "null",
|
||||
"whitelist_user_i_ds": "null"
|
||||
}
|
||||
],
|
||||
"protected_tag": []
|
||||
},
|
||||
"pre_cutover_protection_snapshot_sha256": "2bc353f34689242cabebf89856d3faab0e1c496b5d33761dfb7cbd47904516f7",
|
||||
"ratifies_policy_manifest_blob_oid": "65f44007b10fc2a912d69e3f9081c0cfff77dfad",
|
||||
"ratifies_policy_manifest_path": "approvals/governance-policy/SINGLE-OWNER-GOVERNANCE-MIGRATION-20260728T162246Z.json",
|
||||
"ratifies_policy_manifest_sha256": "c097475afbe3074e6482f125b635981f8c0284f3d763c926e2a63e1a3ee1c6c3",
|
||||
"ratifies_policy_merge_oid": "a5489f1b67870622162b2daa781bd0237735831a",
|
||||
"ratifies_policy_pr_number": 26,
|
||||
"ratifies_policy_request_oid": "312d3b483049a748675e887a516ec739c81ff474",
|
||||
"ratifies_policy_tree_oid": "5c6974c058bba6210dcc1bdd0968fb7e6c84a844",
|
||||
"repository": "aios/aios",
|
||||
"request_status": "HUMAN_OWNER_AND_LEGACY_PROTECTION_APPROVAL_PENDING",
|
||||
"required_machine_gates": [
|
||||
"owner_exact_head_review_valid",
|
||||
"legacy_protection_reviews_valid",
|
||||
"ordinary_merge_commit",
|
||||
"parent_order_verified",
|
||||
"manifest_add_only",
|
||||
"quarantine_verification",
|
||||
"approval_mirror_fast_forward",
|
||||
"validator_tests",
|
||||
"state_machine_tests",
|
||||
"protection_compare_and_swap",
|
||||
"positive_governance_tests",
|
||||
"negative_governance_tests",
|
||||
"secret_scan",
|
||||
"rollback_integrity",
|
||||
"production_write_false"
|
||||
],
|
||||
"rollback_configuration_path": "/var/lib/aios-gitops/runtime/tasks/AIOS-SINGLE-OWNER-GOVERNANCE-MIGRATION-20260729/rollback-configuration.tar",
|
||||
"rollback_configuration_sha256": "fc8dcf68aaf53edb0cda45833ee45ba5b8b205791af9073258ca617c8b8187d6",
|
||||
"schema": "aios.single-owner-governance-control-plane-deployment/v1",
|
||||
"scope": "pr26-ratification-mirror-and-single-owner-control-plane-deployment-only",
|
||||
"task_id": "AIOS-SINGLE-OWNER-CONTROL-PLANE-DEPLOYMENT-20260729"
|
||||
}
|
||||
@@ -0,0 +1,484 @@
|
||||
{
|
||||
"allowed_human_approvers": [
|
||||
"owner"
|
||||
],
|
||||
"approval_expires_at": "2026-07-29T16:22:46Z",
|
||||
"approval_request_branch": "governance/request/single-owner-governance-migration-20260728T162246Z",
|
||||
"approval_type": "SINGLE_OWNER_GOVERNANCE_POLICY",
|
||||
"approval_types_migrated_after_policy_effective_at": [
|
||||
"SOURCE_MAPPING_AND_IMPORT_EXECUTION_RENEWAL",
|
||||
"CONTROL_PLANE_DEPLOYMENT_APPROVAL",
|
||||
"DEVELOPMENT_MERGE_APPROVAL",
|
||||
"RELEASE_CANDIDATE_APPROVAL",
|
||||
"PRODUCTION_RELEASE_APPROVAL",
|
||||
"TAG_APPROVAL",
|
||||
"REGISTRY_BASELINE_PROMOTION_APPROVAL",
|
||||
"PRODUCTION_DEPLOYMENT_APPROVAL",
|
||||
"ROLLBACK_APPROVAL"
|
||||
],
|
||||
"atomic_migration": {
|
||||
"backup_checksums_sha256": "72e2fe0b03d1011c872bb39a6cff489f08398ac7d52092f720cbbc63bcf6f753",
|
||||
"backup_root": "/var/lib/aios-gitops/runtime/tasks/AIOS-SINGLE-OWNER-GOVERNANCE-MIGRATION-20260729/pre-migration-backup",
|
||||
"direct_database_update": false,
|
||||
"direct_ref_update": false,
|
||||
"dual_capable_validator_shadow_required": true,
|
||||
"identity_snapshot_sha256": "2d7b4e72d1fdf187425092089589547e0d76784c611d00f915a8c212f17d3b15",
|
||||
"live_protection_canonical_sha256": "2bc353f34689242cabebf89856d3faab0e1c496b5d33761dfb7cbd47904516f7",
|
||||
"maintenance_lock_required": true,
|
||||
"old_rules_remain_active_until_owner_approval_and_machine_gates_pass": true,
|
||||
"protected_branch_snapshot_sha256": "2bc353f34689242cabebf89856d3faab0e1c496b5d33761dfb7cbd47904516f7",
|
||||
"protection_update_method": "authenticated_admin_API_compare_and_swap",
|
||||
"reviewable_backup_file_hashes": {
|
||||
"app.ini": "0693e4fa8296a22533429339faeee2ac1856b6c966c6c1009bb139ab4784f687",
|
||||
"authoritative-aios.bundle": "4fb4ccc979c3061065cde451af0e733326cb56fa6f860124bb73f153a60907c9",
|
||||
"authoritative-refs.txt": "85cb664c8edf3902bebfc6c2253a3ee16cff8534f86561ba771a73df160186c6",
|
||||
"backup-metadata.json": "a38ef6dacb52ae61c4332bf8d087f479fb0d5f51d5f636a132a711bddd6e7732",
|
||||
"checksums.sha256": "72e2fe0b03d1011c872bb39a6cff489f08398ac7d52092f720cbbc63bcf6f753",
|
||||
"gitea-aios.bundle": "a343c7437f3a1a2aa842dd9d986a1d3cf80691058faae978565224320ff5ed90",
|
||||
"gitea-refs.txt": "1792b2c4f1cf1f232704601ee968e16425f926a6eaab1bdcddf273525d046e4d",
|
||||
"gitea.db": "aadcc1587a05827ed3b2c4ffc2403f26139020cd0328afaa03f7afbf6083a2f1",
|
||||
"governance-identities.json": "2d7b4e72d1fdf187425092089589547e0d76784c611d00f915a8c212f17d3b15",
|
||||
"protected-branch.json": "5cc4ae4fe5614baa3d4bb83d71872a4ad1ce3c5e39061e02b336be40d5b5b75d",
|
||||
"protected-tag.json": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
|
||||
"repository/active-runtime/app/aios_isolated_writer_policy.py": "d86ec23fd5d8b7e9844c0895c50385b6f8f3071c3dac3c0f26c70768ae00a188",
|
||||
"repository/active-runtime/app/isolated_writer_authz.py": "cfae1cdf70c1dabbe013e9365c308a45f11acf20035881a3bb6143774682bbf0",
|
||||
"repository/active-runtime/app/isolated_writer_path_policy.py": "c6c9ea8e45dfc03c4703503f4dc9c92f887bbacdbf1c7779a487567eee6c257c",
|
||||
"repository/active-runtime/app/silent_release_controller.py": "524a0a9961b15c2a35b669d1b89b2142377c13ddd3c721780f374331b7d2b6eb",
|
||||
"repository/active-runtime/app/silent_release_hardening.py": "524aba7034f2cd89d897fe01e9799c835bae9f11ca738182ce469aa59e05cc1a",
|
||||
"repository/services/aios-autonomous-release-candidate/aios_autonomous_release_candidate/approval.py": "cc937a81aef881206cb1d6fd2ad1feb1a1c2e0ad91c908095d56fd96402c3373",
|
||||
"repository/services/aios-autonomous-release-candidate/aios_autonomous_release_candidate/models.py": "aa3b3dd41c03c17e4e116ce444d9f4f779694a9cde684e8329cf892bd319069a",
|
||||
"repository/services/aios-autonomous-release-candidate/aios_autonomous_release_candidate/policy.py": "24b3f3326e6dcd1a4c376d892c53ad20c68e0dd6f861a3a34e11c9a2bba4c756",
|
||||
"repository/services/aios-autonomous-release-candidate/aios_autonomous_release_candidate/release.py": "db535e5006f8e4a3ab80c2f689213045c120b01cda9b43ef461a3022096c9d19",
|
||||
"repository/services/aios-autonomous-release-candidate/aios_autonomous_release_candidate/store.py": "31e1f3b904b0bfc8822d21bfd166a45388f122398d1ec76b803e90ee06dc92f0",
|
||||
"repository/services/aios-autonomous-release-candidate/aios_autonomous_release_candidate/workflow.py": "5f6a8e3a4d66f47717e95842f2eaa443e3cfa321cb08026cc82d74c4ad353bcf",
|
||||
"repository/tests/autonomous_release/test_core_contracts.py": "71852070db50a04901c141213cadc987ae17ec678fe27a901fee165b6d1c3e2a",
|
||||
"repository/tests/autonomous_release/test_external_gates.py": "91cca37eb1d40f0cf9fc38b12e2123045ffebd794a34d6cdef3f1dec470fffd2",
|
||||
"repository/tests/autonomous_release/test_release_gates.py": "d82b23ac00f5389f1a0de628bda9dff898d33f80d375cf6ff74f1531fd138134",
|
||||
"repository/tests/autonomous_release/test_state_and_workflow.py": "c0395a901732adb0b2f1402e6a1cf5202d54aa597bcaec9584a7ea7589c53917"
|
||||
},
|
||||
"reviewable_pre_migration_governance_identity_snapshot": [
|
||||
{
|
||||
"created_unix": 1783321252,
|
||||
"id": 1,
|
||||
"is_active": 1,
|
||||
"is_admin": 0,
|
||||
"last_login_unix": 0,
|
||||
"lower_name": "aios",
|
||||
"prohibit_login": 1,
|
||||
"type": 0
|
||||
},
|
||||
{
|
||||
"created_unix": 1783335380,
|
||||
"id": 2,
|
||||
"is_active": 1,
|
||||
"is_admin": 1,
|
||||
"last_login_unix": 1785086126,
|
||||
"lower_name": "owner",
|
||||
"prohibit_login": 0,
|
||||
"type": 0
|
||||
},
|
||||
{
|
||||
"created_unix": 1783411566,
|
||||
"id": 3,
|
||||
"is_active": 1,
|
||||
"is_admin": 1,
|
||||
"last_login_unix": 0,
|
||||
"lower_name": "gito",
|
||||
"prohibit_login": 0,
|
||||
"type": 0
|
||||
},
|
||||
{
|
||||
"created_unix": 1785078826,
|
||||
"id": 42,
|
||||
"is_active": 1,
|
||||
"is_admin": 0,
|
||||
"last_login_unix": 1785206253,
|
||||
"lower_name": "codex",
|
||||
"prohibit_login": 0,
|
||||
"type": 0
|
||||
},
|
||||
{
|
||||
"created_unix": 1785078882,
|
||||
"id": 43,
|
||||
"is_active": 1,
|
||||
"is_admin": 0,
|
||||
"last_login_unix": 1785206380,
|
||||
"lower_name": "gpt",
|
||||
"prohibit_login": 0,
|
||||
"type": 0
|
||||
}
|
||||
],
|
||||
"reviewable_pre_migration_protection_snapshot": {
|
||||
"protected_branch": [
|
||||
{
|
||||
"approvals_whitelist_team_i_ds": "null",
|
||||
"approvals_whitelist_user_i_ds": "[42,43]",
|
||||
"block_on_official_review_requests": 1,
|
||||
"block_on_outdated_branch": 1,
|
||||
"block_on_rejected_reviews": 1,
|
||||
"branch_name": "governance/approvals",
|
||||
"can_push": 0,
|
||||
"created_unix": 1785093201,
|
||||
"dismiss_stale_approvals": 1,
|
||||
"enable_approvals_whitelist": 1,
|
||||
"enable_merge_whitelist": 1,
|
||||
"enable_status_check": 0,
|
||||
"enable_whitelist": 0,
|
||||
"id": 1,
|
||||
"ignore_stale_approvals": 0,
|
||||
"merge_whitelist_team_i_ds": "null",
|
||||
"merge_whitelist_user_i_ds": "[42,43]",
|
||||
"protected_file_patterns": "",
|
||||
"repo_id": 1,
|
||||
"require_signed_commits": 0,
|
||||
"required_approvals": 2,
|
||||
"status_check_contexts": "[]",
|
||||
"unprotected_file_patterns": "",
|
||||
"updated_unix": 1785093422,
|
||||
"whitelist_deploy_keys": 0,
|
||||
"whitelist_team_i_ds": "null",
|
||||
"whitelist_user_i_ds": "null"
|
||||
},
|
||||
{
|
||||
"approvals_whitelist_team_i_ds": "null",
|
||||
"approvals_whitelist_user_i_ds": "[42,43]",
|
||||
"block_on_official_review_requests": 1,
|
||||
"block_on_outdated_branch": 1,
|
||||
"block_on_rejected_reviews": 1,
|
||||
"branch_name": "integration/chatgpt-autonomous-completion-20260726",
|
||||
"can_push": 0,
|
||||
"created_unix": 1785093201,
|
||||
"dismiss_stale_approvals": 1,
|
||||
"enable_approvals_whitelist": 1,
|
||||
"enable_merge_whitelist": 1,
|
||||
"enable_status_check": 0,
|
||||
"enable_whitelist": 0,
|
||||
"id": 3,
|
||||
"ignore_stale_approvals": 0,
|
||||
"merge_whitelist_team_i_ds": "null",
|
||||
"merge_whitelist_user_i_ds": "[42,43]",
|
||||
"protected_file_patterns": "",
|
||||
"repo_id": 1,
|
||||
"require_signed_commits": 0,
|
||||
"required_approvals": 2,
|
||||
"status_check_contexts": "[]",
|
||||
"unprotected_file_patterns": "",
|
||||
"updated_unix": 1785093422,
|
||||
"whitelist_deploy_keys": 0,
|
||||
"whitelist_team_i_ds": "null",
|
||||
"whitelist_user_i_ds": "null"
|
||||
},
|
||||
{
|
||||
"approvals_whitelist_team_i_ds": "null",
|
||||
"approvals_whitelist_user_i_ds": "[42,43]",
|
||||
"block_on_official_review_requests": 1,
|
||||
"block_on_outdated_branch": 1,
|
||||
"block_on_rejected_reviews": 1,
|
||||
"branch_name": "main",
|
||||
"can_push": 0,
|
||||
"created_unix": 1785093201,
|
||||
"dismiss_stale_approvals": 1,
|
||||
"enable_approvals_whitelist": 1,
|
||||
"enable_merge_whitelist": 1,
|
||||
"enable_status_check": 0,
|
||||
"enable_whitelist": 0,
|
||||
"id": 2,
|
||||
"ignore_stale_approvals": 0,
|
||||
"merge_whitelist_team_i_ds": "null",
|
||||
"merge_whitelist_user_i_ds": "[42,43]",
|
||||
"protected_file_patterns": "",
|
||||
"repo_id": 1,
|
||||
"require_signed_commits": 0,
|
||||
"required_approvals": 2,
|
||||
"status_check_contexts": "[]",
|
||||
"unprotected_file_patterns": "",
|
||||
"updated_unix": 1785093422,
|
||||
"whitelist_deploy_keys": 0,
|
||||
"whitelist_team_i_ds": "null",
|
||||
"whitelist_user_i_ds": "null"
|
||||
}
|
||||
],
|
||||
"protected_tag": []
|
||||
},
|
||||
"rollback_archive_members": {
|
||||
"pre-migration-backup/app.ini": "0693e4fa8296a22533429339faeee2ac1856b6c966c6c1009bb139ab4784f687",
|
||||
"pre-migration-backup/authoritative-aios.bundle": "4fb4ccc979c3061065cde451af0e733326cb56fa6f860124bb73f153a60907c9",
|
||||
"pre-migration-backup/authoritative-refs.txt": "85cb664c8edf3902bebfc6c2253a3ee16cff8534f86561ba771a73df160186c6",
|
||||
"pre-migration-backup/backup-metadata.json": "a38ef6dacb52ae61c4332bf8d087f479fb0d5f51d5f636a132a711bddd6e7732",
|
||||
"pre-migration-backup/checksums.sha256": "72e2fe0b03d1011c872bb39a6cff489f08398ac7d52092f720cbbc63bcf6f753",
|
||||
"pre-migration-backup/gitea-aios.bundle": "a343c7437f3a1a2aa842dd9d986a1d3cf80691058faae978565224320ff5ed90",
|
||||
"pre-migration-backup/gitea-refs.txt": "1792b2c4f1cf1f232704601ee968e16425f926a6eaab1bdcddf273525d046e4d",
|
||||
"pre-migration-backup/gitea.db": "aadcc1587a05827ed3b2c4ffc2403f26139020cd0328afaa03f7afbf6083a2f1",
|
||||
"pre-migration-backup/governance-identities.json": "2d7b4e72d1fdf187425092089589547e0d76784c611d00f915a8c212f17d3b15",
|
||||
"pre-migration-backup/protected-branch.json": "5cc4ae4fe5614baa3d4bb83d71872a4ad1ce3c5e39061e02b336be40d5b5b75d",
|
||||
"pre-migration-backup/protected-tag.json": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
|
||||
"pre-migration-backup/repository/active-runtime/app/aios_isolated_writer_policy.py": "d86ec23fd5d8b7e9844c0895c50385b6f8f3071c3dac3c0f26c70768ae00a188",
|
||||
"pre-migration-backup/repository/active-runtime/app/isolated_writer_authz.py": "cfae1cdf70c1dabbe013e9365c308a45f11acf20035881a3bb6143774682bbf0",
|
||||
"pre-migration-backup/repository/active-runtime/app/isolated_writer_path_policy.py": "c6c9ea8e45dfc03c4703503f4dc9c92f887bbacdbf1c7779a487567eee6c257c",
|
||||
"pre-migration-backup/repository/active-runtime/app/silent_release_controller.py": "524a0a9961b15c2a35b669d1b89b2142377c13ddd3c721780f374331b7d2b6eb",
|
||||
"pre-migration-backup/repository/active-runtime/app/silent_release_hardening.py": "524aba7034f2cd89d897fe01e9799c835bae9f11ca738182ce469aa59e05cc1a",
|
||||
"pre-migration-backup/repository/services/aios-autonomous-release-candidate/aios_autonomous_release_candidate/approval.py": "cc937a81aef881206cb1d6fd2ad1feb1a1c2e0ad91c908095d56fd96402c3373",
|
||||
"pre-migration-backup/repository/services/aios-autonomous-release-candidate/aios_autonomous_release_candidate/models.py": "aa3b3dd41c03c17e4e116ce444d9f4f779694a9cde684e8329cf892bd319069a",
|
||||
"pre-migration-backup/repository/services/aios-autonomous-release-candidate/aios_autonomous_release_candidate/policy.py": "24b3f3326e6dcd1a4c376d892c53ad20c68e0dd6f861a3a34e11c9a2bba4c756",
|
||||
"pre-migration-backup/repository/services/aios-autonomous-release-candidate/aios_autonomous_release_candidate/release.py": "db535e5006f8e4a3ab80c2f689213045c120b01cda9b43ef461a3022096c9d19",
|
||||
"pre-migration-backup/repository/services/aios-autonomous-release-candidate/aios_autonomous_release_candidate/store.py": "31e1f3b904b0bfc8822d21bfd166a45388f122398d1ec76b803e90ee06dc92f0",
|
||||
"pre-migration-backup/repository/services/aios-autonomous-release-candidate/aios_autonomous_release_candidate/workflow.py": "5f6a8e3a4d66f47717e95842f2eaa443e3cfa321cb08026cc82d74c4ad353bcf",
|
||||
"pre-migration-backup/repository/tests/autonomous_release/test_core_contracts.py": "71852070db50a04901c141213cadc987ae17ec678fe27a901fee165b6d1c3e2a",
|
||||
"pre-migration-backup/repository/tests/autonomous_release/test_external_gates.py": "91cca37eb1d40f0cf9fc38b12e2123045ffebd794a34d6cdef3f1dec470fffd2",
|
||||
"pre-migration-backup/repository/tests/autonomous_release/test_release_gates.py": "d82b23ac00f5389f1a0de628bda9dff898d33f80d375cf6ff74f1531fd138134",
|
||||
"pre-migration-backup/repository/tests/autonomous_release/test_state_and_workflow.py": "c0395a901732adb0b2f1402e6a1cf5202d54aa597bcaec9584a7ea7589c53917"
|
||||
},
|
||||
"rollback_configuration_path": "/var/lib/aios-gitops/runtime/tasks/AIOS-SINGLE-OWNER-GOVERNANCE-MIGRATION-20260729/rollback-configuration.tar",
|
||||
"rollback_configuration_sha256": "fc8dcf68aaf53edb0cda45833ee45ba5b8b205791af9073258ca617c8b8187d6",
|
||||
"rollback_restores_protection_before_validator": true
|
||||
},
|
||||
"auto_merge_after_owner_approval": false,
|
||||
"auto_merge_machine_conditions": [
|
||||
"owner_approval_valid",
|
||||
"head_oid_unchanged",
|
||||
"required_checks_pass",
|
||||
"secret_scan_pass",
|
||||
"evidence_closure_pass",
|
||||
"approval_not_expired",
|
||||
"protected_branch_rules_pass"
|
||||
],
|
||||
"automatic_renewal_policy": {
|
||||
"approval_ref_fast_forward_descendant_required": true,
|
||||
"changed_condition_fallback": "HUMAN_OWNER_APPROVAL_PENDING",
|
||||
"secret_scan": "PASS_REQUIRED",
|
||||
"single_renewal_max_hours": 12,
|
||||
"status": "PROPOSED_AFTER_MIGRATION",
|
||||
"total_window_max_days": 7,
|
||||
"unchanged_fields_required": [
|
||||
"original_policy_sha256",
|
||||
"allowed_paths",
|
||||
"allowed_operations",
|
||||
"denied_operations",
|
||||
"source_sha256",
|
||||
"source_blob_oid"
|
||||
]
|
||||
},
|
||||
"break_glass_executor_may_merge_only_after_owner_approval_machine_gates_and_atomic_cutover_preconditions": true,
|
||||
"break_glass_expires_at": "2026-07-29T16:22:46Z",
|
||||
"desired_branch_protection": {
|
||||
"approval_whitelist_user_ids": [
|
||||
2
|
||||
],
|
||||
"approval_whitelist_usernames": [
|
||||
"owner"
|
||||
],
|
||||
"block_on_official_review_requests": true,
|
||||
"block_on_outdated_branch": true,
|
||||
"block_on_rejected_reviews": true,
|
||||
"branch_deletion": false,
|
||||
"branch_patterns": [
|
||||
"governance/approvals",
|
||||
"main",
|
||||
"release/**",
|
||||
"integration/**"
|
||||
],
|
||||
"direct_push": false,
|
||||
"dismiss_stale_approvals": true,
|
||||
"force_push": false,
|
||||
"require_status_checks": true,
|
||||
"required_approvals": 1,
|
||||
"required_status_contexts": [
|
||||
"aios/machine-gates",
|
||||
"aios/secret-scan",
|
||||
"aios/evidence-closure"
|
||||
],
|
||||
"wip_blocks_merge": true
|
||||
},
|
||||
"executor_must_not_approve": true,
|
||||
"expected_governance_base_oid": "ac2dc5c30eb35748ef11c98297812729d0fd5828",
|
||||
"expires_at": "2026-07-29T16:22:46Z",
|
||||
"global_constraints": {
|
||||
"AIOS_GITOPS_WRITES_ENABLED": false,
|
||||
"approval_impersonation": false,
|
||||
"direct_main_push": false,
|
||||
"fake_review": false,
|
||||
"force_push": false,
|
||||
"production_change_without_owner_approval": false,
|
||||
"secret_commit": false,
|
||||
"tag_rewrite": false
|
||||
},
|
||||
"governance_model": "SINGLE_OWNER_WITH_MACHINE_GATES_V1",
|
||||
"historical_policy": {
|
||||
"historical_manifests_modified": false,
|
||||
"old_policy_decoder_retained_read_only": true,
|
||||
"requests_created_before_policy_effective_at_use_old_policy": true
|
||||
},
|
||||
"issued_at": "2026-07-28T16:22:46Z",
|
||||
"machine_gates": {
|
||||
"development_branch": [
|
||||
"allowed_branch",
|
||||
"allowed_paths",
|
||||
"compileall",
|
||||
"targeted_tests",
|
||||
"secret_scan",
|
||||
"git_diff_check"
|
||||
],
|
||||
"production": [
|
||||
"owner_approval",
|
||||
"tag_consistency",
|
||||
"registry_consistency",
|
||||
"deployment_plan",
|
||||
"rollback_plan",
|
||||
"artifact_integrity",
|
||||
"production_preflight"
|
||||
],
|
||||
"protected_branch_merge": [
|
||||
"expected_base_oid",
|
||||
"current_head_oid",
|
||||
"ordinary_merge_commit",
|
||||
"parent_order_verified",
|
||||
"artifact_binding",
|
||||
"audit_chain_valid"
|
||||
],
|
||||
"release": [
|
||||
"version_manifest",
|
||||
"explicit_release_target_oid",
|
||||
"artifact_sha256",
|
||||
"rollback_artifact_sha256",
|
||||
"sbom",
|
||||
"test_evidence",
|
||||
"evidence_closure"
|
||||
]
|
||||
},
|
||||
"merge_mode": "CREATE_MERGE_COMMIT",
|
||||
"migration_acceptance_tests": [
|
||||
"agent_request_pass",
|
||||
"owner_current_head_approval_pass",
|
||||
"machine_gates_then_merge_commit_pass",
|
||||
"agent_self_approval_denied",
|
||||
"non_owner_approval_denied",
|
||||
"stale_head_approval_denied",
|
||||
"wip_merge_denied",
|
||||
"machine_test_failure_merge_denied",
|
||||
"secret_scan_failure_merge_denied",
|
||||
"main_direct_write_denied",
|
||||
"tag_direct_write_denied",
|
||||
"force_push_denied",
|
||||
"missing_tag_target_oid_denied",
|
||||
"missing_evidence_closure_oid_denied",
|
||||
"production_without_owner_denied",
|
||||
"dormant_only_production_activation_denied"
|
||||
],
|
||||
"migration_mode": "OWNER_BREAK_GLASS_GOVERNANCE_RESET",
|
||||
"new_policy": "SINGLE_OWNER_WITH_MACHINE_GATES_V1",
|
||||
"old_policy": "DUAL_HUMAN_APPROVAL",
|
||||
"owner_approval_contract": {
|
||||
"dismissed": false,
|
||||
"does_not_claim_old_dual_approval_compliance": true,
|
||||
"head_change_invalidates_owner_approval": true,
|
||||
"owner_review_is_break_glass_signature": true,
|
||||
"review_created_before_approval_expires_at": true,
|
||||
"review_state": "APPROVED",
|
||||
"reviewed_head_oid_must_equal_current_pr_head_oid": true,
|
||||
"reviewer_must_equal": "owner",
|
||||
"stale": false
|
||||
},
|
||||
"owner_break_glass_declaration_to_be_attested_by_review": {
|
||||
"attestation_status": "PENDING_OWNER_REVIEW",
|
||||
"expires_at": "2026-07-29T16:22:46Z",
|
||||
"new_policy": "SINGLE_OWNER_WITH_MACHINE_GATES_V1",
|
||||
"old_policy": "DUAL_HUMAN_APPROVAL",
|
||||
"owner_acknowledges_old_policy_exception": true,
|
||||
"reason": "single-maintainer-system",
|
||||
"scope": "governance-policy-migration-only"
|
||||
},
|
||||
"owner_is_only_human_approver": true,
|
||||
"policy_effective_at": null,
|
||||
"pre_approval_non_authorizations": {
|
||||
"approval_validator_update_allowed": false,
|
||||
"gitea_branch_protection_update_allowed": false,
|
||||
"merge_allowed": false,
|
||||
"old_dual_rule_disable_allowed": false,
|
||||
"production_change_allowed": false,
|
||||
"state_machine_update_allowed": false,
|
||||
"writer_credential_issue_allowed": false
|
||||
},
|
||||
"prohibited_merge_modes": [
|
||||
"squash",
|
||||
"rebase",
|
||||
"force",
|
||||
"fast_forward_policy_bypass"
|
||||
],
|
||||
"reason": "single-maintainer-system",
|
||||
"repository": "aios/aios",
|
||||
"request_status": "HUMAN_OWNER_APPROVAL_PENDING",
|
||||
"requester_must_not_approve": true,
|
||||
"requesters": [
|
||||
"codex-chartgpt-bridge",
|
||||
"aios-governance-agent",
|
||||
"aios-release-agent",
|
||||
"aios-governed-development-writer"
|
||||
],
|
||||
"required_distinct_human_approvers": 1,
|
||||
"required_human_approvals": 1,
|
||||
"required_owner_approvals": 1,
|
||||
"resolved_owner": {
|
||||
"confidence_before_review": "MEDIUM",
|
||||
"confirmation_rule": "valid APPROVED review by owner on this exact immutable head",
|
||||
"email_sha256": "f5dc2928f6202c24fa4db8bb33db84e1b77c6eff30d1b95abdadeaae209bc4c9",
|
||||
"identity_source": [
|
||||
"gitea_repository_owner_account_aios_is_login_prohibited",
|
||||
"all_login_enabled_site_admins_enumerated_owner_and_never_logged_in_service_gito",
|
||||
"only_interactive_site_admin_named_owner",
|
||||
"gitea_owner_last_login_present",
|
||||
"historical_governance_records_use_approved_by_role_owner",
|
||||
"user_declared_single_natural_person_maintainer"
|
||||
],
|
||||
"login_enabled_site_admins": [
|
||||
{
|
||||
"observed_prior_login": true,
|
||||
"user_id": 2,
|
||||
"username": "owner"
|
||||
},
|
||||
{
|
||||
"classification": "service_admin_not_human_approver",
|
||||
"observed_prior_login": false,
|
||||
"user_id": 3,
|
||||
"username": "gito"
|
||||
}
|
||||
],
|
||||
"user_id": 2,
|
||||
"username": "owner"
|
||||
},
|
||||
"schema": "aios.single-owner-governance-policy/v1",
|
||||
"scope": "governance-policy-migration-only",
|
||||
"standing_autonomous_development_policy": {
|
||||
"allowed_without_repeated_owner_approval": [
|
||||
"patch_branch_create",
|
||||
"allowlisted_development_write",
|
||||
"patch_branch_commit_and_push",
|
||||
"test_execute",
|
||||
"artifact_build",
|
||||
"approval_pr_create",
|
||||
"read_only_evidence",
|
||||
"task_writer_renewal"
|
||||
],
|
||||
"owner_approval_still_required": [
|
||||
"protected_branch_merge",
|
||||
"tag",
|
||||
"registry_update",
|
||||
"production_baseline_update",
|
||||
"production_deploy",
|
||||
"production_rollback",
|
||||
"governance_policy_change"
|
||||
],
|
||||
"status": "PROPOSED_AFTER_MIGRATION"
|
||||
},
|
||||
"standing_single_owner_governance_migration_authorization": true,
|
||||
"state_machine": {
|
||||
"auto_merge_ready_state": "AUTO_MERGE_READY",
|
||||
"execution_ready_state": "OWNER_APPROVED_EXECUTION_READY",
|
||||
"machine_verified_state": "MACHINE_GATES_VERIFIED",
|
||||
"owner_approval_alone_is_not_execution_ready": true,
|
||||
"owner_verified_state": "OWNER_APPROVAL_VERIFIED",
|
||||
"pending_state": "HUMAN_OWNER_APPROVAL_PENDING"
|
||||
},
|
||||
"task_id": "AIOS-SINGLE-OWNER-GOVERNANCE-MIGRATION-20260729",
|
||||
"this_manifest_authorizes_only_governance_policy_migration": true,
|
||||
"this_manifest_grants_standing_development_authorization": false
|
||||
}
|
||||
+49
@@ -0,0 +1,49 @@
|
||||
{
|
||||
"schema": "aios.gitea-native-approval/v1",
|
||||
"approval_type": "IMPLEMENTATION",
|
||||
"task_id": "AIOS-INTEGRATION-REFRESH-PROVISIONER-IMPLEMENTATION-20260726",
|
||||
"repository": "aios/aios",
|
||||
"branch": "patch/integration-refresh-grant-provisioner-20260726",
|
||||
"base_oid": "875d41c3101093149414292067fd700dc214d4d3",
|
||||
"actor_identity": "chatgpt-gitops-bot",
|
||||
"writer_audience": "aios-integration-refresh-provisioner-development-writer",
|
||||
"risk": "high",
|
||||
"allowed_paths": [
|
||||
"active-runtime/app/**",
|
||||
"tests/test_integration_refresh_provisioner.py",
|
||||
"tests/test_isolated_writer_integration_merge.py",
|
||||
"tests/test_gitea_branch_read_projection.py",
|
||||
"tests/test_isolated_writer_token_policy_v2.py",
|
||||
"deploy/systemd/aios-integration-refresh-*.service",
|
||||
"docs/autonomous-release/**"
|
||||
],
|
||||
"allowed_operations": [
|
||||
"repository.source_map.read",
|
||||
"runtime.unit_metadata.read",
|
||||
"git.ref.read",
|
||||
"git.diff.read",
|
||||
"git.branch.patch.create",
|
||||
"commit_file.atomic",
|
||||
"git.push.patch_branch",
|
||||
"test.execute.allowlisted",
|
||||
"artifact.build.control_plane_patch",
|
||||
"evidence.write.task_runtime"
|
||||
],
|
||||
"denied_operations": [
|
||||
"main.write",
|
||||
"main.merge",
|
||||
"tag.write",
|
||||
"tag.delete",
|
||||
"tag.rewrite",
|
||||
"force_push",
|
||||
"history.rewrite",
|
||||
"baseline.write",
|
||||
"registry.write",
|
||||
"production.deploy",
|
||||
"production.rollback",
|
||||
"secret.read",
|
||||
"secret.export",
|
||||
"bare_ref_direct_write"
|
||||
],
|
||||
"expires_at": "2026-07-27T03:35:55Z"
|
||||
}
|
||||
+49
@@ -0,0 +1,49 @@
|
||||
{
|
||||
"schema": "aios.gitea-native-approval/v1",
|
||||
"approval_type": "IMPLEMENTATION",
|
||||
"task_id": "AIOS-INTEGRATION-REFRESH-PROVISIONER-IMPLEMENTATION-20260726",
|
||||
"repository": "aios/aios",
|
||||
"branch": "patch/integration-refresh-grant-provisioner-20260726",
|
||||
"base_oid": "875d41c3101093149414292067fd700dc214d4d3",
|
||||
"actor_identity": "chatgpt-gitops-bot",
|
||||
"writer_audience": "aios-integration-refresh-provisioner-development-writer",
|
||||
"risk": "high",
|
||||
"allowed_paths": [
|
||||
"active-runtime/app/**",
|
||||
"tests/test_integration_refresh_provisioner.py",
|
||||
"tests/test_isolated_writer_integration_merge.py",
|
||||
"tests/test_gitea_branch_read_projection.py",
|
||||
"tests/test_isolated_writer_token_policy_v2.py",
|
||||
"deploy/systemd/aios-integration-refresh-*.service",
|
||||
"docs/autonomous-release/**"
|
||||
],
|
||||
"allowed_operations": [
|
||||
"repository.source_map.read",
|
||||
"runtime.unit_metadata.read",
|
||||
"git.ref.read",
|
||||
"git.diff.read",
|
||||
"git.branch.patch.create",
|
||||
"commit_file.atomic",
|
||||
"git.push.patch_branch",
|
||||
"test.execute.allowlisted",
|
||||
"artifact.build.control_plane_patch",
|
||||
"evidence.write.task_runtime"
|
||||
],
|
||||
"denied_operations": [
|
||||
"main.write",
|
||||
"main.merge",
|
||||
"tag.write",
|
||||
"tag.delete",
|
||||
"tag.rewrite",
|
||||
"force_push",
|
||||
"history.rewrite",
|
||||
"baseline.write",
|
||||
"registry.write",
|
||||
"production.deploy",
|
||||
"production.rollback",
|
||||
"secret.read",
|
||||
"secret.export",
|
||||
"bare_ref_direct_write"
|
||||
],
|
||||
"expires_at": "2026-07-27T13:00:11Z"
|
||||
}
|
||||
+151
@@ -0,0 +1,151 @@
|
||||
{
|
||||
"actor_identity": "chatgpt-gitops-bot",
|
||||
"allowed_operations": [
|
||||
"runtime.source.read.exact",
|
||||
"runtime.source.hash",
|
||||
"runtime.unit_metadata.read",
|
||||
"package.ownership.read",
|
||||
"git.object.lookup.by_blob_oid",
|
||||
"secret.scan.local_source",
|
||||
"repository.source_import.byte_exact",
|
||||
"repository.module.create",
|
||||
"source_map.evidence.write",
|
||||
"git.branch.patch.create",
|
||||
"commit_file.atomic",
|
||||
"git.push.patch_branch",
|
||||
"test.execute.allowlisted",
|
||||
"artifact.build.control_plane_patch",
|
||||
"evidence.write.task_runtime"
|
||||
],
|
||||
"allowed_paths": [
|
||||
"active-runtime/app/aios_autonomous_release_authorization/**",
|
||||
"active-runtime/app/aios_chatgpt_action_task_router/**",
|
||||
"active-runtime/app/aios_isolated_writer_api/**",
|
||||
"tests/test_integration_refresh_provisioner.py",
|
||||
"tests/test_isolated_writer_integration_merge.py",
|
||||
"tests/test_gitea_branch_read_projection.py",
|
||||
"tests/test_isolated_writer_token_policy_v2.py",
|
||||
"deploy/systemd/aios-integration-refresh-*.service",
|
||||
"docs/autonomous-release/**"
|
||||
],
|
||||
"allowed_runtime_reads": [
|
||||
"/usr/libexec/aios-autonomous-release-authorization/controller.py",
|
||||
"/usr/libexec/aios-chatgpt-action-task-router-continuous-20260726/action_task_router.py"
|
||||
],
|
||||
"approval_type": "SOURCE_MAPPING_AND_IMPORT",
|
||||
"canonical_paths": {
|
||||
"controller": "active-runtime/app/aios_autonomous_release_authorization/controller.py",
|
||||
"isolated_writer_api": "active-runtime/app/aios_isolated_writer_api/**",
|
||||
"router": "active-runtime/app/aios_chatgpt_action_task_router/action_task_router.py"
|
||||
},
|
||||
"denied_operations": [
|
||||
"secret.value.return",
|
||||
"secret.matching_line.return",
|
||||
"secret.context.return",
|
||||
"runtime.source.modify",
|
||||
"runtime.unit.modify",
|
||||
"arbitrary_path.read",
|
||||
"secret.read.by_agent",
|
||||
"secret.export",
|
||||
"main.write",
|
||||
"main.merge",
|
||||
"governance.approvals.direct_write",
|
||||
"integration.write",
|
||||
"tag.write",
|
||||
"tag.delete",
|
||||
"tag.rewrite",
|
||||
"force_push",
|
||||
"history.rewrite",
|
||||
"bare_ref_direct_write",
|
||||
"baseline.write",
|
||||
"registry.write",
|
||||
"production.deploy",
|
||||
"production.rollback"
|
||||
],
|
||||
"expires_at": "2026-07-27T22:36:27Z",
|
||||
"extends_approval_merge_oid": "45f3226662965c8d41ec1775e8c8857f2b986397",
|
||||
"finding_policy": {
|
||||
"controller_byte_exact_import_allowed": true,
|
||||
"controller_finding_status": "PASS",
|
||||
"controller_sanitized_source_creation_allowed": false,
|
||||
"finding_classification_by_automation": "PROHIBITED",
|
||||
"isolated_writer_api_module_create_allowed": true,
|
||||
"matching_source_lines_disclosed": false,
|
||||
"router_byte_exact_import_allowed": true,
|
||||
"router_finding_status": "PASS",
|
||||
"router_sanitized_source_creation_allowed": false,
|
||||
"scan_decision": "MAPPING_IMPORT_ONLY",
|
||||
"secret_content_disclosed": false
|
||||
},
|
||||
"implementation_base_oid": "875d41c3101093149414292067fd700dc214d4d3",
|
||||
"implementation_branch": "patch/integration-refresh-grant-provisioner-20260726",
|
||||
"issued_at": "2026-07-27T10:36:27Z",
|
||||
"repository": "aios/aios",
|
||||
"required_controls": {
|
||||
"controller_finding_must_be_resolved": false,
|
||||
"controller_import_blob_must_equal_runtime_blob": true,
|
||||
"controller_import_must_be_byte_exact": true,
|
||||
"controller_import_sha256_must_equal_runtime_sha256": true,
|
||||
"dual_human_approval_required": true,
|
||||
"global_gitops_writes_must_remain_disabled": true,
|
||||
"new_task_bound_grant_required": true,
|
||||
"old_implementation_token_auth_status": 401,
|
||||
"old_implementation_writer_revoked": true,
|
||||
"old_secret_revocation_must_be_verified": false,
|
||||
"router_finding_must_be_resolved": false,
|
||||
"router_import_blob_must_equal_runtime_blob": true,
|
||||
"router_import_must_be_byte_exact": true,
|
||||
"router_import_sha256_must_equal_runtime_sha256": true,
|
||||
"sanitized_source_behavioral_equivalence_required": false,
|
||||
"sanitized_source_secret_scan_must_pass": false,
|
||||
"true_positive_requires_rotation": false
|
||||
},
|
||||
"risk": "high",
|
||||
"runtime_source_evidence": {
|
||||
"controller": {
|
||||
"finding_count": 0,
|
||||
"findings": [],
|
||||
"git_blob_oid": "9c68be36e0b6a18260e3099e3d7b0dcab6d814aa",
|
||||
"group_gid": 0,
|
||||
"mode": "0o700",
|
||||
"mtime_ns": 1785043975531298047,
|
||||
"owner_uid": 0,
|
||||
"package_owner": "UNPACKAGED",
|
||||
"referencing_systemd_units": [
|
||||
"aios-autonomous-release-authorization-controller.service"
|
||||
],
|
||||
"resolved_path": "/usr/libexec/aios-autonomous-release-authorization/controller.py",
|
||||
"secret_content_returned": false,
|
||||
"secret_scan": "PASS",
|
||||
"sha256": "39b532c3058c4ae49ed691d7c66c3b7745a25111fa61039f388c8335fd262c50",
|
||||
"size": 24204,
|
||||
"utf8_python": true
|
||||
},
|
||||
"isolated_writer_api": {
|
||||
"canonical_source_path": "NOT_ESTABLISHED",
|
||||
"runtime_module_status": "ABSENT"
|
||||
},
|
||||
"router": {
|
||||
"finding_count": 0,
|
||||
"findings": [],
|
||||
"git_blob_oid": "ee0595ca61a0598f8cb3dc10865a255e21679b2a",
|
||||
"group_gid": 0,
|
||||
"mode": "0o644",
|
||||
"mtime_ns": 1785042222580725999,
|
||||
"owner_uid": 0,
|
||||
"package_owner": "UNPACKAGED",
|
||||
"referencing_systemd_units": [
|
||||
"aios-chatgpt-autonomous-release-route.service"
|
||||
],
|
||||
"resolved_path": "/usr/libexec/aios-chatgpt-action-task-router-continuous-20260726/action_task_router.py",
|
||||
"secret_content_returned": false,
|
||||
"secret_scan": "PASS",
|
||||
"sha256": "738fb7035a6b792115d6467b4d1e4ffd47a8c5d5646f7032ebc68ea587cbd554",
|
||||
"size": 25952,
|
||||
"utf8_python": true
|
||||
}
|
||||
},
|
||||
"schema": "aios.gitea-native-approval/v1",
|
||||
"task_id": "AIOS-INTEGRATION-REFRESH-PROVISIONER-IMPLEMENTATION-20260726",
|
||||
"writer_audience": "aios-integration-refresh-provisioner-development-writer"
|
||||
}
|
||||
+157
@@ -0,0 +1,157 @@
|
||||
{
|
||||
"actor_identity": "chatgpt-gitops-bot",
|
||||
"allowed_operations": [
|
||||
"runtime.source.read.exact",
|
||||
"runtime.source.hash",
|
||||
"runtime.unit_metadata.read",
|
||||
"package.ownership.read",
|
||||
"git.object.lookup.by_blob_oid",
|
||||
"secret.scan.local_source",
|
||||
"repository.source_import.byte_exact",
|
||||
"repository.module.create",
|
||||
"source_map.evidence.write",
|
||||
"git.branch.patch.create",
|
||||
"commit_file.atomic",
|
||||
"git.push.patch_branch",
|
||||
"test.execute.allowlisted",
|
||||
"artifact.build.control_plane_patch",
|
||||
"evidence.write.task_runtime"
|
||||
],
|
||||
"allowed_paths": [
|
||||
"active-runtime/app/aios_autonomous_release_authorization/**",
|
||||
"active-runtime/app/aios_chatgpt_action_task_router/**",
|
||||
"active-runtime/app/aios_isolated_writer_api/**",
|
||||
"tests/test_integration_refresh_provisioner.py",
|
||||
"tests/test_isolated_writer_integration_merge.py",
|
||||
"tests/test_gitea_branch_read_projection.py",
|
||||
"tests/test_isolated_writer_token_policy_v2.py",
|
||||
"deploy/systemd/aios-integration-refresh-*.service",
|
||||
"docs/autonomous-release/**"
|
||||
],
|
||||
"allowed_runtime_reads": [
|
||||
"/usr/libexec/aios-autonomous-release-authorization/controller.py",
|
||||
"/usr/libexec/aios-chatgpt-action-task-router-continuous-20260726/action_task_router.py"
|
||||
],
|
||||
"approval_type": "SOURCE_MAPPING_AND_IMPORT_EXECUTION_RENEWAL",
|
||||
"canonical_paths": {
|
||||
"controller": "active-runtime/app/aios_autonomous_release_authorization/controller.py",
|
||||
"isolated_writer_api": "active-runtime/app/aios_isolated_writer_api/**",
|
||||
"router": "active-runtime/app/aios_chatgpt_action_task_router/action_task_router.py"
|
||||
},
|
||||
"denied_operations": [
|
||||
"secret.value.return",
|
||||
"secret.matching_line.return",
|
||||
"secret.context.return",
|
||||
"runtime.source.modify",
|
||||
"runtime.unit.modify",
|
||||
"arbitrary_path.read",
|
||||
"secret.read.by_agent",
|
||||
"secret.export",
|
||||
"main.write",
|
||||
"main.merge",
|
||||
"governance.approvals.direct_write",
|
||||
"integration.write",
|
||||
"tag.write",
|
||||
"tag.delete",
|
||||
"tag.rewrite",
|
||||
"force_push",
|
||||
"history.rewrite",
|
||||
"bare_ref_direct_write",
|
||||
"baseline.write",
|
||||
"registry.write",
|
||||
"production.deploy",
|
||||
"production.rollback"
|
||||
],
|
||||
"expected_governance_base_oid": "9514062cf33e45941abe4a8c98377e0713b0b3f6",
|
||||
"expires_at": "2026-07-28T18:10:44Z",
|
||||
"extends_approval_merge_oid": "45f3226662965c8d41ec1775e8c8857f2b986397",
|
||||
"finding_policy": {
|
||||
"controller_byte_exact_import_allowed": true,
|
||||
"controller_finding_status": "PASS",
|
||||
"controller_sanitized_source_creation_allowed": false,
|
||||
"finding_classification_by_automation": "PROHIBITED",
|
||||
"isolated_writer_api_module_create_allowed": true,
|
||||
"matching_source_lines_disclosed": false,
|
||||
"router_byte_exact_import_allowed": true,
|
||||
"router_finding_status": "PASS",
|
||||
"router_sanitized_source_creation_allowed": false,
|
||||
"scan_decision": "MAPPING_IMPORT_ONLY",
|
||||
"secret_content_disclosed": false
|
||||
},
|
||||
"implementation_base_oid": "875d41c3101093149414292067fd700dc214d4d3",
|
||||
"implementation_branch": "patch/integration-refresh-grant-provisioner-20260726",
|
||||
"issued_at": "2026-07-28T06:10:44Z",
|
||||
"renews_source_mapping_approval_merge_oid": "9514062cf33e45941abe4a8c98377e0713b0b3f6",
|
||||
"renews_source_mapping_manifest_blob_oid": "00c5e90e529cb98c7750a5761b0a0d6ccf507174",
|
||||
"renews_source_mapping_manifest_sha256": "17af8ebc43673c0e46bb9ab1bf44497e8c147f1b4a9d7a8a404edd5c0abd6f21",
|
||||
"repository": "aios/aios",
|
||||
"required_controls": {
|
||||
"controller_finding_must_be_resolved": false,
|
||||
"controller_import_blob_must_equal_runtime_blob": true,
|
||||
"controller_import_must_be_byte_exact": true,
|
||||
"controller_import_sha256_must_equal_runtime_sha256": true,
|
||||
"dual_human_approval_required": true,
|
||||
"global_gitops_writes_must_remain_disabled": true,
|
||||
"new_task_bound_grant_required": true,
|
||||
"old_implementation_token_auth_status": 401,
|
||||
"old_implementation_writer_revoked": true,
|
||||
"old_secret_revocation_must_be_verified": false,
|
||||
"router_finding_must_be_resolved": false,
|
||||
"router_import_blob_must_equal_runtime_blob": true,
|
||||
"router_import_must_be_byte_exact": true,
|
||||
"router_import_sha256_must_equal_runtime_sha256": true,
|
||||
"sanitized_source_behavioral_equivalence_required": false,
|
||||
"sanitized_source_secret_scan_must_pass": false,
|
||||
"true_positive_requires_rotation": false
|
||||
},
|
||||
"risk": "high",
|
||||
"runtime_source_evidence": {
|
||||
"controller": {
|
||||
"finding_count": 0,
|
||||
"findings": [],
|
||||
"git_blob_oid": "9c68be36e0b6a18260e3099e3d7b0dcab6d814aa",
|
||||
"group_gid": 0,
|
||||
"mode": "0o700",
|
||||
"mtime_ns": 1785043975531298047,
|
||||
"owner_uid": 0,
|
||||
"package_owner": "UNPACKAGED",
|
||||
"referencing_systemd_units": [
|
||||
"aios-autonomous-release-authorization-controller.service"
|
||||
],
|
||||
"resolved_path": "/usr/libexec/aios-autonomous-release-authorization/controller.py",
|
||||
"secret_content_returned": false,
|
||||
"secret_scan": "PASS",
|
||||
"sha256": "39b532c3058c4ae49ed691d7c66c3b7745a25111fa61039f388c8335fd262c50",
|
||||
"size": 24204,
|
||||
"utf8_python": true
|
||||
},
|
||||
"isolated_writer_api": {
|
||||
"canonical_source_path": "NOT_ESTABLISHED",
|
||||
"runtime_module_status": "ABSENT"
|
||||
},
|
||||
"observed_at": "2026-07-28T05:53:01.253909Z",
|
||||
"router": {
|
||||
"finding_count": 0,
|
||||
"findings": [],
|
||||
"git_blob_oid": "ee0595ca61a0598f8cb3dc10865a255e21679b2a",
|
||||
"group_gid": 0,
|
||||
"mode": "0o644",
|
||||
"mtime_ns": 1785042222580725999,
|
||||
"owner_uid": 0,
|
||||
"package_owner": "UNPACKAGED",
|
||||
"referencing_systemd_units": [
|
||||
"aios-chatgpt-autonomous-release-route.service"
|
||||
],
|
||||
"resolved_path": "/usr/libexec/aios-chatgpt-action-task-router-continuous-20260726/action_task_router.py",
|
||||
"secret_content_returned": false,
|
||||
"secret_scan": "PASS",
|
||||
"sha256": "738fb7035a6b792115d6467b4d1e4ffd47a8c5d5646f7032ebc68ea587cbd554",
|
||||
"size": 25952,
|
||||
"utf8_python": true
|
||||
}
|
||||
},
|
||||
"runtime_source_read_projection_evidence_sha256": "23f03d9c6abe6f11e1acda5210b8708696e3b77caa0083b6bfaef30a44b2a35f",
|
||||
"schema": "aios.gitea-native-approval/v1",
|
||||
"task_id": "AIOS-INTEGRATION-REFRESH-PROVISIONER-IMPLEMENTATION-20260726",
|
||||
"writer_audience": "aios-integration-refresh-provisioner-development-writer"
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
# Isolated Writer Exact-Path Policy V1
|
||||
|
||||
This policy is an additional, service-owned authorization layer for
|
||||
`POST /api/commit-file`. It never expands a Token Policy V2 grant.
|
||||
|
||||
An atomic write is permitted only when all of the following agree:
|
||||
|
||||
1. socket peer, audience, operation, branch, base and exact path in the
|
||||
Token Policy V2 grant;
|
||||
2. service audience, repository identity, branch, operation and exact path
|
||||
in `AIOS_GITOPS_PATH_POLICY_FILE`;
|
||||
3. lexical path, resolved containment and symlink checks;
|
||||
4. the configured content-size and content-profile checks;
|
||||
5. operation digest, expected branch HEAD and durable operation ledger.
|
||||
|
||||
Any failed layer returns a fail-closed policy error. Coarse `/api/file` and
|
||||
`/api/commit` routes remain forbidden.
|
||||
|
||||
## File contract
|
||||
|
||||
The configured path must be absolute. The parent directory must be
|
||||
`root:gitops 0750`; the regular, non-symlink file must be
|
||||
`root:gitops 0640`. JSON must use UTF-8 canonical encoding with sorted keys,
|
||||
compact separators and one trailing newline. Unknown fields, unsupported
|
||||
operations or profiles, duplicate paths, oversized policies and
|
||||
group/world-writable files are rejected during Writer startup.
|
||||
|
||||
Example:
|
||||
|
||||
```json
|
||||
{"allowed_branch":"patch/codex-review-test-worker-20260724","allowed_operations":{"commit_file.atomic":{"content_profile":"systemd-codex-review-worker-v1","exact_paths":["deploy/systemd/aios-codex-review-test-worker.service"],"max_content_bytes":32768}},"audience":"aios-codex-review-worker-writer.service","policy_id":"codex-review-worker-path-policy-v1","repository_identity":"aios/aios","schema_version":1}
|
||||
```
|
||||
|
||||
The SHA-256 of the canonical policy is exposed in the isolated runtime
|
||||
status and written to the operation audit record. The request body cannot
|
||||
select or override the policy file.
|
||||
|
||||
## Content profile
|
||||
|
||||
`systemd-codex-review-worker-v1` is an exact, fail-closed unit grammar. The
|
||||
only sections are `[Unit]`, `[Service]` and `[Install]`. Each section has a
|
||||
directive allowlist; every single-value directive and every repeated
|
||||
`Environment`, `ReadOnlyPaths` and `ReadWritePaths` entry must match the
|
||||
governed Shadow unit contract exactly.
|
||||
|
||||
The profile fixes the Python module
|
||||
`task_plane.workers.codex_review_test`, `User=aiman`, `Group=gitops`,
|
||||
the controlled `/usr/libexec/aios-codex-review-test-worker/current`
|
||||
working directory, all hardening switches and the three permitted
|
||||
environment variables. It grants read access only to the authoritative
|
||||
bare repository and the versioned worker artifact, and write access only
|
||||
to the worker runtime, state and cache directories.
|
||||
|
||||
Unknown sections or directives are rejected, including lifecycle hooks,
|
||||
credential injection, environment imports, root images, bind mounts,
|
||||
capability expansion, devices, sockets and top-level writable paths. The
|
||||
parser also rejects duplicate sections, duplicate single-value directives,
|
||||
empty resets, continuation lines, CRLF, NUL, non-UTF-8 surrogate content,
|
||||
overlong lines and Unicode lookalike directive names. It does not use
|
||||
`ConfigParser(strict=False)` or silently merge repeated values.
|
||||
|
||||
Production credentials, if later authorized, must be supplied by a
|
||||
separately governed deployment drop-in. They are not permitted in the
|
||||
source unit or this content profile.
|
||||
|
||||
Policy changes require a separate maintenance branch and Owner-reviewed
|
||||
ordinary merge commit. A task-scoped Writer must not edit its own policy
|
||||
file.
|
||||
@@ -51,6 +51,31 @@ components:
|
||||
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:
|
||||
@@ -428,6 +453,25 @@ paths:
|
||||
schema: {$ref: '#/components/schemas/FileCommitRequest'}
|
||||
responses: {'200': {description: File committed}, '400': {description: Secret rejected or invalid path}, '401': {description: Unauthorized}}
|
||||
x-openai-isConsequential: true
|
||||
/api/commit-file:
|
||||
post:
|
||||
operationId: commitFileAtomic
|
||||
summary: Atomically write one allowlisted file, commit, push, and verify using Token Policy V2
|
||||
security: [{bearerAuth: []}]
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema: {$ref: '#/components/schemas/AtomicFileCommitRequest'}
|
||||
responses:
|
||||
'200': {description: Completed or idempotently replayed}
|
||||
'400': {description: Invalid canonical request}
|
||||
'401': {description: Missing, invalid, expired, or revoked Token Policy V2 grant}
|
||||
'403': {description: Audience, source, branch, operation, path, or policy denied}
|
||||
'409': {description: Idempotency conflict or manual review required}
|
||||
'413': {description: Request too large}
|
||||
'503': {description: Push or remote verification unavailable; recoverable}
|
||||
x-openai-isConsequential: true
|
||||
/api/commit:
|
||||
post:
|
||||
operationId: commitChanges
|
||||
|
||||
@@ -0,0 +1,417 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
import tempfile
|
||||
import unittest
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from pathlib import Path
|
||||
from unittest import mock
|
||||
|
||||
import aios_isolated_writer_policy as writer_policy
|
||||
import isolated_writer_path_policy as path_policy
|
||||
from isolated_writer_authz import AuthorizationError, _validate_grant
|
||||
|
||||
|
||||
TARGET = "deploy/systemd/aios-codex-review-test-worker.service"
|
||||
BRANCH = "patch/codex-review-test-worker-20260724"
|
||||
AUDIENCE = "aios-codex-review-worker-writer.service"
|
||||
|
||||
VALID_UNIT = """\
|
||||
[Unit]
|
||||
Description=AIOS Codex Review/Test Worker
|
||||
After=network-online.target
|
||||
Wants=network-online.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=aiman
|
||||
Group=gitops
|
||||
WorkingDirectory=/usr/libexec/aios-codex-review-test-worker/current
|
||||
Environment=AIOS_CODEX_WORKER_MODE=shadow
|
||||
Environment=AIOS_TASK_PLANE_EXECUTION_ENABLED=false
|
||||
Environment=PYTHONDONTWRITEBYTECODE=1
|
||||
ExecStart=/usr/bin/python3 -m task_plane.workers.codex_review_test
|
||||
Restart=on-failure
|
||||
RestartSec=2
|
||||
NoNewPrivileges=true
|
||||
PrivateTmp=true
|
||||
PrivateDevices=true
|
||||
ProtectSystem=strict
|
||||
ProtectHome=true
|
||||
ProtectKernelTunables=true
|
||||
ProtectKernelModules=true
|
||||
ProtectControlGroups=true
|
||||
LockPersonality=true
|
||||
RestrictSUIDSGID=true
|
||||
RestrictRealtime=true
|
||||
MemoryDenyWriteExecute=true
|
||||
RuntimeDirectory=aios-codex-review-test-worker
|
||||
StateDirectory=aios-codex-review-test-worker
|
||||
CacheDirectory=aios-codex-review-test-worker
|
||||
ReadOnlyPaths=/data/aios-gitops/repo.git
|
||||
ReadOnlyPaths=/usr/libexec/aios-codex-review-test-worker
|
||||
ReadWritePaths=/run/aios-codex-review-test-worker
|
||||
ReadWritePaths=/var/lib/aios-codex-review-test-worker
|
||||
ReadWritePaths=/var/cache/aios-codex-review-test-worker
|
||||
UMask=0077
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
"""
|
||||
|
||||
|
||||
def document(*, paths=None, **updates):
|
||||
value = {
|
||||
"schema_version": 1,
|
||||
"policy_id": "codex-review-worker-path-policy-v1",
|
||||
"audience": AUDIENCE,
|
||||
"repository_identity": "aios/aios",
|
||||
"allowed_branch": BRANCH,
|
||||
"allowed_operations": {
|
||||
"commit_file.atomic": {
|
||||
"exact_paths": paths or [TARGET],
|
||||
"max_content_bytes": 32768,
|
||||
"content_profile": "systemd-codex-review-worker-v1",
|
||||
}
|
||||
},
|
||||
}
|
||||
value.update(updates)
|
||||
return value
|
||||
|
||||
|
||||
class ExactPathPolicyTests(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.saved = dict(os.environ)
|
||||
self.temp = tempfile.TemporaryDirectory()
|
||||
self.root = Path(self.temp.name)
|
||||
self.policy_dir = self.root / "policies"
|
||||
self.policy_dir.mkdir(mode=0o750)
|
||||
self.policy_file = self.policy_dir / "writer.json"
|
||||
self.workspace = self.root / "workspace"
|
||||
self.workspace.mkdir(mode=0o700)
|
||||
os.environ.update(
|
||||
{
|
||||
"AIOS_GITOPS_ISOLATED_WRITER_ENFORCEMENT": "required",
|
||||
"AIOS_GITOPS_SERVICE_AUDIENCE": AUDIENCE,
|
||||
"AIOS_GITOPS_REPOSITORY_IDENTITY": "aios/aios",
|
||||
"AIOS_GITOPS_ALLOWED_BRANCH": BRANCH,
|
||||
}
|
||||
)
|
||||
|
||||
def tearDown(self):
|
||||
path_policy.clear_path_policy_cache()
|
||||
os.environ.clear()
|
||||
os.environ.update(self.saved)
|
||||
self.temp.cleanup()
|
||||
|
||||
def write_policy(self, value=None):
|
||||
payload = document() if value is None else value
|
||||
raw = (
|
||||
json.dumps(payload, ensure_ascii=False, sort_keys=True, separators=(",", ":"))
|
||||
+ "\n"
|
||||
)
|
||||
self.policy_file.write_text(raw, encoding="utf-8")
|
||||
self.policy_file.chmod(0o640)
|
||||
return path_policy.load_path_policy(
|
||||
self.policy_file,
|
||||
expected_owner_uid=os.getuid(),
|
||||
expected_group_gid=os.getgid(),
|
||||
)
|
||||
|
||||
def configured(self, loaded):
|
||||
return mock.patch.object(
|
||||
path_policy,
|
||||
"configured_path_policy",
|
||||
return_value=loaded,
|
||||
)
|
||||
|
||||
def test_exact_systemd_path_and_exact_token_path_are_allowed(self):
|
||||
loaded = self.write_policy()
|
||||
with self.configured(loaded):
|
||||
self.assertEqual(TARGET, writer_policy.validate_path(TARGET, self.workspace))
|
||||
writer_policy.validate_content_for_path(TARGET, VALID_UNIT)
|
||||
|
||||
def test_token_path_cannot_expand_service_policy(self):
|
||||
loaded = self.write_policy(document(paths=["deploy/systemd/other.service"]))
|
||||
with self.configured(loaded), self.assertRaisesRegex(
|
||||
PermissionError, "isolated_writer_service_path_rejected"
|
||||
):
|
||||
writer_policy.validate_path(TARGET, self.workspace)
|
||||
|
||||
def test_service_policy_cannot_expand_token_path(self):
|
||||
now = datetime.now(timezone.utc)
|
||||
grant = {
|
||||
"schema_version": 2,
|
||||
"audience": AUDIENCE,
|
||||
"source_cidrs": ["127.0.0.1/32"],
|
||||
"created_at": (now - timedelta(seconds=5)).isoformat(),
|
||||
"not_before": (now - timedelta(seconds=2)).isoformat(),
|
||||
"expires_at": (now + timedelta(minutes=5)).isoformat(),
|
||||
"revoked": False,
|
||||
"allowed_operations": ["commit_file.atomic"],
|
||||
"allowed_branch": BRANCH,
|
||||
"allowed_paths": ["deploy/systemd/other.service"],
|
||||
"approved_base_oid": "a" * 40,
|
||||
}
|
||||
with mock.patch(
|
||||
"isolated_writer_authz._git_is_ancestor", return_value=True
|
||||
), self.assertRaisesRegex(AuthorizationError, "writer_token_path_rejected"):
|
||||
_validate_grant(
|
||||
grant,
|
||||
audience=AUDIENCE,
|
||||
source_ip="127.0.0.1",
|
||||
operation="commit_file.atomic",
|
||||
branch=BRANCH,
|
||||
path=TARGET,
|
||||
workspace=self.workspace,
|
||||
current_time=now,
|
||||
)
|
||||
|
||||
def test_other_service_file_is_rejected(self):
|
||||
loaded = self.write_policy()
|
||||
with self.configured(loaded), self.assertRaisesRegex(
|
||||
PermissionError, "isolated_writer_service_path_rejected"
|
||||
):
|
||||
writer_policy.validate_path(
|
||||
"deploy/systemd/other.service", self.workspace
|
||||
)
|
||||
|
||||
def test_path_traversal_is_rejected(self):
|
||||
for value in ("deploy/systemd/../x", "../deploy/systemd/x"):
|
||||
with self.subTest(value=value), self.assertRaises(PermissionError):
|
||||
writer_policy.validate_path(value, self.workspace)
|
||||
|
||||
def test_url_encoded_traversal_is_rejected(self):
|
||||
with self.assertRaises(PermissionError):
|
||||
writer_policy.validate_path(
|
||||
"deploy/systemd/%2e%2e/x", self.workspace
|
||||
)
|
||||
|
||||
def test_symlink_policy_file_is_rejected(self):
|
||||
self.write_policy()
|
||||
link = self.policy_dir / "link.json"
|
||||
link.symlink_to(self.policy_file)
|
||||
with self.assertRaisesRegex(
|
||||
path_policy.PathPolicyError, "isolated_writer_path_policy_symlink_rejected"
|
||||
):
|
||||
path_policy.load_path_policy(
|
||||
link,
|
||||
expected_owner_uid=os.getuid(),
|
||||
expected_group_gid=os.getgid(),
|
||||
)
|
||||
|
||||
def test_world_writable_policy_file_is_rejected(self):
|
||||
self.write_policy()
|
||||
self.policy_file.chmod(0o646)
|
||||
with self.assertRaisesRegex(
|
||||
path_policy.PathPolicyError,
|
||||
"isolated_writer_path_policy_permissions_rejected",
|
||||
):
|
||||
path_policy.load_path_policy(
|
||||
self.policy_file,
|
||||
expected_owner_uid=os.getuid(),
|
||||
expected_group_gid=os.getgid(),
|
||||
)
|
||||
|
||||
def test_unknown_policy_field_is_rejected(self):
|
||||
value = document()
|
||||
value["unexpected"] = True
|
||||
with self.assertRaisesRegex(
|
||||
path_policy.PathPolicyError,
|
||||
"isolated_writer_path_policy_fields_rejected",
|
||||
):
|
||||
self.write_policy(value)
|
||||
|
||||
def test_audience_mismatch_is_rejected(self):
|
||||
loaded = self.write_policy()
|
||||
os.environ["AIOS_GITOPS_PATH_POLICY_FILE"] = str(self.policy_file)
|
||||
os.environ["AIOS_GITOPS_SERVICE_AUDIENCE"] = "other.service"
|
||||
path_policy.clear_path_policy_cache()
|
||||
with mock.patch.object(
|
||||
path_policy, "load_path_policy", return_value=loaded
|
||||
), self.assertRaisesRegex(
|
||||
path_policy.PathPolicyError,
|
||||
"isolated_writer_path_policy_audience_rejected",
|
||||
):
|
||||
path_policy.configured_path_policy()
|
||||
|
||||
def test_branch_mismatch_is_rejected(self):
|
||||
loaded = self.write_policy()
|
||||
os.environ["AIOS_GITOPS_PATH_POLICY_FILE"] = str(self.policy_file)
|
||||
os.environ["AIOS_GITOPS_ALLOWED_BRANCH"] = "patch/other"
|
||||
path_policy.clear_path_policy_cache()
|
||||
with mock.patch.object(
|
||||
path_policy, "load_path_policy", return_value=loaded
|
||||
), self.assertRaisesRegex(
|
||||
path_policy.PathPolicyError,
|
||||
"isolated_writer_path_policy_branch_rejected",
|
||||
):
|
||||
path_policy.configured_path_policy()
|
||||
|
||||
def assert_profile_rejected(self, content):
|
||||
loaded = self.write_policy()
|
||||
with self.configured(loaded), self.assertRaisesRegex(
|
||||
PermissionError, "isolated_writer_systemd_profile_rejected"
|
||||
):
|
||||
writer_policy.validate_content_for_path(TARGET, content)
|
||||
|
||||
def test_disallowed_execution_and_credential_directives_are_rejected(self):
|
||||
directives = (
|
||||
"ExecStartPre=/usr/bin/id",
|
||||
"ExecStartPost=/usr/bin/id",
|
||||
"ExecReload=/usr/bin/id",
|
||||
"ExecStop=/usr/bin/id",
|
||||
"ExecStopPost=/usr/bin/id",
|
||||
"ExecCondition=/usr/bin/true",
|
||||
"EnvironmentFile=/run/worker.env",
|
||||
"PassEnvironment=PATH",
|
||||
"LoadCredential=worker:/run/worker",
|
||||
"SetCredential=worker:value",
|
||||
"ImportCredential=worker",
|
||||
"StandardInput=socket",
|
||||
"StandardOutput=socket",
|
||||
)
|
||||
for directive in directives:
|
||||
with self.subTest(directive=directive):
|
||||
self.assert_profile_rejected(
|
||||
VALID_UNIT.replace(
|
||||
"Restart=on-failure",
|
||||
f"Restart=on-failure\n{directive}",
|
||||
)
|
||||
)
|
||||
|
||||
def test_disallowed_namespace_and_capability_directives_are_rejected(self):
|
||||
directives = (
|
||||
"RootDirectory=/srv/worker",
|
||||
"RootImage=/srv/worker.raw",
|
||||
"ReadWritePaths=/",
|
||||
"BindPaths=/etc:/etc",
|
||||
"BindReadOnlyPaths=/etc:/etc",
|
||||
"TemporaryFileSystem=/etc",
|
||||
"CapabilityBoundingSet=CAP_SYS_ADMIN",
|
||||
"AmbientCapabilities=CAP_SYS_ADMIN",
|
||||
"DeviceAllow=/dev/sda rw",
|
||||
"SupplementaryGroups=docker",
|
||||
"Sockets=unsafe.socket",
|
||||
)
|
||||
for directive in directives:
|
||||
with self.subTest(directive=directive):
|
||||
self.assert_profile_rejected(
|
||||
VALID_UNIT.replace(
|
||||
"Restart=on-failure",
|
||||
f"Restart=on-failure\n{directive}",
|
||||
)
|
||||
)
|
||||
|
||||
def test_duplicate_single_directives_and_sections_are_rejected(self):
|
||||
bad_values = (
|
||||
VALID_UNIT.replace("User=aiman", "User=aiman\nUser=aiman"),
|
||||
VALID_UNIT.replace(
|
||||
"ExecStart=/usr/bin/python3 -m task_plane.workers.codex_review_test",
|
||||
"ExecStart=/usr/bin/python3 -m task_plane.workers.codex_review_test\n"
|
||||
"ExecStart=/usr/bin/python3 -m task_plane.workers.codex_review_test",
|
||||
),
|
||||
VALID_UNIT + "\n[Service]\nType=simple\n",
|
||||
)
|
||||
for value in bad_values:
|
||||
with self.subTest(value=value):
|
||||
self.assert_profile_rejected(value)
|
||||
|
||||
def test_empty_reset_continuation_crlf_nul_and_long_lines_are_rejected(self):
|
||||
bad_values = (
|
||||
VALID_UNIT.replace(
|
||||
"ExecStart=/usr/bin/python3 -m task_plane.workers.codex_review_test",
|
||||
"ExecStart=",
|
||||
),
|
||||
VALID_UNIT.replace(
|
||||
"Restart=on-failure",
|
||||
"Restart=on-failure\n continuation-injection",
|
||||
),
|
||||
VALID_UNIT.replace("\n", "\r\n"),
|
||||
VALID_UNIT.replace("Restart=on-failure", "Restart=on-failure\x00"),
|
||||
VALID_UNIT.replace(
|
||||
"Description=AIOS Codex Review/Test Worker",
|
||||
"Description=" + ("A" * 4097),
|
||||
),
|
||||
)
|
||||
for value in bad_values:
|
||||
with self.subTest(kind=repr(value[:80])):
|
||||
self.assert_profile_rejected(value)
|
||||
|
||||
def test_unknown_environment_and_production_execution_are_rejected(self):
|
||||
bad_values = (
|
||||
VALID_UNIT.replace(
|
||||
"Environment=PYTHONDONTWRITEBYTECODE=1",
|
||||
"Environment=UNKNOWN_KEY=value",
|
||||
),
|
||||
VALID_UNIT.replace(
|
||||
"AIOS_TASK_PLANE_EXECUTION_ENABLED=false",
|
||||
"AIOS_TASK_PLANE_EXECUTION_ENABLED=true",
|
||||
),
|
||||
VALID_UNIT.replace(
|
||||
"Environment=PYTHONDONTWRITEBYTECODE=1",
|
||||
"Environment=Authorization: Bearer placeholder",
|
||||
),
|
||||
)
|
||||
for value in bad_values:
|
||||
with self.subTest(value=value):
|
||||
self.assert_profile_rejected(value)
|
||||
|
||||
def test_identity_working_directory_and_path_prefix_bypasses_are_rejected(self):
|
||||
bad_values = (
|
||||
VALID_UNIT.replace("User=aiman", "User=root"),
|
||||
VALID_UNIT.replace(
|
||||
"/usr/libexec/aios-codex-review-test-worker/current",
|
||||
"/data/aios-gitops/worktrees/worker",
|
||||
),
|
||||
VALID_UNIT.replace(
|
||||
"ReadWritePaths=/run/aios-codex-review-test-worker",
|
||||
"ReadWritePaths=/etc",
|
||||
),
|
||||
VALID_UNIT.replace(
|
||||
"ReadOnlyPaths=/data/aios-gitops/repo.git",
|
||||
"ReadOnlyPaths=/data",
|
||||
),
|
||||
)
|
||||
for value in bad_values:
|
||||
with self.subTest(value=value):
|
||||
self.assert_profile_rejected(value)
|
||||
|
||||
def test_unknown_section_directive_and_unicode_lookalike_are_rejected(self):
|
||||
bad_values = (
|
||||
VALID_UNIT.replace("[Install]", "[Socket]"),
|
||||
VALID_UNIT.replace(
|
||||
"Restart=on-failure",
|
||||
"Restart=on-failure\nProtectClock=true",
|
||||
),
|
||||
VALID_UNIT.replace("ExecStart=", "\u0415xecStart="),
|
||||
)
|
||||
for value in bad_values:
|
||||
with self.subTest(value=value):
|
||||
self.assert_profile_rejected(value)
|
||||
|
||||
def test_invalid_unicode_surrogate_and_missing_final_newline_are_rejected(self):
|
||||
for value in (VALID_UNIT + "\ud800", VALID_UNIT.rstrip("\n")):
|
||||
with self.subTest(value=repr(value[-30:])):
|
||||
self.assert_profile_rejected(value)
|
||||
|
||||
def test_writer_request_cannot_modify_policy_file(self):
|
||||
with self.assertRaises(PermissionError):
|
||||
writer_policy.validate_path(
|
||||
"etc/aios-gitops/writer-path-policies/codex-review-worker.json",
|
||||
self.workspace,
|
||||
)
|
||||
|
||||
def test_coarse_file_and_commit_endpoints_remain_forbidden(self):
|
||||
for endpoint in ("/api/file", "/api/commit"):
|
||||
with self.subTest(endpoint=endpoint), self.assertRaisesRegex(
|
||||
PermissionError, "isolated_writer_operation_forbidden"
|
||||
):
|
||||
writer_policy.validate_endpoint(
|
||||
"POST", endpoint, self.workspace, self.root
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -15,6 +15,10 @@ class IsolatedWriterPolicyTests(unittest.TestCase):
|
||||
os.environ["AIOS_GITOPS_ISOLATED_WRITER_ENFORCEMENT"] = "required"
|
||||
os.environ["AIOS_GITOPS_ALLOWED_BRANCH"] = "patch/aios-agent-task-control-plane-20260724"
|
||||
os.environ["AIOS_GITOPS_ALLOWED_BASE_OID"] = "a8e97868976b0dce17b7cb8487eadc1d9c687622"
|
||||
os.environ["AIOS_GITOPS_TOKEN_POLICY_VERSION_REQUIRED"] = "2"
|
||||
os.environ["AIOS_GITOPS_SERVICE_AUDIENCE"] = "test-isolated-writer.service"
|
||||
os.environ["AIOS_GITOPS_TOKEN_REGISTRY_V2"] = "/tmp/test-writer-token-registry.sqlite3"
|
||||
os.environ["AIOS_GITOPS_WRITER_OPERATION_LEDGER"] = "/tmp/test-writer-operations.sqlite3"
|
||||
|
||||
def tearDown(self):
|
||||
os.environ.clear()
|
||||
|
||||
@@ -0,0 +1,471 @@
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
import sqlite3
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
import threading
|
||||
import time
|
||||
import unittest
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from pathlib import Path
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "active-runtime" / "app"))
|
||||
|
||||
import aios_isolated_writer_policy as policy
|
||||
from isolated_writer_authz import AuthorizationError, authorize, operation_for
|
||||
from isolated_writer_commit_file import (
|
||||
AtomicCommitFile,
|
||||
CommitFileError,
|
||||
OperationLedger,
|
||||
SimulatedCrash,
|
||||
)
|
||||
from isolated_writer_token_registry import TokenRegistry, token_digest
|
||||
|
||||
|
||||
BRANCH = "patch/codex-review-test-worker-20260724"
|
||||
AUDIENCE = "aios-codex-review-worker-writer.service"
|
||||
TASK_ID = "AIOS-CODEX-WRITER-SMOKE-20260724"
|
||||
TARGET_PATH = "docs/reliability/codex-review-worker-writer-smoke.json"
|
||||
|
||||
|
||||
def run(cwd, *args):
|
||||
return subprocess.check_output(args, cwd=cwd, text=True).strip()
|
||||
|
||||
|
||||
@dataclass
|
||||
class DummyGrant:
|
||||
token_id: str = "test-token-id"
|
||||
task_id: str = TASK_ID
|
||||
operation: str = "commit_file.atomic"
|
||||
record: dict = None
|
||||
|
||||
def __post_init__(self):
|
||||
if self.record is None:
|
||||
self.record = {"audience": AUDIENCE, "_source_ip": "127.0.0.1"}
|
||||
|
||||
|
||||
class RepositoryCase(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.saved = dict(os.environ)
|
||||
self.temp = tempfile.TemporaryDirectory()
|
||||
self.root = Path(self.temp.name)
|
||||
self.authoritative = self.root / "authoritative.git"
|
||||
self.gitea = self.root / "gitea.git"
|
||||
self.seed = self.root / "seed"
|
||||
self.workspace = self.root / "writer"
|
||||
subprocess.run(["git", "init", "--bare", str(self.authoritative)], check=True, capture_output=True)
|
||||
subprocess.run(["git", "init", str(self.seed)], check=True, capture_output=True)
|
||||
subprocess.run(["git", "config", "user.email", "writer@example.invalid"], cwd=self.seed, check=True)
|
||||
subprocess.run(["git", "config", "user.name", "Writer Test"], cwd=self.seed, check=True)
|
||||
(self.seed / "README.md").write_text("base\n", encoding="utf-8")
|
||||
subprocess.run(["git", "add", "README.md"], cwd=self.seed, check=True)
|
||||
subprocess.run(["git", "commit", "-m", "base"], cwd=self.seed, check=True, capture_output=True)
|
||||
subprocess.run(["git", "branch", "-M", "main"], cwd=self.seed, check=True)
|
||||
subprocess.run(["git", "remote", "add", "origin", str(self.authoritative)], cwd=self.seed, check=True)
|
||||
subprocess.run(["git", "push", "origin", "main"], cwd=self.seed, check=True, capture_output=True)
|
||||
self.base = run(self.seed, "git", "rev-parse", "HEAD")
|
||||
subprocess.run(["git", "checkout", "-b", BRANCH], cwd=self.seed, check=True, capture_output=True)
|
||||
subprocess.run(["git", "push", "origin", BRANCH], cwd=self.seed, check=True, capture_output=True)
|
||||
subprocess.run(
|
||||
["git", "--git-dir", str(self.authoritative), "worktree", "add", str(self.workspace), BRANCH],
|
||||
check=True,
|
||||
capture_output=True,
|
||||
)
|
||||
subprocess.run(["git", "config", "user.email", "writer@example.invalid"], cwd=self.workspace, check=True)
|
||||
subprocess.run(["git", "config", "user.name", "Writer Test"], cwd=self.workspace, check=True)
|
||||
subprocess.run(["git", "clone", "--bare", str(self.authoritative), str(self.gitea)], check=True, capture_output=True)
|
||||
subprocess.run(["git", "remote", "add", "gitea-http", str(self.gitea)], cwd=self.workspace, check=True)
|
||||
self.registry = TokenRegistry(self.root / "registry.sqlite3")
|
||||
os.environ.update({
|
||||
"AIOS_GITOPS_ALLOWED_BRANCH": BRANCH,
|
||||
"AIOS_GITOPS_TOKEN_POLICY_VERSION_REQUIRED": "2",
|
||||
"AIOS_GITOPS_SERVICE_AUDIENCE": AUDIENCE,
|
||||
"AIOS_GITOPS_ISOLATED_WRITER_ENFORCEMENT": "required",
|
||||
})
|
||||
|
||||
def tearDown(self):
|
||||
os.environ.clear()
|
||||
os.environ.update(self.saved)
|
||||
self.temp.cleanup()
|
||||
|
||||
def grant(self, **updates):
|
||||
now = datetime.now(timezone.utc)
|
||||
grant = {
|
||||
"token_id": "smoke-" + hashlib.sha256(os.urandom(16)).hexdigest()[:12],
|
||||
"audience": AUDIENCE,
|
||||
"task_id": TASK_ID,
|
||||
"source_cidrs": ["127.0.0.1/32", "::1/128"],
|
||||
"allowed_branch": BRANCH,
|
||||
"approved_base_oid": self.base,
|
||||
"allowed_operations": [
|
||||
"runtime.status.read",
|
||||
"baseline.read",
|
||||
"patches.read",
|
||||
"commit_file.atomic",
|
||||
],
|
||||
"allowed_paths": [TARGET_PATH],
|
||||
"not_before": (now - timedelta(seconds=2)).isoformat(),
|
||||
"expires_at": (now + timedelta(minutes=20)).isoformat(),
|
||||
"max_uses": {
|
||||
"runtime.status.read": 2,
|
||||
"baseline.read": 2,
|
||||
"patches.read": 2,
|
||||
"commit_file.atomic": 4,
|
||||
},
|
||||
"issued_by": "owner-authorized-local-maintenance-executor",
|
||||
"created_at": (now - timedelta(seconds=3)).isoformat(),
|
||||
}
|
||||
grant.update(updates)
|
||||
return grant
|
||||
|
||||
def issue(self, **updates):
|
||||
return self.registry.issue(self.grant(**updates))
|
||||
|
||||
def auth(self, token, **updates):
|
||||
args = {
|
||||
"method": "GET",
|
||||
"path": "/api/runtime-status",
|
||||
"source_ip": "127.0.0.1",
|
||||
"workspace": self.workspace,
|
||||
}
|
||||
args.update(updates)
|
||||
return authorize(self.registry, token, **args)
|
||||
|
||||
def request(self, key="writer-smoke-0001", content='{"status":"READY"}\n'):
|
||||
return {
|
||||
"branch": BRANCH,
|
||||
"path": TARGET_PATH,
|
||||
"content": content,
|
||||
"message": "chore(task-plane): verify Codex writer endpoint",
|
||||
"expected_head_oid": run(self.workspace, "git", "rev-parse", "HEAD"),
|
||||
"idempotency_key": key,
|
||||
}
|
||||
|
||||
def writer(self, crash=None, remote="gitea-http", audit=None):
|
||||
return AtomicCommitFile(
|
||||
workspace=self.workspace,
|
||||
bare_repo=self.authoritative,
|
||||
ledger=OperationLedger(self.root / "operations.sqlite3"),
|
||||
remote=remote,
|
||||
audit=audit or (lambda record: None),
|
||||
crash_after_state=crash,
|
||||
)
|
||||
|
||||
|
||||
class TokenPolicyV2Tests(RepositoryCase):
|
||||
def test_v1_coarse_scope_token_is_rejected(self):
|
||||
token, record = self.issue()
|
||||
record["schema_version"] = 1
|
||||
with self.registry._connect() as db:
|
||||
db.execute(
|
||||
"UPDATE token_grants SET grant_json=? WHERE token_id=?",
|
||||
(json.dumps(record), record["token_id"]),
|
||||
)
|
||||
with self.assertRaisesRegex(AuthorizationError, "writer_token_policy_v2_required"):
|
||||
self.auth(token)
|
||||
|
||||
def test_audience_mismatch_is_rejected(self):
|
||||
token, _ = self.issue(audience="other.service")
|
||||
with self.assertRaisesRegex(AuthorizationError, "writer_token_audience_rejected"):
|
||||
self.auth(token)
|
||||
|
||||
def test_source_ip_mismatch_is_rejected(self):
|
||||
token, _ = self.issue(source_cidrs=["192.0.2.0/24"])
|
||||
with self.assertRaisesRegex(AuthorizationError, "writer_token_source_rejected"):
|
||||
self.auth(token)
|
||||
|
||||
def test_branch_mismatch_is_rejected(self):
|
||||
token, _ = self.issue(allowed_branch="patch/other")
|
||||
with self.assertRaisesRegex(AuthorizationError, "writer_token_branch_rejected"):
|
||||
self.auth(token)
|
||||
|
||||
def test_operation_mismatch_is_rejected(self):
|
||||
token, _ = self.issue(allowed_operations=["baseline.read"])
|
||||
with self.assertRaisesRegex(AuthorizationError, "writer_token_operation_rejected"):
|
||||
self.auth(token)
|
||||
|
||||
def test_path_mismatch_is_rejected(self):
|
||||
token, _ = self.issue()
|
||||
with self.assertRaisesRegex(AuthorizationError, "writer_token_path_rejected"):
|
||||
self.auth(
|
||||
token,
|
||||
method="POST",
|
||||
path="/api/commit-file",
|
||||
branch=BRANCH,
|
||||
target_path="docs/other.json",
|
||||
)
|
||||
|
||||
def test_expired_token_is_rejected(self):
|
||||
now = datetime.now(timezone.utc)
|
||||
token, _ = self.issue(
|
||||
created_at=(now - timedelta(minutes=10)).isoformat(),
|
||||
not_before=(now - timedelta(minutes=9)).isoformat(),
|
||||
expires_at=(now - timedelta(seconds=1)).isoformat(),
|
||||
)
|
||||
with self.assertRaisesRegex(AuthorizationError, "writer_token_expired"):
|
||||
self.auth(token)
|
||||
|
||||
def test_not_before_token_is_rejected(self):
|
||||
now = datetime.now(timezone.utc)
|
||||
token, _ = self.issue(
|
||||
created_at=now.isoformat(),
|
||||
not_before=(now + timedelta(minutes=1)).isoformat(),
|
||||
expires_at=(now + timedelta(minutes=5)).isoformat(),
|
||||
)
|
||||
with self.assertRaisesRegex(AuthorizationError, "writer_token_not_yet_valid"):
|
||||
self.auth(token)
|
||||
|
||||
def test_revoked_token_is_rejected(self):
|
||||
token, grant = self.issue()
|
||||
self.assertTrue(self.registry.revoke(grant["token_id"]))
|
||||
with self.assertRaisesRegex(AuthorizationError, "writer_token_revoked"):
|
||||
self.auth(token)
|
||||
|
||||
def test_operation_use_count_is_atomic_and_exhausted(self):
|
||||
token, _ = self.issue(max_uses={
|
||||
"runtime.status.read": 1,
|
||||
"baseline.read": 1,
|
||||
"patches.read": 1,
|
||||
"commit_file.atomic": 1,
|
||||
})
|
||||
self.auth(token)
|
||||
with self.assertRaisesRegex(AuthorizationError, "writer_token_operation_exhausted"):
|
||||
self.auth(token)
|
||||
|
||||
def test_concurrent_use_count_never_exceeds_limit(self):
|
||||
token, _ = self.issue(max_uses={
|
||||
"runtime.status.read": 2,
|
||||
"baseline.read": 1,
|
||||
"patches.read": 1,
|
||||
"commit_file.atomic": 1,
|
||||
})
|
||||
successes, failures = [], []
|
||||
|
||||
def invoke():
|
||||
try:
|
||||
successes.append(self.auth(token))
|
||||
except AuthorizationError as exc:
|
||||
failures.append(exc.code)
|
||||
|
||||
threads = [threading.Thread(target=invoke) for _ in range(6)]
|
||||
for thread in threads:
|
||||
thread.start()
|
||||
for thread in threads:
|
||||
thread.join()
|
||||
self.assertEqual(2, len(successes))
|
||||
self.assertEqual(4, failures.count("writer_token_operation_exhausted"))
|
||||
|
||||
def test_registry_does_not_store_plaintext_token(self):
|
||||
token, _ = self.issue()
|
||||
registry_bytes = b"".join(
|
||||
item.read_bytes()
|
||||
for item in self.registry.path.parent.glob(self.registry.path.name + "*")
|
||||
if item.is_file()
|
||||
)
|
||||
self.assertNotIn(token.encode(), registry_bytes)
|
||||
with self.registry._connect() as db:
|
||||
stored = db.execute("SELECT token_digest FROM token_grants").fetchone()[0]
|
||||
self.assertEqual(token_digest(token), stored)
|
||||
|
||||
def test_coarse_routes_have_no_operation_mapping(self):
|
||||
for path in ("/api/file", "/api/commit", "/api/tag", "/api/rollback", "/api/model"):
|
||||
with self.subTest(path=path), self.assertRaises(AuthorizationError) as captured:
|
||||
operation_for("POST", path)
|
||||
self.assertEqual(403, captured.exception.status)
|
||||
|
||||
def test_route_mapping_is_fixed_not_request_controlled(self):
|
||||
self.assertEqual("runtime.status.read", operation_for("GET", "/api/runtime-status"))
|
||||
self.assertEqual("baseline.read", operation_for("GET", "/api/baseline"))
|
||||
self.assertEqual("patches.read", operation_for("GET", "/api/patches"))
|
||||
self.assertEqual("commit_file.atomic", operation_for("POST", "/api/commit-file"))
|
||||
|
||||
def test_x_forwarded_for_cannot_replace_socket_peer(self):
|
||||
token, _ = self.issue(source_cidrs=["203.0.113.9/32"])
|
||||
with self.assertRaisesRegex(AuthorizationError, "writer_token_source_rejected"):
|
||||
self.auth(token, source_ip="127.0.0.1")
|
||||
|
||||
|
||||
class AtomicCommitFileTests(RepositoryCase):
|
||||
def test_commit_file_succeeds_and_verifies_bare_and_gitea(self):
|
||||
result = self.writer().execute(DummyGrant(), self.request())
|
||||
self.assertEqual("COMPLETED", result["state"])
|
||||
self.assertEqual(
|
||||
result["commit_oid"],
|
||||
run(self.root, "git", "--git-dir", str(self.gitea), "rev-parse", f"refs/heads/{BRANCH}"),
|
||||
)
|
||||
|
||||
def test_same_idempotency_key_returns_same_commit(self):
|
||||
request = self.request()
|
||||
first = self.writer().execute(DummyGrant(), request)
|
||||
count = run(self.workspace, "git", "rev-list", "--count", "HEAD")
|
||||
second = self.writer().execute(DummyGrant(), request)
|
||||
self.assertEqual(first["commit_oid"], second["commit_oid"])
|
||||
self.assertEqual(count, run(self.workspace, "git", "rev-list", "--count", "HEAD"))
|
||||
self.assertTrue(second["idempotent"])
|
||||
|
||||
def test_same_key_with_different_payload_returns_409(self):
|
||||
request = self.request()
|
||||
self.writer().execute(DummyGrant(), request)
|
||||
request["content"] = '{"status":"DIFFERENT"}\n'
|
||||
with self.assertRaises(CommitFileError) as captured:
|
||||
self.writer().execute(DummyGrant(), request)
|
||||
self.assertEqual(409, captured.exception.status)
|
||||
|
||||
def test_same_key_from_different_task_returns_409(self):
|
||||
request = self.request()
|
||||
self.writer().execute(DummyGrant(), request)
|
||||
other = DummyGrant(token_id="other-token", task_id="OTHER-TASK")
|
||||
with self.assertRaisesRegex(CommitFileError, "writer_idempotency_owner_conflict"):
|
||||
self.writer().execute(other, request)
|
||||
|
||||
def test_file_written_crash_is_recoverable(self):
|
||||
request = self.request()
|
||||
with self.assertRaises(SimulatedCrash):
|
||||
self.writer(crash="FILE_WRITTEN").execute(DummyGrant(), request)
|
||||
result = self.writer().execute(DummyGrant(), request)
|
||||
self.assertEqual("COMPLETED", result["state"])
|
||||
|
||||
def test_committed_crash_is_recoverable_without_second_commit(self):
|
||||
request = self.request()
|
||||
with self.assertRaises(SimulatedCrash):
|
||||
self.writer(crash="COMMITTED").execute(DummyGrant(), request)
|
||||
committed = run(self.workspace, "git", "rev-parse", "HEAD")
|
||||
result = self.writer().execute(DummyGrant(), request)
|
||||
self.assertEqual(committed, result["commit_oid"])
|
||||
|
||||
def test_pushed_crash_only_reverifies(self):
|
||||
request = self.request()
|
||||
with self.assertRaises(SimulatedCrash):
|
||||
self.writer(crash="PUSHED").execute(DummyGrant(), request)
|
||||
result = self.writer().execute(DummyGrant(), request)
|
||||
self.assertEqual("COMPLETED", result["state"])
|
||||
|
||||
def test_push_failure_is_failed_recoverable(self):
|
||||
request = self.request()
|
||||
with self.assertRaisesRegex(CommitFileError, "writer_gitea_push_failed"):
|
||||
self.writer(remote="missing-remote").execute(DummyGrant(), request)
|
||||
row = OperationLedger(self.root / "operations.sqlite3").get(request["idempotency_key"])
|
||||
self.assertEqual("FAILED_RECOVERABLE", row["state"])
|
||||
|
||||
def test_branch_head_conflict_requires_manual_review(self):
|
||||
request = self.request()
|
||||
ledger = OperationLedger(self.root / "operations.sqlite3")
|
||||
ledger.get_or_create(DummyGrant(), request, __import__("isolated_writer_commit_file").canonical_digest(request))
|
||||
(self.workspace / "README.md").write_text("drift\n", encoding="utf-8")
|
||||
subprocess.run(["git", "commit", "-am", "drift"], cwd=self.workspace, check=True, capture_output=True)
|
||||
with self.assertRaisesRegex(CommitFileError, "writer_branch_head_conflict"):
|
||||
self.writer().execute(DummyGrant(), request)
|
||||
self.assertEqual("MANUAL_REVIEW_REQUIRED", ledger.get(request["idempotency_key"])["state"])
|
||||
|
||||
def test_extra_staged_file_is_rejected(self):
|
||||
(self.workspace / "README.md").write_text("staged\n", encoding="utf-8")
|
||||
subprocess.run(["git", "add", "README.md"], cwd=self.workspace, check=True)
|
||||
with self.assertRaisesRegex(CommitFileError, "writer_extra_staged_files_rejected"):
|
||||
self.writer().execute(DummyGrant(), self.request())
|
||||
|
||||
def test_path_traversal_and_protected_roots_are_rejected(self):
|
||||
for path in (
|
||||
"../docs/x", "%2e%2e/docs/x", "/etc/x", r"C:\temp\x",
|
||||
r"\\server\share\x", "baseline/current.json", "runtime/x",
|
||||
):
|
||||
request = self.request(key="writer-path-" + hashlib.sha256(path.encode()).hexdigest()[:12])
|
||||
request["path"] = path
|
||||
with self.subTest(path=path), self.assertRaises((PermissionError, CommitFileError)):
|
||||
self.writer().execute(DummyGrant(), request)
|
||||
|
||||
def test_audit_contains_ids_but_not_content_or_authorization(self):
|
||||
records = []
|
||||
request = self.request(content='{"value":"non-secret-body-marker"}\n')
|
||||
self.writer(audit=records.append).execute(DummyGrant(), request)
|
||||
encoded = json.dumps(records)
|
||||
self.assertIn("test-token-id", encoded)
|
||||
self.assertNotIn("non-secret-body-marker", encoded)
|
||||
self.assertNotIn("Authorization", encoded)
|
||||
|
||||
def test_concurrent_same_idempotency_creates_one_commit(self):
|
||||
request = self.request()
|
||||
results, errors = [], []
|
||||
|
||||
def invoke():
|
||||
try:
|
||||
results.append(self.writer().execute(DummyGrant(), request))
|
||||
except Exception as exc:
|
||||
errors.append(exc)
|
||||
|
||||
threads = [threading.Thread(target=invoke) for _ in range(4)]
|
||||
for thread in threads:
|
||||
thread.start()
|
||||
for thread in threads:
|
||||
thread.join()
|
||||
self.assertFalse(errors)
|
||||
self.assertEqual(1, len({item["commit_oid"] for item in results}))
|
||||
|
||||
def test_different_operations_are_serialized_by_branch_lock(self):
|
||||
active = 0
|
||||
maximum = 0
|
||||
guard = threading.Lock()
|
||||
|
||||
class Probe(AtomicCommitFile):
|
||||
def _execute_locked(inner, grant, data, digest):
|
||||
nonlocal active, maximum
|
||||
with guard:
|
||||
active += 1
|
||||
maximum = max(maximum, active)
|
||||
time.sleep(0.05)
|
||||
with guard:
|
||||
active -= 1
|
||||
return {"state": "PROBED"}
|
||||
|
||||
probe = Probe(
|
||||
workspace=self.workspace,
|
||||
bare_repo=self.authoritative,
|
||||
ledger=OperationLedger(self.root / "probe.sqlite3"),
|
||||
remote="gitea-http",
|
||||
audit=lambda record: None,
|
||||
)
|
||||
requests = [self.request(key=f"writer-lock-{index:04d}") for index in range(3)]
|
||||
threads = [threading.Thread(target=probe.execute, args=(DummyGrant(), item)) for item in requests]
|
||||
for thread in threads:
|
||||
thread.start()
|
||||
for thread in threads:
|
||||
thread.join()
|
||||
self.assertEqual(1, maximum)
|
||||
|
||||
def test_invalid_expected_head_and_idempotency_are_rejected(self):
|
||||
request = self.request()
|
||||
request["expected_head_oid"] = "short"
|
||||
with self.assertRaisesRegex(CommitFileError, "writer_expected_head_rejected"):
|
||||
self.writer().execute(DummyGrant(), request)
|
||||
request = self.request()
|
||||
request["idempotency_key"] = "../bad"
|
||||
with self.assertRaisesRegex(CommitFileError, "writer_idempotency_key_rejected"):
|
||||
self.writer().execute(DummyGrant(), request)
|
||||
|
||||
def test_invalid_message_and_nul_content_are_rejected(self):
|
||||
request = self.request()
|
||||
request["message"] = "bad\nmessage"
|
||||
with self.assertRaisesRegex(CommitFileError, "writer_message_rejected"):
|
||||
self.writer().execute(DummyGrant(), request)
|
||||
request = self.request()
|
||||
request["content"] = "bad\x00content"
|
||||
with self.assertRaisesRegex(CommitFileError, "writer_content_rejected"):
|
||||
self.writer().execute(DummyGrant(), request)
|
||||
|
||||
def test_force_flags_are_403_and_unknown_fields_are_rejected(self):
|
||||
for field in ("force", "force_push"):
|
||||
request = self.request(key=f"writer-force-{field}")
|
||||
request[field] = True
|
||||
with self.subTest(field=field), self.assertRaises(CommitFileError) as captured:
|
||||
self.writer().execute(DummyGrant(), request)
|
||||
self.assertEqual(403, captured.exception.status)
|
||||
request = self.request(key="writer-extra-field")
|
||||
request["operation"] = "commit_file.atomic"
|
||||
with self.assertRaisesRegex(CommitFileError, "writer_request_fields_rejected"):
|
||||
self.writer().execute(DummyGrant(), request)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in New Issue
Block a user