Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8814dff202 | ||
|
|
ff6743e851 | ||
|
|
ebd7439874 | ||
|
|
c41e278c59 | ||
|
|
1071979079 | ||
|
|
9a158fcab9 | ||
|
|
8b7f9d9028 | ||
|
|
3aa1872084 | ||
|
|
f6bef90633 | ||
|
|
c2eed5b379 | ||
|
|
6e8212608e | ||
|
|
e6a53888fe | ||
|
|
2bef44f6bc | ||
|
|
a96f36bf72 | ||
|
|
4e6ef70029 | ||
|
|
80feef309f | ||
|
|
a5489f1b67 | ||
|
|
312d3b4830 | ||
|
|
ac2dc5c30e | ||
|
|
5ef261b265 | ||
|
|
3a22c3c7d8 | ||
|
|
1c9f28a613 | ||
|
|
9514062cf3 | ||
|
|
a701a32cb4 | ||
|
|
45f3226662 | ||
|
|
975d239d40 | ||
|
|
3bc44accb5 | ||
|
|
3304d87872 | ||
|
|
875d41c310 | ||
|
|
870ac61dc2 | ||
|
|
009f276699 | ||
|
|
600c717e17 | ||
|
|
02be6d28ed | ||
|
|
b1e240dda3 | ||
|
|
2dfbe837f4 | ||
|
|
80df1e0d87 | ||
|
|
7ecf7cbfce | ||
|
|
bbada7c863 | ||
|
|
df099420d2 | ||
|
|
2d802f3d51 | ||
|
|
dd34dfaad2 | ||
|
|
346426b6ca | ||
|
|
967b90af91 | ||
|
|
2458fa5f28 | ||
|
|
40f2f3ebed | ||
|
|
687112d86c | ||
|
|
f0d1e8479b | ||
|
|
85b4610eb3 | ||
|
|
a0b5160534 | ||
|
|
cf57ef6bcc | ||
|
|
958e49e2cf | ||
|
|
3f6cabd4ac | ||
|
|
146d2123a5 | ||
|
|
ecd4ee1cc9 | ||
|
|
eaf6cd739f | ||
|
|
d4276e2cf5 | ||
|
|
f7a04ccec6 | ||
|
|
5cf628deca | ||
|
|
9914ebc9d4 | ||
|
|
39ffd23335 | ||
|
|
467015e3fc | ||
|
|
cdefcc18dd | ||
|
|
1594ac9a18 | ||
|
|
0ba2cdb63d | ||
|
|
a8e9786897 | ||
|
|
58511395c1 | ||
|
|
d23aafc04c | ||
|
|
312aeff1ab | ||
|
|
1673575b21 | ||
|
|
8b97275ff7 | ||
|
|
578f0a7e28 | ||
|
|
9cc04bf146 | ||
|
|
e0b48dae24 | ||
|
|
9893652270 | ||
|
|
57234854f7 | ||
|
|
ccd64610b0 | ||
|
|
bad1120d6d | ||
|
|
c8d2386677 | ||
|
|
08897d3968 | ||
|
|
8497848120 |
@@ -24,6 +24,15 @@ from gitea_tag_publication import (
|
||||
validate_version,
|
||||
)
|
||||
from orchestrator.action_monitor import ActionMonitor
|
||||
from silent_release_hardening import (
|
||||
ApprovalKeyRing,
|
||||
ApprovalRevocationStore,
|
||||
CapabilityAuthorizationError,
|
||||
SagaConflict,
|
||||
SignedApprovalVerifier,
|
||||
SignedReleasePlanApproval,
|
||||
SilentReleaseActionService,
|
||||
)
|
||||
try:
|
||||
from authoritative_git import (
|
||||
AuthoritativeGitError,
|
||||
@@ -60,6 +69,17 @@ OPENAPI = WS / 'docs' / 'gitops' / 'openapi-chatgpt-actions.yaml'
|
||||
BARE_REPO = Path(os.getenv('AIOS_GITOPS_BARE_REPO', str(ROOT / 'runtime' / 'gitea-bin' / 'data' / 'repositories' / 'aios' / 'aios.git')))
|
||||
AUTHORITATIVE_GIT_DIR = Path(os.getenv('AIOS_GITOPS_AUTHORITATIVE_GIT_DIR')) if os.getenv('AIOS_GITOPS_AUTHORITATIVE_GIT_DIR') else None
|
||||
REPOSITORY_IDENTITY = os.getenv('AIOS_GITOPS_REPOSITORY_IDENTITY', '')
|
||||
SILENT_RELEASE_STATE_ROOT = Path(os.getenv(
|
||||
'AIOS_SILENT_RELEASE_STATE_ROOT',
|
||||
'/var/lib/aios-gitops/silent-release',
|
||||
))
|
||||
SILENT_RELEASE_EXECUTION_ENABLED = str(os.getenv(
|
||||
'AIOS_SILENT_RELEASE_EXECUTION_ENABLED', 'false'
|
||||
)).lower() in {'1', 'true', 'yes', 'on'}
|
||||
SILENT_RELEASE_PUBLIC_KEYS = Path(os.getenv(
|
||||
'AIOS_SILENT_RELEASE_PUBLIC_KEYS',
|
||||
'/etc/aios-gitops/silent-release-owner-public-keys.json',
|
||||
))
|
||||
monitor = ActionMonitor()
|
||||
|
||||
BASE_MODELS = [
|
||||
@@ -68,7 +88,10 @@ BASE_MODELS = [
|
||||
{'id':'base.gpt-5.5','provider':'openai','model':'gpt-5.5','display_name':'GPT-5.5','scope':'global'},
|
||||
]
|
||||
SECRET_RE = re.compile(r'(?i)(api[_-]?key|secret[_-]?access[_-]?key|secret|password|token)\s*[:=]\s*["\']?([A-Za-z0-9_./+=:-]{16,})')
|
||||
SECRET_VAL_RE = re.compile(r'(sk-[A-Za-z0-9_-]{20,}|HPUA[A-Z0-9]{10,}|SDC[A-Za-z0-9]{20,})')
|
||||
SECRET_VAL_RE = re.compile(
|
||||
r'(?<![A-Za-z0-9_])'
|
||||
r'(sk-[A-Za-z0-9_-]{20,}|HPUA[A-Z0-9]{10,}|SDC[A-Za-z0-9]{20,})'
|
||||
)
|
||||
|
||||
READ_ONLY_GET_PATHS = frozenset({
|
||||
'/', '/health', '/api/health', '/api/openapi.yaml',
|
||||
@@ -80,11 +103,23 @@ READ_ONLY_GET_PATHS = frozenset({
|
||||
'/admin/ai-evolution/api/data',
|
||||
'/api/release-consistency', '/api/tag-publication-status',
|
||||
'/api/release-acceptance',
|
||||
'/getReleaseCandidate', '/getReleasePlan', '/getReleaseAcceptance',
|
||||
'/getApprovalEnvelope', '/getReleaseSaga', '/getCapabilityStatus',
|
||||
'/getRecoveryStatus',
|
||||
})
|
||||
SILENT_RELEASE_POST_PATHS = frozenset({
|
||||
'/createReleaseCandidate',
|
||||
'/createReleaseIntent',
|
||||
'/submitSignedApproval',
|
||||
'/submitOwnerApproval',
|
||||
'/startDeployment',
|
||||
})
|
||||
|
||||
def reject_non_read_http(handler, method: str, path: str) -> bool:
|
||||
if method == 'GET':
|
||||
return False
|
||||
if path in SILENT_RELEASE_POST_PATHS:
|
||||
return False
|
||||
decision = evaluate_git_write(WS)
|
||||
if decision.allowed:
|
||||
return False
|
||||
@@ -101,6 +136,41 @@ def sh(args, check=False):
|
||||
def jload(p: Path, default):
|
||||
try: return json.loads(p.read_text(encoding='utf-8'))
|
||||
except Exception: return default
|
||||
|
||||
def _file_sha256(path: Path) -> str:
|
||||
digest=hashlib.sha256()
|
||||
with path.open('rb') as source:
|
||||
for chunk in iter(lambda: source.read(1024 * 1024), b''):
|
||||
digest.update(chunk)
|
||||
return digest.hexdigest()
|
||||
|
||||
def _build_silent_release_service() -> SilentReleaseActionService:
|
||||
verifier=None
|
||||
if SILENT_RELEASE_PUBLIC_KEYS.is_file():
|
||||
verifier=SignedApprovalVerifier(
|
||||
ApprovalKeyRing.from_file(SILENT_RELEASE_PUBLIC_KEYS),
|
||||
ApprovalRevocationStore(
|
||||
SILENT_RELEASE_STATE_ROOT / 'revocations'
|
||||
),
|
||||
)
|
||||
policy_path=WS / 'config' / 'gitops' / 'silent-release-policy.json'
|
||||
policy_sha=_file_sha256(policy_path) if policy_path.is_file() else ''
|
||||
return SilentReleaseActionService(
|
||||
SILENT_RELEASE_STATE_ROOT,
|
||||
execution_enabled=SILENT_RELEASE_EXECUTION_ENABLED,
|
||||
approval_verifier=verifier,
|
||||
expected_repository=REPOSITORY_IDENTITY,
|
||||
policy_sha256=policy_sha,
|
||||
)
|
||||
|
||||
# Construct once during service startup. If production execution is enabled
|
||||
# without every trusted builder/orchestrator dependency, construction fails
|
||||
# closed before the HTTP server accepts traffic.
|
||||
_SILENT_RELEASE_SERVICE = _build_silent_release_service()
|
||||
|
||||
|
||||
def silent_release_service() -> SilentReleaseActionService:
|
||||
return _SILENT_RELEASE_SERVICE
|
||||
def relpath(path: str) -> str:
|
||||
x = str(path or '').replace('\\','/').strip()
|
||||
if not x or x.startswith('/') or '..' in Path(x).parts or Path(x).parts[0] == '.git': raise ValueError('invalid path')
|
||||
@@ -468,12 +538,51 @@ class H(BaseHTTPRequestHandler):
|
||||
if path == '/api/release-acceptance':
|
||||
request=publication_request_from_query(q)
|
||||
return self.sendj(publication_publisher().run_release_acceptance(request))
|
||||
if path in (
|
||||
'/getReleaseCandidate',
|
||||
'/getReleasePlan',
|
||||
'/getReleaseAcceptance',
|
||||
'/getApprovalEnvelope',
|
||||
'/getReleaseSaga',
|
||||
'/getCapabilityStatus',
|
||||
):
|
||||
candidate_id=(q.get('candidate_id') or [''])[0]
|
||||
if not candidate_id:
|
||||
return self.sendj({'error':'candidate_id is required'},400)
|
||||
service=silent_release_service()
|
||||
if path == '/getReleaseCandidate':
|
||||
return self.sendj(
|
||||
service.get_release_candidate(candidate_id)
|
||||
)
|
||||
if path == '/getApprovalEnvelope':
|
||||
return self.sendj(
|
||||
service.get_approval_envelope(candidate_id)
|
||||
)
|
||||
if path == '/getReleasePlan':
|
||||
return self.sendj(service.get_release_plan(candidate_id))
|
||||
if path == '/getReleaseAcceptance':
|
||||
return self.sendj(service.get_acceptance(candidate_id))
|
||||
if path == '/getCapabilityStatus':
|
||||
return self.sendj(
|
||||
service.get_capability_status(candidate_id)
|
||||
)
|
||||
return self.sendj(service.get_release_saga(candidate_id))
|
||||
if path == '/getRecoveryStatus':
|
||||
return self.sendj(
|
||||
silent_release_service().get_recovery_status()
|
||||
)
|
||||
# Admin AI Evolution Dashboard
|
||||
if path in ('/admin/ai-evolution','/admin/ai-evolution/'):
|
||||
return self.ai_evolution_page()
|
||||
if path in ('/admin/ai-evolution/api/data'):
|
||||
return self.ai_evolution_data()
|
||||
return self.sendj({'error':'not_found','path':path},404)
|
||||
except FileNotFoundError:
|
||||
return self.sendj({'error':'release candidate not found'},404)
|
||||
except ValueError as e:
|
||||
error=redact(e)
|
||||
monitor.record_failure('do_GET', error, {'path':path})
|
||||
return self.sendj({'error':error},400)
|
||||
except Exception as e:
|
||||
error=redact(e)
|
||||
monitor.record_failure('do_GET', error, {'path':path})
|
||||
@@ -489,6 +598,32 @@ class H(BaseHTTPRequestHandler):
|
||||
if reject_non_read_http(self, 'POST', path): return
|
||||
data=self.body()
|
||||
try:
|
||||
if path == '/createReleaseCandidate':
|
||||
a=self.need('release:candidate')
|
||||
if not a: return
|
||||
return self.sendj(
|
||||
silent_release_service().create_release_candidate(data)
|
||||
)
|
||||
if path == '/createReleaseIntent':
|
||||
a=self.need('release:candidate')
|
||||
if not a: return
|
||||
return self.sendj(
|
||||
silent_release_service().create_release_intent(data)
|
||||
)
|
||||
if path in ('/submitOwnerApproval','/submitSignedApproval'):
|
||||
a=self.need('release:approve')
|
||||
if not a: return
|
||||
return self.sendj(
|
||||
silent_release_service().submit_owner_approval(data)
|
||||
)
|
||||
if path == '/startDeployment':
|
||||
a=self.need('release:deploy')
|
||||
if not a: return
|
||||
return self.sendj(
|
||||
silent_release_service().start_deployment(
|
||||
str(data.get('candidate_id') or '')
|
||||
)
|
||||
)
|
||||
if path in ('/api/branch','/api/create-branch'): return self.branch(data)
|
||||
if path in ('/api/file','/api/commit-file'): return self.file(data)
|
||||
if path == '/api/commit': return self.commit(data)
|
||||
@@ -499,9 +634,22 @@ class H(BaseHTTPRequestHandler):
|
||||
if path == '/api/model-priority': return self.model_priority(data)
|
||||
if path == '/api/nl': return self.nl(data)
|
||||
return self.sendj({'error':'not_found','path':path},404)
|
||||
except (ValueError, FileNotFoundError) as e:
|
||||
error=redact(e)
|
||||
monitor.record_failure('do_POST', error, {'path':path})
|
||||
return self.sendj({'error':error},400)
|
||||
except CapabilityAuthorizationError as e:
|
||||
error=redact(e)
|
||||
monitor.record_failure('do_POST', error, {'path':path})
|
||||
return self.sendj({'error':error},403)
|
||||
except SagaConflict as e:
|
||||
error=redact(e)
|
||||
monitor.record_failure('do_POST', error, {'path':path})
|
||||
return self.sendj({'error':error},409)
|
||||
except Exception as e:
|
||||
monitor.record_failure('do_POST', str(e), {'path':path})
|
||||
return self.sendj({'error':str(e)},500)
|
||||
error=redact(e)
|
||||
monitor.record_failure('do_POST', error, {'path':path})
|
||||
return self.sendj({'error':error},500)
|
||||
def do_PATCH(self):
|
||||
path=urllib.parse.urlparse(self.path).path.rstrip('/')
|
||||
if reject_non_read_http(self, 'PATCH', path): return
|
||||
@@ -703,7 +851,7 @@ return{vs,cmds,ar,cmd,cdet,tot,sok,sfail,runs,baseline,refreshMsg,loadCmd,selVer
|
||||
|
||||
|
||||
def api_spec():
|
||||
return {'version':'V1R1C0-chatgpt-actions','openapi':'/api/openapi.yaml','read':['GET /api/base','GET /api/ver','GET /api/diff','GET /api/patch','GET /api/model','GET /api/run','GET /admin/ai-evolution'],'write':['POST /api/branch','POST /api/file','POST /api/commit','POST /api/patch','POST /api/tag','POST /api/rollback','POST /api/model','PUT /api/model-priority','POST /api/nl'],'policy':{'bearer_required_for_write':True,'direct_main_push':False,'delete_tag':False,'owner_approval_required_for_v_level':True,'api_keys_in_git':False,'git_secret_field':'secret_ref','online_git_sync':True}}
|
||||
return {'version':'V1R1C0-chatgpt-actions','openapi':'/api/openapi.yaml','read':['GET /api/base','GET /api/ver','GET /api/diff','GET /api/patch','GET /api/model','GET /api/run','GET /getReleaseCandidate','GET /getApprovalEnvelope','GET /getReleaseSaga','GET /admin/ai-evolution'],'write':['POST /api/branch','POST /api/file','POST /api/commit','POST /api/patch','POST /api/tag','POST /api/rollback','POST /api/model','PUT /api/model-priority','POST /api/nl','POST /createReleaseCandidate','POST /submitOwnerApproval','POST /startDeployment'],'policy':{'bearer_required_for_write':True,'direct_main_push':False,'delete_tag':False,'owner_approval_required_for_v_level':True,'api_keys_in_git':False,'git_secret_field':'secret_ref','online_git_sync':True,'silent_release_execution_enabled':SILENT_RELEASE_EXECUTION_ENABLED,'silent_release_shadow_default':True}}
|
||||
|
||||
def main():
|
||||
print(f'AIOS ChatGPT GitOps adapter listening on {HOST}:{PORT}')
|
||||
|
||||
@@ -0,0 +1,316 @@
|
||||
"""Task-scoped hardened wrapper around the existing Action API."""
|
||||
from __future__ import annotations
|
||||
|
||||
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:
|
||||
if _reject(handler, method, path):
|
||||
return True
|
||||
try:
|
||||
policy.validate_endpoint(method, path, api.WS, api.BARE_REPO)
|
||||
return False
|
||||
except PermissionError as exc:
|
||||
handler.sendj({"error": str(exc)}, 403)
|
||||
return True
|
||||
|
||||
|
||||
def checkout(branch, base="main", create=False):
|
||||
api.ensure_git()
|
||||
target = api.branch_name(branch)
|
||||
policy.validate_branch_request({"branch": target, "base": base})
|
||||
exists = api.sh(["rev-parse", "--verify", target])["code"] == 0
|
||||
if exists:
|
||||
if api.sh(["rev-parse", "--abbrev-ref", "HEAD"], True)["stdout"] != target:
|
||||
api.sh(["checkout", target], True)
|
||||
elif create:
|
||||
api.sh(["checkout", "-b", target, base], True)
|
||||
else:
|
||||
raise PermissionError("isolated_writer_branch_missing")
|
||||
policy.require_service_invariants(api.WS, api.BARE_REPO)
|
||||
return target
|
||||
|
||||
|
||||
def write_file(path, content):
|
||||
policy.validate_path(path, api.WS)
|
||||
return _write_file(path, content)
|
||||
|
||||
|
||||
def sync_branch(branch: str):
|
||||
policy.validate_sync_branch(branch)
|
||||
local = _sync_branch(branch)
|
||||
remote = os.getenv("AIOS_GITOPS_GITEA_PUSH_REMOTE", "").strip()
|
||||
if remote != "gitea-http":
|
||||
raise PermissionError("isolated_writer_gitea_remote_rejected")
|
||||
head = api.sh(["rev-parse", "HEAD"], True)["stdout"]
|
||||
pushed = subprocess.run(
|
||||
["git", "push", "--porcelain", remote, f"HEAD:refs/heads/{branch}"],
|
||||
cwd=api.WS,
|
||||
text=True,
|
||||
capture_output=True,
|
||||
)
|
||||
if pushed.returncode != 0:
|
||||
raise RuntimeError("isolated_writer_gitea_push_failed")
|
||||
verified = subprocess.run(
|
||||
["git", "ls-remote", "--heads", remote, f"refs/heads/{branch}"],
|
||||
cwd=api.WS,
|
||||
text=True,
|
||||
capture_output=True,
|
||||
)
|
||||
if verified.returncode != 0:
|
||||
raise RuntimeError("isolated_writer_gitea_verification_failed")
|
||||
fields = verified.stdout.strip().split()
|
||||
if len(fields) != 2 or fields[0] != head or fields[1] != f"refs/heads/{branch}":
|
||||
raise RuntimeError("isolated_writer_gitea_oid_mismatch")
|
||||
return {
|
||||
"status": "verified",
|
||||
"target": "authoritative-bare+gitea",
|
||||
"branch": branch,
|
||||
"oid": head,
|
||||
"local": local.get("status"),
|
||||
}
|
||||
|
||||
|
||||
def commit(paths, msg):
|
||||
policy.validate_commit_paths(paths, api.WS)
|
||||
api.ensure_git()
|
||||
selected = [api.relpath(p) for p in paths] if paths else ["."]
|
||||
api.sh(["add", *selected], True)
|
||||
policy.validate_staged_paths(api.WS)
|
||||
branch = api.sh(["rev-parse", "--abbrev-ref", "HEAD"], True)["stdout"]
|
||||
if not api.sh(["status", "--short"])["stdout"]:
|
||||
return {"status": "no_changes", "branch": branch, "sync": sync_branch(branch)}
|
||||
out = api.sh(["commit", "-m", msg], True)
|
||||
branch = api.sh(["rev-parse", "--abbrev-ref", "HEAD"], True)["stdout"]
|
||||
return {
|
||||
"status": "committed",
|
||||
"branch": branch,
|
||||
"rev": api.sh(["rev-parse", "--short", "HEAD"], True)["stdout"],
|
||||
"git": out,
|
||||
"sync": sync_branch(branch),
|
||||
}
|
||||
|
||||
|
||||
def audit(actor, action, target, payload):
|
||||
policy.append_audit_record(
|
||||
{
|
||||
"at": api.now(),
|
||||
"actor": actor,
|
||||
"action": action,
|
||||
"target": target,
|
||||
"payload": api.sanitize(payload),
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def _validate_request(self, data, validate):
|
||||
try:
|
||||
validate(data)
|
||||
return True
|
||||
except PermissionError as exc:
|
||||
self.sendj({"error": str(exc)}, 403)
|
||||
return False
|
||||
|
||||
|
||||
def _checked_branch(self, data):
|
||||
if not _validate_request(self, data, policy.validate_branch_request):
|
||||
return
|
||||
return _branch(self, data)
|
||||
|
||||
|
||||
def _checked_file(self, data):
|
||||
data = dict(data)
|
||||
data.setdefault("branch", policy.allowed_branch())
|
||||
if not _validate_request(self, data, policy.validate_branch_request):
|
||||
return
|
||||
if not _validate_request(
|
||||
self,
|
||||
{"path": data.get("path", "")},
|
||||
lambda item: policy.validate_path(item["path"], api.WS),
|
||||
):
|
||||
return
|
||||
return _file(self, data)
|
||||
|
||||
|
||||
def _checked_commit(self, data):
|
||||
data = dict(data)
|
||||
data.setdefault("branch", policy.allowed_branch())
|
||||
if not _validate_request(self, data, policy.validate_branch_request):
|
||||
return
|
||||
return _commit(self, data)
|
||||
|
||||
|
||||
def _checked_patch(self, data):
|
||||
data = dict(data)
|
||||
data["branch"] = policy.allowed_branch()
|
||||
if not _validate_request(self, data, policy.validate_branch_request):
|
||||
return
|
||||
return _patch(self, data)
|
||||
|
||||
|
||||
_branch, _file, _commit, _patch = api.H.branch, api.H.file, api.H.commit, api.H.patch
|
||||
api.reject_non_read_http = reject_non_read_http
|
||||
api.checkout, api.write_file, api.sync_branch, api.commit, api.audit = (
|
||||
checkout,
|
||||
write_file,
|
||||
sync_branch,
|
||||
commit,
|
||||
audit,
|
||||
)
|
||||
api.H.branch, api.H.file, api.H.commit, api.H.patch = (
|
||||
_checked_branch,
|
||||
_checked_file,
|
||||
_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)
|
||||
api.ThreadingHTTPServer((api.HOST, api.PORT), api.H).serve_forever()
|
||||
@@ -0,0 +1,310 @@
|
||||
"""Fail-closed policy used only by the task-scoped Action writer."""
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import json
|
||||
import stat
|
||||
import subprocess
|
||||
import re
|
||||
from pathlib import Path, PurePosixPath, PureWindowsPath
|
||||
from urllib.parse import unquote
|
||||
|
||||
import isolated_writer_path_policy as path_policy
|
||||
|
||||
WRITE_ENDPOINTS = frozenset({"/api/commit-file"})
|
||||
ALLOWED_ROOTS = (
|
||||
"active-runtime",
|
||||
"docs",
|
||||
"patches",
|
||||
"reports",
|
||||
"scripts",
|
||||
"test",
|
||||
"tests",
|
||||
)
|
||||
DENIED_ROOTS = ("baseline", "config", "deploy", "deployments", "ops", "runtime", "services")
|
||||
_TRUE = frozenset({"1", "true", "yes", "on"})
|
||||
_FALSE = frozenset({"0", "false", "no", "off"})
|
||||
_DENY_FLAGS = (
|
||||
"AIOS_GITOPS_ALLOW_MAIN_WRITE",
|
||||
"AIOS_GITOPS_ALLOW_TAG_WRITE",
|
||||
"AIOS_GITOPS_ALLOW_BASELINE_WRITE",
|
||||
"AIOS_GITOPS_ALLOW_REGISTRY_WRITE",
|
||||
"AIOS_GITOPS_ALLOW_ROLLBACK",
|
||||
"AIOS_GITOPS_ALLOW_DEPLOYMENT",
|
||||
)
|
||||
|
||||
|
||||
def enforcement_required() -> bool:
|
||||
return os.getenv("AIOS_GITOPS_ISOLATED_WRITER_ENFORCEMENT", "") == "required"
|
||||
|
||||
|
||||
def allowed_branch() -> str:
|
||||
value = os.getenv("AIOS_GITOPS_ALLOWED_BRANCH", "").strip()
|
||||
if not value:
|
||||
raise PermissionError("isolated_writer_missing_allowed_branch")
|
||||
return value
|
||||
|
||||
|
||||
def allowed_base_oid() -> str:
|
||||
value = os.getenv("AIOS_GITOPS_ALLOWED_BASE_OID", "").strip().lower()
|
||||
if len(value) != 40 or any(ch not in "0123456789abcdef" for ch in value):
|
||||
raise PermissionError("isolated_writer_invalid_allowed_base_oid")
|
||||
return value
|
||||
|
||||
|
||||
def _git(workspace: Path, *args: str) -> str:
|
||||
result = subprocess.run(["git", *args], cwd=workspace, text=True, capture_output=True)
|
||||
if result.returncode != 0:
|
||||
raise PermissionError("isolated_writer_git_validation_failed")
|
||||
return result.stdout.strip()
|
||||
|
||||
|
||||
def require_service_invariants(workspace: Path, bare_repo: Path) -> None:
|
||||
if not enforcement_required():
|
||||
return
|
||||
if os.getenv("AIOS_GITOPS_WRITES_ENABLED", "").strip().lower() not in _TRUE:
|
||||
raise PermissionError("isolated_writer_write_gate_required")
|
||||
if os.getenv("AIOS_GITOPS_RUNTIME_ROLE", "") != "isolated-writer":
|
||||
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")
|
||||
if os.getenv("AIOS_GITOPS_REPOSITORY_IDENTITY", "") != "aios/aios":
|
||||
raise PermissionError("isolated_writer_repository_identity_rejected")
|
||||
configured_bare_value = os.getenv("AIOS_GITOPS_AUTHORITATIVE_GIT_DIR", "").strip()
|
||||
if not configured_bare_value:
|
||||
raise PermissionError("isolated_writer_authoritative_repo_rejected")
|
||||
configured_bare = Path(configured_bare_value)
|
||||
if configured_bare.resolve(strict=True) != bare_repo.resolve(strict=True):
|
||||
raise PermissionError("isolated_writer_authoritative_repo_rejected")
|
||||
writer_root_value = os.getenv("AIOS_GITOPS_APPROVED_WRITER_ROOT", "").strip()
|
||||
if not writer_root_value:
|
||||
raise PermissionError("isolated_writer_workspace_outside_approved_root")
|
||||
writer_root = Path(writer_root_value).resolve(strict=True)
|
||||
lexical_workspace = Path(os.path.abspath(str(workspace)))
|
||||
canonical_workspace = lexical_workspace.resolve(strict=True)
|
||||
if lexical_workspace != canonical_workspace:
|
||||
raise PermissionError("isolated_writer_workspace_symlink_rejected")
|
||||
try:
|
||||
canonical_workspace.relative_to(writer_root)
|
||||
except (ValueError, FileNotFoundError):
|
||||
raise PermissionError("isolated_writer_workspace_outside_approved_root") from None
|
||||
if _git(workspace, "rev-parse", "--abbrev-ref", "HEAD") != allowed_branch():
|
||||
raise PermissionError("isolated_writer_current_branch_rejected")
|
||||
approved_base = allowed_base_oid()
|
||||
try:
|
||||
_git(workspace, "cat-file", "-e", f"{approved_base}^{{commit}}")
|
||||
subprocess.run(
|
||||
["git", "merge-base", "--is-ancestor", approved_base, "HEAD"],
|
||||
cwd=workspace,
|
||||
check=True,
|
||||
stdout=subprocess.DEVNULL,
|
||||
stderr=subprocess.DEVNULL,
|
||||
)
|
||||
except (PermissionError, subprocess.CalledProcessError):
|
||||
raise PermissionError("isolated_writer_approved_base_not_ancestor") from None
|
||||
if _git(workspace, "merge-base", approved_base, "HEAD") != approved_base:
|
||||
raise PermissionError("isolated_writer_merge_base_rejected")
|
||||
common = Path(_git(workspace, "rev-parse", "--git-common-dir"))
|
||||
if not common.is_absolute():
|
||||
common = (workspace / common).resolve()
|
||||
if common.resolve() != bare_repo.resolve():
|
||||
raise PermissionError("isolated_writer_common_repo_rejected")
|
||||
|
||||
|
||||
def approved_audit_log_path() -> Path:
|
||||
root_value = os.getenv("AIOS_GITOPS_APPROVED_AUDIT_ROOT", "").strip()
|
||||
log_value = os.getenv("AIOS_GITOPS_AUDIT_LOG", "").strip()
|
||||
if not root_value or not log_value:
|
||||
raise PermissionError("isolated_writer_audit_path_missing")
|
||||
lexical_root = Path(os.path.abspath(root_value))
|
||||
lexical_log = Path(os.path.abspath(log_value))
|
||||
root = lexical_root.resolve(strict=True)
|
||||
if lexical_root != root or root.is_symlink() or not root.is_dir():
|
||||
raise PermissionError("isolated_writer_audit_root_rejected")
|
||||
try:
|
||||
lexical_log.relative_to(root)
|
||||
except ValueError:
|
||||
raise PermissionError("isolated_writer_audit_path_outside_approved_root") from None
|
||||
if lexical_log.parent.resolve(strict=True) != lexical_log.parent:
|
||||
raise PermissionError("isolated_writer_audit_parent_symlink_rejected")
|
||||
if lexical_log.exists() and lexical_log.is_symlink():
|
||||
raise PermissionError("isolated_writer_audit_log_symlink_rejected")
|
||||
os.chmod(root, 0o750)
|
||||
os.chmod(lexical_log.parent, 0o750)
|
||||
return lexical_log
|
||||
|
||||
|
||||
def append_audit_record(record: dict) -> None:
|
||||
path = approved_audit_log_path()
|
||||
flags = os.O_WRONLY | os.O_APPEND | os.O_CREAT
|
||||
if hasattr(os, "O_NOFOLLOW"):
|
||||
flags |= os.O_NOFOLLOW
|
||||
descriptor = os.open(path, flags, 0o600)
|
||||
try:
|
||||
info = os.fstat(descriptor)
|
||||
if not stat.S_ISREG(info.st_mode):
|
||||
raise PermissionError("isolated_writer_audit_log_not_regular")
|
||||
os.fchmod(descriptor, 0o600)
|
||||
line = json.dumps(
|
||||
_audit_sanitize(record),
|
||||
ensure_ascii=False,
|
||||
sort_keys=True,
|
||||
separators=(",", ":"),
|
||||
).encode("utf-8") + b"\n"
|
||||
os.write(descriptor, line)
|
||||
os.fsync(descriptor)
|
||||
finally:
|
||||
os.close(descriptor)
|
||||
parent_fd = os.open(path.parent, os.O_RDONLY | getattr(os, "O_DIRECTORY", 0))
|
||||
try:
|
||||
os.fsync(parent_fd)
|
||||
finally:
|
||||
os.close(parent_fd)
|
||||
|
||||
|
||||
def _audit_sanitize(value):
|
||||
if isinstance(value, dict):
|
||||
return {
|
||||
key: (
|
||||
"[REDACTED]"
|
||||
if any(
|
||||
marker in str(key).lower()
|
||||
for marker in (
|
||||
"authorization",
|
||||
"credential",
|
||||
"password",
|
||||
"secret",
|
||||
"token",
|
||||
"private_key",
|
||||
)
|
||||
)
|
||||
and str(key).lower() != "token_id"
|
||||
else _audit_sanitize(inner)
|
||||
)
|
||||
for key, inner in value.items()
|
||||
}
|
||||
if isinstance(value, list):
|
||||
return [_audit_sanitize(item) for item in value]
|
||||
if isinstance(value, str) and re.search(
|
||||
r"(?i)(authorization\s*:\s*bearer|(?:token|password|secret)\s*[:=])",
|
||||
value,
|
||||
):
|
||||
return "[REDACTED]"
|
||||
return value
|
||||
|
||||
|
||||
def validate_endpoint(method: str, path: str, workspace: Path, bare_repo: Path) -> None:
|
||||
if not enforcement_required():
|
||||
return
|
||||
if method != "POST" or path not in WRITE_ENDPOINTS:
|
||||
raise PermissionError("isolated_writer_operation_forbidden")
|
||||
require_service_invariants(workspace, bare_repo)
|
||||
|
||||
|
||||
def validate_branch_request(data: dict) -> None:
|
||||
if not enforcement_required():
|
||||
return
|
||||
branch = allowed_branch() if "branch" not in data else str(data.get("branch") or "").strip()
|
||||
if branch != allowed_branch():
|
||||
raise PermissionError("isolated_writer_branch_rejected")
|
||||
if str(data.get("base", "main")).strip() != "main":
|
||||
raise PermissionError("isolated_writer_base_rejected")
|
||||
if data.get("force") or data.get("force_push"):
|
||||
raise PermissionError("isolated_writer_force_rejected")
|
||||
|
||||
|
||||
def validate_path(path: str, workspace: Path | None = None) -> str:
|
||||
if not isinstance(path, str):
|
||||
raise PermissionError("isolated_writer_path_rejected")
|
||||
value = path
|
||||
if not value or value != value.strip() or "\x00" in value or "\\" in value:
|
||||
raise PermissionError("isolated_writer_path_rejected")
|
||||
try:
|
||||
decoded = unquote(value, errors="strict")
|
||||
except (UnicodeDecodeError, ValueError):
|
||||
raise PermissionError("isolated_writer_path_rejected") from None
|
||||
if decoded != value:
|
||||
raise PermissionError("isolated_writer_path_rejected")
|
||||
windows = PureWindowsPath(value)
|
||||
posix = PurePosixPath(value)
|
||||
parts = value.split("/")
|
||||
if (
|
||||
posix.is_absolute()
|
||||
or windows.is_absolute()
|
||||
or bool(windows.drive)
|
||||
or any(part in {"", ".", ".."} for part in parts)
|
||||
):
|
||||
raise PermissionError("isolated_writer_path_rejected")
|
||||
first = parts[0]
|
||||
service_exact_path = False
|
||||
if first in DENIED_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)))
|
||||
root = lexical_root.resolve(strict=True)
|
||||
if lexical_root != root:
|
||||
raise PermissionError("isolated_writer_workspace_symlink_rejected")
|
||||
current = root
|
||||
for part in parts:
|
||||
current = current / part
|
||||
if current.is_symlink():
|
||||
raise PermissionError("isolated_writer_path_symlink_rejected")
|
||||
candidate = (root / value).resolve(strict=False)
|
||||
try:
|
||||
candidate.relative_to(root)
|
||||
except ValueError:
|
||||
raise PermissionError("isolated_writer_path_escapes_workspace") from None
|
||||
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:
|
||||
if str(path).strip() != ".":
|
||||
validate_path(str(path), workspace)
|
||||
|
||||
|
||||
def validate_staged_paths(workspace: Path) -> None:
|
||||
if not enforcement_required():
|
||||
return
|
||||
for path in _git(workspace, "diff", "--cached", "--name-only").splitlines():
|
||||
validate_path(path, workspace)
|
||||
|
||||
|
||||
def validate_sync_branch(branch: str) -> None:
|
||||
if enforcement_required() and branch != allowed_branch():
|
||||
raise PermissionError("isolated_writer_sync_branch_rejected")
|
||||
@@ -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())
|
||||
@@ -46,6 +46,57 @@ def redact(value: Any) -> str:
|
||||
return text
|
||||
|
||||
|
||||
class EnvironmentAllowList:
|
||||
"""Build a subprocess environment without inheriting process secrets."""
|
||||
|
||||
SENSITIVE_NAME_RE = re.compile(
|
||||
r"(?i)(authorization|credential|password|secret|token|api[_-]?key|"
|
||||
r"askpass|auth[_-]?sock|ld_preload|pythonpath|database_url)"
|
||||
)
|
||||
|
||||
def __init__(self, allowed_names: set[str] | frozenset[str]):
|
||||
self.allowed_names = frozenset(str(name) for name in allowed_names)
|
||||
|
||||
def build(self, values: dict[str, str]) -> dict[str, str]:
|
||||
result: dict[str, str] = {}
|
||||
for name, raw in values.items():
|
||||
if (
|
||||
name not in self.allowed_names
|
||||
or self.SENSITIVE_NAME_RE.search(name)
|
||||
):
|
||||
raise ValueError(f"environment variable is not allowed: {name}")
|
||||
value = str(raw)
|
||||
if redact(value) != value:
|
||||
raise ValueError(
|
||||
f"environment value is not allowed: {name}"
|
||||
)
|
||||
result[name] = value
|
||||
return result
|
||||
|
||||
|
||||
SAFE_PROCESS_ENVIRONMENT = EnvironmentAllowList(
|
||||
{
|
||||
"PATH",
|
||||
"LANG",
|
||||
"LC_ALL",
|
||||
"TZ",
|
||||
"HOME",
|
||||
"GIT_OPTIONAL_LOCKS",
|
||||
"PYTHONDONTWRITEBYTECODE",
|
||||
}
|
||||
).build(
|
||||
{
|
||||
"PATH": "/usr/bin:/bin",
|
||||
"LANG": "C.UTF-8",
|
||||
"LC_ALL": "C.UTF-8",
|
||||
"TZ": "UTC",
|
||||
"HOME": "/var/empty",
|
||||
"GIT_OPTIONAL_LOCKS": "0",
|
||||
"PYTHONDONTWRITEBYTECODE": "1",
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def _sanitize(value: Any) -> Any:
|
||||
if isinstance(value, dict):
|
||||
return {
|
||||
@@ -510,11 +561,16 @@ class DeploymentController:
|
||||
ledger: ApprovalLedger,
|
||||
policy: SilentReleasePolicy,
|
||||
store: ReleaseCandidateStore | None = None,
|
||||
*,
|
||||
allow_legacy_unsigned_approval: bool = False,
|
||||
):
|
||||
self.deployment = deployment
|
||||
self.ledger = ledger
|
||||
self.policy = policy
|
||||
self.store = store
|
||||
self.allow_legacy_unsigned_approval = (
|
||||
allow_legacy_unsigned_approval
|
||||
)
|
||||
|
||||
def _save(self, candidate: ReleaseCandidate) -> None:
|
||||
if self.store:
|
||||
@@ -525,6 +581,11 @@ class DeploymentController:
|
||||
candidate: ReleaseCandidate,
|
||||
approval: ApprovalEnvelope,
|
||||
) -> dict[str, Any]:
|
||||
if not self.allow_legacy_unsigned_approval:
|
||||
raise ApprovalError(
|
||||
"legacy unsigned runtime approval is disabled; "
|
||||
"use SignedReleasePlanApproval"
|
||||
)
|
||||
if candidate.state != ReleaseState.READY_FOR_APPROVAL:
|
||||
raise ApprovalError("candidate is not ready for approval")
|
||||
if not self.policy.requires_owner("production_runtime_switch"):
|
||||
@@ -675,7 +736,7 @@ class CommandReleaseBuilder:
|
||||
text=True,
|
||||
capture_output=True,
|
||||
timeout=self.config.timeout,
|
||||
env={**os.environ, "PYTHONDONTWRITEBYTECODE": "1"},
|
||||
env=dict(SAFE_PROCESS_ENVIRONMENT),
|
||||
)
|
||||
except (OSError, subprocess.TimeoutExpired) as exc:
|
||||
raise RuntimeError("release build unavailable") from exc
|
||||
@@ -741,12 +802,25 @@ class TagController:
|
||||
adapter: TagAdapter,
|
||||
policy: SilentReleasePolicy,
|
||||
ledger: ApprovalLedger | None,
|
||||
*,
|
||||
allow_legacy_unsigned_approval: bool = False,
|
||||
):
|
||||
self.adapter = adapter
|
||||
self.policy = policy
|
||||
self.ledger = ledger
|
||||
self.allow_legacy_unsigned_approval = (
|
||||
allow_legacy_unsigned_approval
|
||||
)
|
||||
|
||||
def _require_legacy_test_mode(self) -> None:
|
||||
if not self.allow_legacy_unsigned_approval:
|
||||
raise ApprovalError(
|
||||
"legacy unsigned tag approval is disabled; "
|
||||
"use SignedReleasePlanApproval"
|
||||
)
|
||||
|
||||
def approve(self, envelope: TagApprovalEnvelope) -> TagApprovalEnvelope:
|
||||
self._require_legacy_test_mode()
|
||||
if not self.policy.requires_owner("production_tag"):
|
||||
raise ApprovalError("production tag is not owner governed")
|
||||
if not self.policy.is_forbidden("tag_rewrite"):
|
||||
@@ -757,6 +831,7 @@ class TagController:
|
||||
def execute(
|
||||
self, approved: TagApprovalEnvelope
|
||||
) -> dict[str, Any]:
|
||||
self._require_legacy_test_mode()
|
||||
approved.validate()
|
||||
existing = self.adapter.lookup(approved.version)
|
||||
expected = (approved.tag_oid, approved.target_oid)
|
||||
@@ -793,6 +868,7 @@ class TagController:
|
||||
def verify(
|
||||
self, approved: TagApprovalEnvelope
|
||||
) -> dict[str, Any]:
|
||||
self._require_legacy_test_mode()
|
||||
actual = self.adapter.verify(approved.version)
|
||||
expected = (approved.tag_oid, approved.target_oid)
|
||||
if actual != expected:
|
||||
@@ -837,7 +913,7 @@ class CommandReleaseProbe:
|
||||
text=True,
|
||||
capture_output=True,
|
||||
timeout=300,
|
||||
env={**os.environ, "GIT_OPTIONAL_LOCKS": "0"},
|
||||
env=dict(SAFE_PROCESS_ENVIRONMENT),
|
||||
)
|
||||
except (OSError, subprocess.TimeoutExpired) as exc:
|
||||
raise RuntimeError("COMMAND_UNAVAILABLE") from exc
|
||||
@@ -1000,6 +1076,13 @@ class SystemdDeploymentConfig:
|
||||
health_url: str
|
||||
command_prefix: tuple[str, ...] = ()
|
||||
dropin_name: str = "aios-gitops-model-api-production.conf"
|
||||
service_allowlist: tuple[str, ...] = ("aios-gitops-model-api",)
|
||||
dropin_allowlist: tuple[str, ...] = (
|
||||
"aios-gitops-model-api-production.conf",
|
||||
)
|
||||
deployment_lock: Path = Path(
|
||||
"/run/lock/aios-silent-release-deployment.lock"
|
||||
)
|
||||
|
||||
|
||||
class SystemdDeploymentAdapter:
|
||||
@@ -1007,39 +1090,134 @@ class SystemdDeploymentAdapter:
|
||||
|
||||
def __init__(self, config: SystemdDeploymentConfig):
|
||||
self.config = config
|
||||
self._lock_handle = None
|
||||
self._restart_evidence: dict[str, Any] = {}
|
||||
if self.config.service_name not in self.config.service_allowlist:
|
||||
raise ValueError("systemd service is not allowlisted")
|
||||
if (
|
||||
self.config.dropin_name not in self.config.dropin_allowlist
|
||||
or "/" in self.config.dropin_name
|
||||
or "\\" in self.config.dropin_name
|
||||
):
|
||||
raise ValueError("systemd drop-in is not allowlisted")
|
||||
|
||||
def _acquire_lock(self) -> None:
|
||||
if self._lock_handle is not None:
|
||||
raise RuntimeError("deployment lock is already held")
|
||||
path = self.config.deployment_lock
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
handle = path.open("a+", encoding="utf-8")
|
||||
try:
|
||||
import fcntl
|
||||
|
||||
fcntl.flock(handle.fileno(), fcntl.LOCK_EX)
|
||||
except ImportError:
|
||||
pass
|
||||
self._lock_handle = handle
|
||||
|
||||
def _release_lock(self) -> None:
|
||||
handle = self._lock_handle
|
||||
self._lock_handle = None
|
||||
if handle is None:
|
||||
return
|
||||
try:
|
||||
import fcntl
|
||||
|
||||
fcntl.flock(handle.fileno(), fcntl.LOCK_UN)
|
||||
except ImportError:
|
||||
pass
|
||||
handle.close()
|
||||
|
||||
def _service_identity(self) -> dict[str, Any]:
|
||||
result = subprocess.run(
|
||||
[
|
||||
*self.config.command_prefix,
|
||||
"systemctl",
|
||||
"show",
|
||||
self.config.service_name,
|
||||
"--property=MainPID",
|
||||
"--property=ActiveEnterTimestampMonotonic",
|
||||
"--value",
|
||||
],
|
||||
check=True,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
env=dict(SAFE_PROCESS_ENVIRONMENT),
|
||||
)
|
||||
values = result.stdout.splitlines()
|
||||
if len(values) < 2:
|
||||
raise RuntimeError("systemd process identity unavailable")
|
||||
try:
|
||||
pid = int(values[0])
|
||||
started = int(values[1])
|
||||
except ValueError as exc:
|
||||
raise RuntimeError(
|
||||
"systemd process identity is invalid"
|
||||
) from exc
|
||||
return {"main_pid": pid, "started_monotonic": started}
|
||||
|
||||
def atomic_switch(
|
||||
self, candidate: ReleaseCandidate
|
||||
) -> dict[str, Any]:
|
||||
target = (
|
||||
self.config.release_root
|
||||
/ candidate.source_commit
|
||||
/ "deployment"
|
||||
/ self.config.dropin_name
|
||||
).resolve(strict=True)
|
||||
link = self.config.cutover_link
|
||||
if link.exists() and not link.is_symlink():
|
||||
raise RuntimeError("cutover path is not a managed symlink")
|
||||
previous = os.readlink(link) if link.is_symlink() else None
|
||||
link.parent.mkdir(parents=True, exist_ok=True)
|
||||
temporary = link.with_name(f".{link.name}.{os.getpid()}.tmp")
|
||||
if temporary.exists() or temporary.is_symlink():
|
||||
temporary.unlink()
|
||||
os.symlink(str(target), temporary)
|
||||
os.replace(temporary, link)
|
||||
return {
|
||||
"previous_target": previous,
|
||||
"new_target": str(target),
|
||||
"cutover_link": str(link),
|
||||
}
|
||||
self._acquire_lock()
|
||||
try:
|
||||
release_root = self.config.release_root.resolve(strict=True)
|
||||
raw_target = (
|
||||
release_root
|
||||
/ candidate.source_commit
|
||||
/ "deployment"
|
||||
/ self.config.dropin_name
|
||||
)
|
||||
if raw_target.is_symlink():
|
||||
raise RuntimeError("release drop-in must not be a symlink")
|
||||
target = raw_target.resolve(strict=True)
|
||||
if target.parent.parent.parent != release_root:
|
||||
raise RuntimeError("release drop-in escapes release root")
|
||||
link = self.config.cutover_link
|
||||
if link.exists() and not link.is_symlink():
|
||||
raise RuntimeError("cutover path is not a managed symlink")
|
||||
previous = os.readlink(link) if link.is_symlink() else None
|
||||
if previous:
|
||||
previous_path = Path(previous)
|
||||
if not previous_path.is_absolute():
|
||||
previous_path = link.parent / previous_path
|
||||
try:
|
||||
previous_path.resolve().relative_to(release_root)
|
||||
except ValueError as exc:
|
||||
raise RuntimeError(
|
||||
"existing cutover symlink escapes release root"
|
||||
) from exc
|
||||
link.parent.mkdir(parents=True, exist_ok=True)
|
||||
temporary = link.with_name(f".{link.name}.{os.getpid()}.tmp")
|
||||
if temporary.exists() or temporary.is_symlink():
|
||||
temporary.unlink()
|
||||
os.symlink(str(target), temporary)
|
||||
os.replace(temporary, link)
|
||||
return {
|
||||
"previous_target": previous,
|
||||
"new_target": str(target),
|
||||
"cutover_link": str(link),
|
||||
}
|
||||
except Exception:
|
||||
self._release_lock()
|
||||
raise
|
||||
|
||||
def restart(self) -> None:
|
||||
if self._lock_handle is None:
|
||||
raise RuntimeError("deployment lock is not held")
|
||||
before = self._service_identity()
|
||||
command = [
|
||||
*self.config.command_prefix,
|
||||
"systemctl",
|
||||
"daemon-reload",
|
||||
]
|
||||
subprocess.run(command, check=True, capture_output=True, text=True)
|
||||
subprocess.run(
|
||||
command,
|
||||
check=True,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
env=dict(SAFE_PROCESS_ENVIRONMENT),
|
||||
)
|
||||
subprocess.run(
|
||||
[
|
||||
*self.config.command_prefix,
|
||||
@@ -1050,7 +1228,18 @@ class SystemdDeploymentAdapter:
|
||||
check=True,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
env=dict(SAFE_PROCESS_ENVIRONMENT),
|
||||
)
|
||||
after = self._service_identity()
|
||||
if (
|
||||
after["main_pid"] <= 0
|
||||
or after["started_monotonic"]
|
||||
<= before["started_monotonic"]
|
||||
):
|
||||
raise RuntimeError(
|
||||
"systemd restart did not produce a new start identity"
|
||||
)
|
||||
self._restart_evidence = {"before": before, "after": after}
|
||||
|
||||
def health_check(
|
||||
self, candidate: ReleaseCandidate
|
||||
@@ -1062,44 +1251,58 @@ class SystemdDeploymentAdapter:
|
||||
payload = json.loads(response.read().decode("utf-8"))
|
||||
except Exception as exc:
|
||||
raise RuntimeError("runtime health check unavailable") from exc
|
||||
return {
|
||||
"passed": response.status == 200,
|
||||
"runtime_commit": payload.get("runtime_release_commit"),
|
||||
"writes_enabled": payload.get("writes_enabled"),
|
||||
"authoritative_head_oid": payload.get(
|
||||
"authoritative_head_oid"
|
||||
),
|
||||
"repository_identity": payload.get("repository_identity"),
|
||||
}
|
||||
try:
|
||||
return {
|
||||
"passed": response.status == 200,
|
||||
"runtime_commit": payload.get("runtime_release_commit"),
|
||||
"writes_enabled": payload.get("writes_enabled"),
|
||||
"authoritative_head_oid": payload.get(
|
||||
"authoritative_head_oid"
|
||||
),
|
||||
"repository_identity": payload.get("repository_identity"),
|
||||
"systemd_restart": _sanitize(self._restart_evidence),
|
||||
}
|
||||
finally:
|
||||
self._release_lock()
|
||||
|
||||
def rollback(
|
||||
self, switch_evidence: dict[str, Any]
|
||||
) -> dict[str, Any]:
|
||||
link = self.config.cutover_link
|
||||
previous = switch_evidence.get("previous_target")
|
||||
temporary = link.with_name(f".{link.name}.{os.getpid()}.rollback")
|
||||
if previous:
|
||||
if temporary.exists() or temporary.is_symlink():
|
||||
temporary.unlink()
|
||||
os.symlink(str(previous), temporary)
|
||||
os.replace(temporary, link)
|
||||
elif link.is_symlink():
|
||||
link.unlink()
|
||||
self.restart()
|
||||
try:
|
||||
with urllib.request.urlopen(
|
||||
self.config.health_url, timeout=15
|
||||
) as response:
|
||||
payload = json.loads(response.read().decode("utf-8"))
|
||||
except Exception as exc:
|
||||
raise RuntimeError(
|
||||
"rollback health check unavailable"
|
||||
) from exc
|
||||
return {
|
||||
"passed": response.status == 200,
|
||||
"writes_enabled": payload.get("writes_enabled"),
|
||||
"runtime_commit": payload.get("runtime_release_commit"),
|
||||
}
|
||||
if self._lock_handle is None:
|
||||
self._acquire_lock()
|
||||
link = self.config.cutover_link
|
||||
previous = switch_evidence.get("previous_target")
|
||||
temporary = link.with_name(
|
||||
f".{link.name}.{os.getpid()}.rollback"
|
||||
)
|
||||
if previous:
|
||||
if temporary.exists() or temporary.is_symlink():
|
||||
temporary.unlink()
|
||||
os.symlink(str(previous), temporary)
|
||||
os.replace(temporary, link)
|
||||
elif link.is_symlink():
|
||||
link.unlink()
|
||||
self.restart()
|
||||
try:
|
||||
with urllib.request.urlopen(
|
||||
self.config.health_url, timeout=15
|
||||
) as response:
|
||||
payload = json.loads(
|
||||
response.read().decode("utf-8")
|
||||
)
|
||||
except Exception as exc:
|
||||
raise RuntimeError(
|
||||
"rollback health check unavailable"
|
||||
) from exc
|
||||
return {
|
||||
"passed": response.status == 200,
|
||||
"writes_enabled": payload.get("writes_enabled"),
|
||||
"runtime_commit": payload.get("runtime_release_commit"),
|
||||
"systemd_restart": _sanitize(self._restart_evidence),
|
||||
}
|
||||
finally:
|
||||
self._release_lock()
|
||||
|
||||
|
||||
def _sha256(path: Path) -> str:
|
||||
|
||||
@@ -0,0 +1,564 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Execution-readiness adapters and the fail-closed Shadow controller service."""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import base64
|
||||
import dataclasses
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
import subprocess
|
||||
import threading
|
||||
import time
|
||||
from http import HTTPStatus
|
||||
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
||||
from pathlib import Path
|
||||
from typing import Any, Callable, Mapping, Protocol
|
||||
from urllib.parse import parse_qs, urlparse
|
||||
|
||||
from cryptography.hazmat.primitives.asymmetric.ed25519 import (
|
||||
Ed25519PrivateKey,
|
||||
)
|
||||
|
||||
from silent_release_controller import _sanitize, utc_now
|
||||
from silent_release_hardening import (
|
||||
BuilderAttestation,
|
||||
CapabilityAuthorizationError,
|
||||
ReleaseRecoveryWorker,
|
||||
SignedApprovalVerifier,
|
||||
SignedReleasePlanApproval,
|
||||
SilentReleaseActionService,
|
||||
TrustedCandidateBundle,
|
||||
_canonical_json,
|
||||
_durable_json,
|
||||
_safe_child,
|
||||
)
|
||||
|
||||
|
||||
class BuilderSigner(Protocol):
|
||||
@property
|
||||
def key_id(self) -> str:
|
||||
...
|
||||
|
||||
def sign(self, payload: bytes) -> bytes:
|
||||
...
|
||||
|
||||
|
||||
class LocalTestBuilderSigner:
|
||||
"""Ephemeral in-memory signer. It is forbidden for production execution."""
|
||||
|
||||
test_only = True
|
||||
|
||||
def __init__(self, key_id: str, private_key: Ed25519PrivateKey):
|
||||
self._key_id = str(key_id)
|
||||
self._private_key = private_key
|
||||
|
||||
@property
|
||||
def key_id(self) -> str:
|
||||
return self._key_id
|
||||
|
||||
@classmethod
|
||||
def generate(
|
||||
cls, key_id: str = "shadow-builder-test-key"
|
||||
) -> "LocalTestBuilderSigner":
|
||||
return cls(key_id, Ed25519PrivateKey.generate())
|
||||
|
||||
@property
|
||||
def public_key(self):
|
||||
return self._private_key.public_key()
|
||||
|
||||
def sign(self, payload: bytes) -> bytes:
|
||||
return self._private_key.sign(payload)
|
||||
|
||||
|
||||
class ExternalKmsBuilderSigner:
|
||||
"""Signer facade whose private operation remains in KMS/HSM."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
key_id: str,
|
||||
credential_reference: str,
|
||||
sign_callback: Callable[[str, bytes], bytes],
|
||||
):
|
||||
if not str(credential_reference).startswith(
|
||||
("kms://", "hsm://", "secret-manager://")
|
||||
):
|
||||
raise ValueError("production builder requires a KMS/HSM reference")
|
||||
self._key_id = str(key_id)
|
||||
self.credential_reference = str(credential_reference)
|
||||
self._sign_callback = sign_callback
|
||||
|
||||
@property
|
||||
def key_id(self) -> str:
|
||||
return self._key_id
|
||||
|
||||
def sign(self, payload: bytes) -> bytes:
|
||||
return bytes(self._sign_callback(self.credential_reference, payload))
|
||||
|
||||
|
||||
class SignedTrustedBuilderAdapter:
|
||||
"""Signs evidence produced by a trusted collector, never caller evidence."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
collector: Callable[[Mapping[str, Any]], TrustedCandidateBundle],
|
||||
signer: BuilderSigner,
|
||||
):
|
||||
self.collector = collector
|
||||
self.signer = signer
|
||||
|
||||
def build(self, intent: Mapping[str, Any]) -> TrustedCandidateBundle:
|
||||
forbidden = {
|
||||
"artifact_sha256",
|
||||
"canary",
|
||||
"checks",
|
||||
"manifest_sha256",
|
||||
"tests",
|
||||
"tree_oid",
|
||||
}
|
||||
if forbidden.intersection(intent):
|
||||
raise CapabilityAuthorizationError(
|
||||
"trusted builder rejects caller-supplied evidence"
|
||||
)
|
||||
unsigned = self.collector(dict(intent))
|
||||
if unsigned.attestation is not None:
|
||||
raise CapabilityAuthorizationError(
|
||||
"collector cannot self-attest its evidence"
|
||||
)
|
||||
candidate = unsigned.candidate
|
||||
test_sha = hashlib.sha256(
|
||||
_canonical_json(_sanitize(candidate.tests))
|
||||
).hexdigest()
|
||||
canary_sha = hashlib.sha256(
|
||||
_canonical_json(_sanitize(candidate.canary))
|
||||
).hexdigest()
|
||||
attestation_id = "builder-attestation-" + hashlib.sha256(
|
||||
(
|
||||
candidate.candidate_id
|
||||
+ "\0"
|
||||
+ candidate.source_commit
|
||||
+ "\0"
|
||||
+ candidate.artifact_sha256
|
||||
).encode("utf-8")
|
||||
).hexdigest()[:24]
|
||||
attestation = BuilderAttestation(
|
||||
builder_attestation_id=attestation_id,
|
||||
builder_key_id=self.signer.key_id,
|
||||
source_commit=candidate.source_commit,
|
||||
tree_oid=candidate.tree_oid,
|
||||
artifact_sha256=candidate.artifact_sha256,
|
||||
manifest_sha256=candidate.manifest_sha256,
|
||||
package_closure_sha256=(
|
||||
unsigned.release_plan.deployment.package_closure_sha256
|
||||
),
|
||||
test_evidence_sha256=test_sha,
|
||||
canary_evidence_sha256=canary_sha,
|
||||
issued_at=utc_now(),
|
||||
signature="",
|
||||
)
|
||||
attestation = dataclasses.replace(
|
||||
attestation,
|
||||
signature=base64.b64encode(
|
||||
self.signer.sign(attestation.canonical_bytes())
|
||||
).decode("ascii"),
|
||||
)
|
||||
return dataclasses.replace(unsigned, attestation=attestation)
|
||||
|
||||
|
||||
class ApprovalBackendAdapter(Protocol):
|
||||
@property
|
||||
def available(self) -> bool:
|
||||
...
|
||||
|
||||
def verify(
|
||||
self,
|
||||
approval: SignedReleasePlanApproval,
|
||||
*,
|
||||
candidate,
|
||||
release_plan,
|
||||
policy_sha256: str,
|
||||
) -> SignedReleasePlanApproval:
|
||||
...
|
||||
|
||||
|
||||
class LocalTestApprovalSigner:
|
||||
"""Test-only approval signer used solely for Shadow acceptance."""
|
||||
|
||||
test_only = True
|
||||
|
||||
def __init__(self, key_id: str, private_key: Ed25519PrivateKey):
|
||||
self.key_id = str(key_id)
|
||||
self.private_key = private_key
|
||||
|
||||
@classmethod
|
||||
def generate(
|
||||
cls, key_id: str = "shadow-owner-test-key"
|
||||
) -> "LocalTestApprovalSigner":
|
||||
return cls(key_id, Ed25519PrivateKey.generate())
|
||||
|
||||
def sign(
|
||||
self, approval: SignedReleasePlanApproval
|
||||
) -> SignedReleasePlanApproval:
|
||||
unsigned = dataclasses.replace(
|
||||
approval, key_id=self.key_id, signature=""
|
||||
)
|
||||
return dataclasses.replace(
|
||||
unsigned,
|
||||
signature=base64.b64encode(
|
||||
self.private_key.sign(unsigned.canonical_bytes())
|
||||
).decode("ascii"),
|
||||
)
|
||||
|
||||
|
||||
class ProductionApprovalBackendAdapter:
|
||||
"""Verification-only production adapter configured by a secret/KMS ref."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
verifier: SignedApprovalVerifier,
|
||||
credential_reference: str,
|
||||
):
|
||||
if not str(credential_reference).startswith(
|
||||
("kms://", "hsm://", "secret-manager://")
|
||||
):
|
||||
raise ValueError(
|
||||
"production approval backend requires a KMS/secret reference"
|
||||
)
|
||||
self.verifier = verifier
|
||||
self.credential_reference = str(credential_reference)
|
||||
|
||||
@property
|
||||
def available(self) -> bool:
|
||||
return True
|
||||
|
||||
def verify(
|
||||
self,
|
||||
approval: SignedReleasePlanApproval,
|
||||
*,
|
||||
candidate,
|
||||
release_plan,
|
||||
policy_sha256: str,
|
||||
) -> SignedReleasePlanApproval:
|
||||
return self.verifier.verify_release_plan(
|
||||
approval, candidate, release_plan, policy_sha256
|
||||
)
|
||||
|
||||
|
||||
class UnavailableProductionApprovalBackend:
|
||||
credential_reference = None
|
||||
|
||||
@property
|
||||
def available(self) -> bool:
|
||||
return False
|
||||
|
||||
def verify(self, *args, **kwargs):
|
||||
raise CapabilityAuthorizationError(
|
||||
"production approval backend is not configured"
|
||||
)
|
||||
|
||||
|
||||
class LeaderLock:
|
||||
def __init__(self, path: Path):
|
||||
self.path = Path(path)
|
||||
self.handle = None
|
||||
|
||||
def __enter__(self):
|
||||
self.path.parent.mkdir(parents=True, exist_ok=True)
|
||||
os.chmod(self.path.parent, 0o700)
|
||||
self.handle = self.path.open("a+", encoding="utf-8")
|
||||
os.chmod(self.path, 0o600)
|
||||
try:
|
||||
import fcntl
|
||||
|
||||
fcntl.flock(
|
||||
self.handle.fileno(), fcntl.LOCK_EX | fcntl.LOCK_NB
|
||||
)
|
||||
except BlockingIOError as exc:
|
||||
self.handle.close()
|
||||
self.handle = None
|
||||
raise RuntimeError("silent release leader is already active") from exc
|
||||
return self
|
||||
|
||||
def __exit__(self, exc_type, exc, tb):
|
||||
if self.handle is not None:
|
||||
try:
|
||||
import fcntl
|
||||
|
||||
fcntl.flock(self.handle.fileno(), fcntl.LOCK_UN)
|
||||
finally:
|
||||
self.handle.close()
|
||||
self.handle = None
|
||||
|
||||
|
||||
class PersistentSilentReleaseController:
|
||||
"""Idempotent main-event controller with durable startup/recovery reports."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
root: Path,
|
||||
service: SilentReleaseActionService,
|
||||
*,
|
||||
main_probe: Callable[[], str],
|
||||
recovery_worker: ReleaseRecoveryWorker | None = None,
|
||||
):
|
||||
self.root = Path(root)
|
||||
self.service = service
|
||||
self.main_probe = main_probe
|
||||
self.recovery_worker = recovery_worker
|
||||
self.leader_path = self.root / "controller.leader.lock"
|
||||
self.status_path = self.root / "controller-status.json"
|
||||
|
||||
def startup_verify(self) -> dict[str, Any]:
|
||||
checked = 0
|
||||
failed = 0
|
||||
for journal in sorted(self.service.sagas.root.glob("*.events.jsonl")):
|
||||
candidate_id = journal.name[: -len(".events.jsonl")]
|
||||
saga = self.service.sagas.load(candidate_id)
|
||||
checked += 1
|
||||
if not saga.journal_integrity:
|
||||
failed += 1
|
||||
status = {
|
||||
"checked_sagas": checked,
|
||||
"execution_enabled": self.service.execution_enabled,
|
||||
"failed_closed": failed,
|
||||
"shadow_mode": not self.service.execution_enabled,
|
||||
"side_effects_executed": False,
|
||||
"status": "READY" if failed == 0 else "FAIL_CLOSED",
|
||||
}
|
||||
_durable_json(self.status_path, status)
|
||||
return status
|
||||
|
||||
def run_once(self) -> dict[str, Any]:
|
||||
with LeaderLock(self.leader_path):
|
||||
startup = self.startup_verify()
|
||||
if startup["status"] != "READY":
|
||||
result = {
|
||||
"execution_enabled": self.service.execution_enabled,
|
||||
"intent": None,
|
||||
"notification": None,
|
||||
"recovery": None,
|
||||
"shadow_mode": not self.service.execution_enabled,
|
||||
"side_effects_executed": False,
|
||||
"startup": startup,
|
||||
"status": "FAIL_CLOSED",
|
||||
}
|
||||
_durable_json(self.status_path, result)
|
||||
return _sanitize(result)
|
||||
source_commit = str(self.main_probe())
|
||||
event_id = f"main-{source_commit}"
|
||||
intent = self.service.create_release_intent(
|
||||
{"event_id": event_id, "source_commit": source_commit}
|
||||
)
|
||||
notification = None
|
||||
if intent.get("candidate_id"):
|
||||
notification = self.service.ensure_owner_notification(
|
||||
str(intent["candidate_id"])
|
||||
)
|
||||
recovery = (
|
||||
self.recovery_worker.run_once()
|
||||
if self.recovery_worker is not None
|
||||
else self.service.get_recovery_status()
|
||||
)
|
||||
result = {
|
||||
"execution_enabled": self.service.execution_enabled,
|
||||
"intent": intent,
|
||||
"notification": notification,
|
||||
"recovery": recovery,
|
||||
"shadow_mode": not self.service.execution_enabled,
|
||||
"side_effects_executed": False,
|
||||
"startup": startup,
|
||||
}
|
||||
_durable_json(self.status_path, result)
|
||||
return _sanitize(result)
|
||||
|
||||
|
||||
class ShadowActionHandler(BaseHTTPRequestHandler):
|
||||
service: SilentReleaseActionService
|
||||
controller: PersistentSilentReleaseController
|
||||
|
||||
def log_message(self, fmt, *args):
|
||||
return
|
||||
|
||||
def _json(self, payload: Mapping[str, Any], status=HTTPStatus.OK):
|
||||
body = json.dumps(_sanitize(dict(payload)), sort_keys=True).encode()
|
||||
self.send_response(int(status))
|
||||
self.send_header("Content-Type", "application/json")
|
||||
self.send_header("Content-Length", str(len(body)))
|
||||
self.end_headers()
|
||||
self.wfile.write(body)
|
||||
|
||||
def _body(self) -> dict[str, Any]:
|
||||
size = int(self.headers.get("Content-Length", "0"))
|
||||
if size > 1024 * 1024:
|
||||
raise ValueError("request body is too large")
|
||||
return json.loads(self.rfile.read(size) or b"{}")
|
||||
|
||||
def do_GET(self):
|
||||
parsed = urlparse(self.path)
|
||||
query = parse_qs(parsed.query)
|
||||
candidate_id = str((query.get("candidate_id") or [""])[0])
|
||||
try:
|
||||
if parsed.path == "/healthz":
|
||||
return self._json(
|
||||
{
|
||||
"execution_enabled": False,
|
||||
"shadow_mode": True,
|
||||
"side_effects_executed": False,
|
||||
"status": "ok",
|
||||
}
|
||||
)
|
||||
routes = {
|
||||
"/getReleaseCandidate": self.service.get_release_candidate,
|
||||
"/getReleasePlan": self.service.get_release_plan,
|
||||
"/getReleaseAcceptance": self.service.get_acceptance,
|
||||
"/getReleaseSaga": self.service.get_release_saga,
|
||||
"/getCapabilityStatus": self.service.get_capability_status,
|
||||
}
|
||||
if parsed.path in routes:
|
||||
return self._json(routes[parsed.path](candidate_id))
|
||||
if parsed.path == "/getRecoveryStatus":
|
||||
return self._json(self.service.get_recovery_status())
|
||||
self._json({"error": "not_found"}, HTTPStatus.NOT_FOUND)
|
||||
except (ValueError, FileNotFoundError) as exc:
|
||||
self._json(
|
||||
{"error": type(exc).__name__}, HTTPStatus.BAD_REQUEST
|
||||
)
|
||||
|
||||
def do_POST(self):
|
||||
parsed = urlparse(self.path)
|
||||
try:
|
||||
body = self._body()
|
||||
if parsed.path == "/createReleaseIntent":
|
||||
return self._json(self.service.create_release_intent(body))
|
||||
if parsed.path in {
|
||||
"/submitSignedApproval",
|
||||
"/submitOwnerApproval",
|
||||
}:
|
||||
return self._json(self.service.submit_owner_approval(body))
|
||||
if parsed.path == "/startDeployment":
|
||||
return self._json(
|
||||
self.service.start_deployment(
|
||||
str(body.get("candidate_id") or "")
|
||||
)
|
||||
)
|
||||
self._json({"error": "not_found"}, HTTPStatus.NOT_FOUND)
|
||||
except (ValueError, FileNotFoundError, CapabilityAuthorizationError) as exc:
|
||||
self._json(
|
||||
{"error": type(exc).__name__}, HTTPStatus.BAD_REQUEST
|
||||
)
|
||||
|
||||
|
||||
def git_main_probe(repository: Path) -> Callable[[], str]:
|
||||
repository = Path(repository)
|
||||
|
||||
def probe() -> str:
|
||||
completed = subprocess.run(
|
||||
[
|
||||
"git",
|
||||
f"--git-dir={repository}",
|
||||
"rev-parse",
|
||||
"refs/heads/main",
|
||||
],
|
||||
check=True,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=15,
|
||||
env={"PATH": os.environ.get("PATH", "")},
|
||||
)
|
||||
return completed.stdout.strip()
|
||||
|
||||
return probe
|
||||
|
||||
|
||||
def serve_shadow(
|
||||
*,
|
||||
state_root: Path,
|
||||
repository: Path,
|
||||
host: str,
|
||||
port: int,
|
||||
poll_seconds: float,
|
||||
) -> None:
|
||||
service = SilentReleaseActionService.shadow(state_root)
|
||||
controller = PersistentSilentReleaseController(
|
||||
state_root,
|
||||
service,
|
||||
main_probe=git_main_probe(repository),
|
||||
)
|
||||
controller.run_once()
|
||||
stop = threading.Event()
|
||||
|
||||
def controller_loop() -> None:
|
||||
while not stop.wait(poll_seconds):
|
||||
try:
|
||||
controller.run_once()
|
||||
except Exception as exc: # fail closed and keep the API available
|
||||
_durable_json(
|
||||
controller.status_path,
|
||||
{
|
||||
"error": type(exc).__name__,
|
||||
"execution_enabled": False,
|
||||
"shadow_mode": True,
|
||||
"side_effects_executed": False,
|
||||
"status": "FAIL_CLOSED",
|
||||
},
|
||||
)
|
||||
|
||||
handler = type(
|
||||
"ConfiguredShadowActionHandler",
|
||||
(ShadowActionHandler,),
|
||||
{"service": service, "controller": controller},
|
||||
)
|
||||
server = ThreadingHTTPServer((host, port), handler)
|
||||
api_thread = threading.Thread(
|
||||
target=server.serve_forever,
|
||||
name="silent-release-shadow-api",
|
||||
daemon=False,
|
||||
)
|
||||
controller_thread = threading.Thread(
|
||||
target=controller_loop,
|
||||
name="silent-release-shadow-controller",
|
||||
daemon=True,
|
||||
)
|
||||
api_thread.start()
|
||||
controller_thread.start()
|
||||
try:
|
||||
while api_thread.is_alive():
|
||||
api_thread.join(timeout=1)
|
||||
finally:
|
||||
stop.set()
|
||||
server.shutdown()
|
||||
server.server_close()
|
||||
controller_thread.join(timeout=max(1.0, poll_seconds + 1.0))
|
||||
|
||||
|
||||
def main() -> None:
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--state-root", required=True, type=Path)
|
||||
parser.add_argument("--repository", required=True, type=Path)
|
||||
parser.add_argument("--host", default="127.0.0.1")
|
||||
parser.add_argument("--port", default=18891, type=int)
|
||||
parser.add_argument("--poll-seconds", default=30.0, type=float)
|
||||
arguments = parser.parse_args()
|
||||
if arguments.poll_seconds <= 0:
|
||||
raise SystemExit("poll seconds must be positive")
|
||||
if os.environ.get("AIOS_GITOPS_WRITES_ENABLED", "false").lower() != "false":
|
||||
raise SystemExit("GitOps writes must remain disabled")
|
||||
if (
|
||||
os.environ.get(
|
||||
"AIOS_SILENT_RELEASE_EXECUTION_ENABLED", "false"
|
||||
).lower()
|
||||
!= "false"
|
||||
):
|
||||
raise SystemExit("Silent Release execution must remain disabled")
|
||||
serve_shadow(
|
||||
state_root=arguments.state_root,
|
||||
repository=arguments.repository,
|
||||
host=arguments.host,
|
||||
port=arguments.port,
|
||||
poll_seconds=arguments.poll_seconds,
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,21 @@
|
||||
"""Task Plane runtime package."""
|
||||
|
||||
from .models import (
|
||||
EvidenceEnvelope,
|
||||
ProbeEvidence,
|
||||
ProbeOutcome,
|
||||
TaskPlan,
|
||||
TaskRecord,
|
||||
TaskStatus,
|
||||
TaskStateTransitionError,
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
"EvidenceEnvelope",
|
||||
"ProbeEvidence",
|
||||
"ProbeOutcome",
|
||||
"TaskPlan",
|
||||
"TaskRecord",
|
||||
"TaskStatus",
|
||||
"TaskStateTransitionError",
|
||||
]
|
||||
@@ -0,0 +1,17 @@
|
||||
"""Adapters for executing tasks via worker backends."""
|
||||
|
||||
from .base import WorkerAdapter
|
||||
from .openclaw import OpenClawAdapter
|
||||
from .codex import CodexAdapter
|
||||
from .hermes import HermesAdapter
|
||||
from .a2a import A2AAdapter
|
||||
from .systemd_worker import SystemdWorkerAdapter
|
||||
|
||||
__all__ = [
|
||||
"WorkerAdapter",
|
||||
"OpenClawAdapter",
|
||||
"CodexAdapter",
|
||||
"HermesAdapter",
|
||||
"A2AAdapter",
|
||||
"SystemdWorkerAdapter",
|
||||
]
|
||||
@@ -0,0 +1,31 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from .base import WorkerAdapter
|
||||
|
||||
|
||||
class A2AAdapter(WorkerAdapter):
|
||||
worker_name = "a2a"
|
||||
|
||||
def capabilities(self) -> list[str]:
|
||||
return ["task.exchange", "agent.handoff"]
|
||||
|
||||
def claim(self, task):
|
||||
return {"worker": self.worker_name, "task_id": task.task_id, "status": "claimed"}
|
||||
|
||||
def execute(self, task, claim):
|
||||
return {"worker": self.worker_name, "task_id": task.task_id, "status": "SHADOW_VERIFIED", "claim_id": claim.claim_id, "execution_mode": "shadow", "production_side_effects": False}
|
||||
|
||||
def heartbeat(self, claim):
|
||||
return {"worker": self.worker_name, "claim_id": claim.claim_id, "status": "alive"}
|
||||
|
||||
def report_progress(self, claim, progress):
|
||||
return {"worker": self.worker_name, "claim_id": claim.claim_id, "progress": progress}
|
||||
|
||||
def probe(self, claim):
|
||||
return {"worker": self.worker_name, "claim_id": claim.claim_id, "status": claim.status}
|
||||
|
||||
def cancel(self, claim):
|
||||
return {"worker": self.worker_name, "claim_id": claim.claim_id, "status": "cancelled"}
|
||||
|
||||
def collect_result(self, claim):
|
||||
return {"worker": self.worker_name, "claim_id": claim.claim_id, "result": "not_collected"}
|
||||
@@ -0,0 +1,41 @@
|
||||
"""Base worker adapter contract."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import abc
|
||||
|
||||
|
||||
class WorkerAdapter(abc.ABC):
|
||||
worker_name = "base"
|
||||
|
||||
@abc.abstractmethod
|
||||
def capabilities(self) -> list[str]:
|
||||
raise NotImplementedError
|
||||
|
||||
@abc.abstractmethod
|
||||
def claim(self, task):
|
||||
raise NotImplementedError
|
||||
|
||||
@abc.abstractmethod
|
||||
def execute(self, task, claim):
|
||||
raise NotImplementedError
|
||||
|
||||
@abc.abstractmethod
|
||||
def heartbeat(self, claim):
|
||||
raise NotImplementedError
|
||||
|
||||
@abc.abstractmethod
|
||||
def report_progress(self, claim, progress):
|
||||
raise NotImplementedError
|
||||
|
||||
@abc.abstractmethod
|
||||
def probe(self, claim):
|
||||
raise NotImplementedError
|
||||
|
||||
@abc.abstractmethod
|
||||
def cancel(self, claim):
|
||||
raise NotImplementedError
|
||||
|
||||
@abc.abstractmethod
|
||||
def collect_result(self, claim):
|
||||
raise NotImplementedError
|
||||
@@ -0,0 +1,31 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from .base import WorkerAdapter
|
||||
|
||||
|
||||
class CodexAdapter(WorkerAdapter):
|
||||
worker_name = "codex"
|
||||
|
||||
def capabilities(self) -> list[str]:
|
||||
return ["code.read", "code.write.patch", "test.python", "review.security"]
|
||||
|
||||
def claim(self, task):
|
||||
return {"worker": self.worker_name, "task_id": task.task_id, "status": "claimed"}
|
||||
|
||||
def execute(self, task, claim):
|
||||
return {"worker": self.worker_name, "task_id": task.task_id, "status": "SHADOW_VERIFIED", "claim_id": claim.claim_id, "execution_mode": "shadow", "production_side_effects": False}
|
||||
|
||||
def heartbeat(self, claim):
|
||||
return {"worker": self.worker_name, "claim_id": claim.claim_id, "status": "alive"}
|
||||
|
||||
def report_progress(self, claim, progress):
|
||||
return {"worker": self.worker_name, "claim_id": claim.claim_id, "progress": progress}
|
||||
|
||||
def probe(self, claim):
|
||||
return {"worker": self.worker_name, "claim_id": claim.claim_id, "status": claim.status}
|
||||
|
||||
def cancel(self, claim):
|
||||
return {"worker": self.worker_name, "claim_id": claim.claim_id, "status": "cancelled"}
|
||||
|
||||
def collect_result(self, claim):
|
||||
return {"worker": self.worker_name, "claim_id": claim.claim_id, "result": "not_collected"}
|
||||
@@ -0,0 +1,31 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from .base import WorkerAdapter
|
||||
|
||||
|
||||
class HermesAdapter(WorkerAdapter):
|
||||
worker_name = "hermes"
|
||||
|
||||
def capabilities(self) -> list[str]:
|
||||
return ["architecture.review", "policy.review", "threat.model"]
|
||||
|
||||
def claim(self, task):
|
||||
return {"worker": self.worker_name, "task_id": task.task_id, "status": "claimed"}
|
||||
|
||||
def execute(self, task, claim):
|
||||
return {"worker": self.worker_name, "task_id": task.task_id, "status": "SHADOW_VERIFIED", "claim_id": claim.claim_id, "execution_mode": "shadow", "production_side_effects": False}
|
||||
|
||||
def heartbeat(self, claim):
|
||||
return {"worker": self.worker_name, "claim_id": claim.claim_id, "status": "alive"}
|
||||
|
||||
def report_progress(self, claim, progress):
|
||||
return {"worker": self.worker_name, "claim_id": claim.claim_id, "progress": progress}
|
||||
|
||||
def probe(self, claim):
|
||||
return {"worker": self.worker_name, "claim_id": claim.claim_id, "status": claim.status}
|
||||
|
||||
def cancel(self, claim):
|
||||
return {"worker": self.worker_name, "claim_id": claim.claim_id, "status": "cancelled"}
|
||||
|
||||
def collect_result(self, claim):
|
||||
return {"worker": self.worker_name, "claim_id": claim.claim_id, "result": "not_collected"}
|
||||
@@ -0,0 +1,31 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from .base import WorkerAdapter
|
||||
|
||||
|
||||
class OpenClawAdapter(WorkerAdapter):
|
||||
worker_name = "openclaw"
|
||||
|
||||
def capabilities(self) -> list[str]:
|
||||
return ["code.read", "code.write.patch", "test.execute", "runtime.shadow"]
|
||||
|
||||
def claim(self, task):
|
||||
return {"worker": self.worker_name, "task_id": task.task_id, "status": "claimed"}
|
||||
|
||||
def execute(self, task, claim):
|
||||
return {"worker": self.worker_name, "task_id": task.task_id, "status": "SHADOW_VERIFIED", "claim_id": claim.claim_id, "execution_mode": "shadow", "production_side_effects": False}
|
||||
|
||||
def heartbeat(self, claim):
|
||||
return {"worker": self.worker_name, "claim_id": claim.claim_id, "status": "alive"}
|
||||
|
||||
def report_progress(self, claim, progress):
|
||||
return {"worker": self.worker_name, "claim_id": claim.claim_id, "progress": progress}
|
||||
|
||||
def probe(self, claim):
|
||||
return {"worker": self.worker_name, "claim_id": claim.claim_id, "status": claim.status}
|
||||
|
||||
def cancel(self, claim):
|
||||
return {"worker": self.worker_name, "claim_id": claim.claim_id, "status": "cancelled"}
|
||||
|
||||
def collect_result(self, claim):
|
||||
return {"worker": self.worker_name, "claim_id": claim.claim_id, "result": "not_collected"}
|
||||
@@ -0,0 +1,31 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from .base import WorkerAdapter
|
||||
|
||||
|
||||
class SystemdWorkerAdapter(WorkerAdapter):
|
||||
worker_name = "systemd_worker"
|
||||
|
||||
def capabilities(self) -> list[str]:
|
||||
return ["systemd.exec", "runtime.shell"]
|
||||
|
||||
def claim(self, task):
|
||||
return {"worker": self.worker_name, "task_id": task.task_id, "status": "claimed"}
|
||||
|
||||
def execute(self, task, claim):
|
||||
return {"worker": self.worker_name, "task_id": task.task_id, "status": "SHADOW_VERIFIED", "claim_id": claim.claim_id, "execution_mode": "shadow", "production_side_effects": False}
|
||||
|
||||
def heartbeat(self, claim):
|
||||
return {"worker": self.worker_name, "claim_id": claim.claim_id, "status": "alive"}
|
||||
|
||||
def report_progress(self, claim, progress):
|
||||
return {"worker": self.worker_name, "claim_id": claim.claim_id, "progress": progress}
|
||||
|
||||
def probe(self, claim):
|
||||
return {"worker": self.worker_name, "claim_id": claim.claim_id, "status": claim.status}
|
||||
|
||||
def cancel(self, claim):
|
||||
return {"worker": self.worker_name, "claim_id": claim.claim_id, "status": "cancelled"}
|
||||
|
||||
def collect_result(self, claim):
|
||||
return {"worker": self.worker_name, "claim_id": claim.claim_id, "result": "not_collected"}
|
||||
@@ -0,0 +1,27 @@
|
||||
"""Agent registry abstraction."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from .errors import NotFoundError
|
||||
|
||||
|
||||
class AgentRegistry:
|
||||
def __init__(self):
|
||||
self._agents: dict[str, dict] = {}
|
||||
|
||||
def register(self, agent_id: str, metadata: dict) -> None:
|
||||
self._agents[agent_id] = {"agent_id": agent_id, **metadata}
|
||||
|
||||
def unregister(self, agent_id: str) -> None:
|
||||
if agent_id not in self._agents:
|
||||
raise NotFoundError("agent")
|
||||
del self._agents[agent_id]
|
||||
|
||||
def list(self) -> list[dict]:
|
||||
return list(self._agents.values())
|
||||
|
||||
def get(self, agent_id: str) -> dict:
|
||||
try:
|
||||
return self._agents[agent_id]
|
||||
except KeyError:
|
||||
raise NotFoundError("agent")
|
||||
@@ -0,0 +1,557 @@
|
||||
"""Task Plane HTTP API implementation (stdlib-only)."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
import time
|
||||
import uuid
|
||||
import urllib.parse
|
||||
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
||||
from typing import Any, Callable
|
||||
|
||||
from .delegation import DelegationEngine
|
||||
from .errors import (
|
||||
ConflictError,
|
||||
ForbiddenError,
|
||||
NotFoundError,
|
||||
PayloadTooLargeError,
|
||||
TaskPlaneHTTPError,
|
||||
UnauthorizedError,
|
||||
ValidationError,
|
||||
)
|
||||
from .evidence import EvidenceService
|
||||
from .journal import TaskJournal
|
||||
from .lease import LeaseManager
|
||||
from .models import (
|
||||
EvidenceEnvelope,
|
||||
ResultEnvelope,
|
||||
TaskPlan,
|
||||
TaskRecord,
|
||||
iso8601_now,
|
||||
contains_secret_text,
|
||||
validate_identifier,
|
||||
)
|
||||
from .policy import PolicyEngine
|
||||
from .store import TaskPlaneStore
|
||||
from .worker_registry import WorkerRegistry
|
||||
|
||||
|
||||
def _json_payload(data: dict[str, Any], status: int = 200) -> tuple[int, bytes]:
|
||||
body = json.dumps(data, ensure_ascii=False).encode("utf-8")
|
||||
return status, body
|
||||
|
||||
|
||||
def _bounded_int(value: Any, name: str, minimum: int, maximum: int) -> int:
|
||||
try:
|
||||
number = int(value)
|
||||
except (TypeError, ValueError) as exc:
|
||||
raise ValidationError(f"invalid {name}") from exc
|
||||
if not minimum <= number <= maximum:
|
||||
raise ValidationError(f"invalid {name}")
|
||||
return number
|
||||
|
||||
|
||||
class TaskPlaneService:
|
||||
def __init__(self, store: TaskPlaneStore, policy: PolicyEngine | None = None):
|
||||
self.store = store
|
||||
self.policy = policy or PolicyEngine(
|
||||
execution_enabled=False,
|
||||
identity_capabilities={
|
||||
"anon": {"task.creator", "task.read", "task.audit"}
|
||||
},
|
||||
)
|
||||
self.journal = TaskJournal(store)
|
||||
self.lease = LeaseManager(store)
|
||||
self.workers = WorkerRegistry(store)
|
||||
self.evidence = EvidenceService(store)
|
||||
self.delegation = DelegationEngine(store)
|
||||
self.store.probe_attestation_verifier = self.policy.probe_attestation_verifier
|
||||
|
||||
def create_task(self, body: dict[str, Any], idempotency_key: str | None = None) -> dict[str, Any]:
|
||||
if body.get("parent_task_id"):
|
||||
raise ForbiddenError("child tasks must use the delegate endpoint")
|
||||
issuer = body.get("issuer", {})
|
||||
actor = issuer.get("id", "anon") if isinstance(issuer, dict) else "anon"
|
||||
self.policy.assert_allowed("create", actor)
|
||||
try:
|
||||
plan = TaskPlan.from_dict(body)
|
||||
except (TypeError, ValueError) as exc:
|
||||
raise ValidationError("invalid task plan") from exc
|
||||
task = self.store.create_task_with_event(plan, idempotency_key=idempotency_key)
|
||||
return task.to_dict()
|
||||
|
||||
def list_tasks(self, actor: str | None = None) -> list[dict[str, Any]]:
|
||||
if actor is not None:
|
||||
self.policy.assert_allowed("read", actor)
|
||||
return [task.to_dict(include_task=False) for task in self.store.list_tasks()]
|
||||
|
||||
def get_task(self, task_id: str, actor: str | None = None) -> dict[str, Any]:
|
||||
if actor is not None:
|
||||
self.policy.assert_allowed("read", actor, task=self.store.get_task(task_id))
|
||||
return self.store.get_task(task_id).to_dict()
|
||||
|
||||
def get_worker(self, worker_id: str, actor: str | None = None) -> dict[str, Any]:
|
||||
if actor is not None:
|
||||
self.policy.assert_allowed("read", actor)
|
||||
return self.workers.get(worker_id).to_dict()
|
||||
|
||||
def list_workers(self, actor: str | None = None) -> list[dict[str, Any]]:
|
||||
if actor is not None:
|
||||
self.policy.assert_allowed("read", actor)
|
||||
return [w.to_dict() for w in self.workers.list()]
|
||||
|
||||
def register_worker(self, body: dict[str, Any]) -> dict[str, Any]:
|
||||
actor = str(body.get("actor_identity") or body.get("identity") or "")
|
||||
is_admin = actor in self.policy.admin_identities
|
||||
try:
|
||||
existing = self.store.get_worker(str(body.get("worker_id") or ""))
|
||||
except NotFoundError:
|
||||
existing = None
|
||||
if existing is not None and str(body.get("identity") or "") != existing.identity:
|
||||
raise ConflictError("worker identity takeover rejected")
|
||||
self.policy.assert_readonly_worker_registration(actor, body)
|
||||
self.policy.assert_allowed(
|
||||
"worker.admin" if is_admin else "worker.register", actor
|
||||
)
|
||||
try:
|
||||
worker = self.workers.register(
|
||||
body,
|
||||
actor_identity=actor,
|
||||
is_admin=is_admin,
|
||||
expected_revision=body.get("expected_revision"),
|
||||
)
|
||||
except (TypeError, ValueError, KeyError) as exc:
|
||||
raise ValidationError("invalid worker registration") from exc
|
||||
return worker.to_dict()
|
||||
|
||||
def worker_heartbeat(self, worker_id: str, payload: dict[str, Any]) -> dict[str, Any]:
|
||||
interval = payload.get("heartbeat_interval_seconds")
|
||||
actor = str(payload.get("actor_identity") or "")
|
||||
self.policy.assert_readonly_worker_binding(actor, worker_id)
|
||||
self.policy.assert_allowed("worker.register", actor)
|
||||
return self.workers.heartbeat(
|
||||
worker_id,
|
||||
heartbeat_interval_seconds=interval,
|
||||
actor_identity=actor,
|
||||
expected_revision=payload.get("expected_revision"),
|
||||
).to_dict()
|
||||
|
||||
def disable_worker(self, worker_id: str, payload: dict[str, Any]) -> dict[str, Any]:
|
||||
actor = str(payload.get("actor_identity") or "")
|
||||
self.policy.assert_allowed("worker.admin", actor)
|
||||
return self.store.disable_worker(
|
||||
worker_id,
|
||||
actor_identity=actor,
|
||||
expected_revision=int(payload.get("expected_revision")),
|
||||
reason=str(payload.get("reason") or "disabled by administrator"),
|
||||
).to_dict()
|
||||
|
||||
def restore_worker(self, worker_id: str, payload: dict[str, Any]) -> dict[str, Any]:
|
||||
actor = str(payload.get("actor_identity") or "")
|
||||
self.policy.assert_allowed("worker.admin", actor)
|
||||
return self.store.restore_worker(
|
||||
worker_id,
|
||||
actor_identity=actor,
|
||||
expected_revision=int(payload.get("expected_revision")),
|
||||
).to_dict()
|
||||
|
||||
def claim(self, task_id: str, body: dict[str, Any]) -> dict[str, Any]:
|
||||
actor = body.get("actor_identity") or body.get("worker_identity") or "worker"
|
||||
self.policy.assert_allowed("claim", actor)
|
||||
worker_id = body.get("worker_id")
|
||||
if not worker_id:
|
||||
raise ValidationError("worker_id required")
|
||||
|
||||
task = self.store.get_task(task_id)
|
||||
self.policy.assert_readonly_worker_binding(actor, str(worker_id))
|
||||
self.policy.assert_allowed("claim", actor, task=task)
|
||||
lease_seconds = _bounded_int(
|
||||
body.get("lease_seconds", 30), "lease_seconds", 1, 3600
|
||||
)
|
||||
claim = self.lease.claim(
|
||||
task_id=task_id,
|
||||
worker_id=worker_id,
|
||||
worker_identity=actor,
|
||||
lease_seconds=lease_seconds,
|
||||
expected_revision=task.revision,
|
||||
operation_digest=task.task_plan.operation_digest(),
|
||||
)
|
||||
return {"claim_id": claim.claim_id, **claim.to_dict()}
|
||||
|
||||
def heartbeat(self, task_id: str, body: dict[str, Any]) -> dict[str, Any]:
|
||||
claim_id = body.get("claim_id")
|
||||
if not claim_id:
|
||||
raise ValidationError("claim_id required")
|
||||
actor = body.get("actor_identity") or body.get("worker_identity") or "worker"
|
||||
task = self.store.get_task(task_id)
|
||||
claim = self.store.get_claim(claim_id)
|
||||
self.policy.assert_readonly_worker_binding(actor, claim.worker_id)
|
||||
self.policy.assert_allowed("heartbeat", actor, task=task)
|
||||
if claim.worker_identity != actor:
|
||||
raise ForbiddenError("claim identity mismatch")
|
||||
lease_seconds = _bounded_int(
|
||||
body.get("lease_seconds", 30), "lease_seconds", 1, 3600
|
||||
)
|
||||
claim = self.lease.heartbeat(task_id, claim_id, lease_seconds)
|
||||
return {"claim_id": claim_id, "lease_until": claim.lease_until}
|
||||
|
||||
def progress(self, task_id: str, body: dict[str, Any]) -> dict[str, Any]:
|
||||
claim_id = body.get("claim_id")
|
||||
if not claim_id:
|
||||
raise ValidationError("claim_id required")
|
||||
actor = body.get("actor_identity") or body.get("worker_identity") or "worker"
|
||||
progress = body.get("progress")
|
||||
stage = body.get("stage", "RUNNING")
|
||||
message = body.get("message")
|
||||
if progress is None:
|
||||
raise ValidationError("progress required")
|
||||
task = self.store.get_task(task_id)
|
||||
claim = self.store.get_claim(claim_id)
|
||||
self.policy.assert_readonly_worker_binding(actor, claim.worker_id)
|
||||
self.policy.assert_allowed("progress", actor, task=task)
|
||||
if task.task_plan.data_classification == "credential-secret":
|
||||
if message not in {None, ""} or contains_secret_text(stage):
|
||||
raise ValidationError(
|
||||
"credential-secret progress must contain metadata only"
|
||||
)
|
||||
lease_seconds = _bounded_int(
|
||||
body.get("lease_seconds", 30), "lease_seconds", 1, 3600
|
||||
)
|
||||
claim = self.store.record_progress(
|
||||
task_id,
|
||||
claim_id,
|
||||
actor,
|
||||
progress=progress,
|
||||
stage=str(stage),
|
||||
message=None if message is None else str(message),
|
||||
lease_seconds=lease_seconds,
|
||||
)
|
||||
return {"claim_id": claim.claim_id, "status": claim.status, "progress": progress}
|
||||
|
||||
def complete(self, task_id: str, body: dict[str, Any]) -> dict[str, Any]:
|
||||
claim_id = body.get("claim_id")
|
||||
if not claim_id:
|
||||
raise ValidationError("claim_id required")
|
||||
actor = body.get("actor_identity") or body.get("worker_identity") or "worker"
|
||||
task = self.store.get_task(task_id)
|
||||
|
||||
status = body.get("status", "COMPLETED")
|
||||
outputs = body.get("outputs", [])
|
||||
evidences = body.get("evidence", [])
|
||||
if not isinstance(outputs, list):
|
||||
raise ValidationError("outputs must be list")
|
||||
if not isinstance(evidences, list):
|
||||
raise ValidationError("evidence must be list")
|
||||
if any(not isinstance(ev, dict) for ev in evidences):
|
||||
raise ValidationError("evidence items must be objects")
|
||||
claim = self.store.get_claim(claim_id)
|
||||
self.policy.assert_readonly_worker_binding(actor, claim.worker_id)
|
||||
self.policy.assert_allowed("result", actor, task=task)
|
||||
if claim.worker_identity != actor:
|
||||
raise ForbiddenError("claim identity mismatch")
|
||||
if task.task_plan.data_classification == "credential-secret":
|
||||
allowed_result_keys = {
|
||||
"kind",
|
||||
"hash",
|
||||
"sha256",
|
||||
"count",
|
||||
"status",
|
||||
"error_code",
|
||||
"signature",
|
||||
"attestation_ref",
|
||||
}
|
||||
for entry in [*outputs, *evidences]:
|
||||
if (
|
||||
not isinstance(entry, dict)
|
||||
or set(entry) - allowed_result_keys
|
||||
or contains_secret_text(entry)
|
||||
):
|
||||
raise ValidationError(
|
||||
"credential-secret result must contain hash/count/status only"
|
||||
)
|
||||
|
||||
result = ResultEnvelope(
|
||||
task_id=task_id,
|
||||
claim_id=claim_id,
|
||||
worker_identity=actor,
|
||||
status=status,
|
||||
outputs=outputs,
|
||||
evidence=evidences,
|
||||
started_at=str(body.get("started_at") or iso8601_now()),
|
||||
completed_at=str(body.get("completed_at") or iso8601_now()),
|
||||
result_digest=body.get("result_digest"),
|
||||
signature=body.get("signature"),
|
||||
attestation_ref=body.get("attestation_ref"),
|
||||
operation_digest=claim.operation_digest,
|
||||
)
|
||||
evidence_decision = self.policy.evidence_verifier.verify(task, evidences)
|
||||
if not evidence_decision.allowed:
|
||||
raise ForbiddenError(evidence_decision.reason)
|
||||
result_decision = self.policy.verify_result(result, task)
|
||||
if not result_decision.allowed:
|
||||
raise ForbiddenError(result_decision.reason)
|
||||
result.result_digest = result.computed_digest()
|
||||
previous = self.store._last_evidence_hash(task_id)
|
||||
evidence_entries = []
|
||||
for evidence in evidences:
|
||||
envelope = EvidenceEnvelope.create(
|
||||
task_id=task_id,
|
||||
claim_id=claim_id,
|
||||
kind=str(evidence.get("kind") or evidence.get("type") or "generic"),
|
||||
worker_identity=actor,
|
||||
payload=evidence,
|
||||
previous_evidence_hash=previous,
|
||||
signature=evidence.get("signature"),
|
||||
attestation_ref=evidence.get("attestation_ref"),
|
||||
)
|
||||
evidence_entries.append(envelope)
|
||||
previous = envelope.evidence_digest
|
||||
self.store.complete_claim_with_evidence_and_event(
|
||||
task_id,
|
||||
claim_id,
|
||||
status
|
||||
if status
|
||||
in {"COMPLETED", "FAILED_FINAL", "FAILED_RECOVERABLE", "CANCELLED"}
|
||||
else "COMPLETED",
|
||||
result.to_dict(),
|
||||
evidence_entries,
|
||||
)
|
||||
return result.to_dict()
|
||||
|
||||
def fail(self, task_id: str, body: dict[str, Any]) -> dict[str, Any]:
|
||||
claim_id = body.get("claim_id")
|
||||
if not claim_id:
|
||||
raise ValidationError("claim_id required")
|
||||
reason = body.get("reason", "failed")
|
||||
actor = body.get("actor_identity") or body.get("worker_identity") or "worker"
|
||||
self.policy.assert_allowed("fail", actor)
|
||||
task = self.store.get_task(task_id)
|
||||
self.policy.assert_allowed("fail", actor, task=task)
|
||||
claim = self.store.get_claim(claim_id)
|
||||
if claim.worker_identity != actor:
|
||||
raise ForbiddenError("claim identity mismatch")
|
||||
status = "FAILED_RECOVERABLE" if body.get("recoverable", False) else "FAILED_FINAL"
|
||||
self.store.fail_claim_with_event(task_id, claim_id, status, str(reason))
|
||||
return {"task_id": task_id, "status": status, "reason": reason}
|
||||
|
||||
def cancel(self, task_id: str, body: dict[str, Any]) -> dict[str, Any]:
|
||||
actor = body.get("actor_identity") or body.get("worker_identity") or "owner"
|
||||
self.policy.assert_allowed("cancel", actor)
|
||||
reason = body.get("reason", "cancelled")
|
||||
task = self.store.get_task(task_id)
|
||||
self.policy.assert_allowed("cancel", actor, task=task)
|
||||
updated = self.store.cancel_task_with_event(
|
||||
task_id, task.revision, reason=reason
|
||||
)
|
||||
return updated.to_dict()
|
||||
|
||||
def delegate(self, task_id: str, body: dict[str, Any]) -> dict[str, Any]:
|
||||
actor = body.get("actor_identity") or "owner"
|
||||
self.policy.assert_allowed("delegate", actor)
|
||||
parent = self.store.get_task(task_id)
|
||||
self.policy.assert_allowed("delegate", actor, task=parent)
|
||||
if "task" in body:
|
||||
try:
|
||||
child_plan = TaskPlan.from_dict(body["task"])
|
||||
except (TypeError, ValueError) as exc:
|
||||
raise ValidationError("invalid child task plan") from exc
|
||||
else:
|
||||
plan_body = dict(body)
|
||||
plan_body.pop("actor_identity", None)
|
||||
try:
|
||||
child_plan = TaskPlan.from_dict(plan_body)
|
||||
except (TypeError, ValueError) as exc:
|
||||
raise ValidationError("invalid child task plan") from exc
|
||||
child_plan.parent_task_id = task_id
|
||||
child = self.delegation.delegate(task_id, child_plan, caller=actor)
|
||||
return child.to_dict()
|
||||
|
||||
def get_events(self, task_id: str, actor: str | None = None) -> list[dict[str, Any]]:
|
||||
if actor is not None:
|
||||
self.policy.assert_allowed("audit", actor, task=self.store.get_task(task_id))
|
||||
self.store.get_task(task_id)
|
||||
return self.store.list_events(task_id)
|
||||
|
||||
def get_evidence(self, task_id: str, actor: str | None = None) -> list[dict[str, Any]]:
|
||||
if actor is not None:
|
||||
self.policy.assert_allowed("audit", actor, task=self.store.get_task(task_id))
|
||||
self.store.get_task(task_id)
|
||||
return self.evidence.list(task_id)
|
||||
|
||||
def get_children(self, task_id: str, actor: str | None = None) -> list[str]:
|
||||
if actor is not None:
|
||||
self.policy.assert_allowed("read", actor, task=self.store.get_task(task_id))
|
||||
self.store.get_task(task_id)
|
||||
return self.store.list_children(task_id)
|
||||
|
||||
|
||||
class TaskPlaneRequestHandler(BaseHTTPRequestHandler):
|
||||
service: TaskPlaneService | None = None
|
||||
authenticator: Callable[[str], str | None] | None = None
|
||||
max_body_bytes = 1_000_000
|
||||
|
||||
def log_message(self, format: str, *args: Any) -> None:
|
||||
# BaseHTTPRequestHandler logs the raw request target; suppress it so
|
||||
# attacker-controlled paths or query strings cannot leak credentials.
|
||||
return
|
||||
|
||||
def _json(self) -> tuple[str | None, dict[str, Any]]:
|
||||
try:
|
||||
length = int(self.headers.get("Content-Length", "0"))
|
||||
except ValueError as exc:
|
||||
raise ValidationError("invalid content length") from exc
|
||||
if length < 0:
|
||||
raise ValidationError("invalid content length")
|
||||
if length > self.max_body_bytes:
|
||||
raise PayloadTooLargeError("payload exceeds limit")
|
||||
raw = self.rfile.read(length) if length > 0 else b""
|
||||
if not raw:
|
||||
return None, {}
|
||||
try:
|
||||
decoded = raw.decode("utf-8")
|
||||
payload = json.loads(decoded)
|
||||
if not isinstance(payload, dict):
|
||||
raise ValidationError("JSON payload must be an object")
|
||||
return decoded, payload
|
||||
except (UnicodeDecodeError, json.JSONDecodeError) as exc:
|
||||
raise ValidationError("invalid JSON payload") from exc
|
||||
|
||||
def _response(self, status: int, payload: Any) -> None:
|
||||
encoded = json.dumps(payload, ensure_ascii=False).encode("utf-8")
|
||||
self.send_response(status)
|
||||
self.send_header("Content-Type", "application/json; charset=utf-8")
|
||||
self.send_header("Content-Length", str(len(encoded)))
|
||||
self.end_headers()
|
||||
self.wfile.write(encoded)
|
||||
|
||||
def _authenticated_identity(self) -> str:
|
||||
authenticator = type(self).authenticator
|
||||
if authenticator is None:
|
||||
raise UnauthorizedError("authentication unavailable")
|
||||
header = self.headers.get("Authorization", "")
|
||||
if not header.startswith("Bearer "):
|
||||
raise UnauthorizedError("bearer authentication required")
|
||||
token = header[7:]
|
||||
if not token or any(ord(ch) < 33 for ch in token):
|
||||
raise UnauthorizedError("invalid bearer token")
|
||||
identity = authenticator(token)
|
||||
if not identity:
|
||||
raise UnauthorizedError("invalid credentials")
|
||||
return str(identity)
|
||||
|
||||
def _handle(self, handler):
|
||||
try:
|
||||
if self.service is None:
|
||||
raise TaskPlaneHTTPError("service not configured")
|
||||
result = handler()
|
||||
if isinstance(result, tuple):
|
||||
status, payload = result
|
||||
else:
|
||||
status, payload = 200, result
|
||||
self._response(status, payload)
|
||||
except TaskPlaneHTTPError as exc:
|
||||
self._response(exc.status_code, exc.to_payload())
|
||||
except Exception as exc: # defensive
|
||||
if isinstance(exc, TaskPlaneHTTPError):
|
||||
raise
|
||||
self._response(
|
||||
500,
|
||||
{"error": "internal_error", "message": "internal server error"},
|
||||
)
|
||||
|
||||
def do_GET(self): # noqa: N802
|
||||
def route():
|
||||
path = urllib.parse.urlparse(self.path).path
|
||||
parts = [p for p in path.split("/") if p]
|
||||
for part in parts[1:]:
|
||||
if part not in {"events", "evidence", "children", "heartbeat"}:
|
||||
try:
|
||||
validate_identifier(part, "resource identifier")
|
||||
except ValueError as exc:
|
||||
raise ValidationError("invalid resource identifier") from exc
|
||||
identity = self._authenticated_identity()
|
||||
|
||||
if len(parts) == 1 and parts[0] == "tasks":
|
||||
return 200, self.service.list_tasks(identity)
|
||||
if len(parts) == 2 and parts[0] == "tasks":
|
||||
task_id = parts[1]
|
||||
return 200, self.service.get_task(task_id, identity)
|
||||
if len(parts) == 3 and parts[0] == "tasks" and parts[2] == "events":
|
||||
return 200, self.service.get_events(parts[1], identity)
|
||||
if len(parts) == 3 and parts[0] == "tasks" and parts[2] == "evidence":
|
||||
return 200, self.service.get_evidence(parts[1], identity)
|
||||
if len(parts) == 3 and parts[0] == "tasks" and parts[2] == "children":
|
||||
return 200, self.service.get_children(parts[1], identity)
|
||||
if len(parts) == 1 and parts[0] == "workers":
|
||||
return 200, self.service.list_workers(identity)
|
||||
if len(parts) == 2 and parts[0] == "workers":
|
||||
return 200, self.service.get_worker(parts[1], identity)
|
||||
raise NotFoundError("unknown endpoint")
|
||||
|
||||
self._handle(route)
|
||||
|
||||
def do_POST(self): # noqa: N802
|
||||
def route():
|
||||
body_text, body = self._json()
|
||||
identity = self._authenticated_identity()
|
||||
path = urllib.parse.urlparse(self.path).path
|
||||
parts = [p for p in path.split("/") if p]
|
||||
headers = {k.lower(): v for k, v in self.headers.items()}
|
||||
|
||||
if len(parts) == 1 and parts[0] == "tasks":
|
||||
issuer = body.get("issuer")
|
||||
if issuer and (
|
||||
not isinstance(issuer, dict) or issuer.get("id") != identity
|
||||
):
|
||||
raise ForbiddenError("issuer identity mismatch")
|
||||
body["issuer"] = {"type": "authenticated", "id": identity}
|
||||
return 201, self.service.create_task(body or {}, headers.get("idempotency-key"))
|
||||
if body.get("actor_identity") not in {None, identity}:
|
||||
raise ForbiddenError("actor identity mismatch")
|
||||
body["actor_identity"] = identity
|
||||
if len(parts) == 3 and parts[0] == "tasks" and parts[2] == "claim":
|
||||
return 200, self.service.claim(parts[1], body or {})
|
||||
if len(parts) == 3 and parts[0] == "tasks" and parts[2] == "heartbeat":
|
||||
return 200, self.service.heartbeat(parts[1], body or {})
|
||||
if len(parts) == 3 and parts[0] == "tasks" and parts[2] == "progress":
|
||||
return 200, self.service.progress(parts[1], body or {})
|
||||
if len(parts) == 3 and parts[0] == "tasks" and parts[2] == "result":
|
||||
return 200, self.service.complete(parts[1], body or {})
|
||||
if len(parts) == 3 and parts[0] == "tasks" and parts[2] == "fail":
|
||||
return 200, self.service.fail(parts[1], body or {})
|
||||
if len(parts) == 3 and parts[0] == "tasks" and parts[2] == "cancel":
|
||||
return 200, self.service.cancel(parts[1], body or {})
|
||||
if len(parts) == 3 and parts[0] == "tasks" and parts[2] == "delegate":
|
||||
return 200, self.service.delegate(parts[1], body or {})
|
||||
|
||||
if len(parts) == 2 and parts[0] == "workers" and parts[1] == "register":
|
||||
if (
|
||||
body.get("identity") != identity
|
||||
and identity not in self.service.policy.admin_identities
|
||||
):
|
||||
raise ForbiddenError("worker identity mismatch")
|
||||
return 201, self.service.register_worker(body or {})
|
||||
if len(parts) == 3 and parts[0] == "workers" and parts[2] == "heartbeat":
|
||||
return 200, self.service.worker_heartbeat(parts[1], body or {})
|
||||
if len(parts) == 3 and parts[0] == "workers" and parts[2] == "disable":
|
||||
return 200, self.service.disable_worker(parts[1], body or {})
|
||||
if len(parts) == 3 and parts[0] == "workers" and parts[2] == "restore":
|
||||
return 200, self.service.restore_worker(parts[1], body or {})
|
||||
raise NotFoundError("unknown endpoint")
|
||||
|
||||
self._handle(route)
|
||||
|
||||
|
||||
def run_server(
|
||||
host: str,
|
||||
port: int,
|
||||
service: TaskPlaneService,
|
||||
*,
|
||||
authenticator: Callable[[str], str | None] | None = None,
|
||||
):
|
||||
handler = TaskPlaneRequestHandler
|
||||
handler.service = service
|
||||
handler.authenticator = authenticator
|
||||
server = ThreadingHTTPServer((host, port), handler)
|
||||
server.serve_forever()
|
||||
@@ -0,0 +1,36 @@
|
||||
"""Capability matcher helpers."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from fnmatch import fnmatch
|
||||
|
||||
|
||||
def _normalize_capability(capability: str) -> str:
|
||||
return capability.strip().lower()
|
||||
|
||||
|
||||
def capability_matches(granted: str, required: str) -> bool:
|
||||
granted = _normalize_capability(granted)
|
||||
required = _normalize_capability(required)
|
||||
if granted == required:
|
||||
return True
|
||||
# A granted wildcard such as "test.*" should match required "test.unit"
|
||||
if "*" in granted:
|
||||
return fnmatch(required, granted)
|
||||
# A required wildcard is treated as a suffix/prefix requirement for callers.
|
||||
if "*" in required:
|
||||
return fnmatch(granted, required)
|
||||
return False
|
||||
|
||||
|
||||
def has_capabilities(granted_caps: list[str], required_caps: list[str]) -> bool:
|
||||
granted = [_normalize_capability(c) for c in granted_caps]
|
||||
for required in required_caps:
|
||||
required = _normalize_capability(required)
|
||||
if not any(capability_matches(g, required) for g in granted):
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def normalize_capabilities(caps: list[str]) -> list[str]:
|
||||
return sorted({_normalize_capability(cap) for cap in caps})
|
||||
@@ -0,0 +1,87 @@
|
||||
"""Delegation guardrails."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from .errors import ConflictError, ForbiddenError, NotFoundError
|
||||
from .models import Budget, TaskPlan, TaskRecord
|
||||
from .store import TaskPlaneStore
|
||||
|
||||
|
||||
class DelegationEngine:
|
||||
def __init__(self, store: TaskPlaneStore):
|
||||
self.store = store
|
||||
|
||||
def can_delegate(self, parent_id: str, child_plan: TaskPlan) -> bool:
|
||||
parent = self.store.get_task(parent_id)
|
||||
if parent.status == "COMPLETED":
|
||||
raise ConflictError("cannot delegate completed task")
|
||||
if parent.task_plan.max_delegation_depth <= 0:
|
||||
raise ConflictError("max delegation depth not configured")
|
||||
|
||||
depth = self._depth(parent_id)
|
||||
if depth >= parent.task_plan.max_delegation_depth:
|
||||
raise ConflictError("max delegation depth reached")
|
||||
|
||||
if child_plan.parent_task_id not in {None, parent_id}:
|
||||
raise ConflictError("delegation parent mismatch")
|
||||
current = parent_id
|
||||
while current:
|
||||
if child_plan.task_id and current == child_plan.task_id:
|
||||
raise ConflictError("delegation cycle")
|
||||
current = self.store.parent_of(current)
|
||||
return True
|
||||
|
||||
def delegate(self, parent_id: str, child_plan: TaskPlan, *, caller: str) -> TaskRecord:
|
||||
self.can_delegate(parent_id, child_plan)
|
||||
parent = self.store.get_task(parent_id)
|
||||
if child_plan.required_capabilities and not set(child_plan.required_capabilities).issubset(
|
||||
set(parent.task_plan.required_capabilities or [])
|
||||
):
|
||||
raise ForbiddenError("child capabilities exceed parent constraints")
|
||||
risk_rank = {"low": 1, "medium": 2, "high": 3}
|
||||
if risk_rank.get(child_plan.risk, 99) > risk_rank.get(parent.task_plan.risk, -1):
|
||||
raise ForbiddenError("child risk exceeds parent constraints")
|
||||
child_plan.parent_task_id = parent_id
|
||||
child_plan.correlation_id = parent.task_plan.correlation_id or parent.task_id
|
||||
child_plan.preferred_workers = parent.task_plan.preferred_workers
|
||||
child_plan.max_delegation_depth = min(
|
||||
child_plan.max_delegation_depth,
|
||||
parent.task_plan.max_delegation_depth,
|
||||
)
|
||||
child_plan.approval_policy = parent.task_plan.approval_policy
|
||||
|
||||
parent_budget = self.store.get_task_budget(parent_id)
|
||||
required = Budget()
|
||||
for name in ("time_seconds", "attempts", "tokens", "cost"):
|
||||
available = getattr(parent_budget, name)
|
||||
requested = getattr(child_plan.budgets, name)
|
||||
if name == "attempts" and requested is None:
|
||||
requested = 1
|
||||
if available is not None:
|
||||
requested = 0 if requested is None else requested
|
||||
if requested > available:
|
||||
raise ConflictError(f"budget exceeded for {name}")
|
||||
setattr(child_plan.budgets, name, requested)
|
||||
setattr(required, name, requested)
|
||||
|
||||
# A retried identical delegation is a read, not a second budget charge.
|
||||
if child_plan.task_id:
|
||||
try:
|
||||
existing = self.store.get_task(child_plan.task_id)
|
||||
except NotFoundError:
|
||||
existing = None
|
||||
if existing is not None:
|
||||
if (
|
||||
existing.task_plan.parent_task_id == parent_id
|
||||
and existing.task_plan.operation_digest()
|
||||
== child_plan.operation_digest()
|
||||
):
|
||||
return existing
|
||||
raise ConflictError("delegation task_id conflict")
|
||||
|
||||
return self.store.delegate_task_with_budget_and_events(
|
||||
parent_id, child_plan, caller=caller
|
||||
)
|
||||
|
||||
def _depth(self, task_id: str) -> int:
|
||||
return self.store._compute_depth(task_id)
|
||||
@@ -0,0 +1,71 @@
|
||||
"""Error types for task plane operations."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
|
||||
class TaskPlaneError(Exception):
|
||||
"""Base exception for API-compatible failures."""
|
||||
|
||||
status_code = 500
|
||||
error_code = "task_plane_error"
|
||||
|
||||
def __init__(self, message: str, *, code: str | None = None, details: dict | None = None):
|
||||
super().__init__(message)
|
||||
self.message = message
|
||||
self.details = details or {}
|
||||
if code:
|
||||
self.error_code = code
|
||||
|
||||
def to_payload(self) -> dict:
|
||||
return {"error": self.error_code, "message": self.message, "details": self.details}
|
||||
|
||||
|
||||
class TaskPlaneHTTPError(TaskPlaneError):
|
||||
"""Base class with explicit HTTP status."""
|
||||
|
||||
status_code = 500
|
||||
|
||||
|
||||
class ValidationError(TaskPlaneHTTPError):
|
||||
status_code = 400
|
||||
error_code = "validation_error"
|
||||
|
||||
|
||||
class UnauthorizedError(TaskPlaneHTTPError):
|
||||
status_code = 401
|
||||
error_code = "unauthorized"
|
||||
|
||||
|
||||
class ForbiddenError(TaskPlaneHTTPError):
|
||||
status_code = 403
|
||||
error_code = "forbidden"
|
||||
|
||||
|
||||
class NotFoundError(TaskPlaneHTTPError):
|
||||
status_code = 404
|
||||
error_code = "not_found"
|
||||
|
||||
|
||||
class ConflictError(TaskPlaneHTTPError):
|
||||
status_code = 409
|
||||
error_code = "conflict"
|
||||
|
||||
|
||||
class PayloadTooLargeError(TaskPlaneHTTPError):
|
||||
status_code = 413
|
||||
error_code = "payload_too_large"
|
||||
|
||||
|
||||
class FailedDependencyError(TaskPlaneHTTPError):
|
||||
status_code = 424
|
||||
error_code = "failed_dependency"
|
||||
|
||||
|
||||
class LockedError(TaskPlaneHTTPError):
|
||||
status_code = 423
|
||||
error_code = "locked"
|
||||
|
||||
|
||||
class ServiceUnavailableError(TaskPlaneHTTPError):
|
||||
status_code = 503
|
||||
error_code = "service_unavailable"
|
||||
@@ -0,0 +1,108 @@
|
||||
"""CloudEvents-compatible event envelope and durable outbox helpers."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime, timezone
|
||||
from typing import Callable, Protocol
|
||||
|
||||
from .store import TaskPlaneStore
|
||||
|
||||
|
||||
@dataclass
|
||||
class CloudEvent:
|
||||
specversion: str = "1.0"
|
||||
id: str = ""
|
||||
type: str = ""
|
||||
source: str = "spiffe://aios/task-plane"
|
||||
subject: str = ""
|
||||
datacontenttype: str = "application/json"
|
||||
data: dict | None = None
|
||||
|
||||
@property
|
||||
def time(self) -> str:
|
||||
return datetime.now(timezone.utc).replace(microsecond=0).isoformat().replace("+00:00", "Z")
|
||||
|
||||
def asdict(self) -> dict:
|
||||
return {
|
||||
"specversion": self.specversion,
|
||||
"id": self.id,
|
||||
"type": self.type,
|
||||
"source": self.source,
|
||||
"subject": self.subject,
|
||||
"time": self.time,
|
||||
"datacontenttype": self.datacontenttype,
|
||||
"data": self.data or {},
|
||||
}
|
||||
|
||||
|
||||
NOT_DELIVERED = "NOT_DELIVERED"
|
||||
DELIVERED = "DELIVERED"
|
||||
FAILED_DELIVERY = "FAILED"
|
||||
|
||||
|
||||
class NotificationAdapter(Protocol):
|
||||
name: str
|
||||
|
||||
def deliver(self, envelope: dict) -> str | None:
|
||||
"""Return a provider receipt, or None when delivery is unconfirmed."""
|
||||
|
||||
|
||||
class CallbackNotificationAdapter:
|
||||
"""Adapter seam for webhook, NATS, GPT, dashboard, and WeChat transports."""
|
||||
|
||||
def __init__(self, name: str, callback: Callable[[dict], str | None]):
|
||||
self.name = name
|
||||
self._callback = callback
|
||||
|
||||
def deliver(self, envelope: dict) -> str | None:
|
||||
return self._callback(envelope)
|
||||
|
||||
|
||||
class WebhookAdapter(CallbackNotificationAdapter):
|
||||
def __init__(self, callback: Callable[[dict], str | None]):
|
||||
super().__init__("webhook", callback)
|
||||
|
||||
|
||||
class NatsJetStreamAdapter(CallbackNotificationAdapter):
|
||||
def __init__(self, callback: Callable[[dict], str | None]):
|
||||
super().__init__("nats-jetstream", callback)
|
||||
|
||||
|
||||
class GPTResultCallbackAdapter(CallbackNotificationAdapter):
|
||||
def __init__(self, callback: Callable[[dict], str | None]):
|
||||
super().__init__("gpt-result-callback", callback)
|
||||
|
||||
|
||||
class DashboardAdapter(CallbackNotificationAdapter):
|
||||
def __init__(self, callback: Callable[[dict], str | None]):
|
||||
super().__init__("dashboard", callback)
|
||||
|
||||
|
||||
class WeChatGatewayAdapter(CallbackNotificationAdapter):
|
||||
def __init__(self, callback: Callable[[dict], str | None]):
|
||||
super().__init__("wechat-gateway", callback)
|
||||
|
||||
|
||||
class DurableOutboxDispatcher:
|
||||
def __init__(self, store: TaskPlaneStore):
|
||||
self.store = store
|
||||
|
||||
def dispatch(self, message_id: str, adapter: NotificationAdapter) -> dict:
|
||||
row = self.store.get_outbox_message(message_id)
|
||||
import json
|
||||
|
||||
envelope = json.loads(row["payload"])
|
||||
try:
|
||||
receipt = adapter.deliver(envelope)
|
||||
except Exception:
|
||||
return self.store.record_outbox_delivery(
|
||||
message_id,
|
||||
receipt=None,
|
||||
error=f"{adapter.name} delivery failed",
|
||||
)
|
||||
return self.store.record_outbox_delivery(
|
||||
message_id,
|
||||
receipt=str(receipt) if receipt else None,
|
||||
error=None if receipt else f"{adapter.name} receipt unavailable",
|
||||
)
|
||||
@@ -0,0 +1,31 @@
|
||||
"""Evidence helpers."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import json
|
||||
|
||||
from .store import TaskPlaneStore
|
||||
|
||||
|
||||
def evidence_digest(payload: dict) -> str:
|
||||
body = json.dumps(payload, sort_keys=True, separators=(",", ":"))
|
||||
return hashlib.sha256(body.encode("utf-8")).hexdigest()
|
||||
|
||||
|
||||
class EvidenceService:
|
||||
def __init__(self, store: TaskPlaneStore):
|
||||
self.store = store
|
||||
|
||||
def add(self, task_id: str, kind: str, payload: dict, worker_identity: str) -> str:
|
||||
return self.store.add_evidence(task_id, kind, payload, worker_identity)
|
||||
|
||||
def list(self, task_id: str) -> list[dict]:
|
||||
return self.store.list_evidence(task_id)
|
||||
|
||||
def verify(self, task_id: str, required_kinds: list[str]) -> bool:
|
||||
evidences = self.list(task_id)
|
||||
observed = {e["kind"] for e in evidences}
|
||||
return set(required_kinds).issubset(
|
||||
observed
|
||||
) and self.store.verify_evidence_integrity(task_id)
|
||||
@@ -0,0 +1,22 @@
|
||||
"""Event journaling helpers."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from .errors import ValidationError
|
||||
from .store import TaskPlaneStore
|
||||
|
||||
|
||||
class TaskJournal:
|
||||
def __init__(self, store: TaskPlaneStore):
|
||||
self.store = store
|
||||
|
||||
def append(self, task_id: str, event_type: str, data: dict, *, subject: str | None = None) -> None:
|
||||
if not event_type:
|
||||
raise ValidationError("event_type required")
|
||||
self.store.append_event(task_id, event_type, data, subject=subject)
|
||||
|
||||
def list(self, task_id: str) -> list[dict]:
|
||||
return self.store.list_events(task_id)
|
||||
|
||||
def check_chain(self, task_id: str) -> bool:
|
||||
return self.store.verify_event_chain(task_id)
|
||||
@@ -0,0 +1,32 @@
|
||||
"""Claim and lease management helpers."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from .errors import LockedError
|
||||
from .models import ProbeEvidence
|
||||
from .store import TaskPlaneStore
|
||||
|
||||
|
||||
class LeaseManager:
|
||||
def __init__(self, store: TaskPlaneStore):
|
||||
self.store = store
|
||||
|
||||
def claim(self, task_id: str, worker_id: str, worker_identity: str, lease_seconds: int, expected_revision: int, operation_digest: str):
|
||||
return self.store.claim_task(task_id, worker_id, worker_identity, lease_seconds, expected_revision, operation_digest)
|
||||
|
||||
def heartbeat(self, task_id: str, claim_id: str, lease_seconds: int):
|
||||
claim = self.store.heartbeat_claim(task_id, claim_id, lease_seconds)
|
||||
return claim
|
||||
|
||||
def probe(self, claim_id: str):
|
||||
claim = self.store.get_claim(claim_id)
|
||||
if claim.status != "ACTIVE":
|
||||
raise LockedError("claim not active")
|
||||
return claim
|
||||
|
||||
def recover(self, task_id: str, claim_id: str, probe: ProbeEvidence):
|
||||
return self.store.recover_expired_claim(
|
||||
task_id,
|
||||
claim_id,
|
||||
probe,
|
||||
)
|
||||
@@ -0,0 +1,829 @@
|
||||
"""Core data models for the task plane."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import json
|
||||
import re
|
||||
import time
|
||||
import uuid
|
||||
import urllib.parse
|
||||
from enum import StrEnum
|
||||
from dataclasses import dataclass, field
|
||||
from datetime import datetime, timezone
|
||||
from typing import Any
|
||||
|
||||
|
||||
SENSITIVE_KEYS = {
|
||||
"secret",
|
||||
"password",
|
||||
"token",
|
||||
"api_key",
|
||||
"access_token",
|
||||
"refresh_token",
|
||||
"authorization",
|
||||
"private_key",
|
||||
"client_secret",
|
||||
"credential",
|
||||
}
|
||||
IDENTIFIER_RE = re.compile(r"[A-Za-z0-9][A-Za-z0-9._:-]{0,127}")
|
||||
TYPE_RE = re.compile(r"[A-Za-z0-9][A-Za-z0-9._-]{0,127}")
|
||||
RISK_LEVELS = {"low", "medium", "high"}
|
||||
DATA_CLASSIFICATIONS = {
|
||||
"public",
|
||||
"internal",
|
||||
"confidential",
|
||||
"credential-secret",
|
||||
}
|
||||
SECRET_TEXT_PATTERNS = (
|
||||
re.compile(r"(?i)\bAuthorization\s*:\s*Bearer\s+\S+"),
|
||||
re.compile(r"(?i)\b(?:password|passwd|api[_-]?key|access[_-]?token|secret)\s*[:=]\s*\S+"),
|
||||
re.compile(r"\bsk-[A-Za-z0-9_-]{12,}\b"),
|
||||
re.compile(r"\bAKIA[0-9A-Z]{16}\b"),
|
||||
re.compile(r"-----BEGIN (?:RSA |EC |OPENSSH )?PRIVATE KEY-----"),
|
||||
)
|
||||
TASK_PLAN_KEYS = {
|
||||
"schema_version",
|
||||
"task_id",
|
||||
"task_type",
|
||||
"issuer",
|
||||
"intent",
|
||||
"required_capabilities",
|
||||
"preferred_workers",
|
||||
"risk",
|
||||
"approval_policy",
|
||||
"dependencies",
|
||||
"max_delegation_depth",
|
||||
"budgets",
|
||||
"evidence_requirements",
|
||||
"parent_task_id",
|
||||
"correlation_id",
|
||||
"metadata",
|
||||
"data_classification",
|
||||
"execution_zone",
|
||||
"network_policy",
|
||||
"logging_policy",
|
||||
"allowed_input_paths",
|
||||
"allowed_output_paths",
|
||||
"output_policy",
|
||||
}
|
||||
|
||||
|
||||
def _now_ts() -> float:
|
||||
return time.time()
|
||||
|
||||
|
||||
def canonical_json(value: Any) -> str:
|
||||
return json.dumps(value, ensure_ascii=False, sort_keys=True, separators=(",", ":"))
|
||||
|
||||
|
||||
def compute_sha256_hex(value: Any) -> str:
|
||||
return hashlib.sha256(canonical_json(value).encode("utf-8")).hexdigest()
|
||||
|
||||
|
||||
def contains_secret_text(value: Any) -> bool:
|
||||
if isinstance(value, str):
|
||||
return any(pattern.search(value) for pattern in SECRET_TEXT_PATTERNS)
|
||||
if isinstance(value, dict):
|
||||
return any(contains_secret_text(item) for item in value.values())
|
||||
if isinstance(value, (list, tuple)):
|
||||
return any(contains_secret_text(item) for item in value)
|
||||
return False
|
||||
|
||||
|
||||
def validate_relative_paths(values: Any, name: str) -> list[str]:
|
||||
paths = _normalize_list(values)
|
||||
for value in paths:
|
||||
decoded = urllib.parse.unquote(value)
|
||||
parts = value.replace("\\", "/").split("/")
|
||||
if (
|
||||
decoded != value
|
||||
or "\\" in value
|
||||
or value.startswith("/")
|
||||
or re.match(r"^[A-Za-z]:", value)
|
||||
or any(part in {"", ".", ".."} for part in parts)
|
||||
):
|
||||
raise ValueError(f"invalid {name}")
|
||||
return paths
|
||||
|
||||
|
||||
def validate_identifier(value: Any, name: str) -> str:
|
||||
text = str(value or "")
|
||||
if (
|
||||
not IDENTIFIER_RE.fullmatch(text)
|
||||
or urllib.parse.unquote(text) != text
|
||||
or any(ord(ch) < 32 or ord(ch) == 127 for ch in text)
|
||||
):
|
||||
raise ValueError(f"invalid {name}")
|
||||
return text
|
||||
|
||||
|
||||
def validate_type(value: Any, name: str) -> str:
|
||||
text = str(value or "")
|
||||
if not TYPE_RE.fullmatch(text):
|
||||
raise ValueError(f"invalid {name}")
|
||||
return text
|
||||
|
||||
|
||||
def redact_value(value: Any) -> Any:
|
||||
if isinstance(value, dict):
|
||||
redacted = {}
|
||||
for key, inner in value.items():
|
||||
lowered = str(key).lower()
|
||||
if lowered in SENSITIVE_KEYS or lowered.endswith(
|
||||
("_password", "_secret", "_access_token", "_refresh_token", "_private_key")
|
||||
):
|
||||
redacted[key] = "[REDACTED]"
|
||||
else:
|
||||
redacted[key] = redact_value(inner)
|
||||
return redacted
|
||||
if isinstance(value, list):
|
||||
return [redact_value(v) for v in value]
|
||||
if isinstance(value, tuple):
|
||||
return tuple(redact_value(v) for v in value)
|
||||
return value
|
||||
|
||||
|
||||
class TaskStateTransitionError(ValueError):
|
||||
pass
|
||||
|
||||
|
||||
class TaskStatus(StrEnum):
|
||||
CREATED = "CREATED"
|
||||
QUEUED = "QUEUED"
|
||||
CLAIMED = "CLAIMED"
|
||||
RUNNING = "RUNNING"
|
||||
WAITING_CHILD = "WAITING_CHILD"
|
||||
VERIFYING = "VERIFYING"
|
||||
COMPLETED = "COMPLETED"
|
||||
FAILED_RECOVERABLE = "FAILED_RECOVERABLE"
|
||||
FAILED_FINAL = "FAILED_FINAL"
|
||||
CANCELLED = "CANCELLED"
|
||||
MANUAL_REVIEW_REQUIRED = "MANUAL_REVIEW_REQUIRED"
|
||||
|
||||
|
||||
TASK_STATES = {
|
||||
status.value for status in TaskStatus
|
||||
}
|
||||
|
||||
|
||||
TASK_NEXT_STATES = {
|
||||
"CREATED": {"QUEUED", "CLAIMED", "CANCELLED", "MANUAL_REVIEW_REQUIRED"},
|
||||
"QUEUED": {"CLAIMED", "CANCELLED", "FAILED_RECOVERABLE", "FAILED_FINAL"},
|
||||
"CLAIMED": {"RUNNING", "FAILED_RECOVERABLE", "FAILED_FINAL", "CANCELLED"},
|
||||
"RUNNING": {"VERIFYING", "WAITING_CHILD", "COMPLETED", "FAILED_RECOVERABLE", "FAILED_FINAL", "CANCELLED"},
|
||||
"WAITING_CHILD": {"RUNNING", "VERIFYING", "FAILED_RECOVERABLE", "FAILED_FINAL", "CANCELLED"},
|
||||
"VERIFYING": {"COMPLETED", "FAILED_RECOVERABLE", "FAILED_FINAL", "CANCELLED"},
|
||||
"COMPLETED": set(),
|
||||
"FAILED_RECOVERABLE": {"CLAIMED", "RUNNING", "CANCELLED"},
|
||||
"FAILED_FINAL": set(),
|
||||
"CANCELLED": set(),
|
||||
"MANUAL_REVIEW_REQUIRED": {"RUNNING", "CANCELLED"},
|
||||
}
|
||||
|
||||
|
||||
@dataclass
|
||||
class Budget:
|
||||
time_seconds: int | None = None
|
||||
attempts: int | None = None
|
||||
tokens: int | None = None
|
||||
cost: float | None = None
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, data: dict[str, Any] | None) -> "Budget":
|
||||
if not data:
|
||||
return cls()
|
||||
if not isinstance(data, dict) or set(data) - {
|
||||
"time_seconds",
|
||||
"attempts",
|
||||
"tokens",
|
||||
"cost",
|
||||
}:
|
||||
raise ValueError("invalid budget")
|
||||
for name in ("time_seconds", "attempts", "tokens", "cost"):
|
||||
value = data.get(name)
|
||||
if value is not None:
|
||||
try:
|
||||
if float(value) < 0:
|
||||
raise ValueError("budget values must be non-negative")
|
||||
except (TypeError, ValueError) as exc:
|
||||
raise ValueError("invalid budget value") from exc
|
||||
return cls(
|
||||
time_seconds=_to_positive_int(data.get("time_seconds")),
|
||||
attempts=_to_positive_int(data.get("attempts")),
|
||||
tokens=_to_positive_int(data.get("tokens")),
|
||||
cost=_to_float(data.get("cost")),
|
||||
)
|
||||
|
||||
def to_dict(self) -> dict[str, Any]:
|
||||
return {
|
||||
"time_seconds": self.time_seconds,
|
||||
"attempts": self.attempts,
|
||||
"tokens": self.tokens,
|
||||
"cost": self.cost,
|
||||
}
|
||||
|
||||
def merge(self, other: "Budget") -> "Budget":
|
||||
return Budget(
|
||||
time_seconds=_first_non_none(self.time_seconds, other.time_seconds),
|
||||
attempts=_first_non_none(self.attempts, other.attempts),
|
||||
tokens=_first_non_none(self.tokens, other.tokens),
|
||||
cost=_first_non_none(self.cost, other.cost),
|
||||
)
|
||||
|
||||
|
||||
@dataclass
|
||||
class TaskPlan:
|
||||
schema_version: int = 1
|
||||
task_id: str | None = None
|
||||
task_type: str = "code.change"
|
||||
issuer: dict[str, Any] = field(default_factory=dict)
|
||||
intent: str | None = None
|
||||
required_capabilities: list[str] = field(default_factory=list)
|
||||
preferred_workers: list[str] = field(default_factory=list)
|
||||
risk: str = "medium"
|
||||
approval_policy: str = "patch-only"
|
||||
dependencies: list[str] = field(default_factory=list)
|
||||
max_delegation_depth: int = 5
|
||||
budgets: Budget = field(default_factory=Budget)
|
||||
evidence_requirements: list[str] = field(default_factory=list)
|
||||
parent_task_id: str | None = None
|
||||
correlation_id: str | None = None
|
||||
metadata: dict[str, Any] = field(default_factory=dict)
|
||||
data_classification: str = "internal"
|
||||
execution_zone: str = "isolated-development"
|
||||
network_policy: str = "controlled"
|
||||
logging_policy: str = "redacted"
|
||||
allowed_input_paths: list[str] = field(default_factory=list)
|
||||
allowed_output_paths: list[str] = field(default_factory=list)
|
||||
output_policy: str = "standard"
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, data: dict[str, Any] | None) -> "TaskPlan":
|
||||
if not isinstance(data, dict):
|
||||
raise ValueError("task plan payload must be an object")
|
||||
unknown = set(data) - TASK_PLAN_KEYS
|
||||
if unknown:
|
||||
raise ValueError("unknown task plan fields")
|
||||
if "task_id" not in data or data["task_id"] is None:
|
||||
data = dict(data)
|
||||
data["task_id"] = f"task-{uuid.uuid4()}"
|
||||
risk = str(data.get("risk", "medium"))
|
||||
if risk not in RISK_LEVELS:
|
||||
raise ValueError("invalid risk")
|
||||
issuer = data.get("issuer", {})
|
||||
if not isinstance(issuer, dict):
|
||||
raise ValueError("issuer must be an object")
|
||||
schema_version = int(data.get("schema_version", 1))
|
||||
if schema_version != 1:
|
||||
raise ValueError("unsupported schema_version")
|
||||
max_depth = _to_positive_int(data.get("max_delegation_depth"), default=5)
|
||||
if max_depth is None or max_depth > 32:
|
||||
raise ValueError("invalid max_delegation_depth")
|
||||
classification = str(data.get("data_classification", "internal"))
|
||||
if classification not in DATA_CLASSIFICATIONS:
|
||||
raise ValueError("invalid data_classification")
|
||||
execution_zone = validate_type(
|
||||
data.get("execution_zone", "isolated-development"), "execution_zone"
|
||||
)
|
||||
network_policy = validate_type(
|
||||
data.get("network_policy", "controlled"), "network_policy"
|
||||
)
|
||||
logging_policy = validate_type(
|
||||
data.get("logging_policy", "redacted"), "logging_policy"
|
||||
)
|
||||
output_policy = validate_type(
|
||||
data.get("output_policy", "standard"), "output_policy"
|
||||
)
|
||||
if contains_secret_text(
|
||||
{
|
||||
"intent": data.get("intent"),
|
||||
"metadata": data.get("metadata", {}),
|
||||
}
|
||||
):
|
||||
raise ValueError("task content contains secret material")
|
||||
if classification == "credential-secret":
|
||||
if execution_zone != "secure-file-transformer":
|
||||
raise ValueError("credential-secret requires secure-file-transformer")
|
||||
if network_policy != "denied":
|
||||
raise ValueError("credential-secret requires network denied")
|
||||
if logging_policy != "metadata-only":
|
||||
raise ValueError("credential-secret requires metadata-only logging")
|
||||
if output_policy != "hash-count-status-only":
|
||||
raise ValueError("credential-secret requires restricted output")
|
||||
return cls(
|
||||
schema_version=schema_version,
|
||||
task_id=validate_identifier(data["task_id"], "task_id"),
|
||||
task_type=validate_type(data.get("task_type", "code.change"), "task_type"),
|
||||
issuer=issuer,
|
||||
intent=data.get("intent"),
|
||||
required_capabilities=_normalize_list(data.get("required_capabilities", [])),
|
||||
preferred_workers=_normalize_list(data.get("preferred_workers", [])),
|
||||
risk=risk,
|
||||
approval_policy=validate_type(
|
||||
data.get("approval_policy", "patch-only"), "approval_policy"
|
||||
),
|
||||
dependencies=_normalize_list(data.get("dependencies", [])),
|
||||
max_delegation_depth=max_depth,
|
||||
budgets=Budget.from_dict(data.get("budgets")),
|
||||
evidence_requirements=_normalize_list(data.get("evidence_requirements", [])),
|
||||
parent_task_id=data.get("parent_task_id"),
|
||||
correlation_id=data.get("correlation_id"),
|
||||
metadata=data.get("metadata", {}) if isinstance(data.get("metadata", {}), dict) else {},
|
||||
data_classification=classification,
|
||||
execution_zone=execution_zone,
|
||||
network_policy=network_policy,
|
||||
logging_policy=logging_policy,
|
||||
allowed_input_paths=validate_relative_paths(
|
||||
data.get("allowed_input_paths", []), "allowed_input_paths"
|
||||
),
|
||||
allowed_output_paths=validate_relative_paths(
|
||||
data.get("allowed_output_paths", []), "allowed_output_paths"
|
||||
),
|
||||
output_policy=output_policy,
|
||||
)
|
||||
|
||||
def to_dict(self, *, redact: bool = False) -> dict[str, Any]:
|
||||
payload = {
|
||||
"schema_version": self.schema_version,
|
||||
"task_id": self.task_id,
|
||||
"task_type": self.task_type,
|
||||
"issuer": self.issuer,
|
||||
"intent": self.intent,
|
||||
"required_capabilities": list(self.required_capabilities),
|
||||
"preferred_workers": list(self.preferred_workers),
|
||||
"risk": self.risk,
|
||||
"approval_policy": self.approval_policy,
|
||||
"dependencies": list(self.dependencies),
|
||||
"max_delegation_depth": self.max_delegation_depth,
|
||||
"budgets": self.budgets.to_dict(),
|
||||
"evidence_requirements": list(self.evidence_requirements),
|
||||
"parent_task_id": self.parent_task_id,
|
||||
"correlation_id": self.correlation_id,
|
||||
"metadata": dict(self.metadata),
|
||||
"data_classification": self.data_classification,
|
||||
"execution_zone": self.execution_zone,
|
||||
"network_policy": self.network_policy,
|
||||
"logging_policy": self.logging_policy,
|
||||
"allowed_input_paths": list(self.allowed_input_paths),
|
||||
"allowed_output_paths": list(self.allowed_output_paths),
|
||||
"output_policy": self.output_policy,
|
||||
}
|
||||
if redact:
|
||||
payload = redact_value(payload)
|
||||
return payload
|
||||
|
||||
def operation_digest(self) -> str:
|
||||
return compute_sha256_hex(self.to_dict())
|
||||
|
||||
|
||||
@dataclass
|
||||
class TaskRecord:
|
||||
task_id: str
|
||||
status: str
|
||||
revision: int
|
||||
task_plan: TaskPlan
|
||||
created_at: float
|
||||
updated_at: float
|
||||
root_task_id: str
|
||||
idempotency_key: str | None = None
|
||||
failure_reason: str | None = None
|
||||
|
||||
def with_status(self, status: str) -> "TaskRecord":
|
||||
allowed = TASK_NEXT_STATES.get(self.status, set())
|
||||
if status != self.status and (not allowed and self.status not in {"COMPLETED", "FAILED_FINAL", "CANCELLED"}):
|
||||
raise TaskStateTransitionError(f"Cannot transition {self.status} -> {status}")
|
||||
if status not in TASK_STATES:
|
||||
raise TaskStateTransitionError(f"Invalid status {status}")
|
||||
if status != self.status and self.status in TASK_NEXT_STATES and status not in TASK_NEXT_STATES[self.status]:
|
||||
raise TaskStateTransitionError(f"Cannot transition {self.status} -> {status}")
|
||||
return TaskRecord(
|
||||
task_id=self.task_id,
|
||||
status=status,
|
||||
revision=self.revision + 1,
|
||||
task_plan=self.task_plan,
|
||||
created_at=self.created_at,
|
||||
updated_at=_now_ts(),
|
||||
root_task_id=self.root_task_id,
|
||||
idempotency_key=self.idempotency_key,
|
||||
failure_reason=self.failure_reason,
|
||||
)
|
||||
|
||||
def to_dict(self, *, include_task=True, redact_payload: bool = True) -> dict[str, Any]:
|
||||
payload = {
|
||||
"task_id": self.task_id,
|
||||
"status": self.status,
|
||||
"revision": self.revision,
|
||||
"root_task_id": self.root_task_id,
|
||||
"parent_task_id": self.task_plan.parent_task_id,
|
||||
"created_at": _fmt_ts(self.created_at),
|
||||
"updated_at": _fmt_ts(self.updated_at),
|
||||
"failure_reason": self.failure_reason,
|
||||
}
|
||||
if include_task:
|
||||
payload["plan"] = self.task_plan.to_dict(redact=redact_payload)
|
||||
if self.idempotency_key:
|
||||
payload["idempotency_key"] = self.idempotency_key
|
||||
return payload
|
||||
|
||||
|
||||
@dataclass
|
||||
class WorkerRegistration:
|
||||
worker_id: str
|
||||
worker_type: str
|
||||
identity: str
|
||||
capabilities: list[str]
|
||||
trust_zone: str
|
||||
max_concurrency: int
|
||||
heartbeat_interval_seconds: int
|
||||
supported_protocols: list[str]
|
||||
registered_at: float
|
||||
last_heartbeat: float
|
||||
status: str = "active"
|
||||
disabled: bool = False
|
||||
owner_identity: str = ""
|
||||
registration_revision: int = 1
|
||||
registration_policy: str = "self"
|
||||
created_at: float = 0.0
|
||||
updated_at: float = 0.0
|
||||
disabled_reason: str | None = None
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, data: dict[str, Any]) -> "WorkerRegistration":
|
||||
if not isinstance(data, dict):
|
||||
raise ValueError("worker registration payload must be an object")
|
||||
now = _now_ts()
|
||||
worker_id = validate_identifier(data.get("worker_id"), "worker_id")
|
||||
worker_type = validate_type(data.get("worker_type"), "worker_type")
|
||||
identity = str(data.get("identity") or "").strip()
|
||||
if not identity or any(ord(ch) < 32 for ch in identity):
|
||||
raise ValueError("invalid identity")
|
||||
max_concurrency = _to_positive_int(data.get("max_concurrency"), default=1)
|
||||
heartbeat_interval = _to_positive_int(
|
||||
data.get("heartbeat_interval_seconds"), default=15
|
||||
)
|
||||
if not max_concurrency:
|
||||
raise ValueError("max_concurrency must be positive")
|
||||
if not heartbeat_interval:
|
||||
raise ValueError("heartbeat_interval_seconds must be positive")
|
||||
return cls(
|
||||
worker_id=worker_id,
|
||||
worker_type=worker_type,
|
||||
identity=identity,
|
||||
capabilities=_normalize_list(data.get("capabilities", [])),
|
||||
trust_zone=str(data.get("trust_zone") or "default"),
|
||||
max_concurrency=max_concurrency,
|
||||
heartbeat_interval_seconds=heartbeat_interval,
|
||||
supported_protocols=_normalize_list(
|
||||
data.get("supported_protocols", ["aios-task-v1"])
|
||||
),
|
||||
registered_at=float(data.get("registered_at") or now),
|
||||
last_heartbeat=float(data.get("last_heartbeat") or now),
|
||||
status=str(data.get("status") or "active"),
|
||||
disabled=bool(data.get("disabled", False)),
|
||||
owner_identity=str(data.get("owner_identity") or identity),
|
||||
registration_revision=int(data.get("registration_revision") or 1),
|
||||
registration_policy=str(data.get("registration_policy") or "self"),
|
||||
created_at=float(data.get("created_at") or data.get("registered_at") or now),
|
||||
updated_at=float(data.get("updated_at") or now),
|
||||
disabled_reason=data.get("disabled_reason"),
|
||||
)
|
||||
|
||||
def to_dict(self, redact_payload: bool = True) -> dict[str, Any]:
|
||||
payload = {
|
||||
"worker_id": self.worker_id,
|
||||
"worker_type": self.worker_type,
|
||||
"identity": self.identity,
|
||||
"capabilities": list(self.capabilities),
|
||||
"trust_zone": self.trust_zone,
|
||||
"max_concurrency": self.max_concurrency,
|
||||
"heartbeat_interval_seconds": self.heartbeat_interval_seconds,
|
||||
"supported_protocols": list(self.supported_protocols),
|
||||
"registered_at": _fmt_ts(self.registered_at),
|
||||
"last_heartbeat": _fmt_ts(self.last_heartbeat),
|
||||
"status": self.status,
|
||||
"disabled": self.disabled,
|
||||
"owner_identity": self.owner_identity,
|
||||
"registration_revision": self.registration_revision,
|
||||
"registration_policy": self.registration_policy,
|
||||
"created_at": _fmt_ts(self.created_at or self.registered_at),
|
||||
"updated_at": _fmt_ts(self.updated_at or self.last_heartbeat),
|
||||
"disabled_reason": self.disabled_reason,
|
||||
}
|
||||
if redact_payload:
|
||||
payload = redact_value(payload)
|
||||
return payload
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class EvidenceEnvelope:
|
||||
schema_version: int
|
||||
evidence_id: str
|
||||
task_id: str
|
||||
claim_id: str
|
||||
kind: str
|
||||
worker_identity: str
|
||||
payload: dict[str, Any]
|
||||
created_at: float
|
||||
previous_evidence_hash: str
|
||||
evidence_digest: str
|
||||
signature: str | None = None
|
||||
attestation_ref: str | None = None
|
||||
|
||||
@classmethod
|
||||
def create(
|
||||
cls,
|
||||
*,
|
||||
task_id: str,
|
||||
claim_id: str,
|
||||
kind: str,
|
||||
worker_identity: str,
|
||||
payload: dict[str, Any],
|
||||
previous_evidence_hash: str,
|
||||
signature: str | None = None,
|
||||
attestation_ref: str | None = None,
|
||||
evidence_id: str | None = None,
|
||||
created_at: float | None = None,
|
||||
) -> "EvidenceEnvelope":
|
||||
envelope = cls(
|
||||
schema_version=1,
|
||||
evidence_id=evidence_id or f"evd-{uuid.uuid4()}",
|
||||
task_id=validate_identifier(task_id, "task_id"),
|
||||
claim_id=validate_identifier(claim_id, "claim_id"),
|
||||
kind=validate_type(kind, "evidence kind"),
|
||||
worker_identity=str(worker_identity),
|
||||
payload=redact_value(payload),
|
||||
created_at=float(created_at or _now_ts()),
|
||||
previous_evidence_hash=str(previous_evidence_hash or ""),
|
||||
evidence_digest="",
|
||||
signature=signature,
|
||||
attestation_ref=attestation_ref,
|
||||
)
|
||||
return dataclass_replace(
|
||||
envelope, evidence_digest=compute_sha256_hex(envelope.digest_payload())
|
||||
)
|
||||
|
||||
def digest_payload(self) -> dict[str, Any]:
|
||||
return {
|
||||
"schema_version": self.schema_version,
|
||||
"evidence_id": self.evidence_id,
|
||||
"task_id": self.task_id,
|
||||
"claim_id": self.claim_id,
|
||||
"kind": self.kind,
|
||||
"worker_identity": self.worker_identity,
|
||||
"payload": self.payload,
|
||||
"created_at": self.created_at,
|
||||
"previous_evidence_hash": self.previous_evidence_hash,
|
||||
"attestation_ref": self.attestation_ref,
|
||||
}
|
||||
|
||||
def verify(
|
||||
self,
|
||||
*,
|
||||
expected_task_id: str,
|
||||
expected_claim_id: str,
|
||||
expected_previous_hash: str,
|
||||
attestation_verifier: Any = None,
|
||||
) -> bool:
|
||||
if (
|
||||
self.schema_version != 1
|
||||
or self.task_id != expected_task_id
|
||||
or self.claim_id != expected_claim_id
|
||||
or self.previous_evidence_hash != expected_previous_hash
|
||||
or self.evidence_digest != compute_sha256_hex(self.digest_payload())
|
||||
):
|
||||
return False
|
||||
if self.signature or self.attestation_ref:
|
||||
return True if attestation_verifier is None else bool(attestation_verifier(self))
|
||||
return True
|
||||
|
||||
def to_dict(self) -> dict[str, Any]:
|
||||
return {
|
||||
**self.digest_payload(),
|
||||
"evidence_digest": self.evidence_digest,
|
||||
"signature": self.signature,
|
||||
}
|
||||
|
||||
|
||||
class ProbeOutcome(StrEnum):
|
||||
NOT_APPLIED = "NOT_APPLIED"
|
||||
APPLIED_VERIFIED = "APPLIED_VERIFIED"
|
||||
INDETERMINATE = "INDETERMINATE"
|
||||
CONFLICT = "CONFLICT"
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class ProbeEvidence:
|
||||
schema_version: int
|
||||
probe_id: str
|
||||
task_id: str
|
||||
claim_id: str
|
||||
worker_id: str
|
||||
worker_identity: str
|
||||
operation_digest: str
|
||||
outcome: ProbeOutcome
|
||||
observed_at: float
|
||||
expires_at: float
|
||||
probe_executor_identity: str
|
||||
evidence_digest: str
|
||||
signature: str | None = None
|
||||
attestation_ref: str | None = None
|
||||
|
||||
@classmethod
|
||||
def create(cls, **data: Any) -> "ProbeEvidence":
|
||||
outcome = data["outcome"]
|
||||
if not isinstance(outcome, ProbeOutcome):
|
||||
outcome = ProbeOutcome(str(outcome))
|
||||
probe = cls(
|
||||
schema_version=1,
|
||||
probe_id=data.get("probe_id") or f"probe-{uuid.uuid4()}",
|
||||
task_id=validate_identifier(data["task_id"], "task_id"),
|
||||
claim_id=validate_identifier(data["claim_id"], "claim_id"),
|
||||
worker_id=validate_identifier(data["worker_id"], "worker_id"),
|
||||
worker_identity=str(data["worker_identity"]),
|
||||
operation_digest=str(data["operation_digest"]),
|
||||
outcome=outcome,
|
||||
observed_at=float(data["observed_at"]),
|
||||
expires_at=float(data["expires_at"]),
|
||||
probe_executor_identity=str(data["probe_executor_identity"]),
|
||||
evidence_digest="",
|
||||
signature=data.get("signature"),
|
||||
attestation_ref=data.get("attestation_ref"),
|
||||
)
|
||||
return dataclass_replace(
|
||||
probe, evidence_digest=compute_sha256_hex(probe.digest_payload())
|
||||
)
|
||||
|
||||
def digest_payload(self) -> dict[str, Any]:
|
||||
return {
|
||||
"schema_version": self.schema_version,
|
||||
"probe_id": self.probe_id,
|
||||
"task_id": self.task_id,
|
||||
"claim_id": self.claim_id,
|
||||
"worker_id": self.worker_id,
|
||||
"worker_identity": self.worker_identity,
|
||||
"operation_digest": self.operation_digest,
|
||||
"outcome": self.outcome.value,
|
||||
"observed_at": self.observed_at,
|
||||
"expires_at": self.expires_at,
|
||||
"probe_executor_identity": self.probe_executor_identity,
|
||||
"attestation_ref": self.attestation_ref,
|
||||
}
|
||||
|
||||
def verify(self, *, now: float, attestation_verifier: Any) -> bool:
|
||||
if (
|
||||
self.schema_version != 1
|
||||
or self.expires_at <= now
|
||||
or self.observed_at > now
|
||||
or self.evidence_digest != compute_sha256_hex(self.digest_payload())
|
||||
or not (self.signature or self.attestation_ref)
|
||||
):
|
||||
return False
|
||||
return bool(attestation_verifier and attestation_verifier(self))
|
||||
|
||||
def to_dict(self) -> dict[str, Any]:
|
||||
return {
|
||||
**self.digest_payload(),
|
||||
"evidence_digest": self.evidence_digest,
|
||||
"signature": self.signature,
|
||||
}
|
||||
|
||||
|
||||
def dataclass_replace(value: Any, **changes: Any) -> Any:
|
||||
from dataclasses import replace
|
||||
|
||||
return replace(value, **changes)
|
||||
|
||||
|
||||
@dataclass
|
||||
class WorkerClaim:
|
||||
claim_id: str
|
||||
task_id: str
|
||||
worker_id: str
|
||||
worker_identity: str
|
||||
claimed_at: float
|
||||
lease_until: float
|
||||
attempt: int
|
||||
task_revision: int
|
||||
operation_digest: str
|
||||
status: str = "ACTIVE"
|
||||
|
||||
def to_dict(self, redact_payload: bool = True) -> dict[str, Any]:
|
||||
payload = {
|
||||
"claim_id": self.claim_id,
|
||||
"task_id": self.task_id,
|
||||
"worker_id": self.worker_id,
|
||||
"worker_identity": self.worker_identity,
|
||||
"claimed_at": _fmt_ts(self.claimed_at),
|
||||
"lease_until": _fmt_ts(self.lease_until),
|
||||
"attempt": self.attempt,
|
||||
"task_revision": self.task_revision,
|
||||
"operation_digest": self.operation_digest,
|
||||
"status": self.status,
|
||||
}
|
||||
if redact_payload:
|
||||
payload = redact_value(payload)
|
||||
return payload
|
||||
|
||||
|
||||
@dataclass
|
||||
class ResultEnvelope:
|
||||
task_id: str
|
||||
claim_id: str
|
||||
worker_identity: str
|
||||
status: str
|
||||
outputs: list[dict[str, Any]]
|
||||
evidence: list[dict[str, Any]]
|
||||
started_at: str
|
||||
completed_at: str
|
||||
result_digest: str | None = None
|
||||
signature: str | None = None
|
||||
attestation_ref: str | None = None
|
||||
operation_digest: str | None = None
|
||||
trust_status: str = "untrusted_shadow_result"
|
||||
|
||||
def digest_payload(self) -> dict[str, Any]:
|
||||
return {
|
||||
"task_id": self.task_id,
|
||||
"claim_id": self.claim_id,
|
||||
"worker_identity": self.worker_identity,
|
||||
"status": self.status,
|
||||
"outputs": self.outputs,
|
||||
"evidence": self.evidence,
|
||||
"started_at": self.started_at,
|
||||
"completed_at": self.completed_at,
|
||||
"operation_digest": self.operation_digest,
|
||||
"attestation_ref": self.attestation_ref,
|
||||
}
|
||||
|
||||
def computed_digest(self) -> str:
|
||||
return compute_sha256_hex(self.digest_payload())
|
||||
|
||||
def to_dict(self, redact_payload: bool = True) -> dict[str, Any]:
|
||||
payload = {
|
||||
"task_id": self.task_id,
|
||||
"claim_id": self.claim_id,
|
||||
"worker_identity": self.worker_identity,
|
||||
"status": self.status,
|
||||
"outputs": self.outputs,
|
||||
"evidence": self.evidence,
|
||||
"started_at": self.started_at,
|
||||
"completed_at": self.completed_at,
|
||||
"result_digest": self.result_digest,
|
||||
"signature": self.signature,
|
||||
"attestation_ref": self.attestation_ref,
|
||||
"operation_digest": self.operation_digest,
|
||||
"trust_status": self.trust_status,
|
||||
}
|
||||
if redact_payload:
|
||||
payload = redact_value(payload)
|
||||
return payload
|
||||
|
||||
|
||||
def _normalize_list(value: Any) -> list[str]:
|
||||
if not isinstance(value, list):
|
||||
raise ValueError("expected list")
|
||||
if len(value) > 128 or any(
|
||||
not isinstance(v, str)
|
||||
or not v
|
||||
or len(v) > 256
|
||||
or any(ord(ch) < 32 for ch in v)
|
||||
for v in value
|
||||
):
|
||||
raise ValueError("invalid list item")
|
||||
return list(value)
|
||||
|
||||
|
||||
def _to_positive_int(value: Any, default: int | None = None) -> int | None:
|
||||
if value is None:
|
||||
return default
|
||||
try:
|
||||
number = int(value)
|
||||
except (TypeError, ValueError):
|
||||
return default
|
||||
return number if number >= 0 else default
|
||||
|
||||
|
||||
def _to_float(value: Any) -> float | None:
|
||||
if value is None:
|
||||
return None
|
||||
try:
|
||||
return float(value)
|
||||
except (TypeError, ValueError):
|
||||
return None
|
||||
|
||||
|
||||
def _first_non_none(primary: Any, fallback: Any) -> Any:
|
||||
if primary is not None:
|
||||
return primary
|
||||
return fallback
|
||||
|
||||
|
||||
def iso8601_now() -> str:
|
||||
return datetime.now(timezone.utc).replace(microsecond=0).isoformat().replace("+00:00", "Z")
|
||||
|
||||
|
||||
def _fmt_ts(value: float | None) -> str | None:
|
||||
if value is None:
|
||||
return None
|
||||
return datetime.fromtimestamp(value, tz=timezone.utc).replace(microsecond=0).isoformat().replace("+00:00", "Z")
|
||||
@@ -0,0 +1,226 @@
|
||||
"""Policy and verification logic (fail-closed)."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import Callable
|
||||
|
||||
from .errors import ForbiddenError, LockedError
|
||||
from .models import ProbeEvidence, ResultEnvelope, TaskRecord
|
||||
|
||||
|
||||
RISK_RANK = {"low": 1, "medium": 2, "high": 3}
|
||||
READONLY_WORKER_ACTIONS = frozenset(
|
||||
{"claim", "result", "fail", "progress", "heartbeat"}
|
||||
)
|
||||
ALL_ACTION_CAPABILITIES = frozenset(
|
||||
{
|
||||
"task.creator",
|
||||
"worker.self_register",
|
||||
"worker.admin",
|
||||
"task.claim",
|
||||
"task.cancel",
|
||||
"task.delegate",
|
||||
"task.read",
|
||||
"task.audit",
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@dataclass
|
||||
class PolicyDecision:
|
||||
allowed: bool
|
||||
reason: str = ""
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class ReadonlyWorkerProfile:
|
||||
"""A narrowly-scoped identity allowed to process safe Shadow tasks only."""
|
||||
|
||||
worker_id: str
|
||||
identity: str
|
||||
capabilities: frozenset[str]
|
||||
task_types: frozenset[str] = frozenset({"code.review", "test.execute"})
|
||||
|
||||
def permits_task(self, task: TaskRecord) -> bool:
|
||||
plan = task.task_plan
|
||||
if plan.task_type not in self.task_types or plan.risk != "low":
|
||||
return False
|
||||
if plan.data_classification not in {"public", "internal"}:
|
||||
return False
|
||||
if plan.metadata.get("production_side_effects") is not False:
|
||||
return False
|
||||
return set(plan.required_capabilities).issubset(self.capabilities)
|
||||
|
||||
|
||||
class IdentityVerifier:
|
||||
def __init__(self, trusted_identities: set[str] | None = None):
|
||||
self.trusted_identities = trusted_identities
|
||||
|
||||
def verify(self, identity: str) -> bool:
|
||||
if self.trusted_identities is None:
|
||||
return True
|
||||
return identity in self.trusted_identities
|
||||
|
||||
|
||||
class EvidenceVerifier:
|
||||
def __init__(self, mandatory: list[str] | None = None):
|
||||
self.mandatory = set(mandatory or [])
|
||||
|
||||
def verify(self, task: TaskRecord, evidences: list[dict]) -> PolicyDecision:
|
||||
required = set(task.task_plan.evidence_requirements or [])
|
||||
if not required and not self.mandatory:
|
||||
return PolicyDecision(True)
|
||||
observed = {e.get("kind") for e in evidences}
|
||||
missing = required.union(self.mandatory) - observed
|
||||
if missing:
|
||||
return PolicyDecision(False, f"missing evidence: {', '.join(sorted(missing))}")
|
||||
return PolicyDecision(True)
|
||||
|
||||
|
||||
class PolicyEngine:
|
||||
"""
|
||||
Fail-closed policy engine with minimal but explicit decisions.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
identity_verifier: IdentityVerifier | None = None,
|
||||
evidence_verifier: EvidenceVerifier | None = None,
|
||||
execution_enabled: bool = False,
|
||||
max_risk: str = "medium",
|
||||
result_signature_verifier: Callable[[ResultEnvelope], bool] | None = None,
|
||||
probe_attestation_verifier: Callable[[ProbeEvidence], bool] | None = None,
|
||||
identity_capabilities: dict[str, set[str]] | None = None,
|
||||
admin_identities: set[str] | None = None,
|
||||
readonly_worker_execution_enabled: bool = False,
|
||||
readonly_worker_profiles: list[ReadonlyWorkerProfile] | None = None,
|
||||
) -> None:
|
||||
if max_risk not in RISK_RANK:
|
||||
raise ValueError("invalid max_risk")
|
||||
self.identity_verifier = identity_verifier or IdentityVerifier()
|
||||
self.evidence_verifier = evidence_verifier or EvidenceVerifier()
|
||||
self.execution_enabled = execution_enabled
|
||||
self.max_risk = max_risk
|
||||
self.result_signature_verifier = result_signature_verifier
|
||||
self.probe_attestation_verifier = probe_attestation_verifier
|
||||
self.identity_capabilities = {
|
||||
identity: set(capabilities)
|
||||
for identity, capabilities in (identity_capabilities or {}).items()
|
||||
}
|
||||
self.admin_identities = set(admin_identities or ())
|
||||
self.readonly_worker_execution_enabled = readonly_worker_execution_enabled
|
||||
self.readonly_worker_profiles = {
|
||||
profile.identity: profile for profile in (readonly_worker_profiles or [])
|
||||
}
|
||||
|
||||
def _readonly_worker_allows(
|
||||
self, action: str, actor: str, task: TaskRecord | None
|
||||
) -> bool:
|
||||
profile = self.readonly_worker_profiles.get(actor)
|
||||
return bool(
|
||||
self.readonly_worker_execution_enabled
|
||||
and action in READONLY_WORKER_ACTIONS
|
||||
and profile is not None
|
||||
and (task is None or profile.permits_task(task))
|
||||
)
|
||||
|
||||
def assert_readonly_worker_binding(self, actor: str, worker_id: str) -> None:
|
||||
profile = self.readonly_worker_profiles.get(actor)
|
||||
if profile is not None and profile.worker_id != worker_id:
|
||||
raise ForbiddenError("worker identity binding mismatch")
|
||||
|
||||
def assert_readonly_worker_registration(self, actor: str, payload: dict) -> None:
|
||||
profile = self.readonly_worker_profiles.get(actor)
|
||||
if profile is None:
|
||||
return
|
||||
if (
|
||||
str(payload.get("worker_id") or "") != profile.worker_id
|
||||
or str(payload.get("identity") or "") != profile.identity
|
||||
or set(payload.get("capabilities") or []) != set(profile.capabilities)
|
||||
or str(payload.get("trust_zone") or "") != "isolated-review"
|
||||
or int(payload.get("max_concurrency") or 0) != 1
|
||||
or int(payload.get("heartbeat_interval_seconds") or 0) != 15
|
||||
):
|
||||
raise ForbiddenError("readonly worker registration mismatch")
|
||||
|
||||
def verify_result(
|
||||
self, result: ResultEnvelope, task: TaskRecord | None = None
|
||||
) -> PolicyDecision:
|
||||
computed = result.computed_digest()
|
||||
if result.result_digest and result.result_digest != computed:
|
||||
return PolicyDecision(False, "result digest mismatch")
|
||||
sensitive = bool(
|
||||
task
|
||||
and (
|
||||
task.task_plan.data_classification
|
||||
in {"confidential", "credential-secret"}
|
||||
or any(
|
||||
capability.startswith(
|
||||
("code.write", "deploy", "secret", "production")
|
||||
)
|
||||
for capability in task.task_plan.required_capabilities
|
||||
)
|
||||
)
|
||||
)
|
||||
if sensitive and not (result.signature or result.attestation_ref):
|
||||
return PolicyDecision(False, "trusted result proof required")
|
||||
if result.signature is not None or result.attestation_ref is not None:
|
||||
if self.result_signature_verifier is None:
|
||||
return PolicyDecision(False, "result signature verifier unavailable")
|
||||
if not self.result_signature_verifier(result):
|
||||
return PolicyDecision(False, "result signature invalid")
|
||||
result.trust_status = "trusted_result"
|
||||
else:
|
||||
result.trust_status = "untrusted_shadow_result"
|
||||
return PolicyDecision(True)
|
||||
|
||||
def authorize(self, action: str, actor: str, task: TaskRecord | None = None, context: dict | None = None) -> PolicyDecision:
|
||||
context = context or {}
|
||||
if not self.identity_verifier.verify(actor):
|
||||
return PolicyDecision(False, "identity untrusted")
|
||||
|
||||
action_capabilities = {
|
||||
"create": "task.creator",
|
||||
"read": "task.read",
|
||||
"audit": "task.audit",
|
||||
"claim": "task.claim",
|
||||
"result": "task.claim",
|
||||
"fail": "task.claim",
|
||||
"progress": "task.claim",
|
||||
"heartbeat": "task.claim",
|
||||
"cancel": "task.cancel",
|
||||
"delegate": "task.delegate",
|
||||
"worker.register": "worker.self_register",
|
||||
"worker.admin": "worker.admin",
|
||||
}
|
||||
required_capability = action_capabilities.get(action)
|
||||
if required_capability:
|
||||
granted = set(
|
||||
self.identity_capabilities.get(
|
||||
actor, self.identity_capabilities.get("*", set())
|
||||
)
|
||||
)
|
||||
if required_capability not in granted:
|
||||
return PolicyDecision(False, f"action {action} not permitted")
|
||||
|
||||
if action in {"claim", "result", "fail", "cancel", "progress", "delegate", "heartbeat"} and not self.execution_enabled:
|
||||
if not self._readonly_worker_allows(action, actor, task):
|
||||
return PolicyDecision(False, "execution disabled")
|
||||
|
||||
if action == "delegate" and task and task.status not in {"COMPLETED", "FAILED_FINAL", "CANCELLED"}:
|
||||
risk = str(context.get("risk", task.task_plan.risk))
|
||||
if RISK_RANK.get(risk, 2) > RISK_RANK.get(self.max_risk, 1):
|
||||
return PolicyDecision(False, f"risk {risk} exceeds policy")
|
||||
|
||||
return PolicyDecision(True)
|
||||
|
||||
def assert_allowed(self, action: str, actor: str, task: TaskRecord | None = None, context: dict | None = None):
|
||||
decision = self.authorize(action, actor, task, context=context)
|
||||
if not decision.allowed:
|
||||
if decision.reason == "identity untrusted":
|
||||
raise ForbiddenError(decision.reason)
|
||||
if decision.reason == "execution disabled":
|
||||
raise LockedError("execution disabled")
|
||||
raise ForbiddenError(decision.reason)
|
||||
@@ -0,0 +1,76 @@
|
||||
"""Task scheduler primitives."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import time
|
||||
from .errors import NotFoundError
|
||||
from .capabilities import has_capabilities
|
||||
from .models import WorkerClaim
|
||||
from .store import TaskPlaneStore
|
||||
|
||||
|
||||
class TaskScheduler:
|
||||
def __init__(self, store: TaskPlaneStore):
|
||||
self.store = store
|
||||
|
||||
def next_queued_task(self, worker_id: str):
|
||||
worker = self.store.get_worker(worker_id)
|
||||
tasks = sorted(
|
||||
self.store.list_tasks(),
|
||||
key=lambda task: (
|
||||
-int(task.task_plan.metadata.get("priority", 0)),
|
||||
task.created_at,
|
||||
task.task_id,
|
||||
),
|
||||
)
|
||||
for task in tasks:
|
||||
if task.status not in {"CREATED", "QUEUED", "FAILED_RECOVERABLE"}:
|
||||
continue
|
||||
if task.task_plan.task_type == "worker.registry.audit":
|
||||
continue
|
||||
if task.status == "FAILED_RECOVERABLE" and not self.store.task_has_retryable_probe(
|
||||
task.task_id
|
||||
):
|
||||
continue
|
||||
if not has_capabilities(
|
||||
worker.capabilities, task.task_plan.required_capabilities
|
||||
):
|
||||
continue
|
||||
if any(
|
||||
self.store.get_task(dependency).status != "COMPLETED"
|
||||
for dependency in task.task_plan.dependencies
|
||||
):
|
||||
continue
|
||||
if (
|
||||
task.task_plan.data_classification == "credential-secret"
|
||||
and worker.worker_type != "secure_file_transformer"
|
||||
and "secure_file_transformer" not in worker.capabilities
|
||||
):
|
||||
continue
|
||||
return task
|
||||
return None
|
||||
|
||||
def schedule_for_worker(self, worker_id: str, lease_seconds: int = 15) -> WorkerClaim:
|
||||
worker = self.store.get_worker(worker_id)
|
||||
if worker.disabled:
|
||||
from .errors import LockedError
|
||||
|
||||
raise LockedError("worker disabled")
|
||||
if time.time() > worker.last_heartbeat + (
|
||||
worker.heartbeat_interval_seconds * 3
|
||||
):
|
||||
from .errors import LockedError
|
||||
|
||||
raise LockedError("worker offline")
|
||||
task = self.next_queued_task(worker_id)
|
||||
if task is None:
|
||||
raise NotFoundError("no queued tasks available")
|
||||
claim = self.store.claim_task(
|
||||
task.task_id,
|
||||
worker_id,
|
||||
worker.identity,
|
||||
lease_seconds=lease_seconds,
|
||||
expected_revision=task.revision,
|
||||
operation_digest=task.task_plan.operation_digest(),
|
||||
)
|
||||
return claim
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,68 @@
|
||||
"""Worker registry abstraction and read helpers."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import time
|
||||
|
||||
from .capabilities import has_capabilities
|
||||
from .errors import NotFoundError
|
||||
from .models import WorkerRegistration
|
||||
from .store import TaskPlaneStore
|
||||
|
||||
|
||||
class WorkerRegistry:
|
||||
def __init__(self, store: TaskPlaneStore):
|
||||
self.store = store
|
||||
|
||||
def register(
|
||||
self,
|
||||
payload: dict,
|
||||
*,
|
||||
actor_identity: str,
|
||||
is_admin: bool,
|
||||
expected_revision: int | None,
|
||||
) -> WorkerRegistration:
|
||||
reg = WorkerRegistration.from_dict(payload)
|
||||
return self.store.register_worker(
|
||||
reg,
|
||||
actor_identity=actor_identity,
|
||||
is_admin=is_admin,
|
||||
expected_revision=expected_revision,
|
||||
)
|
||||
|
||||
def heartbeat(
|
||||
self,
|
||||
worker_id: str,
|
||||
heartbeat_interval_seconds: int | None = None,
|
||||
*,
|
||||
actor_identity: str,
|
||||
expected_revision: int | None,
|
||||
) -> WorkerRegistration:
|
||||
return self.store.heartbeat_worker(
|
||||
worker_id,
|
||||
heartbeat_interval_seconds=heartbeat_interval_seconds,
|
||||
actor_identity=actor_identity,
|
||||
expected_revision=expected_revision,
|
||||
)
|
||||
|
||||
def list(self):
|
||||
return self.store.list_workers()
|
||||
|
||||
def get(self, worker_id: str) -> WorkerRegistration:
|
||||
try:
|
||||
return self.store.get_worker(worker_id)
|
||||
except NotFoundError:
|
||||
raise
|
||||
|
||||
def can_execute(self, worker_id: str, required_capabilities: list[str]) -> bool:
|
||||
worker = self.store.get_worker(worker_id)
|
||||
return has_capabilities(worker.capabilities, required_capabilities)
|
||||
|
||||
def is_online(self, worker_id: str, *, now: float | None = None) -> bool:
|
||||
worker = self.store.get_worker(worker_id)
|
||||
deadline = worker.last_heartbeat + (worker.heartbeat_interval_seconds * 3)
|
||||
return not worker.disabled and (time.time() if now is None else now) <= deadline
|
||||
|
||||
def stale_workers(self, *, now: float | None = None) -> list[WorkerRegistration]:
|
||||
current = time.time() if now is None else now
|
||||
return [worker for worker in self.list() if not self.is_online(worker.worker_id, now=current)]
|
||||
+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
|
||||
}
|
||||
+127
@@ -0,0 +1,127 @@
|
||||
{
|
||||
"schema": "aios.governance-approval/v1",
|
||||
"approval_type": "TASK_SCOPED_GOVERNANCE_BOOTSTRAP_POLICY_ACTIVATION",
|
||||
"approval_plan_id": "AIOS-TSG-BS-PHASE-A-RENEWAL-20260731T081147Z",
|
||||
"task_id": "AIOS-V2R1C4-TWO-STAGE-OWNER-CAMPAIGN-V2-PHASE-A-RENEWAL",
|
||||
"repository": "aios/aios",
|
||||
"issued_at": "2026-07-31T08:11:47Z",
|
||||
"expires_at": "2026-07-31T20:11:47Z",
|
||||
"expected_governance_base_oid": "ebd74398743e36523b593fcc395b73945d4924c5",
|
||||
"expected_governance_base_tree_oid": "ad1255a1127204fad1d1208d8f31dc94fa64e6e1",
|
||||
"approval_request_branch": "governance/request/two-stage-owner-campaign-v2-phase-a-renewal-20260731T081147Z",
|
||||
"request_head_binding": "GITEA_EXACT_HEAD_REVIEW_POST_COMMIT",
|
||||
"human_authorizer": "owner",
|
||||
"authorization_mode": "GITEA_EXACT_HEAD_REVIEW",
|
||||
"required_human_approvals": 1,
|
||||
"allowed_human_approvers": [
|
||||
"owner"
|
||||
],
|
||||
"require_latest_head_approval": true,
|
||||
"dismiss_stale_approvals": true,
|
||||
"auto_merge_after_machine_gates": true,
|
||||
"phase_a_scope": {
|
||||
"schema_id": "TASK_SCOPED_GOVERNANCE_BOOTSTRAP",
|
||||
"schema_version": 1,
|
||||
"activate_schema_and_validator_only": true,
|
||||
"phase_b_release_authorized": false,
|
||||
"publisher_authorized": false,
|
||||
"writer_authorized": false,
|
||||
"credential_projection_authorized": false,
|
||||
"tag_authorized": false,
|
||||
"registry_authorized": false,
|
||||
"baseline_authorized": false,
|
||||
"production_deployment_authorized": false
|
||||
},
|
||||
"content_bindings": {
|
||||
"schema_document": {
|
||||
"path": "services/aios-single-owner-governance/task-scoped-governance-bootstrap-v1.schema.json",
|
||||
"blob_oid": "9b117667c0d98f995ac059b0e1b5ffed06ca6444",
|
||||
"sha256": "6ec912a96c8bac5c0d3c0a017abc078cb9567ed54dcb20bb0eadb5926ac54fdd"
|
||||
},
|
||||
"validator_policy": {
|
||||
"path": "services/aios-single-owner-governance/aios_single_owner_governance/task_scoped_bootstrap.py",
|
||||
"blob_oid": "c18d15f31340f19fc5da87944901bdfb867ab6dc",
|
||||
"sha256": "de11123b5cd9ccdad4961f1d5d084a8c4a1b2270ce793652ab9c50bb95694eac"
|
||||
},
|
||||
"evidence_manifest": {
|
||||
"path": "evidence/governance-policy/AIOS-TWO-STAGE-OWNER-CAMPAIGN-V2-PHASE-A-20260730T023551Z.json",
|
||||
"blob_oid": "c3d215b3ea525a25de514c32c5aad91d47e140b4",
|
||||
"sha256": "1b0a993940b41a4b16724b557f6abd8b6cebb56dc64cc8f813bd605b6a3399c2"
|
||||
},
|
||||
"checksums_manifest": {
|
||||
"path": "evidence/governance-policy/AIOS-TWO-STAGE-OWNER-CAMPAIGN-V2-PHASE-A-20260730T023551Z.sha256",
|
||||
"blob_oid": "45ed084234dae6a00e69a41cd4f716f46a97d9fb",
|
||||
"sha256": "51c4b029c0792dc98d41f754e484ba3861cf7604443d9c5f289b3b0af768a41a"
|
||||
}
|
||||
},
|
||||
"offline_candidate_bindings": {
|
||||
"schema_seed_commit_oid": "8da47545a507cb798d3d6ab30ff8552a2d0a5ecf",
|
||||
"schema_seed_tree_oid": "991e68577a469657c1c4996814b6634ce5ed752f",
|
||||
"implementation_commit_oid": "a637a9dbb6e0021cb7dcf0d6b0ca00dfe10dce38",
|
||||
"implementation_tree_oid": "bf6e0c16fcad7515465bec4aefdd2e77b1e9bef0",
|
||||
"evidence_closure_commit_oid": "1e4885197f508f7921b148d71e545b3de0029b0a",
|
||||
"evidence_closure_tree_oid": "e55cf9ef19d1c6804f161de54fd3395a06149807",
|
||||
"source_diff_sha256": "c06d002db06da6cbf16f9c8321f1acbfd431f009b5127a13fd2cb5d63b278867",
|
||||
"source_pathset_sha256": "5061402d516c0e9413208ce64e2b52f77d224e6eec1f7b07c5b3a10b58df5eaf",
|
||||
"implementation_bundle_sha256": "15711b225b9ef41cca0588d21b2b95d7a95ac155efc27fcb8772c435fcdab147",
|
||||
"evidence_bundle_sha256": "ec97b8460151fc0e7e7ea9ae044ca13a1543c4b79c41a437ef087d3b1c271857"
|
||||
},
|
||||
"runtime_dependency_bindings": {
|
||||
"handoff_zip_sha256": "668239a549b4a2b2094fe9b5104b84703fe8ed75011fd8ed71ed3958a6dc0706",
|
||||
"installed_wheel_sha256": "abbf616366eb3467d4e651bb5306e22ae5e931b613df5362a0892935c4a1c0c7",
|
||||
"anchor_service": "aios-campaign-anchor.service",
|
||||
"anchor_uid": 988,
|
||||
"runner_uid": 986,
|
||||
"anchor_database": "/var/lib/aios-campaign-anchor/anchor.sqlite3",
|
||||
"anchor_socket": "/run/aios-campaign-anchor/anchor.sock",
|
||||
"AIOS_GITOPS_WRITES_ENABLED": false
|
||||
},
|
||||
"required_machine_status_contexts": [
|
||||
"governance/schema-contract",
|
||||
"governance/validator-security",
|
||||
"governance/fault-injection",
|
||||
"governance/runtime-isolation",
|
||||
"governance/authority-mirror"
|
||||
],
|
||||
"allowed_operations_after_exact_head_owner_approval": [
|
||||
"observe_governance_pr",
|
||||
"wait_governance_gates",
|
||||
"observe_governance_merge",
|
||||
"observe_validator_activation",
|
||||
"complete_policy_activation"
|
||||
],
|
||||
"denied_operations": [
|
||||
"main.write",
|
||||
"integration.write",
|
||||
"tag.write",
|
||||
"registry.write",
|
||||
"baseline.write",
|
||||
"production.deploy",
|
||||
"force_push",
|
||||
"history_rewrite",
|
||||
"governance.direct_push",
|
||||
"approval.impersonate",
|
||||
"publisher.create",
|
||||
"writer.create",
|
||||
"credential.project",
|
||||
"phase_b.execute"
|
||||
],
|
||||
"merge_constraints": {
|
||||
"allowed_modes": [
|
||||
"FAST_FORWARD_TO_APPROVED_HEAD",
|
||||
"EXACT_TWO_PARENT_MERGE_WITH_APPROVED_TREE"
|
||||
],
|
||||
"force": false,
|
||||
"plus_refspec": false,
|
||||
"tags": false,
|
||||
"prune": false
|
||||
},
|
||||
"governance_declarations": {
|
||||
"AIOS_GITOPS_WRITES_ENABLED": false,
|
||||
"force_push_used": false,
|
||||
"tag_rewritten": false,
|
||||
"direct_main_push": false,
|
||||
"secrets_committed": false,
|
||||
"owner_approval_respected": true
|
||||
}
|
||||
}
|
||||
+79
@@ -0,0 +1,79 @@
|
||||
{
|
||||
"AIOS_GITOPS_WRITES_ENABLED": false,
|
||||
"acknowledges_pr30_owner_approval_was_missing": true,
|
||||
"activation_prerequisites_artifact_sha256": "729077800c6510bf8930c97a78781218d1bce9ec40157aa99fc2bd205466e895",
|
||||
"activation_prerequisites_commit_oid": "fa038d17b29001e26f1ac88019a407a4b3ebaa7e",
|
||||
"activation_prerequisites_rollback_artifact_sha256": "00e40eace10a0aae1731eb98851fec71a0c13c48e1e6e5a113e30a4356c2086f",
|
||||
"activation_prerequisites_tree_oid": "d5ff67f848807ef2c7a695c05cdd0bb5f2644fc2",
|
||||
"approval_request_branch": "governance/request/single-owner-atomic-activation-owner-ratification-20260729T014918Z-e503d96665ee",
|
||||
"approval_type": "SINGLE_OWNER_GOVERNANCE_ATOMIC_ACTIVATION_OWNER_RATIFICATION",
|
||||
"branch_protection_adapter_sha256": "2706dc6095ff639838f72c91a66c1f80711893c8c0498555b388feaacf2db569",
|
||||
"denied_operations": [
|
||||
"main.write",
|
||||
"integration.write",
|
||||
"tag.write",
|
||||
"registry.write",
|
||||
"baseline.write",
|
||||
"production.deploy",
|
||||
"force_push",
|
||||
"history_rewrite",
|
||||
"governance.direct_push",
|
||||
"approval.impersonate"
|
||||
],
|
||||
"direct_protected_branch_push_allowed": false,
|
||||
"dismiss_stale_approvals": true,
|
||||
"dormant_deployment_evidence_sha256": "c4c326784a3d66d62e32c46be3506c801585c06a34e62c9a1f210492a6c1bc06",
|
||||
"evidence_closure_oid": "5f45139ca5f7f7f3da244141c5115d12f5af58ae",
|
||||
"evidence_closure_sha256": "53e5f2fdc01a6225048dfdf6eac973e99743f0295f17f3a42f01dce7e7050ab8",
|
||||
"expected_governance_base_oid": "f6bef90633f428454e85721f353cd82cdf04caf0",
|
||||
"expires_at": "2026-07-29T13:49:18Z",
|
||||
"explicitly_allowed_operations_after_exact_head_approvals_and_merge": [
|
||||
"approval_validator.activate",
|
||||
"governance_state_machine.activate",
|
||||
"gitea_branch_protection.atomic_update",
|
||||
"rollback_configuration.install",
|
||||
"single_owner_policy.activate",
|
||||
"break_glass.expire",
|
||||
"post_activation_tests.execute"
|
||||
],
|
||||
"force_push_allowed": false,
|
||||
"human_authorizer": "owner",
|
||||
"issued_at": "2026-07-29T01:49:18Z",
|
||||
"legacy_mechanical_approvals": [
|
||||
"codex",
|
||||
"gpt"
|
||||
],
|
||||
"merge_mode": "CREATE_MERGE_COMMIT",
|
||||
"pr30_codex_approval_review_id": 45,
|
||||
"pr30_codex_reviewed_at": "2026-07-29T01:24:38Z",
|
||||
"pr30_codex_reviewed_head_oid": "c2eed5b37983a6461783361efb6de43a28f909bb",
|
||||
"pr30_gpt_approval_review_id": 46,
|
||||
"pr30_gpt_reviewed_at": "2026-07-29T01:25:02Z",
|
||||
"pr30_gpt_reviewed_head_oid": "c2eed5b37983a6461783361efb6de43a28f909bb",
|
||||
"pr30_manifest_path": "approvals/governance-policy/SINGLE-OWNER-GOVERNANCE-ATOMIC-ACTIVATION-20260728T204808Z-4cab1d5829d6.json",
|
||||
"pr30_merge_tree_oid": "219974f085efe473fcc6ad2170f67312d8e57c54",
|
||||
"pr30_merged_at": "2026-07-29T01:25:08Z",
|
||||
"pr30_owner_review_id": 42,
|
||||
"pr30_owner_review_state_observed": "REQUESTED_NOT_APPROVED",
|
||||
"pr30_owner_reviewed_head_oid": null,
|
||||
"pr30_premerge_base_oid": "6e8212608ee310e2fb45f1e802359f4db2e9077b",
|
||||
"production_deployment_allowed": false,
|
||||
"ratifies_pr30_manifest_blob_oid": "38c95dde2d3831f8e3c80b342369f1c3e1cbf34c",
|
||||
"ratifies_pr30_manifest_sha256": "bc19ada0e8eeb47d421a6360cf1d66b6d1129eb6056a447d9ff70ebd5ada1718",
|
||||
"ratifies_pr30_merge_oid": "f6bef90633f428454e85721f353cd82cdf04caf0",
|
||||
"ratifies_pr30_request_oid": "c2eed5b37983a6461783361efb6de43a28f909bb",
|
||||
"ratifies_pr_number": 30,
|
||||
"require_latest_head_approval": true,
|
||||
"required_reviewers": [
|
||||
"owner",
|
||||
"codex",
|
||||
"gpt"
|
||||
],
|
||||
"rollback_configuration_sha256": "eb44fc5f655f8c30c4b687185055ba3a2725189e19538e3a9a46a2c0cf141b8c",
|
||||
"schema": "aios.governance-approval/v1",
|
||||
"state_machine_sha256": "0b08a2c378d143cb9031a25132afc7b7d825bdc03d11efe10610341d4b70167e",
|
||||
"task_id": "AIOS-PR30-SINGLE-OWNER-ATOMIC-ACTIVATION-OWNER-RATIFICATION-20260729",
|
||||
"test_evidence_sha256": "42168da7437028440bece3432d8aa38a8722b4db18244423d9daaa380675c45a",
|
||||
"validator_sha256": "754639846cb15cb997d612a1fe0ceb0224ddef5bbc3ccac9802a3747f122dac1",
|
||||
"wip_blocks_merge": true
|
||||
}
|
||||
+91
@@ -0,0 +1,91 @@
|
||||
{
|
||||
"approval_request_branch": "governance/request/single-owner-governance-activation-prerequisites-20260728T183025127107Z",
|
||||
"approval_type": "SINGLE_OWNER_GOVERNANCE_ACTIVATION_PREREQUISITES",
|
||||
"binds_final_ratification_manifest_blob_oid": "2865fbec766901eb61fc8aed0542970e951e0d1d",
|
||||
"binds_final_ratification_manifest_sha256": "216e54503d8e6a3f4c908084e19ce1e6242d3ba15f7b8011a9a9d9f7975e63e7",
|
||||
"binds_final_ratification_merge_oid": "2bef44f6bc8cf7bc04c959ab629ac95561fa609f",
|
||||
"binds_mirror_receipt_sha256": "6873133f7233bfed0d3b78b01847ad4251b8b67f3b6db7011efd07664f8d088f",
|
||||
"expected_governance_base_oid": "2bef44f6bc8cf7bc04c959ab629ac95561fa609f",
|
||||
"expires_at": "2026-07-29T06:30:25Z",
|
||||
"explicitly_allowed_after_owner_approval_and_legacy_merge": [
|
||||
"task_bound_activation_writer_grant.create",
|
||||
"gitea_owner_review_adapter.create_and_dormant_deploy",
|
||||
"machine_status_producer.create_and_dormant_deploy",
|
||||
"machine_status_producer.credential_rotation_and_revocation",
|
||||
"maintenance_lock.create",
|
||||
"branch_protection_compensation_saga.create",
|
||||
"targeted_online_rollback_executor.create",
|
||||
"disposable_gitea_fixture.create_test_destroy",
|
||||
"single_owner_activation_prerequisite_tests.execute",
|
||||
"activation_prerequisite_evidence.create"
|
||||
],
|
||||
"implementation_branch": "patch/single-owner-governance-activation-prerequisites-20260729",
|
||||
"implementation_paths": [
|
||||
"services/aios-single-owner-governance/**",
|
||||
"tests/single_owner_governance/**",
|
||||
"deploy/systemd/aios-governance-*.service",
|
||||
"deploy/systemd/aios-governance-*.timer"
|
||||
],
|
||||
"issued_at": "2026-07-28T18:30:25Z",
|
||||
"machine_status_contexts": [
|
||||
"aios/machine-gates",
|
||||
"aios/secret-scan",
|
||||
"aios/evidence-closure"
|
||||
],
|
||||
"observed_blockers": {
|
||||
"commit_status_row_count": 0,
|
||||
"disposable_governance_fixture_present": false,
|
||||
"gitea_owner_review_validator_present": false,
|
||||
"gitea_version": "1.22.6",
|
||||
"native_branch_protection_etag_cas_present": false,
|
||||
"required_status_context_producers_present": false,
|
||||
"targeted_online_rollback_executor_present": false
|
||||
},
|
||||
"owner_approval_contract": {
|
||||
"agent_must_not_submit_review": true,
|
||||
"dismissed": false,
|
||||
"head_change_invalidates_approval": true,
|
||||
"only_human_approver": "owner",
|
||||
"owner_user_id": 2,
|
||||
"review_state": "APPROVED",
|
||||
"reviewed_before_expires_at": true,
|
||||
"reviewed_head_oid_must_equal_current_head_oid": true,
|
||||
"stale": false
|
||||
},
|
||||
"permanent_constraints": {
|
||||
"AIOS_GITOPS_WRITES_ENABLED": false,
|
||||
"direct_main_push": false,
|
||||
"force_push": false,
|
||||
"history_rewrite": false,
|
||||
"secret_commit": false,
|
||||
"tag_rewrite": false
|
||||
},
|
||||
"pre_approval_non_authorizations": {
|
||||
"activation_allowed": false,
|
||||
"baseline_write_allowed": false,
|
||||
"branch_protection_change_allowed": false,
|
||||
"fixture_deployment_allowed": false,
|
||||
"integration_write_allowed": false,
|
||||
"main_write_allowed": false,
|
||||
"merge_allowed": false,
|
||||
"production_write_allowed": false,
|
||||
"registry_write_allowed": false,
|
||||
"tag_write_allowed": false,
|
||||
"writer_grant_allowed": false
|
||||
},
|
||||
"reason": "safe-atomic-activation-prerequisites-absent",
|
||||
"repository": "aios/aios",
|
||||
"request_status": "HUMAN_OWNER_APPROVAL_PENDING",
|
||||
"schema": "aios.single-owner-governance-activation-prerequisites/v1",
|
||||
"task_id": "AIOS-SINGLE-OWNER-GOVERNANCE-ACTIVATION-PREREQUISITES-20260729",
|
||||
"wip": true,
|
||||
"wip_transition_contract": {
|
||||
"codex_and_gpt_not_requested_before_owner_approval": true,
|
||||
"remove_wip_without_head_change_only_after_owner_approval": true,
|
||||
"then_request_legacy_mechanical_reviews": [
|
||||
"codex",
|
||||
"gpt"
|
||||
],
|
||||
"wip_must_remain_until_owner_exact_head_approval": true
|
||||
}
|
||||
}
|
||||
+68
@@ -0,0 +1,68 @@
|
||||
{
|
||||
"AIOS_GITOPS_WRITES_ENABLED": false,
|
||||
"activation_prerequisites_artifact_sha256": "729077800c6510bf8930c97a78781218d1bce9ec40157aa99fc2bd205466e895",
|
||||
"activation_prerequisites_commit_oid": "fa038d17b29001e26f1ac88019a407a4b3ebaa7e",
|
||||
"activation_prerequisites_rollback_artifact_sha256": "00e40eace10a0aae1731eb98851fec71a0c13c48e1e6e5a113e30a4356c2086f",
|
||||
"activation_prerequisites_tree_oid": "d5ff67f848807ef2c7a695c05cdd0bb5f2644fc2",
|
||||
"approval_request_branch": "governance/request/single-owner-governance-atomic-activation-20260728T204808Z-4cab1d5829d6",
|
||||
"approval_type": "SINGLE_OWNER_GOVERNANCE_ATOMIC_ACTIVATION",
|
||||
"branch_protection_adapter_sha256": "2706dc6095ff639838f72c91a66c1f80711893c8c0498555b388feaacf2db569",
|
||||
"denied_operations": [
|
||||
"main.write",
|
||||
"integration.write",
|
||||
"tag.write",
|
||||
"registry.write",
|
||||
"baseline.write",
|
||||
"production.deploy",
|
||||
"force_push",
|
||||
"history_rewrite",
|
||||
"governance.direct_push",
|
||||
"approval.impersonate"
|
||||
],
|
||||
"deployment_mode_before_execution": "DORMANT_SHADOW_ONLY",
|
||||
"direct_protected_branch_push_allowed": false,
|
||||
"dismiss_stale_approvals": true,
|
||||
"dormant_deployment_evidence_sha256": "c4c326784a3d66d62e32c46be3506c801585c06a34e62c9a1f210492a6c1bc06",
|
||||
"evidence_closure_oid": "5f45139ca5f7f7f3da244141c5115d12f5af58ae",
|
||||
"evidence_closure_sha256": "53e5f2fdc01a6225048dfdf6eac973e99743f0295f17f3a42f01dce7e7050ab8",
|
||||
"expected_governance_base_oid": "6e8212608ee310e2fb45f1e802359f4db2e9077b",
|
||||
"expires_at": "2026-07-29T08:48:08Z",
|
||||
"explicitly_allowed_operations_after_exact_head_approvals_and_merge": [
|
||||
"approval_validator.activate",
|
||||
"governance_state_machine.activate",
|
||||
"gitea_branch_protection.atomic_update",
|
||||
"rollback_configuration.install",
|
||||
"single_owner_policy.activate",
|
||||
"break_glass.expire",
|
||||
"post_activation_tests.execute"
|
||||
],
|
||||
"force_push_allowed": false,
|
||||
"issued_at": "2026-07-28T20:48:08Z",
|
||||
"legacy_mechanical_approvals": [
|
||||
"codex",
|
||||
"gpt"
|
||||
],
|
||||
"machine_gates_required": true,
|
||||
"merge_mode": "CREATE_MERGE_COMMIT",
|
||||
"pr26_merge_oid": "a5489f1b67870622162b2daa781bd0237735831a",
|
||||
"pr27_merge_oid": "4e6ef70029b1d94626e432892f7d3beb8e3e5627",
|
||||
"pr28_merge_oid": "2bef44f6bc8cf7bc04c959ab629ac95561fa609f",
|
||||
"pr29_merge_oid": "6e8212608ee310e2fb45f1e802359f4db2e9077b",
|
||||
"production_deployment_allowed": false,
|
||||
"require_latest_head_approval": true,
|
||||
"required_human_authorizer": "owner",
|
||||
"required_reviewers": [
|
||||
"owner",
|
||||
"codex",
|
||||
"gpt"
|
||||
],
|
||||
"rollback_configuration_sha256": "eb44fc5f655f8c30c4b687185055ba3a2725189e19538e3a9a46a2c0cf141b8c",
|
||||
"rollback_required_on_any_activation_failure": true,
|
||||
"schema": "aios.governance-approval/v1",
|
||||
"single_owner_policy_active_before_execution": false,
|
||||
"state_machine_sha256": "0b08a2c378d143cb9031a25132afc7b7d825bdc03d11efe10610341d4b70167e",
|
||||
"task_id": "AIOS-SINGLE-OWNER-GOVERNANCE-ATOMIC-ACTIVATION-20260729",
|
||||
"test_evidence_sha256": "42168da7437028440bece3432d8aa38a8722b4db18244423d9daaa380675c45a",
|
||||
"validator_sha256": "754639846cb15cb997d612a1fe0ceb0224ddef5bbc3ccac9802a3747f122dac1",
|
||||
"wip_blocks_merge": true
|
||||
}
|
||||
+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"
|
||||
}
|
||||
+260
@@ -0,0 +1,260 @@
|
||||
{
|
||||
"acknowledges_pr26_and_pr27_owner_approval_was_missing": true,
|
||||
"approval_request_branch": "governance/request/single-owner-governance-final-owner-ratification-20260728T173634412838Z",
|
||||
"approval_type": "SINGLE_OWNER_GOVERNANCE_FINAL_OWNER_RATIFICATION",
|
||||
"backup_checksums_sha256": "72e2fe0b03d1011c872bb39a6cff489f08398ac7d52092f720cbbc63bcf6f753",
|
||||
"backup_root": "/var/lib/aios-gitops/runtime/tasks/AIOS-SINGLE-OWNER-GOVERNANCE-MIGRATION-20260729/pre-migration-backup",
|
||||
"break_glass_expires_immediately_after_activation": true,
|
||||
"does_not_claim_old_two_natural_person_compliance": true,
|
||||
"expires_at": "2026-07-29T05:36:34Z",
|
||||
"inherits_only_pr27_control_plane_allowlist": [
|
||||
"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"
|
||||
],
|
||||
"issued_at": "2026-07-28T17:36:34Z",
|
||||
"legacy_mechanical_protection_gate": {
|
||||
"account_reviews_do_not_count_as_owner_ratification": true,
|
||||
"account_reviews_do_not_prove_distinct_natural_persons": true,
|
||||
"approval_and_merge_whitelist_user_ids": [
|
||||
42,
|
||||
43
|
||||
],
|
||||
"both_reviews_must_bind_current_request_head": true,
|
||||
"required_approvals": 2,
|
||||
"reviewers": [
|
||||
"codex",
|
||||
"gpt"
|
||||
]
|
||||
},
|
||||
"merge_contract": {
|
||||
"first_parent_must_equal_premerge_governance_oid": true,
|
||||
"force": false,
|
||||
"merge_mode": "CREATE_MERGE_COMMIT",
|
||||
"parent_count": 2,
|
||||
"rebase": false,
|
||||
"second_parent_must_equal_final_ratification_request_oid": true,
|
||||
"single_file_add_only": true,
|
||||
"squash": false
|
||||
},
|
||||
"migration_mode": "OWNER_BREAK_GLASS_GOVERNANCE_RESET",
|
||||
"mirror_contract": {
|
||||
"expected_authoritative_pre_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,
|
||||
"required_chain": [
|
||||
"ac2dc5c30eb35748ef11c98297812729d0fd5828",
|
||||
"a5489f1b67870622162b2daa781bd0237735831a",
|
||||
"4e6ef70029b1d94626e432892f7d3beb8e3e5627",
|
||||
"FINAL_RATIFICATION_MERGE_OID"
|
||||
],
|
||||
"source": "gitea_repository_file_url_only",
|
||||
"tags": false
|
||||
},
|
||||
"owner_ratification_contract": {
|
||||
"agent_must_not_submit_review": true,
|
||||
"dismissed": false,
|
||||
"only_valid_human_approver": "owner",
|
||||
"owner_user_id": 2,
|
||||
"review_state": "APPROVED",
|
||||
"review_type_must_be_approved_not_requested": true,
|
||||
"reviewed_before_expires_at": true,
|
||||
"reviewed_before_merge": true,
|
||||
"reviewed_head_oid_must_equal_current_request_head_oid": true,
|
||||
"stale": false
|
||||
},
|
||||
"permanent_constraints": {
|
||||
"AIOS_GITOPS_WRITES_ENABLED": false,
|
||||
"direct_main_push": false,
|
||||
"force_push_used": false,
|
||||
"owner_approval_respected": true,
|
||||
"secrets_committed": false,
|
||||
"tag_rewritten": false
|
||||
},
|
||||
"post_merge_activation_preconditions": [
|
||||
"owner_exact_head_approval_valid",
|
||||
"legacy_mechanical_reviews_valid",
|
||||
"ordinary_merge_commit",
|
||||
"one_file_add_only",
|
||||
"quarantine_chain_verified",
|
||||
"approval_mirror_fast_forward_closed",
|
||||
"rollback_integrity_pass",
|
||||
"validator_and_state_machine_tests_pass",
|
||||
"protection_compare_and_swap_pass",
|
||||
"positive_and_negative_governance_tests_pass",
|
||||
"production_write_false"
|
||||
],
|
||||
"pre_activation_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_activation_protection_snapshot_sha256": "2bc353f34689242cabebf89856d3faab0e1c496b5d33761dfb7cbd47904516f7",
|
||||
"pre_approval_non_authorizations": {
|
||||
"baseline_write_allowed": false,
|
||||
"branch_protection_update_allowed": false,
|
||||
"control_plane_activation_allowed": false,
|
||||
"development_allowed": false,
|
||||
"integration_write_allowed": false,
|
||||
"main_write_allowed": false,
|
||||
"merge_allowed": false,
|
||||
"mirror_allowed": false,
|
||||
"production_write_allowed": false,
|
||||
"registry_write_allowed": false,
|
||||
"rollback_install_allowed": false,
|
||||
"standing_writer_allowed": false,
|
||||
"state_machine_update_allowed": false,
|
||||
"tag_write_allowed": false,
|
||||
"validator_update_allowed": false
|
||||
},
|
||||
"ratification_scope": "GOVERNANCE_MIGRATION_AND_CONTROL_PLANE_ACTIVATION_ONLY",
|
||||
"ratifies_pr26_manifest_blob_oid": "65f44007b10fc2a912d69e3f9081c0cfff77dfad",
|
||||
"ratifies_pr26_manifest_path": "approvals/governance-policy/SINGLE-OWNER-GOVERNANCE-MIGRATION-20260728T162246Z.json",
|
||||
"ratifies_pr26_manifest_sha256": "c097475afbe3074e6482f125b635981f8c0284f3d763c926e2a63e1a3ee1c6c3",
|
||||
"ratifies_pr26_merge_oid": "a5489f1b67870622162b2daa781bd0237735831a",
|
||||
"ratifies_pr26_request_oid": "312d3b483049a748675e887a516ec739c81ff474",
|
||||
"ratifies_pr26_tree_oid": "5c6974c058bba6210dcc1bdd0968fb7e6c84a844",
|
||||
"ratifies_pr27_manifest_blob_oid": "77f3c2ab85cba2c30c8c0aa36e591232d0f4c396",
|
||||
"ratifies_pr27_manifest_path": "approvals/governance-policy/SINGLE-OWNER-GOVERNANCE-CONTROL-PLANE-DEPLOYMENT-20260728T170740Z-ea7a102dac417c23.json",
|
||||
"ratifies_pr27_manifest_sha256": "2ed09180c0aeadbef2e5d6310efe7c2d0de15e0e87b6135e7a8494cb4f0967c7",
|
||||
"ratifies_pr27_merge_oid": "4e6ef70029b1d94626e432892f7d3beb8e3e5627",
|
||||
"ratifies_pr27_request_oid": "80feef309fd8e1db7795f68d5bf0f15ce805b365",
|
||||
"ratifies_pr27_tree_oid": "85af0d1e88e8484a9c3a0562a81cbc1de09250ae",
|
||||
"ratifies_pr_numbers": [
|
||||
26,
|
||||
27
|
||||
],
|
||||
"repository": "aios/aios",
|
||||
"request_status": "HUMAN_OWNER_APPROVAL_PENDING",
|
||||
"retroactive_authorization_for_other_operations": false,
|
||||
"rollback_archive_member_hashes": {
|
||||
"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",
|
||||
"schema": "aios.single-owner-governance-final-owner-ratification/v1",
|
||||
"single_owner_policy_effective_at": "FINAL_RATIFICATION_MERGE_OID",
|
||||
"task_id": "AIOS-SINGLE-OWNER-GOVERNANCE-FINAL-RATIFICATION-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"
|
||||
}
|
||||
+73
@@ -0,0 +1,73 @@
|
||||
{
|
||||
"approval_type": "SINGLE_OWNER_GOVERNANCE_LIVE_ACTIVATION_IMPLEMENTATION_AND_EXECUTION",
|
||||
"artifact_sha256": "01e0226f0302bb8e2f8d81299393075bf0332c6e22e00877469fb297c51f3802",
|
||||
"authorization_scope": [
|
||||
"deploy_live_activation_implementation",
|
||||
"execute_one_atomic_governance_activation",
|
||||
"execute_acceptance_tests",
|
||||
"control_plane_rollback_on_failure",
|
||||
"write_activation_receipt"
|
||||
],
|
||||
"denied_operations": [
|
||||
"main.write",
|
||||
"integration.write",
|
||||
"tag.write",
|
||||
"registry.write",
|
||||
"baseline.write",
|
||||
"production.deploy",
|
||||
"force_push",
|
||||
"history_rewrite"
|
||||
],
|
||||
"evidence_closure_oid": "c6316943d2485f06f97242df6338ded21de1ee38",
|
||||
"evidence_closure_sha256": "bd521103c907d6194acbbbd42a947c5af2ccbda91797e5dc75e81887a042becd",
|
||||
"execution_binding": {
|
||||
"backend": "sqlite",
|
||||
"branch": "governance/approvals",
|
||||
"evidence_closure_oid": "c6316943d2485f06f97242df6338ded21de1ee38",
|
||||
"executor": "aios-governance-single-owner-activation",
|
||||
"expected_branch_protection_sha256": "f27992c8024426708d93bce072d231707bae47a927aa2393d566aae64d5ed079",
|
||||
"expected_gitea_schema_sha256": "3524f4912165939cb746c2c65d70583a909f5abbd00c4e07ae76f7206d49aa54",
|
||||
"expected_state_machine_sha256": "0b08a2c378d143cb9031a25132afc7b7d825bdc03d11efe10610341d4b70167e",
|
||||
"expected_validator_sha256": "754639846cb15cb997d612a1fe0ceb0224ddef5bbc3ccac9802a3747f122dac1",
|
||||
"forbidden_operations": {
|
||||
"baseline": false,
|
||||
"force_push": false,
|
||||
"governance_direct": false,
|
||||
"history_rewrite": false,
|
||||
"integration": false,
|
||||
"main": false,
|
||||
"production": false,
|
||||
"registry": false,
|
||||
"tags": false
|
||||
},
|
||||
"operation_id": "single-owner-live-activation-20260729T033228Z",
|
||||
"repo_id": 1,
|
||||
"requester": "codex-agent",
|
||||
"target_branch_protection_sha256": "61b424b0c495eed71c73171c5c4434ac66d074de36886a73ec4c445fd1905db8",
|
||||
"task_id": "AIOS-SINGLE-OWNER-GOVERNANCE-ACTIVATION-20260729"
|
||||
},
|
||||
"execution_binding_sha256": "6b2ed671a94fa83704383728fd2bb2df3b67523b4273100fa371777d984be102",
|
||||
"expected_governance_base_oid": "8b7f9d9028f6ff9beb225bf85f53dbeaac6830e7",
|
||||
"expires_at": "2026-07-29T15:32:28Z",
|
||||
"human_authorizer": "owner",
|
||||
"implementation_commit_oid": "e1e438564ce8b987a36df0947e6eaa324dfede74",
|
||||
"implementation_tree_oid": "dd93dc1df10cb8b8fe59b09d3214a9ae6fee671e",
|
||||
"issued_at": "2026-07-29T03:32:28Z",
|
||||
"legacy_mechanical_approvals": [
|
||||
"codex",
|
||||
"gpt"
|
||||
],
|
||||
"machine_gate_contexts": [
|
||||
"aios/branch-protection",
|
||||
"aios/evidence-closure",
|
||||
"aios/secret-scan",
|
||||
"aios/state-machine",
|
||||
"aios/validator"
|
||||
],
|
||||
"pr30_merge_oid": "f6bef90633f428454e85721f353cd82cdf04caf0",
|
||||
"pr31_merge_oid": "8b7f9d9028f6ff9beb225bf85f53dbeaac6830e7",
|
||||
"rollback_artifact_sha256": "ed9307cc2f07767b75866fcb370fa3f98b76c5147e4b7fc49ecf04cf21cf7c31",
|
||||
"signed_merge_requirement_effective": false,
|
||||
"task_id": "AIOS-SINGLE-OWNER-GOVERNANCE-ACTIVATION-20260729",
|
||||
"test_evidence_sha256": "172a7cf812c6b901524138fb72851a9b7d2e895dedc98893407d0bab9b52c6c1"
|
||||
}
|
||||
+171
@@ -0,0 +1,171 @@
|
||||
{
|
||||
"approval_type": "SINGLE_OWNER_GOVERNANCE_LIVE_ACTIVATION_IMPLEMENTATION_AND_EXECUTION",
|
||||
"artifact_path": "/var/lib/aios-gitops/runtime/tasks/AIOS-SINGLE-OWNER-GOVERNANCE-ACTIVATION-20260729/artifacts/hardening-71def340e7cc34f4f19918dcf8d3c847abbe4fbf/live-activation-71def340e7cc34f4f19918dcf8d3c847abbe4fbf.tar.gz",
|
||||
"artifact_sha256": "32bc49c207ec454b6fd81b0484f5b257bb789481b37175995029a2ec78cc62fa",
|
||||
"authorization_scope": [
|
||||
"deploy_exact_approved_artifact",
|
||||
"publish_exact_head_machine_statuses",
|
||||
"execute_one_atomic_governance_activation",
|
||||
"verify_and_rollback_exact_transaction"
|
||||
],
|
||||
"denied_operations": [
|
||||
"main.write",
|
||||
"integration.write",
|
||||
"tag.write",
|
||||
"registry.write",
|
||||
"baseline.write",
|
||||
"production.deploy",
|
||||
"force_push",
|
||||
"history_rewrite"
|
||||
],
|
||||
"evidence_closure_blob_oid": "6af6d017f22c4e0c512b0cd62644cdc6ab2135d5",
|
||||
"evidence_closure_oid": "310ab2af902a9fde9108d227077f433cc743ce7c",
|
||||
"evidence_closure_path": "evidence/single-owner-governance/AIOS-SINGLE-OWNER-LIVE-ACTIVATION-HARDENING-20260729T043010Z.json",
|
||||
"evidence_closure_sha256": "e482fe52c8381f765ee508d2de1fe2e77dff52e6e342f01be4ca481ff2f76820",
|
||||
"execution_binding": {
|
||||
"artifact_path": "/var/lib/aios-gitops/runtime/tasks/AIOS-SINGLE-OWNER-GOVERNANCE-ACTIVATION-20260729/artifacts/hardening-71def340e7cc34f4f19918dcf8d3c847abbe4fbf/live-activation-71def340e7cc34f4f19918dcf8d3c847abbe4fbf.tar.gz",
|
||||
"artifact_sha256": "32bc49c207ec454b6fd81b0484f5b257bb789481b37175995029a2ec78cc62fa",
|
||||
"backend": "sqlite",
|
||||
"branch": "governance/approvals",
|
||||
"evidence_closure_oid": "310ab2af902a9fde9108d227077f433cc743ce7c",
|
||||
"evidence_closure_path": "evidence/single-owner-governance/AIOS-SINGLE-OWNER-LIVE-ACTIVATION-HARDENING-20260729T043010Z.json",
|
||||
"evidence_closure_sha256": "e482fe52c8381f765ee508d2de1fe2e77dff52e6e342f01be4ca481ff2f76820",
|
||||
"executor": "aios-governance-single-owner-activation",
|
||||
"expected_branch_protection_sha256": "f27992c8024426708d93bce072d231707bae47a927aa2393d566aae64d5ed079",
|
||||
"expected_gitea_schema_sha256": "3524f4912165939cb746c2c65d70583a909f5abbd00c4e07ae76f7206d49aa54",
|
||||
"expected_state_machine_sha256": "0b08a2c378d143cb9031a25132afc7b7d825bdc03d11efe10610341d4b70167e",
|
||||
"expected_validator_sha256": "754639846cb15cb997d612a1fe0ceb0224ddef5bbc3ccac9802a3747f122dac1",
|
||||
"forbidden_operations": {
|
||||
"baseline": false,
|
||||
"force_push": false,
|
||||
"governance_direct": false,
|
||||
"history_rewrite": false,
|
||||
"integration": false,
|
||||
"main": false,
|
||||
"production": false,
|
||||
"registry": false,
|
||||
"tags": false
|
||||
},
|
||||
"implementation_commit_oid": "71def340e7cc34f4f19918dcf8d3c847abbe4fbf",
|
||||
"implementation_tree_oid": "3cc61bfbf69cbe0a24d54a0e53a6c50bcc130066",
|
||||
"machine_status_producer_id": 13,
|
||||
"machine_status_producer_name": "gitops-bot",
|
||||
"operation_id": "single-owner-live-activation-hardening-20260729T043149Z",
|
||||
"repo_id": 1,
|
||||
"requester": "codex-agent",
|
||||
"rollback_artifact_path": "/var/lib/aios-gitops/runtime/tasks/AIOS-SINGLE-OWNER-GOVERNANCE-ACTIVATION-20260729/artifacts/hardening-71def340e7cc34f4f19918dcf8d3c847abbe4fbf/live-activation-rollback-71def340e7cc34f4f19918dcf8d3c847abbe4fbf.tar.gz",
|
||||
"rollback_artifact_sha256": "4efe1ea73d4fd72e5364fc91a16ad5e9d1e6ebe78d8aa466f7b11c4915b08403",
|
||||
"runtime_sources": [
|
||||
{
|
||||
"git_path": "services/aios-single-owner-governance/aios_single_owner_governance/activation.py",
|
||||
"runtime_path": "/opt/aios/single-owner-governance/releases/71def340e7cc34f4f19918dcf8d3c847abbe4fbf/services/aios-single-owner-governance/aios_single_owner_governance/activation.py",
|
||||
"sha256": "48846a58fa3afb2b7c8ffe6e88504a9333863932df867f33ba0321ad3bac6003"
|
||||
},
|
||||
{
|
||||
"git_path": "services/aios-single-owner-governance/aios_single_owner_governance/gitea_protected_branch.py",
|
||||
"runtime_path": "/opt/aios/single-owner-governance/releases/71def340e7cc34f4f19918dcf8d3c847abbe4fbf/services/aios-single-owner-governance/aios_single_owner_governance/gitea_protected_branch.py",
|
||||
"sha256": "00775efe3f3d631d88379eff4ed2dcfaf386a2fa43111abe961e75109e80d768"
|
||||
},
|
||||
{
|
||||
"git_path": "services/aios-single-owner-governance/aios_single_owner_governance/journal.py",
|
||||
"runtime_path": "/opt/aios/single-owner-governance/releases/71def340e7cc34f4f19918dcf8d3c847abbe4fbf/services/aios-single-owner-governance/aios_single_owner_governance/journal.py",
|
||||
"sha256": "a8f121245e04487d99c99fb03ada83f0c907723f8eb18552872ff46dcf5a7873"
|
||||
},
|
||||
{
|
||||
"git_path": "services/aios-single-owner-governance/aios_single_owner_governance/state_machine.py",
|
||||
"runtime_path": "/opt/aios/single-owner-governance/releases/71def340e7cc34f4f19918dcf8d3c847abbe4fbf/services/aios-single-owner-governance/aios_single_owner_governance/state_machine.py",
|
||||
"sha256": "0b08a2c378d143cb9031a25132afc7b7d825bdc03d11efe10610341d4b70167e"
|
||||
},
|
||||
{
|
||||
"git_path": "services/aios-single-owner-governance/aios_single_owner_governance/validator.py",
|
||||
"runtime_path": "/opt/aios/single-owner-governance/releases/71def340e7cc34f4f19918dcf8d3c847abbe4fbf/services/aios-single-owner-governance/aios_single_owner_governance/validator.py",
|
||||
"sha256": "754639846cb15cb997d612a1fe0ceb0224ddef5bbc3ccac9802a3747f122dac1"
|
||||
}
|
||||
],
|
||||
"target_branch_protection_sha256": "4713f0494b79103668fbe5bd8fd7da5d60a43e56fb545aa6e7c773f43dc07c10",
|
||||
"task_id": "AIOS-SINGLE-OWNER-GOVERNANCE-ACTIVATION-20260729",
|
||||
"test_evidence_path": "/var/lib/aios-gitops/runtime/tasks/AIOS-SINGLE-OWNER-GOVERNANCE-ACTIVATION-20260729/artifacts/hardening-71def340e7cc34f4f19918dcf8d3c847abbe4fbf/test-evidence-71def340e7cc34f4f19918dcf8d3c847abbe4fbf.json",
|
||||
"test_evidence_sha256": "c1633732767f3f051ffb01404d12d05a6168a4b01b91efdb34a64988d6dee684"
|
||||
},
|
||||
"execution_binding_sha256": "63705dd0f5cc25debde132179f96819a844a9703b33c092db4ac48d5680d536a",
|
||||
"expected_governance_base_oid": "1071979079deab8a4028473d967c2a205c3b111e",
|
||||
"expires_at": "2026-07-29T16:31:49Z",
|
||||
"human_authorizer": "owner",
|
||||
"implementation_commit_oid": "71def340e7cc34f4f19918dcf8d3c847abbe4fbf",
|
||||
"implementation_tree_oid": "3cc61bfbf69cbe0a24d54a0e53a6c50bcc130066",
|
||||
"issued_at": "2026-07-29T04:31:49Z",
|
||||
"legacy_mechanical_approvals": [
|
||||
"codex",
|
||||
"gpt"
|
||||
],
|
||||
"machine_gate_contexts": [
|
||||
"aios/branch-protection",
|
||||
"aios/evidence-closure",
|
||||
"aios/secret-scan",
|
||||
"aios/state-machine",
|
||||
"aios/validator"
|
||||
],
|
||||
"machine_status_producer_id": 13,
|
||||
"machine_status_producer_name": "gitops-bot",
|
||||
"rollback_artifact_path": "/var/lib/aios-gitops/runtime/tasks/AIOS-SINGLE-OWNER-GOVERNANCE-ACTIVATION-20260729/artifacts/hardening-71def340e7cc34f4f19918dcf8d3c847abbe4fbf/live-activation-rollback-71def340e7cc34f4f19918dcf8d3c847abbe4fbf.tar.gz",
|
||||
"rollback_artifact_sha256": "4efe1ea73d4fd72e5364fc91a16ad5e9d1e6ebe78d8aa466f7b11c4915b08403",
|
||||
"runtime_sources": [
|
||||
{
|
||||
"git_path": "services/aios-single-owner-governance/aios_single_owner_governance/activation.py",
|
||||
"runtime_path": "/opt/aios/single-owner-governance/releases/71def340e7cc34f4f19918dcf8d3c847abbe4fbf/services/aios-single-owner-governance/aios_single_owner_governance/activation.py",
|
||||
"sha256": "48846a58fa3afb2b7c8ffe6e88504a9333863932df867f33ba0321ad3bac6003"
|
||||
},
|
||||
{
|
||||
"git_path": "services/aios-single-owner-governance/aios_single_owner_governance/gitea_protected_branch.py",
|
||||
"runtime_path": "/opt/aios/single-owner-governance/releases/71def340e7cc34f4f19918dcf8d3c847abbe4fbf/services/aios-single-owner-governance/aios_single_owner_governance/gitea_protected_branch.py",
|
||||
"sha256": "00775efe3f3d631d88379eff4ed2dcfaf386a2fa43111abe961e75109e80d768"
|
||||
},
|
||||
{
|
||||
"git_path": "services/aios-single-owner-governance/aios_single_owner_governance/journal.py",
|
||||
"runtime_path": "/opt/aios/single-owner-governance/releases/71def340e7cc34f4f19918dcf8d3c847abbe4fbf/services/aios-single-owner-governance/aios_single_owner_governance/journal.py",
|
||||
"sha256": "a8f121245e04487d99c99fb03ada83f0c907723f8eb18552872ff46dcf5a7873"
|
||||
},
|
||||
{
|
||||
"git_path": "services/aios-single-owner-governance/aios_single_owner_governance/state_machine.py",
|
||||
"runtime_path": "/opt/aios/single-owner-governance/releases/71def340e7cc34f4f19918dcf8d3c847abbe4fbf/services/aios-single-owner-governance/aios_single_owner_governance/state_machine.py",
|
||||
"sha256": "0b08a2c378d143cb9031a25132afc7b7d825bdc03d11efe10610341d4b70167e"
|
||||
},
|
||||
{
|
||||
"git_path": "services/aios-single-owner-governance/aios_single_owner_governance/validator.py",
|
||||
"runtime_path": "/opt/aios/single-owner-governance/releases/71def340e7cc34f4f19918dcf8d3c847abbe4fbf/services/aios-single-owner-governance/aios_single_owner_governance/validator.py",
|
||||
"sha256": "754639846cb15cb997d612a1fe0ceb0224ddef5bbc3ccac9802a3747f122dac1"
|
||||
}
|
||||
],
|
||||
"target_branch_protection": {
|
||||
"approvals_whitelist_team_i_ds": "null",
|
||||
"approvals_whitelist_user_i_ds": "[2]",
|
||||
"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": 1,
|
||||
"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": 1,
|
||||
"status_check_contexts": "[\"aios/branch-protection\",\"aios/evidence-closure\",\"aios/secret-scan\",\"aios/state-machine\",\"aios/validator\"]",
|
||||
"unprotected_file_patterns": "",
|
||||
"updated_unix": 1785299509,
|
||||
"whitelist_deploy_keys": 0,
|
||||
"whitelist_team_i_ds": "null",
|
||||
"whitelist_user_i_ds": "null"
|
||||
},
|
||||
"task_id": "AIOS-SINGLE-OWNER-GOVERNANCE-ACTIVATION-20260729",
|
||||
"test_evidence_path": "/var/lib/aios-gitops/runtime/tasks/AIOS-SINGLE-OWNER-GOVERNANCE-ACTIVATION-20260729/artifacts/hardening-71def340e7cc34f4f19918dcf8d3c847abbe4fbf/test-evidence-71def340e7cc34f4f19918dcf8d3c847abbe4fbf.json",
|
||||
"test_evidence_sha256": "c1633732767f3f051ffb01404d12d05a6168a4b01b91efdb34a64988d6dee684"
|
||||
}
|
||||
+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"
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"schema_version": 1,
|
||||
"schema_version": 4,
|
||||
"automatic": [
|
||||
"build",
|
||||
"test",
|
||||
@@ -8,8 +8,7 @@
|
||||
"evidence"
|
||||
],
|
||||
"owner": [
|
||||
"production_runtime_switch",
|
||||
"production_tag"
|
||||
"signed_release_plan"
|
||||
],
|
||||
"forbidden": [
|
||||
"force_push",
|
||||
@@ -19,7 +18,8 @@
|
||||
"registry_mutation"
|
||||
],
|
||||
"runtime_deployment": {
|
||||
"approval_action": "production_runtime_switch",
|
||||
"approval_action": "signed_release_plan",
|
||||
"required_capability": "runtime_switch",
|
||||
"tag_operations_allowed": false,
|
||||
"baseline_mutation_allowed": false,
|
||||
"registry_mutation_allowed": false,
|
||||
@@ -27,10 +27,77 @@
|
||||
"rollback_required": true
|
||||
},
|
||||
"tag_release": {
|
||||
"approval_action": "production_tag",
|
||||
"approval_action": "signed_release_plan",
|
||||
"required_capability": "tag_publish",
|
||||
"separate_controller": true,
|
||||
"duplicate_conflict": "fail_closed",
|
||||
"rewrite_allowed": false,
|
||||
"force_push_allowed": false
|
||||
},
|
||||
"baseline_registry_finalize": {
|
||||
"approval_action": "signed_release_plan",
|
||||
"required_capability": "baseline_registry_finalize",
|
||||
"separate_controller": true,
|
||||
"remote_tag_verification_required": true,
|
||||
"gitea_api_verification_required": true,
|
||||
"registry_baseline_consistency_required": true
|
||||
},
|
||||
"approval": {
|
||||
"algorithm": "Ed25519",
|
||||
"trust_domain": "aios.production",
|
||||
"maximum_lifetime_seconds": 3600,
|
||||
"release_plan_sha256_required": true,
|
||||
"not_before_required": true,
|
||||
"replay_protection": true,
|
||||
"revocation_fail_closed": true,
|
||||
"key_rotation_supported": true,
|
||||
"capabilities": [
|
||||
"runtime_switch",
|
||||
"tag_publish",
|
||||
"baseline_registry_finalize"
|
||||
]
|
||||
},
|
||||
"execution": {
|
||||
"environment_variable": "AIOS_SILENT_RELEASE_EXECUTION_ENABLED",
|
||||
"default_enabled": false,
|
||||
"shadow_side_effects_allowed": false,
|
||||
"durable_saga_required": true,
|
||||
"pre_switch_revalidation_required": true,
|
||||
"trusted_candidate_builder_required": true,
|
||||
"actual_plan_regeneration_required": true,
|
||||
"startup_fail_closed_on_missing_component": true,
|
||||
"recovery_worker_shadow_default": true,
|
||||
"production_approval_backend_required": true,
|
||||
"production_builder_signer": "kms-hsm-secret-manager-only",
|
||||
"production_execution_without_backend": "unavailable"
|
||||
},
|
||||
"durability": {
|
||||
"journal": "sha256-hash-chain",
|
||||
"probe_journal": "sha256-hash-chain",
|
||||
"exclusive_capability_claims": true,
|
||||
"side_effect_probe_before_lease_takeover": true,
|
||||
"side_effect_probe_before_failed_recoverable_retry": true,
|
||||
"probe_results": [
|
||||
"NOT_APPLIED",
|
||||
"APPLIED_VERIFIED",
|
||||
"INDETERMINATE",
|
||||
"CONFLICT"
|
||||
],
|
||||
"probe_operation_digest_required": true,
|
||||
"probe_freshness_required": true,
|
||||
"tamper_result": "MANUAL_OWNER_REVIEW_REQUIRED"
|
||||
},
|
||||
"tag_pending": {
|
||||
"required_observations": [
|
||||
"remote_git_ref",
|
||||
"annotated_tag_object",
|
||||
"peeled_target",
|
||||
"gitea_api",
|
||||
"gitea_listing",
|
||||
"repository_identity"
|
||||
],
|
||||
"indeterminate_result": "RECONCILIATION_REQUIRED",
|
||||
"conflict_result": "MANUAL_OWNER_REVIEW_REQUIRED",
|
||||
"automatic_reconciliation": false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
[Unit]
|
||||
Description=AIOS Silent Release Shadow Controller
|
||||
After=network-online.target
|
||||
Wants=network-online.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=aiman
|
||||
Group=gitops
|
||||
UMask=0077
|
||||
Environment=AIOS_GITOPS_WRITES_ENABLED=false
|
||||
Environment=AIOS_SILENT_RELEASE_EXECUTION_ENABLED=false
|
||||
Environment=PYTHONDONTWRITEBYTECODE=1
|
||||
ExecStart=/usr/bin/python3 /usr/libexec/aios-silent-release-shadow/current/active-runtime/app/silent_release_execution.py --state-root /var/lib/aios-silent-release-shadow --repository /data/aios-gitops/repo.git --host 127.0.0.1 --port 18891 --poll-seconds 30
|
||||
Restart=on-failure
|
||||
RestartSec=5
|
||||
NoNewPrivileges=true
|
||||
PrivateTmp=true
|
||||
ProtectSystem=strict
|
||||
ProtectHome=true
|
||||
ReadWritePaths=/var/lib/aios-silent-release-shadow
|
||||
ReadOnlyPaths=/data/aios-gitops/repo.git /usr/libexec/aios-silent-release-shadow
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
@@ -1,91 +1,80 @@
|
||||
# AIOS Silent Release Controller Design Report
|
||||
|
||||
状态:isolated branch implementation
|
||||
Production mutation:forbidden
|
||||
目标流程:自动准备 → 自动验证 → 单次 Owner runtime 授权 → 自动部署/失败回滚
|
||||
状态:隔离分支实现;生产 mutation 禁止
|
||||
目标:自动准备 → 自动验证 → 单次 Owner 签名授权 → 分控制器自动执行与验收
|
||||
|
||||
## 1. 当前痛点
|
||||
|
||||
当前发布依赖多轮人工传递 commit、tree、archive、manifest、drop-in、测试和
|
||||
canary 证据。证据在不同消息和报告中重复抄写,容易出现:
|
||||
现有流程需要多轮人工传递 commit、tree、artifact、manifest、测试、canary、drop-in
|
||||
和回滚证据。证据在不同消息里重复抄写,容易出现:
|
||||
|
||||
- artifact 在测试后被 cache 文件污染,closure 数量漂移;
|
||||
- canary 通过,但批准文本与 runtime 实际字段契约不同;
|
||||
- Git、Gitea、artifact、systemd 和 rollback 门禁由人手工拼接;
|
||||
- runtime deployment 与 production tag 容易被误认为同一授权;
|
||||
- 授权重放、失败回滚和持久化状态缺少统一接口。
|
||||
- 构建后 artifact、manifest 或 policy 已变化,但旧批准仍被使用;
|
||||
- Git、Gitea、systemd 与回滚结果由人工拼接,缺少同一一致性快照;
|
||||
- runtime switch、tag、Baseline、Registry 的授权边界不清;
|
||||
- 重启后只剩工作树内临时 JSON,无法可靠恢复;
|
||||
- approval replay、并发 writer、key revoke 和 secret inheritance 缺少统一防线。
|
||||
|
||||
## 2. 新架构
|
||||
|
||||
```text
|
||||
ReleaseBuilder adapter
|
||||
Build / Test / Canary
|
||||
|
|
||||
v
|
||||
SilentReleaseController.prepare(source_commit)
|
||||
|
|
||||
+--> ReleaseCandidateStore
|
||||
ReleaseCandidate + DeploymentPlan
|
||||
|
|
||||
v
|
||||
ReleaseAcceptanceEngine.evaluate(candidate)
|
||||
|
|
||||
+--> ReleaseProbe adapter
|
||||
- local/bare/Gitea main
|
||||
- repository identity
|
||||
- artifact/manifest
|
||||
- tests/canary
|
||||
- rollback
|
||||
- authoritative Git binding
|
||||
ReleaseAcceptanceEngine
|
||||
|
|
||||
v
|
||||
READY_FOR_APPROVAL
|
||||
|
|
||||
v one ApprovalEnvelope: production_runtime_switch
|
||||
DeploymentController.deploy(candidate, approval)
|
||||
v
|
||||
SignedReleasePlanApproval (Ed25519)
|
||||
|
|
||||
+--> atomic switch
|
||||
+--> restart only GitOps runtime
|
||||
+--> health verification
|
||||
+--> rollback on any failure
|
||||
+--> runtime_switch capability
|
||||
| DeploymentController
|
||||
|
|
||||
+--> tag_publish capability
|
||||
| TagController
|
||||
|
|
||||
+--> baseline_registry_finalize capability
|
||||
BaselineRegistryFinalizeController
|
||||
|
||||
TagController (separate seam)
|
||||
|
|
||||
+--> separate production_tag approval
|
||||
+--> execute
|
||||
+--> verify
|
||||
Durable ReleaseSagaStore + CapabilityLedger + Event Journal
|
||||
贯穿三个控制器;控制器互相不拥有对方的 mutation API。
|
||||
```
|
||||
|
||||
`SilentReleaseController` 是外部深模块,只暴露 `prepare()` 和 `deploy()`。
|
||||
构建、探针、systemd 与 tag provider 都是 adapter;复杂实现保持在模块内部。
|
||||
`CommandReleaseBuilder` 使用无 shell 的 argv 调用确定性构建流水线,并只接受
|
||||
与请求 source commit 一致的候选 JSON。
|
||||
一次 Owner 批准是同一个不可变 release plan 的签名,不是一个万能执行入口。三个 capability
|
||||
分别消费、分别留痕、分别幂等恢复。`force_push`、`tag_rewrite`、`direct_main_push`
|
||||
永远不是可签发 capability。
|
||||
|
||||
## 3. 状态机
|
||||
|
||||
```text
|
||||
BUILDING
|
||||
|
|
||||
v
|
||||
VERIFYING ------------------------> FAILED
|
||||
|
|
||||
v
|
||||
READY_FOR_APPROVAL --------------> FAILED
|
||||
|
|
||||
v
|
||||
APPROVED ------------------------> FAILED
|
||||
|
|
||||
v
|
||||
DEPLOYING -------> VERIFIED
|
||||
| |
|
||||
+-------------> ROLLED_BACK
|
||||
|
|
||||
+-------------> FAILED (rollback itself failed)
|
||||
PREPARING
|
||||
-> VERIFYING
|
||||
-> READY_FOR_APPROVAL
|
||||
-> APPROVED
|
||||
-> PRE_SWITCH_REVALIDATING
|
||||
-> SWITCH_PENDING
|
||||
-> SWITCHED
|
||||
-> RUNTIME_VERIFYING
|
||||
-> TAG_PENDING
|
||||
-> TAG_VERIFIED
|
||||
-> BASELINE_REGISTRY_PENDING
|
||||
-> COMPLETED
|
||||
|
||||
任一可恢复故障 -> RECONCILIATION_REQUIRED
|
||||
对象冲突 -> MANUAL_OWNER_REVIEW_REQUIRED
|
||||
切换后失败 -> ROLLBACK_PENDING -> ROLLED_BACK
|
||||
```
|
||||
|
||||
状态不可跳跃。`VERIFIED` 只允许显式进入 `ROLLED_BACK`,终态不能自动回到准备态。
|
||||
状态快照使用 revision compare-and-set;事件同时写入 append-only journal。writer 崩溃后根据
|
||||
状态确定唯一恢复动作。并发控制器中只有一个 CAS winner。
|
||||
|
||||
## 4. 数据模型
|
||||
|
||||
`ReleaseCandidate`:
|
||||
### ReleaseCandidate
|
||||
|
||||
```text
|
||||
candidate_id
|
||||
@@ -99,201 +88,181 @@ rollback_ready
|
||||
git_consistency
|
||||
gitea_consistency
|
||||
approval_state
|
||||
state
|
||||
failure_reason
|
||||
created_at
|
||||
updated_at
|
||||
```
|
||||
|
||||
核心 OID 和 SHA 在构造时验证。候选 fingerprint 绑定 candidate、commit、tree、
|
||||
artifact 和 manifest,ApprovalEnvelope 必须逐字段匹配。
|
||||
|
||||
`AcceptanceEvidence` 是 adapter 与 acceptance engine 之间的 seam,包含三方 main、
|
||||
repository identity、artifact/manifest、测试、canary、rollback 和 authoritative Git。
|
||||
|
||||
## 5. Interface
|
||||
|
||||
Python interface:
|
||||
|
||||
```python
|
||||
candidate = controller.prepare(source_commit)
|
||||
|
||||
approval = ApprovalEnvelope.for_candidate(
|
||||
candidate,
|
||||
approval_id=...,
|
||||
owner_id=...,
|
||||
expires_at=...,
|
||||
)
|
||||
|
||||
result = controller.deploy(candidate.candidate_id, approval)
|
||||
```
|
||||
|
||||
建议 HTTP Action adapter:
|
||||
### DeploymentPlan
|
||||
|
||||
```text
|
||||
POST /api/silent-release/candidates
|
||||
GET /api/silent-release/candidates/{candidate_id}
|
||||
POST /api/silent-release/candidates/{candidate_id}/verify
|
||||
POST /api/silent-release/candidates/{candidate_id}/approve-runtime
|
||||
POST /api/silent-release/candidates/{candidate_id}/deploy
|
||||
GET /api/silent-release/candidates/{candidate_id}/acceptance
|
||||
service
|
||||
release_path / release_root
|
||||
dropin_path / dropin_sha256
|
||||
rollback_path / rollback_sha256
|
||||
source_commit
|
||||
artifact_sha256
|
||||
manifest_sha256
|
||||
policy_sha256
|
||||
health_contract
|
||||
sha256
|
||||
```
|
||||
|
||||
Tag interface 必须位于独立 namespace:
|
||||
### SignedReleasePlanApproval
|
||||
|
||||
```text
|
||||
POST /api/tag-releases/{version}/approve
|
||||
POST /api/tag-releases/{version}/execute
|
||||
GET /api/tag-releases/{version}/verify
|
||||
release_plan_id
|
||||
candidate_id
|
||||
source_commit
|
||||
tree_oid
|
||||
artifact_sha256
|
||||
manifest_sha256
|
||||
deployment_plan_sha256
|
||||
policy_sha256
|
||||
approval_plan_id
|
||||
evidence_closure_oid
|
||||
owner_approval_record_oid
|
||||
capabilities
|
||||
expires_at
|
||||
nonce
|
||||
key_id
|
||||
signature
|
||||
fingerprint
|
||||
```
|
||||
|
||||
runtime approval 不得被 TagController 接受;tag approval 不得被
|
||||
DeploymentController 接受。
|
||||
签名内容使用 canonical JSON,`signature` 不参与被签名字段。验证包括 Ed25519 签名、过期时间、
|
||||
公钥状态、approval revoke、candidate/plan/policy 逐字段绑定和 capability allowlist。
|
||||
|
||||
## 6. ReleaseAcceptanceEngine
|
||||
|
||||
`CommandReleaseProbe` 提供实际只读 adapter:
|
||||
|
||||
- local main:指定 local repository;
|
||||
- bare main:指定 authoritative bare Git;
|
||||
- Gitea main/identity:认证 API,只从 credential reference 读取 token;
|
||||
- artifact/manifest:流式 SHA-256 和 JSON 绑定;
|
||||
- tests/canary:无 shell 的 argv command;
|
||||
- runtime without `.git`;
|
||||
- rollback artifact;
|
||||
- authoritative Git 路径与可读性。
|
||||
|
||||
任何命令、Git、Gitea 或证据异常均转为 `FAILED`,不会降级为空成功。
|
||||
|
||||
## 7. ApprovalEnvelope
|
||||
|
||||
Owner 的 runtime envelope 只授权:
|
||||
## 5. API
|
||||
|
||||
```text
|
||||
action=production_runtime_switch
|
||||
POST /createReleaseCandidate
|
||||
GET /getReleaseCandidate?candidate_id=...
|
||||
GET /getApprovalEnvelope?candidate_id=...
|
||||
POST /submitOwnerApproval
|
||||
POST /startDeployment
|
||||
GET /getReleaseSaga?candidate_id=...
|
||||
```
|
||||
|
||||
Envelope 绑定:
|
||||
写接口要求 Bearer scope。`AIOS_SILENT_RELEASE_EXECUTION_ENABLED=false` 是默认值;此时可以
|
||||
持久化 candidate、生成 approval envelope、保存经验证的 Owner approval 和读取 Saga,
|
||||
但 `/startDeployment` 只返回 `SHADOW_MODE` 与空 `side_effects`。
|
||||
|
||||
- candidate_id/fingerprint;
|
||||
- source commit/tree;
|
||||
- artifact/manifest SHA;
|
||||
- owner、approval ID、签发和过期时间。
|
||||
## 6. PreSwitchValidator
|
||||
|
||||
`ApprovalLedger` 使用 `O_EXCL` 原子消费 approval ID,重放直接拒绝。
|
||||
切换前重新收集并比较:
|
||||
|
||||
生产 tag 使用完全独立的 `TagApprovalEnvelope`:
|
||||
- source commit 与 tree;
|
||||
- artifact、manifest、policy 和 deployment plan SHA-256;
|
||||
- rollback readiness;
|
||||
- local、authoritative bare 与 Gitea refs;
|
||||
- repository identity;
|
||||
- task writer/production writer 边界;
|
||||
- health contract。
|
||||
|
||||
任一变化都抛出:
|
||||
|
||||
```text
|
||||
action=production_tag
|
||||
version
|
||||
tag_oid
|
||||
target_oid
|
||||
APPROVAL_STALE_REAPPROVAL_REQUIRED
|
||||
```
|
||||
|
||||
## 8. DeploymentController
|
||||
旧批准不会被“最佳努力”继续使用。
|
||||
|
||||
只接受 `READY_FOR_APPROVAL` 候选和完整 runtime envelope:
|
||||
## 7. systemd 防线
|
||||
|
||||
1. 验证 Policy as Code;
|
||||
2. 原子消费 approval;
|
||||
3. 原子切换 versioned drop-in symlink;
|
||||
4. 仅 restart 指定 GitOps runtime;
|
||||
5. 验证 runtime commit 和 writer=false;
|
||||
6. 任一异常切回旧 symlink并再次 restart;
|
||||
7. rollback 后重新执行健康检查并确认 writer=false;
|
||||
8. rollback 或 rollback health 失败时进入 `FAILED`,禁止伪装成功。
|
||||
- service 与 drop-in 均为显式 allowlist;
|
||||
- release 与 drop-in resolved path 必须位于 release root 内;
|
||||
- 拒绝普通文件被替换为 symlink,拒绝 symlink escape;
|
||||
- deployment lock 覆盖 switch、restart 和首次 health check;
|
||||
- restart 前后记录 MainPID 与 ActiveEnterTimestampMonotonic;
|
||||
- artifact、drop-in、rollback 和 manifest 均按 plan hash 复验;
|
||||
- 子进程只接收固定 allowlist 环境,不继承 token、password、credential、askpass、
|
||||
auth socket、`LD_PRELOAD`、`PYTHONPATH` 或数据库连接串。
|
||||
|
||||
Deployment adapter 的 interface 中没有 tag、Baseline 或 Registry 方法,因此 runtime
|
||||
部署路径无法顺带调用这些能力。
|
||||
## 8. TagController 分离
|
||||
|
||||
## 9. TagController 分离
|
||||
TagController 必须独立完成 approval、execute、verify,并留下:
|
||||
|
||||
TagController 的三个动作严格分开:
|
||||
```text
|
||||
evidence_closure_oid
|
||||
approval_plan_id
|
||||
repository_identity
|
||||
remote_tag_oid
|
||||
remote_peeled_target_oid
|
||||
gitea_api_tag_oid
|
||||
gitea_listing_visible
|
||||
```
|
||||
|
||||
1. `approve(envelope)`
|
||||
2. `execute(approved)`
|
||||
3. `verify(approved)`
|
||||
同名 tag 的 tag OID 或 peeled target 不同,进入
|
||||
`MANUAL_OWNER_REVIEW_REQUIRED`;自动流程不 push、不删除、不重写、不 force push。
|
||||
|
||||
同名 tag 已存在且 tag OID 或 target OID 不一致时抛出
|
||||
`DuplicateTagConflict`。接口没有 delete、rewrite 或 force push。
|
||||
## 9. Baseline / Registry 门禁
|
||||
|
||||
只有以下全部一致时,`baseline_registry_finalize` capability 才能完成:
|
||||
|
||||
```text
|
||||
Registry = Baseline = remote tag = Gitea API = expected target
|
||||
```
|
||||
|
||||
UNKNOWN、API 不可达、repository identity 不符或 listing 不可见均 fail closed。
|
||||
|
||||
## 10. Policy as Code
|
||||
|
||||
`config/gitops/silent-release-policy.json`:
|
||||
|
||||
```text
|
||||
automatic: build, test, artifact, canary, evidence
|
||||
owner: production_runtime_switch, production_tag
|
||||
forbidden: force_push, tag_rewrite, direct_main_push,
|
||||
baseline_mutation, registry_mutation
|
||||
automatic:
|
||||
build, test, artifact, canary, evidence
|
||||
|
||||
owner:
|
||||
signed_release_plan
|
||||
|
||||
capabilities:
|
||||
runtime_switch
|
||||
tag_publish
|
||||
baseline_registry_finalize
|
||||
|
||||
forbidden:
|
||||
force_push
|
||||
tag_rewrite
|
||||
direct_main_push
|
||||
baseline_mutation
|
||||
registry_mutation
|
||||
```
|
||||
|
||||
Policy 缺少任一部署禁止项时,DeploymentController 拒绝运行。
|
||||
最后两个 forbidden 项指“脱离受治理 finalize controller 的直接 mutation”。
|
||||
|
||||
## 11. Security model
|
||||
|
||||
- fail-closed Git/Gitea/manifest/health adapter;
|
||||
- credential 只从受控文件读取,不进入模型或持久化候选;
|
||||
- candidate store 对 Authorization、token、credential、password、secret 字段脱敏;
|
||||
- 错误文本统一清除 Authorization、token、URL userinfo 和 password;
|
||||
- approval ID 一次性消费并有过期时间;
|
||||
- candidate fingerprint 防止对其他 artifact 重放批准;
|
||||
- runtime 默认 writer=false;
|
||||
- systemd adapter 只允许管理 symlink,拒绝覆盖普通配置文件;
|
||||
- no tag rewrite、force push、direct main push;
|
||||
- runtime deployment 无 Baseline/Registry/tag capability。
|
||||
- Ed25519 公钥 ring 支持 rotation 和 revoke,私钥不进入 runtime;
|
||||
- approval plan 可撤销,过期、未知 key、错误签名均 fail closed;
|
||||
- nonce 与 capability operation 持久化,重复相同请求幂等,不同 fingerprint 重放拒绝;
|
||||
- Saga snapshot 原子写、journal append-only、CAS 防并发 writer;
|
||||
- credential 只通过受控 reference 使用,不进入 Git、API 响应或异常;
|
||||
- 子进程环境从空集合构建,不复制 `os.environ`;
|
||||
- Shadow 模式默认开启,execution adapter 缺失时不能降级执行;
|
||||
- runtime、tag、Baseline/Registry 分属三个能力最小化控制器。
|
||||
|
||||
## 12. Migration plan
|
||||
|
||||
### Phase 1:shadow
|
||||
1. Shadow:生成 candidate、plan、acceptance、approval package,仅比较现有人工结果。
|
||||
2. Signed approval:接入 Owner 公钥 ring、撤销源和审计 record;仍不接生产 executor。
|
||||
3. Canary execution:使用非生产 service/release root 验证锁、PID、health、崩溃恢复和回滚。
|
||||
4. Controlled production switch:另行 Owner 授权启用 execution flag,只开放 runtime capability。
|
||||
5. Tag/finalize:分别完成 Gitea 与 Registry/Baseline 生产验收后再启用对应 capability。
|
||||
|
||||
- 只生成 ReleaseCandidate 和 acceptance evidence;
|
||||
- 与现有人工报告并行比较;
|
||||
- 不接入 production executor。
|
||||
|
||||
### Phase 2:approval envelope
|
||||
|
||||
- Owner UI/Action 只生成一次性 runtime envelope;
|
||||
- production 仍由现有受控 executor 执行;
|
||||
- 验证 approval replay 和过期处理。
|
||||
|
||||
### Phase 3:controlled deployment
|
||||
|
||||
- 接入 versioned drop-in symlink;
|
||||
- 先在 canary service 完成自动 switch/restart/rollback;
|
||||
- production writer 始终为 false。
|
||||
|
||||
### Phase 4:silent release
|
||||
|
||||
- 自动 build/test/artifact/canary/evidence;
|
||||
- READY_FOR_APPROVAL 时只通知一次 Owner;
|
||||
- 获得 runtime envelope 后自动部署和验收;
|
||||
- TagController 始终保持独立授权。
|
||||
每个阶段均可退回上一个阶段;不开启下一能力不会影响已经启用的控制器。
|
||||
|
||||
## 13. Rollback plan
|
||||
|
||||
- 切换前记录旧 symlink target;
|
||||
- 新 runtime 任一健康、Git、hash、writer 或一致性门禁失败即恢复旧 target;
|
||||
- 只 restart GitOps runtime;
|
||||
- rollback 后再次验证旧 runtime、writer=false 和只读 Git-backed API;
|
||||
- 保留失败候选、脱敏证据和 approval ledger;
|
||||
- 不使用 Git reset、force push、tag rewrite;
|
||||
- rollback 不修改 Baseline、Registry 或 production tag。
|
||||
- switch 前保存旧 symlink target、PID、启动时间、artifact 与 plan fingerprint;
|
||||
- restart 或 health 任一失败进入 `ROLLBACK_PENDING`;
|
||||
- 原子恢复旧 target,只 restart 指定 allowlist service;
|
||||
- 重新验证旧 runtime commit、writer=false 与 repository identity;
|
||||
- 成功进入 `ROLLED_BACK`,失败进入 `MANUAL_OWNER_REVIEW_REQUIRED`;
|
||||
- rollback 不修改 Git main、production tag、Baseline 或 Registry;
|
||||
- 保留脱敏 journal、approval fingerprint 和失败证据。
|
||||
|
||||
## 14. 自动测试矩阵
|
||||
## 14. 测试与外部依赖边界
|
||||
|
||||
实现测试覆盖:
|
||||
自动测试覆盖签名伪造、字段篡改、过期/revoke/key rotation、nonce replay、CAS race、
|
||||
崩溃恢复、artifact/policy/plan mutation、tag conflict、secret leakage、runtime without
|
||||
`.git`、rollback 和 shadow no-side-effects。
|
||||
|
||||
- artifact mismatch;
|
||||
- Gitea unavailable;
|
||||
- authoritative Git unavailable;
|
||||
- health failure rollback;
|
||||
- approval replay;
|
||||
- duplicate tag conflict;
|
||||
- secret leakage;
|
||||
- runtime without `.git`;
|
||||
- state transition;
|
||||
- policy separation;
|
||||
- candidate persistence;
|
||||
- automatic prepare 到单次 Owner gate。
|
||||
本隔离实现不创建生产 KMS/key、不安装 systemd unit、不部署 Action runtime、不创建 tag,
|
||||
也不修改 Baseline/Registry。上述动作必须在单独生产批准中执行。
|
||||
|
||||
@@ -0,0 +1,107 @@
|
||||
# AIOS Silent Release Execution Readiness
|
||||
|
||||
## Scope and stop point
|
||||
|
||||
This change makes the controller execution-ready while deploying only a
|
||||
localhost-bound Shadow control plane. Both production gates remain false:
|
||||
|
||||
```text
|
||||
AIOS_GITOPS_WRITES_ENABLED=false
|
||||
AIOS_SILENT_RELEASE_EXECUTION_ENABLED=false
|
||||
```
|
||||
|
||||
Shadow completion is evidence preparation, not a production release. It cannot
|
||||
write a production tag, switch a business runtime, or mutate Baseline/Registry.
|
||||
|
||||
## Recovery state machine
|
||||
|
||||
Every expired or `FAILED_RECOVERABLE` capability requires fresh, operation-bound
|
||||
probe evidence before another adapter may run:
|
||||
|
||||
```text
|
||||
NOT_APPLIED -> one new exclusive claim may retry
|
||||
APPLIED_VERIFIED -> mark COMPLETED without adapter replay
|
||||
INDETERMINATE -> RECONCILIATION_REQUIRED; no automatic writes
|
||||
CONFLICT -> MANUAL_OWNER_REVIEW_REQUIRED; no automatic writes
|
||||
```
|
||||
|
||||
Probe evidence binds the immutable operation digest, is freshness checked, and
|
||||
is appended to a separate SHA-256 hash-chained journal. Completion still
|
||||
requires the active claim ID. File locks and leases ensure that concurrent
|
||||
recovery workers cannot execute the same adapter.
|
||||
|
||||
`TAG_PENDING` uses independent remote ref, annotated object, peeled target,
|
||||
Gitea API, Gitea listing, and repository identity evidence. Exact absence may
|
||||
roll back a switched runtime. Exact publication continues to finalization.
|
||||
Unavailable/divergent evidence stops at reconciliation; a different OID or
|
||||
target stops for Owner review. No branch can force, delete, or rewrite a tag.
|
||||
|
||||
## Trusted build and approval
|
||||
|
||||
Callers submit only an event ID and exact source commit. Test, canary, artifact,
|
||||
manifest, rollback, secret scan, Git and Gitea evidence are generated by the
|
||||
trusted builder. The builder emits an Ed25519 attestation binding:
|
||||
|
||||
- builder attestation/key IDs;
|
||||
- source commit and tree OID;
|
||||
- artifact, manifest and package-closure SHA-256;
|
||||
- test and canary evidence SHA-256;
|
||||
- issuance time and signature.
|
||||
|
||||
The controller contains only trusted public keys. Production signing is exposed
|
||||
through a KMS/HSM/secret-manager adapter; a local private-key adapter is marked
|
||||
test-only and is used only for ephemeral Shadow acceptance.
|
||||
|
||||
Owner approvals are immutable, time-bounded, revocable, trust-domain bound, and
|
||||
single-use per capability ledger. If the production approval backend reference
|
||||
is absent, production execution is unavailable while Shadow remains available.
|
||||
Caller-supplied `owner_id` is never a fallback.
|
||||
|
||||
## Persistent controller and API
|
||||
|
||||
`aios-silent-release-controller.service` runs independently from the business
|
||||
GitOps API. It uses a single leader lock, durable intents/candidates/Sagas,
|
||||
startup journal verification, idempotent main events, one notification record,
|
||||
and recovery reporting. The final deployed service is report-only Shadow mode.
|
||||
|
||||
The Action contract exposes:
|
||||
|
||||
- `createReleaseIntent`
|
||||
- `getReleaseCandidate`
|
||||
- `getReleasePlan`
|
||||
- `getReleaseAcceptance`
|
||||
- `submitSignedApproval`
|
||||
- `getReleaseSaga`
|
||||
- `getCapabilityStatus`
|
||||
- `startDeployment`
|
||||
- `getRecoveryStatus`
|
||||
|
||||
Every Shadow response declares `execution_enabled=false`, `shadow_mode=true`
|
||||
and `side_effects_executed=false`. `startDeployment` returns a simulated
|
||||
`SHADOW_COMPLETED` result and never claims production capabilities.
|
||||
|
||||
## Migration and rollback
|
||||
|
||||
1. Merge through a governed pull request.
|
||||
2. Build from a clean detached worktree at the exact merge commit.
|
||||
3. Verify artifact, manifest, package closure, tests, live fixture, no-.git
|
||||
canary, rollback archive and secret scan.
|
||||
4. Install a versioned Shadow release without changing the business runtime.
|
||||
5. Start the separate localhost-only controller with both gates false.
|
||||
6. Run Shadow candidates and freeze production PID/tag/Baseline/Registry hashes.
|
||||
7. Remove ephemeral test keys and temporary repositories.
|
||||
|
||||
Rollback stops and disables only the Shadow controller and restores/removes its
|
||||
versioned `current` link. The existing production API, tag set, Baseline and
|
||||
Registry are not involved.
|
||||
|
||||
## Future activation envelope
|
||||
|
||||
Activation requires a new one-time Owner approval binding the exact controller
|
||||
commit, artifact/manifest/policy/unit hashes, trusted key IDs, KMS/secret
|
||||
references, release and rollback roots, authoritative Git/repository identity,
|
||||
Shadow candidate/evidence IDs, test results, activation plan and rollback plan.
|
||||
That approval may only flip
|
||||
`AIOS_SILENT_RELEASE_EXECUTION_ENABLED=false` to `true`; each real release still
|
||||
requires its own signed ReleasePlan. This change does not request or consume
|
||||
that activation approval.
|
||||
@@ -0,0 +1,84 @@
|
||||
# Silent Release Controller Production Contract
|
||||
|
||||
Status: review-only implementation. Production execution remains disabled.
|
||||
|
||||
## Trust boundary
|
||||
|
||||
The caller submits only release intent. A configured trusted builder owns ref
|
||||
validation, build, tests, artifact and manifest hashing, package closure,
|
||||
canary, rollback readiness, Git/Gitea consistency, and secret scanning. Caller
|
||||
supplied success evidence is rejected.
|
||||
|
||||
The resulting immutable `ReleasePlan` contains deployment, tag, and
|
||||
finalization plans. One Ed25519 owner signature binds its SHA-256 digest,
|
||||
candidate evidence, policy digest, owner identity, trust domain, validity
|
||||
window, evidence lineage, and three narrow capabilities.
|
||||
|
||||
## Durable execution
|
||||
|
||||
The Saga journal is the source of truth. Every record is sequence-numbered and
|
||||
SHA-256 hash-chained from a fixed genesis hash. Snapshots are rebuildable
|
||||
caches. Journal corruption, truncation, reordering, missing records, or hash
|
||||
mismatch moves the release to `MANUAL_OWNER_REVIEW_REQUIRED`.
|
||||
|
||||
Capability execution uses an exclusive, leased claim. A second worker receives
|
||||
`BUSY`; an expired claim is not reassigned until a capability-specific
|
||||
side-effect probe reports that the operation was not applied. Runtime, tag,
|
||||
and finalization remain separately claimed and recoverable.
|
||||
|
||||
## Execution order
|
||||
|
||||
```text
|
||||
APPROVED
|
||||
-> PRE_SWITCH_REVALIDATING
|
||||
-> SWITCH_PENDING
|
||||
-> SWITCHED
|
||||
-> RUNTIME_VERIFYING
|
||||
-> RUNTIME_VERIFIED
|
||||
-> TAG_PENDING
|
||||
-> TAG_VERIFIED
|
||||
-> BASELINE_REGISTRY_PENDING
|
||||
-> COMPLETED
|
||||
```
|
||||
|
||||
The actual adapter configuration is regenerated before execution and must
|
||||
match the signed plan. Trusted allowlists come from controlled policy, not the
|
||||
request.
|
||||
|
||||
A failure before verified tag publication rolls the runtime back. A failure
|
||||
after the exact tag is verified enters `RECONCILIATION_REQUIRED`; the tag is
|
||||
never deleted or rewritten. A same-name/different-OID conflict enters
|
||||
`MANUAL_OWNER_REVIEW_REQUIRED`. Registry/Baseline finalization requires exact
|
||||
remote and Gitea tag proof, and Baseline is recorded last.
|
||||
|
||||
## Action and recovery contract
|
||||
|
||||
`submitOwnerApproval` verifies and durably stores the signed plan, then moves
|
||||
the Saga to `APPROVED`. `startDeployment` loads that durable approval and
|
||||
ignores caller-supplied approval content. Enabling execution without the
|
||||
trusted builder, verifier, validator, policy, and orchestrator fails during
|
||||
service construction.
|
||||
|
||||
The recovery worker scans incomplete journals and resumes through the same
|
||||
claims and verification gates. It never automatically resumes a manual-review
|
||||
Saga. In shadow mode it only reports state and performs no adapter side
|
||||
effects.
|
||||
|
||||
The deployed Action factory does not enable the legacy caller-evidence path.
|
||||
If the trusted builder is absent, candidate creation is rejected even while
|
||||
execution remains in shadow mode. The compatibility path exists only through
|
||||
the explicit test-only `SilentReleaseActionService.shadow()` constructor.
|
||||
|
||||
Finalization is composed from separate `RegistryController` and
|
||||
`BaselineController` claims. Registry is verified first and Baseline is the
|
||||
last mutation; a retry reuses completed Registry evidence and never moves
|
||||
Baseline ahead of tag verification.
|
||||
|
||||
## Governance
|
||||
|
||||
- `force_push`: forbidden
|
||||
- `tag_rewrite`: forbidden
|
||||
- `direct_main_push`: forbidden
|
||||
- production execution default: disabled
|
||||
- production tag operations in this change: not executed
|
||||
- Baseline/Registry mutations in this change: not executed
|
||||
@@ -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:
|
||||
@@ -135,7 +160,216 @@ components:
|
||||
properties:
|
||||
text: {type: string}
|
||||
branch: {type: string}
|
||||
DeploymentPlan:
|
||||
type: object
|
||||
required: [service, release_path, release_root, cutover_link, dropin_path, dropin_sha256, systemd_unit_sha256, rollback_path, rollback_sha256, source_commit, tree_oid, artifact_sha256, manifest_sha256, package_closure_sha256, authoritative_git_dir, repository_identity, writer_expected, baseline_before_sha256, registry_before_sha256, production_tag_refset_before_sha256, health_contract_version, timeout_policy, retry_policy, policy_sha256, health_contract]
|
||||
properties:
|
||||
service: {type: string}
|
||||
release_path: {type: string}
|
||||
release_root: {type: string}
|
||||
cutover_link: {type: string}
|
||||
dropin_path: {type: string}
|
||||
dropin_sha256: {type: string, pattern: '^[0-9a-f]{64}$'}
|
||||
systemd_unit_sha256: {type: string, pattern: '^[0-9a-f]{64}$'}
|
||||
rollback_path: {type: string}
|
||||
rollback_sha256: {type: string, pattern: '^[0-9a-f]{64}$'}
|
||||
source_commit: {type: string, pattern: '^[0-9a-f]{40}$'}
|
||||
tree_oid: {type: string, pattern: '^[0-9a-f]{40}$'}
|
||||
artifact_sha256: {type: string, pattern: '^[0-9a-f]{64}$'}
|
||||
manifest_sha256: {type: string, pattern: '^[0-9a-f]{64}$'}
|
||||
package_closure_sha256: {type: string, pattern: '^[0-9a-f]{64}$'}
|
||||
authoritative_git_dir: {type: string}
|
||||
repository_identity: {type: string}
|
||||
writer_expected: {type: boolean, enum: [false]}
|
||||
baseline_before_sha256: {type: string, pattern: '^[0-9a-f]{64}$'}
|
||||
registry_before_sha256: {type: string, pattern: '^[0-9a-f]{64}$'}
|
||||
production_tag_refset_before_sha256: {type: string, pattern: '^[0-9a-f]{64}$'}
|
||||
health_contract_version: {type: string}
|
||||
timeout_policy: {type: object}
|
||||
retry_policy: {type: object}
|
||||
policy_sha256: {type: string, pattern: '^[0-9a-f]{64}$'}
|
||||
health_contract: {type: object, additionalProperties: true}
|
||||
ReleaseCandidateInput:
|
||||
type: object
|
||||
required: [source_commit, version]
|
||||
additionalProperties: false
|
||||
properties:
|
||||
candidate_id: {type: string, pattern: '^[A-Za-z0-9][A-Za-z0-9_.-]{2,127}$'}
|
||||
source_commit: {type: string, pattern: '^[0-9a-f]{40}$'}
|
||||
version: {type: string, pattern: '^V[0-9]+R[0-9]+C[0-9]+(?:[-.][A-Za-z0-9][A-Za-z0-9.-]*)?$'}
|
||||
ReleaseIntentInput:
|
||||
type: object
|
||||
required: [event_id, source_commit]
|
||||
additionalProperties: false
|
||||
properties:
|
||||
event_id: {type: string, pattern: '^[A-Za-z0-9][A-Za-z0-9_.:-]{2,255}$'}
|
||||
source_commit: {type: string, pattern: '^[0-9a-f]{40}$'}
|
||||
SignedReleasePlanApproval:
|
||||
type: object
|
||||
required: [release_plan_id, candidate_id, source_commit, tree_oid, artifact_sha256, manifest_sha256, deployment_plan_sha256, release_plan_sha256, policy_sha256, approval_plan_id, evidence_closure_oid, owner_approval_record_oid, capabilities, owner_identity, trust_domain, issued_at, not_before, expires_at, nonce, key_id, signature]
|
||||
properties:
|
||||
release_plan_id: {type: string}
|
||||
candidate_id: {type: string}
|
||||
source_commit: {type: string, pattern: '^[0-9a-f]{40}$'}
|
||||
tree_oid: {type: string, pattern: '^[0-9a-f]{40}$'}
|
||||
artifact_sha256: {type: string, pattern: '^[0-9a-f]{64}$'}
|
||||
manifest_sha256: {type: string, pattern: '^[0-9a-f]{64}$'}
|
||||
deployment_plan_sha256: {type: string, pattern: '^[0-9a-f]{64}$'}
|
||||
release_plan_sha256: {type: string, pattern: '^[0-9a-f]{64}$'}
|
||||
policy_sha256: {type: string, pattern: '^[0-9a-f]{64}$'}
|
||||
approval_plan_id: {type: string}
|
||||
evidence_closure_oid: {type: string, pattern: '^[0-9a-f]{40}$'}
|
||||
owner_approval_record_oid: {type: string, pattern: '^[0-9a-f]{40}$'}
|
||||
owner_identity: {type: string}
|
||||
trust_domain: {type: string}
|
||||
issued_at: {type: string, format: date-time}
|
||||
not_before: {type: string, format: date-time}
|
||||
capabilities:
|
||||
type: array
|
||||
uniqueItems: true
|
||||
items:
|
||||
type: string
|
||||
enum: [runtime_switch, tag_publish, baseline_registry_finalize]
|
||||
expires_at: {type: string, format: date-time}
|
||||
nonce: {type: string, minLength: 16}
|
||||
key_id: {type: string}
|
||||
signature: {type: string, format: byte, writeOnly: true}
|
||||
StartDeploymentRequest:
|
||||
type: object
|
||||
required: [candidate_id]
|
||||
additionalProperties: false
|
||||
properties:
|
||||
candidate_id: {type: string}
|
||||
paths:
|
||||
/createReleaseIntent:
|
||||
post:
|
||||
operationId: createReleaseIntent
|
||||
summary: Create an idempotent intent; all build evidence comes from the trusted builder
|
||||
security: [{bearerAuth: []}]
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema: {$ref: '#/components/schemas/ReleaseIntentInput'}
|
||||
responses:
|
||||
'200': {description: Intent created or idempotently returned}
|
||||
'400': {description: Caller evidence or invalid intent rejected}
|
||||
x-openai-isConsequential: true
|
||||
/createReleaseCandidate:
|
||||
post:
|
||||
operationId: createReleaseCandidate
|
||||
summary: Submit release intent to the trusted builder; caller evidence is rejected
|
||||
security: [{bearerAuth: []}]
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema: {$ref: '#/components/schemas/ReleaseCandidateInput'}
|
||||
responses:
|
||||
'200': {description: Candidate created or idempotently returned}
|
||||
'400': {description: Invalid or conflicting candidate evidence}
|
||||
'401': {description: Unauthorized}
|
||||
x-openai-isConsequential: true
|
||||
/getReleaseCandidate:
|
||||
get:
|
||||
operationId: getReleaseCandidate
|
||||
summary: Read a durable release candidate
|
||||
parameters:
|
||||
- {in: query, name: candidate_id, required: true, schema: {type: string}}
|
||||
responses:
|
||||
'200': {description: Candidate}
|
||||
'404': {description: Candidate not found}
|
||||
/getReleasePlan:
|
||||
get:
|
||||
operationId: getReleasePlan
|
||||
parameters:
|
||||
- {in: query, name: candidate_id, required: true, schema: {type: string}}
|
||||
responses:
|
||||
'200': {description: Immutable release plan}
|
||||
'404': {description: Candidate not found}
|
||||
/getReleaseAcceptance:
|
||||
get:
|
||||
operationId: getReleaseAcceptance
|
||||
parameters:
|
||||
- {in: query, name: candidate_id, required: true, schema: {type: string}}
|
||||
responses:
|
||||
'200': {description: Builder attestation and acceptance checks}
|
||||
'404': {description: Candidate not found}
|
||||
/getApprovalEnvelope:
|
||||
get:
|
||||
operationId: getApprovalEnvelope
|
||||
summary: Generate the immutable owner-signature payload
|
||||
parameters:
|
||||
- {in: query, name: candidate_id, required: true, schema: {type: string}}
|
||||
responses:
|
||||
'200': {description: Approval envelope fingerprint inputs}
|
||||
'404': {description: Candidate not found}
|
||||
/submitOwnerApproval:
|
||||
post:
|
||||
operationId: submitOwnerApproval
|
||||
summary: Verify and durably store a signed owner release plan
|
||||
security: [{bearerAuth: []}]
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema: {$ref: '#/components/schemas/SignedReleasePlanApproval'}
|
||||
responses:
|
||||
'200': {description: Signature verified and approval stored}
|
||||
'400': {description: Invalid approval}
|
||||
'403': {description: Signature, key, expiry, or revocation failure}
|
||||
'409': {description: Conflicting release plan}
|
||||
x-openai-isConsequential: true
|
||||
/submitSignedApproval:
|
||||
post:
|
||||
operationId: submitSignedApproval
|
||||
summary: Alias for immutable signed owner approval submission
|
||||
security: [{bearerAuth: []}]
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema: {$ref: '#/components/schemas/SignedReleasePlanApproval'}
|
||||
responses:
|
||||
'200': {description: Signature verified and approval stored}
|
||||
'403': {description: Production approval backend unavailable or signature invalid}
|
||||
x-openai-isConsequential: true
|
||||
/startDeployment:
|
||||
post:
|
||||
operationId: startDeployment
|
||||
summary: Resume the durable approved Saga; caller cannot replace approval
|
||||
security: [{bearerAuth: []}]
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema: {$ref: '#/components/schemas/StartDeploymentRequest'}
|
||||
responses:
|
||||
'200': {description: Shadow result or deployment Saga result}
|
||||
'403': {description: Execution disabled or approval invalid}
|
||||
'409': {description: Saga conflict}
|
||||
x-openai-isConsequential: true
|
||||
/getReleaseSaga:
|
||||
get:
|
||||
operationId: getReleaseSaga
|
||||
summary: Read durable Saga state and event journal
|
||||
parameters:
|
||||
- {in: query, name: candidate_id, required: true, schema: {type: string}}
|
||||
responses:
|
||||
'200': {description: Saga snapshot}
|
||||
'404': {description: Saga not found}
|
||||
/getCapabilityStatus:
|
||||
get:
|
||||
operationId: getCapabilityStatus
|
||||
parameters:
|
||||
- {in: query, name: candidate_id, required: true, schema: {type: string}}
|
||||
responses:
|
||||
'200': {description: Durable single-use capability records}
|
||||
/getRecoveryStatus:
|
||||
get:
|
||||
operationId: getRecoveryStatus
|
||||
responses:
|
||||
'200': {description: Fail-closed recovery classification}
|
||||
/api/base:
|
||||
get:
|
||||
operationId: getBaseline
|
||||
@@ -219,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,42 @@
|
||||
# AIOS Task Plane
|
||||
|
||||
该模块是与任务发起方、Worker 类型和通知后端解耦的持久化任务控制平面。当前交付仅用于隔离开发与 Shadow 验收;默认 `execution_enabled=false`,不会修改生产、tag、Baseline 或 Registry。
|
||||
|
||||
## 架构
|
||||
|
||||
- `TaskPlaneService`:统一服务边界和 HTTP 路由。
|
||||
- `TaskPlaneStore`:SQLite WAL、revision/CAS、串行事务、租约、hash-chained journal、evidence 和 durable outbox。
|
||||
- `WorkerRegistry` / `LeaseManager`:身份绑定、capability、并发额度、心跳、过期探测后恢复。
|
||||
- `DelegationEngine`:父子 DAG、全局深度、capability/risk/approval/budget 单调约束和幂等重放。
|
||||
- `PolicyEngine`:默认关闭执行;身份、证据、结果摘要和可插拔签名验签均 fail-closed。
|
||||
- `WorkerAdapter`:OpenClaw、Codex、Hermes、A2A 和 systemd Worker 的统一契约。默认实现只返回 `SHADOW_VERIFIED`,不执行外部命令。
|
||||
- `DurableOutboxDispatcher`:Webhook、NATS JetStream、GPT callback、Dashboard 和 WeChat 的回执式通知接口。没有真实回执时始终保留 `NOT_DELIVERED`。
|
||||
|
||||
## 状态与恢复
|
||||
|
||||
正常路径为 `CREATED → QUEUED/CLAIMED → RUNNING → VERIFYING/COMPLETED`;失败路径使用 `FAILED_RECOVERABLE`、`FAILED_FINAL`、`CANCELLED` 或 `MANUAL_REVIEW_REQUIRED`。每次状态事件都进入同一事务中的 append-only hash chain。
|
||||
|
||||
租约过期不会自动重放副作用。控制平面必须先调用 Worker probe,只有明确确认原执行已停止,才可将 claim 标记为 `EXPIRED` 并把任务转为 `FAILED_RECOVERABLE`。结果必须匹配 task、claim、Worker identity、未过期 lease 和 operation digest;重复结果返回冲突。
|
||||
|
||||
## 安全模型
|
||||
|
||||
- task/worker/claim/message ID 使用 allowlist pattern,拒绝 URL 编码差异、路径分隔符和控制字符。
|
||||
- HTTP body 最大 1 MB,非对象 JSON、异常 Content-Length 和非法参数返回 400/413。
|
||||
- 内部异常响应不回显路径、payload 或凭据;默认 HTTP access log 不记录原始 request target。
|
||||
- secret-like 字段在 durable task、event、evidence 和 outbox 写入前脱敏。
|
||||
- 结果签名存在但没有配置验签器时拒绝;摘要、evidence 或 event 被篡改时验收失败。
|
||||
- SQLite 文件创建为 0600;新建状态目录使用 0700。
|
||||
|
||||
## 验收
|
||||
|
||||
```bash
|
||||
PYTHONPATH=active-runtime \
|
||||
bash scripts/task-plane/run_task_plane_acceptance.sh
|
||||
|
||||
PYTHONPATH=active-runtime \
|
||||
python3 scripts/task-plane/create_shadow_tasks.py \
|
||||
--database reports/task-plane-shadow.sqlite3 \
|
||||
--report reports/task-plane-shadow.json
|
||||
```
|
||||
|
||||
上述脚本只使用本地 SQLite 和 Shadow adapter,不触发真实 OpenClaw、Codex、Hermes、微信或生产执行。
|
||||
@@ -0,0 +1,314 @@
|
||||
openapi: 3.0.3
|
||||
info:
|
||||
title: AIOS Task Plane API
|
||||
version: 1.0.0
|
||||
description: >
|
||||
Durable control-plane APIs for task orchestration with fail-closed policy.
|
||||
Every POST operation requires bearerAuth; read-only GET operations may be
|
||||
exposed according to the surrounding runtime policy.
|
||||
servers:
|
||||
- url: /
|
||||
paths:
|
||||
/tasks:
|
||||
get:
|
||||
summary: List tasks
|
||||
responses:
|
||||
"200":
|
||||
description: Task list
|
||||
post:
|
||||
security: [ { bearerAuth: [] } ]
|
||||
summary: Create task
|
||||
parameters:
|
||||
- in: header
|
||||
name: Idempotency-Key
|
||||
required: false
|
||||
schema:
|
||||
type: string
|
||||
minLength: 1
|
||||
maxLength: 128
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/TaskPlan"
|
||||
responses:
|
||||
"201": { description: Created }
|
||||
"400": { description: Bad request }
|
||||
"401": { description: Unauthorized }
|
||||
"403": { description: Forbidden }
|
||||
"409": { description: Conflict }
|
||||
"413": { description: Payload too large }
|
||||
"423": { description: Execution or resource locked }
|
||||
"503": { description: Authentication or service unavailable }
|
||||
/tasks/{task_id}:
|
||||
get:
|
||||
summary: Get task
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/TaskId"
|
||||
responses:
|
||||
"200": { description: OK }
|
||||
"404": { description: Not found }
|
||||
/tasks/{task_id}/claim:
|
||||
post:
|
||||
security: [ { bearerAuth: [] } ]
|
||||
summary: Claim a task
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/TaskId"
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/TaskClaimRequest"
|
||||
responses:
|
||||
"200": { description: Claimed }
|
||||
"401": { description: Unauthorized }
|
||||
"403": { description: Forbidden }
|
||||
"404": { description: Not found }
|
||||
"409": { description: Conflict }
|
||||
"423": { description: Execution disabled }
|
||||
/tasks/{task_id}/heartbeat:
|
||||
post:
|
||||
security: [ { bearerAuth: [] } ]
|
||||
summary: Renew task claim lease
|
||||
parameters: [ $ref: "#/components/parameters/TaskId" ]
|
||||
responses:
|
||||
"200": { description: Leased }
|
||||
"400": { description: Bad request }
|
||||
"401": { description: Unauthorized }
|
||||
"403": { description: Forbidden }
|
||||
"404": { description: Task or claim not found }
|
||||
"409": { description: Claim conflict }
|
||||
"413": { description: Payload too large }
|
||||
"423": { description: Lease or execution locked }
|
||||
"503": { description: Service unavailable }
|
||||
/tasks/{task_id}/progress:
|
||||
post:
|
||||
security: [ { bearerAuth: [] } ]
|
||||
summary: Report task progress
|
||||
parameters: [ $ref: "#/components/parameters/TaskId" ]
|
||||
responses:
|
||||
"200": { description: Progress saved }
|
||||
"400": { description: Bad request }
|
||||
"401": { description: Unauthorized }
|
||||
"403": { description: Forbidden }
|
||||
"404": { description: Task or claim not found }
|
||||
"409": { description: Claim conflict }
|
||||
"413": { description: Payload too large }
|
||||
"423": { description: Lease or execution locked }
|
||||
"503": { description: Service unavailable }
|
||||
/tasks/{task_id}/result:
|
||||
post:
|
||||
security: [ { bearerAuth: [] } ]
|
||||
summary: Submit task result
|
||||
parameters: [ $ref: "#/components/parameters/TaskId" ]
|
||||
responses:
|
||||
"200": { description: Result accepted }
|
||||
"400": { description: Bad request }
|
||||
"401": { description: Unauthorized }
|
||||
"403": { description: Result policy rejected }
|
||||
"404": { description: Task or claim not found }
|
||||
"409": { description: Duplicate or mismatched result }
|
||||
"413": { description: Payload too large }
|
||||
"423": { description: Lease or execution locked }
|
||||
"503": { description: Verification service unavailable }
|
||||
/tasks/{task_id}/fail:
|
||||
post:
|
||||
security: [ { bearerAuth: [] } ]
|
||||
summary: Mark task failed
|
||||
parameters: [ $ref: "#/components/parameters/TaskId" ]
|
||||
responses:
|
||||
"200": { description: Failure recorded }
|
||||
"400": { description: Bad request }
|
||||
"401": { description: Unauthorized }
|
||||
"403": { description: Forbidden }
|
||||
"404": { description: Task or claim not found }
|
||||
"409": { description: Claim conflict }
|
||||
"413": { description: Payload too large }
|
||||
"423": { description: Lease or execution locked }
|
||||
"503": { description: Service unavailable }
|
||||
/tasks/{task_id}/cancel:
|
||||
post:
|
||||
security: [ { bearerAuth: [] } ]
|
||||
summary: Cancel task
|
||||
parameters: [ $ref: "#/components/parameters/TaskId" ]
|
||||
responses:
|
||||
"200": { description: Cancelled }
|
||||
"400": { description: Bad request }
|
||||
"401": { description: Unauthorized }
|
||||
"403": { description: Forbidden }
|
||||
"404": { description: Task not found }
|
||||
"409": { description: Revision or state conflict }
|
||||
"413": { description: Payload too large }
|
||||
"423": { description: Resource locked }
|
||||
"503": { description: Service unavailable }
|
||||
/tasks/{task_id}/delegate:
|
||||
post:
|
||||
security: [ { bearerAuth: [] } ]
|
||||
summary: Delegate task
|
||||
parameters: [ $ref: "#/components/parameters/TaskId" ]
|
||||
responses:
|
||||
"200": { description: Delegated }
|
||||
"400": { description: Bad request }
|
||||
"401": { description: Unauthorized }
|
||||
"403": { description: Forbidden }
|
||||
"404": { description: Parent task not found }
|
||||
"409": { description: Conflict }
|
||||
"413": { description: Payload too large }
|
||||
"423": { description: Execution locked }
|
||||
"503": { description: Service unavailable }
|
||||
/workers/register:
|
||||
post:
|
||||
security: [ { bearerAuth: [] } ]
|
||||
summary: Register worker
|
||||
responses:
|
||||
"201": { description: Worker registered }
|
||||
"400": { description: Bad request }
|
||||
"401": { description: Unauthorized }
|
||||
"403": { description: Worker identity mismatch }
|
||||
"409": { description: Registration conflict }
|
||||
"413": { description: Payload too large }
|
||||
"423": { description: Resource locked }
|
||||
"503": { description: Service unavailable }
|
||||
/workers/{worker_id}/heartbeat:
|
||||
post:
|
||||
security: [ { bearerAuth: [] } ]
|
||||
summary: Worker heartbeat
|
||||
parameters: [ $ref: "#/components/parameters/WorkerId" ]
|
||||
responses:
|
||||
"200": { description: Heartbeat accepted }
|
||||
"400": { description: Bad request }
|
||||
"401": { description: Unauthorized }
|
||||
"403": { description: Forbidden }
|
||||
"404": { description: Worker not found }
|
||||
"409": { description: Worker state conflict }
|
||||
"413": { description: Payload too large }
|
||||
"423": { description: Worker locked }
|
||||
"503": { description: Service unavailable }
|
||||
/workers:
|
||||
get:
|
||||
summary: List workers
|
||||
responses:
|
||||
"200": { description: Worker list }
|
||||
/workers/{worker_id}:
|
||||
get:
|
||||
summary: Get worker
|
||||
parameters: [ $ref: "#/components/parameters/WorkerId" ]
|
||||
responses:
|
||||
"200": { description: Worker details }
|
||||
/tasks/{task_id}/events:
|
||||
get:
|
||||
summary: Task events
|
||||
parameters: [ $ref: "#/components/parameters/TaskId" ]
|
||||
responses:
|
||||
"200": { description: Event list }
|
||||
/tasks/{task_id}/evidence:
|
||||
get:
|
||||
summary: Task evidence
|
||||
parameters: [ $ref: "#/components/parameters/TaskId" ]
|
||||
responses:
|
||||
"200": { description: Evidence list }
|
||||
/tasks/{task_id}/children:
|
||||
get:
|
||||
summary: Task child tasks
|
||||
parameters: [ $ref: "#/components/parameters/TaskId" ]
|
||||
responses:
|
||||
"200": { description: Child list }
|
||||
components:
|
||||
securitySchemes:
|
||||
bearerAuth:
|
||||
type: http
|
||||
scheme: bearer
|
||||
bearerFormat: opaque
|
||||
parameters:
|
||||
TaskId:
|
||||
name: task_id
|
||||
in: path
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
minLength: 1
|
||||
maxLength: 128
|
||||
pattern: '^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$'
|
||||
WorkerId:
|
||||
name: worker_id
|
||||
in: path
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
minLength: 1
|
||||
maxLength: 128
|
||||
pattern: '^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$'
|
||||
schemas:
|
||||
TaskPlan:
|
||||
type: object
|
||||
additionalProperties: false
|
||||
properties:
|
||||
schema_version: { type: integer, enum: [1] }
|
||||
task_id:
|
||||
type: string
|
||||
minLength: 1
|
||||
maxLength: 128
|
||||
pattern: '^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$'
|
||||
task_type:
|
||||
type: string
|
||||
minLength: 1
|
||||
maxLength: 128
|
||||
pattern: '^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$'
|
||||
issuer: { type: object }
|
||||
intent: { type: string, maxLength: 16384 }
|
||||
required_capabilities:
|
||||
type: array
|
||||
maxItems: 128
|
||||
items: { type: string }
|
||||
preferred_workers:
|
||||
type: array
|
||||
maxItems: 64
|
||||
items: { type: string }
|
||||
risk: { type: string, enum: [low, medium, high] }
|
||||
approval_policy:
|
||||
type: string
|
||||
maxLength: 128
|
||||
dependencies:
|
||||
type: array
|
||||
maxItems: 128
|
||||
items: { type: string }
|
||||
max_delegation_depth: { type: integer, minimum: 0, maximum: 32 }
|
||||
budgets:
|
||||
type: object
|
||||
additionalProperties: false
|
||||
properties:
|
||||
time_seconds: { type: integer, minimum: 0 }
|
||||
attempts: { type: integer, minimum: 0 }
|
||||
tokens: { type: integer, minimum: 0 }
|
||||
cost: { type: number, minimum: 0 }
|
||||
evidence_requirements:
|
||||
type: array
|
||||
maxItems: 128
|
||||
items: { type: string }
|
||||
parent_task_id:
|
||||
type: string
|
||||
maxLength: 128
|
||||
correlation_id:
|
||||
type: string
|
||||
maxLength: 128
|
||||
metadata:
|
||||
type: object
|
||||
TaskClaimRequest:
|
||||
type: object
|
||||
additionalProperties: false
|
||||
required: [worker_id, actor_identity]
|
||||
properties:
|
||||
worker_id:
|
||||
type: string
|
||||
maxLength: 128
|
||||
pattern: '^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$'
|
||||
lease_seconds:
|
||||
type: integer
|
||||
minimum: 1
|
||||
maximum: 3600
|
||||
actor_identity:
|
||||
type: string
|
||||
minLength: 1
|
||||
maxLength: 512
|
||||
+75
@@ -0,0 +1,75 @@
|
||||
{
|
||||
"evidence_type": "TWO_STAGE_OWNER_CAMPAIGN_V2_PHASE_A_CANDIDATE",
|
||||
"generated_at": "2026-07-30T02:35:51Z",
|
||||
"execution_status": "PARTIALLY_COMPLETED",
|
||||
"blocking_layer": "PHASE_A_OWNER_EXACT_HEAD_APPROVAL",
|
||||
"repository": "aios/aios",
|
||||
"governance_base_oid": "ebd74398743e36523b593fcc395b73945d4924c5",
|
||||
"governance_tree_oid": "ad1255a1127204fad1d1208d8f31dc94fa64e6e1",
|
||||
"authority_main_oid": "875d41c3101093149414292067fd700dc214d4d3",
|
||||
"schema_seed_commit_oid": "8da47545a507cb798d3d6ab30ff8552a2d0a5ecf",
|
||||
"schema_seed_tree_oid": "991e68577a469657c1c4996814b6634ce5ed752f",
|
||||
"candidate_source_commit_oid": "a637a9dbb6e0021cb7dcf0d6b0ca00dfe10dce38",
|
||||
"candidate_source_tree_oid": "bf6e0c16fcad7515465bec4aefdd2e77b1e9bef0",
|
||||
"candidate_diff_sha256": "c06d002db06da6cbf16f9c8321f1acbfd431f009b5127a13fd2cb5d63b278867",
|
||||
"candidate_files_sha256": "5061402d516c0e9413208ce64e2b52f77d224e6eec1f7b07c5b3a10b58df5eaf",
|
||||
"candidate_file_count": 44,
|
||||
"schema_document_path": "services/aios-single-owner-governance/task-scoped-governance-bootstrap-v1.schema.json",
|
||||
"schema_document_sha256": "6ec912a96c8bac5c0d3c0a017abc078cb9567ed54dcb20bb0eadb5926ac54fdd",
|
||||
"validator_policy_path": "services/aios-single-owner-governance/aios_single_owner_governance/task_scoped_bootstrap.py",
|
||||
"validator_policy_sha256": "de11123b5cd9ccdad4961f1d5d084a8c4a1b2270ce793652ab9c50bb95694eac",
|
||||
"two_stage_policy_schema_sha256": "6b4def7c1eef19c01ef73c43252eb0cd1a87c103e1047249a3cfcc75c7922bf6",
|
||||
"two_stage_release_schema_sha256": "82ee589dc87db6998ddf0779ce264194cadea0deca66088b0797378f6ce89ad3",
|
||||
"runtime_observer_sha256": "181e632192f1417a98c1efb35631cf80246759080b2e04243576abcdc7da22af",
|
||||
"journal_anchor_sha256": "18feb0792b95f1060aa696cc60943379695a246984aecf76098838f9ed37426e",
|
||||
"package_zip_sha256": "668239a549b4a2b2094fe9b5104b84703fe8ed75011fd8ed71ed3958a6dc0706",
|
||||
"candidate_bundle_sha256": "15711b225b9ef41cca0588d21b2b95d7a95ac155efc27fcb8772c435fcdab147",
|
||||
"anchor_install_wheel_sha256": "abbf616366eb3467d4e651bb5306e22ae5e931b613df5362a0892935c4a1c0c7",
|
||||
"tests": {
|
||||
"two_stage_campaign": {"passed": 67, "failed": 0, "skipped": 0, "collection_errors": 0},
|
||||
"single_owner_governance": {"passed": 105, "failed": 0, "subtests_passed": 17, "collection_errors": 0},
|
||||
"task_scoped_bootstrap": {"passed": 15, "failed": 0, "collection_errors": 0},
|
||||
"compileall": "PASS",
|
||||
"quarantine_fsck": "PASS",
|
||||
"systemd_verify": "PASS_WITH_UNRELATED_HOST_WARNINGS"
|
||||
},
|
||||
"anchor": {
|
||||
"service_status": "active",
|
||||
"anchor_uid": 988,
|
||||
"runner_uid": 986,
|
||||
"uids_distinct": true,
|
||||
"runner_database_write": "DENIED",
|
||||
"unauthorized_socket_peer": "DENIED",
|
||||
"runner_registration_action": "ANCHOR_REQUEST_INVALID",
|
||||
"journal_mode": "WAL",
|
||||
"synchronous": "FULL",
|
||||
"foreign_keys": "ON",
|
||||
"restart_open_reservation": "PASS",
|
||||
"selftest_open_reservation_cleanup": "ABORTED"
|
||||
},
|
||||
"phase_separation": {
|
||||
"required_owner_decisions": 2,
|
||||
"phase_a_authorization": "GITEA_EXACT_HEAD_REVIEW",
|
||||
"phase_b_authorization": "DETACHED_SSH_SIGNATURE",
|
||||
"phase_a_release_authority": false,
|
||||
"one_shot_authority": false
|
||||
},
|
||||
"isolation": {
|
||||
"AIOS_GITOPS_WRITES_ENABLED": false,
|
||||
"publisher_created": false,
|
||||
"writer_created": false,
|
||||
"credential_projection_created": false,
|
||||
"target_branch_created": false,
|
||||
"tag_created": false,
|
||||
"registry_modified": false,
|
||||
"baseline_modified": false,
|
||||
"production_modified": false
|
||||
},
|
||||
"governance": {
|
||||
"force_push_used": false,
|
||||
"tag_rewritten": false,
|
||||
"direct_main_push": false,
|
||||
"secrets_committed": false,
|
||||
"owner_approval_respected": true
|
||||
}
|
||||
}
|
||||
+44
@@ -0,0 +1,44 @@
|
||||
1f4a83cf91e9aacb6f50bc4fb24d4f745a7a31fd1dd7f2d3d3ed0de05e38234f deploy/systemd/aios-campaign-anchor.service
|
||||
6b2525d31b8544488bce1544deb39a087cc2d830fb1b16fda26cde6ce430dbd2 deploy/systemd/aios-policy-activation-verification@.service
|
||||
fad52587775ff5f1c5cc16862c5fb1d910bb097c6b3330c00d75db4f7bef0bbd deploy/systemd/aios-policy-campaign-register@.service
|
||||
de11123b5cd9ccdad4961f1d5d084a8c4a1b2270ce793652ab9c50bb95694eac services/aios-single-owner-governance/aios_single_owner_governance/task_scoped_bootstrap.py
|
||||
6ec912a96c8bac5c0d3c0a017abc078cb9567ed54dcb20bb0eadb5926ac54fdd services/aios-single-owner-governance/task-scoped-governance-bootstrap-v1.schema.json
|
||||
5b6d65ae34b2c3bebc1bde8f38c6b0bb73269336d7d4ee5e3416f79c323134d6 services/aios-two-stage-owner-campaign/ANCHOR_REGISTRATION_CONTRACT.md
|
||||
8ba5278f96447bf4308813b49fcc433d0cc7a5019cd9e78aeb0d8ada5f00d53b services/aios-two-stage-owner-campaign/README.md
|
||||
7285527358bc8da1d4bc7ff70162dfc3a734086851b734daac89d6f9b330798d services/aios-two-stage-owner-campaign/SECURITY_DELTA_V2.md
|
||||
89f03fdf156d35807bf39f2f1dbf304c01e7e9ad64399276fb72c68d795eda65 services/aios-two-stage-owner-campaign/STEP_EXECUTOR_CONTRACT.md
|
||||
7453177223b62509e4854051a75d53f24d8d1f737986f309cc6ae7a2e721459f services/aios-two-stage-owner-campaign/TWO_STAGE_OWNER_INTERACTION_SPEC.md
|
||||
9308ee245a28a0f0ff623fbfc8e3961204b8e67194bffe536a9c3485ca126ac7 services/aios-two-stage-owner-campaign/examples/allowed_signers.example
|
||||
9ac7b7fdeb73a99d4f41bf3410ef16f67091d85f9082075287d388e6e5e37eed services/aios-two-stage-owner-campaign/examples/campaign-anchor.env.example
|
||||
926f0c0cfb7bd6db0ebab6bcb5a0530057b8d38ae1e0b436a69a12b44039462b services/aios-two-stage-owner-campaign/examples/executor-release.example.json
|
||||
8d46ec9279eb58c03337d41b63de2abdd9eb0d03f98f0806768dcd309d481ee9 services/aios-two-stage-owner-campaign/examples/policy-activation-verification.example.json
|
||||
b34d8895fe872ef27c62c759449757def698eb05157daff8badd0c86f64ee18c services/aios-two-stage-owner-campaign/examples/release-campaign-manifest.example.json
|
||||
a2992f493cf0547e1f4acd3fcd9b20e03c912d8b255e196f640d47e291ec9d8e services/aios-two-stage-owner-campaign/examples/runtime-observer-policy.example.json
|
||||
150c9a8debdcaa4dbb9fd51817ca8710ef805a32f2f96dc5490d0c4f77f2cb6f services/aios-two-stage-owner-campaign/examples/runtime-observer-release.example.json
|
||||
675b39d13223ad281a4d14a04e739639e470e4e2a2591b85264ba37f840f2b49 services/aios-two-stage-owner-campaign/payload/scripts/generate_diff_chunks.py
|
||||
5751764275e626c787dad17c2561edd86b2710ae0b3cfeafeec44d98126d9015 services/aios-two-stage-owner-campaign/payload/services/aios-autonomous-control-plane/aios_autonomous_control_plane/lineage_audit.py
|
||||
2b501bf32a22e1b10c7254d2268d11aa2424aac5756c52441557c25dbaaaa8d5 services/aios-two-stage-owner-campaign/payload/tests/autonomous_control_plane/test_lineage_audit_v3.py
|
||||
3f118bc7a4f2bdfdd47e2c86112e87f95162dba4d88c1579337bd1d442ec9832 services/aios-two-stage-owner-campaign/pyproject.toml
|
||||
6b4def7c1eef19c01ef73c43252eb0cd1a87c103e1047249a3cfcc75c7922bf6 services/aios-two-stage-owner-campaign/schemas/governance-policy-activation-verification-v1.schema.json
|
||||
82ee589dc87db6998ddf0779ce264194cadea0deca66088b0797378f6ce89ad3 services/aios-two-stage-owner-campaign/schemas/task-scoped-release-campaign-v1.schema.json
|
||||
d04543b5407dcefc0a41af1b8a65ebc2e62f779582ae959449f1ac37b305eecf services/aios-two-stage-owner-campaign/src/aios_two_stage_campaign/__init__.py
|
||||
935a1c1166b0c1ea35a82256345000bf2c73ded718d77773bc27a71ecce28f7d services/aios-two-stage-owner-campaign/src/aios_two_stage_campaign/__main__.py
|
||||
4e317e2fbf912662bfbf57d2f7fe1a9f7fd3d8c00f7ad09602d9e885ee4ef3cc services/aios-two-stage-owner-campaign/src/aios_two_stage_campaign/canonical.py
|
||||
863be28723553e5bf5df90eda49e96edf8417e765da7b41579f050b04336d480 services/aios-two-stage-owner-campaign/src/aios_two_stage_campaign/cli.py
|
||||
dae1e0ed0f841d928a1dd5094c64b1f39b3213301c1da747f45d032a18833c1e services/aios-two-stage-owner-campaign/src/aios_two_stage_campaign/errors.py
|
||||
baf8931566d2c2fb2985d1814b5ded89719bb4b0eba01e94cca9f3690baadeb2 services/aios-two-stage-owner-campaign/src/aios_two_stage_campaign/executor.py
|
||||
18feb0792b95f1060aa696cc60943379695a246984aecf76098838f9ed37426e services/aios-two-stage-owner-campaign/src/aios_two_stage_campaign/journal_anchor.py
|
||||
e24765d3c62f9934a921f099e43a002d349a6c9934438da82040e40238070c58 services/aios-two-stage-owner-campaign/src/aios_two_stage_campaign/orchestrator.py
|
||||
181e632192f1417a98c1efb35631cf80246759080b2e04243576abcdc7da22af services/aios-two-stage-owner-campaign/src/aios_two_stage_campaign/runtime_observer.py
|
||||
3f3ce81490b6f0893665ba296fc23be904ae0e5dcbcd3b602e25feb2571262b0 services/aios-two-stage-owner-campaign/src/aios_two_stage_campaign/signature.py
|
||||
c71d85d5df2ed0e5367de9aacd230f586e77f3b4c11cf52960c84bbdeec43eb7 services/aios-two-stage-owner-campaign/src/aios_two_stage_campaign/state_machine.py
|
||||
75ea8ed1c9b49b1ca3ce452fe229524ebd0ccd802945d3362dd668602ce1f98c services/aios-two-stage-owner-campaign/src/aios_two_stage_campaign/validator.py
|
||||
b1be7e1ed0fa0e16e041f77200d651cf46f6bb029c89d31f0b286d32c026cb66 services/aios-two-stage-owner-campaign/tests/test_canonical_snapshot.py
|
||||
0bb956135c90cf71fcb655c4d700c1f71c3442b9e9fddc376959890cdad823de services/aios-two-stage-owner-campaign/tests/test_executor_v2.py
|
||||
9bb8a9f1c3bd9aaf44db3dd2f5b810eb0cb650da0e5a5f5e2ea50be46f12664d services/aios-two-stage-owner-campaign/tests/test_lineage_payload.py
|
||||
681c14ddc0be90f94fd3c3d8a6768fe602ec8acd130ed00a8668505d2db57aa2 services/aios-two-stage-owner-campaign/tests/test_orchestrator_v2.py
|
||||
9edf44fe0129098c31d66f6b6f7ab252879b425e2844969d09e22c3c88e6b77d services/aios-two-stage-owner-campaign/tests/test_runtime_observer.py
|
||||
204e77c760380f8e29d10fc0afa4227497109d27493245c55a851de6ff69ac86 services/aios-two-stage-owner-campaign/tests/test_signature.py
|
||||
9bc5dfa237e6bf37ccc90bbc618a807892d323cdd41345717b296838b44cd22a services/aios-two-stage-owner-campaign/tests/test_state_machine.py
|
||||
a9ef9c11120f480e0cde3ccbf8a816705621506657d8ac10faf7e38fb54bf367 services/aios-two-stage-owner-campaign/tests/test_validator.py
|
||||
4ec67865474c6c7ebaa7d125490486e8644618b1666cbde597420e4e6fe8f1d4 tests/single_owner_governance/test_task_scoped_bootstrap.py
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"type": "branch",
|
||||
"branch": "patch/aios-agent-task-control-plane-20260724",
|
||||
"base": "main",
|
||||
"created_by": "agent-task-plane-writer",
|
||||
"created_at": "2026-07-24T02:20:56Z",
|
||||
"direct_main_push": false,
|
||||
"owner_approval_required": true
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"execution_status": "PARTIALLY_COMPLETED",
|
||||
"tests_run": 19,
|
||||
"failures": 0,
|
||||
"errors": 0,
|
||||
"skipped": 0,
|
||||
"multi_agent_shadow_verified": true,
|
||||
"production_side_effects": false,
|
||||
"notification_delivery_status": "NOT_DELIVERED"
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
{
|
||||
"root_task_id": "AIOS-AGENT-TASK-CONTROL-PLANE-20260724-SHADOW",
|
||||
"execution_mode": "shadow_adapter",
|
||||
"production_side_effects": false,
|
||||
"tasks": [
|
||||
{
|
||||
"task_id": "AIOS-TASK-PLANE-OPENCLAW-ACCEPTANCE",
|
||||
"parent_task_id": "AIOS-AGENT-TASK-CONTROL-PLANE-20260724-SHADOW",
|
||||
"task_status": "COMPLETED",
|
||||
"worker_result": {
|
||||
"worker": "openclaw",
|
||||
"task_id": "AIOS-TASK-PLANE-OPENCLAW-ACCEPTANCE",
|
||||
"status": "SHADOW_VERIFIED",
|
||||
"claim_id": "claim-093cbf4c-11d5-47a3-97dc-a373a8a53940",
|
||||
"execution_mode": "shadow",
|
||||
"production_side_effects": false
|
||||
},
|
||||
"result_digest": "b3a8ed4e0a60a0a9364d1eea1a6d30a403da1fb5ff79ff38a489a52227f4116e"
|
||||
},
|
||||
{
|
||||
"task_id": "AIOS-TASK-PLANE-CODEX-REVIEW",
|
||||
"parent_task_id": "AIOS-AGENT-TASK-CONTROL-PLANE-20260724-SHADOW",
|
||||
"task_status": "COMPLETED",
|
||||
"worker_result": {
|
||||
"worker": "codex",
|
||||
"task_id": "AIOS-TASK-PLANE-CODEX-REVIEW",
|
||||
"status": "SHADOW_VERIFIED",
|
||||
"claim_id": "claim-3cfe4031-27c7-409c-9841-cd5c0d26218d",
|
||||
"execution_mode": "shadow",
|
||||
"production_side_effects": false
|
||||
},
|
||||
"result_digest": "65fb825869b9b32d923b387796839e57b9ca7053716720861988ceb8d7e9c93a"
|
||||
},
|
||||
{
|
||||
"task_id": "AIOS-TASK-PLANE-HERMES-ARCHITECTURE",
|
||||
"parent_task_id": "AIOS-AGENT-TASK-CONTROL-PLANE-20260724-SHADOW",
|
||||
"task_status": "COMPLETED",
|
||||
"worker_result": {
|
||||
"worker": "hermes",
|
||||
"task_id": "AIOS-TASK-PLANE-HERMES-ARCHITECTURE",
|
||||
"status": "SHADOW_VERIFIED",
|
||||
"claim_id": "claim-fd7ba481-ab71-47dc-8e91-3655cbef1c40",
|
||||
"execution_mode": "shadow",
|
||||
"production_side_effects": false
|
||||
},
|
||||
"result_digest": "09c9ce563424b6490b7be1e854d06de786bd243a48b099137a7a2e9cb7035265"
|
||||
}
|
||||
],
|
||||
"event_chains_valid": true,
|
||||
"notification_delivery_status": [
|
||||
"NOT_DELIVERED"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
{"schema_version":1,"task_id":"AIOS-AGENT-TASK-CONTROL-PLANE-20260724","writer_status":"READY","production_side_effects":false}
|
||||
@@ -0,0 +1,268 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Build a deterministic, secret-scanned Silent Release evidence package."""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import shutil
|
||||
import subprocess
|
||||
import tarfile
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
|
||||
OID_RE = re.compile(r"^[0-9a-f]{40}$")
|
||||
AUTHORIZATION_PATTERN = re.compile(
|
||||
rb"Authorization:\s*(?:Bearer|token)\s+([A-Za-z0-9._~+/=-]{16,})",
|
||||
re.I,
|
||||
)
|
||||
SECRET_PATTERNS = (
|
||||
re.compile(rb"(?:ghp|glpat)_[A-Za-z0-9_-]{20,}", re.I),
|
||||
re.compile(
|
||||
rb"-----BEGIN (?:RSA |EC |OPENSSH )?PRIVATE KEY-----\r?\n"
|
||||
rb"[A-Za-z0-9+/=\r\n]{64,}\r?\n"
|
||||
rb"-----END (?:RSA |EC |OPENSSH )?PRIVATE KEY-----"
|
||||
),
|
||||
)
|
||||
ALLOWED_AUTHORIZATION_PLACEHOLDER_SHA256 = frozenset(
|
||||
{
|
||||
"009f7191815ec615d09d0698457d048432e9a73f62ab4f26af1875009d0afbbc",
|
||||
"599a7f359d1e11124054f8afeae201f2d265b8988d91cb5b7d4dc4c9e2225c30",
|
||||
"f39dac6cbaba535e2c207cd0cd8f154974223c848f727f98b3564cea569b41cf",
|
||||
}
|
||||
)
|
||||
TARGET_TESTS = (
|
||||
"tests/test_silent_release_execution_readiness.py",
|
||||
"tests/test_silent_release_controller.py",
|
||||
"tests/test_silent_release_hardening.py",
|
||||
"tests/test_silent_release_production_contract.py",
|
||||
"tests/test_chatgpt_actions_tag.py",
|
||||
"tests/test_gitea_tag_publication.py",
|
||||
"tests/test_authoritative_git_context.py",
|
||||
)
|
||||
|
||||
|
||||
def sha256(path: Path) -> str:
|
||||
digest = hashlib.sha256()
|
||||
with path.open("rb") as source:
|
||||
for chunk in iter(lambda: source.read(1024 * 1024), b""):
|
||||
digest.update(chunk)
|
||||
return digest.hexdigest()
|
||||
|
||||
|
||||
def atomic_json(path: Path, payload: dict[str, Any]) -> None:
|
||||
temporary = path.with_suffix(path.suffix + ".tmp")
|
||||
with temporary.open("w", encoding="utf-8", newline="\n") as output:
|
||||
json.dump(payload, output, sort_keys=True, separators=(",", ":"))
|
||||
output.write("\n")
|
||||
output.flush()
|
||||
os.fsync(output.fileno())
|
||||
os.replace(temporary, path)
|
||||
|
||||
|
||||
def redact_output(value: str) -> str:
|
||||
value = re.sub(
|
||||
r"(?i)(Authorization:\s*(?:Bearer|token)\s+)"
|
||||
r"[A-Za-z0-9._~+/=-]{8,}",
|
||||
r"\1<redacted>",
|
||||
str(value),
|
||||
)
|
||||
value = re.sub(
|
||||
r"-----BEGIN (?:RSA |EC |OPENSSH )?PRIVATE KEY-----.*?"
|
||||
r"-----END (?:RSA |EC |OPENSSH )?PRIVATE KEY-----",
|
||||
"<redacted-private-key>",
|
||||
value,
|
||||
flags=re.DOTALL,
|
||||
)
|
||||
return value
|
||||
|
||||
|
||||
def git(git_dir: Path, *arguments: str) -> str:
|
||||
completed = subprocess.run(
|
||||
["git", f"--git-dir={git_dir}", *arguments],
|
||||
check=True,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=60,
|
||||
env={"PATH": os.environ.get("PATH", "")},
|
||||
)
|
||||
return completed.stdout.strip()
|
||||
|
||||
|
||||
def run_evidence(
|
||||
command: list[str],
|
||||
*,
|
||||
cwd: Path,
|
||||
output: Path,
|
||||
timeout: int,
|
||||
) -> dict[str, Any]:
|
||||
completed = subprocess.run(
|
||||
command,
|
||||
cwd=cwd,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=timeout,
|
||||
env={
|
||||
"HOME": str(cwd / ".builder-home"),
|
||||
"PATH": os.environ.get("PATH", ""),
|
||||
"PYTHONDONTWRITEBYTECODE": "1",
|
||||
},
|
||||
)
|
||||
record = {
|
||||
"command": command,
|
||||
"exit_code": completed.returncode,
|
||||
"stderr": redact_output(completed.stderr[-20000:]),
|
||||
"stdout": redact_output(completed.stdout[-20000:]),
|
||||
}
|
||||
atomic_json(output, record)
|
||||
if completed.returncode != 0:
|
||||
raise RuntimeError(f"trusted builder command failed: {command[0]}")
|
||||
return record
|
||||
|
||||
|
||||
def secret_scan(root: Path) -> dict[str, Any]:
|
||||
findings: list[str] = []
|
||||
for path in sorted(item for item in root.rglob("*") if item.is_file()):
|
||||
if path.stat().st_size > 5 * 1024 * 1024:
|
||||
continue
|
||||
data = path.read_bytes()
|
||||
authorization_tokens = AUTHORIZATION_PATTERN.findall(data)
|
||||
unsafe_authorization = any(
|
||||
hashlib.sha256(token.lower()).hexdigest()
|
||||
not in ALLOWED_AUTHORIZATION_PLACEHOLDER_SHA256
|
||||
for token in authorization_tokens
|
||||
)
|
||||
if unsafe_authorization or any(
|
||||
pattern.search(data) for pattern in SECRET_PATTERNS
|
||||
):
|
||||
findings.append(path.relative_to(root).as_posix())
|
||||
return {"findings": findings, "passed": not findings}
|
||||
|
||||
|
||||
def build(git_dir: Path, commit: str, output: Path) -> dict[str, Any]:
|
||||
commit = git(git_dir, "rev-parse", f"{commit}^{{commit}}")
|
||||
if not OID_RE.fullmatch(commit):
|
||||
raise ValueError("commit must resolve to a full OID")
|
||||
tree_oid = git(git_dir, "rev-parse", f"{commit}^{{tree}}")
|
||||
if output.exists():
|
||||
raise FileExistsError("artifact output already exists")
|
||||
output.mkdir(parents=True, mode=0o700)
|
||||
source_tar = output / "source.tar"
|
||||
subprocess.run(
|
||||
[
|
||||
"git",
|
||||
f"--git-dir={git_dir}",
|
||||
"archive",
|
||||
"--format=tar",
|
||||
f"--output={source_tar}",
|
||||
commit,
|
||||
],
|
||||
check=True,
|
||||
timeout=120,
|
||||
env={"PATH": os.environ.get("PATH", "")},
|
||||
)
|
||||
canary = output / "canary-src"
|
||||
canary.mkdir(mode=0o700)
|
||||
with tarfile.open(source_tar, "r") as archive:
|
||||
archive.extractall(canary, filter="data")
|
||||
if any(path.name == ".git" for path in canary.rglob(".git")):
|
||||
raise RuntimeError("no-.git canary contains Git metadata")
|
||||
|
||||
test_record = run_evidence(
|
||||
["python3", "-m", "pytest", "-q", *TARGET_TESTS],
|
||||
cwd=canary,
|
||||
output=output / "test-evidence.json",
|
||||
timeout=600,
|
||||
)
|
||||
fixture_record = run_evidence(
|
||||
["bash", "tests/run_live_gitea_fixture.sh"],
|
||||
cwd=canary,
|
||||
output=output / "live-fixture-evidence.json",
|
||||
timeout=600,
|
||||
)
|
||||
canary_record = {
|
||||
"git_directory_present": False,
|
||||
"live_fixture_exit_code": fixture_record["exit_code"],
|
||||
"passed": True,
|
||||
"target_test_exit_code": test_record["exit_code"],
|
||||
}
|
||||
atomic_json(output / "canary-evidence.json", canary_record)
|
||||
|
||||
scan = secret_scan(canary)
|
||||
atomic_json(output / "secret-scan.json", scan)
|
||||
if not scan["passed"]:
|
||||
raise RuntimeError("secret scan failed")
|
||||
|
||||
manifest_files: list[dict[str, Any]] = []
|
||||
tracked = git(git_dir, "ls-tree", "-r", "--name-only", commit).splitlines()
|
||||
for relative in tracked:
|
||||
path = canary / relative
|
||||
if path.is_file():
|
||||
manifest_files.append(
|
||||
{
|
||||
"path": relative,
|
||||
"sha256": sha256(path),
|
||||
"size": path.stat().st_size,
|
||||
}
|
||||
)
|
||||
manifest = {
|
||||
"commit": commit,
|
||||
"file_count": len(manifest_files),
|
||||
"files": manifest_files,
|
||||
"tree_oid": tree_oid,
|
||||
}
|
||||
atomic_json(output / "manifest.json", manifest)
|
||||
shutil.copyfile(source_tar, output / "rollback-source.tar")
|
||||
|
||||
closure = {
|
||||
"artifact_sha256": sha256(source_tar),
|
||||
"canary_evidence_sha256": sha256(output / "canary-evidence.json"),
|
||||
"commit": commit,
|
||||
"live_fixture_evidence_sha256": sha256(
|
||||
output / "live-fixture-evidence.json"
|
||||
),
|
||||
"manifest_sha256": sha256(output / "manifest.json"),
|
||||
"no_git_canary": True,
|
||||
"rollback_artifact_sha256": sha256(
|
||||
output / "rollback-source.tar"
|
||||
),
|
||||
"secret_scan_sha256": sha256(output / "secret-scan.json"),
|
||||
"test_evidence_sha256": sha256(output / "test-evidence.json"),
|
||||
"tree_oid": tree_oid,
|
||||
}
|
||||
atomic_json(output / "package-closure.json", closure)
|
||||
closure["package_closure_sha256"] = sha256(
|
||||
output / "package-closure.json"
|
||||
)
|
||||
checksummed = sorted(
|
||||
path
|
||||
for path in output.iterdir()
|
||||
if path.is_file() and path.name != "checksums.sha256"
|
||||
)
|
||||
with (output / "checksums.sha256").open(
|
||||
"w", encoding="utf-8", newline="\n"
|
||||
) as checksums:
|
||||
for path in checksummed:
|
||||
checksums.write(f"{sha256(path)} {path.name}\n")
|
||||
return closure
|
||||
|
||||
|
||||
def main() -> None:
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--git-dir", required=True, type=Path)
|
||||
parser.add_argument("--commit", required=True)
|
||||
parser.add_argument("--output", required=True, type=Path)
|
||||
arguments = parser.parse_args()
|
||||
result = build(
|
||||
arguments.git_dir.resolve(),
|
||||
arguments.commit,
|
||||
arguments.output.resolve(),
|
||||
)
|
||||
print(json.dumps(result, sort_keys=True))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,64 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Deterministic, local-only fault injection for Task Plane recovery."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import time
|
||||
from unittest import mock
|
||||
|
||||
from app.task_plane.api import TaskPlaneService
|
||||
from app.task_plane.policy import PolicyEngine
|
||||
from app.task_plane.store import TaskPlaneStore
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--database", required=True)
|
||||
args = parser.parse_args()
|
||||
store = TaskPlaneStore(args.database)
|
||||
service = TaskPlaneService(
|
||||
store, policy=PolicyEngine(execution_enabled=True)
|
||||
)
|
||||
identity = "spiffe://aios/agent/codex/fault-injection"
|
||||
service.register_worker(
|
||||
{
|
||||
"worker_id": "codex-fault-injection",
|
||||
"worker_type": "codex",
|
||||
"identity": identity,
|
||||
"capabilities": ["test.execute"],
|
||||
"trust_zone": "isolated-shadow",
|
||||
"max_concurrency": 1,
|
||||
"heartbeat_interval_seconds": 1,
|
||||
"supported_protocols": ["aios-task-v1"],
|
||||
}
|
||||
)
|
||||
task = service.create_task(
|
||||
{"task_id": "task-fault-expired-lease", "required_capabilities": ["test.execute"]}
|
||||
)
|
||||
claim = service.claim(
|
||||
task["task_id"],
|
||||
{
|
||||
"worker_id": "codex-fault-injection",
|
||||
"actor_identity": identity,
|
||||
"lease_seconds": 1,
|
||||
},
|
||||
)
|
||||
with mock.patch("app.task_plane.store._now", return_value=time.time() + 5):
|
||||
recovered = store.recover_expired_claim(
|
||||
task["task_id"], claim["claim_id"], probe_confirms_stopped=True
|
||||
)
|
||||
output = {
|
||||
"fault": "expired_lease",
|
||||
"recovered_status": recovered.status,
|
||||
"event_chain_valid": store.verify_event_chain(task["task_id"]),
|
||||
"production_side_effects": False,
|
||||
}
|
||||
print(json.dumps(output, sort_keys=True))
|
||||
store.close()
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,470 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Run two zero-side-effect Silent Release candidates in Shadow mode."""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import dataclasses
|
||||
import hashlib
|
||||
import json
|
||||
import sys
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from pathlib import Path
|
||||
|
||||
from cryptography.hazmat.primitives import serialization
|
||||
from cryptography.hazmat.primitives.asymmetric.ed25519 import (
|
||||
Ed25519PrivateKey,
|
||||
)
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
sys.path.insert(0, str(ROOT / "active-runtime" / "app"))
|
||||
|
||||
from silent_release_controller import ReleaseCandidate, ReleaseState # noqa: E402
|
||||
from silent_release_execution import ( # noqa: E402
|
||||
LocalTestApprovalSigner,
|
||||
LocalTestBuilderSigner,
|
||||
PersistentSilentReleaseController,
|
||||
SignedTrustedBuilderAdapter,
|
||||
)
|
||||
from silent_release_hardening import ( # noqa: E402
|
||||
ApprovalKeyRing,
|
||||
ApprovalRevocationStore,
|
||||
BuilderAttestationVerifier,
|
||||
DeploymentPlan,
|
||||
FinalizationPlan,
|
||||
ReleasePlan,
|
||||
SignedApprovalVerifier,
|
||||
SignedReleasePlanApproval,
|
||||
SilentReleaseActionService,
|
||||
TagPlan,
|
||||
TagPublicationProbeEvidence,
|
||||
TrustedCandidateBundle,
|
||||
TrustedReleasePolicy,
|
||||
classify_tag_publication,
|
||||
)
|
||||
|
||||
|
||||
def sha256_file(path: Path) -> str:
|
||||
digest = hashlib.sha256()
|
||||
with path.open("rb") as source:
|
||||
for block in iter(lambda: source.read(1024 * 1024), b""):
|
||||
digest.update(block)
|
||||
return digest.hexdigest()
|
||||
|
||||
|
||||
def write_json(path: Path, value: object) -> None:
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
temporary = path.with_suffix(path.suffix + ".tmp")
|
||||
temporary.write_text(
|
||||
json.dumps(value, indent=2, sort_keys=True) + "\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
temporary.replace(path)
|
||||
|
||||
|
||||
def oid(seed: str) -> str:
|
||||
return hashlib.sha1(seed.encode("utf-8")).hexdigest()
|
||||
|
||||
|
||||
def candidate_id(event_id: str, source_commit: str) -> str:
|
||||
return "rc-shadow-" + hashlib.sha256(
|
||||
f"{event_id}\0{source_commit}".encode("utf-8")
|
||||
).hexdigest()[:20]
|
||||
|
||||
|
||||
def make_plan(arguments: argparse.Namespace, candidate: str) -> ReleasePlan:
|
||||
version = "shadow-" + candidate.removeprefix("rc-shadow-")
|
||||
tag_oid = oid("tag:" + candidate)
|
||||
approval_plan_id = "shadow-plan-" + candidate.removeprefix("rc-shadow-")
|
||||
release_path = str(Path(arguments.release_path))
|
||||
previous_baseline = arguments.baseline_before_sha256
|
||||
return ReleasePlan(
|
||||
deployment=DeploymentPlan(
|
||||
service="aios-silent-release-controller",
|
||||
release_path=release_path,
|
||||
release_root=str(Path(release_path).parent.parent),
|
||||
cutover_link=str(Path(release_path).parent.parent / "current"),
|
||||
dropin_path=str(
|
||||
Path(release_path)
|
||||
/ "deploy"
|
||||
/ "systemd"
|
||||
/ "aios-silent-release-controller.service"
|
||||
),
|
||||
dropin_sha256=arguments.systemd_unit_sha256,
|
||||
systemd_unit_sha256=arguments.systemd_unit_sha256,
|
||||
rollback_path=str(Path(arguments.rollback_artifact)),
|
||||
rollback_sha256=arguments.artifact_sha256,
|
||||
source_commit=arguments.source_commit,
|
||||
tree_oid=arguments.tree_oid,
|
||||
artifact_sha256=arguments.artifact_sha256,
|
||||
manifest_sha256=arguments.manifest_sha256,
|
||||
package_closure_sha256=arguments.package_closure_sha256,
|
||||
authoritative_git_dir=str(Path(arguments.repository)),
|
||||
repository_identity=arguments.repository_identity,
|
||||
writer_expected=False,
|
||||
baseline_before_sha256=previous_baseline,
|
||||
registry_before_sha256=arguments.registry_before_sha256,
|
||||
production_tag_refset_before_sha256=(
|
||||
arguments.tag_refset_before_sha256
|
||||
),
|
||||
health_contract_version="shadow-health/v1",
|
||||
timeout_policy={"health_seconds": 10},
|
||||
retry_policy={"health_attempts": 1},
|
||||
policy_sha256=arguments.policy_sha256,
|
||||
health_contract={
|
||||
"execution_enabled": False,
|
||||
"shadow_mode": True,
|
||||
"side_effects_executed": False,
|
||||
},
|
||||
),
|
||||
tag=TagPlan(
|
||||
version=version,
|
||||
tag_type="annotated",
|
||||
expected_tag_oid=tag_oid,
|
||||
expected_target_oid=arguments.source_commit,
|
||||
evidence_closure_oid=arguments.source_commit,
|
||||
approval_plan_id=approval_plan_id,
|
||||
repository_identity=arguments.repository_identity,
|
||||
gitea_owner="aios",
|
||||
gitea_repository="aios",
|
||||
current_remote_tag_oid=None,
|
||||
remote_absent_evidence_sha256=hashlib.sha256(
|
||||
f"shadow-only:{candidate}".encode("utf-8")
|
||||
).hexdigest(),
|
||||
verification_contract="shadow-no-tag-write/v1",
|
||||
),
|
||||
finalization=FinalizationPlan(
|
||||
version=version,
|
||||
expected_tag_oid=tag_oid,
|
||||
expected_target_oid=arguments.source_commit,
|
||||
expected_previous_baseline_sha256=previous_baseline,
|
||||
registry_expected_record={
|
||||
"shadow_only": True,
|
||||
"target_oid": arguments.source_commit,
|
||||
},
|
||||
baseline_expected_record={
|
||||
"shadow_only": True,
|
||||
"target_oid": arguments.source_commit,
|
||||
},
|
||||
final_consistency_contract="shadow-no-finalization/v1",
|
||||
),
|
||||
policy_sha256=arguments.policy_sha256,
|
||||
)
|
||||
|
||||
|
||||
def make_candidate(
|
||||
arguments: argparse.Namespace, event_id: str
|
||||
) -> ReleaseCandidate:
|
||||
return ReleaseCandidate(
|
||||
candidate_id=candidate_id(event_id, arguments.source_commit),
|
||||
source_commit=arguments.source_commit,
|
||||
tree_oid=arguments.tree_oid,
|
||||
artifact_sha256=arguments.artifact_sha256,
|
||||
manifest_sha256=arguments.manifest_sha256,
|
||||
tests={"passed": True, "source": "trusted-artifact-evidence"},
|
||||
canary={"passed": True, "without_git": True},
|
||||
rollback_ready=True,
|
||||
git_consistency={"consistent": True},
|
||||
gitea_consistency={"consistent": True},
|
||||
approval_state="PENDING",
|
||||
state=ReleaseState.READY_FOR_APPROVAL,
|
||||
)
|
||||
|
||||
|
||||
def sign_approval(
|
||||
service: SilentReleaseActionService,
|
||||
signer: LocalTestApprovalSigner,
|
||||
candidate: str,
|
||||
nonce_suffix: str,
|
||||
) -> SignedReleasePlanApproval:
|
||||
release_candidate = service.candidates.load(candidate)
|
||||
plan = service.release_plans.load(candidate)
|
||||
now = datetime.now(timezone.utc)
|
||||
unsigned = SignedReleasePlanApproval(
|
||||
release_plan_id="shadow-release-" + candidate.removeprefix("rc-"),
|
||||
candidate_id=candidate,
|
||||
source_commit=release_candidate.source_commit,
|
||||
tree_oid=release_candidate.tree_oid,
|
||||
artifact_sha256=release_candidate.artifact_sha256,
|
||||
manifest_sha256=release_candidate.manifest_sha256,
|
||||
deployment_plan_sha256=plan.deployment.sha256,
|
||||
release_plan_sha256=plan.sha256,
|
||||
policy_sha256=plan.policy_sha256,
|
||||
approval_plan_id=plan.tag.approval_plan_id,
|
||||
evidence_closure_oid=plan.tag.evidence_closure_oid,
|
||||
owner_approval_record_oid=oid("shadow-owner:" + candidate),
|
||||
capabilities=(
|
||||
"runtime_switch",
|
||||
"tag_publish",
|
||||
"baseline_registry_finalize",
|
||||
),
|
||||
owner_identity="owner:shadow-acceptance-test",
|
||||
trust_domain="aios.shadow",
|
||||
issued_at=now.isoformat().replace("+00:00", "Z"),
|
||||
not_before=now.isoformat().replace("+00:00", "Z"),
|
||||
expires_at=(now + timedelta(minutes=10))
|
||||
.isoformat()
|
||||
.replace("+00:00", "Z"),
|
||||
nonce="shadow-nonce-" + nonce_suffix,
|
||||
key_id=signer.key_id,
|
||||
signature="",
|
||||
)
|
||||
return signer.sign(unsigned)
|
||||
|
||||
|
||||
def tag_probe_results(plan: TagPlan) -> dict[str, str]:
|
||||
exact = TagPublicationProbeEvidence(
|
||||
repository_identity=plan.repository_identity,
|
||||
remote_reachable=True,
|
||||
gitea_api_reachable=True,
|
||||
gitea_listing_reachable=True,
|
||||
remote_tag_oid=plan.expected_tag_oid,
|
||||
annotated_tag_object_oid=plan.expected_tag_oid,
|
||||
remote_peeled_target_oid=plan.expected_target_oid,
|
||||
gitea_api_tag_oid=plan.expected_tag_oid,
|
||||
gitea_api_peeled_target_oid=plan.expected_target_oid,
|
||||
gitea_listing_visible=True,
|
||||
gitea_listing_tag_oid=plan.expected_tag_oid,
|
||||
gitea_listing_peeled_target_oid=plan.expected_target_oid,
|
||||
observed_at=1,
|
||||
)
|
||||
absent = dataclasses.replace(
|
||||
exact,
|
||||
remote_tag_oid=None,
|
||||
annotated_tag_object_oid=None,
|
||||
remote_peeled_target_oid=None,
|
||||
gitea_api_tag_oid=None,
|
||||
gitea_api_peeled_target_oid=None,
|
||||
gitea_listing_visible=False,
|
||||
gitea_listing_tag_oid=None,
|
||||
gitea_listing_peeled_target_oid=None,
|
||||
)
|
||||
indeterminate = dataclasses.replace(exact, gitea_api_reachable=False)
|
||||
conflict = dataclasses.replace(exact, remote_tag_oid=oid("conflict"))
|
||||
return {
|
||||
"APPLIED_VERIFIED": classify_tag_publication(exact, plan).value,
|
||||
"CONFLICT": classify_tag_publication(conflict, plan).value,
|
||||
"INDETERMINATE": classify_tag_publication(
|
||||
indeterminate, plan
|
||||
).value,
|
||||
"NOT_APPLIED": classify_tag_publication(absent, plan).value,
|
||||
}
|
||||
|
||||
|
||||
def parser() -> argparse.ArgumentParser:
|
||||
value = argparse.ArgumentParser()
|
||||
value.add_argument("--state-root", required=True, type=Path)
|
||||
value.add_argument("--repository", required=True, type=Path)
|
||||
value.add_argument("--release-path", required=True, type=Path)
|
||||
value.add_argument("--rollback-artifact", required=True, type=Path)
|
||||
value.add_argument("--source-commit", required=True)
|
||||
value.add_argument("--tree-oid", required=True)
|
||||
value.add_argument("--artifact-sha256", required=True)
|
||||
value.add_argument("--manifest-sha256", required=True)
|
||||
value.add_argument("--package-closure-sha256", required=True)
|
||||
value.add_argument("--policy-sha256", required=True)
|
||||
value.add_argument("--systemd-unit-sha256", required=True)
|
||||
value.add_argument("--baseline-before-sha256", required=True)
|
||||
value.add_argument("--registry-before-sha256", required=True)
|
||||
value.add_argument("--tag-refset-before-sha256", required=True)
|
||||
value.add_argument("--repository-identity", default="aios/aios")
|
||||
value.add_argument("--output", required=True, type=Path)
|
||||
return value
|
||||
|
||||
|
||||
def main() -> None:
|
||||
arguments = parser().parse_args()
|
||||
if sha256_file(arguments.rollback_artifact) != arguments.artifact_sha256:
|
||||
raise SystemExit("rollback artifact hash mismatch")
|
||||
unit_path = (
|
||||
arguments.release_path
|
||||
/ "deploy"
|
||||
/ "systemd"
|
||||
/ "aios-silent-release-controller.service"
|
||||
)
|
||||
if sha256_file(unit_path) != arguments.systemd_unit_sha256:
|
||||
raise SystemExit("systemd unit hash mismatch")
|
||||
|
||||
builder_signer = LocalTestBuilderSigner.generate()
|
||||
builder_public = builder_signer.public_key.public_bytes(
|
||||
serialization.Encoding.Raw,
|
||||
serialization.PublicFormat.Raw,
|
||||
)
|
||||
owner_private = Ed25519PrivateKey.generate()
|
||||
owner_public = owner_private.public_key().public_bytes(
|
||||
serialization.Encoding.Raw,
|
||||
serialization.PublicFormat.Raw,
|
||||
)
|
||||
owner_signer = LocalTestApprovalSigner(
|
||||
"shadow-owner-ephemeral", owner_private
|
||||
)
|
||||
|
||||
def collector(intent):
|
||||
event_id = str(intent["event_id"])
|
||||
candidate = make_candidate(arguments, event_id)
|
||||
return TrustedCandidateBundle(
|
||||
candidate=candidate,
|
||||
release_plan=make_plan(arguments, candidate.candidate_id),
|
||||
checks={
|
||||
"authoritative_git": True,
|
||||
"artifact": True,
|
||||
"canary": True,
|
||||
"gitea": True,
|
||||
"manifest": True,
|
||||
"package_closure": True,
|
||||
"rollback": True,
|
||||
"secret_scan": True,
|
||||
"tests": True,
|
||||
},
|
||||
)
|
||||
|
||||
release_root = str(arguments.release_path.parent.parent)
|
||||
dropin = str(
|
||||
arguments.release_path
|
||||
/ "deploy"
|
||||
/ "systemd"
|
||||
/ "aios-silent-release-controller.service"
|
||||
)
|
||||
policy = TrustedReleasePolicy(
|
||||
services=frozenset({"aios-silent-release-controller"}),
|
||||
release_roots=frozenset({release_root}),
|
||||
cutover_links=frozenset({str(Path(release_root) / "current")}),
|
||||
dropin_paths=frozenset({dropin}),
|
||||
authoritative_git_dirs=frozenset({str(arguments.repository)}),
|
||||
repository_identities=frozenset(
|
||||
{arguments.repository_identity}
|
||||
),
|
||||
)
|
||||
verifier = SignedApprovalVerifier(
|
||||
ApprovalKeyRing({"shadow-owner-ephemeral": owner_public}),
|
||||
ApprovalRevocationStore(arguments.state_root / "revocations"),
|
||||
allowed_trust_domains={"aios.shadow"},
|
||||
)
|
||||
service = SilentReleaseActionService(
|
||||
arguments.state_root,
|
||||
execution_enabled=False,
|
||||
approval_verifier=verifier,
|
||||
candidate_builder=SignedTrustedBuilderAdapter(
|
||||
collector, builder_signer
|
||||
),
|
||||
builder_attestation_verifier=BuilderAttestationVerifier(
|
||||
{builder_signer.key_id: builder_public}
|
||||
),
|
||||
trusted_policy=policy,
|
||||
expected_repository=arguments.repository_identity,
|
||||
policy_sha256=arguments.policy_sha256,
|
||||
)
|
||||
|
||||
first_event = "main-" + arguments.source_commit
|
||||
first = service.create_release_intent(
|
||||
{
|
||||
"event_id": first_event,
|
||||
"source_commit": arguments.source_commit,
|
||||
}
|
||||
)
|
||||
first_duplicate = service.create_release_intent(
|
||||
{
|
||||
"event_id": first_event,
|
||||
"source_commit": arguments.source_commit,
|
||||
}
|
||||
)
|
||||
first_notification = service.ensure_owner_notification(
|
||||
first["candidate_id"]
|
||||
)
|
||||
repeated_notification = service.ensure_owner_notification(
|
||||
first["candidate_id"]
|
||||
)
|
||||
first_approval = sign_approval(
|
||||
service, owner_signer, first["candidate_id"], "candidate-one"
|
||||
)
|
||||
first_submit = service.submit_owner_approval(
|
||||
first_approval.to_dict()
|
||||
)
|
||||
first_submit_repeated = service.submit_owner_approval(
|
||||
first_approval.to_dict()
|
||||
)
|
||||
first_start = service.start_deployment(first["candidate_id"])
|
||||
|
||||
second = service.create_release_intent(
|
||||
{
|
||||
"event_id": "shadow-indeterminate-" + arguments.source_commit,
|
||||
"source_commit": arguments.source_commit,
|
||||
}
|
||||
)
|
||||
second_plan = service.release_plans.load(second["candidate_id"])
|
||||
probe_results = tag_probe_results(second_plan.tag)
|
||||
|
||||
first_controller = PersistentSilentReleaseController(
|
||||
arguments.state_root,
|
||||
service,
|
||||
main_probe=lambda: arguments.source_commit,
|
||||
)
|
||||
controller_first = first_controller.run_once()
|
||||
controller_restarted = PersistentSilentReleaseController(
|
||||
arguments.state_root,
|
||||
service,
|
||||
main_probe=lambda: arguments.source_commit,
|
||||
).run_once()
|
||||
|
||||
report = {
|
||||
"approval_backend": "ephemeral-shadow-test-only",
|
||||
"artifact_sha256": arguments.artifact_sha256,
|
||||
"builder_attestation": "VERIFIED",
|
||||
"candidate_count": 2,
|
||||
"candidates": [
|
||||
{
|
||||
"candidate_id": first["candidate_id"],
|
||||
"duplicate_intent_idempotent": first_duplicate["idempotent"],
|
||||
"notification_idempotent": (
|
||||
repeated_notification["idempotent"]
|
||||
),
|
||||
"notification_initial_idempotent": (
|
||||
first_notification["idempotent"]
|
||||
),
|
||||
"replayed_approval_status": (
|
||||
first_submit_repeated["status"]
|
||||
),
|
||||
"shadow_start": first_start,
|
||||
"signed_approval_status": first_submit["status"],
|
||||
},
|
||||
{
|
||||
"candidate_id": second["candidate_id"],
|
||||
"tag_publication_probe": "INDETERMINATE",
|
||||
},
|
||||
],
|
||||
"controller_restart_idempotent": (
|
||||
controller_restarted["intent"]["idempotent"]
|
||||
),
|
||||
"controller_restart_notification_idempotent": (
|
||||
controller_restarted["notification"]["idempotent"]
|
||||
),
|
||||
"controller_status": controller_first["startup"]["status"],
|
||||
"execution_enabled": False,
|
||||
"manifest_sha256": arguments.manifest_sha256,
|
||||
"package_closure_sha256": arguments.package_closure_sha256,
|
||||
"private_key_files_written": False,
|
||||
"production_tag": "NOT_EXECUTED",
|
||||
"repository_identity": arguments.repository_identity,
|
||||
"shadow_mode": True,
|
||||
"side_effects_executed": False,
|
||||
"source_commit": arguments.source_commit,
|
||||
"tag_probe_outcomes": probe_results,
|
||||
"tree_oid": arguments.tree_oid,
|
||||
}
|
||||
write_json(arguments.output, report)
|
||||
print(
|
||||
json.dumps(
|
||||
{
|
||||
"candidate_count": 2,
|
||||
"execution_enabled": False,
|
||||
"output": str(arguments.output),
|
||||
"shadow_mode": True,
|
||||
"side_effects_executed": False,
|
||||
"status": "SHADOW_ACCEPTANCE_PASSED",
|
||||
},
|
||||
sort_keys=True,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,180 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Create one authenticated, no-side-effect Task Plane Shadow task.
|
||||
|
||||
The client deliberately has no worker, Git, deployment, or credential-writing
|
||||
capabilities. Its token is accepted only from a protected file reference or
|
||||
stdin so that it cannot enter the process command line.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
import urllib.error
|
||||
import urllib.request
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
|
||||
MINIMUM_CLIENT_CAPABILITIES = (
|
||||
"task.creator",
|
||||
"task.read",
|
||||
"task.events.read",
|
||||
"task.evidence.read",
|
||||
)
|
||||
|
||||
|
||||
class ShadowClientError(RuntimeError):
|
||||
"""HTTP failure that intentionally carries no response body."""
|
||||
|
||||
def __init__(self, status: int):
|
||||
super().__init__(f"shadow task request failed with HTTP {status}")
|
||||
self.status = status
|
||||
|
||||
|
||||
def _request_json(
|
||||
base_url: str,
|
||||
path: str,
|
||||
token: str,
|
||||
*,
|
||||
method: str = "GET",
|
||||
payload: dict[str, Any] | None = None,
|
||||
idempotency_key: str | None = None,
|
||||
) -> tuple[int, dict[str, Any]]:
|
||||
headers = {
|
||||
"Accept": "application/json",
|
||||
"Authorization": f"Bearer {token}",
|
||||
}
|
||||
data = None
|
||||
if payload is not None:
|
||||
headers["Content-Type"] = "application/json"
|
||||
data = json.dumps(payload, sort_keys=True, separators=(",", ":")).encode("utf-8")
|
||||
if idempotency_key:
|
||||
headers["Idempotency-Key"] = idempotency_key
|
||||
|
||||
request = urllib.request.Request(
|
||||
base_url.rstrip("/") + path,
|
||||
data=data,
|
||||
headers=headers,
|
||||
method=method,
|
||||
)
|
||||
try:
|
||||
with urllib.request.urlopen(request, timeout=10) as response:
|
||||
body = response.read()
|
||||
parsed = json.loads(body.decode("utf-8")) if body else {}
|
||||
return response.status, parsed
|
||||
except urllib.error.HTTPError as error:
|
||||
# Never read or print an error body: it could contain sensitive data.
|
||||
raise ShadowClientError(error.code) from None
|
||||
except urllib.error.URLError as error:
|
||||
raise ShadowClientError(503) from error
|
||||
|
||||
|
||||
def run_shadow_client(
|
||||
*,
|
||||
base_url: str,
|
||||
token: str,
|
||||
task_id: str,
|
||||
idempotency_key: str,
|
||||
) -> dict[str, Any]:
|
||||
"""Create and read one deterministic Shadow task without executing it."""
|
||||
create_status, _ = _request_json(
|
||||
base_url,
|
||||
"/tasks",
|
||||
token,
|
||||
method="POST",
|
||||
idempotency_key=idempotency_key,
|
||||
payload={
|
||||
"task_id": task_id,
|
||||
"task_type": "platform.shadow",
|
||||
"risk": "low",
|
||||
"approval_policy": "patch-only",
|
||||
"metadata": {"production_side_effects": False},
|
||||
},
|
||||
)
|
||||
task_status, task = _request_json(base_url, f"/tasks/{task_id}", token)
|
||||
events_status, events = _request_json(base_url, f"/tasks/{task_id}/events", token)
|
||||
evidence_status, evidence = _request_json(base_url, f"/tasks/{task_id}/evidence", token)
|
||||
return {
|
||||
"task_id": str(task.get("task_id", task_id)),
|
||||
"status": str(task.get("status", "UNKNOWN")),
|
||||
"event_count": len(events if isinstance(events, list) else events.get("events", [])),
|
||||
"evidence_count": len(evidence if isinstance(evidence, list) else evidence.get("evidence", [])),
|
||||
"http_statuses": {
|
||||
"create": create_status,
|
||||
"task": task_status,
|
||||
"events": events_status,
|
||||
"evidence": evidence_status,
|
||||
},
|
||||
"production_side_effects": False,
|
||||
}
|
||||
|
||||
|
||||
def _reject_plaintext_token_argument(argv: list[str]) -> None:
|
||||
if any(argument == "--token" or argument.startswith("--token=") for argument in argv):
|
||||
raise SystemExit("plaintext token arguments are forbidden")
|
||||
|
||||
|
||||
def _read_token(args: argparse.Namespace) -> str:
|
||||
if args.token_stdin:
|
||||
token = sys.stdin.buffer.read().decode("utf-8").strip()
|
||||
else:
|
||||
token_file = args.token_file or os.environ.get("AIOS_TASK_PLANE_TOKEN_FILE")
|
||||
if not token_file:
|
||||
raise SystemExit("runtime credential reference is required")
|
||||
try:
|
||||
token = Path(token_file).read_text(encoding="utf-8").strip()
|
||||
except OSError:
|
||||
raise SystemExit("runtime credential reference is unavailable") from None
|
||||
if not token:
|
||||
raise SystemExit("runtime credential reference is unavailable")
|
||||
return token
|
||||
|
||||
|
||||
def _write_report(path: Path, summary: dict[str, Any]) -> str:
|
||||
encoded = (json.dumps(summary, sort_keys=True, separators=(",", ":")) + "\n").encode("utf-8")
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
path.write_bytes(encoded)
|
||||
return hashlib.sha256(encoded).hexdigest()
|
||||
|
||||
|
||||
def main(argv: list[str] | None = None) -> int:
|
||||
argv = list(sys.argv[1:] if argv is None else argv)
|
||||
_reject_plaintext_token_argument(argv)
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("--base-url", default="http://127.0.0.1:18922")
|
||||
token_group = parser.add_mutually_exclusive_group()
|
||||
token_group.add_argument("--token-file", help="protected runtime credential reference")
|
||||
token_group.add_argument("--token-stdin", action="store_true")
|
||||
parser.add_argument("--task-id", default="AIOS-TASK-PLANE-SHADOW-CLIENT")
|
||||
parser.add_argument("--idempotency-key", default="task-plane-shadow-client-v1")
|
||||
parser.add_argument("--report", required=True, type=Path)
|
||||
args = parser.parse_args(argv)
|
||||
|
||||
token = _read_token(args)
|
||||
try:
|
||||
summary = run_shadow_client(
|
||||
base_url=args.base_url,
|
||||
token=token,
|
||||
task_id=args.task_id,
|
||||
idempotency_key=args.idempotency_key,
|
||||
)
|
||||
except ShadowClientError as error:
|
||||
print(f"http_status={error.status}", file=sys.stderr)
|
||||
return error.status
|
||||
|
||||
report_sha256 = _write_report(args.report, summary)
|
||||
print(f"task_id={summary['task_id']}")
|
||||
print(f"status={summary['status']}")
|
||||
print(f"event_count={summary['event_count']}")
|
||||
print(f"evidence_count={summary['evidence_count']}")
|
||||
print("http_statuses=" + json.dumps(summary["http_statuses"], sort_keys=True))
|
||||
print(f"report_sha256={report_sha256}")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,40 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
REPORT_PATH="${1:-reports/task-plane-acceptance.json}"
|
||||
mkdir -p "$(dirname "${REPORT_PATH}")"
|
||||
export TASKPLANE_REPORT="${REPORT_PATH}"
|
||||
export PYTHONPATH="${PYTHONPATH:-}:$(pwd)/active-runtime"
|
||||
|
||||
python3 - <<'PY'
|
||||
import io
|
||||
import json
|
||||
import sys
|
||||
import unittest
|
||||
|
||||
from pathlib import Path
|
||||
from contextlib import redirect_stdout
|
||||
|
||||
report_path = Path(__import__('os').environ.get("TASKPLANE_REPORT", "reports/task-plane-acceptance.json"))
|
||||
stream = io.StringIO()
|
||||
|
||||
loader = unittest.TestLoader()
|
||||
suite = loader.discover("tests/task_plane/public_interface", pattern="test_*.py")
|
||||
runner = unittest.TextTestRunner(stream=sys.stdout, verbosity=2)
|
||||
result = runner.run(suite)
|
||||
|
||||
payload = {
|
||||
"execution_status": "PARTIALLY_COMPLETED" if result.wasSuccessful() else "FAILED",
|
||||
"tests_run": result.testsRun,
|
||||
"failures": len(result.failures),
|
||||
"errors": len(result.errors),
|
||||
"skipped": len(result.skipped),
|
||||
"multi_agent_shadow_verified": result.wasSuccessful(),
|
||||
"production_side_effects": False,
|
||||
"notification_delivery_status": "NOT_DELIVERED",
|
||||
}
|
||||
report_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
report_path.write_text(json.dumps(payload, indent=2) + "\n", encoding="utf-8")
|
||||
print(f"acceptance_report={report_path}")
|
||||
sys.exit(0 if result.wasSuccessful() else 1)
|
||||
PY
|
||||
+1090
File diff suppressed because it is too large
Load Diff
+323
@@ -0,0 +1,323 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"$id": "TASK_SCOPED_GOVERNANCE_BOOTSTRAP",
|
||||
"title": "TASK_SCOPED_GOVERNANCE_BOOTSTRAP observed approval envelope",
|
||||
"description": "Versioned fail-closed validation envelope assembled from an immutable request plus observed Owner, Git, machine-gate, runtime, and mirror evidence.",
|
||||
"x-schema-version": 1,
|
||||
"x-schema-status": "CANDIDATE",
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": [
|
||||
"document_kind",
|
||||
"request_id",
|
||||
"schema_id",
|
||||
"schema_version",
|
||||
"request_created_at",
|
||||
"request_expires_at",
|
||||
"requester_identity",
|
||||
"repository_identity",
|
||||
"approval_plan_path",
|
||||
"approval_plan_blob_oid",
|
||||
"approval_plan_sha256",
|
||||
"schema_document_sha256",
|
||||
"validator_policy_sha256",
|
||||
"required_machine_contexts",
|
||||
"governance_base_ref",
|
||||
"governance_base_oid",
|
||||
"request_head_ref",
|
||||
"request_head_oid",
|
||||
"code_base_ref",
|
||||
"code_base_oid",
|
||||
"candidate_commit_oid",
|
||||
"candidate_parent_oid",
|
||||
"candidate_tree_oid",
|
||||
"candidate_diff_sha256",
|
||||
"candidate_files_sha256",
|
||||
"target_ref",
|
||||
"target_initial_state",
|
||||
"provision_publisher",
|
||||
"provision_writer",
|
||||
"provision_credential_projection",
|
||||
"publisher_identity",
|
||||
"writer_identity",
|
||||
"credential_secret_ref",
|
||||
"publisher_scope",
|
||||
"writer_scope",
|
||||
"credential_scope",
|
||||
"observed_preapproval_operations",
|
||||
"allowed_operations",
|
||||
"allowed_operations_sha256",
|
||||
"denied_operations",
|
||||
"maximum_operation_count",
|
||||
"maximum_pr_count",
|
||||
"maximum_branch_count",
|
||||
"maximum_commit_count",
|
||||
"allowed_paths",
|
||||
"allowed_paths_sha256",
|
||||
"approved_at",
|
||||
"expires_at",
|
||||
"maximum_lifetime_seconds",
|
||||
"auto_revoke",
|
||||
"required_human_approvals",
|
||||
"allowed_human_approvers",
|
||||
"approved_by",
|
||||
"owner_review_id",
|
||||
"owner_review_commit_oid",
|
||||
"approval_plan_id",
|
||||
"approval_audit_event_id",
|
||||
"production_runtime_anchor_change",
|
||||
"production_writes_enabled",
|
||||
"tag_operations",
|
||||
"registry_operations",
|
||||
"baseline_operations",
|
||||
"deployment_operations",
|
||||
"force_push_allowed",
|
||||
"tag_rewrite_allowed",
|
||||
"direct_main_push_allowed"
|
||||
],
|
||||
"properties": {
|
||||
"document_kind": {"const": "ENVELOPE"},
|
||||
"request_id": {"type": "string", "minLength": 3, "maxLength": 192, "pattern": "^[A-Za-z0-9][A-Za-z0-9_.:-]+$"},
|
||||
"schema_id": {"const": "TASK_SCOPED_GOVERNANCE_BOOTSTRAP"},
|
||||
"schema_version": {"const": 1},
|
||||
"request_created_at": {"type": "string", "format": "date-time", "pattern": "Z$"},
|
||||
"request_expires_at": {"type": "string", "format": "date-time", "pattern": "Z$"},
|
||||
"requester_identity": {"$ref": "#/$defs/identity"},
|
||||
"repository_identity": {"const": "aios/aios"},
|
||||
"approval_plan_path": {"type": "string", "pattern": "^approvals/governance-bootstrap/[A-Za-z0-9._-]+\\.json$"},
|
||||
"approval_plan_blob_oid": {"$ref": "#/$defs/oid"},
|
||||
"approval_plan_sha256": {"$ref": "#/$defs/sha256"},
|
||||
"schema_document_sha256": {"$ref": "#/$defs/sha256"},
|
||||
"validator_policy_sha256": {"$ref": "#/$defs/sha256"},
|
||||
"required_machine_contexts": {
|
||||
"type": "array",
|
||||
"minItems": 5,
|
||||
"maxItems": 5,
|
||||
"uniqueItems": true,
|
||||
"items": {"enum": [
|
||||
"governance/authority-mirror",
|
||||
"governance/fault-injection",
|
||||
"governance/runtime-isolation",
|
||||
"governance/schema-contract",
|
||||
"governance/validator-security"
|
||||
]}
|
||||
},
|
||||
"governance_base_ref": {"const": "refs/heads/governance/approvals"},
|
||||
"governance_base_oid": {"$ref": "#/$defs/oid"},
|
||||
"request_head_ref": {"type": "string", "pattern": "^refs/heads/governance/request/[A-Za-z0-9._/-]+$"},
|
||||
"request_head_oid": {"$ref": "#/$defs/oid"},
|
||||
"code_base_ref": {"type": "string", "pattern": "^refs/heads/patch/[A-Za-z0-9._/-]+$"},
|
||||
"code_base_oid": {"$ref": "#/$defs/oid"},
|
||||
"candidate_commit_oid": {"$ref": "#/$defs/oid"},
|
||||
"candidate_parent_oid": {"$ref": "#/$defs/oid"},
|
||||
"candidate_tree_oid": {"$ref": "#/$defs/oid"},
|
||||
"candidate_diff_sha256": {"$ref": "#/$defs/sha256"},
|
||||
"candidate_files_sha256": {"$ref": "#/$defs/sha256"},
|
||||
"target_ref": {"type": "string", "pattern": "^refs/heads/patch/[A-Za-z0-9._/-]+$"},
|
||||
"target_initial_state": {"const": "ABSENT"},
|
||||
"provision_publisher": {"const": true},
|
||||
"provision_writer": {"const": true},
|
||||
"provision_credential_projection": {"const": true},
|
||||
"publisher_identity": {"$ref": "#/$defs/identity"},
|
||||
"writer_identity": {"$ref": "#/$defs/identity"},
|
||||
"credential_secret_ref": {"type": "string", "pattern": "^(systemd-credential|secret-ref)://[A-Za-z0-9_.-]+$"},
|
||||
"publisher_scope": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["repository", "local_code_write", "administrator"],
|
||||
"properties": {
|
||||
"repository": {"const": "aios/aios"},
|
||||
"local_code_write": {"const": false},
|
||||
"administrator": {"const": false}
|
||||
}
|
||||
},
|
||||
"writer_scope": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["repository", "remote_credentials", "remote_push", "run_as_root"],
|
||||
"properties": {
|
||||
"repository": {"const": "aios/aios"},
|
||||
"remote_credentials": {"const": false},
|
||||
"remote_push": {"const": false},
|
||||
"run_as_root": {"const": false}
|
||||
}
|
||||
},
|
||||
"credential_scope": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": [
|
||||
"provider", "projection", "audience", "writer_visible",
|
||||
"runtime_anchor_visible", "expires_with_request", "revocation_required",
|
||||
"post_revoke_http_status"
|
||||
],
|
||||
"properties": {
|
||||
"provider": {"const": "systemd"},
|
||||
"projection": {"const": "LoadCredential"},
|
||||
"audience": {"$ref": "#/$defs/identity"},
|
||||
"writer_visible": {"const": false},
|
||||
"runtime_anchor_visible": {"const": false},
|
||||
"expires_with_request": {"const": true},
|
||||
"revocation_required": {"const": true},
|
||||
"post_revoke_http_status": {"const": 401}
|
||||
}
|
||||
},
|
||||
"observed_preapproval_operations": {
|
||||
"type": "array",
|
||||
"minItems": 2,
|
||||
"maxItems": 2,
|
||||
"uniqueItems": true,
|
||||
"items": {"enum": [
|
||||
"create_governance_request_branch",
|
||||
"create_governance_approval_pr"
|
||||
]}
|
||||
},
|
||||
"allowed_operations": {
|
||||
"type": "array",
|
||||
"minItems": 1,
|
||||
"uniqueItems": true,
|
||||
"items": {"enum": [
|
||||
"provision_task_publisher",
|
||||
"provision_task_writer",
|
||||
"provision_task_credential_projection",
|
||||
"create_approved_code_branch",
|
||||
"push_approved_code_branch",
|
||||
"read_remote_refs",
|
||||
"revoke_task_credentials",
|
||||
"remove_task_identities"
|
||||
]}
|
||||
},
|
||||
"allowed_operations_sha256": {"$ref": "#/$defs/sha256"},
|
||||
"denied_operations": {"type": "array", "minItems": 12, "uniqueItems": true, "items": {"type": "string"}},
|
||||
"maximum_operation_count": {"type": "integer", "minimum": 1, "maximum": 10},
|
||||
"maximum_pr_count": {"const": 1},
|
||||
"maximum_branch_count": {"const": 1},
|
||||
"maximum_commit_count": {"const": 1},
|
||||
"allowed_paths": {"type": "array", "minItems": 1, "uniqueItems": true, "items": {"type": "string", "minLength": 1}},
|
||||
"allowed_paths_sha256": {"$ref": "#/$defs/sha256"},
|
||||
"approved_at": {"type": "string", "format": "date-time", "pattern": "Z$"},
|
||||
"expires_at": {"type": "string", "format": "date-time", "pattern": "Z$"},
|
||||
"maximum_lifetime_seconds": {"type": "integer", "minimum": 1, "maximum": 43200},
|
||||
"auto_revoke": {"const": true},
|
||||
"required_human_approvals": {"const": 1},
|
||||
"allowed_human_approvers": {"const": ["owner"]},
|
||||
"approved_by": {"const": "owner"},
|
||||
"owner_review_id": {"type": "integer", "minimum": 1},
|
||||
"owner_review_commit_oid": {"$ref": "#/$defs/oid"},
|
||||
"approval_plan_id": {"type": "string", "minLength": 3, "maxLength": 192},
|
||||
"approval_audit_event_id": {"type": "string", "minLength": 3, "maxLength": 192},
|
||||
"production_runtime_anchor_change": {"const": false},
|
||||
"production_writes_enabled": {"const": false},
|
||||
"tag_operations": {"const": false},
|
||||
"registry_operations": {"const": false},
|
||||
"baseline_operations": {"const": false},
|
||||
"deployment_operations": {"const": false},
|
||||
"force_push_allowed": {"const": false},
|
||||
"tag_rewrite_allowed": {"const": false},
|
||||
"direct_main_push_allowed": {"const": false}
|
||||
},
|
||||
"$defs": {
|
||||
"plan": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": [
|
||||
"document_kind", "schema_id", "schema_version", "payload_sha256",
|
||||
"payload"
|
||||
],
|
||||
"properties": {
|
||||
"document_kind": {"const": "PLAN"},
|
||||
"schema_id": {"const": "TASK_SCOPED_GOVERNANCE_BOOTSTRAP_PLAN"},
|
||||
"schema_version": {"const": 1},
|
||||
"payload_sha256": {"type": "string", "pattern": "^[0-9a-f]{64}$"},
|
||||
"payload": {"$ref": "#/$defs/planPayload"}
|
||||
}
|
||||
},
|
||||
"planPayload": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": [
|
||||
"allowed_human_approvers", "allowed_operations", "allowed_operations_sha256",
|
||||
"allowed_paths", "allowed_paths_sha256", "approval_plan_id", "approval_plan_path",
|
||||
"approved_by", "auto_revoke", "baseline_operations", "candidate_commit_oid",
|
||||
"candidate_diff_sha256", "candidate_files_sha256", "candidate_parent_oid",
|
||||
"candidate_tree_oid", "code_base_oid", "code_base_ref", "credential_scope",
|
||||
"credential_secret_ref", "denied_operations", "deployment_operations",
|
||||
"direct_main_push_allowed", "expires_at", "force_push_allowed",
|
||||
"governance_base_oid", "governance_base_ref", "maximum_branch_count",
|
||||
"maximum_commit_count", "maximum_lifetime_seconds", "maximum_operation_count",
|
||||
"maximum_pr_count", "observed_preapproval_operations",
|
||||
"production_runtime_anchor_change", "production_writes_enabled",
|
||||
"provision_credential_projection", "provision_publisher", "provision_writer",
|
||||
"publisher_identity", "publisher_scope", "registry_operations",
|
||||
"repository_identity", "request_created_at", "request_expires_at",
|
||||
"request_head_ref", "request_id", "requester_identity",
|
||||
"required_human_approvals", "required_machine_contexts",
|
||||
"schema_document_sha256", "schema_id", "schema_version", "tag_operations",
|
||||
"tag_rewrite_allowed", "target_initial_state", "target_ref",
|
||||
"validator_policy_sha256", "writer_identity", "writer_scope"
|
||||
],
|
||||
"properties": {
|
||||
"allowed_human_approvers": {"$ref": "#/properties/allowed_human_approvers"},
|
||||
"allowed_operations": {"$ref": "#/properties/allowed_operations"},
|
||||
"allowed_operations_sha256": {"$ref": "#/properties/allowed_operations_sha256"},
|
||||
"allowed_paths": {"$ref": "#/properties/allowed_paths"},
|
||||
"allowed_paths_sha256": {"$ref": "#/properties/allowed_paths_sha256"},
|
||||
"approval_plan_id": {"$ref": "#/properties/approval_plan_id"},
|
||||
"approval_plan_path": {"$ref": "#/properties/approval_plan_path"},
|
||||
"approved_by": {"$ref": "#/properties/approved_by"},
|
||||
"auto_revoke": {"$ref": "#/properties/auto_revoke"},
|
||||
"baseline_operations": {"$ref": "#/properties/baseline_operations"},
|
||||
"candidate_commit_oid": {"$ref": "#/properties/candidate_commit_oid"},
|
||||
"candidate_diff_sha256": {"$ref": "#/properties/candidate_diff_sha256"},
|
||||
"candidate_files_sha256": {"$ref": "#/properties/candidate_files_sha256"},
|
||||
"candidate_parent_oid": {"$ref": "#/properties/candidate_parent_oid"},
|
||||
"candidate_tree_oid": {"$ref": "#/properties/candidate_tree_oid"},
|
||||
"code_base_oid": {"$ref": "#/properties/code_base_oid"},
|
||||
"code_base_ref": {"$ref": "#/properties/code_base_ref"},
|
||||
"credential_scope": {"$ref": "#/properties/credential_scope"},
|
||||
"credential_secret_ref": {"$ref": "#/properties/credential_secret_ref"},
|
||||
"denied_operations": {"$ref": "#/properties/denied_operations"},
|
||||
"deployment_operations": {"$ref": "#/properties/deployment_operations"},
|
||||
"direct_main_push_allowed": {"$ref": "#/properties/direct_main_push_allowed"},
|
||||
"expires_at": {"$ref": "#/properties/expires_at"},
|
||||
"force_push_allowed": {"$ref": "#/properties/force_push_allowed"},
|
||||
"governance_base_oid": {"$ref": "#/properties/governance_base_oid"},
|
||||
"governance_base_ref": {"$ref": "#/properties/governance_base_ref"},
|
||||
"maximum_branch_count": {"$ref": "#/properties/maximum_branch_count"},
|
||||
"maximum_commit_count": {"$ref": "#/properties/maximum_commit_count"},
|
||||
"maximum_lifetime_seconds": {"$ref": "#/properties/maximum_lifetime_seconds"},
|
||||
"maximum_operation_count": {"$ref": "#/properties/maximum_operation_count"},
|
||||
"maximum_pr_count": {"$ref": "#/properties/maximum_pr_count"},
|
||||
"observed_preapproval_operations": {"$ref": "#/properties/observed_preapproval_operations"},
|
||||
"production_runtime_anchor_change": {"$ref": "#/properties/production_runtime_anchor_change"},
|
||||
"production_writes_enabled": {"$ref": "#/properties/production_writes_enabled"},
|
||||
"provision_credential_projection": {"$ref": "#/properties/provision_credential_projection"},
|
||||
"provision_publisher": {"$ref": "#/properties/provision_publisher"},
|
||||
"provision_writer": {"$ref": "#/properties/provision_writer"},
|
||||
"publisher_identity": {"$ref": "#/properties/publisher_identity"},
|
||||
"publisher_scope": {"$ref": "#/properties/publisher_scope"},
|
||||
"registry_operations": {"$ref": "#/properties/registry_operations"},
|
||||
"repository_identity": {"$ref": "#/properties/repository_identity"},
|
||||
"request_created_at": {"$ref": "#/properties/request_created_at"},
|
||||
"request_expires_at": {"$ref": "#/properties/request_expires_at"},
|
||||
"request_head_ref": {"$ref": "#/properties/request_head_ref"},
|
||||
"request_id": {"$ref": "#/properties/request_id"},
|
||||
"requester_identity": {"$ref": "#/properties/requester_identity"},
|
||||
"required_human_approvals": {"$ref": "#/properties/required_human_approvals"},
|
||||
"required_machine_contexts": {"$ref": "#/properties/required_machine_contexts"},
|
||||
"schema_document_sha256": {"$ref": "#/properties/schema_document_sha256"},
|
||||
"schema_id": {"$ref": "#/properties/schema_id"},
|
||||
"schema_version": {"$ref": "#/properties/schema_version"},
|
||||
"tag_operations": {"$ref": "#/properties/tag_operations"},
|
||||
"tag_rewrite_allowed": {"$ref": "#/properties/tag_rewrite_allowed"},
|
||||
"target_initial_state": {"$ref": "#/properties/target_initial_state"},
|
||||
"target_ref": {"$ref": "#/properties/target_ref"},
|
||||
"validator_policy_sha256": {"$ref": "#/properties/validator_policy_sha256"},
|
||||
"writer_identity": {"$ref": "#/properties/writer_identity"},
|
||||
"writer_scope": {"$ref": "#/properties/writer_scope"}
|
||||
}
|
||||
},
|
||||
"oid": {"type": "string", "pattern": "^[0-9a-f]{40}$"},
|
||||
"sha256": {"type": "string", "pattern": "^[0-9a-f]{64}$"},
|
||||
"identity": {"type": "string", "minLength": 3, "maxLength": 128, "pattern": "^[A-Za-z0-9][A-Za-z0-9_.-]+$"}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,172 @@
|
||||
import contextlib
|
||||
import importlib.util
|
||||
import io
|
||||
import json
|
||||
import sys
|
||||
import tempfile
|
||||
import threading
|
||||
import unittest
|
||||
import urllib.error
|
||||
import urllib.request
|
||||
from http.server import ThreadingHTTPServer
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[0]
|
||||
|
||||
|
||||
def _load_client(root: Path):
|
||||
path = root / "scripts" / "task-plane" / "create_shadow_tasks.py"
|
||||
spec = importlib.util.spec_from_file_location("shadow_client", path)
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
assert spec and spec.loader
|
||||
spec.loader.exec_module(module)
|
||||
return module
|
||||
|
||||
|
||||
class ShadowClientRBACPublicInterfaceTests(unittest.TestCase):
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
cls.repo_root = Path.cwd()
|
||||
sys.path.insert(0, str(cls.repo_root / "active-runtime"))
|
||||
from app.task_plane.api import TaskPlaneRequestHandler, TaskPlaneService
|
||||
from app.task_plane.policy import IdentityVerifier, PolicyEngine
|
||||
from app.task_plane.store import TaskPlaneStore
|
||||
|
||||
cls.RequestHandler = TaskPlaneRequestHandler
|
||||
cls.TaskPlaneService = TaskPlaneService
|
||||
cls.IdentityVerifier = IdentityVerifier
|
||||
cls.PolicyEngine = PolicyEngine
|
||||
cls.TaskPlaneStore = TaskPlaneStore
|
||||
cls.client = _load_client(cls.repo_root)
|
||||
|
||||
def _server(self, capabilities):
|
||||
store = self.TaskPlaneStore(":memory:")
|
||||
policy = self.PolicyEngine(
|
||||
identity_verifier=self.IdentityVerifier({"shadow-client"}),
|
||||
execution_enabled=False,
|
||||
identity_capabilities={"shadow-client": set(capabilities)},
|
||||
)
|
||||
service = self.TaskPlaneService(store, policy=policy)
|
||||
|
||||
class Handler(self.RequestHandler):
|
||||
pass
|
||||
|
||||
Handler.service = service
|
||||
Handler.authenticator = lambda value: "shadow-client" if value == "test-token" else None
|
||||
server = ThreadingHTTPServer(("127.0.0.1", 0), Handler)
|
||||
thread = threading.Thread(target=server.serve_forever, daemon=True)
|
||||
thread.start()
|
||||
self.addCleanup(server.shutdown)
|
||||
self.addCleanup(server.server_close)
|
||||
self.addCleanup(thread.join, 2)
|
||||
return f"http://127.0.0.1:{server.server_port}", store
|
||||
|
||||
@staticmethod
|
||||
def _request(base, path, token=None, body=None):
|
||||
headers = {"Content-Type": "application/json"}
|
||||
if token is not None:
|
||||
headers["Authorization"] = f"Bearer {token}"
|
||||
req = urllib.request.Request(
|
||||
base + path,
|
||||
data=None if body is None else json.dumps(body).encode(),
|
||||
headers=headers,
|
||||
method="POST" if body is not None else "GET",
|
||||
)
|
||||
return urllib.request.urlopen(req, timeout=3)
|
||||
|
||||
def test_missing_credential_returns_401(self):
|
||||
base, _ = self._server({"task.creator", "task.read", "task.audit"})
|
||||
with self.assertRaises(urllib.error.HTTPError) as raised:
|
||||
self._request(base, "/tasks")
|
||||
self.assertEqual(raised.exception.code, 401)
|
||||
|
||||
def test_missing_task_creator_returns_403(self):
|
||||
base, _ = self._server({"task.read", "task.audit"})
|
||||
with self.assertRaises(self.client.ShadowClientError) as raised:
|
||||
self.client.run_shadow_client(
|
||||
base_url=base,
|
||||
token="test-token",
|
||||
task_id="SHADOW-RBAC-NO-CREATE",
|
||||
idempotency_key="shadow-rbac-no-create-v1",
|
||||
)
|
||||
self.assertEqual(raised.exception.status, 403)
|
||||
|
||||
def test_minimum_client_is_idempotent_and_cannot_claim(self):
|
||||
base, store = self._server({"task.creator", "task.read", "task.audit"})
|
||||
first = self.client.run_shadow_client(
|
||||
base_url=base,
|
||||
token="test-token",
|
||||
task_id="SHADOW-RBAC-IDEMPOTENT",
|
||||
idempotency_key="shadow-rbac-idempotent-v1",
|
||||
)
|
||||
second = self.client.run_shadow_client(
|
||||
base_url=base,
|
||||
token="test-token",
|
||||
task_id="SHADOW-RBAC-IDEMPOTENT",
|
||||
idempotency_key="shadow-rbac-idempotent-v1",
|
||||
)
|
||||
self.assertEqual(first["task_id"], second["task_id"])
|
||||
self.assertEqual(first["status"], "CREATED")
|
||||
self.assertEqual(first["event_count"], 1)
|
||||
self.assertEqual(first["evidence_count"], 0)
|
||||
self.assertEqual(store.list_workers(), [])
|
||||
claim_base, _ = self._server({"task.claim"})
|
||||
with self.assertRaises(urllib.error.HTTPError) as raised:
|
||||
self._request(
|
||||
claim_base,
|
||||
"/tasks/SHADOW-RBAC-IDEMPOTENT/claim",
|
||||
"test-token",
|
||||
{"worker_id": "shadow-worker", "lease_seconds": 30},
|
||||
)
|
||||
self.assertEqual(raised.exception.code, 423)
|
||||
|
||||
def test_forged_request_body_identity_is_rejected(self):
|
||||
base, _ = self._server({"task.creator", "task.read", "task.audit"})
|
||||
with self.assertRaises(urllib.error.HTTPError) as raised:
|
||||
self._request(
|
||||
base,
|
||||
"/tasks",
|
||||
"test-token",
|
||||
{
|
||||
"task_id": "SHADOW-RBAC-FORGED",
|
||||
"actor_identity": "administrator",
|
||||
},
|
||||
)
|
||||
self.assertEqual(raised.exception.code, 400)
|
||||
|
||||
def test_default_deny_get_rbac_and_redacted_report(self):
|
||||
base, _ = self._server({"task.creator", "task.read", "task.audit"})
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
root = Path(directory)
|
||||
token_file = root / "token"
|
||||
report = root / "report.json"
|
||||
token_file.write_text("test-token", encoding="utf-8")
|
||||
stdout = io.StringIO()
|
||||
with contextlib.redirect_stdout(stdout):
|
||||
self.client.main(
|
||||
[
|
||||
"--base-url", base,
|
||||
"--token-file", str(token_file),
|
||||
"--task-id", "SHADOW-RBAC-REPORT",
|
||||
"--idempotency-key", "shadow-rbac-report-v1",
|
||||
"--report", str(report),
|
||||
]
|
||||
)
|
||||
output = stdout.getvalue()
|
||||
self.assertNotIn("test-token", output)
|
||||
self.assertNotIn("Authorization", output)
|
||||
self.assertNotIn("test-token", report.read_text(encoding="utf-8"))
|
||||
self.assertNotIn("Authorization", report.read_text(encoding="utf-8"))
|
||||
self.assertIn("report_sha256=", output)
|
||||
|
||||
denied_base, _ = self._server({"task.creator"})
|
||||
with self.assertRaises(urllib.error.HTTPError) as raised:
|
||||
self._request(denied_base, "/tasks", "test-token")
|
||||
self.assertEqual(raised.exception.code, 403)
|
||||
|
||||
def test_plaintext_token_argument_is_rejected_without_echo(self):
|
||||
stderr = io.StringIO()
|
||||
with contextlib.redirect_stderr(stderr):
|
||||
with self.assertRaises(SystemExit):
|
||||
self.client.main(["--token", "sentinel-token"])
|
||||
self.assertNotIn("sentinel-token", stderr.getvalue())
|
||||
@@ -0,0 +1,215 @@
|
||||
import os
|
||||
import sys
|
||||
import unittest
|
||||
|
||||
ROOT = os.getcwd()
|
||||
if os.path.join(ROOT, "active-runtime") not in sys.path:
|
||||
sys.path.append(os.path.join(ROOT, "active-runtime"))
|
||||
|
||||
from app.task_plane.api import TaskPlaneService
|
||||
from app.task_plane.policy import (
|
||||
ALL_ACTION_CAPABILITIES,
|
||||
IdentityVerifier,
|
||||
PolicyEngine,
|
||||
ReadonlyWorkerProfile,
|
||||
)
|
||||
|
||||
|
||||
def _policy(**kwargs):
|
||||
return PolicyEngine(
|
||||
identity_capabilities={"*": set(ALL_ACTION_CAPABILITIES)}, **kwargs
|
||||
)
|
||||
from app.task_plane.store import TaskPlaneStore
|
||||
|
||||
|
||||
class TaskPlaneAPIBehaviorTests(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.store = TaskPlaneStore(":memory:")
|
||||
self.service = TaskPlaneService(self.store, policy=_policy(execution_enabled=False))
|
||||
|
||||
def test_task_crud_and_events_chain(self):
|
||||
task = self.service.create_task(
|
||||
{
|
||||
"task_type": "code.change",
|
||||
"intent": "verify core",
|
||||
"required_capabilities": ["code.read", "test.python"],
|
||||
}
|
||||
)
|
||||
self.assertIn("task_id", task)
|
||||
read = self.service.get_task(task["task_id"])
|
||||
self.assertEqual("CREATED", read["status"])
|
||||
events = self.service.get_events(task["task_id"])
|
||||
self.assertEqual(len(events), 1)
|
||||
|
||||
def test_execution_disabled_blocks_claim(self):
|
||||
task = self.service.create_task({"task_type": "code.change"})
|
||||
self.store.register_worker(
|
||||
{
|
||||
"worker_id": "codex-dev-01",
|
||||
"worker_type": "codex",
|
||||
"identity": "spiffe://aios/agent/codex/dev-01",
|
||||
"capabilities": ["code.read", "test.python"],
|
||||
"trust_zone": "isolated-development",
|
||||
"max_concurrency": 1,
|
||||
"heartbeat_interval_seconds": 15,
|
||||
"supported_protocols": ["aios-task-v1"],
|
||||
}
|
||||
)
|
||||
with self.assertRaises(Exception):
|
||||
self.service.claim(task["task_id"], {"worker_id": "codex-dev-01", "lease_seconds": 20})
|
||||
|
||||
def test_workers_and_events_outbox(self):
|
||||
policy = _policy(execution_enabled=True)
|
||||
service = TaskPlaneService(TaskPlaneStore(":memory:"), policy=policy)
|
||||
service.register_worker(
|
||||
{
|
||||
"worker_id": "codex-dev-01",
|
||||
"worker_type": "codex",
|
||||
"identity": "spiffe://aios/agent/codex/dev-01",
|
||||
"capabilities": ["code.read", "test.python", "code.write.patch"],
|
||||
"trust_zone": "isolated-development",
|
||||
"max_concurrency": 2,
|
||||
"heartbeat_interval_seconds": 15,
|
||||
"supported_protocols": ["aios-task-v1"],
|
||||
}
|
||||
)
|
||||
task = service.create_task({"required_capabilities": ["code.read"], "task_type": "code.change"})
|
||||
task_record = service.store.get_task(task["task_id"])
|
||||
identity = "spiffe://aios/agent/codex/dev-01"
|
||||
claim = service.claim(task["task_id"], {"worker_id": "codex-dev-01", "lease_seconds": 15, "actor_identity": identity})
|
||||
service.heartbeat(task["task_id"], {"claim_id": claim["claim_id"], "lease_seconds": 20, "actor_identity": identity})
|
||||
service.progress(task["task_id"], {"claim_id": claim["claim_id"], "progress": 40, "stage": "tests", "message": "running", "actor_identity": identity})
|
||||
service.complete(task["task_id"], {"claim_id": claim["claim_id"], "status": "COMPLETED", "outputs": [], "evidence": [], "actor_identity": identity})
|
||||
events = service.get_events(task["task_id"])
|
||||
self.assertGreater(len(events), 1)
|
||||
self.assertTrue(service.store.verify_event_chain(task["task_id"]))
|
||||
self.assertEqual(service.get_children(task["task_id"]), [])
|
||||
|
||||
|
||||
class ReadonlyCodexWorkerPolicyTests(unittest.TestCase):
|
||||
identity = "spiffe://aios/agent/codex/review-test-01"
|
||||
worker_id = "codex-review-test-01"
|
||||
capabilities = frozenset(
|
||||
{
|
||||
"code.read",
|
||||
"git.diff.read",
|
||||
"code.review",
|
||||
"review.security",
|
||||
"review.api-contract",
|
||||
"test.python",
|
||||
"test.shell.allowlisted",
|
||||
"task.progress",
|
||||
"task.result",
|
||||
"task.evidence.submit",
|
||||
}
|
||||
)
|
||||
|
||||
def setUp(self):
|
||||
profile = ReadonlyWorkerProfile(self.worker_id, self.identity, self.capabilities)
|
||||
policy = PolicyEngine(
|
||||
identity_verifier=IdentityVerifier({"anon", self.identity}),
|
||||
execution_enabled=False,
|
||||
readonly_worker_execution_enabled=True,
|
||||
readonly_worker_profiles=[profile],
|
||||
identity_capabilities={
|
||||
"anon": {"task.creator"},
|
||||
self.identity: {"worker.self_register", "task.claim"},
|
||||
},
|
||||
)
|
||||
self.service = TaskPlaneService(TaskPlaneStore(":memory:"), policy=policy)
|
||||
self.registration = {
|
||||
"worker_id": self.worker_id,
|
||||
"worker_type": "codex",
|
||||
"identity": self.identity,
|
||||
"capabilities": sorted(self.capabilities),
|
||||
"trust_zone": "isolated-review",
|
||||
"max_concurrency": 1,
|
||||
"heartbeat_interval_seconds": 15,
|
||||
"supported_protocols": ["aios-task-v1", "a2a"],
|
||||
"actor_identity": self.identity,
|
||||
}
|
||||
self.service.register_worker(self.registration)
|
||||
|
||||
def _task(self, *, task_type="code.review", capabilities=None, risk="low"):
|
||||
return self.service.create_task(
|
||||
{
|
||||
"task_type": task_type,
|
||||
"required_capabilities": capabilities or ["code.read", "code.review"],
|
||||
"risk": risk,
|
||||
"metadata": {"production_side_effects": False},
|
||||
}
|
||||
)
|
||||
|
||||
def test_readonly_profile_can_claim_only_safe_task(self):
|
||||
task = self._task()
|
||||
claim = self.service.claim(
|
||||
task["task_id"],
|
||||
{"worker_id": self.worker_id, "lease_seconds": 120, "actor_identity": self.identity},
|
||||
)
|
||||
self.assertTrue(claim["claim_id"])
|
||||
|
||||
def test_readonly_profile_rejects_wrong_worker_or_write_task(self):
|
||||
task = self._task()
|
||||
with self.assertRaises(Exception):
|
||||
self.service.claim(
|
||||
task["task_id"],
|
||||
{"worker_id": "other-worker", "lease_seconds": 120, "actor_identity": self.identity},
|
||||
)
|
||||
write_task = self._task(task_type="code.change", capabilities=["code.write"])
|
||||
with self.assertRaises(Exception):
|
||||
self.service.claim(
|
||||
write_task["task_id"],
|
||||
{"worker_id": self.worker_id, "lease_seconds": 120, "actor_identity": self.identity},
|
||||
)
|
||||
|
||||
def test_readonly_registration_cannot_escalate_capabilities(self):
|
||||
payload = dict(self.registration)
|
||||
payload["capabilities"] = [*payload["capabilities"], "git.commit"]
|
||||
with self.assertRaises(Exception):
|
||||
self.service.register_worker(payload)
|
||||
|
||||
|
||||
class DelegationGuardTests(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.store = TaskPlaneStore(":memory:")
|
||||
self.policy = _policy(execution_enabled=True)
|
||||
self.service = TaskPlaneService(self.store, policy=self.policy)
|
||||
self.parent = self.service.create_task(
|
||||
{
|
||||
"task_type": "code.change",
|
||||
"required_capabilities": ["code.read"],
|
||||
"budgets": {"attempts": 3, "tokens": 100, "time_seconds": 1000},
|
||||
"max_delegation_depth": 2,
|
||||
}
|
||||
)
|
||||
|
||||
def test_delegation_budget_and_depth(self):
|
||||
child = self.service.delegate(
|
||||
self.parent["task_id"],
|
||||
{
|
||||
"task_type": "code.change",
|
||||
"required_capabilities": ["code.read"],
|
||||
"budgets": {"attempts": 1},
|
||||
"actor_identity": "owner",
|
||||
},
|
||||
)
|
||||
self.assertEqual(child["plan"]["parent_task_id"], self.parent["task_id"])
|
||||
grandchild = self.service.delegate(
|
||||
child["task_id"],
|
||||
{
|
||||
"task_type": "code.change",
|
||||
"required_capabilities": ["code.read"],
|
||||
"budgets": {"attempts": 1},
|
||||
"actor_identity": "owner",
|
||||
},
|
||||
)
|
||||
with self.assertRaises(Exception):
|
||||
self.service.delegate(
|
||||
grandchild["task_id"],
|
||||
{
|
||||
"task_type": "code.change",
|
||||
"required_capabilities": ["code.read"],
|
||||
"budgets": {"attempts": 1},
|
||||
"actor_identity": "owner",
|
||||
},
|
||||
)
|
||||
@@ -0,0 +1,381 @@
|
||||
import json
|
||||
import http.client
|
||||
import os
|
||||
import tempfile
|
||||
import threading
|
||||
import time
|
||||
import unittest
|
||||
from http.server import ThreadingHTTPServer
|
||||
from unittest import mock
|
||||
|
||||
from app.task_plane.adapters.codex import CodexAdapter
|
||||
from app.task_plane.adapters.hermes import HermesAdapter
|
||||
from app.task_plane.adapters.openclaw import OpenClawAdapter
|
||||
from app.task_plane.api import TaskPlaneRequestHandler, TaskPlaneService
|
||||
from app.task_plane.errors import ConflictError, LockedError, ValidationError
|
||||
from app.task_plane.events import DurableOutboxDispatcher, WeChatGatewayAdapter
|
||||
from app.task_plane.models import ProbeEvidence, ProbeOutcome, TaskPlan
|
||||
from app.task_plane.policy import ALL_ACTION_CAPABILITIES, PolicyEngine
|
||||
from app.task_plane.store import TaskPlaneStore
|
||||
|
||||
|
||||
IDENTITY = "spiffe://aios/agent/codex/dev-01"
|
||||
|
||||
|
||||
def _policy(**kwargs):
|
||||
return PolicyEngine(
|
||||
identity_capabilities={"*": set(ALL_ACTION_CAPABILITIES)}, **kwargs
|
||||
)
|
||||
|
||||
|
||||
def register_worker(service):
|
||||
return service.register_worker(
|
||||
{
|
||||
"worker_id": "codex-dev-01",
|
||||
"worker_type": "codex",
|
||||
"identity": IDENTITY,
|
||||
"capabilities": ["code.read", "code.write.patch", "test.python"],
|
||||
"trust_zone": "isolated-development",
|
||||
"max_concurrency": 2,
|
||||
"heartbeat_interval_seconds": 15,
|
||||
"supported_protocols": ["aios-task-v1"],
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
class DurableStoreTests(unittest.TestCase):
|
||||
def test_idempotency_replay_and_payload_conflict(self):
|
||||
service = TaskPlaneService(TaskPlaneStore(":memory:"))
|
||||
first = service.create_task(
|
||||
{"task_id": "task-idempotent", "intent": "same"}, "idem-1"
|
||||
)
|
||||
replay = service.create_task(
|
||||
{"task_id": "task-idempotent", "intent": "same"}, "idem-1"
|
||||
)
|
||||
self.assertEqual(first["task_id"], replay["task_id"])
|
||||
with self.assertRaises(ConflictError):
|
||||
service.create_task(
|
||||
{"task_id": "task-other", "intent": "different"}, "idem-1"
|
||||
)
|
||||
|
||||
def test_cas_rejects_stale_revision(self):
|
||||
store = TaskPlaneStore(":memory:")
|
||||
task = store.create_task(TaskPlan.from_dict({"task_id": "task-cas"}))
|
||||
store.update_task_status(task.task_id, task.revision, "QUEUED")
|
||||
with self.assertRaises(ConflictError):
|
||||
store.update_task_status(task.task_id, task.revision, "CANCELLED")
|
||||
|
||||
def test_crash_recovery_preserves_task_events_and_outbox(self):
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
path = os.path.join(directory, "task-plane.sqlite3")
|
||||
first = TaskPlaneService(TaskPlaneStore(path))
|
||||
task = first.create_task({"task_id": "task-restart"})
|
||||
first.store.close()
|
||||
second = TaskPlaneService(TaskPlaneStore(path))
|
||||
self.assertEqual("CREATED", second.get_task(task["task_id"])["status"])
|
||||
self.assertTrue(second.store.verify_event_chain(task["task_id"]))
|
||||
self.assertEqual(1, len(second.store.list_outbox()))
|
||||
second.store.close()
|
||||
|
||||
def test_event_and_evidence_tamper_are_detected(self):
|
||||
store = TaskPlaneStore(":memory:")
|
||||
service = TaskPlaneService(store)
|
||||
task = service.create_task({"task_id": "task-tamper"})
|
||||
evidence_id = store.add_evidence(
|
||||
task["task_id"], "test-report", {"passed": True}, IDENTITY
|
||||
)
|
||||
store.conn.execute(
|
||||
"UPDATE events SET event_data=? WHERE task_id=?",
|
||||
(json.dumps({"changed": True}), task["task_id"]),
|
||||
)
|
||||
store.conn.execute(
|
||||
"UPDATE evidence SET payload=? WHERE evidence_id=?",
|
||||
(json.dumps({"passed": False}), evidence_id),
|
||||
)
|
||||
self.assertFalse(store.verify_event_chain(task["task_id"]))
|
||||
self.assertFalse(store.verify_evidence_integrity(task["task_id"]))
|
||||
|
||||
def test_secrets_are_redacted_at_rest_and_in_outbox(self):
|
||||
store = TaskPlaneStore(":memory:")
|
||||
service = TaskPlaneService(store)
|
||||
secret = "never-store-this-value"
|
||||
task = service.create_task(
|
||||
{
|
||||
"task_id": "task-redaction",
|
||||
"metadata": {"access_token": secret, "token_count": 12},
|
||||
}
|
||||
)
|
||||
raw_task = store.conn.execute(
|
||||
"SELECT task_json FROM tasks WHERE task_id=?", (task["task_id"],)
|
||||
).fetchone()[0]
|
||||
raw_outbox = store.conn.execute(
|
||||
"SELECT payload FROM outbox WHERE task_id=?", (task["task_id"],)
|
||||
).fetchone()[0]
|
||||
self.assertNotIn(secret, raw_task + raw_outbox)
|
||||
self.assertIn("[REDACTED]", raw_task)
|
||||
self.assertIn("token_count", raw_task)
|
||||
|
||||
|
||||
class ClaimLeaseTests(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.store = TaskPlaneStore(":memory:")
|
||||
self.service = TaskPlaneService(
|
||||
self.store, policy=_policy(execution_enabled=True)
|
||||
)
|
||||
register_worker(self.service)
|
||||
|
||||
def test_only_one_worker_can_claim_concurrently(self):
|
||||
task = self.service.create_task({"task_id": "task-race"})
|
||||
revision = self.store.get_task(task["task_id"]).revision
|
||||
outcomes = []
|
||||
barrier = threading.Barrier(2)
|
||||
|
||||
def attempt():
|
||||
barrier.wait()
|
||||
try:
|
||||
self.store.claim_task(
|
||||
task["task_id"],
|
||||
"codex-dev-01",
|
||||
IDENTITY,
|
||||
30,
|
||||
revision,
|
||||
self.store.get_task(task["task_id"]).task_plan.operation_digest(),
|
||||
)
|
||||
outcomes.append("claimed")
|
||||
except ConflictError:
|
||||
outcomes.append("conflict")
|
||||
|
||||
threads = [threading.Thread(target=attempt) for _ in range(2)]
|
||||
for thread in threads:
|
||||
thread.start()
|
||||
for thread in threads:
|
||||
thread.join()
|
||||
self.assertCountEqual(["claimed", "conflict"], outcomes)
|
||||
|
||||
def test_expired_lease_requires_probe_before_reassignment(self):
|
||||
task = self.service.create_task({"task_id": "task-expiry"})
|
||||
claim = self.service.claim(
|
||||
task["task_id"],
|
||||
{
|
||||
"worker_id": "codex-dev-01",
|
||||
"actor_identity": IDENTITY,
|
||||
"lease_seconds": 1,
|
||||
},
|
||||
)
|
||||
future = time.time() + 5
|
||||
with mock.patch("app.task_plane.store._now", return_value=future):
|
||||
self.store.probe_attestation_verifier = lambda probe: (
|
||||
probe.attestation_ref == "trusted://probe/recovery"
|
||||
)
|
||||
probe = ProbeEvidence.create(
|
||||
task_id=task["task_id"],
|
||||
claim_id=claim["claim_id"],
|
||||
worker_id="codex-dev-01",
|
||||
worker_identity=IDENTITY,
|
||||
operation_digest=claim["operation_digest"],
|
||||
outcome=ProbeOutcome.NOT_APPLIED,
|
||||
observed_at=future,
|
||||
expires_at=future + 60,
|
||||
probe_executor_identity="spiffe://aios/admin/recovery",
|
||||
attestation_ref="trusted://probe/recovery",
|
||||
)
|
||||
recovered = self.store.recover_claim_with_probe_and_event(
|
||||
task["task_id"],
|
||||
claim["claim_id"],
|
||||
probe,
|
||||
)
|
||||
self.assertEqual("FAILED_RECOVERABLE", recovered.status)
|
||||
retry = self.service.claim(
|
||||
task["task_id"],
|
||||
{
|
||||
"worker_id": "codex-dev-01",
|
||||
"actor_identity": IDENTITY,
|
||||
"lease_seconds": 30,
|
||||
},
|
||||
)
|
||||
self.assertEqual(2, retry["attempt"])
|
||||
|
||||
def test_wrong_claim_identity_and_duplicate_result_are_rejected(self):
|
||||
task = self.service.create_task({"task_id": "task-result"})
|
||||
claim = self.service.claim(
|
||||
task["task_id"],
|
||||
{"worker_id": "codex-dev-01", "actor_identity": IDENTITY},
|
||||
)
|
||||
with self.assertRaises(Exception):
|
||||
self.service.complete(
|
||||
task["task_id"],
|
||||
{
|
||||
"claim_id": claim["claim_id"],
|
||||
"actor_identity": "spiffe://aios/agent/codex/other",
|
||||
},
|
||||
)
|
||||
self.service.progress(
|
||||
task["task_id"],
|
||||
{
|
||||
"claim_id": claim["claim_id"],
|
||||
"actor_identity": IDENTITY,
|
||||
"progress": 10,
|
||||
"stage": "running",
|
||||
},
|
||||
)
|
||||
self.service.complete(
|
||||
task["task_id"],
|
||||
{"claim_id": claim["claim_id"], "actor_identity": IDENTITY},
|
||||
)
|
||||
with self.assertRaises(ConflictError):
|
||||
self.service.complete(
|
||||
task["task_id"],
|
||||
{"claim_id": claim["claim_id"], "actor_identity": IDENTITY},
|
||||
)
|
||||
|
||||
def test_tampered_result_digest_and_unverifiable_signature_fail_closed(self):
|
||||
for suffix, extra in (
|
||||
("digest", {"result_digest": "0" * 64}),
|
||||
("signature", {"signature": "tampered"}),
|
||||
):
|
||||
task = self.service.create_task({"task_id": f"task-{suffix}"})
|
||||
claim = self.service.claim(
|
||||
task["task_id"],
|
||||
{"worker_id": "codex-dev-01", "actor_identity": IDENTITY},
|
||||
)
|
||||
self.service.progress(
|
||||
task["task_id"],
|
||||
{
|
||||
"claim_id": claim["claim_id"],
|
||||
"actor_identity": IDENTITY,
|
||||
"progress": 10,
|
||||
"stage": "running",
|
||||
},
|
||||
)
|
||||
with self.assertRaises(Exception):
|
||||
self.service.complete(
|
||||
task["task_id"],
|
||||
{
|
||||
"claim_id": claim["claim_id"],
|
||||
"actor_identity": IDENTITY,
|
||||
**extra,
|
||||
},
|
||||
)
|
||||
self.assertEqual(
|
||||
"ACTIVE", self.store.get_claim(claim["claim_id"]).status
|
||||
)
|
||||
|
||||
|
||||
class DelegationAndNotificationTests(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.service = TaskPlaneService(
|
||||
TaskPlaneStore(":memory:"),
|
||||
policy=_policy(execution_enabled=True, max_risk="high"),
|
||||
)
|
||||
self.parent = self.service.create_task(
|
||||
{
|
||||
"task_id": "task-parent",
|
||||
"task_type": "code.review",
|
||||
"required_capabilities": ["code.read"],
|
||||
"risk": "medium",
|
||||
"approval_policy": "patch-only",
|
||||
"max_delegation_depth": 1,
|
||||
"budgets": {"attempts": 1, "tokens": 10},
|
||||
}
|
||||
)
|
||||
|
||||
def test_capability_risk_budget_and_cycle_escalation_are_rejected(self):
|
||||
base = {
|
||||
"actor_identity": "owner",
|
||||
"task_type": "code.review",
|
||||
"budgets": {"attempts": 1},
|
||||
}
|
||||
for payload in (
|
||||
{**base, "required_capabilities": ["git.force_push"]},
|
||||
{**base, "required_capabilities": ["code.read"], "risk": "high"},
|
||||
{**base, "required_capabilities": ["code.read"], "budgets": {"tokens": 11}},
|
||||
{
|
||||
**base,
|
||||
"task_id": "task-parent",
|
||||
"required_capabilities": ["code.read"],
|
||||
},
|
||||
):
|
||||
with self.assertRaises(Exception):
|
||||
self.service.delegate(self.parent["task_id"], payload)
|
||||
|
||||
def test_missing_gateway_receipt_remains_not_delivered(self):
|
||||
message = self.service.store.list_outbox()[0]
|
||||
dispatcher = DurableOutboxDispatcher(self.service.store)
|
||||
updated = dispatcher.dispatch(
|
||||
message["message_id"], WeChatGatewayAdapter(lambda envelope: None)
|
||||
)
|
||||
self.assertEqual("NOT_DELIVERED", updated["delivery_status"])
|
||||
delivered = self.service.store.record_outbox_delivery(
|
||||
message["message_id"], receipt="gateway-receipt-1"
|
||||
)
|
||||
replay = self.service.store.record_outbox_delivery(
|
||||
message["message_id"], receipt="gateway-receipt-1"
|
||||
)
|
||||
self.assertEqual("DELIVERED", delivered["delivery_status"])
|
||||
self.assertEqual(delivered["receipt"], replay["receipt"])
|
||||
|
||||
def test_worker_offline_detection_is_fail_closed(self):
|
||||
register_worker(self.service)
|
||||
worker = self.service.store.get_worker("codex-dev-01")
|
||||
after_deadline = (
|
||||
worker.last_heartbeat + worker.heartbeat_interval_seconds * 3 + 1
|
||||
)
|
||||
self.assertFalse(
|
||||
self.service.workers.is_online("codex-dev-01", now=after_deadline)
|
||||
)
|
||||
|
||||
def test_path_and_parameter_injection_are_rejected(self):
|
||||
for value in ("../task", "task/child", "%2e%2e", "task\ninjected", ""):
|
||||
with self.assertRaises(ValidationError):
|
||||
self.service.store.get_task(value)
|
||||
|
||||
def test_shadow_adapters_only_return_descriptions(self):
|
||||
task = TaskPlan.from_dict({"task_id": "task-shadow"})
|
||||
claim = type("Claim", (), {"claim_id": "claim-shadow", "status": "ACTIVE"})()
|
||||
for adapter in (OpenClawAdapter(), CodexAdapter(), HermesAdapter()):
|
||||
result = adapter.execute(task, claim)
|
||||
self.assertEqual("SHADOW_VERIFIED", result["status"])
|
||||
self.assertFalse(result["production_side_effects"])
|
||||
self.assertNotIn("command", result)
|
||||
|
||||
def test_http_rejects_encoded_path_and_non_object_json(self):
|
||||
TaskPlaneRequestHandler.service = self.service
|
||||
TaskPlaneRequestHandler.authenticator = None
|
||||
server = ThreadingHTTPServer(("127.0.0.1", 0), TaskPlaneRequestHandler)
|
||||
thread = threading.Thread(target=server.serve_forever, daemon=True)
|
||||
thread.start()
|
||||
try:
|
||||
connection = http.client.HTTPConnection(
|
||||
"127.0.0.1", server.server_address[1], timeout=3
|
||||
)
|
||||
connection.request("GET", "/tasks/%2e%2e")
|
||||
response = connection.getresponse()
|
||||
payload = response.read().decode()
|
||||
self.assertEqual(400, response.status)
|
||||
self.assertNotIn("Traceback", payload)
|
||||
connection.request(
|
||||
"POST",
|
||||
"/tasks",
|
||||
body="[]",
|
||||
headers={"Content-Type": "application/json"},
|
||||
)
|
||||
response = connection.getresponse()
|
||||
response.read()
|
||||
self.assertEqual(400, response.status)
|
||||
connection.request(
|
||||
"POST",
|
||||
"/tasks",
|
||||
body=json.dumps({"task_id": "task-http-unauthorized"}),
|
||||
headers={"Content-Type": "application/json"},
|
||||
)
|
||||
response = connection.getresponse()
|
||||
response.read()
|
||||
self.assertEqual(401, response.status)
|
||||
finally:
|
||||
server.shutdown()
|
||||
server.server_close()
|
||||
thread.join(timeout=3)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -0,0 +1,892 @@
|
||||
import dataclasses
|
||||
import http.client
|
||||
import json
|
||||
import multiprocessing
|
||||
import os
|
||||
import tempfile
|
||||
import threading
|
||||
import time
|
||||
import unittest
|
||||
from http.server import ThreadingHTTPServer
|
||||
|
||||
from app.task_plane.api import TaskPlaneRequestHandler, TaskPlaneService
|
||||
from app.task_plane.errors import (
|
||||
ConflictError,
|
||||
ForbiddenError,
|
||||
LockedError,
|
||||
ValidationError,
|
||||
)
|
||||
from app.task_plane.models import (
|
||||
EvidenceEnvelope,
|
||||
ProbeEvidence,
|
||||
ProbeOutcome,
|
||||
TaskPlan,
|
||||
)
|
||||
from app.task_plane.policy import ALL_ACTION_CAPABILITIES, PolicyEngine
|
||||
from app.task_plane.scheduler import TaskScheduler
|
||||
from app.task_plane.store import TaskPlaneStore
|
||||
|
||||
|
||||
WORKER_ID = "codex-secure-01"
|
||||
WORKER_IDENTITY = "spiffe://aios/agent/codex/secure-01"
|
||||
ADMIN_IDENTITY = "spiffe://aios/admin/task-plane"
|
||||
|
||||
|
||||
def _worker_payload(**overrides):
|
||||
payload = {
|
||||
"worker_id": WORKER_ID,
|
||||
"worker_type": "codex",
|
||||
"identity": WORKER_IDENTITY,
|
||||
"owner_identity": WORKER_IDENTITY,
|
||||
"capabilities": ["code.read", "test.python"],
|
||||
"trust_zone": "isolated-development",
|
||||
"max_concurrency": 1,
|
||||
"heartbeat_interval_seconds": 15,
|
||||
"supported_protocols": ["aios-task-v1"],
|
||||
"registration_policy": "self",
|
||||
"actor_identity": WORKER_IDENTITY,
|
||||
}
|
||||
payload.update(overrides)
|
||||
return payload
|
||||
|
||||
|
||||
def _service(store=None, **policy_kwargs):
|
||||
grants = {
|
||||
ADMIN_IDENTITY: {
|
||||
"task.creator",
|
||||
"task.read",
|
||||
"task.audit",
|
||||
"task.cancel",
|
||||
"task.delegate",
|
||||
"task.claim",
|
||||
"worker.admin",
|
||||
"worker.self_register",
|
||||
},
|
||||
WORKER_IDENTITY: {
|
||||
"task.read",
|
||||
"task.claim",
|
||||
"worker.self_register",
|
||||
},
|
||||
}
|
||||
return TaskPlaneService(
|
||||
store or TaskPlaneStore(":memory:"),
|
||||
policy=PolicyEngine(
|
||||
execution_enabled=True,
|
||||
identity_capabilities=grants,
|
||||
admin_identities={ADMIN_IDENTITY},
|
||||
**policy_kwargs,
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def _claim_for_service(service, *, task_id="task-secure", plan=None):
|
||||
plan = plan or {
|
||||
"task_id": task_id,
|
||||
"issuer": {"type": "agent", "id": ADMIN_IDENTITY},
|
||||
"required_capabilities": ["code.read"],
|
||||
"risk": "low",
|
||||
}
|
||||
service.register_worker(
|
||||
_worker_payload(
|
||||
capabilities=sorted(
|
||||
{"code.read", "test.python", *plan.get("required_capabilities", [])}
|
||||
)
|
||||
)
|
||||
)
|
||||
task = service.create_task(plan)
|
||||
claim = service.claim(
|
||||
task["task_id"],
|
||||
{"worker_id": WORKER_ID, "actor_identity": WORKER_IDENTITY},
|
||||
)
|
||||
return task, claim
|
||||
|
||||
|
||||
def _process_claim(db_path, task_id, expected_revision, digest, queue):
|
||||
store = TaskPlaneStore(db_path)
|
||||
try:
|
||||
store.claim_task(
|
||||
task_id,
|
||||
WORKER_ID,
|
||||
WORKER_IDENTITY,
|
||||
30,
|
||||
expected_revision,
|
||||
digest,
|
||||
)
|
||||
queue.put("claimed")
|
||||
except Exception as exc:
|
||||
queue.put(type(exc).__name__)
|
||||
finally:
|
||||
store.close()
|
||||
|
||||
|
||||
class EvidenceEnvelopeContractTests(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.base = EvidenceEnvelope.create(
|
||||
task_id="task-evidence",
|
||||
claim_id="claim-evidence",
|
||||
kind="test-report",
|
||||
worker_identity=WORKER_IDENTITY,
|
||||
payload={"passed": True},
|
||||
previous_evidence_hash="",
|
||||
attestation_ref="trusted://shadow/test-report",
|
||||
)
|
||||
|
||||
def test_digest_binds_complete_envelope_and_order(self):
|
||||
self.assertTrue(
|
||||
self.base.verify(
|
||||
expected_task_id="task-evidence",
|
||||
expected_claim_id="claim-evidence",
|
||||
expected_previous_hash="",
|
||||
)
|
||||
)
|
||||
mutations = {
|
||||
"kind": "secret-scan",
|
||||
"worker_identity": "spiffe://attacker",
|
||||
"task_id": "task-other",
|
||||
"claim_id": "claim-other",
|
||||
"created_at": self.base.created_at + 1,
|
||||
"payload": {"passed": False},
|
||||
"previous_evidence_hash": "f" * 64,
|
||||
}
|
||||
for field, value in mutations.items():
|
||||
with self.subTest(field=field):
|
||||
self.assertFalse(
|
||||
dataclasses.replace(self.base, **{field: value}).verify(
|
||||
expected_task_id="task-evidence",
|
||||
expected_claim_id="claim-evidence",
|
||||
expected_previous_hash="",
|
||||
)
|
||||
)
|
||||
|
||||
def test_store_rejects_cross_task_required_evidence_and_chain_tamper(self):
|
||||
store = TaskPlaneStore(":memory:")
|
||||
store.create_task_with_event(
|
||||
TaskPlan.from_dict({"task_id": "task-evidence"})
|
||||
)
|
||||
store.add_evidence_envelope(self.base)
|
||||
self.assertTrue(store.verify_evidence_integrity("task-evidence"))
|
||||
store.conn.execute(
|
||||
"UPDATE evidence SET envelope_json=json_set(envelope_json,'$.task_id','task-other')"
|
||||
)
|
||||
self.assertFalse(store.verify_evidence_integrity("task-evidence"))
|
||||
|
||||
|
||||
class AtomicMutationContractTests(unittest.TestCase):
|
||||
def _fault(self, point):
|
||||
def inject(observed):
|
||||
if observed == point:
|
||||
raise RuntimeError("injected fault")
|
||||
|
||||
return inject
|
||||
|
||||
def test_create_cancel_and_delegate_roll_back_at_event_boundaries(self):
|
||||
store = TaskPlaneStore(":memory:")
|
||||
store.fault_injector = self._fault("create.after_task_before_event")
|
||||
with self.assertRaises(RuntimeError):
|
||||
store.create_task_with_event(TaskPlan.from_dict({"task_id": "task-create"}))
|
||||
self.assertEqual([], store.list_tasks())
|
||||
|
||||
store.fault_injector = None
|
||||
parent = store.create_task_with_event(
|
||||
TaskPlan.from_dict(
|
||||
{
|
||||
"task_id": "task-parent-atomic",
|
||||
"required_capabilities": ["code.read"],
|
||||
"budgets": {"attempts": 2},
|
||||
}
|
||||
)
|
||||
)
|
||||
store.fault_injector = self._fault("cancel.after_state_before_event")
|
||||
with self.assertRaises(RuntimeError):
|
||||
store.cancel_task_with_event(parent.task_id, parent.revision, "stop")
|
||||
self.assertEqual("CREATED", store.get_task(parent.task_id).status)
|
||||
|
||||
store.fault_injector = self._fault("delegate.after_budget_before_child")
|
||||
with self.assertRaises(RuntimeError):
|
||||
store.delegate_task_with_budget_and_events(
|
||||
parent.task_id,
|
||||
TaskPlan.from_dict(
|
||||
{
|
||||
"task_id": "task-child-atomic",
|
||||
"required_capabilities": ["code.read"],
|
||||
"budgets": {"attempts": 1},
|
||||
}
|
||||
),
|
||||
caller=ADMIN_IDENTITY,
|
||||
)
|
||||
self.assertEqual(2, store.get_task_budget(parent.task_id).attempts)
|
||||
self.assertEqual([], store.list_children(parent.task_id))
|
||||
|
||||
def test_complete_fail_and_recover_roll_back_wholly(self):
|
||||
service = _service()
|
||||
task, claim = _claim_for_service(service, task_id="task-complete-atomic")
|
||||
service.progress(
|
||||
task["task_id"],
|
||||
{
|
||||
"claim_id": claim["claim_id"],
|
||||
"actor_identity": WORKER_IDENTITY,
|
||||
"progress": 10,
|
||||
"stage": "running",
|
||||
},
|
||||
)
|
||||
service.store.fault_injector = self._fault(
|
||||
"complete.after_evidence_before_state"
|
||||
)
|
||||
envelope = EvidenceEnvelope.create(
|
||||
task_id=task["task_id"],
|
||||
claim_id=claim["claim_id"],
|
||||
kind="test-report",
|
||||
worker_identity=WORKER_IDENTITY,
|
||||
payload={"passed": True},
|
||||
previous_evidence_hash="",
|
||||
attestation_ref="trusted://shadow/test",
|
||||
)
|
||||
with self.assertRaises(RuntimeError):
|
||||
service.store.complete_claim_with_evidence_and_event(
|
||||
task["task_id"],
|
||||
claim["claim_id"],
|
||||
"COMPLETED",
|
||||
{"status": "COMPLETED"},
|
||||
[envelope],
|
||||
)
|
||||
self.assertEqual([], service.store.list_evidence(task["task_id"]))
|
||||
self.assertEqual("RUNNING", service.store.get_task(task["task_id"]).status)
|
||||
|
||||
def test_fail_rolls_back_claim_state_and_event(self):
|
||||
service = _service()
|
||||
task, claim = _claim_for_service(service, task_id="task-fail-atomic")
|
||||
service.store.fault_injector = self._fault("fail.after_state_before_commit")
|
||||
with self.assertRaises(RuntimeError):
|
||||
service.store.fail_claim_with_event(
|
||||
task["task_id"], claim["claim_id"], "FAILED_FINAL", "injected"
|
||||
)
|
||||
self.assertEqual("ACTIVE", service.store.get_claim(claim["claim_id"]).status)
|
||||
self.assertEqual("CLAIMED", service.store.get_task(task["task_id"]).status)
|
||||
self.assertNotIn(
|
||||
"TASK_FAILED",
|
||||
{
|
||||
event["type"]
|
||||
for event in service.store.list_events(task["task_id"])
|
||||
},
|
||||
)
|
||||
|
||||
def test_recovery_rolls_back_probe_state_and_event(self):
|
||||
service = _service(
|
||||
probe_attestation_verifier=lambda probe: probe.attestation_ref
|
||||
== "trusted://probe/atomic"
|
||||
)
|
||||
task, claim = _claim_for_service(service, task_id="task-recover-atomic")
|
||||
service.store.conn.execute(
|
||||
"UPDATE claims SET lease_until=? WHERE claim_id=?",
|
||||
(time.time() - 10, claim["claim_id"]),
|
||||
)
|
||||
probe = ProbeEvidence.create(
|
||||
task_id=task["task_id"],
|
||||
claim_id=claim["claim_id"],
|
||||
worker_id=WORKER_ID,
|
||||
worker_identity=WORKER_IDENTITY,
|
||||
operation_digest=claim["operation_digest"],
|
||||
outcome=ProbeOutcome.NOT_APPLIED,
|
||||
observed_at=time.time(),
|
||||
expires_at=time.time() + 60,
|
||||
probe_executor_identity=ADMIN_IDENTITY,
|
||||
attestation_ref="trusted://probe/atomic",
|
||||
)
|
||||
service.store.fault_injector = self._fault(
|
||||
"recover.after_probe_before_state"
|
||||
)
|
||||
with self.assertRaises(RuntimeError):
|
||||
service.store.recover_claim_with_probe_and_event(
|
||||
task["task_id"], claim["claim_id"], probe
|
||||
)
|
||||
self.assertEqual(
|
||||
0,
|
||||
service.store.conn.execute(
|
||||
"SELECT COUNT(*) FROM probe_evidence WHERE probe_id=?",
|
||||
(probe.probe_id,),
|
||||
).fetchone()[0],
|
||||
)
|
||||
self.assertEqual("ACTIVE", service.store.get_claim(claim["claim_id"]).status)
|
||||
self.assertEqual("CLAIMED", service.store.get_task(task["task_id"]).status)
|
||||
|
||||
|
||||
class WorkerOwnershipContractTests(unittest.TestCase):
|
||||
def test_identity_takeover_escalation_and_disabled_reregistration_rejected(self):
|
||||
service = _service()
|
||||
registered = service.register_worker(_worker_payload())
|
||||
revision = registered["registration_revision"]
|
||||
with self.assertRaises(ConflictError):
|
||||
service.register_worker(
|
||||
_worker_payload(
|
||||
identity="spiffe://aios/attacker",
|
||||
owner_identity="spiffe://aios/attacker",
|
||||
actor_identity="spiffe://aios/attacker",
|
||||
expected_revision=revision,
|
||||
)
|
||||
)
|
||||
with self.assertRaises(ForbiddenError):
|
||||
service.register_worker(
|
||||
_worker_payload(
|
||||
capabilities=["code.read", "deploy.production"],
|
||||
expected_revision=revision,
|
||||
)
|
||||
)
|
||||
disabled = service.disable_worker(
|
||||
WORKER_ID,
|
||||
{
|
||||
"actor_identity": ADMIN_IDENTITY,
|
||||
"expected_revision": revision,
|
||||
"reason": "security review",
|
||||
},
|
||||
)
|
||||
with self.assertRaises(LockedError):
|
||||
service.register_worker(
|
||||
_worker_payload(expected_revision=disabled["registration_revision"])
|
||||
)
|
||||
|
||||
def test_worker_cannot_heartbeat_other_identity_and_all_updates_use_cas(self):
|
||||
service = _service()
|
||||
worker = service.register_worker(_worker_payload())
|
||||
with self.assertRaises(ForbiddenError):
|
||||
service.worker_heartbeat(
|
||||
WORKER_ID,
|
||||
{
|
||||
"actor_identity": "spiffe://aios/attacker",
|
||||
"expected_revision": worker["registration_revision"],
|
||||
},
|
||||
)
|
||||
with self.assertRaises(ConflictError):
|
||||
service.worker_heartbeat(
|
||||
WORKER_ID,
|
||||
{
|
||||
"actor_identity": WORKER_IDENTITY,
|
||||
"expected_revision": worker["registration_revision"] + 1,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
class RecoveryProbeContractTests(unittest.TestCase):
|
||||
def _expired_claim(self, suffix):
|
||||
service = _service(
|
||||
probe_attestation_verifier=lambda probe: probe.attestation_ref
|
||||
== "trusted://probe/test"
|
||||
)
|
||||
task, claim = _claim_for_service(service, task_id=f"task-probe-{suffix}")
|
||||
service.store.conn.execute(
|
||||
"UPDATE claims SET lease_until=? WHERE claim_id=?",
|
||||
(time.time() - 10, claim["claim_id"]),
|
||||
)
|
||||
return service, task, claim
|
||||
|
||||
def _probe(self, task, claim, outcome, **overrides):
|
||||
data = {
|
||||
"task_id": task["task_id"],
|
||||
"claim_id": claim["claim_id"],
|
||||
"worker_id": WORKER_ID,
|
||||
"worker_identity": WORKER_IDENTITY,
|
||||
"operation_digest": claim["operation_digest"],
|
||||
"outcome": outcome,
|
||||
"observed_at": time.time(),
|
||||
"expires_at": time.time() + 60,
|
||||
"probe_executor_identity": ADMIN_IDENTITY,
|
||||
"attestation_ref": "trusted://probe/test",
|
||||
}
|
||||
data.update(overrides)
|
||||
return ProbeEvidence.create(**data)
|
||||
|
||||
def test_four_probe_outcomes_have_distinct_fail_closed_transitions(self):
|
||||
expected = {
|
||||
ProbeOutcome.NOT_APPLIED: "FAILED_RECOVERABLE",
|
||||
ProbeOutcome.APPLIED_VERIFIED: "COMPLETED",
|
||||
ProbeOutcome.INDETERMINATE: "FAILED_RECOVERABLE",
|
||||
ProbeOutcome.CONFLICT: "MANUAL_REVIEW_REQUIRED",
|
||||
}
|
||||
for outcome, status in expected.items():
|
||||
with self.subTest(outcome=outcome):
|
||||
service, task, claim = self._expired_claim(outcome.value.lower())
|
||||
recovered = service.store.recover_claim_with_probe_and_event(
|
||||
task["task_id"],
|
||||
claim["claim_id"],
|
||||
self._probe(task, claim, outcome),
|
||||
)
|
||||
self.assertEqual(status, recovered.status)
|
||||
if outcome == ProbeOutcome.INDETERMINATE:
|
||||
self.assertFalse(
|
||||
service.store.task_has_retryable_probe(task["task_id"])
|
||||
)
|
||||
|
||||
def test_forged_expired_or_mismatched_probe_is_rejected(self):
|
||||
service, task, claim = self._expired_claim("invalid")
|
||||
valid = self._probe(task, claim, ProbeOutcome.NOT_APPLIED)
|
||||
for probe in (
|
||||
dataclasses.replace(valid, evidence_digest="0" * 64),
|
||||
self._probe(
|
||||
task,
|
||||
claim,
|
||||
ProbeOutcome.NOT_APPLIED,
|
||||
expires_at=time.time() - 1,
|
||||
),
|
||||
self._probe(
|
||||
task,
|
||||
claim,
|
||||
ProbeOutcome.NOT_APPLIED,
|
||||
operation_digest="f" * 64,
|
||||
),
|
||||
):
|
||||
with self.assertRaises((ForbiddenError, ConflictError, LockedError)):
|
||||
service.store.recover_claim_with_probe_and_event(
|
||||
task["task_id"], claim["claim_id"], probe
|
||||
)
|
||||
|
||||
|
||||
class SchedulerAndClassificationTests(unittest.TestCase):
|
||||
def test_scheduler_skips_incompatible_and_dependency_blocked_tasks(self):
|
||||
service = _service()
|
||||
service.register_worker(_worker_payload())
|
||||
service.create_task(
|
||||
{
|
||||
"task_id": "task-incompatible",
|
||||
"issuer": {"type": "agent", "id": ADMIN_IDENTITY},
|
||||
"required_capabilities": ["gpu.train"],
|
||||
}
|
||||
)
|
||||
service.create_task(
|
||||
{
|
||||
"task_id": "task-dependency",
|
||||
"issuer": {"type": "agent", "id": ADMIN_IDENTITY},
|
||||
}
|
||||
)
|
||||
service.create_task(
|
||||
{
|
||||
"task_id": "task-blocked",
|
||||
"issuer": {"type": "agent", "id": ADMIN_IDENTITY},
|
||||
"required_capabilities": ["code.read"],
|
||||
"dependencies": ["task-dependency"],
|
||||
}
|
||||
)
|
||||
service.create_task(
|
||||
{
|
||||
"task_id": "task-eligible",
|
||||
"issuer": {"type": "agent", "id": ADMIN_IDENTITY},
|
||||
"required_capabilities": ["code.read"],
|
||||
"metadata": {"priority": 100},
|
||||
}
|
||||
)
|
||||
claim = TaskScheduler(service.store).schedule_for_worker(WORKER_ID)
|
||||
self.assertEqual("task-eligible", claim.task_id)
|
||||
self.assertEqual(WORKER_IDENTITY, claim.worker_identity)
|
||||
|
||||
def test_credential_secret_rejects_generic_worker_and_redacts_free_text(self):
|
||||
service = _service()
|
||||
service.register_worker(_worker_payload())
|
||||
task = service.create_task(
|
||||
{
|
||||
"task_id": "task-credential-secret",
|
||||
"issuer": {"type": "agent", "id": ADMIN_IDENTITY},
|
||||
"required_capabilities": ["secret.transform"],
|
||||
"data_classification": "credential-secret",
|
||||
"execution_zone": "secure-file-transformer",
|
||||
"network_policy": "denied",
|
||||
"logging_policy": "metadata-only",
|
||||
"output_policy": "hash-count-status-only",
|
||||
}
|
||||
)
|
||||
with self.assertRaises(ForbiddenError):
|
||||
service.claim(
|
||||
task["task_id"],
|
||||
{"worker_id": WORKER_ID, "actor_identity": WORKER_IDENTITY},
|
||||
)
|
||||
with self.assertRaises(ValueError):
|
||||
TaskPlan.from_dict(
|
||||
{
|
||||
"task_id": "task-secret-leak",
|
||||
"intent": "Authorization: Bearer super-secret-value",
|
||||
"data_classification": "credential-secret",
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
class ReadRBACAndResultProofTests(unittest.TestCase):
|
||||
def test_get_evidence_requires_authentication(self):
|
||||
service = _service()
|
||||
service.create_task(
|
||||
{
|
||||
"task_id": "task-http-read",
|
||||
"issuer": {"type": "agent", "id": ADMIN_IDENTITY},
|
||||
}
|
||||
)
|
||||
TaskPlaneRequestHandler.service = service
|
||||
TaskPlaneRequestHandler.authenticator = lambda token: (
|
||||
ADMIN_IDENTITY if token == "valid" else None
|
||||
)
|
||||
server = ThreadingHTTPServer(("127.0.0.1", 0), TaskPlaneRequestHandler)
|
||||
thread = threading.Thread(target=server.serve_forever, daemon=True)
|
||||
thread.start()
|
||||
try:
|
||||
connection = http.client.HTTPConnection(
|
||||
"127.0.0.1", server.server_address[1], timeout=3
|
||||
)
|
||||
connection.request("GET", "/tasks/task-http-read/evidence")
|
||||
response = connection.getresponse()
|
||||
response.read()
|
||||
self.assertEqual(401, response.status)
|
||||
connection.request(
|
||||
"GET",
|
||||
"/tasks/task-http-read/evidence",
|
||||
headers={"Authorization": "Bearer valid"},
|
||||
)
|
||||
response = connection.getresponse()
|
||||
response.read()
|
||||
self.assertEqual(200, response.status)
|
||||
finally:
|
||||
server.shutdown()
|
||||
server.server_close()
|
||||
thread.join(timeout=3)
|
||||
|
||||
def test_sensitive_result_requires_worker_proof_but_low_shadow_is_marked(self):
|
||||
service = _service(result_signature_verifier=lambda result: False)
|
||||
task, claim = _claim_for_service(
|
||||
service,
|
||||
task_id="task-write-proof",
|
||||
plan={
|
||||
"task_id": "task-write-proof",
|
||||
"issuer": {"type": "agent", "id": ADMIN_IDENTITY},
|
||||
"required_capabilities": ["code.read", "code.write.patch"],
|
||||
"risk": "medium",
|
||||
},
|
||||
)
|
||||
service.progress(
|
||||
task["task_id"],
|
||||
{
|
||||
"claim_id": claim["claim_id"],
|
||||
"actor_identity": WORKER_IDENTITY,
|
||||
"progress": 100,
|
||||
"stage": "done",
|
||||
},
|
||||
)
|
||||
with self.assertRaises(ForbiddenError):
|
||||
service.complete(
|
||||
task["task_id"],
|
||||
{
|
||||
"claim_id": claim["claim_id"],
|
||||
"actor_identity": WORKER_IDENTITY,
|
||||
"signature": "invalid",
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
class SQLiteAndEventIntegrityTests(unittest.TestCase):
|
||||
def test_event_hash_binds_metadata(self):
|
||||
store = TaskPlaneStore(":memory:")
|
||||
task = store.create_task_with_event(
|
||||
TaskPlan.from_dict({"task_id": "task-event-metadata"})
|
||||
)
|
||||
self.assertTrue(store.verify_event_chain(task.task_id))
|
||||
for column, value in (
|
||||
("event_id", "evt-forged"),
|
||||
("subject", "task/other"),
|
||||
("created_at", 1.0),
|
||||
):
|
||||
with self.subTest(column=column):
|
||||
original = dict(
|
||||
store.conn.execute(
|
||||
"SELECT * FROM events WHERE task_id=?", (task.task_id,)
|
||||
).fetchone()
|
||||
)
|
||||
store.conn.execute(
|
||||
f"UPDATE events SET {column}=? WHERE task_id=?",
|
||||
(value, task.task_id),
|
||||
)
|
||||
self.assertFalse(store.verify_event_chain(task.task_id))
|
||||
store.conn.execute(
|
||||
f"UPDATE events SET {column}=? WHERE task_id=?",
|
||||
(original[column], task.task_id),
|
||||
)
|
||||
|
||||
def test_sqlite_high_assurance_schema_integrity_checkpoint_and_backup(self):
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
path = os.path.join(directory, "task-plane.sqlite3")
|
||||
store = TaskPlaneStore(path, high_assurance=True)
|
||||
self.assertGreaterEqual(store.schema_version(), 2)
|
||||
self.assertEqual(5000, store.conn.execute("PRAGMA busy_timeout").fetchone()[0])
|
||||
self.assertEqual(2, store.conn.execute("PRAGMA synchronous").fetchone()[0])
|
||||
self.assertEqual("ok", store.integrity_check())
|
||||
store.create_task_with_event(
|
||||
TaskPlan.from_dict({"task_id": "task-backup"})
|
||||
)
|
||||
backup = os.path.join(directory, "backup.sqlite3")
|
||||
store.checkpoint_and_backup(backup)
|
||||
self.assertTrue(os.path.exists(backup))
|
||||
store.close()
|
||||
|
||||
def test_multi_process_claim_race_has_single_winner(self):
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
path = os.path.join(directory, "task-plane.sqlite3")
|
||||
store = TaskPlaneStore(path)
|
||||
service = _service(store)
|
||||
service.register_worker(_worker_payload(max_concurrency=2))
|
||||
task = service.create_task(
|
||||
{
|
||||
"task_id": "task-multiprocess",
|
||||
"issuer": {"type": "agent", "id": ADMIN_IDENTITY},
|
||||
"required_capabilities": ["code.read"],
|
||||
}
|
||||
)
|
||||
record = store.get_task(task["task_id"])
|
||||
queue = multiprocessing.Queue()
|
||||
processes = [
|
||||
multiprocessing.Process(
|
||||
target=_process_claim,
|
||||
args=(
|
||||
path,
|
||||
task["task_id"],
|
||||
record.revision,
|
||||
record.task_plan.operation_digest(),
|
||||
queue,
|
||||
),
|
||||
)
|
||||
for _ in range(2)
|
||||
]
|
||||
for process in processes:
|
||||
process.start()
|
||||
for process in processes:
|
||||
process.join(10)
|
||||
outcomes = [queue.get(timeout=3) for _ in processes]
|
||||
self.assertEqual(1, outcomes.count("claimed"))
|
||||
store.close()
|
||||
|
||||
|
||||
class ExplicitSecurityMatrixTests(unittest.TestCase):
|
||||
def test_evidence_kind_tamper_is_rejected(self):
|
||||
envelope = EvidenceEnvelope.create(
|
||||
task_id="task-kind",
|
||||
claim_id="claim-kind",
|
||||
kind="test-report",
|
||||
worker_identity=WORKER_IDENTITY,
|
||||
payload={"passed": True},
|
||||
previous_evidence_hash="",
|
||||
)
|
||||
self.assertFalse(
|
||||
dataclasses.replace(envelope, kind="forged").verify(
|
||||
expected_task_id="task-kind",
|
||||
expected_claim_id="claim-kind",
|
||||
expected_previous_hash="",
|
||||
)
|
||||
)
|
||||
|
||||
def test_worker_admin_restore_is_cas_and_journaled(self):
|
||||
service = _service()
|
||||
worker = service.register_worker(_worker_payload())
|
||||
disabled = service.disable_worker(
|
||||
WORKER_ID,
|
||||
{
|
||||
"actor_identity": ADMIN_IDENTITY,
|
||||
"expected_revision": worker["registration_revision"],
|
||||
"reason": "test",
|
||||
},
|
||||
)
|
||||
restored = service.restore_worker(
|
||||
WORKER_ID,
|
||||
{
|
||||
"actor_identity": ADMIN_IDENTITY,
|
||||
"expected_revision": disabled["registration_revision"],
|
||||
},
|
||||
)
|
||||
self.assertFalse(restored["disabled"])
|
||||
events = service.store.list_events("task-worker-registry")
|
||||
self.assertIn("WORKER_RESTORED", {event["type"] for event in events})
|
||||
|
||||
def test_disabled_worker_heartbeat_is_rejected(self):
|
||||
service = _service()
|
||||
worker = service.register_worker(_worker_payload())
|
||||
disabled = service.disable_worker(
|
||||
WORKER_ID,
|
||||
{
|
||||
"actor_identity": ADMIN_IDENTITY,
|
||||
"expected_revision": worker["registration_revision"],
|
||||
"reason": "test",
|
||||
},
|
||||
)
|
||||
with self.assertRaises(LockedError):
|
||||
service.worker_heartbeat(
|
||||
WORKER_ID,
|
||||
{
|
||||
"actor_identity": WORKER_IDENTITY,
|
||||
"expected_revision": disabled["registration_revision"],
|
||||
},
|
||||
)
|
||||
|
||||
def test_low_risk_unsigned_result_is_explicitly_untrusted(self):
|
||||
service = _service()
|
||||
task, claim = _claim_for_service(service, task_id="task-low-shadow")
|
||||
service.progress(
|
||||
task["task_id"],
|
||||
{
|
||||
"claim_id": claim["claim_id"],
|
||||
"actor_identity": WORKER_IDENTITY,
|
||||
"progress": 100,
|
||||
"stage": "done",
|
||||
},
|
||||
)
|
||||
result = service.complete(
|
||||
task["task_id"],
|
||||
{
|
||||
"claim_id": claim["claim_id"],
|
||||
"actor_identity": WORKER_IDENTITY,
|
||||
},
|
||||
)
|
||||
self.assertEqual("untrusted_shadow_result", result["trust_status"])
|
||||
|
||||
def test_boolean_recovery_contract_is_rejected(self):
|
||||
service = _service()
|
||||
task, claim = _claim_for_service(service, task_id="task-no-bool-probe")
|
||||
service.store.conn.execute(
|
||||
"UPDATE claims SET lease_until=? WHERE claim_id=?",
|
||||
(time.time() - 10, claim["claim_id"]),
|
||||
)
|
||||
with self.assertRaises(ValidationError):
|
||||
service.store.recover_expired_claim(
|
||||
task["task_id"], claim["claim_id"], True
|
||||
)
|
||||
|
||||
def test_scheduler_rejects_offline_worker(self):
|
||||
service = _service()
|
||||
service.register_worker(_worker_payload())
|
||||
service.create_task(
|
||||
{
|
||||
"task_id": "task-offline",
|
||||
"issuer": {"type": "agent", "id": ADMIN_IDENTITY},
|
||||
"required_capabilities": ["code.read"],
|
||||
}
|
||||
)
|
||||
service.store.conn.execute(
|
||||
"UPDATE workers SET last_heartbeat=? WHERE worker_id=?",
|
||||
(time.time() - 1000, WORKER_ID),
|
||||
)
|
||||
with self.assertRaises(LockedError):
|
||||
TaskScheduler(service.store).schedule_for_worker(WORKER_ID)
|
||||
|
||||
def test_schedule_decision_event_is_single_for_claim(self):
|
||||
service = _service()
|
||||
service.register_worker(_worker_payload())
|
||||
service.create_task(
|
||||
{
|
||||
"task_id": "task-schedule-event",
|
||||
"issuer": {"type": "agent", "id": ADMIN_IDENTITY},
|
||||
"required_capabilities": ["code.read"],
|
||||
}
|
||||
)
|
||||
TaskScheduler(service.store).schedule_for_worker(WORKER_ID)
|
||||
events = service.store.list_events("task-schedule-event")
|
||||
self.assertEqual(
|
||||
1, sum(event["type"] == "SCHEDULE_DECISION" for event in events)
|
||||
)
|
||||
|
||||
def _credential_service_and_claim(self):
|
||||
identity = "spiffe://aios/agent/secure-file-transformer/01"
|
||||
service = TaskPlaneService(
|
||||
TaskPlaneStore(":memory:"),
|
||||
policy=PolicyEngine(
|
||||
execution_enabled=True,
|
||||
identity_capabilities={"*": set(ALL_ACTION_CAPABILITIES)},
|
||||
),
|
||||
)
|
||||
service.register_worker(
|
||||
{
|
||||
"worker_id": "secure-transformer-01",
|
||||
"worker_type": "secure_file_transformer",
|
||||
"identity": identity,
|
||||
"capabilities": ["secret.transform", "secure_file_transformer"],
|
||||
"trust_zone": "secure",
|
||||
"max_concurrency": 1,
|
||||
"heartbeat_interval_seconds": 15,
|
||||
"supported_protocols": ["aios-task-v1"],
|
||||
}
|
||||
)
|
||||
task = service.create_task(
|
||||
{
|
||||
"task_id": "task-secure-output",
|
||||
"required_capabilities": ["secret.transform"],
|
||||
"data_classification": "credential-secret",
|
||||
"execution_zone": "secure-file-transformer",
|
||||
"network_policy": "denied",
|
||||
"logging_policy": "metadata-only",
|
||||
"output_policy": "hash-count-status-only",
|
||||
}
|
||||
)
|
||||
claim = service.claim(
|
||||
task["task_id"],
|
||||
{"worker_id": "secure-transformer-01", "actor_identity": identity},
|
||||
)
|
||||
return service, task, claim, identity
|
||||
|
||||
def test_credential_progress_free_text_is_rejected(self):
|
||||
service, task, claim, identity = self._credential_service_and_claim()
|
||||
with self.assertRaises(ValidationError):
|
||||
service.progress(
|
||||
task["task_id"],
|
||||
{
|
||||
"claim_id": claim["claim_id"],
|
||||
"actor_identity": identity,
|
||||
"progress": 10,
|
||||
"stage": "running",
|
||||
"message": "Authorization: Bearer forbidden",
|
||||
},
|
||||
)
|
||||
|
||||
def test_credential_result_content_is_rejected(self):
|
||||
service, task, claim, identity = self._credential_service_and_claim()
|
||||
service.progress(
|
||||
task["task_id"],
|
||||
{
|
||||
"claim_id": claim["claim_id"],
|
||||
"actor_identity": identity,
|
||||
"progress": 100,
|
||||
"stage": "done",
|
||||
},
|
||||
)
|
||||
with self.assertRaises(ValidationError):
|
||||
service.complete(
|
||||
task["task_id"],
|
||||
{
|
||||
"claim_id": claim["claim_id"],
|
||||
"actor_identity": identity,
|
||||
"outputs": [{"content": "forbidden"}],
|
||||
},
|
||||
)
|
||||
|
||||
def test_action_rbac_denies_read_without_task_read(self):
|
||||
store = TaskPlaneStore(":memory:")
|
||||
service = TaskPlaneService(
|
||||
store,
|
||||
policy=PolicyEngine(
|
||||
identity_capabilities={"spiffe://reader/none": set()}
|
||||
),
|
||||
)
|
||||
store.create_task_with_event(TaskPlan.from_dict({"task_id": "task-rbac"}))
|
||||
with self.assertRaises(ForbiddenError):
|
||||
service.get_task("task-rbac", "spiffe://reader/none")
|
||||
|
||||
def test_default_service_policy_denies_authenticated_read(self):
|
||||
service = TaskPlaneService(TaskPlaneStore(":memory:"))
|
||||
service.create_task({"task_id": "task-default-deny"})
|
||||
with self.assertRaises(ForbiddenError):
|
||||
service.get_task("task-default-deny", "spiffe://reader/unconfigured")
|
||||
|
||||
def test_fail_path_emits_one_task_failed_event(self):
|
||||
service = _service()
|
||||
task, claim = _claim_for_service(service, task_id="task-single-fail")
|
||||
service.fail(
|
||||
task["task_id"],
|
||||
{
|
||||
"claim_id": claim["claim_id"],
|
||||
"actor_identity": WORKER_IDENTITY,
|
||||
"reason": "expected test failure",
|
||||
},
|
||||
)
|
||||
events = service.store.list_events(task["task_id"])
|
||||
self.assertEqual(1, sum(event["type"] == "TASK_FAILED" for event in events))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -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()
|
||||
@@ -0,0 +1,174 @@
|
||||
import os
|
||||
import sys
|
||||
import tempfile
|
||||
import subprocess
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "active-runtime" / "app"))
|
||||
import aios_isolated_writer_policy as policy
|
||||
|
||||
|
||||
class IsolatedWriterPolicyTests(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.saved = dict(os.environ)
|
||||
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()
|
||||
os.environ.update(self.saved)
|
||||
|
||||
def test_only_expected_branch_is_accepted(self):
|
||||
policy.validate_branch_request({
|
||||
"branch": os.environ["AIOS_GITOPS_ALLOWED_BRANCH"],
|
||||
"base": "main",
|
||||
})
|
||||
for branch in ("patch/other", "main", ""):
|
||||
with self.subTest(branch=branch), self.assertRaises(PermissionError):
|
||||
policy.validate_branch_request({"branch": branch, "base": "main"})
|
||||
|
||||
def test_write_paths_are_scoped_and_canonical(self):
|
||||
with tempfile.TemporaryDirectory() as root:
|
||||
workspace = Path(root)
|
||||
self.assertEqual(
|
||||
policy.validate_path("active-runtime/app/x.py", workspace),
|
||||
"active-runtime/app/x.py",
|
||||
)
|
||||
self.assertEqual(policy.validate_path("docs/x.md", workspace), "docs/x.md")
|
||||
self.assertEqual(
|
||||
policy.validate_path("scripts/task-plane/run.sh", workspace),
|
||||
"scripts/task-plane/run.sh",
|
||||
)
|
||||
self.assertEqual(
|
||||
policy.validate_path("reports/task-plane.json", workspace),
|
||||
"reports/task-plane.json",
|
||||
)
|
||||
rejected = (
|
||||
"",
|
||||
"../docs/x.md",
|
||||
"/docs/x.md",
|
||||
"docs/../x.md",
|
||||
"%2e%2e/docs/x.md",
|
||||
"%2Fdocs/x.md",
|
||||
r"C:\temp\x",
|
||||
r"\\server\share\x",
|
||||
"./docs/x.md",
|
||||
"docs//x.md",
|
||||
"docs/./x.md",
|
||||
"docs/x.md\x00tail",
|
||||
"baseline/current.json",
|
||||
)
|
||||
for value in rejected:
|
||||
with self.subTest(value=value), self.assertRaises(PermissionError):
|
||||
policy.validate_path(value, workspace)
|
||||
|
||||
@unittest.skipIf(os.name == "nt", "symlink creation requires Windows privilege")
|
||||
def test_external_symlink_is_rejected(self):
|
||||
with tempfile.TemporaryDirectory() as root, tempfile.TemporaryDirectory() as outside:
|
||||
workspace = Path(root)
|
||||
(workspace / "docs").symlink_to(Path(outside), target_is_directory=True)
|
||||
with self.assertRaises(PermissionError):
|
||||
policy.validate_path("docs/escape.md", workspace)
|
||||
|
||||
def test_force_and_forbidden_endpoints_are_rejected(self):
|
||||
with self.assertRaises(PermissionError):
|
||||
policy.validate_branch_request({
|
||||
"branch": os.environ["AIOS_GITOPS_ALLOWED_BRANCH"],
|
||||
"force": True,
|
||||
})
|
||||
for endpoint in (
|
||||
"/api/tag",
|
||||
"/api/rollback",
|
||||
"/api/model",
|
||||
"/api/model-priority",
|
||||
"/api/baseline",
|
||||
"/api/registry",
|
||||
"/api/deploy",
|
||||
):
|
||||
self.assertNotIn(endpoint, policy.WRITE_ENDPOINTS)
|
||||
|
||||
@unittest.skipIf(os.name == "nt", "authoritative writer runs on POSIX")
|
||||
def test_approved_base_is_ancestor_even_when_main_drifts(self):
|
||||
with tempfile.TemporaryDirectory() as root:
|
||||
workspace = Path(root) / "writer"
|
||||
workspace.mkdir()
|
||||
subprocess.run(["git", "init"], cwd=workspace, check=True, capture_output=True)
|
||||
subprocess.run(
|
||||
["git", "config", "user.email", "task-plane@example.invalid"],
|
||||
cwd=workspace,
|
||||
check=True,
|
||||
)
|
||||
subprocess.run(
|
||||
["git", "config", "user.name", "Task Plane Test"],
|
||||
cwd=workspace,
|
||||
check=True,
|
||||
)
|
||||
(workspace / "README.md").write_text("base\n", encoding="utf-8")
|
||||
subprocess.run(["git", "add", "README.md"], cwd=workspace, check=True)
|
||||
subprocess.run(["git", "commit", "-m", "base"], cwd=workspace, check=True, capture_output=True)
|
||||
base = subprocess.check_output(
|
||||
["git", "rev-parse", "HEAD"], cwd=workspace, text=True
|
||||
).strip()
|
||||
branch = os.environ["AIOS_GITOPS_ALLOWED_BRANCH"]
|
||||
subprocess.run(["git", "checkout", "-b", branch], cwd=workspace, check=True, capture_output=True)
|
||||
(workspace / "README.md").write_text("branch\n", encoding="utf-8")
|
||||
subprocess.run(["git", "commit", "-am", "branch"], cwd=workspace, check=True, capture_output=True)
|
||||
os.environ.update(
|
||||
{
|
||||
"AIOS_GITOPS_ALLOWED_BASE_OID": base,
|
||||
"AIOS_GITOPS_WRITES_ENABLED": "true",
|
||||
"AIOS_GITOPS_RUNTIME_ROLE": "isolated-writer",
|
||||
"AIOS_GITOPS_WORKSPACE_ROLE": "isolated-writer",
|
||||
"AIOS_GITOPS_REPOSITORY_IDENTITY": "aios/aios",
|
||||
"AIOS_GITOPS_AUTHORITATIVE_GIT_DIR": str(workspace / ".git"),
|
||||
"AIOS_GITOPS_APPROVED_WRITER_ROOT": str(Path(root)),
|
||||
}
|
||||
)
|
||||
for name in policy._DENY_FLAGS:
|
||||
os.environ[name] = "false"
|
||||
policy.require_service_invariants(workspace, workspace / ".git")
|
||||
|
||||
@unittest.skipIf(os.name == "nt", "POSIX audit mode and O_NOFOLLOW contract")
|
||||
def test_audit_path_is_contained_durable_and_redacted(self):
|
||||
with tempfile.TemporaryDirectory() as root:
|
||||
approved = Path(root) / "approved"
|
||||
approved.mkdir(mode=0o750)
|
||||
log = approved / "writer.jsonl"
|
||||
os.environ["AIOS_GITOPS_APPROVED_AUDIT_ROOT"] = str(approved)
|
||||
os.environ["AIOS_GITOPS_AUDIT_LOG"] = str(log)
|
||||
policy.append_audit_record(
|
||||
{
|
||||
"action": "commit",
|
||||
"Authorization": "Bearer must-not-appear",
|
||||
"payload": "token=must-not-appear",
|
||||
}
|
||||
)
|
||||
text = log.read_text(encoding="utf-8")
|
||||
self.assertNotIn("must-not-appear", text)
|
||||
self.assertEqual(0o600, log.stat().st_mode & 0o777)
|
||||
os.environ["AIOS_GITOPS_AUDIT_LOG"] = str(Path(root) / "escape.jsonl")
|
||||
with self.assertRaises(PermissionError):
|
||||
policy.approved_audit_log_path()
|
||||
|
||||
|
||||
class SecretScannerBoundaryTests(unittest.TestCase):
|
||||
def test_task_control_plane_branch_is_not_an_api_key(self):
|
||||
import aios_chatgpt_actions_api as api
|
||||
|
||||
payload = (
|
||||
'{"branch":"patch/aios-agent-task-control-plane-20260724",'
|
||||
'"created_by":"agent-task-plane-writer"}'
|
||||
)
|
||||
self.assertFalse(api.content_has_secret(payload))
|
||||
|
||||
def test_real_openai_style_key_is_still_rejected(self):
|
||||
import aios_chatgpt_actions_api as api
|
||||
|
||||
payload = '{"value":"sk-abcdefghijklmnopqrstuvwxyz123456"}'
|
||||
self.assertTrue(api.content_has_secret(payload))
|
||||
@@ -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()
|
||||
@@ -154,6 +154,7 @@ class SilentReleaseControllerTests(unittest.TestCase):
|
||||
ApprovalLedger(Path(td) / "approvals"),
|
||||
SilentReleasePolicy.default(),
|
||||
store,
|
||||
allow_legacy_unsigned_approval=True,
|
||||
),
|
||||
store=store,
|
||||
policy=SilentReleasePolicy.default(),
|
||||
@@ -248,6 +249,7 @@ class SilentReleaseControllerTests(unittest.TestCase):
|
||||
deployment,
|
||||
ApprovalLedger(Path(td) / "approvals"),
|
||||
SilentReleasePolicy.default(),
|
||||
allow_legacy_unsigned_approval=True,
|
||||
)
|
||||
result = controller.deploy(value, envelope)
|
||||
self.assertEqual(value.state, ReleaseState.VERIFIED)
|
||||
@@ -273,6 +275,7 @@ class SilentReleaseControllerTests(unittest.TestCase):
|
||||
deployment,
|
||||
ApprovalLedger(Path(td) / "approvals"),
|
||||
SilentReleasePolicy.default(),
|
||||
allow_legacy_unsigned_approval=True,
|
||||
).deploy(value, envelope)
|
||||
self.assertFalse(result["verified"])
|
||||
self.assertEqual(value.state, ReleaseState.ROLLED_BACK)
|
||||
@@ -292,7 +295,10 @@ class SilentReleaseControllerTests(unittest.TestCase):
|
||||
with tempfile.TemporaryDirectory() as td:
|
||||
ledger = ApprovalLedger(Path(td) / "approvals")
|
||||
controller = DeploymentController(
|
||||
FakeDeployment(), ledger, SilentReleasePolicy.default()
|
||||
FakeDeployment(),
|
||||
ledger,
|
||||
SilentReleasePolicy.default(),
|
||||
allow_legacy_unsigned_approval=True,
|
||||
)
|
||||
controller.deploy(value, envelope)
|
||||
second = candidate()
|
||||
@@ -309,6 +315,7 @@ class SilentReleaseControllerTests(unittest.TestCase):
|
||||
adapter,
|
||||
SilentReleasePolicy.default(),
|
||||
ledger=None,
|
||||
allow_legacy_unsigned_approval=True,
|
||||
)
|
||||
envelope = TagApprovalEnvelope(
|
||||
approval_id="tag-approval-001",
|
||||
|
||||
@@ -0,0 +1,728 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import dataclasses
|
||||
import json
|
||||
import sys
|
||||
import threading
|
||||
import urllib.request
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from http.server import ThreadingHTTPServer
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
from cryptography.hazmat.primitives import serialization
|
||||
from cryptography.hazmat.primitives.asymmetric.ed25519 import (
|
||||
Ed25519PrivateKey,
|
||||
)
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
sys.path.insert(0, str(ROOT / "active-runtime" / "app"))
|
||||
sys.path.insert(0, str(ROOT / "scripts"))
|
||||
|
||||
from silent_release_hardening import ( # noqa: E402
|
||||
CapabilityAuthorizationError,
|
||||
CapabilityController,
|
||||
CapabilityLedger,
|
||||
ClaimProbeResult,
|
||||
ApprovalKeyRing,
|
||||
ApprovalRevocationStore,
|
||||
BuilderAttestationVerifier,
|
||||
DeploymentPlan,
|
||||
FinalizationPlan,
|
||||
ProbeEvidence,
|
||||
ReleasePlan,
|
||||
SignedApprovalVerifier,
|
||||
SignedReleasePlanApproval,
|
||||
SilentReleaseActionService,
|
||||
TagPlan,
|
||||
TagPublicationProbeEvidence,
|
||||
TrustedCandidateBundle,
|
||||
TrustedReleasePolicy,
|
||||
classify_tag_publication,
|
||||
)
|
||||
from silent_release_controller import ReleaseCandidate, ReleaseState # noqa: E402
|
||||
from silent_release_execution import ( # noqa: E402
|
||||
LocalTestApprovalSigner,
|
||||
LocalTestBuilderSigner,
|
||||
PersistentSilentReleaseController,
|
||||
ShadowActionHandler,
|
||||
SignedTrustedBuilderAdapter,
|
||||
UnavailableProductionApprovalBackend,
|
||||
)
|
||||
from build_silent_release_artifact import ( # noqa: E402
|
||||
redact_output,
|
||||
secret_scan,
|
||||
)
|
||||
|
||||
|
||||
COMMIT = "1" * 40
|
||||
TAG_OID = "2" * 40
|
||||
|
||||
|
||||
def approval() -> SignedReleasePlanApproval:
|
||||
return SignedReleasePlanApproval(
|
||||
release_plan_id="release-plan-readiness-001",
|
||||
candidate_id="rc-readiness-001",
|
||||
source_commit=COMMIT,
|
||||
tree_oid="3" * 40,
|
||||
artifact_sha256="4" * 64,
|
||||
manifest_sha256="5" * 64,
|
||||
deployment_plan_sha256="6" * 64,
|
||||
policy_sha256="7" * 64,
|
||||
approval_plan_id="owner-plan-readiness-001",
|
||||
evidence_closure_oid="8" * 40,
|
||||
owner_approval_record_oid="9" * 40,
|
||||
capabilities=(
|
||||
"runtime_switch",
|
||||
"tag_publish",
|
||||
"baseline_registry_finalize",
|
||||
),
|
||||
expires_at="2099-01-01T00:00:00Z",
|
||||
nonce="nonce-readiness-000000001",
|
||||
key_id="owner-key-readiness-1",
|
||||
signature="test-signature",
|
||||
)
|
||||
|
||||
|
||||
def tag_plan() -> TagPlan:
|
||||
return TagPlan(
|
||||
version="shadow-readiness-001",
|
||||
tag_type="annotated",
|
||||
expected_tag_oid=TAG_OID,
|
||||
expected_target_oid=COMMIT,
|
||||
evidence_closure_oid="8" * 40,
|
||||
approval_plan_id="owner-plan-readiness-001",
|
||||
repository_identity="aios/aios",
|
||||
gitea_owner="aios",
|
||||
gitea_repository="aios",
|
||||
current_remote_tag_oid=None,
|
||||
remote_absent_evidence_sha256="a" * 64,
|
||||
verification_contract="git+api+listing/v1",
|
||||
)
|
||||
|
||||
|
||||
def make_failed_claim(
|
||||
ledger: CapabilityLedger,
|
||||
*,
|
||||
operation: str,
|
||||
) -> dict:
|
||||
claimed = ledger.claim(
|
||||
approval(),
|
||||
"runtime_switch",
|
||||
operation=operation,
|
||||
executor_id="worker-initial",
|
||||
lease_seconds=10,
|
||||
now=100,
|
||||
)
|
||||
ledger.fail_recoverable(
|
||||
approval(),
|
||||
"runtime_switch",
|
||||
operation=operation,
|
||||
claim_id=claimed["claim_id"],
|
||||
reason="adapter_error",
|
||||
)
|
||||
return claimed
|
||||
|
||||
|
||||
def test_failed_recoverable_not_applied_retries_once(tmp_path):
|
||||
ledger = CapabilityLedger(
|
||||
tmp_path / "ledger", maximum_probe_age_seconds=30
|
||||
)
|
||||
make_failed_claim(ledger, operation="runtime-retry")
|
||||
calls: list[str] = []
|
||||
controller = CapabilityController(
|
||||
capability="runtime_switch",
|
||||
ledger=ledger,
|
||||
adapter=lambda: calls.append("adapter") or {"verified": True},
|
||||
recovery_probe=lambda: ClaimProbeResult.NOT_APPLIED,
|
||||
executor_id="worker-recovery",
|
||||
)
|
||||
|
||||
assert controller.execute(
|
||||
approval(), operation="runtime-retry"
|
||||
)["status"] == "EXECUTED"
|
||||
assert controller.execute(
|
||||
approval(), operation="runtime-retry"
|
||||
)["status"] == "IDEMPOTENT"
|
||||
assert calls == ["adapter"]
|
||||
|
||||
|
||||
def test_failed_recoverable_applied_verified_skips_adapter(tmp_path):
|
||||
ledger = CapabilityLedger(tmp_path / "ledger")
|
||||
make_failed_claim(ledger, operation="runtime-applied")
|
||||
calls: list[str] = []
|
||||
controller = CapabilityController(
|
||||
capability="runtime_switch",
|
||||
ledger=ledger,
|
||||
adapter=lambda: calls.append("adapter") or {"verified": True},
|
||||
recovery_probe=lambda: ClaimProbeResult.APPLIED_VERIFIED,
|
||||
executor_id="worker-recovery",
|
||||
)
|
||||
|
||||
assert controller.execute(
|
||||
approval(), operation="runtime-applied"
|
||||
)["status"] == "IDEMPOTENT"
|
||||
assert calls == []
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("probe", "expected"),
|
||||
(
|
||||
(
|
||||
ClaimProbeResult.INDETERMINATE,
|
||||
"RECONCILIATION_REQUIRED",
|
||||
),
|
||||
(
|
||||
ClaimProbeResult.CONFLICT,
|
||||
"MANUAL_OWNER_REVIEW_REQUIRED",
|
||||
),
|
||||
),
|
||||
)
|
||||
def test_failed_recoverable_unknown_or_conflict_stops_writes(
|
||||
tmp_path, probe, expected
|
||||
):
|
||||
ledger = CapabilityLedger(tmp_path / "ledger")
|
||||
make_failed_claim(ledger, operation=f"runtime-{probe.value.lower()}")
|
||||
calls: list[str] = []
|
||||
controller = CapabilityController(
|
||||
capability="runtime_switch",
|
||||
ledger=ledger,
|
||||
adapter=lambda: calls.append("adapter") or {"verified": True},
|
||||
recovery_probe=lambda: probe,
|
||||
executor_id="worker-recovery",
|
||||
)
|
||||
|
||||
assert controller.execute(
|
||||
approval(), operation=f"runtime-{probe.value.lower()}"
|
||||
)["status"] == expected
|
||||
assert calls == []
|
||||
|
||||
|
||||
def test_probe_must_bind_operation_and_be_fresh(tmp_path):
|
||||
ledger = CapabilityLedger(
|
||||
tmp_path / "ledger", maximum_probe_age_seconds=10
|
||||
)
|
||||
make_failed_claim(ledger, operation="runtime-freshness")
|
||||
record = ledger.read(
|
||||
approval(), "runtime_switch", operation="runtime-freshness"
|
||||
)
|
||||
assert record is not None
|
||||
|
||||
with pytest.raises(
|
||||
CapabilityAuthorizationError, match="operation digest"
|
||||
):
|
||||
ledger.claim(
|
||||
approval(),
|
||||
"runtime_switch",
|
||||
operation="runtime-freshness",
|
||||
executor_id="worker-recovery",
|
||||
lease_seconds=10,
|
||||
now=120,
|
||||
side_effect_probe=lambda: ProbeEvidence(
|
||||
result=ClaimProbeResult.NOT_APPLIED,
|
||||
operation_digest="0" * 64,
|
||||
observed_at=120,
|
||||
),
|
||||
)
|
||||
|
||||
with pytest.raises(CapabilityAuthorizationError, match="freshness"):
|
||||
ledger.claim(
|
||||
approval(),
|
||||
"runtime_switch",
|
||||
operation="runtime-freshness",
|
||||
executor_id="worker-recovery",
|
||||
lease_seconds=10,
|
||||
now=120,
|
||||
side_effect_probe=lambda: ProbeEvidence(
|
||||
result=ClaimProbeResult.NOT_APPLIED,
|
||||
operation_digest=record["operation_digest"],
|
||||
observed_at=100,
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def test_probe_journal_is_hash_chained_and_tamper_fails_closed(tmp_path):
|
||||
ledger = CapabilityLedger(tmp_path / "ledger")
|
||||
make_failed_claim(ledger, operation="runtime-journal")
|
||||
ledger.claim(
|
||||
approval(),
|
||||
"runtime_switch",
|
||||
operation="runtime-journal",
|
||||
executor_id="worker-recovery",
|
||||
lease_seconds=10,
|
||||
now=101,
|
||||
side_effect_probe=lambda: ClaimProbeResult.INDETERMINATE,
|
||||
)
|
||||
operation_path = ledger._operation_path(
|
||||
approval(), "runtime_switch", "runtime-journal"
|
||||
)
|
||||
records = ledger.verify_probe_journal(operation_path)
|
||||
assert records[0]["payload"]["result"] == "INDETERMINATE"
|
||||
journal = ledger._probe_journal_path(operation_path)
|
||||
payload = json.loads(journal.read_text(encoding="utf-8"))
|
||||
payload["payload"]["result"] = "NOT_APPLIED"
|
||||
journal.write_text(json.dumps(payload) + "\n", encoding="utf-8")
|
||||
with pytest.raises(Exception, match="hash chain"):
|
||||
ledger.verify_probe_journal(operation_path)
|
||||
|
||||
|
||||
def tag_evidence(**overrides) -> TagPublicationProbeEvidence:
|
||||
values = {
|
||||
"repository_identity": "aios/aios",
|
||||
"remote_reachable": True,
|
||||
"gitea_api_reachable": True,
|
||||
"gitea_listing_reachable": True,
|
||||
"remote_tag_oid": TAG_OID,
|
||||
"annotated_tag_object_oid": TAG_OID,
|
||||
"remote_peeled_target_oid": COMMIT,
|
||||
"gitea_api_tag_oid": TAG_OID,
|
||||
"gitea_api_peeled_target_oid": COMMIT,
|
||||
"gitea_listing_visible": True,
|
||||
"gitea_listing_tag_oid": TAG_OID,
|
||||
"gitea_listing_peeled_target_oid": COMMIT,
|
||||
"observed_at": 100,
|
||||
}
|
||||
values.update(overrides)
|
||||
return TagPublicationProbeEvidence(**values)
|
||||
|
||||
|
||||
def test_tag_probe_exact_absence_is_not_applied():
|
||||
evidence = tag_evidence(
|
||||
remote_tag_oid=None,
|
||||
annotated_tag_object_oid=None,
|
||||
remote_peeled_target_oid=None,
|
||||
gitea_api_tag_oid=None,
|
||||
gitea_api_peeled_target_oid=None,
|
||||
gitea_listing_visible=False,
|
||||
gitea_listing_tag_oid=None,
|
||||
gitea_listing_peeled_target_oid=None,
|
||||
)
|
||||
assert (
|
||||
classify_tag_publication(evidence, tag_plan())
|
||||
== ClaimProbeResult.NOT_APPLIED
|
||||
)
|
||||
|
||||
|
||||
def test_tag_probe_exact_publication_is_applied_verified():
|
||||
assert (
|
||||
classify_tag_publication(tag_evidence(), tag_plan())
|
||||
== ClaimProbeResult.APPLIED_VERIFIED
|
||||
)
|
||||
|
||||
|
||||
def test_tag_probe_unavailable_or_divergent_is_indeterminate():
|
||||
assert (
|
||||
classify_tag_publication(
|
||||
tag_evidence(gitea_api_reachable=False), tag_plan()
|
||||
)
|
||||
== ClaimProbeResult.INDETERMINATE
|
||||
)
|
||||
assert (
|
||||
classify_tag_publication(
|
||||
tag_evidence(gitea_listing_tag_oid=None), tag_plan()
|
||||
)
|
||||
== ClaimProbeResult.INDETERMINATE
|
||||
)
|
||||
|
||||
|
||||
def test_tag_probe_same_name_different_oid_is_conflict():
|
||||
assert (
|
||||
classify_tag_publication(
|
||||
tag_evidence(remote_tag_oid="f" * 40), tag_plan()
|
||||
)
|
||||
== ClaimProbeResult.CONFLICT
|
||||
)
|
||||
assert (
|
||||
classify_tag_publication(
|
||||
tag_evidence(remote_peeled_target_oid="e" * 40), tag_plan()
|
||||
)
|
||||
== ClaimProbeResult.CONFLICT
|
||||
)
|
||||
|
||||
|
||||
def readiness_release_plan(source_commit: str = COMMIT) -> ReleasePlan:
|
||||
policy_sha = "7" * 64
|
||||
return ReleasePlan(
|
||||
deployment=DeploymentPlan(
|
||||
service="aios-silent-release-controller",
|
||||
release_path="/usr/libexec/aios-silent-release-shadow/release-1",
|
||||
release_root="/usr/libexec/aios-silent-release-shadow",
|
||||
cutover_link="/usr/libexec/aios-silent-release-shadow/current",
|
||||
dropin_path=(
|
||||
"/usr/libexec/aios-silent-release-shadow/release-1/"
|
||||
"deploy/systemd/aios-silent-release-controller.service"
|
||||
),
|
||||
dropin_sha256="a" * 64,
|
||||
systemd_unit_sha256="a" * 64,
|
||||
rollback_path=(
|
||||
"/usr/libexec/aios-silent-release-shadow/rollback-1"
|
||||
),
|
||||
rollback_sha256="b" * 64,
|
||||
source_commit=source_commit,
|
||||
tree_oid="3" * 40,
|
||||
artifact_sha256="4" * 64,
|
||||
manifest_sha256="5" * 64,
|
||||
package_closure_sha256="c" * 64,
|
||||
authoritative_git_dir="/data/aios-gitops/repo.git",
|
||||
repository_identity="aios/aios",
|
||||
writer_expected=False,
|
||||
baseline_before_sha256="d" * 64,
|
||||
registry_before_sha256="e" * 64,
|
||||
production_tag_refset_before_sha256="f" * 64,
|
||||
health_contract_version="shadow-health/v1",
|
||||
timeout_policy={"health_seconds": 10},
|
||||
retry_policy={"health_attempts": 1},
|
||||
policy_sha256=policy_sha,
|
||||
health_contract={"shadow_mode": True},
|
||||
),
|
||||
tag=TagPlan(
|
||||
version="shadow-readiness-001",
|
||||
tag_type="annotated",
|
||||
expected_tag_oid=TAG_OID,
|
||||
expected_target_oid=source_commit,
|
||||
evidence_closure_oid="8" * 40,
|
||||
approval_plan_id="owner-plan-readiness-001",
|
||||
repository_identity="aios/aios",
|
||||
gitea_owner="aios",
|
||||
gitea_repository="aios",
|
||||
current_remote_tag_oid=None,
|
||||
remote_absent_evidence_sha256="a" * 64,
|
||||
verification_contract="shadow-only/v1",
|
||||
),
|
||||
finalization=FinalizationPlan(
|
||||
version="shadow-readiness-001",
|
||||
expected_tag_oid=TAG_OID,
|
||||
expected_target_oid=source_commit,
|
||||
expected_previous_baseline_sha256="d" * 64,
|
||||
registry_expected_record={
|
||||
"shadow": True,
|
||||
"target_oid": source_commit,
|
||||
},
|
||||
baseline_expected_record={
|
||||
"shadow": True,
|
||||
"target_oid": source_commit,
|
||||
},
|
||||
final_consistency_contract="shadow-simulated/v1",
|
||||
),
|
||||
policy_sha256=policy_sha,
|
||||
)
|
||||
|
||||
|
||||
def readiness_candidate(source_commit: str = COMMIT) -> ReleaseCandidate:
|
||||
return ReleaseCandidate(
|
||||
candidate_id="rc-shadow-" + source_commit[:12],
|
||||
source_commit=source_commit,
|
||||
tree_oid="3" * 40,
|
||||
artifact_sha256="4" * 64,
|
||||
manifest_sha256="5" * 64,
|
||||
tests={"passed": True, "source": "trusted-builder"},
|
||||
canary={"passed": True, "without_git": True},
|
||||
rollback_ready=True,
|
||||
git_consistency={"consistent": True},
|
||||
gitea_consistency={"consistent": True},
|
||||
approval_state="PENDING",
|
||||
state=ReleaseState.READY_FOR_APPROVAL,
|
||||
)
|
||||
|
||||
|
||||
def trusted_service(tmp_path):
|
||||
builder_signer = LocalTestBuilderSigner.generate()
|
||||
builder_public = builder_signer.public_key.public_bytes(
|
||||
serialization.Encoding.Raw,
|
||||
serialization.PublicFormat.Raw,
|
||||
)
|
||||
|
||||
def collector(intent):
|
||||
source_commit = str(intent["source_commit"])
|
||||
return TrustedCandidateBundle(
|
||||
candidate=readiness_candidate(source_commit),
|
||||
release_plan=readiness_release_plan(source_commit),
|
||||
checks={
|
||||
"authoritative_git": True,
|
||||
"artifact": True,
|
||||
"canary": True,
|
||||
"gitea": True,
|
||||
"manifest": True,
|
||||
"package_closure": True,
|
||||
"rollback": True,
|
||||
"secret_scan": True,
|
||||
"tests": True,
|
||||
},
|
||||
)
|
||||
|
||||
owner_private = Ed25519PrivateKey.generate()
|
||||
owner_public = owner_private.public_key().public_bytes(
|
||||
serialization.Encoding.Raw,
|
||||
serialization.PublicFormat.Raw,
|
||||
)
|
||||
verifier = SignedApprovalVerifier(
|
||||
ApprovalKeyRing({"shadow-owner-test-key": owner_public}),
|
||||
ApprovalRevocationStore(tmp_path / "revocations"),
|
||||
allowed_trust_domains={"aios.shadow"},
|
||||
)
|
||||
policy = TrustedReleasePolicy(
|
||||
services=frozenset({"aios-silent-release-controller"}),
|
||||
release_roots=frozenset(
|
||||
{"/usr/libexec/aios-silent-release-shadow"}
|
||||
),
|
||||
cutover_links=frozenset(
|
||||
{"/usr/libexec/aios-silent-release-shadow/current"}
|
||||
),
|
||||
dropin_paths=frozenset(
|
||||
{
|
||||
"/usr/libexec/aios-silent-release-shadow/release-1/"
|
||||
"deploy/systemd/aios-silent-release-controller.service"
|
||||
}
|
||||
),
|
||||
authoritative_git_dirs=frozenset(
|
||||
{"/data/aios-gitops/repo.git"}
|
||||
),
|
||||
repository_identities=frozenset({"aios/aios"}),
|
||||
)
|
||||
service = SilentReleaseActionService(
|
||||
tmp_path / "state",
|
||||
execution_enabled=False,
|
||||
approval_verifier=verifier,
|
||||
candidate_builder=SignedTrustedBuilderAdapter(
|
||||
collector, builder_signer
|
||||
),
|
||||
builder_attestation_verifier=BuilderAttestationVerifier(
|
||||
{builder_signer.key_id: builder_public}
|
||||
),
|
||||
trusted_policy=policy,
|
||||
expected_repository="aios/aios",
|
||||
policy_sha256="7" * 64,
|
||||
)
|
||||
return service, LocalTestApprovalSigner(
|
||||
"shadow-owner-test-key", owner_private
|
||||
)
|
||||
|
||||
|
||||
def signed_shadow_approval(
|
||||
service: SilentReleaseActionService,
|
||||
signer: LocalTestApprovalSigner,
|
||||
candidate_id: str,
|
||||
) -> SignedReleasePlanApproval:
|
||||
candidate = service.candidates.load(candidate_id)
|
||||
plan = service.release_plans.load(candidate_id)
|
||||
now = datetime.now(timezone.utc)
|
||||
value = SignedReleasePlanApproval(
|
||||
release_plan_id="release-plan-shadow-readiness-001",
|
||||
candidate_id=candidate_id,
|
||||
source_commit=candidate.source_commit,
|
||||
tree_oid=candidate.tree_oid,
|
||||
artifact_sha256=candidate.artifact_sha256,
|
||||
manifest_sha256=candidate.manifest_sha256,
|
||||
deployment_plan_sha256=plan.deployment.sha256,
|
||||
release_plan_sha256=plan.sha256,
|
||||
policy_sha256=plan.policy_sha256,
|
||||
approval_plan_id=plan.tag.approval_plan_id,
|
||||
evidence_closure_oid=plan.tag.evidence_closure_oid,
|
||||
owner_approval_record_oid="9" * 40,
|
||||
capabilities=(
|
||||
"runtime_switch",
|
||||
"tag_publish",
|
||||
"baseline_registry_finalize",
|
||||
),
|
||||
owner_identity="owner:shadow-test",
|
||||
trust_domain="aios.shadow",
|
||||
issued_at=now.isoformat().replace("+00:00", "Z"),
|
||||
not_before=now.isoformat().replace("+00:00", "Z"),
|
||||
expires_at=(now + timedelta(minutes=5))
|
||||
.isoformat()
|
||||
.replace("+00:00", "Z"),
|
||||
nonce="nonce-shadow-readiness-000001",
|
||||
key_id="shadow-owner-test-key",
|
||||
signature="",
|
||||
)
|
||||
return signer.sign(value)
|
||||
|
||||
|
||||
def test_trusted_builder_attestation_and_caller_evidence_gate(tmp_path):
|
||||
service, _ = trusted_service(tmp_path)
|
||||
result = service.create_release_intent(
|
||||
{"event_id": "webhook-main-001", "source_commit": COMMIT}
|
||||
)
|
||||
acceptance = service.get_acceptance(result["candidate_id"])
|
||||
assert acceptance["attestation"]["builder_attestation_id"].startswith(
|
||||
"builder-attestation-"
|
||||
)
|
||||
assert acceptance["checks"]["tests"] is True
|
||||
with pytest.raises(
|
||||
CapabilityAuthorizationError, match="caller-supplied evidence"
|
||||
):
|
||||
service.create_release_intent(
|
||||
{
|
||||
"event_id": "webhook-main-002",
|
||||
"source_commit": COMMIT,
|
||||
"tests": {"passed": True},
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def test_builder_attestation_tamper_is_rejected(tmp_path):
|
||||
service, _ = trusted_service(tmp_path)
|
||||
bundle = service.candidate_builder.build(
|
||||
{"event_id": "webhook-main-001", "source_commit": COMMIT}
|
||||
)
|
||||
tampered = dataclasses.replace(
|
||||
bundle.attestation, artifact_sha256="0" * 64
|
||||
)
|
||||
with pytest.raises(
|
||||
CapabilityAuthorizationError, match="does not bind"
|
||||
):
|
||||
dataclasses.replace(bundle, attestation=tampered).validate(
|
||||
service.builder_attestation_verifier
|
||||
)
|
||||
|
||||
|
||||
def test_production_approval_backend_missing_disables_execution():
|
||||
backend = UnavailableProductionApprovalBackend()
|
||||
assert backend.available is False
|
||||
with pytest.raises(
|
||||
CapabilityAuthorizationError, match="not configured"
|
||||
):
|
||||
backend.verify()
|
||||
|
||||
|
||||
def test_controller_duplicate_webhook_notification_and_restart_are_idempotent(
|
||||
tmp_path,
|
||||
):
|
||||
service, _ = trusted_service(tmp_path)
|
||||
first_controller = PersistentSilentReleaseController(
|
||||
tmp_path / "state",
|
||||
service,
|
||||
main_probe=lambda: COMMIT,
|
||||
)
|
||||
first = first_controller.run_once()
|
||||
second = first_controller.run_once()
|
||||
restarted = PersistentSilentReleaseController(
|
||||
tmp_path / "state",
|
||||
service,
|
||||
main_probe=lambda: COMMIT,
|
||||
).run_once()
|
||||
assert first["intent"]["candidate_id"] == second["intent"]["candidate_id"]
|
||||
assert second["intent"]["idempotent"] is True
|
||||
assert first["notification"]["idempotent"] is False
|
||||
assert second["notification"]["idempotent"] is True
|
||||
assert restarted["notification"]["idempotent"] is True
|
||||
assert restarted["side_effects_executed"] is False
|
||||
|
||||
|
||||
def test_controller_startup_journal_failure_stops_before_main_event(tmp_path):
|
||||
service, _ = trusted_service(tmp_path)
|
||||
service.sagas.create(
|
||||
"rc-shadow-" + COMMIT[:12], "release-plan-shadow-readiness-001"
|
||||
)
|
||||
journal = next(service.sagas.root.glob("*.events.jsonl"))
|
||||
journal.write_text('{"tampered":true}\n', encoding="utf-8")
|
||||
probes: list[str] = []
|
||||
result = PersistentSilentReleaseController(
|
||||
tmp_path / "state",
|
||||
service,
|
||||
main_probe=lambda: probes.append("main") or COMMIT,
|
||||
).run_once()
|
||||
assert result["status"] == "FAIL_CLOSED"
|
||||
assert result["intent"] is None
|
||||
assert result["side_effects_executed"] is False
|
||||
assert probes == []
|
||||
|
||||
|
||||
def test_action_http_shadow_flow_has_zero_side_effects(tmp_path):
|
||||
service, signer = trusted_service(tmp_path)
|
||||
handler = type(
|
||||
"TestShadowHandler",
|
||||
(ShadowActionHandler,),
|
||||
{"service": service, "controller": None},
|
||||
)
|
||||
server = ThreadingHTTPServer(("127.0.0.1", 0), handler)
|
||||
thread = threading.Thread(target=server.serve_forever, daemon=True)
|
||||
thread.start()
|
||||
base = f"http://127.0.0.1:{server.server_port}"
|
||||
|
||||
def post(path, payload):
|
||||
request = urllib.request.Request(
|
||||
base + path,
|
||||
data=json.dumps(payload).encode(),
|
||||
headers={"Content-Type": "application/json"},
|
||||
)
|
||||
with urllib.request.urlopen(request, timeout=5) as response:
|
||||
return json.load(response)
|
||||
|
||||
try:
|
||||
intent = post(
|
||||
"/createReleaseIntent",
|
||||
{"event_id": "http-main-001", "source_commit": COMMIT},
|
||||
)
|
||||
candidate_id = intent["candidate_id"]
|
||||
approval = signed_shadow_approval(service, signer, candidate_id)
|
||||
accepted = post("/submitSignedApproval", approval.to_dict())
|
||||
deployed = post(
|
||||
"/startDeployment", {"candidate_id": candidate_id}
|
||||
)
|
||||
with urllib.request.urlopen(
|
||||
base + "/getReleasePlan?candidate_id=" + candidate_id,
|
||||
timeout=5,
|
||||
) as response:
|
||||
plan = json.load(response)
|
||||
finally:
|
||||
server.shutdown()
|
||||
server.server_close()
|
||||
thread.join(timeout=5)
|
||||
|
||||
assert accepted["status"] == "VERIFIED_AND_STORED"
|
||||
assert deployed == {
|
||||
"candidate_id": candidate_id,
|
||||
"execution_enabled": False,
|
||||
"shadow_mode": True,
|
||||
"side_effects": [],
|
||||
"side_effects_executed": False,
|
||||
"status": "SHADOW_COMPLETED",
|
||||
}
|
||||
assert plan["shadow_mode"] is True
|
||||
assert plan["side_effects_executed"] is False
|
||||
|
||||
|
||||
def test_secret_scan_redacts_placeholders_but_rejects_real_material(tmp_path):
|
||||
(tmp_path / "safe.txt").write_text(
|
||||
"Authorization: Bearer test-placeholder-token-000000",
|
||||
encoding="utf-8",
|
||||
)
|
||||
assert secret_scan(tmp_path) == {"findings": [], "passed": True}
|
||||
(tmp_path / "unsafe.txt").write_text(
|
||||
"-----BEGIN "
|
||||
+ "OPENSSH PRIVATE KEY-----\n"
|
||||
+ ("A" * 80)
|
||||
+ "\n-----END "
|
||||
+ "OPENSSH PRIVATE KEY-----",
|
||||
encoding="utf-8",
|
||||
)
|
||||
result = secret_scan(tmp_path)
|
||||
assert result["passed"] is False
|
||||
assert result["findings"] == ["unsafe.txt"]
|
||||
secret = "live-" + ("A" * 40)
|
||||
redacted = redact_output("Authorization: Bearer " + secret)
|
||||
assert secret not in redacted
|
||||
assert "<redacted>" in redacted
|
||||
(tmp_path / "unsafe.txt").write_text(
|
||||
"Authorization: " + "Bearer live-test-looking-secret-000001",
|
||||
encoding="utf-8",
|
||||
)
|
||||
result = secret_scan(tmp_path)
|
||||
assert result["passed"] is False
|
||||
assert result["findings"] == ["unsafe.txt"]
|
||||
|
||||
|
||||
def test_systemd_shadow_unit_has_both_gates_and_no_secret_environment():
|
||||
unit = (
|
||||
ROOT
|
||||
/ "deploy"
|
||||
/ "systemd"
|
||||
/ "aios-silent-release-controller.service"
|
||||
).read_text(encoding="utf-8")
|
||||
assert "AIOS_GITOPS_WRITES_ENABLED=false" in unit
|
||||
assert "AIOS_SILENT_RELEASE_EXECUTION_ENABLED=false" in unit
|
||||
assert "--host 127.0.0.1" in unit
|
||||
assert "PRIVATE_KEY=" not in unit
|
||||
assert "TOKEN=" not in unit
|
||||
assert "PASSWORD=" not in unit
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user