Compare commits

..
50 changed files with 4664 additions and 745 deletions
+4 -16
View File
@@ -22,7 +22,6 @@ from gitea_tag_publication import (
TagPublisher,
redact,
validate_version,
validate_gitea_api_url,
)
from orchestrator.action_monitor import ActionMonitor
from silent_release_hardening import (
@@ -309,7 +308,8 @@ def resolve_commit_oid(value: str | None, field: str = 'target_oid') -> str:
raise ValueError(f'{field} does not resolve to the exact approved commit') from exc
def existing_tag_target(tag: str) -> str | None:
validate_version(tag)
if not re.fullmatch(r'V\d+R\d+C\d+(?:-[A-Za-z0-9_.-]+)?$', str(tag or '')):
raise ValueError('invalid release version')
try:
result = run_authoritative_git(
['rev-parse', '--verify', f'refs/tags/{tag}^{{commit}}'],
@@ -510,15 +510,7 @@ class H(BaseHTTPRequestHandler):
return self.sendj({'error':redact(exc)},413)
except (AuthoritativeGitError, RuntimeError) as exc:
return self.sendj({'error':redact(exc)},503)
payload = {'from': frm, 'to': to, 'diff': diff}
payload.update({
key: diff[key]
for key in (
'from_oid', 'to_oid', 'changed_paths', 'stats',
'total_size_bytes', 'truncated', 'patch_limit_bytes',
)
})
return self.sendj(payload)
return self.sendj({'from':frm,'to':to,'diff':diff})
if path in ('/api/patch','/api/patches','/api/branches','/patch'):
snapshot = repository_snapshot(
git_dir=AUTHORITATIVE_GIT_DIR,
@@ -781,10 +773,7 @@ class H(BaseHTTPRequestHandler):
a=self.need('tag:create');
if not a: return
ver=data.get('version') or data.get('tag')
try:
validate_version(ver)
except ValueError:
return self.sendj({'error':'version must match VxRxCx'},400)
if not re.match(r'^V\d+R\d+C\d+(-[A-Za-z0-9_.-]+)?$', ver or ''): return self.sendj({'error':'version must match VxRxCx'},400)
if not data.get('target_oid') or not data.get('evidence_closure_oid') or not data.get('approval_plan_id'):
return self.sendj({
'error':'production_tag_evidence_required',
@@ -958,7 +947,6 @@ 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 /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():
validate_gitea_api_url(os.getenv('AIOS_GITOPS_GITEA_API_URL', ''))
print(f'AIOS ChatGPT GitOps adapter listening on {HOST}:{PORT}')
ThreadingHTTPServer((HOST,PORT), H).serve_forever()
if __name__ == '__main__': main()
@@ -46,75 +46,6 @@ OPERATION_REQUIRED_FIELDS = {
"getControlPlaneSnapshot": (),
}
_OPERATION_PATHS = {
"createGovernedTask": "/v2/governed-task",
"acquireWriterLease": "/v2/writer-lease",
"commitFileGoverned": "/v2/commit-file",
"commitFilesGoverned": "/v2/commit-files",
"getGitOpsJob": "/v2/job",
"resumeGitOpsJob": "/v2/job/resume",
"reconcileGitOpsJob": "/v2/job/reconcile",
"getTagPublicationStatus": "/v2/tag/status",
"getReleaseConsistency": "/v2/release/consistency",
"runReleaseAcceptance": "/v2/release/acceptance",
}
def operation_contracts() -> dict[str, dict[str, object]]:
"""Return the runtime contract consumed by adapters and schema tooling."""
return {
operation: {
"method": "POST",
"path": _OPERATION_PATHS.get(operation),
"required_fields": list(fields),
}
for operation, fields in OPERATION_REQUIRED_FIELDS.items()
}
def render_openapi_contract_fragment() -> str:
required = "\n".join(
f" - {field}"
for field in OPERATION_REQUIRED_FIELDS["commitFilesGoverned"]
)
return (
" /v2/commit-files:\n"
" post:\n"
" operationId: commitFilesGoverned\n"
" summary: Submit one atomic governed commit containing multiple files\n"
" requestBody:\n"
" required: true\n"
" content:\n"
" application/json:\n"
" schema:\n"
" type: object\n"
" additionalProperties: false\n"
" required:\n"
f"{required}\n"
" properties:\n"
" task_id: {type: string}\n"
" repository: {type: string, example: aios/aios}\n"
" expected_branch_oid: {type: string, pattern: '^[0-9a-f]{40}$'}\n"
" target_branch: {type: string, pattern: '^agent/[A-Za-z0-9._/-]+$'}\n"
" generation: {type: integer, minimum: 1}\n"
" idempotency_key: {type: string}\n"
" files:\n"
" type: array\n"
" minItems: 1\n"
" items:\n"
" type: object\n"
" additionalProperties: false\n"
" required: [path, content_base64]\n"
" properties:\n"
" path: {type: string, minLength: 1, maxLength: 512}\n"
" content_base64: {type: string}\n"
" message: {type: string, minLength: 1, maxLength: 200}\n"
" responses:\n"
" '200': {description: Governed commit job and verified branch result}\n"
" '400': {description: Contract or policy rejection}\n"
" '409': {description: Branch CAS or idempotency conflict}\n"
)
class RuntimeDriftError(RuntimeError):
pass
@@ -191,7 +122,6 @@ class BridgeV2Adapter:
"legacy_writes_enabled": False,
"governed_writes_enabled": True,
"operations": list(BRIDGE_OPERATIONS),
"operation_contracts": operation_contracts(),
"force_push": False,
"main_write": False,
"tag_write": False,
@@ -14,11 +14,10 @@ from typing import Any, Mapping
from aios_authority_git_executor import AuthorityGitExecutor
from aios_chatgpt_bridge_v2 import BridgeV2Adapter, BridgeV2Error, OPERATION_REQUIRED_FIELDS
from gitea_tag_publication import validate_gitea_api_url
from aios_continuation_executor import AuthorityGitAdapter, ContinuationCommit, ContinuationExecutor, ContinuationPermissions, ContinuationRuntimeBinding, DetachedGitWorkspace, GiteaContinuationRemote, ServerSideCredentialProjection
from aios_continuation_lease import WriterLeaseRequestV2
from aios_control_plane_snapshot import build_control_plane_snapshot
from aios_gitops_job_controller import GitOpsJobController, JobState, JobStore, _RECOVERABLE
from aios_gitops_job_controller import GitOpsJobController, JobStore
from aios_governed_task_bootstrap import BootstrapError, GovernedTaskBootstrap
from governed_production_release import build_executor_config
@@ -53,7 +52,7 @@ class RuntimeConfig:
ssh_key_path: Path
remote_name: str = "gitea-http"
repository: str = "aios/aios"
api_base: str = ""
api_base: str = "https://g.cnnetmkt.top/git/api/v1"
v2_task_id: str = "TASK-P0-CHATGPT-BRIDGE-V2-CONTROL-PLANE-CONVERGENCE-20260809"
v2_branch: str = "agent/p0-chatgpt-bridge-v2-control-plane-convergence-20260809"
target_version: str = "V2R2C1"
@@ -281,8 +280,6 @@ class GovernedRuntime:
return controller.store._receipt(row, duplicate=True).as_dict()
if action == "resume":
return controller.resume(job_id).as_dict()
if JobState(row["state"]) not in _RECOVERABLE:
return controller.store._receipt(row, duplicate=True).as_dict()
return self._executor_from_row(row, controller).reconcile(job_id)
def _snapshot_task(self) -> dict[str, Any]:
@@ -421,7 +418,7 @@ def main() -> None:
token_path,
ssh_key_path,
remote_name=os.environ.get("AIOS_GITEA_REMOTE", "gitea-http"),
api_base=validate_gitea_api_url(os.environ.get("AIOS_GITEA_API_BASE", "")),
api_base=os.environ.get("AIOS_GITEA_API_BASE", "https://g.cnnetmkt.top/git/api/v1"),
target_version=os.environ.get("AIOS_CONTROL_PLANE_TARGET_VERSION", "V2R2C1"),
)
runtime = GovernedRuntime(config)
@@ -41,8 +41,6 @@ class JobState(str, Enum):
REMOTE_SYNCING = "REMOTE_SYNCING"
REMOTE_VERIFIED = "REMOTE_VERIFIED"
COMPLETED = "COMPLETED"
FAILED = "FAILED"
CANCELLED = "CANCELLED"
RECOVERABLE_FAILURE = "RECOVERABLE_FAILURE"
RECONCILING = "RECONCILING"
RECONCILIATION_REQUIRED = "RECONCILIATION_REQUIRED"
@@ -61,12 +59,7 @@ _ORDER = {
JobState.COMPLETED: 8,
}
_RECOVERABLE = {JobState.RECOVERABLE_FAILURE, JobState.RECONCILIATION_REQUIRED}
_TERMINAL = {
JobState.COMPLETED,
JobState.FAILED,
JobState.CANCELLED,
JobState.MANUAL_OWNER_REVIEW_REQUIRED,
}
_TERMINAL = {JobState.COMPLETED, JobState.MANUAL_OWNER_REVIEW_REQUIRED}
def utc_now() -> str:
+1 -7
View File
@@ -5,8 +5,6 @@ from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
from pathlib import Path
from typing import Any
from release_version import validate_release_version
ROOT = Path(os.getenv('AIOS_GITOPS_ROOT', '/data/aios-gitops'))
WORKSPACE = Path(os.getenv('AIOS_GITOPS_WORKSPACE', str(ROOT / 'workspace')))
DB_PATH = Path(os.getenv('AIOS_GITOPS_DB', str(ROOT / 'runtime' / 'gitops_model.sqlite3')))
@@ -220,11 +218,7 @@ class Handler(BaseHTTPRequestHandler):
a = self.require_write(['owner']);
if not a: return
ver=data.get('version') or data.get('tag')
try:
validate_release_version(ver)
except ValueError:
self.send_json({'error':'version must match VxRxCx'},400)
return
if not re.match(r'^V\d+R\d+C\d+(-[a-zA-Z0-9_.-]+)?$', ver or ''): self.send_json({'error':'version must match VxRxCx'},400); return
msg=data.get('message') or f'release {ver}'; out=git_cmd(['tag','-a',ver,'-m',msg]);
if data.get('push',False): git_cmd(['push','origin',ver])
level='V' if re.match(r'^V[1-9]\d*R0C0', ver) else ('R' if re.match(r'^V\d+R[1-9]\d*C0', ver) else 'C'); exec_sql('INSERT OR REPLACE INTO gitops_versions(version,tag,level,summary,status,created_by,created_at) VALUES(?,?,?,?,?,?,?)',(ver,ver,level,msg,'ready',a['username'],now())); exec_sql('UPDATE gitops_baseline SET active_version=?,active_tag=?,updated_at=? WHERE id=?',(ver,ver,now(),'current')); audit(a['username'],'git_tag',ver,{'level':level}); self.send_json({'status':'tagged','version':ver,'git':out}); return
+4 -16
View File
@@ -23,7 +23,6 @@ from gitea_tag_publication import (
TagPublisher,
redact,
validate_version,
validate_gitea_api_url,
)
from orchestrator.action_monitor import ActionMonitor
from silent_release_hardening import (
@@ -320,7 +319,8 @@ def resolve_commit_oid(value: str | None, field: str = 'target_oid') -> str:
raise ValueError(f'{field} does not resolve to the exact approved commit') from exc
def existing_tag_target(tag: str) -> str | None:
validate_version(tag)
if not re.fullmatch(r'V\d+R\d+C\d+(?:-[A-Za-z0-9_.-]+)?$', str(tag or '')):
raise ValueError('invalid release version')
try:
result = run_authoritative_git(
['rev-parse', '--verify', f'refs/tags/{tag}^{{commit}}'],
@@ -553,15 +553,7 @@ class H(BaseHTTPRequestHandler):
return self.sendj({'error':redact(exc)},413)
except (AuthoritativeGitError, RuntimeError) as exc:
return self.sendj({'error':redact(exc)},503)
payload = {'from': frm, 'to': to, 'diff': diff}
payload.update({
key: diff[key]
for key in (
'from_oid', 'to_oid', 'changed_paths', 'stats',
'total_size_bytes', 'truncated', 'patch_limit_bytes',
)
})
return self.sendj(payload)
return self.sendj({'from':frm,'to':to,'diff':diff})
if path in ('/api/patch','/api/patches','/api/branches','/patch'):
snapshot = repository_snapshot(
git_dir=AUTHORITATIVE_GIT_DIR,
@@ -830,10 +822,7 @@ class H(BaseHTTPRequestHandler):
a=self.need('tag:create');
if not a: return
ver=data.get('version') or data.get('tag')
try:
validate_version(ver)
except ValueError:
return self.sendj({'error':'version must match VxRxCx'},400)
if not re.match(r'^V\d+R\d+C\d+(-[A-Za-z0-9_.-]+)?$', ver or ''): return self.sendj({'error':'version must match VxRxCx'},400)
if not data.get('target_oid') or not data.get('evidence_closure_oid') or not data.get('approval_plan_id'):
return self.sendj({
'error':'production_tag_evidence_required',
@@ -1027,7 +1016,6 @@ def api_spec():
def main():
validate_gitea_api_url(os.getenv('AIOS_GITOPS_GITEA_API_URL', ''))
print(f'AIOS ChatGPT GitOps adapter listening on {HOST}:{PORT}')
ThreadingHTTPServer((HOST,PORT), H).serve_forever()
if __name__ == '__main__': main()
+2 -65
View File
@@ -4,7 +4,6 @@ from __future__ import annotations
import os
import re
import subprocess
import time
import urllib.parse
from pathlib import Path
from typing import Iterable
@@ -307,76 +306,14 @@ def authoritative_diff(
from_oid = validate_git_revision(from_revision, "from", git_dir=path)
to_oid = validate_git_revision(to_revision, "to", git_dir=path)
args = ["diff", "--no-ext-diff", "--no-textconv", from_oid, to_oid]
patch_limit = max(1, min(_MAX_OUTPUT, 256 * 1024))
process = subprocess.Popen(
["git", "--git-dir", str(path), *args],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
env={**os.environ, "GIT_OPTIONAL_LOCKS": "0"},
)
chunks: list[bytes] = []
stored_bytes = 0
total_bytes = 0
started = time.monotonic()
try:
while process.stdout is not None:
chunk = process.stdout.read(64 * 1024)
if not chunk:
break
total_bytes += len(chunk)
if stored_bytes < patch_limit:
remaining = patch_limit - stored_bytes
selected = chunk[:remaining]
chunks.append(selected)
stored_bytes += len(selected)
if time.monotonic() - started > _TIMEOUT:
process.kill()
raise AuthoritativeGitError("GIT_REF_QUERY_TIMEOUT")
stderr = (
process.stderr.read(8192).decode("utf-8", errors="replace")
if process.stderr is not None
else ""
)
returncode = process.wait(timeout=max(0.1, _TIMEOUT - (time.monotonic() - started)))
except (OSError, subprocess.TimeoutExpired) as exc:
process.kill()
process.wait()
raise AuthoritativeGitError("GIT_REF_QUERY_FAILED") from exc
if returncode != 0:
raise AuthoritativeGitError(_redact(stderr.strip() or "GIT_REF_QUERY_FAILED")[:200])
try:
names_result = run_authoritative_git(
["diff", "--no-ext-diff", "--no-textconv", "--name-status", from_oid, to_oid],
git_dir=path,
)
changed_paths = [
line.split("\t", 1)[-1]
for line in names_result.stdout.splitlines()
if line.strip()
]
except AuthoritativeGitOutputTooLarge:
changed_paths = []
try:
stats_result = run_authoritative_git(
["diff", "--no-ext-diff", "--no-textconv", "--stat", from_oid, to_oid],
git_dir=path,
)
stats = stats_result.stdout.strip()
except AuthoritativeGitOutputTooLarge:
stats = "GIT_STAT_OUTPUT_TRUNCATED"
patch = b"".join(chunks).decode("utf-8", errors="replace")
result = run_authoritative_git(args, git_dir=path)
return {
"code": 0,
"stdout": patch,
"stdout": result.stdout,
"stderr": "",
"cmd": ["git", f"--git-dir={path}", *args],
"from_oid": from_oid,
"to_oid": to_oid,
"changed_paths": changed_paths,
"stats": stats,
"total_size_bytes": total_bytes,
"truncated": total_bytes > patch_limit,
"patch_limit_bytes": patch_limit,
}
+7 -16
View File
@@ -8,7 +8,6 @@ import json
import os
import re
import subprocess
import sys
import tempfile
import time
import urllib.error
@@ -24,15 +23,10 @@ try:
from authoritative_git import AuthoritativeGitError, run_authoritative_git
except ModuleNotFoundError:
from .authoritative_git import AuthoritativeGitError, run_authoritative_git
try:
from release_version import RELEASE_VERSION_RE, validate_release_version
except ModuleNotFoundError:
sys.path.insert(0, str(Path(__file__).resolve().parents[2]))
from release_version import RELEASE_VERSION_RE, validate_release_version
OID_RE = re.compile(r"^[0-9a-f]{40}$")
VERSION_RE = RELEASE_VERSION_RE
VERSION_RE = re.compile(r"^V\d+R\d+C\d+(?:-[A-Za-z0-9_.-]+)?$")
DEPLOYMENT_EVIDENCE_RE = re.compile(r"^(?:sha256:[0-9a-f]{64}|git:[0-9a-f]{40})$")
FINGERPRINT_FIELDS = (
"version",
@@ -89,14 +83,9 @@ class PublicationManualOwnerReviewRequired(
def validate_version(version: str) -> str:
return validate_release_version(version)
def validate_gitea_api_url(api_url: str) -> str:
value = str(api_url or "").strip().rstrip("/")
parsed = urllib.parse.urlsplit(value)
if parsed.scheme not in {"http", "https"} or not parsed.netloc:
raise ValueError("Gitea API URL must be HTTP(S)")
value = str(version or "")
if not VERSION_RE.fullmatch(value):
raise ValueError("invalid release version")
return value
@@ -422,11 +411,13 @@ class GiteaTagClient:
timeout: float = 15,
token_line: int = 1,
):
self.api_url = validate_gitea_api_url(api_url)
self.api_url = api_url.rstrip("/")
self.repository = repository.strip("/")
self.token_file = token_file
self.timeout = timeout
self.token_line = token_line
if not self.api_url.startswith(("https://", "http://")):
raise ValueError("Gitea API URL must be HTTP(S)")
if self.repository.count("/") != 1:
raise ValueError("Gitea repository must be owner/name")
+2 -6
View File
@@ -34,8 +34,6 @@ from gitea_tag_publication import (
TagPublisher,
)
from release_version import validate_release_version
OID_RE = re.compile(r"^[0-9a-f]{40}$")
PLAN_RE = re.compile(r"^sha256:[0-9a-f]{64}$")
@@ -46,10 +44,8 @@ def load_request() -> dict[str, Any]:
raise ValueError("request_object_required")
if value.get("schema") != "aios.production-executor-request.v1":
raise ValueError("request_schema_invalid")
try:
validate_release_version(value.get("version", ""))
except ValueError as exc:
raise ValueError("version_invalid") from exc
if not re.fullmatch(r"V\d+R\d+C\d+(?:-[A-Za-z0-9_.-]+)?", str(value.get("version", ""))):
raise ValueError("version_invalid")
for name in ("target_oid", "evidence_closure_oid"):
if not OID_RE.fullmatch(str(value.get(name, ""))):
raise ValueError(f"{name}_invalid")
@@ -27,8 +27,6 @@ import urllib.request
from pathlib import Path
from typing import Any, Mapping
from release_version import validate_release_version
OID_RE = re.compile(r"^[0-9a-f]{40}$")
SHA_RE = re.compile(r"^[0-9a-f]{64}$")
PLAN_RE = re.compile(r"^sha256:[0-9a-f]{64}$")
@@ -88,10 +86,8 @@ def load_request() -> dict[str, Any]:
raise ValueError("request_schema_invalid")
if value.get("repository_identity") != "aios/aios":
raise ValueError("repository_identity_invalid")
try:
validate_release_version(value.get("version", ""))
except ValueError as exc:
raise ValueError("version_invalid") from exc
if not re.fullmatch(r"V\d+R\d+C\d+(?:-[A-Za-z0-9_.-]+)?", str(value.get("version", ""))):
raise ValueError("version_invalid")
for name in ("target_oid", "evidence_closure_oid"):
if not OID_RE.fullmatch(str(value.get(name, ""))):
raise ValueError(f"{name}_invalid")
@@ -22,12 +22,11 @@ from enum import Enum
from pathlib import Path
from typing import Any, Protocol
from release_version import validate_release_version
OID_RE = re.compile(r"^[0-9a-f]{40}$")
SHA256_RE = re.compile(r"^[0-9a-f]{64}$")
CANDIDATE_ID_RE = re.compile(r"^[A-Za-z0-9][A-Za-z0-9_.-]{2,127}$")
VERSION_RE = re.compile(r"^V\d+R\d+C\d+(?:-[A-Za-z0-9_.-]+)?$")
SECRET_PATTERNS = (
re.compile(r"(?i)(authorization\s*[:=]\s*)(?:bearer|token)\s+\S+"),
re.compile(r"(?i)((?:access_)?token\s*[:=]\s*)\S+"),
@@ -770,10 +769,8 @@ class TagApprovalEnvelope:
def validate(self) -> None:
if self.action != "production_tag":
raise ApprovalError("approval does not authorize production tag")
try:
validate_release_version(self.version)
except ValueError as exc:
raise ApprovalError("invalid release version") from exc
if not VERSION_RE.fullmatch(self.version):
raise ApprovalError("invalid release version")
_validate_oid("tag_oid", self.tag_oid)
_validate_oid("target_oid", self.target_oid)
if _parse_utc(self.expires_at) <= datetime.now(timezone.utc):
@@ -0,0 +1,18 @@
# Non-secret staging configuration for the OpenClaw engineering backend.
#
# Discover the exact binary with `command -v openclaw`.
# Do not point this at the chat/WeChat Gateway HTTP endpoint.
AIOS_AGENT_RUNTIME_BACKEND_ARGV_JSON='["/usr/bin/python3","/usr/local/lib/aios-agent-runtime-broker/openclaw_engineering_backend.py"]'
# Required: exact absolute path discovered on hw.imruyi.top.
AIOS_OPENCLAW_BIN=/REPLACE/WITH/ABSOLUTE/OPENCLAW/PATH
# Required: provider/model verified on the installed OpenClaw build.
AIOS_OPENCLAW_MODEL=REPLACE_WITH_VERIFIED_PROVIDER_MODEL
# Non-secret map: provider environment variable -> systemd credential file name.
# Example after separately projecting an encrypted credential:
# AIOS_OPENCLAW_PROVIDER_CREDENTIAL_MAP_JSON='{"OPENAI_API_KEY":"openai_api_key"}'
AIOS_OPENCLAW_PROVIDER_CREDENTIAL_MAP_JSON='{}'
AIOS_OPENCLAW_EXEC_TIMEOUT_SECONDS=1800
@@ -0,0 +1,66 @@
#!/bin/sh
set -eu
if [ "$(id -u)" -ne 0 ]; then
echo "must run as root" >&2
exit 20
fi
SRC_ROOT="${1:-.}"
WORKER_SRC="$SRC_ROOT/services/aios-autonomous-agent-worker"
BROKER_SRC="$SRC_ROOT/services/aios-agent-runtime-broker"
UNIT_SRC="$SRC_ROOT/deploy/systemd"
grep -Fq 'AIOS_GOVERNED_WRITE_API_URL=http://127.0.0.1:19998' \
"$UNIT_SRC/aios-autonomous-agent-worker.service" || {
echo "staging bridge binding missing" >&2
exit 21
}
getent group aios-agent-runtime >/dev/null 2>&1 || groupadd --system aios-agent-runtime
id aios-agent-runtime >/dev/null 2>&1 || \
useradd --system --gid aios-agent-runtime --home-dir /nonexistent --shell /usr/sbin/nologin aios-agent-runtime
id aios-autonomous-agent-worker >/dev/null 2>&1 || \
useradd --system --home-dir /nonexistent --shell /usr/sbin/nologin aios-autonomous-agent-worker
usermod -a -G aios-agent-runtime aios-autonomous-agent-worker
install -d -m 0755 /usr/local/lib/aios-autonomous-agent-worker
install -d -m 0755 /usr/local/lib/aios-agent-runtime-broker
install -d -m 0750 -o aios-autonomous-agent-worker -g aios-autonomous-agent-worker \
/var/lib/aios-autonomous-agent-worker
install -d -m 0770 -o aios-autonomous-agent-worker -g aios-agent-runtime \
/var/lib/aios-autonomous-agent-worker/workspaces
for file in autonomous_worker.py intent_runtime.py runtime_client.py register.py runtime_selftest.py; do
install -m 0755 "$WORKER_SRC/$file" "/usr/local/lib/aios-autonomous-agent-worker/$file"
done
install -m 0755 "$BROKER_SRC/broker.py" /usr/local/lib/aios-agent-runtime-broker/broker.py
install -m 0755 "$BROKER_SRC/openclaw_engineering_backend.py" /usr/local/lib/aios-agent-runtime-broker/openclaw_engineering_backend.py
install -m 0644 "$BROKER_SRC/RUNTIME_CONTRACT.md" /usr/local/lib/aios-agent-runtime-broker/RUNTIME_CONTRACT.md
install -m 0644 "$UNIT_SRC/aios-autonomous-agent-worker.service" /etc/systemd/system/
install -m 0644 "$UNIT_SRC/aios-autonomous-agent-register.service" /etc/systemd/system/
install -m 0644 "$UNIT_SRC/aios-agent-runtime-broker@.service" /etc/systemd/system/
systemctl daemon-reload
cat <<'EOF'
Files installed. Services were NOT started or enabled.
Required before activation:
1. Discover `openclaw` with `command -v openclaw`.
2. Verify `openclaw agent exec --help` exposes the required headless coding
options on the installed version.
3. Create /etc/aios/agent-runtime-openclaw.conf from the non-secret example.
4. Project provider/model credentials into the OpenClaw broker service with a
separate systemd encrypted credential. Never reuse the Online Agent Task
token or writer lease.
5. Start only `aios-agent-runtime-broker@openclaw.service`.
6. Run runtime_selftest.py and require a real worktree mutation with unchanged
Git HEAD.
7. Only after self-test PASS register the autonomous identity and enable
continuous polling.
The autonomous worker is staging-only and rejects any Governed Bridge other
than http://127.0.0.1:19998.
EOF
@@ -0,0 +1,42 @@
[Unit]
Description=AIOS isolated %i engineering runtime broker
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
User=aios-agent-runtime
Group=aios-agent-runtime
Environment=PYTHONUNBUFFERED=1
Environment=AIOS_AGENT_RUNTIME_ADAPTER=%i
Environment=AIOS_AGENT_RUNTIME_SOCKET=/run/aios-agent-runtime/%i.sock
Environment=AIOS_AGENT_RUNTIME_WORKSPACE_ROOT=/var/lib/aios-autonomous-agent-worker/workspaces
Environment=AIOS_AGENT_RUNTIME_ALLOWED_PEER_USER=aios-autonomous-agent-worker
Environment=AIOS_AGENT_RUNTIME_CONNECTION_TIMEOUT_SECONDS=15
Environment=AIOS_AGENT_RUNTIME_BACKEND_TIMEOUT_SECONDS=3600
EnvironmentFile=/etc/aios/agent-runtime-%i.conf
ExecStart=/usr/bin/python3 -B /usr/local/lib/aios-agent-runtime-broker/broker.py
Restart=always
RestartSec=5
RuntimeDirectory=aios-agent-runtime
RuntimeDirectoryMode=0770
UMask=0007
NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=strict
ProtectHome=true
ProtectKernelTunables=true
ProtectKernelModules=true
ProtectControlGroups=true
LockPersonality=true
RestrictSUIDSGID=true
RestrictRealtime=true
RestrictNamespaces=true
RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6
ReadWritePaths=/var/lib/aios-autonomous-agent-worker/workspaces /run/aios-agent-runtime
InaccessiblePaths=-/data/aios-gitops/repo.git -/etc/credstore.encrypted/aios-autonomous-agent-worker
CapabilityBoundingSet=
AmbientCapabilities=
[Install]
WantedBy=multi-user.target
@@ -0,0 +1,5 @@
# Example only. Choose the credential name/provider used by the verified model.
# The encrypted credential file contains secret material and must never be committed.
[Service]
# Example:
# LoadCredentialEncrypted=openai_api_key:/etc/credstore.encrypted/aios-agent-runtime-openclaw/openai_api_key
@@ -0,0 +1,38 @@
[Unit]
Description=AIOS Autonomous Intent Agent register/recover
After=network-online.target aios-agent-task-api.service aios-agent-runtime-broker@openclaw.service
Wants=network-online.target
Requires=aios-agent-runtime-broker@openclaw.service
Before=aios-autonomous-agent-worker.service
[Service]
Type=oneshot
RemainAfterExit=yes
User=root
Group=root
LoadCredential=bridge_control_token:/run/credentials/aios-gitops-model-api.service/agent_task_control_token
Environment=AIOS_AGENT_TASK_API_URL=http://127.0.0.1:18940
Environment=AIOS_AGENT_ID=online-engineering-worker-cn-network-mkt-autonomous-20260815
Environment=AIOS_AGENT_VERSION=2026-08-15.4-autonomous-intent
Environment=AIOS_WORKER_REGISTRATION_STATE=/var/lib/aios-autonomous-agent-worker/registration
Environment=AIOS_WORKER_ENCRYPTED_CREDENTIAL=/etc/credstore.encrypted/aios-autonomous-agent-worker/agent_token
Environment=AIOS_HERMES_BROKER_SOCKET=/run/aios-agent-runtime/hermes.sock
Environment=AIOS_OPENCLAW_BROKER_SOCKET=/run/aios-agent-runtime/openclaw.sock
ExecStart=/usr/bin/python3 -B /usr/local/lib/aios-autonomous-agent-worker/register.py
TimeoutStartSec=120
UMask=0077
NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=strict
ProtectHome=true
ProtectKernelTunables=true
ProtectKernelModules=true
ProtectControlGroups=true
LockPersonality=true
RestrictSUIDSGID=true
RestrictRealtime=true
RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6
ReadWritePaths=/etc/credstore.encrypted /var/lib/aios-autonomous-agent-worker
[Install]
WantedBy=multi-user.target
@@ -0,0 +1,56 @@
[Unit]
Description=AIOS Autonomous Intent Engineering Worker (staging)
After=network-online.target aios-agent-task-api.service aios-agent-runtime-broker@openclaw.service aios-autonomous-agent-register.service
Wants=network-online.target aios-agent-runtime-broker@openclaw.service
Requires=aios-autonomous-agent-register.service
[Service]
Type=simple
User=aios-autonomous-agent-worker
Group=aios-autonomous-agent-worker
SupplementaryGroups=aios-agent-runtime gitops
LoadCredentialEncrypted=agent_token:/etc/credstore.encrypted/aios-autonomous-agent-worker/agent_token
Environment=PYTHONUNBUFFERED=1
Environment=AIOS_AGENT_TASK_API_URL=http://127.0.0.1:18940
Environment=AIOS_GOVERNED_WRITE_API_URL=http://127.0.0.1:19998
Environment=AIOS_AGENT_ID=online-engineering-worker-cn-network-mkt-autonomous-20260815
Environment=AIOS_AGENT_VERSION=2026-08-15.4-autonomous-intent
Environment=AIOS_AUTONOMOUS_REPOSITORY=aios/aios
Environment=AIOS_AUTONOMOUS_BASE_REF=refs/heads/main
Environment=AIOS_AUTHORITY_GIT_DIR=/data/aios-gitops/repo.git
Environment=AIOS_AUTONOMOUS_STATE_ROOT=/var/lib/aios-autonomous-agent-worker
Environment=AIOS_AUTONOMOUS_WORKSPACE_ROOT=/var/lib/aios-autonomous-agent-worker/workspaces
Environment=AIOS_AUTONOMOUS_POLL_SECONDS=5
Environment=AIOS_AUTONOMOUS_HEARTBEAT_SECONDS=10
Environment=AIOS_AUTONOMOUS_LEASE_SECONDS=300
Environment=AIOS_AUTONOMOUS_RUNTIME_TIMEOUT_SECONDS=3600
Environment=AIOS_AUTONOMOUS_DEFAULT_ADAPTER=openclaw
Environment=AIOS_AGENT_RUNTIME_SHARED_GROUP=aios-agent-runtime
Environment=AIOS_HERMES_BROKER_SOCKET=/run/aios-agent-runtime/hermes.sock
Environment=AIOS_OPENCLAW_BROKER_SOCKET=/run/aios-agent-runtime/openclaw.sock
ExecStart=/usr/bin/python3 -B /usr/local/lib/aios-autonomous-agent-worker/autonomous_worker.py
Restart=always
RestartSec=5
TimeoutStopSec=30
UMask=0027
NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=strict
ProtectHome=true
ProtectKernelTunables=true
ProtectKernelModules=true
ProtectControlGroups=true
LockPersonality=true
RestrictSUIDSGID=true
RestrictRealtime=true
RestrictNamespaces=true
MemoryDenyWriteExecute=true
RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6
ReadOnlyPaths=/data/aios-gitops/repo.git
ReadWritePaths=/var/lib/aios-autonomous-agent-worker
InaccessiblePaths=-/etc/credstore.encrypted
CapabilityBoundingSet=
AmbientCapabilities=
[Install]
WantedBy=multi-user.target
@@ -4,25 +4,14 @@ After=network-online.target
[Service]
Type=simple
User=aiman
Group=gitops
PermissionsStartOnly=true
WorkingDirectory=@RUNTIME_ROOT@
User=aios-bridge-staging
Group=aios-bridge-staging
WorkingDirectory=/opt/aios/gitops-control-plane/current
Environment=AIOS_BRIDGE_SCHEMA_VERSION=aios.chatgpt-gitops-bridge.v2
Environment=AIOS_GITOPS_WRITES_ENABLED=false
Environment=AIOS_GITOPS_DURABLE_JOB_EXECUTION_ENABLED=true
Environment=AIOS_GITOPS_WRITES_ENABLED=false
Environment=AIOS_GITEA_API_BASE=https://g.cnnetmkt.top/git/api/v1
Environment=AIOS_GITEA_TOKEN_FILE=/run/credentials/aios-chatgpt-bridge-v2-staging.service/gitea_token
Environment=AIOS_GITEA_REMOTE=gitea-http
Environment=AIOS_CONTROL_PLANE_TARGET_VERSION=V2R2C1
Environment=AIOS_CONTROL_PLANE_TARGET_OID=@SOURCE_OID@
Environment=AIOS_CONTROL_PLANE_ARTIFACT_SHA256=@ARTIFACT_SHA256@
Environment=PYTHONPATH=@RUNTIME_ROOT@:@RUNTIME_ROOT@/app:@RUNTIME_ROOT@/runtime-agent-os
Environment=AIOS_DEPLOYMENT_STATE=STAGING
LoadCredential=gitea_token:/run/credentials/aios-governance-executor.service/gitea-governance-executor-token
ExecStartPre=/usr/bin/install -d -o aiman -g gitops -m 0750 /var/lib/aios-gitops/bridge-v2-staging
ExecStart=/usr/bin/python3 -B @RUNTIME_ROOT@/app/aios_chatgpt_bridge_v2_server.py
ExecStart=/usr/bin/python3 /opt/aios/gitops-control-plane/current/bridge-v2-staging-server.py
NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=strict
@@ -7,9 +7,6 @@ Wants=network-online.target
Type=simple
User=aiman
Group=gitops
PermissionsStartOnly=true
ExecStartPre=/usr/bin/install -d -o aiman -g gitops -m 0750 /var/lib/aios-gitops/model-api-staging
ExecStartPre=/usr/bin/install -d -o aiman -g gitops -m 0750 /var/lib/aios-gitops/model-api-staging/silent-release
WorkingDirectory=@RUNTIME_ROOT@
Environment=PYTHONUNBUFFERED=1
Environment=PYTHONDONTWRITEBYTECODE=1
@@ -30,16 +27,10 @@ Environment=AIOS_GITOPS_DEPLOYMENT_STATE=STAGING
Environment=AIOS_GITOPS_MODEL_API_SERVICE=aios-gitops-model-api-v2r2c1-staging.service
Environment=AIOS_SILENT_RELEASE_EXECUTION_ENABLED=false
Environment=AIOS_SILENT_RELEASE_STATE_ROOT=/var/lib/aios-gitops/model-api-staging/silent-release
Environment=AIOS_GITOPS_GITEA_API_URL=https://g.cnnetmkt.top/git/api/v1
Environment=AIOS_GITOPS_GITEA_REPOSITORY=aios/aios
Environment=AIOS_GITOPS_GITEA_REMOTE=gitea-http
Environment=AIOS_GITOPS_GITEA_TOKEN_FILE=/run/credentials/aios-gitops-model-api-v2r2c1-staging.service/gitea_token
Environment=AIOS_GITOPS_GITEA_TOKEN_LINE=1
LoadCredential=gitea_token:/run/credentials/aios-governance-executor.service/gitea-governance-executor-token
Environment=AIOS_BRIDGE_V2_URL=http://127.0.0.1:18892
Environment=AIOS_GITOPS_HOST=127.0.0.1
Environment=AIOS_GITOPS_PORT=18893
Environment=PYTHONPATH=@RUNTIME_ROOT@:@RUNTIME_ROOT@/app:@RUNTIME_ROOT@/runtime-agent-os
Environment=PYTHONPATH=@RUNTIME_ROOT@/app:@RUNTIME_ROOT@/runtime-agent-os
ExecStart=/usr/bin/python3 @RUNTIME_ROOT@/app/aios_model_api_staging.py
Restart=on-failure
RestartSec=2
@@ -0,0 +1,46 @@
[Unit]
Description=AIOS generic governed Online Agent Worker (2026-08-15)
After=aios-agent-task-api.service aios-online-agent-worker-register-20260815.service
Wants=aios-online-agent-worker-register-20260815.service
[Service]
Type=simple
User=aios-engineering-worker
Group=aios-engineering-worker
SupplementaryGroups=gitops
LoadCredentialEncrypted=agent_token:/etc/credstore.encrypted/aios-online-agent-worker-20260815/agent_token
Environment=PYTHONUNBUFFERED=1
Environment=AIOS_AGENT_TASK_API_URL=http://127.0.0.1:18940
Environment=AIOS_GOVERNED_WRITE_API_URL=http://127.0.0.1:18892
Environment=AIOS_AGENT_ID=online-engineering-worker-cn-network-mkt-generic-20260815
Environment=AIOS_AGENT_TYPE=trusted-online-engineering-worker
Environment=AIOS_AGENT_VERSION=2026-08-15.3-generic
Environment=AIOS_AUTHORITY_GIT_DIR=/data/aios-gitops/repo.git
Environment=AIOS_WORKER_STATE_ROOT=/var/lib/aios-online-agent-worker-20260815
Environment=AIOS_WORKER_WORKSPACE_ROOT=/var/lib/aios-online-agent-worker-20260815/workspaces
Environment=AIOS_WORKER_POLL_SECONDS=5
Environment=AIOS_WORKER_HEARTBEAT_SECONDS=10
Environment=AIOS_WORKER_LEASE_SECONDS=300
ExecStart=/usr/bin/python3 -B /usr/local/lib/aios-online-agent-worker-20260815/online_agent_worker.py
Restart=always
RestartSec=5
TimeoutStopSec=15
NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=strict
ProtectHome=true
ProtectKernelTunables=true
ProtectKernelModules=true
ProtectControlGroups=true
LockPersonality=true
RestrictSUIDSGID=true
RestrictRealtime=true
RestrictNamespaces=true
MemoryDenyWriteExecute=true
RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6
ReadOnlyPaths=/data/aios-gitops/repo.git
ReadWritePaths=/var/lib/aios-online-agent-worker-20260815
UMask=0027
[Install]
WantedBy=multi-user.target
@@ -0,0 +1,36 @@
[Unit]
Description=AIOS generic Online Agent register-or-recover (2026-08-15)
After=network-online.target aios-gitops-model-api.service aios-agent-task-api.service
Wants=network-online.target
Before=aios-online-agent-worker-20260815.service
[Service]
Type=oneshot
RemainAfterExit=yes
User=root
Group=root
LoadCredential=bridge_control_token:/run/credentials/aios-gitops-model-api.service/agent_task_control_token
Environment=AIOS_AGENT_TASK_API_URL=http://127.0.0.1:18940
Environment=AIOS_AGENT_ID=online-engineering-worker-cn-network-mkt-generic-20260815
Environment=AIOS_AGENT_TYPE=trusted-online-engineering-worker
Environment=AIOS_AGENT_VERSION=2026-08-15.3-generic
Environment=AIOS_WORKER_ENCRYPTED_CREDENTIAL=/etc/credstore.encrypted/aios-online-agent-worker-20260815/agent_token
Environment=AIOS_WORKER_REGISTRATION_STATE=/var/lib/aios-online-agent-worker-20260815/registration
ExecStart=/usr/bin/python3 -B /usr/local/lib/aios-online-agent-worker-20260815/register.py
NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=strict
ProtectHome=true
ProtectKernelTunables=true
ProtectKernelModules=true
ProtectControlGroups=true
LockPersonality=true
RestrictSUIDSGID=true
RestrictRealtime=true
RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6
ReadWritePaths=/etc/credstore.encrypted /var/lib/aios-online-agent-worker-20260815
UMask=0077
TimeoutStartSec=120
[Install]
WantedBy=multi-user.target
+110
View File
@@ -0,0 +1,110 @@
# Autonomous Intent Worker v2
## Goal
A Custom GPT or other trusted producer creates a prose engineering task with:
- `goal`
- `instructions`
- `success_criteria`
- `required_capabilities`
A long-running autonomous worker claims only tasks explicitly marked with:
- `intent.task`
- `engineering.execute`
and only when the requested engineering runtime capability is actually
available, e.g. `agent.runtime.hermes`.
The runtime edits a disposable worktree. The worker independently inspects the
diff, scans for secrets, reruns verification, and persists through staging
Governed GitOps.
## Security domains
The Online Agent worker owns task/lease/governed-write credentials.
The runtime broker is a separate service identity. It authorizes the worker
over Unix `SO_PEERCRED`, opens the worktree using `O_PATH|O_NOFOLLOW`, and
passes a pinned `/proc/self/fd/<fd>` path to the engineering backend. The
backend never receives the Agent Task token or writer lease.
The worker rejects:
- a runtime-created Git commit (`HEAD != base_oid`);
- direct authoritative ref mutation/push;
- sensitive credential/key paths;
- obvious secret material;
- non-staging Governed Bridge endpoints.
## Staging boundary
`autonomous_worker.py` intentionally rejects any Governed Write API base other
than:
`http://127.0.0.1:19998`
Production/main/tag/baseline/force-push writes remain outside this worker.
## Migration protection
The worker does not infer intent from prose. Historical tasks without
`intent.task` are skipped before claim, so they do not lose an attempt.
The bootstrap task
`implement-autonomous-intent-worker-v2-20260815` currently lacks the intent
marker and is therefore intentionally not claimable by this worker. That is a
control-plane/bootstrap concern, not a reason to weaken the runtime rule.
## Runtime requirement
A chat/WeChat Hermes endpoint is not enough. A compliant engineering backend
must:
1. accept `aios.online-agent.runtime-request.v1`;
2. inspect and mutate the supplied disposable worktree;
3. leave Git HEAD unchanged;
4. return `aios.online-agent.runtime-result.v1`.
The exact installed Hermes/Codex interface is host-specific and must be
discovered before configuring the broker. No CLI flags are invented here.
## Activation gate
Do not register or enable the autonomous consumer until:
1. a real engineering backend is found;
2. broker is configured with that backend;
3. `runtime_selftest.py` reports PASS;
4. backend has no Agent Task token or writer lease.
After activation, complete D/E/F/G:
- D: native intent task, no `metadata.execution`;
- E: goal-only engineering task, runtime finds files/root cause;
- F: unavailable runtime capability remains unclaimed;
- G: restart during real runtime execution, no duplicate commit/branch.
## OpenClaw backend candidate
A current OpenClaw installation can be used as an autonomous engineering
runtime without making OpenClaw itself an AIOS Task API client.
The bundled wrapper uses the official headless coding entry point:
```text
openclaw agent exec
--message-file -
--cwd <pinned-disposable-worktree>
--model <verified-provider/model>
--auth-env-only
--timeout <seconds>
--json
```
Reference used for implementation:
`https://docs.openclaw.ai/cli/agent`
The wrapper maps the stable OpenClaw JSON envelope into
`aios.online-agent.runtime-result.v1`. The AIOS worker remains responsible for
claim/heartbeat, independent diff verification and Governed GitOps.
OpenClaw is advertised as `agent.runtime.openclaw`, not as Hermes or Codex.
-42
View File
@@ -241,48 +241,6 @@ components:
properties:
candidate_id: {type: string}
paths:
/v2/commit-files:
post:
operationId: commitFilesGoverned
summary: Submit one atomic governed commit containing multiple files
requestBody:
required: true
content:
application/json:
schema:
type: object
additionalProperties: false
required:
- task_id
- repository
- expected_branch_oid
- target_branch
- generation
- idempotency_key
- files
- message
properties:
task_id: {type: string}
repository: {type: string, example: aios/aios}
expected_branch_oid: {type: string, pattern: '^[0-9a-f]{40}$'}
target_branch: {type: string, pattern: '^agent/[A-Za-z0-9._/-]+$'}
generation: {type: integer, minimum: 1}
idempotency_key: {type: string}
files:
type: array
minItems: 1
items:
type: object
additionalProperties: false
required: [path, content_base64]
properties:
path: {type: string, minLength: 1, maxLength: 512}
content_base64: {type: string}
message: {type: string, minLength: 1, maxLength: 200}
responses:
'200': {description: Governed commit job and verified branch result}
'400': {description: Contract or policy rejection}
'409': {description: Branch CAS or idempotency conflict}
/createReleaseIntent:
post:
operationId: createReleaseIntent
-21
View File
@@ -1,21 +0,0 @@
"""Canonical AIOS release-version parser shared by runtime and release tools."""
from __future__ import annotations
import re
# The suffix preserves already-supported annotated release names while the
# canonical VnRnCn namespace remains accepted exactly.
RELEASE_VERSION_PATTERN = r"^V[0-9]+R[0-9]+C[0-9]+(?:-[A-Za-z0-9_.-]+)?$"
RELEASE_VERSION_RE = re.compile(RELEASE_VERSION_PATTERN)
def parse_release_version(version: object) -> str:
value = str(version or "").strip()
if not RELEASE_VERSION_RE.fullmatch(value):
raise ValueError("invalid release version")
return value
def validate_release_version(version: object) -> str:
return parse_release_version(version)
@@ -1,75 +0,0 @@
# Action Contract Regression Remediation — 2026-08-25
## Scope
Governed task: `fix-action-contracts-20260825-1407`
Base: `3244f0f17f8845592ac94d585f5ebd65693c9f29`
Target branch: `agent/fix-action-contracts-20260825-1407`
## Verified regressions
1. `getReleaseConsistency` and `runReleaseAcceptance` fail before evaluation with:
`Gitea API URL must be HTTP(S)`.
V2R2C1 release evidence at `a8b906d1bc24088c19647e98de6a679268f6d0a2`
proves Gitea API readback passed for target `998d30304787e31e5ae607c0bd86b5edb2b0e5ad`.
Therefore current staging/contract overlay configuration has regressed.
2. `getReleaseSaga` fails with:
`Permission denied: '/var/lib/aios-gitops/silent-release'`.
Runtime service identity must have read/traverse access to the durable release state root.
Fix ownership/group/ACL at deployment time; do not broaden to world-writable permissions.
3. `getTagPublicationStatus` rejects the authoritative approved version `V2R2C1`
as `invalid release version`, while `getGovernedWriteCapabilities` returns the same
version as the approved release scope. The two Action contracts are internally inconsistent.
Release-version validation must accept the canonical AIOS `^V[0-9]+R[0-9]+C[0-9]+$`
namespace (or, preferably, use one shared validator for all release Actions).
4. `commitFilesGoverned` is unusable from the published Action schema.
Runtime capability metadata declares required fields:
`task_id, repository, expected_branch_oid, target_branch, generation, idempotency_key, files, message`,
but the exposed tool contract has no `files` input. Regenerate/publish the Action schema from
the same operation contract source and add a contract-parity test.
5. `reconcileGitOpsJob` returned `DURABLE_BINDING_INVALID` for a completed governed
single-file job that was otherwise read back as `COMPLETED`.
Reconciliation of a valid terminal job should be idempotent/read-only and must not require
a stale or absent active lease. Add a regression case for terminal completed jobs.
6. `getDiff` returns `ResponseTooLargeError` / `GIT_OUTPUT_LIMIT_EXCEEDED` for large diffs.
Add server-side bounded output: path/stat summary plus truncation metadata, or pagination.
The Action must never fail merely because a valid repository diff is large.
## Required implementation changes
- Centralize release-version parsing and reuse it in tag status, candidate, consistency, and acceptance.
- Validate/normalize Gitea API URL at service startup and fail health checks with a configuration-specific diagnostic.
- Ensure the deployment unit projects a valid HTTP(S) Gitea API base URL into the staging overlay.
- Correct durable release-state directory ownership/ACL for the runtime service account.
- Generate the external Action schema from the runtime operation contract; parity-test required fields.
- Make terminal reconciliation idempotent and independent of expired active writer leases.
- Bound diff output and return `truncated=true` with deterministic metadata rather than transport failure.
## Acceptance gates
All must pass on the same deployed overlay:
- `getReleaseConsistency(V2R2C1, 998d303..., a8b906d..., approved_plan)` returns a structured result, not URL error.
- `runReleaseAcceptance(...)` reaches policy evaluation, not URL error.
- `getTagPublicationStatus("V2R2C1")` returns structured publication state.
- `getReleaseSaga(nonexistent_candidate)` returns NOT_FOUND/empty-state semantics, never filesystem permission failure.
- Published `commitFilesGoverned` schema exposes `files` and a two-file isolated branch commit succeeds.
- Reconcile of a completed governed job is idempotent.
- Large diff returns bounded/truncated data rather than transport exception.
- Existing protected-ref controls remain unchanged: no direct main/tag/baseline/production write.
## Proven stable reference
V2R2C1 evidence closure (`a8b906d1bc24088c19647e98de6a679268f6d0a2`) records:
- Gitea Git/API readback PASS
- staging runtime PASS
- restart recovery PASS
- 59 mandatory tests passed / 0 failed
This evidence should be used as the compatibility baseline when repairing the current
`contract-overlay-v2` / `scheme-a-323b615b...` staging layer.
@@ -1,207 +0,0 @@
# Agent Execution Contract — Action Contract Regression Repair
## Objective
Fully repair the current AIOS Action runtime/contract overlay regressions on the single server, without modifying protected refs directly and without introducing new infrastructure. The task is not complete until the deployed staging/action overlay passes all acceptance gates.
## Authority and scope
- Repository: `aios/aios`
- Base at task creation: `3244f0f17f8845592ac94d585f5ebd65693c9f29`
- Governed task: `fix-action-contracts-20260825-1407`
- Target branch: `agent/fix-action-contracts-20260825-1407`
- Protected refs must not be directly written: `main`, release tags, baseline registry, production runtime.
- No new SaaS, database, agent framework, or monitoring platform may be introduced.
## Non-interactive rules
The Agent must run the whole flow unnattended:
1. Collect facts.
2. Determine root causes.
3. Make the smallest correct changes.
4. Run unit/integration/contract tests.
5. Deploy to staging only.
6. Restart only the affected non-production services.
7. Run the full Action smoke suite.
8. On failure, automatically collect logs, diagnose, fix, and retry.
9. If a change causes a new regression, automatically roll back the staging change to the previous known-good configuration and record the failure.
10. Stop only if an owner decision is required for a high-risk, irreversible, or production-boundary action.
## Fact collection — required before editing
Record all of the following in the final evidence bundle:
- `id`, `hostname`, `uname -a`, `date --iso-seconds`
- `git rev-parse --show HEAD` for authority worktree and running overlay source
- `git status --porcelaine=v2`
- `git remote -v`
- current systemd service unit(s), drop-ins, Environment/EnvironmentFile sources for GitOps/Bredge/release services
- `systemctl show <affected-service>` including `User=`, `Group=`, `Environment=`, `EnvironmentFile=`, `ExecStart=`
- resolved Gitea API base URL after env/config expansion; redact secrets but not scheme/host
- `stat -c, `namei -l`, `getfacl`, als -ld on `/var/lib/aios-gitops/silent-release` and its parents
- find Action OpenAPI/schema generator and runtime operation definitions for `commitFilesGoverned`
- find the release version validator(s) and all callers of `getTagPublicationStatus`
- find `reconcileGitOpsJob` implementation and durable binding validation logic
- find `getDiff implementation and transport output limits
Do not edit any file until the actual implementation and configuration sources have been located.
## Repair 1 — Gitea API URL
Symptoms:
- `getReleaseConsistency` → `Gitea API URL must be HTTP(S)`
- `runReleaseAcceptance` → `Gitea API URL must be HTTP(ÊX
Required behavior:
- Determine which env/config key is projected into the current contract overlay.
- Compare the deployed overlay with V2R2C1 known-good configuration/deployment manifests.
- Restore a valid `HTTP://` or `HTTPS://` Gitea API base URL.
- Add startup config validation so invalid schemes fail at health/startup with a config-specific message, not during a release check.
- Never hard-code a different Gitea host if the existing deployment already has a single authoritative source.
## Repair 2 — durable release state permissions
Symptom:
- `getReleaseSaga` → `Permission denied: '/var/lib/aios-gitops/silent-release'`
Required behavior:
- Identify the actual runtime service User/Group.
- Grant only the required traverse/read/write permissions for the durable state directories the service must use.
- Prefer ownership + group mode or a specific ACL; keep the directory non-world-writable.
- Persist the permission in deployment/install logic so restart/reboot does not regress.
- Verify create/read/update of a staging-only saga state file under the service identity.
## Repair 3 — release version contract
Symptom:
- `getTagPublicationStatus("V2R2C1")` → `invalid release version`
- The same runtime exposes `V2R2C1` as `approved_release_version`.
Required behavior:
- Locate all version validators; consolidate onto one canonical parser.
- Accept the AIOS canonical namespace `^V[0-9]+R[0-9]+C[0-9]+$`.
- Preserve any additional already-supported legacy formats unless they are security-sensitive.
- Add contract tests proving that the version accepted by capabilities is accepted by status, candidate, consistency, acceptance, and tag publication.
## Repair 4 — `commitFilesGoverned` schema parity
Symptom:
- Runtime capability metadata requires `files`.
- Published Action tool schema does not expose `files`.
- Call reaches Bridge and fails `BRIDGE_REQUEST_FIELDS_MISSING`.
Required behavior:
- Find the canonical runtime operation contract.
- Generate the external tool schema from the same source of truth; do not manually copy a parallel schema.
- `files` must be required and must represent a list of file objects with at least `path` and `content_base64`.
- Add a parity test that compares runtime required fields with published Action required fields.
- Add a real governed commit test that writes two test files to an isolated `agent/` ibranch and verifies remote OID.
## Repair 5 — terminal reconciliation
Symptom:
- A governed job read back as `COMPLETED' but `reconcileGitOpsJob` failed `DURABLE_BINDING_INVALID`.
Required behavior:
- Reconciling a terminal successful job must be idempotent.
- An expired writer lease must not invalidate a durable binding for a job that is already terminal.
- Active-job reconciliation may require a live lease; terminal read-back must not.
- Add cases for COMPLETED, FAILED, CANCELLED / equivalent terminal states.
## Repair 6 — bounded `getDiff` output
Symptom:
- Large valid diffs fail with `ResponseTooLargeError` or `GIT_OUTPUT_LIMIT_EXCEEDED`.
Required behavior:
- Do not load unbounded diff text into the Action response.
- Return bounded summary data: commit identities, changed paths/stats, total bytes, and `truncated: true` when the full patch exceeds the safe limit.
- If pagination already exists in the contract use it; otherwise prefer deterministic truncation over a breaking schema change.
- Add tests for small, empty, and oversize diffs.
## Staging deployment procedure
1. Run the existing mandatory test suite before modification; record baseline.
2. Apply code and config changes only on the governed branch/worktree.
3. Run focused regression tests.
4. Run the full mandatory suite.
5. Build the staging contract overlay using the repository's existing build/deploy path.
6. Deploy to staging/non-production only.
7. If systemd units are changed: `systemctl daemon-reload`, then restart only the affected staging services.
8. Wait for the existing health endpoint/runtime status to become healthy.
9. Run the acceptance suite below.
If any step fails:
- collect `journalctl` logs and relevant app logs;
- diagnose root cause;
- automatically repair and retry when non-destructive;
- if a new change breaks staging, restore the previous staging artifact/config and restart the affected service;
- never silently continue after a failed gate.
## Acceptance suite
All of the following must pass on the same deployed staging overlay:
- Runtime status is `active` and healthy after restart.
- Gitea Git/API readback works from the runtime service identity.
- `getReleaseConsistency(V2R2C1, 998d30304787e31e5ae607c0bd86b5edb2b0e5ad, a8b906d1bc24088c19647e98de6a679268f6d0a2, approved_plan) returns a structured result and reaches evaluation.
- `runReleaseAcceptance` for the same identities reaches policy evaluation, not URL validation error.
- `getTagPublicationStatus("V2R2C1")` returns structured state, not `invalid release version`.
- `getReleaseSaga` for a nonexistent candidate returns NOT_FOUND/empty-state semantics, not filesystem permission failure.
- `commitFilesGoverned` exposes its required `files` parameter in the published Action schema.
- A real two-file governed commit to an isolated `agent/` branch succeeds and remote branch OID matches.
- Resuming a COMPLETED job returns the existing `JOB_TERMINAL` semantics or idempotent success as designed.
- Reconciling a COMPLETED governed job is successful/idempotent and does not require an active lease.
- Small/empty `getDiff still works.
- Large `getDiff` returns bounded output with truncation metadata instead of a transport exception.
- Existing protected-ref controls remain unchange: no direct main/tag/baseline/production write.
## Restart / reboot persistence
After the first full pass:
- Restart the staging Action/Bridge/release services one more time without changing files.
- Rerun the acceptance suite.
- Permissions, Gitea URL, and schema must still be correct after restart.
- If the repository already has a safe staging reboot test, run it as well.
## Evidence and completion
Create a single durable report in the repository, e.g. `reports/action-contract-repair-evidence-20260825.md`, containing:
- pre-change facts
- root cause for each regression
- files changed and why
- config/permission changes
- test commands and exact results
- staging deployment identity (commit/artifact/config digest)
- restart-persistence results
- final Action smoke results
- any remaining risks
Commit all source, test, deployment config, and runbook changes to the governed task branch. Do not mark the task SUCCEEDED until all acceptance gates pass on the actually deployed staging overlay.
## Automatic rollback conditions
Automatically roll back the staging deployment if any of the following occur:
- service fails to become healthy after restart
- protected-ref write boundary is broken
- Gitea readback that worked before the change stops working
- durable saga state cannot be read or recovered
- mandatory test suite regresses below the pre-change baseline
Do not automatically roll back an existing production release. Rollback here refers only to the staging overlay/service configuration changes made by this task.
-22
View File
@@ -5,7 +5,6 @@ import hashlib
import json
import os
from pathlib import Path
import sys
import tarfile
import tempfile
@@ -19,18 +18,6 @@ REQUIRED_APP = (
'silent_release_controller.py',
'silent_release_hardening.py',
'aios_gitops_job_controller.py',
'aios_chatgpt_bridge_v2.py',
'aios_chatgpt_bridge_v2_server.py',
'governed_production_release.py',
'v2r2c1_production_adapter.py',
'v2r1c7_production_adapter.py',
'aios_governed_task_bootstrap.py',
'aios_continuation_executor.py',
'aios_continuation_lease.py',
'aios_authority_git_executor.py',
'aios_gitops_reconciliation_core.py',
'authorized_task_writer_provisioning.py',
'signer_contract_compatibility.py',
)
OPTIONAL_APP = (
'aios_chatgpt_actions_api.py',
@@ -69,18 +56,11 @@ def main() -> int:
source = args.source_root.resolve(strict=True)
output = args.output_root.resolve()
app = source / 'active-runtime' / 'app'
sys.path.insert(0, str(app))
from aios_chatgpt_bridge_v2 import render_openapi_contract_fragment
runtime = source / 'runtime' / 'agent-os'
docs = source / 'docs' / 'gitops'
if not app.is_dir():
raise SystemExit('active-runtime/app missing')
paths = []
root_version = source / 'release_version.py'
if not root_version.is_file():
raise SystemExit('required source dependency missing: release_version.py')
paths.append((Path('release_version.py'), root_version))
for name in REQUIRED_APP:
path = app / name
if not path.is_file():
@@ -97,8 +77,6 @@ def main() -> int:
paths.append((Path('runtime-agent-os') / rel, path))
openapi = docs / 'openapi-chatgpt-actions.yaml'
if openapi.is_file():
if ' /v2/commit-files:' not in openapi.read_text(encoding='utf-8'):
raise SystemExit('published OpenAPI is missing generated commitFilesGoverned contract')
paths.append((Path('docs') / 'gitops' / 'openapi-chatgpt-actions.yaml', openapi))
for folder in ('config/model_registry', 'config/model_priority', 'patches', 'versions'):
root = source / folder
+4 -10
View File
@@ -9,16 +9,10 @@ WORKSPACE="${AIOS_GITOPS_WORKSPACE:-/data/aios-gitops/workspace}"
TAG_REMOTE="${AIOS_GITOPS_GITEA_REMOTE:-gitea}"
BRANCH_REMOTE="${AIOS_GITOPS_BRANCH_REMOTE:-gitea}"
SCRIPT_ROOT="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd)"
if ! PYTHONPATH="$SCRIPT_ROOT:${PYTHONPATH:-}" python3 - "$VERSION" <<'PY'
import sys
from release_version import validate_release_version
validate_release_version(sys.argv[1])
PY
then
echo "ERROR: version must match VxRxCx" >&2
exit 64
fi
case "$VERSION" in
V[0-9]*R[0-9]*C[0-9]*) ;;
*) echo "ERROR: version must match VxRxCx" >&2; exit 64 ;;
esac
for value in "$TARGET_OID" "$EVIDENCE_CLOSURE_OID"; do
[[ "$value" =~ ^[0-9a-f]{40}$ ]] || {
@@ -0,0 +1,93 @@
# Autonomous Engineering Runtime Contract v1
The Online Agent Worker and the Hermes/Codex engineering runtime are separate
security domains.
The worker owns:
- Agent Task polling, atomic claim, task lease and heartbeat;
- intent resolution and restart state;
- authoritative repository/base selection;
- independent diff inspection and secret checks;
- verification command allowlisting and re-execution;
- Governed GitOps task/lease/commit;
- final result submission.
The runtime broker and Hermes/Codex backend do **not** receive:
- Agent Task API token;
- task lease token;
- governed writer lease or Bridge credential;
- authority repository write access.
## stdin request
The backend configured behind the broker receives one JSON object:
`schema = aios.online-agent.runtime-request.v1`
It contains task goal/instructions/success criteria and a pinned disposable
worktree path. The backend may inspect and modify that worktree. It must not
commit or push.
## stdout response
The backend returns one JSON object:
```json
{
"schema": "aios.online-agent.runtime-result.v1",
"status": "COMPLETED",
"summary": "root cause and implementation summary",
"verification": [
{
"label": "focused test",
"argv": ["/usr/bin/python3", "-m", "pytest", "-q", "tests/test_x.py"],
"cwd": ".",
"timeout_seconds": 600
}
],
"evidence": {
"root_cause": "..."
}
}
```
`status` may also be `BLOCKED` or `FAILED`.
The worker ignores any backend claim about changed files and independently reads
Git state. It also reruns verification requests through its local command
allowlist.
## Real runtime requirement
A chat/router/WeChat Hermes service is not an engineering backend. A compliant
backend must be able to read and mutate the disposable worktree and emit the
runtime result schema above.
The exact Hermes/Codex CLI/API must be discovered from the installed runtime.
Do not invent flags. `/etc/aios/agent-runtime-hermes.conf` contains only
non-secret backend command configuration. Any model/runtime credential must be
delivered to the broker/backend through its own systemd credential projection,
never through the Online Agent worker token.
## OpenClaw headless engineering backend
For hosts that have a current OpenClaw CLI, use the bundled
`openclaw_engineering_backend.py`.
The wrapper invokes the documented headless coding entry point:
`openclaw agent exec --message-file - --cwd <pinned-worktree> --model <model> --auth-env-only --timeout <seconds> --json`
It deliberately does **not** use the Gateway/WeChat/chat route. `agent exec`
runs an embedded, one-shot agent turn and scopes its workspace/tool working
directory to `--cwd`.
Provider credentials, if required, are projected into the broker service under
separate systemd credentials and mapped to provider environment variables only
for the OpenClaw child. The Online Agent Task token and writer lease remain
absent.
Before enabling the autonomous worker, run a disposable broker self-test that
proves OpenClaw changes the requested worktree while Git HEAD remains at the
base OID.
@@ -0,0 +1,297 @@
#!/usr/bin/env python3
from __future__ import annotations
import array
import json
import os
from pathlib import Path
import pwd
import re
import socket
import stat
import subprocess
from typing import Any, Mapping
MAX_BYTES = 1024 * 1024
ADAPTER = os.environ.get("AIOS_AGENT_RUNTIME_ADAPTER", "").strip()
SOCKET_PATH = Path(os.environ.get("AIOS_AGENT_RUNTIME_SOCKET", ""))
WORKSPACE_ROOT = Path(os.environ.get("AIOS_AGENT_RUNTIME_WORKSPACE_ROOT", ""))
BACKEND_ARGV_JSON = os.environ.get("AIOS_AGENT_RUNTIME_BACKEND_ARGV_JSON", "")
ALLOWED_PEER_UID = int(os.environ.get("AIOS_AGENT_RUNTIME_ALLOWED_PEER_UID", "-1"))
ALLOWED_PEER_USER = os.environ.get("AIOS_AGENT_RUNTIME_ALLOWED_PEER_USER", "").strip()
CONNECTION_TIMEOUT = float(os.environ.get("AIOS_AGENT_RUNTIME_CONNECTION_TIMEOUT_SECONDS", "15"))
BACKEND_TIMEOUT = int(os.environ.get("AIOS_AGENT_RUNTIME_BACKEND_TIMEOUT_SECONDS", "3600"))
BACKEND_ENV_ALLOWLIST = (
"AIOS_OPENCLAW_BIN",
"AIOS_OPENCLAW_MODEL",
"AIOS_OPENCLAW_PROVIDER_CREDENTIAL_MAP_JSON",
"AIOS_OPENCLAW_EXEC_TIMEOUT_SECONDS",
)
SENSITIVE_RE = re.compile(r"(?i)(authorization\s*:\s*bearer\s+\S+|(?:password|passwd|secret|token|api[_-]?key)\s*[:=]\s*\S+)")
class BrokerError(RuntimeError):
def __init__(self, code: str, *, details: Mapping[str, Any] | None = None) -> None:
super().__init__(code)
self.code = code
self.details = dict(details or {})
def peer_uid(connection: socket.socket) -> int:
if not hasattr(socket, "SO_PEERCRED"):
raise BrokerError("SO_PEERCRED_UNAVAILABLE")
raw = connection.getsockopt(socket.SOL_SOCKET, socket.SO_PEERCRED, 12)
pid, uid, gid = array.array("i", raw[:12])
return int(uid)
def configured_peer_uid() -> int:
if ALLOWED_PEER_UID >= 0:
return ALLOWED_PEER_UID
if not ALLOWED_PEER_USER:
raise BrokerError("ALLOWED_PEER_UID_NOT_CONFIGURED")
try:
return int(pwd.getpwnam(ALLOWED_PEER_USER).pw_uid)
except KeyError as exc:
raise BrokerError("ALLOWED_PEER_USER_NOT_FOUND") from exc
def authorize_peer(connection: socket.socket) -> None:
uid = peer_uid(connection)
expected = configured_peer_uid()
if uid != expected:
raise BrokerError("PEER_UID_REJECTED", details={"peer_uid": uid})
def read_request(connection: socket.socket) -> dict[str, Any]:
connection.settimeout(CONNECTION_TIMEOUT)
total = 0
chunks: list[bytes] = []
while True:
try:
chunk = connection.recv(65536)
except socket.timeout as exc:
raise BrokerError("REQUEST_TIMEOUT") from exc
if not chunk:
break
total += len(chunk)
if total > MAX_BYTES:
raise BrokerError("REQUEST_TOO_LARGE")
chunks.append(chunk)
try:
value = json.loads(b"".join(chunks).decode("utf-8"))
except (UnicodeDecodeError, json.JSONDecodeError) as exc:
raise BrokerError("REQUEST_INVALID_JSON") from exc
if not isinstance(value, Mapping):
raise BrokerError("REQUEST_OBJECT_REQUIRED")
return dict(value)
def backend_argv() -> list[str]:
try:
value = json.loads(BACKEND_ARGV_JSON)
except json.JSONDecodeError as exc:
raise BrokerError("BACKEND_ARGV_JSON_INVALID") from exc
if not isinstance(value, list) or not value or not all(isinstance(item, str) and item for item in value):
raise BrokerError("BACKEND_ARGV_INVALID")
executable = Path(value[0])
if not executable.is_absolute():
raise BrokerError("BACKEND_EXECUTABLE_INVALID")
try:
resolved = executable.resolve(strict=True)
except OSError as exc:
raise BrokerError("BACKEND_EXECUTABLE_INVALID") from exc
if not resolved.is_file():
raise BrokerError("BACKEND_EXECUTABLE_INVALID")
return list(value)
def validate_request(value: Mapping[str, Any]) -> tuple[dict[str, Any], int]:
if value.get("schema") != "aios.online-agent.runtime-request.v1":
raise BrokerError("REQUEST_SCHEMA_INVALID")
if value.get("adapter") != ADAPTER or ADAPTER not in {"hermes", "codex", "openclaw"}:
raise BrokerError("REQUEST_ADAPTER_INVALID")
context = value.get("execution_context")
policy = value.get("policy")
if not isinstance(context, Mapping) or not isinstance(policy, Mapping):
raise BrokerError("REQUEST_CONTEXT_INVALID")
worktree_raw = context.get("worktree")
if not isinstance(worktree_raw, str):
raise BrokerError("REQUEST_WORKTREE_INVALID")
for field in (
"authoritative_repository_write",
"direct_git_ref_mutation",
"direct_git_push",
"direct_git_commit",
"main_write",
"force_push",
"production_write",
"tag_write",
"baseline_write",
"task_api_credential_visible",
"writer_lease_visible",
):
if policy.get(field) is not False:
raise BrokerError("REQUEST_POLICY_REJECTED", details={"field": field})
root = WORKSPACE_ROOT.resolve()
candidate = Path(worktree_raw)
if not candidate.is_absolute():
raise BrokerError("REQUEST_WORKTREE_INVALID")
parent = candidate.parent.resolve()
if root == parent or root not in parent.parents:
raise BrokerError("REQUEST_WORKTREE_OUT_OF_SCOPE")
flags = getattr(os, "O_PATH", os.O_RDONLY) | getattr(os, "O_NOFOLLOW", 0) | os.O_DIRECTORY
try:
fd = os.open(str(candidate), flags)
except OSError as exc:
raise BrokerError("REQUEST_WORKTREE_OPEN_FAILED") from exc
st = os.fstat(fd)
if not stat.S_ISDIR(st.st_mode):
os.close(fd)
raise BrokerError("REQUEST_WORKTREE_NOT_DIRECTORY")
pinned = Path(f"/proc/self/fd/{fd}")
try:
resolved = pinned.resolve(strict=True)
except OSError:
os.close(fd)
raise BrokerError("REQUEST_WORKTREE_PIN_FAILED")
if root not in resolved.parents:
os.close(fd)
raise BrokerError("REQUEST_WORKTREE_OUT_OF_SCOPE")
return dict(value), fd
def sanitize(value: Any) -> Any:
if isinstance(value, Mapping):
cleaned: dict[str, Any] = {}
for key, inner in value.items():
lowered = str(key).lower()
if any(marker in lowered for marker in ("token", "secret", "credential", "authorization", "password", "api_key", "apikey", "access_key")):
cleaned[str(key)] = "[REDACTED]"
else:
cleaned[str(key)] = sanitize(inner)
return cleaned
if isinstance(value, list):
return [sanitize(item) for item in value]
if isinstance(value, str):
return SENSITIVE_RE.sub("[REDACTED]", value)
return value
def sanitized_failure(code: str, details: Mapping[str, Any] | None = None) -> dict[str, Any]:
safe: dict[str, Any] = {}
for key, value in dict(details or {}).items():
lowered = str(key).lower()
if any(marker in lowered for marker in ("token", "secret", "credential", "authorization", "password", "api_key", "apikey", "access_key")):
safe[str(key)] = "[REDACTED]"
else:
safe[str(key)] = value
return {
"schema": "aios.online-agent.runtime-result.v1",
"status": "FAILED",
"summary": code,
"verification": [],
"evidence": safe,
}
def execute_backend(request: Mapping[str, Any], worktree_fd: int) -> dict[str, Any]:
argv = backend_argv()
request = dict(request)
context = dict(request["execution_context"])
# Pass a pinned inode-backed path, not the original user-visible pathname.
context["worktree"] = f"/proc/self/fd/{worktree_fd}"
request["execution_context"] = context
payload = json.dumps(request, ensure_ascii=False, sort_keys=True, separators=(",", ":")).encode("utf-8")
environment = {
"PATH": "/usr/local/bin:/usr/bin:/bin",
"LC_ALL": "C.UTF-8",
"AIOS_AGENT_RUNTIME_ADAPTER": ADAPTER,
}
for name in BACKEND_ENV_ALLOWLIST:
value = os.environ.get(name)
if value is not None:
environment[name] = value
credentials_dir = os.environ.get("CREDENTIALS_DIRECTORY")
if credentials_dir:
environment["CREDENTIALS_DIRECTORY"] = credentials_dir
try:
completed = subprocess.run(
argv,
input=payload,
cwd=f"/proc/self/fd/{worktree_fd}",
pass_fds=(worktree_fd,),
capture_output=True,
check=False,
timeout=BACKEND_TIMEOUT,
env=environment,
)
except subprocess.TimeoutExpired:
return sanitized_failure("AGENT_BACKEND_TIMEOUT")
except OSError as exc:
return sanitized_failure("AGENT_BACKEND_EXEC_FAILED", {"error": type(exc).__name__})
if completed.returncode != 0:
return sanitized_failure("AGENT_BACKEND_FAILED", {"exit_code": completed.returncode})
if len(completed.stdout) > MAX_BYTES:
return sanitized_failure("AGENT_BACKEND_RESULT_TOO_LARGE")
try:
value = json.loads(completed.stdout.decode("utf-8"))
except (UnicodeDecodeError, json.JSONDecodeError):
return sanitized_failure("AGENT_BACKEND_RESULT_INVALID")
if not isinstance(value, Mapping) or value.get("schema") != "aios.online-agent.runtime-result.v1":
return sanitized_failure("AGENT_BACKEND_RESULT_SCHEMA_INVALID")
sanitized = sanitize(dict(value))
return sanitized if isinstance(sanitized, dict) else sanitized_failure("AGENT_BACKEND_RESULT_INVALID")
def serve_once(listener: socket.socket) -> None:
connection, _ = listener.accept()
worktree_fd: int | None = None
try:
authorize_peer(connection)
request = read_request(connection)
request, worktree_fd = validate_request(request)
response = execute_backend(request, worktree_fd)
except BrokerError as exc:
response = sanitized_failure(exc.code, exc.details)
except Exception as exc:
response = sanitized_failure("BROKER_INTERNAL_ERROR", {"error": type(exc).__name__})
finally:
if worktree_fd is not None:
os.close(worktree_fd)
raw = json.dumps(response, ensure_ascii=False, sort_keys=True, separators=(",", ":")).encode("utf-8") + b"\n"
try:
connection.settimeout(CONNECTION_TIMEOUT)
connection.sendall(raw[:MAX_BYTES])
finally:
connection.close()
def main() -> int:
if ADAPTER not in {"hermes", "codex", "openclaw"}:
raise BrokerError("BROKER_ADAPTER_INVALID")
if not SOCKET_PATH.is_absolute() or not WORKSPACE_ROOT.is_absolute():
raise BrokerError("BROKER_PATH_INVALID")
SOCKET_PATH.parent.mkdir(parents=True, mode=0o750, exist_ok=True)
if SOCKET_PATH.exists() or SOCKET_PATH.is_symlink():
SOCKET_PATH.unlink()
listener = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
listener.bind(str(SOCKET_PATH))
os.chmod(SOCKET_PATH, 0o660)
listener.listen(8)
try:
while True:
serve_once(listener)
finally:
listener.close()
if SOCKET_PATH.exists():
SOCKET_PATH.unlink()
return 0
if __name__ == "__main__":
raise SystemExit(main())
@@ -0,0 +1,369 @@
#!/usr/bin/env python3
"""OpenClaw engineering backend for the AIOS autonomous runtime broker.
This process is intentionally *not* an Online Agent Task worker. It receives
one secret-free `aios.online-agent.runtime-request.v1` object on stdin, invokes
OpenClaw's headless `agent exec` against the pinned disposable worktree, and
returns one `aios.online-agent.runtime-result.v1` object on stdout.
Online Task claim/heartbeat/result and Governed GitOps remain owned by the
AIOS autonomous worker.
"""
from __future__ import annotations
import json
import os
from pathlib import Path
import re
import subprocess
import sys
from typing import Any, Mapping
REQUEST_SCHEMA = "aios.online-agent.runtime-request.v1"
RESULT_SCHEMA = "aios.online-agent.runtime-result.v1"
MAX_INPUT_BYTES = 1024 * 1024
MAX_OUTPUT_BYTES = 1024 * 1024
DEFAULT_TIMEOUT = 1800
ALLOWED_ADAPTERS = frozenset({"hermes", "codex", "openclaw"})
SECRET_PATTERNS = (
re.compile(r"\bsk-[A-Za-z0-9_-]{20,}\b"),
re.compile(r"\bghp_[A-Za-z0-9]{20,}\b"),
re.compile(r"\bAKIA[0-9A-Z]{16}\b"),
re.compile(r"-----BEGIN (?:RSA |EC |OPENSSH )?PRIVATE KEY-----"),
)
class BackendError(RuntimeError):
def __init__(self, code: str, *, details: Mapping[str, Any] | None = None) -> None:
super().__init__(code)
self.code = code
self.details = dict(details or {})
def redact_text(value: str, limit: int = 8192) -> str:
value = value[:limit]
for pattern in SECRET_PATTERNS:
value = pattern.sub("[REDACTED]", value)
return value
def result(
status: str,
summary: str,
*,
verification: list[dict[str, Any]] | None = None,
evidence: Mapping[str, Any] | None = None,
) -> dict[str, Any]:
return {
"schema": RESULT_SCHEMA,
"status": status,
"summary": redact_text(summary) or status,
"verification": verification or [],
"evidence": dict(evidence or {}),
}
def emit(value: Mapping[str, Any]) -> int:
raw = json.dumps(dict(value), ensure_ascii=False, sort_keys=True, separators=(",", ":"))
sys.stdout.write(raw)
sys.stdout.write("\n")
sys.stdout.flush()
return 0
def read_request() -> dict[str, Any]:
raw = sys.stdin.buffer.read(MAX_INPUT_BYTES + 1)
if len(raw) > MAX_INPUT_BYTES:
raise BackendError("REQUEST_TOO_LARGE")
try:
value = json.loads(raw.decode("utf-8"))
except (UnicodeDecodeError, json.JSONDecodeError) as exc:
raise BackendError("REQUEST_INVALID_JSON") from exc
if not isinstance(value, Mapping):
raise BackendError("REQUEST_OBJECT_REQUIRED")
if value.get("schema") != REQUEST_SCHEMA:
raise BackendError("REQUEST_SCHEMA_INVALID")
return dict(value)
def nonempty(value: Any, field: str, limit: int) -> str:
if not isinstance(value, str):
raise BackendError(f"{field.upper()}_INVALID")
value = value.strip()
if not value or len(value) > limit:
raise BackendError(f"{field.upper()}_INVALID")
return value
def string_list(value: Any, field: str) -> list[str]:
if not isinstance(value, list) or not value or len(value) > 64:
raise BackendError(f"{field.upper()}_INVALID")
result_value: list[str] = []
for item in value:
result_value.append(nonempty(item, field, 4096))
return result_value
def validate_request(value: Mapping[str, Any]) -> tuple[dict[str, Any], Path]:
adapter = str(value.get("adapter") or "")
if adapter not in ALLOWED_ADAPTERS:
raise BackendError("ADAPTER_INVALID")
task = value.get("task")
context = value.get("execution_context")
policy = value.get("policy")
if not isinstance(task, Mapping) or not isinstance(context, Mapping) or not isinstance(policy, Mapping):
raise BackendError("REQUEST_CONTEXT_INVALID")
task_id = nonempty(task.get("task_id"), "task_id", 180)
goal = nonempty(task.get("goal"), "goal", 16384)
instructions = nonempty(task.get("instructions"), "instructions", 65536)
success = string_list(task.get("success_criteria"), "success_criteria")
worktree_raw = context.get("worktree")
if not isinstance(worktree_raw, str) or not worktree_raw.startswith("/proc/self/fd/"):
raise BackendError("PINNED_WORKTREE_REQUIRED")
worktree = Path(worktree_raw)
try:
resolved = worktree.resolve(strict=True)
except OSError as exc:
raise BackendError("PINNED_WORKTREE_UNAVAILABLE") from exc
if not resolved.is_dir():
raise BackendError("PINNED_WORKTREE_INVALID")
for field in (
"authoritative_repository_write",
"direct_git_ref_mutation",
"direct_git_push",
"direct_git_commit",
"main_write",
"force_push",
"production_write",
"tag_write",
"baseline_write",
"task_api_credential_visible",
"writer_lease_visible",
):
if policy.get(field) is not False:
raise BackendError("REQUEST_POLICY_REJECTED", details={"field": field})
return {
"adapter": adapter,
"task_id": task_id,
"goal": goal,
"instructions": instructions,
"success_criteria": success,
"repository": str(context.get("repository") or ""),
"base_oid": str(context.get("base_oid") or ""),
"target_branch": str(context.get("target_branch") or ""),
}, worktree
def openclaw_binary() -> Path:
configured = os.environ.get("AIOS_OPENCLAW_BIN", "").strip()
if not configured:
raise BackendError("OPENCLAW_BINARY_NOT_CONFIGURED")
path = Path(configured)
if not path.is_absolute():
raise BackendError("OPENCLAW_BINARY_INVALID")
try:
resolved = path.resolve(strict=True)
except OSError as exc:
raise BackendError("OPENCLAW_BINARY_UNAVAILABLE") from exc
if not resolved.is_file() or not os.access(resolved, os.X_OK):
raise BackendError("OPENCLAW_BINARY_UNAVAILABLE")
return resolved
def provider_environment() -> dict[str, str]:
"""Build an environment containing only explicitly projected provider auth.
`--auth-env-only` tells OpenClaw not to load ambient config/auth stores.
The mapping is non-secret; values are read from systemd credential files.
"""
raw = os.environ.get("AIOS_OPENCLAW_PROVIDER_CREDENTIAL_MAP_JSON", "{}")
try:
mapping = json.loads(raw)
except json.JSONDecodeError as exc:
raise BackendError("PROVIDER_CREDENTIAL_MAP_INVALID") from exc
if not isinstance(mapping, Mapping):
raise BackendError("PROVIDER_CREDENTIAL_MAP_INVALID")
credentials_directory = os.environ.get("CREDENTIALS_DIRECTORY", "").strip()
environment = {
"PATH": "/usr/local/bin:/usr/bin:/bin",
"LC_ALL": "C.UTF-8",
"NO_COLOR": "1",
# Prevent optional login-shell env snapshot import into exec runs.
"OPENCLAW_EXEC_SHELL_SNAPSHOT": "0",
}
if mapping:
if not credentials_directory:
raise BackendError("PROVIDER_CREDENTIAL_DIRECTORY_UNAVAILABLE")
credential_root = Path(credentials_directory)
for env_name, credential_name in mapping.items():
if (
not isinstance(env_name, str)
or not re.fullmatch(r"[A-Z][A-Z0-9_]{2,63}", env_name)
or not isinstance(credential_name, str)
or not re.fullmatch(r"[A-Za-z0-9._-]{1,128}", credential_name)
):
raise BackendError("PROVIDER_CREDENTIAL_MAP_INVALID")
path = credential_root / credential_name
if not path.is_file() or path.is_symlink():
raise BackendError("PROVIDER_CREDENTIAL_UNAVAILABLE", details={"name": credential_name})
secret = path.read_text(encoding="utf-8").strip()
if not secret:
raise BackendError("PROVIDER_CREDENTIAL_EMPTY", details={"name": credential_name})
environment[env_name] = secret
return environment
def build_prompt(task: Mapping[str, Any]) -> str:
criteria = "\n".join(f"- {item}" for item in task["success_criteria"])
return f"""You are the autonomous engineering runtime for AIOS task {task['task_id']}.
Repository: {task['repository']}
Base OID: {task['base_oid']}
Target branch (owned by AIOS Governed GitOps, not by you): {task['target_branch']}
Goal:
{task['goal']}
Instructions:
{task['instructions']}
Success criteria:
{criteria}
Execution boundary:
- Work only inside the current disposable worktree.
- Inspect the repository yourself and determine the root cause and files to change.
- Implement the smallest correct fix.
- Run focused tests and repair failures when feasible.
- Do NOT run `git commit`, `git push`, `git update-ref`, create branches/tags, or modify authoritative refs.
- Do NOT attempt to access AIOS Agent Task credentials, writer leases, or Governed GitOps credentials.
- Leave Git HEAD unchanged; AIOS independently inspects the diff, reruns verification, and performs the governed commit.
- In your final answer, concisely state root cause, files changed, and tests you ran.
"""
def openclaw_timeout() -> int:
raw = os.environ.get("AIOS_OPENCLAW_EXEC_TIMEOUT_SECONDS", str(DEFAULT_TIMEOUT))
try:
value = int(raw)
except ValueError as exc:
raise BackendError("OPENCLAW_TIMEOUT_INVALID") from exc
if not 60 <= value <= 7200:
raise BackendError("OPENCLAW_TIMEOUT_INVALID")
return value
def invoke_openclaw(task: Mapping[str, Any], worktree: Path) -> dict[str, Any]:
binary = openclaw_binary()
model = nonempty(os.environ.get("AIOS_OPENCLAW_MODEL"), "openclaw_model", 256)
timeout_seconds = openclaw_timeout()
prompt = build_prompt(task)
argv = [
str(binary),
"agent",
"exec",
"--message-file",
"-",
"--cwd",
str(worktree),
"--model",
model,
"--auth-env-only",
"--timeout",
str(timeout_seconds),
"--json",
]
try:
pinned_fd = int(worktree.name)
except ValueError as exc:
raise BackendError("PINNED_WORKTREE_FD_INVALID") from exc
try:
completed = subprocess.run(
argv,
input=prompt.encode("utf-8"),
cwd=str(worktree),
pass_fds=(pinned_fd,),
capture_output=True,
check=False,
timeout=timeout_seconds + 90,
env=provider_environment(),
)
except subprocess.TimeoutExpired:
return result(
"FAILED",
"OPENCLAW_HARD_TIMEOUT",
evidence={"backend": "openclaw-agent-exec", "timeout_seconds": timeout_seconds},
)
except OSError as exc:
return result(
"FAILED",
"OPENCLAW_EXEC_FAILED",
evidence={"backend": "openclaw-agent-exec", "error": type(exc).__name__},
)
stdout = completed.stdout[:MAX_OUTPUT_BYTES]
try:
envelope = json.loads(stdout.decode("utf-8"))
except (UnicodeDecodeError, json.JSONDecodeError):
return result(
"FAILED",
"OPENCLAW_RESULT_INVALID",
evidence={
"backend": "openclaw-agent-exec",
"exit_code": completed.returncode,
"stderr_sha256": __import__("hashlib").sha256(completed.stderr).hexdigest(),
},
)
if not isinstance(envelope, Mapping):
return result("FAILED", "OPENCLAW_RESULT_INVALID", evidence={"backend": "openclaw-agent-exec"})
ok = envelope.get("ok") is True and envelope.get("status") == "ok" and completed.returncode == 0
final_text = str(envelope.get("final") or "").strip()
evidence = {
"backend": "openclaw-agent-exec",
"openclaw_status": envelope.get("status"),
"exit_code": completed.returncode,
"model": envelope.get("model"),
"provider": envelope.get("provider"),
"session_id_present": bool(envelope.get("sessionId")),
"code_mode_engaged": envelope.get("codeModeEngaged"),
"assistant_turns": envelope.get("assistantTurns"),
"tool_summary": envelope.get("toolSummary") if isinstance(envelope.get("toolSummary"), Mapping) else {},
}
if not ok:
error = envelope.get("error")
if isinstance(error, Mapping):
evidence["error_kind"] = error.get("kind")
summary = "OPENCLAW_" + str(envelope.get("status") or "ERROR").upper()
return result("FAILED", summary, evidence=evidence)
summary = final_text or "OpenClaw completed the engineering turn."
return result("COMPLETED", summary, evidence=evidence)
def main() -> int:
try:
request = read_request()
task, worktree = validate_request(request)
return emit(invoke_openclaw(task, worktree))
except BackendError as exc:
return emit(result("BLOCKED", exc.code, evidence=exc.details))
except Exception as exc:
return emit(result("FAILED", "OPENCLAW_BACKEND_INTERNAL_ERROR", evidence={"error": type(exc).__name__}))
if __name__ == "__main__":
raise SystemExit(main())
@@ -0,0 +1,822 @@
#!/usr/bin/env python3
from __future__ import annotations
import base64
from datetime import datetime, timezone
import grp
import hashlib
import json
import os
from pathlib import Path
import re
import shutil
import subprocess
import tempfile
import threading
import time
import urllib.error
import urllib.request
from typing import Any, Mapping, Sequence
from intent_runtime import IntentError, Resolution, is_claimable_intent, resolve
from runtime_client import RuntimeErrorV2, collect_changes, invoke, request_payload, scan_secrets
API_BASE = os.environ.get("AIOS_AGENT_TASK_API_URL", "http://127.0.0.1:18940").rstrip("/")
BRIDGE_BASE = os.environ.get("AIOS_GOVERNED_WRITE_API_URL", "http://127.0.0.1:19998").rstrip("/")
AGENT_ID = os.environ.get("AIOS_AGENT_ID", "online-engineering-worker-cn-network-mkt-autonomous-20260815")
AGENT_VERSION = os.environ.get("AIOS_AGENT_VERSION", "2026-08-15.4-autonomous-intent")
REPOSITORY = os.environ.get("AIOS_AUTONOMOUS_REPOSITORY", "aios/aios")
BASE_REF = os.environ.get("AIOS_AUTONOMOUS_BASE_REF", "refs/heads/main")
AUTHORITY_GIT_DIR = Path(os.environ.get("AIOS_AUTHORITY_GIT_DIR", "/data/aios-gitops/repo.git"))
STATE_ROOT = Path(os.environ.get("AIOS_AUTONOMOUS_STATE_ROOT", "/var/lib/aios-autonomous-agent-worker"))
WORKSPACE_ROOT = Path(os.environ.get("AIOS_AUTONOMOUS_WORKSPACE_ROOT", str(STATE_ROOT / "workspaces")))
SHARED_GROUP = os.environ.get("AIOS_AGENT_RUNTIME_SHARED_GROUP", "aios-agent-runtime")
DEFAULT_ADAPTER = os.environ.get("AIOS_AUTONOMOUS_DEFAULT_ADAPTER", "hermes")
HERMES_SOCKET = Path(os.environ.get("AIOS_HERMES_BROKER_SOCKET", "/run/aios-agent-runtime/hermes.sock"))
CODEX_SOCKET = Path(os.environ.get("AIOS_CODEX_BROKER_SOCKET", "/run/aios-agent-runtime/codex.sock"))
OPENCLAW_SOCKET = Path(os.environ.get("AIOS_OPENCLAW_BROKER_SOCKET", "/run/aios-agent-runtime/openclaw.sock"))
POLL_SECONDS = int(os.environ.get("AIOS_AUTONOMOUS_POLL_SECONDS", "5"))
HEARTBEAT_SECONDS = int(os.environ.get("AIOS_AUTONOMOUS_HEARTBEAT_SECONDS", "10"))
LEASE_SECONDS = int(os.environ.get("AIOS_AUTONOMOUS_LEASE_SECONDS", "300"))
RUNTIME_TIMEOUT_SECONDS = int(os.environ.get("AIOS_AUTONOMOUS_RUNTIME_TIMEOUT_SECONDS", "3600"))
MAX_OUTPUT_BYTES = 64 * 1024
OID_RE = re.compile(r"^[0-9a-f]{40}$")
SENSITIVE_RE = re.compile(
r"(?i)(authorization\s*:\s*bearer\s+\S+|(?:password|passwd|secret|token|api[_-]?key)\s*[:=]\s*\S+)"
)
class WorkerError(RuntimeError):
def __init__(self, code: str, *, phase: str, details: Mapping[str, Any] | None = None) -> None:
super().__init__(code)
self.code = code
self.phase = phase
self.details = dict(details or {})
def now() -> str:
return datetime.now(timezone.utc).isoformat()
def sha256_bytes(value: bytes) -> str:
return hashlib.sha256(value).hexdigest()
def sha256_json(value: Any) -> str:
return sha256_bytes(json.dumps(value, ensure_ascii=False, sort_keys=True, separators=(",", ":")).encode("utf-8"))
def sanitize(value: Any) -> Any:
if isinstance(value, Mapping):
result: dict[str, Any] = {}
for key, inner in value.items():
lowered = str(key).lower()
if any(word in lowered for word in ("token", "secret", "credential", "authorization", "password", "api_key", "apikey", "access_key")):
result[str(key)] = "[REDACTED]"
else:
result[str(key)] = sanitize(inner)
return result
if isinstance(value, list):
return [sanitize(item) for item in value]
if isinstance(value, tuple):
return [sanitize(item) for item in value]
if isinstance(value, str):
return SENSITIVE_RE.sub("[REDACTED]", value)
return value
def audit(event_type: str, **fields: Any) -> None:
STATE_ROOT.mkdir(parents=True, mode=0o750, exist_ok=True)
path = STATE_ROOT / "events.jsonl"
record = {
"event_type": event_type,
"created_at": now(),
"agent_id": AGENT_ID,
"agent_version": AGENT_VERSION,
"credential_material_exposed": False,
"writer_lease_material_exposed": False,
**sanitize(fields),
}
with path.open("a", encoding="utf-8") as handle:
handle.write(json.dumps(record, ensure_ascii=False, sort_keys=True) + "\n")
os.chmod(path, 0o640)
print(json.dumps(record, ensure_ascii=False, sort_keys=True), flush=True)
def atomic_json(path: Path, value: Mapping[str, Any]) -> None:
path.parent.mkdir(parents=True, mode=0o750, exist_ok=True)
if path.parent.is_symlink() or path.is_symlink():
raise WorkerError("STATE_PATH_SYMLINK_REJECTED", phase="state")
fd, temporary = tempfile.mkstemp(prefix=f".{path.name}.", dir=str(path.parent))
try:
os.fchmod(fd, 0o640)
with os.fdopen(fd, "w", encoding="utf-8") as handle:
json.dump(sanitize(dict(value)), handle, ensure_ascii=False, sort_keys=True)
handle.write("\n")
handle.flush()
os.fsync(handle.fileno())
os.replace(temporary, path)
finally:
if os.path.exists(temporary):
os.unlink(temporary)
def state_path(task_id: str) -> Path:
safe = re.sub(r"[^A-Za-z0-9._:-]+", "-", task_id)[:180]
return STATE_ROOT / "tasks" / f"{safe}.json"
def load_state(task_id: str) -> dict[str, Any] | None:
path = state_path(task_id)
if not path.is_file() or path.is_symlink():
return None
try:
value = json.loads(path.read_text(encoding="utf-8"))
except Exception:
return None
return value if isinstance(value, dict) else None
def save_state(task_id: str, value: Mapping[str, Any]) -> None:
atomic_json(state_path(task_id), value)
def api_call(method: str, route: str, token: str, body: Mapping[str, Any] | None = None) -> tuple[int, dict[str, Any]]:
payload = None if body is None else json.dumps(dict(body), ensure_ascii=False, sort_keys=True, separators=(",", ":")).encode("utf-8")
headers = {"Authorization": "Bearer " + token, "Accept": "application/json"}
if payload is not None:
headers["Content-Type"] = "application/json"
request = urllib.request.Request(API_BASE + route, data=payload, headers=headers, method=method)
try:
with urllib.request.urlopen(request, timeout=20) as response:
value = json.loads(response.read().decode("utf-8"))
return response.status, value if isinstance(value, dict) else {}
except urllib.error.HTTPError as exc:
try:
value = json.loads(exc.read(4096).decode("utf-8", "replace"))
except Exception:
value = {"error": "HTTP_ERROR"}
return exc.code, value if isinstance(value, dict) else {"error": "HTTP_ERROR"}
except (urllib.error.URLError, TimeoutError):
return 599, {"error": "AGENT_TASK_API_UNAVAILABLE"}
def bridge_call(route: str, body: Mapping[str, Any]) -> tuple[int, dict[str, Any]]:
if BRIDGE_BASE != "http://127.0.0.1:19998":
raise WorkerError(
"NON_STAGING_BRIDGE_REJECTED",
phase="startup",
details={"bridge_base": BRIDGE_BASE},
)
payload = json.dumps(dict(body), ensure_ascii=False, sort_keys=True, separators=(",", ":")).encode("utf-8")
request = urllib.request.Request(
BRIDGE_BASE + route,
data=payload,
headers={"Accept": "application/json", "Content-Type": "application/json"},
method="POST",
)
try:
with urllib.request.urlopen(request, timeout=60) as response:
value = json.loads(response.read().decode("utf-8"))
return response.status, value if isinstance(value, dict) else {}
except urllib.error.HTTPError as exc:
try:
value = json.loads(exc.read(4096).decode("utf-8", "replace"))
except Exception:
value = {"error": "HTTP_ERROR"}
return exc.code, value if isinstance(value, dict) else {"error": "HTTP_ERROR"}
except (urllib.error.URLError, TimeoutError):
return 599, {"error": "GOVERNED_WRITE_API_UNAVAILABLE"}
def load_agent_token() -> str:
directory = Path(os.environ.get("CREDENTIALS_DIRECTORY", ""))
path = directory / "agent_token"
if not path.is_file():
raise WorkerError("AGENT_RUNTIME_CREDENTIAL_UNAVAILABLE", phase="startup")
token = path.read_text(encoding="utf-8").strip()
if len(token) < 24:
raise WorkerError("AGENT_RUNTIME_CREDENTIAL_INVALID", phase="startup")
return token
def available_adapters() -> tuple[str, ...]:
values: list[str] = []
for name, path in (("hermes", HERMES_SOCKET), ("codex", CODEX_SOCKET), ("openclaw", OPENCLAW_SOCKET)):
try:
if path.is_socket():
values.append(name)
except OSError:
pass
return tuple(values)
def capabilities() -> tuple[str, ...]:
values = {
"online_agent_task_api", "task.poll", "task.claim", "task.heartbeat", "task.result",
"intent.task", "engineering.consumer", "engineering.execute", "engineering.autonomous",
"workspace.worktree", "git.read", "git.diff", "git.test",
"governed.write.branch", "governed.write.file",
}
values.update(f"agent.runtime.{name}" for name in available_adapters())
return tuple(sorted(values))
class LeaseHeartbeat:
def __init__(self, task_id: str, agent_token: str, lease_token: str) -> None:
self.task_id = task_id
self.agent_token = agent_token
self.lease_token = lease_token
self.stage = "claimed"
self.failed: str | None = None
self.stop = threading.Event()
self.thread = threading.Thread(target=self._run, daemon=True, name=f"heartbeat-{task_id}")
def set_stage(self, stage: str) -> None:
self.stage = stage
audit("TASK_PROGRESS_STAGE", task_id=self.task_id, stage=stage)
def send(self) -> None:
status, response = api_call(
"POST",
f"/v1/tasks/{self.task_id}/heartbeat",
self.agent_token,
{
"lease_token": self.lease_token,
"lease_seconds": LEASE_SECONDS,
"progress": {
"stage": self.stage,
"event_type": "TASK_STARTED" if self.stage == "started" else "TASK_PROGRESS",
"execution_mode": "autonomous-intent-v2",
"production_side_effects": False,
"main_write": False,
},
},
)
if 200 <= status < 300:
audit(
"TASK_HEARTBEAT",
task_id=self.task_id,
stage=self.stage,
api_status=status,
task_status=response.get("status"),
lease_expires_at=response.get("lease_expires_at"),
)
else:
self.failed = str(response.get("error") or status)
audit("TASK_HEARTBEAT_FAILED", task_id=self.task_id, stage=self.stage, error=self.failed)
def _run(self) -> None:
while not self.stop.wait(HEARTBEAT_SECONDS):
try:
self.send()
except Exception as exc:
self.failed = type(exc).__name__
audit("TASK_HEARTBEAT_FAILED", task_id=self.task_id, error=self.failed)
def start(self) -> None:
self.thread.start()
def close(self) -> None:
self.stop.set()
self.thread.join(timeout=3)
def ensure_roots() -> None:
if WORKSPACE_ROOT.is_symlink() or STATE_ROOT.is_symlink() or AUTHORITY_GIT_DIR.is_symlink():
raise WorkerError("ROOT_SYMLINK_REJECTED", phase="startup")
if not AUTHORITY_GIT_DIR.is_dir():
raise WorkerError("AUTHORITY_GIT_DIR_UNAVAILABLE", phase="startup")
STATE_ROOT.mkdir(parents=True, mode=0o750, exist_ok=True)
WORKSPACE_ROOT.mkdir(parents=True, mode=0o750, exist_ok=True)
def run_git(args: Sequence[str], *, cwd: Path, timeout: int = 180) -> subprocess.CompletedProcess[bytes]:
completed = subprocess.run(
["/usr/bin/git", *args],
cwd=str(cwd),
capture_output=True,
check=False,
timeout=timeout,
env={"PATH": "/usr/bin:/bin", "LC_ALL": "C.UTF-8"},
)
if completed.returncode != 0:
raise WorkerError(
"GIT_COMMAND_FAILED",
phase="workspace",
details={"operation": args[0] if args else "", "exit_code": completed.returncode},
)
return completed
def prepare_worktree(task_id: str, base_oid: str) -> tuple[Path, Path]:
ensure_roots()
run_root = Path(tempfile.mkdtemp(prefix=f"{task_id[:48]}-", dir=str(WORKSPACE_ROOT)))
os.chmod(run_root, 0o750)
clone = run_root / "repository"
worktree = run_root / "worktree"
run_git(["clone", "--no-local", "--no-hardlinks", str(AUTHORITY_GIT_DIR), str(clone)], cwd=run_root, timeout=300)
run_git(["-C", str(clone), "rev-parse", "--verify", f"{base_oid}^{{commit}}"], cwd=run_root)
run_git(["-C", str(clone), "worktree", "add", "--detach", str(worktree), base_oid], cwd=run_root, timeout=300)
return run_root, worktree
def share_run_root(run_root: Path) -> None:
try:
gid = grp.getgrnam(SHARED_GROUP).gr_gid
except KeyError as exc:
raise WorkerError("RUNTIME_SHARED_GROUP_UNAVAILABLE", phase="workspace") from exc
root = run_root.resolve()
if not root.is_dir() or run_root.is_symlink():
raise WorkerError("RUNTIME_RUN_ROOT_INVALID", phase="workspace")
for current, dirs, files in os.walk(root, topdown=True, followlinks=False):
current_path = Path(current)
for name in dirs + files:
candidate = current_path / name
if candidate.is_symlink():
raise WorkerError(
"RUNTIME_WORKSPACE_SYMLINK_REJECTED",
phase="workspace",
details={"path": str(candidate.relative_to(root))},
)
for candidate in [current_path, *(current_path / name for name in dirs), *(current_path / name for name in files)]:
st = candidate.stat()
os.chown(candidate, -1, gid)
mode = st.st_mode & 0o777
os.chmod(candidate, mode | (0o070 if candidate.is_dir() else 0o060))
def cleanup_run_root(run_root: Path) -> None:
root = WORKSPACE_ROOT.resolve()
candidate = run_root.resolve(strict=False)
if candidate == root or root not in candidate.parents:
raise WorkerError("CLEANUP_SCOPE_INVALID", phase="cleanup")
if candidate.exists():
shutil.rmtree(candidate)
def allowed_programs() -> set[str]:
raw = os.environ.get("AIOS_AUTONOMOUS_ALLOWED_PROGRAMS_JSON", '["/usr/bin/git","/usr/bin/python3"]')
try:
value = json.loads(raw)
except json.JSONDecodeError as exc:
raise WorkerError("ALLOWED_PROGRAMS_CONFIG_INVALID", phase="startup") from exc
if not isinstance(value, list) or not all(isinstance(item, str) and item.startswith("/") for item in value):
raise WorkerError("ALLOWED_PROGRAMS_CONFIG_INVALID", phase="startup")
return set(value)
def validate_verification_argv(value: Any) -> list[str]:
if not isinstance(value, list) or not value or not all(isinstance(item, str) and item for item in value):
raise WorkerError("VERIFICATION_ARGV_INVALID", phase="verification")
argv = list(value)
if argv[0] not in allowed_programs():
raise WorkerError("VERIFICATION_PROGRAM_NOT_ALLOWED", phase="verification", details={"program": Path(argv[0]).name})
if argv[0].endswith("/git") and any(item in {"push", "update-ref", "receive-pack", "send-pack", "commit", "reset", "clean"} for item in argv[1:]):
raise WorkerError("VERIFICATION_GIT_MUTATION_REJECTED", phase="verification")
if argv[0].endswith("/python3") and "-c" in argv:
raise WorkerError("VERIFICATION_INLINE_PYTHON_REJECTED", phase="verification")
return argv
def run_verification(label: str, argv: Sequence[str], *, cwd: Path, timeout: int) -> dict[str, Any]:
started = time.monotonic()
completed = subprocess.run(
list(argv),
cwd=str(cwd),
capture_output=True,
check=False,
timeout=timeout,
env={"PATH": "/usr/local/bin:/usr/bin:/bin", "LC_ALL": "C.UTF-8"},
)
result = {
"label": label[:120],
"argv": [Path(item).name if idx == 0 else item for idx, item in enumerate(argv)],
"exit_code": completed.returncode,
"stdout_sha256": sha256_bytes(completed.stdout),
"stderr_sha256": sha256_bytes(completed.stderr),
"duration_ms": int((time.monotonic() - started) * 1000),
}
audit("VERIFICATION_RESULT", **result)
if completed.returncode != 0:
raise WorkerError(
"VERIFICATION_FAILED",
phase="verification",
details={"label": label[:120], "exit_code": completed.returncode},
)
return result
def independent_verification(
worktree: Path,
changed_files: Sequence[str],
runtime_verification: Sequence[Mapping[str, Any]],
) -> list[dict[str, Any]]:
results = [
run_verification(
"git_diff_check",
["/usr/bin/git", "-C", str(worktree), "diff", "--check"],
cwd=worktree,
timeout=120,
)
]
py_files = [item for item in changed_files if item.endswith(".py")]
if py_files:
results.append(
run_verification(
"changed_python_compile",
["/usr/bin/python3", "-m", "py_compile", *py_files],
cwd=worktree,
timeout=300,
)
)
test_files = [
item for item in py_files
if item.startswith("tests/") or Path(item).name.startswith("test_")
]
if test_files:
results.append(
run_verification(
"changed_pytest",
["/usr/bin/python3", "-m", "pytest", "-q", *test_files],
cwd=worktree,
timeout=900,
)
)
seen: set[str] = set()
for item in runtime_verification:
argv = validate_verification_argv(item.get("argv"))
cwd_value = item.get("cwd", ".")
if cwd_value in {"", "."}:
cwd = worktree
else:
if not isinstance(cwd_value, str) or cwd_value.startswith(("/", "~")) or ".." in Path(cwd_value).parts:
raise WorkerError("VERIFICATION_CWD_INVALID", phase="verification")
cwd = (worktree / cwd_value).resolve()
if worktree.resolve() not in cwd.parents and cwd != worktree.resolve():
raise WorkerError("VERIFICATION_CWD_ESCAPE", phase="verification")
timeout = item.get("timeout_seconds", 300)
if not isinstance(timeout, int) or isinstance(timeout, bool) or not 1 <= timeout <= 1800:
raise WorkerError("VERIFICATION_TIMEOUT_INVALID", phase="verification")
key = sha256_json({"argv": argv, "cwd": str(cwd), "timeout": timeout})
if key in seen:
continue
seen.add(key)
results.append(
run_verification(str(item.get("label") or "agent_requested_verification"), argv, cwd=cwd, timeout=timeout)
)
return results
def governed_commit(
task_id: str,
resolution: Resolution,
worktree: Path,
changed_files: Sequence[str],
) -> dict[str, Any]:
files: dict[str, str] = {}
for relative in changed_files:
path = (worktree / relative).resolve()
if worktree.resolve() not in path.parents or not path.is_file():
raise WorkerError("COMMIT_FILE_INVALID", phase="committing", details={"path": relative})
files[relative] = base64.b64encode(path.read_bytes()).decode("ascii")
governed_task_id = f"{task_id}-writer"
create_key = f"{task_id}-governed-v2"
commit_key = f"{task_id}-commit-v2"
status, created = bridge_call(
"/v2/governed-task",
{
"task_id": governed_task_id,
"repository": resolution.repository,
"base_oid": resolution.base_oid,
"target_branch": resolution.target_branch,
"risk": "engineering",
"description": f"Autonomous intent task {task_id}",
"idempotency_key": create_key,
},
)
if not 200 <= status < 300:
raise WorkerError(
"GOVERNED_TASK_CREATE_FAILED",
phase="committing",
details={"api_status": status, "error": created.get("error")},
)
generation = int(created.get("generation") or 1)
expected = str(created.get("expected_branch_oid") or resolution.base_oid)
branch_mode = str(created.get("branch_mode") or "CREATE_NEW")
status, lease = bridge_call(
"/v2/writer-lease",
{
"schema": "aios.task-writer-lease.v2",
"task_id": governed_task_id,
"repository": resolution.repository,
"branch_mode": branch_mode,
"base_oid": resolution.base_oid,
"expected_branch_oid": expected,
"target_branch": resolution.target_branch,
"generation": generation,
"lease_seconds": 900,
},
)
if not 200 <= status < 300:
raise WorkerError(
"WRITER_LEASE_ACQUIRE_FAILED",
phase="committing",
details={"api_status": status, "error": lease.get("error")},
)
status, committed = bridge_call(
"/v2/commit-files",
{
"task_id": governed_task_id,
"repository": resolution.repository,
"expected_branch_oid": expected,
"target_branch": resolution.target_branch,
"generation": generation,
"idempotency_key": commit_key,
"files": files,
"message": f"chore(agent): execute online intent task {task_id}"[:240],
},
)
if not 200 <= status < 300:
raise WorkerError(
"GOVERNED_COMMIT_FAILED",
phase="committing",
details={"api_status": status, "error": committed.get("error")},
)
commit_oid = committed.get("commit_oid")
if not isinstance(commit_oid, str) or not OID_RE.fullmatch(commit_oid):
raise WorkerError("GOVERNED_COMMIT_OID_INVALID", phase="committing")
return {
"governed_task_id": governed_task_id,
"generation": generation,
"branch_mode": branch_mode,
"commit_oid": commit_oid,
"commit_visible": committed.get("commit_visible"),
"target_branch": resolution.target_branch,
"create_idempotency_key": create_key,
"commit_idempotency_key": commit_key,
}
def socket_for(adapter: str) -> Path:
if adapter == "hermes":
return HERMES_SOCKET
if adapter == "codex":
return CODEX_SOCKET
if adapter == "openclaw":
return OPENCLAW_SOCKET
raise WorkerError("ADAPTER_INVALID", phase="runtime")
def resolve_task(task: Mapping[str, Any], prior: Mapping[str, Any] | None = None) -> Resolution:
try:
return resolve(
task,
authority_git_dir=AUTHORITY_GIT_DIR,
repository=REPOSITORY,
base_ref=BASE_REF,
available_adapters=available_adapters(),
default_adapter=DEFAULT_ADAPTER,
prior=prior,
)
except IntentError as exc:
raise WorkerError(exc.code, phase="intent_resolution", details=exc.details) from exc
def execute_claim(task: Mapping[str, Any], claim: Mapping[str, Any], token: str) -> int:
task_id = str(claim.get("task_id") or task.get("task_id") or "")
lease_token = claim.get("lease_token")
if not isinstance(lease_token, str) or len(lease_token) < 24:
raise WorkerError("LEASE_TOKEN_NOT_RETURNED", phase="claim")
attempt = int(claim.get("attempt") or 0)
state = load_state(task_id) or {"task_id": task_id, "phase": "claimed"}
prior = state.get("resolution") if isinstance(state.get("resolution"), Mapping) else None
resolution = resolve_task(task, prior=prior)
state["resolution"] = resolution.to_json()
state["attempt"] = attempt
save_state(task_id, state)
heartbeat = LeaseHeartbeat(task_id, token, lease_token)
heartbeat.start()
run_root: Path | None = None
try:
heartbeat.set_stage("started")
heartbeat.send()
audit("TASK_STARTED", task_id=task_id, attempt=attempt, adapter=resolution.adapter)
existing_commit = state.get("governed")
if isinstance(existing_commit, Mapping) and isinstance(existing_commit.get("commit_oid"), str):
result = {
"status": "SUCCEEDED",
"verification_state": "VERIFIED_BY_WORKER_READBACK_PENDING_SERVER",
"task_id": task_id,
"execution_mode": "autonomous-intent-v2",
"adapter": resolution.adapter,
"resolution_digest": resolution.resolution_digest,
"governed": dict(existing_commit),
"recovered_after_commit": True,
"secret_material_exposed": False,
"direct_main_write": False,
"force_push": False,
"production_write": False,
}
else:
heartbeat.set_stage("workspace_preparing")
run_root, worktree = prepare_worktree(task_id, resolution.base_oid)
share_run_root(run_root)
state["phase"] = "workspace_prepared"
save_state(task_id, state)
heartbeat.set_stage("agent_executing")
payload = request_payload(
adapter=resolution.adapter,
task_id=task_id,
goal=resolution.goal,
instructions=resolution.instructions,
success_criteria=resolution.success_criteria,
repository=resolution.repository,
base_oid=resolution.base_oid,
target_branch=resolution.target_branch,
worktree=worktree,
)
runtime = invoke(
socket_for(resolution.adapter),
payload,
timeout_seconds=RUNTIME_TIMEOUT_SECONDS,
)
heartbeat.set_stage("validating")
changed_files = collect_changes(worktree, resolution.base_oid)
scan_secrets(worktree, changed_files)
verification = independent_verification(worktree, changed_files, runtime.verification)
heartbeat.set_stage("committing")
governed = governed_commit(task_id, resolution, worktree, changed_files)
state.update({"phase": "commit_done", "governed": governed, "changed_files": list(changed_files)})
save_state(task_id, state)
result = {
"status": "SUCCEEDED",
"verification_state": "VERIFIED_BY_WORKER_READBACK_PENDING_SERVER",
"task_id": task_id,
"execution_mode": "autonomous-intent-v2",
"adapter": resolution.adapter,
"resolution_digest": resolution.resolution_digest,
"base_oid": resolution.base_oid,
"target_branch": resolution.target_branch,
"changed_files": list(changed_files),
"tests": verification,
"agent_summary": runtime.summary,
"agent_evidence": sanitize(runtime.evidence),
"governed": governed,
"secret_material_exposed": False,
"direct_main_write": False,
"force_push": False,
"production_write": False,
}
if heartbeat.failed:
raise WorkerError("HEARTBEAT_UNHEALTHY", phase="result_submitting", details={"error": heartbeat.failed})
heartbeat.set_stage("result_submitting")
result_key = f"{task_id}-result-v2"
status, response = api_call(
"POST",
f"/v1/tasks/{task_id}/result",
token,
{
"lease_token": lease_token,
"idempotency_key": result_key,
"result": result,
},
)
if not 200 <= status < 300:
raise WorkerError(
"TASK_RESULT_SUBMIT_FAILED",
phase="result_submitting",
details={"api_status": status, "error": response.get("error")},
)
audit("RESULT_SUBMITTED", task_id=task_id, task_status=response.get("status"), result_idempotency_key=result_key)
state["phase"] = "result_submitted"
save_state(task_id, state)
return 0
except (WorkerError, RuntimeErrorV2) as exc:
code = exc.code if hasattr(exc, "code") else type(exc).__name__
phase = exc.phase if isinstance(exc, WorkerError) else "runtime"
details = exc.details if hasattr(exc, "details") else {}
audit("AUTONOMOUS_EXECUTION_FAILED", task_id=task_id, error=code, phase=phase, details=details)
failure_status = "BLOCKED" if code in {
"AGENT_RUNTIME_UNAVAILABLE", "RUNTIME_UNAVAILABLE", "RUNTIME_BLOCKED",
"GOVERNED_WRITE_API_UNAVAILABLE", "NON_STAGING_BRIDGE_REJECTED",
} else "FAILED_RETRYABLE"
failure = {
"status": failure_status,
"verification_state": "UNVERIFIED",
"task_id": task_id,
"execution_mode": "autonomous-intent-v2",
"error": code,
"phase": phase,
"details": sanitize(details),
"secret_material_exposed": False,
"direct_main_write": False,
"force_push": False,
"production_write": False,
}
if not heartbeat.failed:
status, response = api_call(
"POST",
f"/v1/tasks/{task_id}/result",
token,
{
"lease_token": lease_token,
"idempotency_key": f"{task_id}-attempt-{attempt}-failure-v2",
"result": failure,
},
)
audit("FAILURE_RESULT_SUBMITTED", task_id=task_id, api_status=status, task_status=response.get("status"))
return 20
finally:
heartbeat.close()
if run_root is not None:
try:
cleanup_run_root(run_root)
audit("WORKSPACE_CLEANED", task_id=task_id, cleanup="PASS")
except Exception as exc:
audit("WORKSPACE_CLEANUP_FAILED", task_id=task_id, error=type(exc).__name__)
def worker_once(token: str) -> int:
status, poll = api_call("POST", "/v1/tasks/poll", token, {"limit": 100})
if status != 200:
audit("TASK_POLL_FAILED", api_status=status, error=poll.get("error"))
return 10
worker_caps = capabilities()
candidates: list[Mapping[str, Any]] = []
for item in poll.get("tasks", []):
if not isinstance(item, Mapping):
continue
if not is_claimable_intent(item, worker_caps):
continue
try:
# Read-only preflight. Do not consume an attempt if runtime/base is unavailable.
resolve_task(item, prior=(load_state(str(item.get("task_id") or "")) or {}).get("resolution"))
except WorkerError as exc:
audit("TASK_SKIPPED_PREFLIGHT", task_id=item.get("task_id"), error=exc.code)
continue
candidates.append(item)
if not candidates:
return 0
# Server priority/order remains authoritative; take the first visible compatible task.
task = candidates[0]
task_id = str(task.get("task_id") or "")
status, claim = api_call("POST", f"/v1/tasks/{task_id}/claim", token, {"lease_seconds": LEASE_SECONDS})
if status != 200:
audit("TASK_CLAIM_FAILED", task_id=task_id, api_status=status, error=claim.get("error"))
return 11
audit(
"TASK_CLAIMED",
task_id=task_id,
attempt=claim.get("attempt"),
revision=claim.get("revision"),
claim_mode="server_atomic",
execution_mode="autonomous-intent-v2",
)
merged = dict(task)
merged.update({key: value for key, value in claim.items() if key not in {"lease_token"}})
merged["task_id"] = task_id
return execute_claim(merged, claim, token)
def main() -> int:
ensure_roots()
if BRIDGE_BASE != "http://127.0.0.1:19998":
raise WorkerError("NON_STAGING_BRIDGE_REJECTED", phase="startup", details={"bridge_base": BRIDGE_BASE})
token = load_agent_token()
audit(
"AUTONOMOUS_WORKER_READY",
poll_mode="continuous",
claim_mode="server_atomic",
task_scope="intent-only",
bridge_base=BRIDGE_BASE,
available_adapters=list(available_adapters()),
capabilities=list(capabilities()),
main_write=False,
production_write=False,
force_push=False,
)
while True:
try:
result = worker_once(token)
except Exception as exc:
audit("WORKER_LOOP_ERROR", error=type(exc).__name__)
result = 20
time.sleep(POLL_SECONDS if result == 0 else max(POLL_SECONDS, 5))
if __name__ == "__main__":
raise SystemExit(main())
@@ -0,0 +1,273 @@
from __future__ import annotations
from dataclasses import asdict, dataclass
import hashlib
import json
from pathlib import Path
import re
import subprocess
from typing import Any, Mapping, Sequence
OID_RE = re.compile(r"^[0-9a-f]{40}$")
TASK_ID_RE = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._:-]{2,179}$")
REPOSITORY_RE = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._/-]{2,199}$")
ALLOWED_ADAPTERS = frozenset({"hermes", "codex", "openclaw"})
class IntentError(RuntimeError):
def __init__(self, code: str, *, details: Mapping[str, Any] | None = None) -> None:
super().__init__(code)
self.code = code
self.details = dict(details or {})
def stable_digest(value: Any) -> str:
raw = json.dumps(value, ensure_ascii=False, sort_keys=True, separators=(",", ":")).encode("utf-8")
return hashlib.sha256(raw).hexdigest()
def _text(value: Any, field: str, limit: int) -> str:
if not isinstance(value, str):
raise IntentError(f"{field.upper()}_INVALID")
value = value.strip()
if not value or len(value) > limit:
raise IntentError(f"{field.upper()}_INVALID")
return value
def _string_list(value: Any, field: str, *, required: bool) -> tuple[str, ...]:
if value is None and not required:
return ()
if not isinstance(value, list) or len(value) > 64 or (required and not value):
raise IntentError(f"{field.upper()}_INVALID")
return tuple(_text(item, field, 4096) for item in value)
@dataclass(frozen=True)
class IntentTask:
task_id: str
goal: str
instructions: str
success_criteria: tuple[str, ...]
required_capabilities: tuple[str, ...]
digest: str
@classmethod
def parse(cls, task: Mapping[str, Any]) -> "IntentTask":
task_id = str(task.get("task_id") or "")
if not TASK_ID_RE.fullmatch(task_id):
raise IntentError("TASK_ID_INVALID")
metadata = task.get("metadata")
if isinstance(metadata, Mapping) and isinstance(metadata.get("execution"), Mapping):
raise IntentError("STRUCTURED_TASK_NOT_INTENT")
goal = _text(task.get("goal"), "goal", 16384)
instructions = _text(
task.get("instructions") or "Inspect the repository, implement the goal, and satisfy the success criteria.",
"instructions",
65536,
)
success = _string_list(task.get("success_criteria"), "success_criteria", required=True)
capabilities = _string_list(task.get("required_capabilities") or [], "required_capabilities", required=False)
required_markers = {"intent.task", "engineering.execute"}
missing = sorted(required_markers - set(capabilities))
if missing:
raise IntentError("INTENT_CAPABILITY_MARKER_REQUIRED", details={"missing": missing})
canonical = {
"task_id": task_id,
"goal": goal,
"instructions": instructions,
"success_criteria": success,
"required_capabilities": sorted(capabilities),
}
return cls(
task_id=task_id,
goal=goal,
instructions=instructions,
success_criteria=success,
required_capabilities=capabilities,
digest=stable_digest(canonical),
)
@dataclass(frozen=True)
class Resolution:
schema: str
task_id: str
repository: str
base_ref: str
base_oid: str
target_branch: str
adapter: str
intent_digest: str
goal: str
instructions: str
success_criteria: tuple[str, ...]
required_capabilities: tuple[str, ...]
resolution_digest: str
def to_json(self) -> dict[str, Any]:
value = asdict(self)
value["success_criteria"] = list(self.success_criteria)
value["required_capabilities"] = list(self.required_capabilities)
return value
@classmethod
def from_json(cls, value: Mapping[str, Any], *, intent: IntentTask) -> "Resolution":
if value.get("schema") != "aios.autonomous-intent-resolution.v2":
raise IntentError("RESOLUTION_SCHEMA_INVALID")
if value.get("task_id") != intent.task_id or value.get("intent_digest") != intent.digest:
raise IntentError("RESOLUTION_TASK_DRIFT")
result = cls(
schema=str(value["schema"]),
task_id=str(value["task_id"]),
repository=str(value["repository"]),
base_ref=str(value["base_ref"]),
base_oid=str(value["base_oid"]),
target_branch=str(value["target_branch"]),
adapter=str(value["adapter"]),
intent_digest=str(value["intent_digest"]),
goal=str(value["goal"]),
instructions=str(value["instructions"]),
success_criteria=tuple(str(item) for item in value.get("success_criteria") or []),
required_capabilities=tuple(str(item) for item in value.get("required_capabilities") or []),
resolution_digest=str(value["resolution_digest"]),
)
validate_resolution(result)
if result.resolution_digest != resolution_digest(result):
raise IntentError("RESOLUTION_DIGEST_INVALID")
return result
def read_base_oid(authority_git_dir: Path, base_ref: str) -> str:
if not base_ref.startswith("refs/heads/"):
raise IntentError("BASE_REF_INVALID")
if authority_git_dir.is_symlink() or not authority_git_dir.is_dir():
raise IntentError("AUTHORITY_GIT_DIR_INVALID")
completed = subprocess.run(
["/usr/bin/git", "--git-dir", str(authority_git_dir), "rev-parse", "--verify", f"{base_ref}^{{commit}}"],
capture_output=True,
text=True,
check=False,
timeout=30,
env={"PATH": "/usr/bin:/bin", "LC_ALL": "C.UTF-8"},
)
oid = completed.stdout.strip()
if completed.returncode != 0 or not OID_RE.fullmatch(oid):
raise IntentError("BASE_OID_UNAVAILABLE")
return oid
def derive_branch(task_id: str, digest: str) -> str:
slug = re.sub(r"[^A-Za-z0-9._-]+", "-", task_id).strip(".-").lower() or "task"
slug = slug[:100]
branch = f"agent/intent/{slug}-{digest[:12]}"
if len(branch) > 180:
branch = branch[:167] + "-" + digest[:12]
if ".." in branch or branch.endswith(("/", ".", ".lock")):
raise IntentError("TARGET_BRANCH_INVALID")
return branch
def select_adapter(capabilities: Sequence[str], available: Sequence[str], default_adapter: str) -> str:
advertised = set(available)
if not advertised.issubset(ALLOWED_ADAPTERS):
raise IntentError("ADAPTER_CONFIGURATION_INVALID")
requested = {
item.split(".", 2)[-1]
for item in capabilities
if item.startswith("agent.runtime.")
}
requested.discard("")
if len(requested) > 1:
raise IntentError("MULTIPLE_AGENT_RUNTIMES_REQUIRED")
adapter = next(iter(requested)) if requested else default_adapter
if adapter not in ALLOWED_ADAPTERS:
raise IntentError("AGENT_RUNTIME_INVALID")
if adapter not in advertised:
raise IntentError("AGENT_RUNTIME_UNAVAILABLE", details={"adapter": adapter})
return adapter
def resolution_digest(value: Resolution) -> str:
return stable_digest({
"schema": value.schema,
"task_id": value.task_id,
"repository": value.repository,
"base_ref": value.base_ref,
"base_oid": value.base_oid,
"target_branch": value.target_branch,
"adapter": value.adapter,
"intent_digest": value.intent_digest,
"goal": value.goal,
"instructions": value.instructions,
"success_criteria": value.success_criteria,
"required_capabilities": sorted(value.required_capabilities),
})
def validate_resolution(value: Resolution) -> None:
if not TASK_ID_RE.fullmatch(value.task_id):
raise IntentError("TASK_ID_INVALID")
if not REPOSITORY_RE.fullmatch(value.repository) or value.repository.startswith(("/", ".")):
raise IntentError("REPOSITORY_INVALID")
if not value.base_ref.startswith("refs/heads/"):
raise IntentError("BASE_REF_INVALID")
if not OID_RE.fullmatch(value.base_oid):
raise IntentError("BASE_OID_INVALID")
if not value.target_branch.startswith("agent/intent/"):
raise IntentError("TARGET_BRANCH_POLICY_INVALID")
if value.adapter not in ALLOWED_ADAPTERS:
raise IntentError("AGENT_RUNTIME_INVALID")
def resolve(
task: Mapping[str, Any],
*,
authority_git_dir: Path,
repository: str,
base_ref: str,
available_adapters: Sequence[str],
default_adapter: str,
prior: Mapping[str, Any] | None = None,
) -> Resolution:
intent = IntentTask.parse(task)
if prior is not None:
return Resolution.from_json(prior, intent=intent)
if not REPOSITORY_RE.fullmatch(repository) or repository.startswith(("/", ".")):
raise IntentError("REPOSITORY_INVALID")
base_oid = read_base_oid(authority_git_dir, base_ref)
adapter = select_adapter(intent.required_capabilities, available_adapters, default_adapter)
provisional = Resolution(
schema="aios.autonomous-intent-resolution.v2",
task_id=intent.task_id,
repository=repository,
base_ref=base_ref,
base_oid=base_oid,
target_branch=derive_branch(intent.task_id, intent.digest),
adapter=adapter,
intent_digest=intent.digest,
goal=intent.goal,
instructions=intent.instructions,
success_criteria=intent.success_criteria,
required_capabilities=intent.required_capabilities,
resolution_digest="",
)
result = Resolution(
**{**asdict(provisional), "resolution_digest": resolution_digest(provisional)}
)
validate_resolution(result)
return result
def is_claimable_intent(task: Mapping[str, Any], worker_capabilities: Sequence[str]) -> bool:
try:
intent = IntentTask.parse(task)
except IntentError:
return False
required = set(intent.required_capabilities)
return required.issubset(set(worker_capabilities))
@@ -0,0 +1,197 @@
#!/usr/bin/env python3
from __future__ import annotations
import json
import os
from pathlib import Path
import subprocess
import tempfile
import urllib.error
import urllib.request
from datetime import datetime, timezone
from typing import Any
BASE = os.environ.get("AIOS_AGENT_TASK_API_URL", "http://127.0.0.1:18940").rstrip("/")
AGENT_ID = os.environ.get("AIOS_AGENT_ID", "online-engineering-worker-cn-network-mkt-autonomous-20260815")
AGENT_TYPE = os.environ.get("AIOS_AGENT_TYPE", "trusted-online-engineering-worker")
AGENT_VERSION = os.environ.get("AIOS_AGENT_VERSION", "2026-08-15.4-autonomous-intent")
STATE_DIR = Path(os.environ.get("AIOS_WORKER_REGISTRATION_STATE", "/var/lib/aios-autonomous-agent-worker/registration"))
CREDENTIAL_PATH = Path(os.environ.get("AIOS_WORKER_ENCRYPTED_CREDENTIAL", "/etc/credstore.encrypted/aios-autonomous-agent-worker/agent_token"))
IDENTITY_PATH = STATE_DIR / "identity.json"
PROJECTION_PROVIDER = "BridgeV2RuntimeCredentialProjection"
def now() -> str:
return datetime.now(timezone.utc).isoformat()
def runtime_capabilities() -> list[str]:
values = [
"online_agent_task_api", "task.poll", "task.claim", "task.heartbeat", "task.result",
"intent.task", "engineering.consumer", "engineering.execute", "engineering.autonomous",
"workspace.worktree", "git.read", "git.diff", "git.test",
"governed.write.branch", "governed.write.file",
]
hermes = os.environ.get("AIOS_HERMES_BROKER_SOCKET", "")
codex = os.environ.get("AIOS_CODEX_BROKER_SOCKET", "")
openclaw = os.environ.get("AIOS_OPENCLAW_BROKER_SOCKET", "")
if hermes and Path(hermes).is_socket():
values.append("agent.runtime.hermes")
if codex and Path(codex).is_socket():
values.append("agent.runtime.codex")
if openclaw and Path(openclaw).is_socket():
values.append("agent.runtime.openclaw")
return values
def api_call(method: str, route: str, token: str, body: dict[str, Any] | None = None) -> tuple[int, dict[str, Any]]:
payload = None if body is None else json.dumps(body, sort_keys=True, separators=(",", ":")).encode("utf-8")
headers = {"Authorization": "Bearer " + token, "Accept": "application/json"}
if payload is not None:
headers["Content-Type"] = "application/json"
request = urllib.request.Request(BASE + route, data=payload, headers=headers, method=method)
try:
with urllib.request.urlopen(request, timeout=15) as response:
value = json.loads(response.read().decode("utf-8"))
return response.status, value if isinstance(value, dict) else {}
except urllib.error.HTTPError as exc:
try:
value = json.loads(exc.read(2048).decode("utf-8"))
except Exception:
value = {"error": "HTTP_ERROR"}
return exc.code, value if isinstance(value, dict) else {"error": "HTTP_ERROR"}
except (urllib.error.URLError, TimeoutError):
return 599, {"error": "AGENT_TASK_API_UNAVAILABLE"}
def encrypted_credential_is_valid() -> bool:
if not CREDENTIAL_PATH.is_file() or CREDENTIAL_PATH.is_symlink():
return False
result = subprocess.run(
["systemd-creds", "decrypt", "--with-key=host", str(CREDENTIAL_PATH), "/dev/null"],
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, check=False, timeout=15,
)
return result.returncode == 0
def load_identity() -> dict[str, Any] | None:
if not IDENTITY_PATH.is_file() or IDENTITY_PATH.is_symlink():
return None
try:
value = json.loads(IDENTITY_PATH.read_text(encoding="utf-8"))
except Exception:
return None
return value if isinstance(value, dict) else None
def identity_matches(value: dict[str, Any] | None) -> bool:
return bool(
value
and value.get("agent_id") == AGENT_ID
and value.get("agent_type") == AGENT_TYPE
and value.get("agent_version") == AGENT_VERSION
and value.get("task_scope") == "intent-only"
and value.get("credential_projection_provider") == PROJECTION_PROVIDER
)
def encrypt_agent_token(token: str) -> None:
CREDENTIAL_PATH.parent.mkdir(parents=True, mode=0o700, exist_ok=True)
fd, temporary = tempfile.mkstemp(prefix=".agent-token.", dir=str(CREDENTIAL_PATH.parent))
os.close(fd)
os.unlink(temporary)
try:
result = subprocess.run(
["systemd-creds", "encrypt", "--with-key=host", "--name=agent_token", "/dev/stdin", temporary],
input=(token + "\n").encode("utf-8"),
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, check=False, timeout=20,
)
if result.returncode != 0:
raise RuntimeError("AGENT_CREDENTIAL_ENCRYPT_FAILED")
os.chmod(temporary, 0o600)
os.replace(temporary, CREDENTIAL_PATH)
finally:
if os.path.exists(temporary):
os.unlink(temporary)
def write_identity() -> None:
value = {
"agent_id": AGENT_ID,
"agent_type": AGENT_TYPE,
"agent_version": AGENT_VERSION,
"task_scope": "intent-only",
"credential_projection_provider": PROJECTION_PROVIDER,
"credential_delivery": "systemd-LoadCredentialEncrypted",
"task_api_url": BASE,
"task_api_schema": "aios.agent-task.v1",
"execution_backend": "autonomous-intent-worker-v2",
"runtime_only_secret_delivery": True,
"main_write": False,
"production_write": False,
"force_push": False,
"tag_write": False,
"baseline_write": False,
"capabilities": runtime_capabilities(),
"created_at": now(),
}
STATE_DIR.mkdir(parents=True, mode=0o750, exist_ok=True)
fd, temporary = tempfile.mkstemp(prefix="identity.", dir=str(STATE_DIR))
try:
with os.fdopen(fd, "w", encoding="utf-8") as handle:
json.dump(value, handle, ensure_ascii=False, sort_keys=True)
handle.write("\n")
os.chmod(temporary, 0o640)
os.replace(temporary, IDENTITY_PATH)
finally:
if os.path.exists(temporary):
os.unlink(temporary)
def register_or_recover() -> str:
STATE_DIR.mkdir(parents=True, mode=0o750, exist_ok=True)
existing = load_identity()
if CREDENTIAL_PATH.exists():
if not identity_matches(existing):
raise RuntimeError("AGENT_IDENTITY_BINDING_CONFLICT")
if not encrypted_credential_is_valid():
raise RuntimeError("AGENT_ENCRYPTED_CREDENTIAL_INVALID")
return "RECOVERED"
if existing is not None:
raise RuntimeError("AGENT_CREDENTIAL_MISSING_AFTER_PRIOR_REGISTRATION")
credential_directory = Path(os.environ.get("CREDENTIALS_DIRECTORY", ""))
control_path = credential_directory / "bridge_control_token"
if not control_path.is_file():
raise RuntimeError("BRIDGE_RUNTIME_CONTROL_CREDENTIAL_UNAVAILABLE")
control_token = control_path.read_text(encoding="utf-8").strip()
status, response = api_call(
"POST", "/v1/agents/register", control_token,
{
"agent_id": AGENT_ID,
"agent_type": AGENT_TYPE,
"agent_version": AGENT_VERSION,
"capabilities": runtime_capabilities(),
"metadata": {
"task_scope": "intent-only",
"credential_projection_provider": PROJECTION_PROVIDER,
"runtime_only_secret_delivery": True,
"execution_backend": "autonomous-intent-worker-v2",
"main_write": False,
"production_write": False,
"force_push": False,
},
},
)
if status != 201:
raise RuntimeError("AGENT_REGISTER_FAILED_" + str(response.get("error") or status))
token = response.get("agent_token")
if not isinstance(token, str) or len(token) < 24:
raise RuntimeError("AGENT_TOKEN_NOT_RETURNED")
encrypt_agent_token(token)
write_identity()
return "REGISTERED"
if __name__ == "__main__":
raise SystemExit(0 if register_or_recover() in {"REGISTERED", "RECOVERED"} else 20)
@@ -0,0 +1,224 @@
from __future__ import annotations
from dataclasses import dataclass
import json
from pathlib import Path
import re
import socket
import subprocess
from typing import Any, Mapping, Sequence
OID_RE = re.compile(r"^[0-9a-f]{40}$")
MAX_BYTES = 1024 * 1024
MAX_CHANGED_FILES = 256
MAX_FILE_BYTES = 4 * 1024 * 1024
SENSITIVE_NAMES = frozenset({
".env", ".env.local", ".env.production", "id_rsa", "id_ed25519",
"credentials", "credentials.json",
})
SENSITIVE_SUFFIXES = (".key", ".p12", ".pfx")
SECRET_PATTERNS = (
re.compile(rb"-----BEGIN (?:RSA |EC |OPENSSH )?PRIVATE KEY-----"),
re.compile(rb"\bAKIA[0-9A-Z]{16}\b"),
re.compile(rb"\bghp_[A-Za-z0-9]{30,}\b"),
re.compile(rb"\bsk-[A-Za-z0-9_-]{24,}\b"),
)
class RuntimeErrorV2(RuntimeError):
def __init__(self, code: str, *, details: Mapping[str, Any] | None = None) -> None:
super().__init__(code)
self.code = code
self.details = dict(details or {})
def safe_relative_path(value: str) -> str:
if (
not value
or value.startswith(("/", "~"))
or "\\" in value
or "\x00" in value
or any(part in {"", ".", ".."} for part in value.split("/"))
):
raise RuntimeErrorV2("CHANGED_PATH_INVALID", details={"path": value[:160]})
lowered = value.split("/")[-1].lower()
if value.startswith(".git/") or value in {".git", ".gitmodules"}:
raise RuntimeErrorV2("GIT_METADATA_CHANGE_REJECTED", details={"path": value})
if lowered in SENSITIVE_NAMES or lowered.endswith(SENSITIVE_SUFFIXES):
raise RuntimeErrorV2("SENSITIVE_PATH_REJECTED", details={"path": value})
return value
def git(worktree: Path, args: Sequence[str], timeout: int = 60) -> bytes:
completed = subprocess.run(
["/usr/bin/git", "-C", str(worktree), *args],
capture_output=True,
check=False,
timeout=timeout,
env={"PATH": "/usr/bin:/bin", "LC_ALL": "C.UTF-8"},
)
if completed.returncode != 0:
raise RuntimeErrorV2("WORKTREE_GIT_FAILED", details={"operation": args[0] if args else ""})
return completed.stdout
def _zpaths(raw: bytes) -> list[str]:
result: list[str] = []
for item in raw.split(b"\0"):
if not item:
continue
try:
result.append(item.decode("utf-8"))
except UnicodeDecodeError as exc:
raise RuntimeErrorV2("WORKTREE_PATH_ENCODING_INVALID") from exc
return result
def collect_changes(worktree: Path, base_oid: str) -> list[str]:
if not OID_RE.fullmatch(base_oid):
raise RuntimeErrorV2("BASE_OID_INVALID")
head = git(worktree, ["rev-parse", "HEAD"]).decode("ascii", "strict").strip()
if head != base_oid:
raise RuntimeErrorV2("WORKTREE_HEAD_MUTATED", details={"expected": base_oid, "actual": head})
tracked = _zpaths(git(worktree, ["diff", "--name-only", "--no-renames", "-z", base_oid, "--"]))
untracked = _zpaths(git(worktree, ["ls-files", "--others", "--exclude-standard", "-z"]))
changed = sorted({safe_relative_path(item) for item in tracked + untracked})
if not changed:
raise RuntimeErrorV2("AGENT_PRODUCED_NO_CHANGES")
if len(changed) > MAX_CHANGED_FILES:
raise RuntimeErrorV2("CHANGED_FILE_LIMIT_EXCEEDED")
root = worktree.resolve()
for relative in changed:
path = (root / relative).resolve(strict=False)
if root not in path.parents or not path.is_file() or path.is_symlink():
raise RuntimeErrorV2("CHANGED_FILE_UNSUPPORTED", details={"path": relative})
if path.stat().st_size > MAX_FILE_BYTES:
raise RuntimeErrorV2("CHANGED_FILE_TOO_LARGE", details={"path": relative})
return changed
def scan_secrets(worktree: Path, changed: Sequence[str]) -> None:
root = worktree.resolve()
for relative in changed:
path = (root / safe_relative_path(relative)).resolve()
raw = path.read_bytes()
for pattern in SECRET_PATTERNS:
if pattern.search(raw):
raise RuntimeErrorV2("SECRET_MATERIAL_REJECTED", details={"path": relative})
@dataclass(frozen=True)
class RuntimeResult:
status: str
summary: str
verification: tuple[dict[str, Any], ...]
evidence: dict[str, Any]
@classmethod
def parse(cls, value: Mapping[str, Any]) -> "RuntimeResult":
if value.get("schema") != "aios.online-agent.runtime-result.v1":
raise RuntimeErrorV2("RUNTIME_RESULT_SCHEMA_INVALID")
status = str(value.get("status") or "")
if status not in {"COMPLETED", "BLOCKED", "FAILED"}:
raise RuntimeErrorV2("RUNTIME_RESULT_STATUS_INVALID")
summary = str(value.get("summary") or "").strip()
if not summary or len(summary) > 8192:
raise RuntimeErrorV2("RUNTIME_RESULT_SUMMARY_INVALID")
verification = value.get("verification") or []
evidence = value.get("evidence") or {}
if not isinstance(verification, list) or len(verification) > 64:
raise RuntimeErrorV2("RUNTIME_VERIFICATION_INVALID")
if not isinstance(evidence, Mapping):
raise RuntimeErrorV2("RUNTIME_EVIDENCE_INVALID")
return cls(status, summary, tuple(dict(item) for item in verification if isinstance(item, Mapping)), dict(evidence))
def request_payload(
*,
adapter: str,
task_id: str,
goal: str,
instructions: str,
success_criteria: Sequence[str],
repository: str,
base_oid: str,
target_branch: str,
worktree: Path,
) -> dict[str, Any]:
if adapter not in {"hermes", "codex", "openclaw"}:
raise RuntimeErrorV2("ADAPTER_INVALID")
if not OID_RE.fullmatch(base_oid):
raise RuntimeErrorV2("BASE_OID_INVALID")
return {
"schema": "aios.online-agent.runtime-request.v1",
"adapter": adapter,
"task": {
"task_id": task_id,
"goal": goal,
"instructions": instructions,
"success_criteria": list(success_criteria),
},
"execution_context": {
"repository": repository,
"base_oid": base_oid,
"target_branch": target_branch,
"worktree": str(worktree),
},
"policy": {
"authoritative_repository_write": False,
"direct_git_ref_mutation": False,
"direct_git_push": False,
"direct_git_commit": False,
"main_write": False,
"force_push": False,
"production_write": False,
"tag_write": False,
"baseline_write": False,
"task_api_credential_visible": False,
"writer_lease_visible": False,
},
}
def invoke(socket_path: Path, payload: Mapping[str, Any], timeout_seconds: int) -> RuntimeResult:
if not socket_path.is_absolute():
raise RuntimeErrorV2("RUNTIME_SOCKET_NOT_ABSOLUTE")
if not 1 <= timeout_seconds <= 7200:
raise RuntimeErrorV2("RUNTIME_TIMEOUT_INVALID")
raw = json.dumps(dict(payload), ensure_ascii=False, sort_keys=True, separators=(",", ":")).encode("utf-8") + b"\n"
if len(raw) > MAX_BYTES:
raise RuntimeErrorV2("RUNTIME_REQUEST_TOO_LARGE")
client = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
client.settimeout(timeout_seconds)
try:
client.connect(str(socket_path))
client.sendall(raw)
client.shutdown(socket.SHUT_WR)
chunks: list[bytes] = []
total = 0
while True:
chunk = client.recv(65536)
if not chunk:
break
total += len(chunk)
if total > MAX_BYTES:
raise RuntimeErrorV2("RUNTIME_RESPONSE_TOO_LARGE")
chunks.append(chunk)
except (OSError, socket.timeout) as exc:
raise RuntimeErrorV2("RUNTIME_UNAVAILABLE", details={"socket": str(socket_path)}) from exc
finally:
client.close()
try:
value = json.loads(b"".join(chunks).decode("utf-8"))
except (UnicodeDecodeError, json.JSONDecodeError) as exc:
raise RuntimeErrorV2("RUNTIME_RESPONSE_INVALID") from exc
if not isinstance(value, Mapping):
raise RuntimeErrorV2("RUNTIME_RESPONSE_INVALID")
result = RuntimeResult.parse(value)
if result.status == "BLOCKED":
raise RuntimeErrorV2("RUNTIME_BLOCKED", details={"summary": result.summary})
if result.status == "FAILED":
raise RuntimeErrorV2("RUNTIME_FAILED", details={"summary": result.summary})
return result
@@ -0,0 +1,101 @@
#!/usr/bin/env python3
from __future__ import annotations
import json
import os
from pathlib import Path
import shutil
import subprocess
import tempfile
from runtime_client import collect_changes, invoke, request_payload, scan_secrets
WORKSPACE_ROOT = Path(os.environ.get(
"AIOS_AUTONOMOUS_WORKSPACE_ROOT",
"/var/lib/aios-autonomous-agent-worker/workspaces",
))
ADAPTER = os.environ.get("AIOS_AUTONOMOUS_SELFTEST_ADAPTER", "openclaw")
SOCKET = Path(os.environ.get(
"AIOS_AUTONOMOUS_SELFTEST_BROKER_SOCKET",
f"/run/aios-agent-runtime/{ADAPTER}.sock",
))
def run(argv: list[str], cwd: Path) -> str:
completed = subprocess.run(
argv,
cwd=str(cwd),
text=True,
capture_output=True,
check=False,
timeout=60,
env={"PATH": "/usr/bin:/bin", "LC_ALL": "C.UTF-8"},
)
if completed.returncode != 0:
raise RuntimeError(f"SELFTEST_COMMAND_FAILED:{argv[0]}:{completed.returncode}")
return completed.stdout.strip()
def main() -> int:
if not SOCKET.is_socket():
raise RuntimeError("AUTONOMOUS_ENGINEERING_BACKEND_UNAVAILABLE")
WORKSPACE_ROOT.mkdir(parents=True, mode=0o750, exist_ok=True)
run_root = Path(tempfile.mkdtemp(prefix="runtime-selftest-", dir=str(WORKSPACE_ROOT)))
repo = run_root / "worktree"
repo.mkdir()
try:
run(["/usr/bin/git", "init", str(repo)], run_root)
run(["/usr/bin/git", "-C", str(repo), "config", "user.email", "selftest@example.invalid"], run_root)
run(["/usr/bin/git", "-C", str(repo), "config", "user.name", "AIOS Runtime Selftest"], run_root)
marker = repo / "agent_runtime_selftest.txt"
marker.write_text("VALUE=1\n", encoding="utf-8")
run(["/usr/bin/git", "-C", str(repo), "add", "agent_runtime_selftest.txt"], run_root)
run(["/usr/bin/git", "-C", str(repo), "commit", "-m", "selftest base"], run_root)
base_oid = run(["/usr/bin/git", "-C", str(repo), "rev-parse", "HEAD"], run_root)
payload = request_payload(
adapter=ADAPTER,
task_id="autonomous-runtime-selftest",
goal="Change agent_runtime_selftest.txt so it contains exactly VALUE=2 followed by a newline.",
instructions=(
"Modify only agent_runtime_selftest.txt in the provided disposable worktree. "
"Do not commit, push, change Git refs, or access task/governed-write credentials."
),
success_criteria=[
"agent_runtime_selftest.txt contains VALUE=2",
"Git HEAD remains unchanged",
],
repository="selftest/disposable",
base_oid=base_oid,
target_branch="agent/intent/runtime-selftest",
worktree=repo,
)
result = invoke(SOCKET, payload, timeout_seconds=300)
changed = collect_changes(repo, base_oid)
scan_secrets(repo, changed)
content = marker.read_text(encoding="utf-8")
head = run(["/usr/bin/git", "-C", str(repo), "rev-parse", "HEAD"], run_root)
if content != "VALUE=2\n":
raise RuntimeError("RUNTIME_SELFTEST_CONTENT_MISMATCH")
if head != base_oid:
raise RuntimeError("RUNTIME_SELFTEST_HEAD_MUTATED")
if changed != ["agent_runtime_selftest.txt"]:
raise RuntimeError("RUNTIME_SELFTEST_UNEXPECTED_DIFF")
print(json.dumps({
"status": "PASS",
"adapter": ADAPTER,
"base_oid": base_oid,
"changed_files": changed,
"head_unchanged": True,
"runtime_summary": result.summary,
"task_api_token_visible_to_runtime": False,
"writer_lease_visible_to_runtime": False,
}, sort_keys=True))
return 0
finally:
shutil.rmtree(run_root, ignore_errors=True)
if __name__ == "__main__":
raise SystemExit(main())
@@ -0,0 +1,54 @@
# Generic Governed Online Agent Worker
This worker is a pull-based, restartable consumer for `aios.agent-task.v1`.
The service configuration contains only stable runtime identity, capability
names, API endpoints, heartbeat timing, and the credential projection. It does
not contain a task id, repository base OID, target branch, or plan-file path.
## Dynamic task contract
Engineering tasks carry `metadata.execution` with schema
`aios.online-agent.execution.v2`. The payload supplies the repository, exact
base OID, target branch, detached-worktree policy, adapter name, explicit file
change set, verification commands, and governed commit idempotency keys.
Task prose is descriptive only and is never passed to a shell.
The worker accepts only structured operations:
- `deterministic`: allow-listed file changes and test commands;
- `codex`: fail-closed until a task supplies an explicit Codex file/command
policy;
- `hermes`: fail-closed because the current Hermes runtime is a user-chat
service, not an engineering executor.
All command execution uses `shell=False`, fixed program allowlists, relative
paths beneath an ephemeral detached worktree, and output redaction. Direct
`git branch`, `update-ref`, `push`, `commit`, `reset`, `clean`, main,
production, tag, and baseline mutations are rejected by the worker.
## Governed write path
The worker calls the Bridge in this order:
1. `POST /v2/governed-task`;
2. `POST /v2/writer-lease`;
3. `POST /v2/commit-files`;
4. read-only authority-ref visibility verification.
CREATE_NEW is represented by the task payload and is normalized by the Bridge
lease authority. The worker never creates an authoritative ref itself.
## Lifecycle and recovery
The runtime registers with `register_or_recover`, receives its agent token
once, and stores only the encrypted systemd credential. The consumer loops over
poll, atomically claims one task, sends the first heartbeat, emits
`TASK_STARTED`, and continues heartbeats while executing.
State is atomically stored per task without agent or lease token material. If
the process stops after a governed commit but before result submission, the
next lease attempt reuses the stable governed idempotency key and submits the
stored commit evidence without issuing a second commit.
The control plane must independently verify submitted results; worker-local
success is not production or mainline authorization.
@@ -0,0 +1,3 @@
"""Generic governed Online Agent Worker runtime."""
__all__ = ["worker"]
@@ -0,0 +1,937 @@
#!/usr/bin/env python3
"""Generic, restartable Online Agent Worker.
The worker is deliberately task-agnostic. A task's signed/durable payload is
the only source of repository, branch, change, verification, and governance
parameters. There is no task id, base OID, branch name, or plan file in the
runtime configuration.
The worker performs local work only in an ephemeral detached worktree. All
authoritative ref updates go through the governed Bridge API; direct branch,
update-ref, push, main, production, tag, and baseline mutations are rejected.
"""
from __future__ import annotations
import base64
from dataclasses import dataclass
from datetime import datetime, timezone
from abc import ABC, abstractmethod
import hashlib
import json
import os
from pathlib import Path
import re
import shutil
import subprocess
import tempfile
import threading
import time
import urllib.error
import urllib.request
from typing import Any, Mapping
from urllib.parse import unquote
API_BASE = os.environ.get("AIOS_AGENT_TASK_API_URL", "http://127.0.0.1:18940").rstrip("/")
BRIDGE_BASE = os.environ.get("AIOS_GOVERNED_WRITE_API_URL", "http://127.0.0.1:18892").rstrip("/")
AGENT_ID = os.environ.get(
"AIOS_AGENT_ID", "online-engineering-worker-cn-network-mkt-generic-20260815"
)
AGENT_TYPE = os.environ.get("AIOS_AGENT_TYPE", "trusted-online-engineering-worker")
AGENT_VERSION = os.environ.get("AIOS_AGENT_VERSION", "2026-08-15.3-generic")
AUTHORITY_GIT_DIR = Path(
os.environ.get("AIOS_AUTHORITY_GIT_DIR", "/data/aios-gitops/repo.git")
)
STATE_ROOT = Path(
os.environ.get("AIOS_WORKER_STATE_ROOT", "/var/lib/aios-online-agent-worker-20260815")
)
WORKSPACE_ROOT = Path(
os.environ.get(
"AIOS_WORKER_WORKSPACE_ROOT", "/var/lib/aios-online-agent-worker-20260815/workspaces"
)
)
GIT_CONFIG_GLOBAL = STATE_ROOT / "gitconfig"
POLL_SECONDS = int(os.environ.get("AIOS_WORKER_POLL_SECONDS", "5"))
HEARTBEAT_SECONDS = int(os.environ.get("AIOS_WORKER_HEARTBEAT_SECONDS", "20"))
LEASE_SECONDS = int(os.environ.get("AIOS_WORKER_LEASE_SECONDS", "300"))
MAX_TASK_BYTES = 512 * 1024
MAX_FILE_BYTES = 2 * 1024 * 1024
OID_RE = re.compile(r"^[0-9a-f]{40}$")
SHA256_RE = re.compile(r"^[0-9a-f]{64}$")
ID_RE = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._:-]{2,179}$")
BRANCH_RE = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._/-]*$")
PATH_RE = re.compile(r"^[^\\\x00-\x1f\x7f]+$")
SENSITIVE_RE = re.compile(
r"(?i)(authorization\s*:\s*bearer\s+\S+|(?:password|passwd|secret|token|api[_-]?key)\s*[:=]\s*\S+)"
)
class WorkerError(RuntimeError):
def __init__(self, code: str, *, phase: str, details: Mapping[str, Any] | None = None) -> None:
super().__init__(code)
self.code = code
self.phase = phase
self.details = dict(details or {})
def now() -> str:
return datetime.now(timezone.utc).isoformat()
def digest_bytes(value: bytes) -> str:
return hashlib.sha256(value).hexdigest()
def digest_json(value: Any) -> str:
return digest_bytes(
json.dumps(value, ensure_ascii=False, sort_keys=True, separators=(",", ":")).encode("utf-8")
)
def sanitize(value: Any) -> Any:
if isinstance(value, Mapping):
cleaned: dict[str, Any] = {}
for key, inner in value.items():
lowered = str(key).lower()
if any(marker in lowered for marker in ("token", "secret", "credential", "password", "authorization")):
cleaned[str(key)] = "[REDACTED]"
else:
cleaned[str(key)] = sanitize(inner)
return cleaned
if isinstance(value, list):
return [sanitize(item) for item in value]
if isinstance(value, tuple):
return [sanitize(item) for item in value]
if isinstance(value, str):
return SENSITIVE_RE.sub("[REDACTED]", value)
return value
def excerpt(value: str, limit: int = 800) -> str:
value = str(sanitize(value))
return value if len(value) <= limit else value[: limit // 2] + "...[TRUNCATED]..." + value[-limit // 2 :]
def audit(event_type: str, **fields: Any) -> None:
STATE_ROOT.mkdir(mode=0o750, parents=True, exist_ok=True)
path = STATE_ROOT / "events.jsonl"
record = {
"event_type": event_type,
"created_at": now(),
"agent_id": AGENT_ID,
"credential_material_exposed": False,
"lease_material_exposed": False,
**sanitize(fields),
}
with path.open("a", encoding="utf-8") as handle:
handle.write(json.dumps(record, ensure_ascii=False, sort_keys=True) + "\n")
os.chmod(path, 0o640)
print(json.dumps(record, ensure_ascii=False, sort_keys=True), flush=True)
def atomic_json(path: Path, value: Mapping[str, Any], mode: int = 0o640) -> None:
path.parent.mkdir(mode=0o750, parents=True, exist_ok=True)
if path.parent.is_symlink() or path.is_symlink():
raise WorkerError("STATE_PATH_SYMLINK_REJECTED", phase="state")
fd, temporary = tempfile.mkstemp(prefix=f".{path.name}.", dir=str(path.parent))
try:
os.fchmod(fd, mode)
with os.fdopen(fd, "w", encoding="utf-8") as handle:
json.dump(sanitize(dict(value)), handle, ensure_ascii=False, sort_keys=True)
handle.write("\n")
handle.flush()
os.fsync(handle.fileno())
os.replace(temporary, path)
os.chmod(path, mode)
finally:
if os.path.exists(temporary):
os.unlink(temporary)
def safe_relative_path(value: Any, *, field: str = "path") -> str:
if not isinstance(value, str) or not value or len(value) > 240:
raise WorkerError(f"{field.upper()}_INVALID", phase="validation")
if unquote(value) != value or not PATH_RE.fullmatch(value):
raise WorkerError(f"{field.upper()}_INVALID", phase="validation")
if value.startswith(("/", "~")) or re.match(r"^[A-Za-z]:", value):
raise WorkerError(f"{field.upper()}_ABSOLUTE", phase="validation")
parts = value.replace("\\", "/").split("/")
if any(part in {"", ".", ".."} for part in parts):
raise WorkerError(f"{field.upper()}_TRAVERSAL", phase="validation")
return "/".join(parts)
def resolve_under(root: Path, relative: str, *, write: bool = False) -> Path:
relative = safe_relative_path(relative)
root = root.resolve()
candidate = (root / relative).resolve(strict=False)
if candidate != root and root not in candidate.parents:
raise WorkerError("WORKSPACE_ESCAPE", phase="validation")
current = root
for part in Path(relative).parts:
current /= part
if current.exists() and current.is_symlink():
raise WorkerError("WORKSPACE_SYMLINK_REJECTED", phase="validation")
if write and candidate.exists() and candidate.is_symlink():
raise WorkerError("WRITE_SYMLINK_REJECTED", phase="validation")
return candidate
def validate_oid(value: Any, field: str = "oid") -> str:
if not isinstance(value, str) or not OID_RE.fullmatch(value):
raise WorkerError(f"{field.upper()}_INVALID", phase="validation")
return value
def validate_sha(value: Any, field: str = "sha256") -> str:
if not isinstance(value, str) or not SHA256_RE.fullmatch(value):
raise WorkerError(f"{field.upper()}_INVALID", phase="validation")
return value
def validate_branch(value: Any) -> str:
if not isinstance(value, str) or not 3 <= len(value) <= 180:
raise WorkerError("TARGET_BRANCH_INVALID", phase="validation")
if value in {"main", "master", "HEAD"} or value.startswith(
("main/", "master/", "refs/", "tags/", "tag/", "release/", "releases/", "baseline/", "production/")
):
raise WorkerError("PROTECTED_BRANCH_REJECTED", phase="validation")
if (
value.startswith("/")
or value.endswith(("/", ".", ".lock"))
or ".." in value
or "//" in value
or "@{" in value
or any(ch in value for ch in " ~^:?*[\\")
or any(part in {"", ".", ".."} for part in value.split("/"))
or not BRANCH_RE.fullmatch(value)
):
raise WorkerError("TARGET_BRANCH_INVALID", phase="validation")
return value
def validate_program(argv: Any, *, internal_git: bool = False) -> list[str]:
if not isinstance(argv, list) or not argv or not all(isinstance(item, str) and item for item in argv):
raise WorkerError("STRUCTURED_ARGV_REQUIRED", phase="validation")
command = argv[0]
if command not in {"/usr/bin/git", "/usr/bin/python3"}:
raise WorkerError("PROGRAM_NOT_ALLOWLISTED", phase="validation", details={"program": Path(command).name})
if command.endswith("python3"):
if any(item in {"-c", "-", "--command"} for item in argv):
raise WorkerError("PYTHON_INLINE_CODE_REJECTED", phase="validation")
if "-m" in argv:
index = argv.index("-m")
if index + 1 >= len(argv) or argv[index + 1] not in {"pytest", "py_compile"}:
raise WorkerError("PYTHON_MODULE_NOT_ALLOWLISTED", phase="validation")
if command.endswith("git") and not internal_git:
rejected = {"push", "update-ref", "receive-pack", "send-pack", "reset", "clean", "branch", "commit", "merge", "rebase"}
if any(item in rejected for item in argv[1:]):
raise WorkerError("GIT_MUTATION_NOT_ALLOWLISTED", phase="validation")
return list(argv)
def run_command(
label: str,
argv: list[str],
*,
cwd: Path,
timeout: int,
internal_git: bool = False,
allowed_returncodes: tuple[int, ...] = (0,),
) -> dict[str, Any]:
validate_program(argv, internal_git=internal_git)
if not 1 <= timeout <= 3600:
raise WorkerError("COMMAND_TIMEOUT_INVALID", phase=label)
started = time.monotonic()
try:
completed = subprocess.run(
argv,
cwd=str(cwd),
text=True,
capture_output=True,
check=False,
shell=False,
timeout=timeout,
env={"PATH": "/usr/bin:/bin", "LC_ALL": "C.UTF-8", "GIT_CONFIG_GLOBAL": str(GIT_CONFIG_GLOBAL)},
)
except subprocess.TimeoutExpired as exc:
raise WorkerError("COMMAND_TIMEOUT", phase=label, details={"timeout_seconds": timeout}) from exc
result = {
"label": label,
"argv": [Path(item).name if index == 0 else item for index, item in enumerate(argv)],
"exit_code": completed.returncode,
"stdout_sha256": digest_bytes(completed.stdout.encode("utf-8", "replace")),
"stderr_sha256": digest_bytes(completed.stderr.encode("utf-8", "replace")),
"stdout_excerpt": excerpt(completed.stdout),
"stderr_excerpt": excerpt(completed.stderr),
"duration_ms": int((time.monotonic() - started) * 1000),
}
audit("COMMAND_RESULT", **result)
if completed.returncode not in allowed_returncodes:
raise WorkerError("COMMAND_FAILED", phase=label, details={"exit_code": completed.returncode, "stderr": excerpt(completed.stderr, 400)})
return result
@dataclass(frozen=True)
class ExecutionEnvelope:
task_id: str
repository: str
base_oid: str
target_branch: str
adapter: str
change_set: tuple[dict[str, Any], ...]
verification: tuple[dict[str, Any], ...]
governance: dict[str, Any]
workspace: dict[str, Any]
digest: str
@classmethod
def from_task(cls, task: Mapping[str, Any]) -> "ExecutionEnvelope":
task_id = str(task.get("task_id") or "")
if not ID_RE.fullmatch(task_id):
raise WorkerError("TASK_ID_INVALID", phase="validation")
metadata = task.get("metadata")
if not isinstance(metadata, Mapping):
raise WorkerError("TASK_METADATA_REQUIRED", phase="validation")
execution = metadata.get("execution")
if not isinstance(execution, Mapping):
raise WorkerError("DYNAMIC_EXECUTION_PAYLOAD_REQUIRED", phase="validation")
if execution.get("schema") != "aios.online-agent.execution.v2":
raise WorkerError("EXECUTION_SCHEMA_INVALID", phase="validation")
repository = execution.get("repository")
if not isinstance(repository, str) or not repository:
raise WorkerError("REPOSITORY_INVALID", phase="validation")
base_oid = validate_oid(execution.get("base_oid"), "base_oid")
target_branch = validate_branch(execution.get("target_branch"))
workspace = execution.get("workspace")
if not isinstance(workspace, Mapping):
raise WorkerError("WORKSPACE_POLICY_REQUIRED", phase="validation")
workspace_value = dict(workspace)
if workspace_value.get("authority_git_dir") != str(AUTHORITY_GIT_DIR):
raise WorkerError("AUTHORITY_ROOT_INVALID", phase="validation")
if workspace_value.get("read_only_repository") is not True:
raise WorkerError("REPOSITORY_WRITE_POLICY_INVALID", phase="validation")
if workspace_value.get("cleanup_policy") not in {"always", "cleanup_on_success"}:
raise WorkerError("WORKSPACE_CLEANUP_POLICY_INVALID", phase="validation")
adapter_value = execution.get("adapter")
if isinstance(adapter_value, Mapping):
adapter = str(adapter_value.get("name") or "")
else:
adapter = str(adapter_value or "")
if adapter not in {"deterministic", "codex", "hermes"}:
raise WorkerError("RUNTIME_ADAPTER_UNSUPPORTED", phase="validation")
change_set = execution.get("change_set")
if not isinstance(change_set, list) or not change_set or len(change_set) > 128:
raise WorkerError("CHANGE_SET_INVALID", phase="validation")
normalized_changes: list[dict[str, Any]] = []
for item in change_set:
if not isinstance(item, Mapping):
raise WorkerError("CHANGE_OPERATION_INVALID", phase="validation")
operation = str(item.get("operation") or "")
relative = safe_relative_path(item.get("path"))
normalized = dict(item)
normalized["operation"] = operation
normalized["path"] = relative
if operation == "write_file":
content = item.get("content")
if not isinstance(content, str) or len(content.encode("utf-8")) > MAX_FILE_BYTES:
raise WorkerError("WRITE_CONTENT_INVALID", phase="validation")
elif operation == "replace_text":
if not isinstance(item.get("old"), str) or not item.get("old") or not isinstance(item.get("new"), str):
raise WorkerError("REPLACE_TEXT_INVALID", phase="validation")
if len(str(item["old"]).encode("utf-8")) > MAX_FILE_BYTES:
raise WorkerError("REPLACE_TEXT_INVALID", phase="validation")
elif operation == "delete_file":
pass
else:
raise WorkerError("CHANGE_OPERATION_UNSUPPORTED", phase="validation", details={"operation": operation})
expected = item.get("expected_sha256")
if expected is not None:
validate_sha(expected, "expected_sha256")
normalized_changes.append(normalized)
verification = execution.get("verification") or []
if not isinstance(verification, list) or len(verification) > 128:
raise WorkerError("VERIFICATION_INVALID", phase="validation")
normalized_verification: list[dict[str, Any]] = []
for item in verification:
if not isinstance(item, Mapping):
raise WorkerError("VERIFICATION_COMMAND_INVALID", phase="validation")
label = str(item.get("label") or "verification")
argv = validate_program(item.get("argv"))
cwd_value = item.get("cwd")
cwd = "." if cwd_value in {None, "", "."} else safe_relative_path(cwd_value, field="cwd")
timeout = item.get("timeout_seconds", 120)
if not isinstance(timeout, int) or isinstance(timeout, bool) or not 1 <= timeout <= 3600:
raise WorkerError("VERIFICATION_TIMEOUT_INVALID", phase="validation")
normalized_verification.append({"label": label, "argv": argv, "cwd": cwd, "timeout_seconds": timeout})
governance = execution.get("governance")
if not isinstance(governance, Mapping):
raise WorkerError("GOVERNANCE_POLICY_REQUIRED", phase="validation")
governance_value = dict(governance)
governed_task_id = str(governance_value.get("task_id") or f"{task_id}-writer")
if not ID_RE.fullmatch(governed_task_id):
raise WorkerError("GOVERNED_TASK_ID_INVALID", phase="validation")
idempotency_key = str(governance_value.get("idempotency_key") or f"{task_id}-governed-v1")
commit_key = str(governance_value.get("commit_idempotency_key") or f"{task_id}-commit-v1")
for field, value in (("idempotency_key", idempotency_key), ("commit_idempotency_key", commit_key)):
if not re.fullmatch(r"[A-Za-z0-9][A-Za-z0-9._:-]{7,159}", value):
raise WorkerError(f"{field.upper()}_INVALID", phase="validation")
governance_value.update(
{
"task_id": governed_task_id,
"idempotency_key": idempotency_key,
"commit_idempotency_key": commit_key,
"message": str(governance_value.get("message") or f"chore(gitops): execute task {task_id}"),
}
)
for field in ("main_write", "force_push", "production_write", "tag_write", "baseline_write", "protected_ref_bypass"):
if governance_value.get(field, False) is not False:
raise WorkerError("GOVERNANCE_POLICY_REJECTED", phase="validation", details={"field": field})
allowed_files = governance_value.get("allowed_files")
if allowed_files is not None:
if not isinstance(allowed_files, list):
raise WorkerError("ALLOWED_FILES_INVALID", phase="validation")
allowed = {safe_relative_path(item, field="allowed_file") for item in allowed_files}
if not {item["path"] for item in normalized_changes}.issubset(allowed):
raise WorkerError("CHANGE_OUTSIDE_ALLOWED_FILES", phase="validation")
governance_value["allowed_files"] = sorted(allowed)
return cls(
task_id=task_id,
repository=repository,
base_oid=base_oid,
target_branch=target_branch,
adapter=adapter,
change_set=tuple(normalized_changes),
verification=tuple(normalized_verification),
governance=governance_value,
workspace=workspace_value,
digest=digest_json(execution),
)
class RuntimeAdapter(ABC):
name: str
@abstractmethod
def execute(self, envelope: ExecutionEnvelope, worktree: Path) -> dict[str, Any]:
raise NotImplementedError
class DeterministicExecutor(RuntimeAdapter):
name = "deterministic"
@staticmethod
def _write_atomic(path: Path, content: str) -> None:
path.parent.mkdir(mode=0o750, parents=True, exist_ok=True)
fd, temporary = tempfile.mkstemp(prefix=f".{path.name}.", dir=str(path.parent))
try:
with os.fdopen(fd, "w", encoding="utf-8") as handle:
handle.write(content)
handle.flush()
os.fsync(handle.fileno())
os.replace(temporary, path)
finally:
if os.path.exists(temporary):
os.unlink(temporary)
def execute(self, envelope: ExecutionEnvelope, worktree: Path) -> dict[str, Any]:
changed: set[str] = set()
for change in envelope.change_set:
relative = str(change["path"])
path = resolve_under(worktree, relative, write=True)
expected = change.get("expected_sha256")
current = path.read_bytes() if path.is_file() else None
if expected is not None and (current is None or digest_bytes(current) != expected):
raise WorkerError("WORKTREE_SOURCE_DRIFT", phase="implementing", details={"path": relative})
operation = change["operation"]
if operation == "write_file":
self._write_atomic(path, str(change["content"]))
elif operation == "replace_text":
if current is None:
raise WorkerError("REPLACE_SOURCE_MISSING", phase="implementing", details={"path": relative})
original = current.decode("utf-8")
old = str(change["old"])
count = original.count(old)
expected_count = int(change.get("count", 1))
if count != expected_count:
raise WorkerError("REPLACE_TEXT_MATCH_COUNT_INVALID", phase="implementing", details={"path": relative, "count": count})
self._write_atomic(path, original.replace(old, str(change["new"])))
elif operation == "delete_file":
if path.exists():
path.unlink()
changed.add(relative)
audit("REPOSITORY_CHANGE_APPLIED", operation=operation, path=relative, sha256=digest_bytes(path.read_bytes()) if path.exists() else None)
commands: list[dict[str, Any]] = []
if changed:
commands.append(run_command("git_intent_to_add", ["/usr/bin/git", "-C", str(worktree), "add", "-N", "--", *sorted(changed)], cwd=worktree, timeout=120, internal_git=True))
commands.append(run_command("git_diff_check", ["/usr/bin/git", "-C", str(worktree), "diff", "--check"], cwd=worktree, timeout=120))
commands.append(run_command("git_diff_stat", ["/usr/bin/git", "-C", str(worktree), "diff", "--no-ext-diff", "--stat"], cwd=worktree, timeout=120))
tests: list[dict[str, Any]] = []
for item in envelope.verification:
cwd = worktree if item["cwd"] == "." else resolve_under(worktree, item["cwd"])
tests.append(run_command(item["label"], item["argv"], cwd=cwd, timeout=item["timeout_seconds"]))
return {
"changed_files": sorted(changed),
"commands": commands,
"tests": tests,
"adapter": self.name,
}
class CodexExecutor(RuntimeAdapter):
name = "codex"
def execute(self, envelope: ExecutionEnvelope, worktree: Path) -> dict[str, Any]:
raise WorkerError(
"CODEX_ADAPTER_REQUIRES_EXPLICIT_POLICY",
phase="adapter",
details={"reason": "Codex is not allowed to mutate a governed worktree without an explicit file/command policy"},
)
class HermesExecutor(RuntimeAdapter):
name = "hermes"
def execute(self, envelope: ExecutionEnvelope, worktree: Path) -> dict[str, Any]:
raise WorkerError(
"HERMES_ENGINEERING_ADAPTER_UNAVAILABLE",
phase="adapter",
details={"reason": "The live Hermes service is a user-chat runtime, not an engineering executor"},
)
def adapter_for(name: str) -> RuntimeAdapter:
return {"deterministic": DeterministicExecutor, "codex": CodexExecutor, "hermes": HermesExecutor}[name]()
class HttpClient:
@staticmethod
def call(base: str, method: str, route: str, token: str | None, body: Mapping[str, Any] | None = None, timeout: int = 30) -> tuple[int, dict[str, Any]]:
payload = None if body is None else json.dumps(body, ensure_ascii=False, sort_keys=True, separators=(",", ":")).encode("utf-8")
headers = {"Accept": "application/json"}
if token:
headers["Authorization"] = "Bearer " + token
if payload is not None:
headers["Content-Type"] = "application/json"
request = urllib.request.Request(base + route, data=payload, headers=headers, method=method)
try:
with urllib.request.urlopen(request, timeout=timeout) as response:
value = json.loads(response.read().decode("utf-8"))
return response.status, value if isinstance(value, dict) else {}
except urllib.error.HTTPError as exc:
raw = exc.read(8192).decode("utf-8", "replace")
try:
value = json.loads(raw)
except json.JSONDecodeError:
value = {"error": "HTTP_ERROR"}
return exc.code, value if isinstance(value, dict) else {"error": "HTTP_ERROR"}
except (urllib.error.URLError, TimeoutError):
return 599, {"error": "SERVICE_UNAVAILABLE"}
class AgentTaskClient:
def __init__(self, token: str) -> None:
self.token = token
def poll(self) -> list[dict[str, Any]]:
status, response = HttpClient.call(API_BASE, "POST", "/v1/tasks/poll", self.token, {"limit": 100})
if status != 200:
raise WorkerError("TASK_POLL_FAILED", phase="poll", details={"api_status": status, "error": response.get("error")})
tasks = response.get("tasks")
return [dict(item) for item in tasks if isinstance(item, Mapping)] if isinstance(tasks, list) else []
def claim(self, task_id: str) -> tuple[int, dict[str, Any]]:
return HttpClient.call(API_BASE, "POST", f"/v1/tasks/{task_id}/claim", self.token, {"lease_seconds": LEASE_SECONDS})
def heartbeat(self, task_id: str, lease_token: str, stage: str) -> tuple[int, dict[str, Any]]:
return HttpClient.call(
API_BASE,
"POST",
f"/v1/tasks/{task_id}/heartbeat",
self.token,
{"lease_token": lease_token, "lease_seconds": LEASE_SECONDS, "progress": {"stage": stage, "worker_version": AGENT_VERSION}},
)
def result(self, task_id: str, lease_token: str, idempotency_key: str, result: Mapping[str, Any]) -> tuple[int, dict[str, Any]]:
return HttpClient.call(
API_BASE,
"POST",
f"/v1/tasks/{task_id}/result",
self.token,
{"lease_token": lease_token, "idempotency_key": idempotency_key, "result": dict(result)},
)
class BridgeClient:
def call(self, route: str, body: Mapping[str, Any]) -> tuple[int, dict[str, Any]]:
return HttpClient.call(BRIDGE_BASE, "POST", route, None, body, timeout=60)
class LeaseHeartbeat:
def __init__(self, client: AgentTaskClient, task_id: str, lease_token: str) -> None:
self.client = client
self.task_id = task_id
self.lease_token = lease_token
self.stage = "claimed"
self.failed: str | None = None
self.stop_event = threading.Event()
self.lock = threading.Lock()
self.thread = threading.Thread(target=self._run, name="online-agent-heartbeat", daemon=True)
def first(self) -> None:
status, response = self.client.heartbeat(self.task_id, self.lease_token, self.stage)
if not 200 <= status < 300:
self.failed = str(response.get("error") or status)
raise WorkerError("TASK_HEARTBEAT_FAILED", phase="heartbeat", details={"api_status": status, "error": self.failed})
audit("TASK_HEARTBEAT", task_id=self.task_id, stage=self.stage, api_status=status)
def start(self) -> None:
self.thread.start()
def set_stage(self, stage: str) -> None:
with self.lock:
self.stage = stage
def _run(self) -> None:
while not self.stop_event.wait(HEARTBEAT_SECONDS):
with self.lock:
stage = self.stage
try:
status, response = self.client.heartbeat(self.task_id, self.lease_token, stage)
if not 200 <= status < 300:
self.failed = str(response.get("error") or status)
audit("TASK_HEARTBEAT_FAILED", task_id=self.task_id, stage=stage, api_status=status, error=self.failed)
else:
audit("TASK_HEARTBEAT", task_id=self.task_id, stage=stage, api_status=status)
except Exception as exc: # pragma: no cover - defensive runtime guard
self.failed = type(exc).__name__
audit("TASK_HEARTBEAT_FAILED", task_id=self.task_id, stage=stage, error=self.failed)
def ensure(self) -> None:
if self.failed:
raise WorkerError("TASK_LEASE_LOST", phase="heartbeat", details={"error": self.failed})
def close(self) -> None:
self.stop_event.set()
if self.thread.ident is not None:
self.thread.join(timeout=2)
def load_agent_token() -> str:
directory = Path(os.environ.get("CREDENTIALS_DIRECTORY", ""))
path = directory / "agent_token"
if not path.is_file() or path.is_symlink():
raise WorkerError("AGENT_RUNTIME_CREDENTIAL_UNAVAILABLE", phase="startup")
value = path.read_text(encoding="utf-8").strip()
if len(value) < 24:
raise WorkerError("AGENT_RUNTIME_CREDENTIAL_INVALID", phase="startup")
return value
def ensure_roots() -> None:
if AUTHORITY_GIT_DIR.is_symlink() or STATE_ROOT.is_symlink() or WORKSPACE_ROOT.is_symlink():
raise WorkerError("ROOT_SYMLINK_REJECTED", phase="startup")
if not AUTHORITY_GIT_DIR.is_dir():
raise WorkerError("AUTHORITY_GIT_DIR_UNAVAILABLE", phase="startup")
STATE_ROOT.mkdir(mode=0o750, parents=True, exist_ok=True)
WORKSPACE_ROOT.mkdir(mode=0o750, parents=True, exist_ok=True)
if GIT_CONFIG_GLOBAL.is_symlink():
raise WorkerError("GIT_CONFIG_SYMLINK_REJECTED", phase="startup")
expected = f"[safe]\n\tdirectory = {AUTHORITY_GIT_DIR}\n"
if GIT_CONFIG_GLOBAL.exists() and GIT_CONFIG_GLOBAL.read_text(encoding="utf-8") != expected:
raise WorkerError("GIT_CONFIG_BINDING_CONFLICT", phase="startup")
if not GIT_CONFIG_GLOBAL.exists():
GIT_CONFIG_GLOBAL.write_text(expected, encoding="utf-8")
os.chmod(GIT_CONFIG_GLOBAL, 0o640)
def task_state_path(task_id: str) -> Path:
if not ID_RE.fullmatch(task_id):
raise WorkerError("TASK_ID_INVALID", phase="state")
return STATE_ROOT / "tasks" / f"{task_id}.json"
def load_task_state(task_id: str, execution_digest: str) -> dict[str, Any] | None:
path = task_state_path(task_id)
if not path.is_file() or path.is_symlink():
return None
try:
value = json.loads(path.read_text(encoding="utf-8"))
except Exception:
raise WorkerError("TASK_STATE_INVALID", phase="state")
if not isinstance(value, dict):
raise WorkerError("TASK_STATE_INVALID", phase="state")
if value.get("execution_digest") not in {None, execution_digest}:
raise WorkerError("TASK_PAYLOAD_DRIFT", phase="recovery")
return value
def save_task_state(task_id: str, value: Mapping[str, Any]) -> None:
atomic_json(task_state_path(task_id), value)
def new_run_root() -> Path:
ensure_roots()
root = Path(tempfile.mkdtemp(prefix="task-", dir=str(WORKSPACE_ROOT)))
os.chmod(root, 0o750)
return root
def cleanup_run_root(path: Path) -> None:
root = WORKSPACE_ROOT.resolve()
candidate = path.resolve(strict=False)
if candidate == root or root not in candidate.parents:
raise WorkerError("CLEANUP_SCOPE_INVALID", phase="cleanup")
if candidate.exists() or candidate.is_symlink():
shutil.rmtree(candidate)
def prepare_worktree(run_root: Path, base_oid: str) -> Path:
clone = run_root / "repository"
worktree = run_root / "worktree"
run_command("workspace_clone", ["/usr/bin/git", "clone", "--no-local", "--no-hardlinks", str(AUTHORITY_GIT_DIR), str(clone)], cwd=run_root, timeout=300, internal_git=True)
run_command("workspace_base_probe", ["/usr/bin/git", "-C", str(clone), "rev-parse", "--verify", base_oid + "^{commit}"], cwd=run_root, timeout=60)
run_command("workspace_worktree_create", ["/usr/bin/git", "-C", str(clone), "worktree", "add", "--detach", str(worktree), base_oid], cwd=run_root, timeout=300, internal_git=True)
return worktree
def governed_commit(envelope: ExecutionEnvelope, worktree: Path, bridge: BridgeClient, heartbeat: LeaseHeartbeat) -> dict[str, Any]:
governance = envelope.governance
create_body = {
"task_id": governance["task_id"],
"repository": envelope.repository,
"base_oid": envelope.base_oid,
"target_branch": envelope.target_branch,
"risk": str(governance.get("risk") or "engineering"),
"description": str(governance.get("description") or f"Governed execution for {envelope.task_id}"),
"idempotency_key": governance["idempotency_key"],
}
status, response = bridge.call("/v2/governed-task", create_body)
if not 200 <= status < 300:
raise WorkerError("GOVERNED_TASK_CREATE_FAILED", phase="committing", details={"api_status": status, "error": response.get("error")})
generation = int(response.get("generation") or 0)
expected_branch_oid = str(response.get("expected_branch_oid") or envelope.base_oid)
branch_mode = str(response.get("branch_mode") or "CREATE_NEW")
lease_body = {
"schema": "aios.task-writer-lease.v2",
"task_id": governance["task_id"],
"repository": envelope.repository,
"branch_mode": branch_mode,
"base_oid": envelope.base_oid,
"expected_branch_oid": expected_branch_oid,
"target_branch": envelope.target_branch,
"generation": generation,
"lease_seconds": int(governance.get("lease_seconds") or 900),
}
status, lease_response = bridge.call("/v2/writer-lease", lease_body)
if not 200 <= status < 300:
raise WorkerError("WRITER_LEASE_ACQUIRE_FAILED", phase="committing", details={"api_status": status, "error": lease_response.get("error")})
heartbeat.ensure()
files: dict[str, str] = {}
paths = governance.get("allowed_files") or [item["path"] for item in envelope.change_set]
for relative in sorted({safe_relative_path(item, field="allowed_file") for item in paths}):
path = resolve_under(worktree, relative)
if not path.is_file():
raise WorkerError("COMMIT_FILE_MISSING", phase="committing", details={"path": relative})
files[relative] = base64.b64encode(path.read_bytes()).decode("ascii")
commit_body = {
"task_id": governance["task_id"],
"repository": envelope.repository,
"expected_branch_oid": expected_branch_oid,
"target_branch": envelope.target_branch,
"generation": generation,
"idempotency_key": governance["commit_idempotency_key"],
"files": files,
"message": governance["message"],
}
status, commit_response = bridge.call("/v2/commit-files", commit_body)
if not 200 <= status < 300:
raise WorkerError("GOVERNED_COMMIT_FAILED", phase="committing", details={"api_status": status, "error": commit_response.get("error")})
commit_oid = commit_response.get("commit_oid")
if not isinstance(commit_oid, str) or not OID_RE.fullmatch(commit_oid):
raise WorkerError("GOVERNED_COMMIT_OID_INVALID", phase="committing")
visible_oid: str | None = None
for _ in range(5):
probe = subprocess.run(
["/usr/bin/git", "--git-dir", str(AUTHORITY_GIT_DIR), "rev-parse", "--verify", f"refs/heads/{envelope.target_branch}"],
text=True,
capture_output=True,
check=False,
env={"PATH": "/usr/bin:/bin", "LC_ALL": "C.UTF-8", "GIT_CONFIG_GLOBAL": str(GIT_CONFIG_GLOBAL)},
timeout=30,
)
if probe.returncode == 0:
visible_oid = probe.stdout.strip()
if visible_oid == commit_oid:
break
time.sleep(1)
if visible_oid != commit_oid:
raise WorkerError("GIT_VISIBILITY_MISMATCH", phase="verification", details={"commit_oid": commit_oid, "visible_oid": visible_oid})
return {
"governed_task_id": governance["task_id"],
"branch_mode": branch_mode,
"generation": generation,
"target_branch": envelope.target_branch,
"commit_oid": commit_oid,
"visible_oid": visible_oid,
"commit_idempotency_key": governance["commit_idempotency_key"],
"api_commit_visible": commit_response.get("commit_visible"),
"direct_main_write": False,
"force_push": False,
"production_write": False,
"protected_ref_bypass": False,
}
def build_result(task_id: str, envelope: ExecutionEnvelope, execution: Mapping[str, Any], governed: Mapping[str, Any]) -> dict[str, Any]:
return {
"status": "SUCCEEDED",
"verification_state": "VERIFIED_BY_WORKER_PENDING_SERVER_REVIEW",
"task_id": task_id,
"execution_backend": "generic-online-agent-worker-v2",
"adapter": envelope.adapter,
"execution_digest": envelope.digest,
"repository": envelope.repository,
"base_oid": envelope.base_oid,
"target_branch": envelope.target_branch,
"changed_files": execution.get("changed_files", []),
"commands": [{k: item.get(k) for k in ("label", "exit_code", "stdout_sha256", "stderr_sha256", "duration_ms")} for item in execution.get("commands", [])],
"tests": [{k: item.get(k) for k in ("label", "exit_code", "stdout_sha256", "stderr_sha256", "duration_ms")} for item in execution.get("tests", [])],
"governed": dict(governed),
"workspace_cleanup": "PASS",
"secret_material_exposed": False,
"direct_main_write": False,
"force_push": False,
"production_write": False,
"protected_ref_bypass": False,
}
class WorkerSupervisor:
def __init__(self, token: str) -> None:
self.agent = AgentTaskClient(token)
self.bridge = BridgeClient()
def process_claim(self, task: Mapping[str, Any], claim: Mapping[str, Any]) -> None:
task_id = str(claim.get("task_id") or task.get("task_id") or "")
lease_token = claim.get("lease_token")
if not isinstance(lease_token, str) or len(lease_token) < 24:
raise WorkerError("LEASE_TOKEN_NOT_RETURNED", phase="claim")
audit("TASK_CLAIMED", task_id=task_id, attempt=claim.get("attempt"), revision=claim.get("revision"))
heartbeat = LeaseHeartbeat(self.agent, task_id, lease_token)
try:
heartbeat.first()
heartbeat.set_stage("started")
heartbeat.start()
audit("TASK_STARTED", task_id=task_id, attempt=claim.get("attempt"))
envelope = ExecutionEnvelope.from_task(task)
state = load_task_state(task_id, envelope.digest) or {"task_id": task_id, "execution_digest": envelope.digest, "phase": "claimed"}
governed = state.get("governed") if isinstance(state.get("governed"), Mapping) else None
if isinstance(governed, Mapping) and isinstance(governed.get("commit_oid"), str) and OID_RE.fullmatch(governed["commit_oid"]):
result = state.get("result")
if not isinstance(result, Mapping):
result = {"status": "SUCCEEDED", "verification_state": "VERIFIED_BY_WORKER_PENDING_SERVER_REVIEW", "task_id": task_id, "execution_digest": envelope.digest, "governed": dict(governed), "recovered_without_duplicate_commit": True, "direct_main_write": False, "production_write": False, "force_push": False, "protected_ref_bypass": False}
audit("TASK_RECOVERED_COMMIT", task_id=task_id, commit_oid=governed["commit_oid"], duplicate_commit=False)
else:
run_root = new_run_root()
try:
worktree = prepare_worktree(run_root, envelope.base_oid)
state.update({"phase": "workspace_prepared", "execution_digest": envelope.digest, "attempt": claim.get("attempt")})
save_task_state(task_id, state)
adapter = adapter_for(envelope.adapter)
execution = adapter.execute(envelope, worktree)
state.update({"phase": "verification_passed", "execution": execution})
save_task_state(task_id, state)
heartbeat.ensure()
heartbeat.set_stage("committing")
governed = governed_commit(envelope, worktree, self.bridge, heartbeat)
result = build_result(task_id, envelope, execution, governed)
state.update({"phase": "commit_done", "governed": governed, "result": result})
save_task_state(task_id, state)
finally:
cleanup_run_root(run_root)
audit("WORKSPACE_CLEANED", task_id=task_id, cleanup="PASS")
heartbeat.ensure()
heartbeat.set_stage("result")
result_key = str(result.get("governed", {}).get("commit_idempotency_key") or f"{task_id}-result-{envelope.digest[:16]}")
status, response = self.agent.result(task_id, lease_token, result_key, result)
if not 200 <= status < 300:
raise WorkerError("TASK_RESULT_SUBMIT_FAILED", phase="result", details={"api_status": status, "error": response.get("error")})
audit("TASK_RESULT_SUBMITTED", task_id=task_id, api_status=status, result_idempotency_key=result_key, commit_oid=result.get("governed", {}).get("commit_oid"))
except WorkerError as exc:
audit("TASK_FAILED", task_id=task_id, error=exc.code, phase=exc.phase, details=exc.details)
failure_key = f"{task_id}-attempt-{claim.get('attempt', 0)}-failure"
failure = {
"status": "FAILED",
"verification_state": "UNVERIFIED",
"task_id": task_id,
"execution_backend": "generic-online-agent-worker-v2",
"error": exc.code,
"phase": exc.phase,
"details": exc.details,
"direct_main_write": False,
"force_push": False,
"production_write": False,
"protected_ref_bypass": False,
"secret_material_exposed": False,
}
try:
status, response = self.agent.result(task_id, lease_token, failure_key, failure)
audit("TASK_FAILURE_RESULT_SUBMITTED", task_id=task_id, api_status=status, error=response.get("error"))
except Exception as submit_exc: # pragma: no cover - defensive runtime guard
audit("TASK_FAILURE_RESULT_SUBMIT_FAILED", task_id=task_id, error=type(submit_exc).__name__)
finally:
heartbeat.close()
def once(self) -> int:
try:
tasks = self.agent.poll()
audit("TASK_POLL", visible_count=len(tasks))
except WorkerError as exc:
audit("TASK_POLL_FAILED", error=exc.code, phase=exc.phase, details=exc.details)
return 10
dynamic_tasks: list[dict[str, Any]] = []
for task in tasks:
metadata = task.get("metadata")
execution = metadata.get("execution") if isinstance(metadata, Mapping) else None
if not isinstance(execution, Mapping) or execution.get("schema") != "aios.online-agent.execution.v2":
audit("TASK_SKIPPED_NON_DYNAMIC", task_id=task.get("task_id"), reason="DYNAMIC_EXECUTION_PAYLOAD_REQUIRED")
continue
dynamic_tasks.append(task)
for task in dynamic_tasks:
task_id = str(task.get("task_id") or "")
status, claim = self.agent.claim(task_id)
if status == 409:
audit("TASK_CLAIM_SKIPPED", task_id=task_id, api_status=status, error=claim.get("error"))
continue
if status != 200:
audit("TASK_CLAIM_FAILED", task_id=task_id, api_status=status, error=claim.get("error"))
continue
self.process_claim(task, claim)
return 0
return 0
def run_forever(self) -> int:
ensure_roots()
audit("WORKER_STARTED", task_agnostic=True, api_base=API_BASE, bridge_base=BRIDGE_BASE, adapter_names=["deterministic", "codex", "hermes"], runtime_only_credentials=True)
while True:
self.once()
time.sleep(POLL_SECONDS)
def main() -> int:
try:
token = load_agent_token()
supervisor = WorkerSupervisor(token)
if os.environ.get("AIOS_WORKER_ONCE") == "1":
ensure_roots()
return supervisor.once()
return supervisor.run_forever()
except WorkerError as exc:
audit("WORKER_START_FAILED", error=exc.code, phase=exc.phase, details=exc.details)
return 20
if __name__ == "__main__":
raise SystemExit(main())
@@ -0,0 +1,157 @@
#!/usr/bin/env python3
"""Register or recover the generic Online Agent using runtime-only secrets."""
from __future__ import annotations
import json
import os
from pathlib import Path
import subprocess
import tempfile
import urllib.error
import urllib.request
from datetime import datetime, timezone
from typing import Any
BASE = os.environ.get("AIOS_AGENT_TASK_API_URL", "http://127.0.0.1:18940").rstrip("/")
AGENT_ID = os.environ.get("AIOS_AGENT_ID", "online-engineering-worker-cn-network-mkt-generic-20260815")
AGENT_TYPE = os.environ.get("AIOS_AGENT_TYPE", "trusted-online-engineering-worker")
AGENT_VERSION = os.environ.get("AIOS_AGENT_VERSION", "2026-08-15.3-generic")
PROJECTION_PROVIDER = "BridgeV2RuntimeCredentialProjection"
STATE_DIR = Path(os.environ.get("AIOS_WORKER_REGISTRATION_STATE", "/var/lib/aios-online-agent-worker-20260815/registration"))
CREDENTIAL_PATH = Path(os.environ.get("AIOS_WORKER_ENCRYPTED_CREDENTIAL", "/etc/credstore.encrypted/aios-online-agent-worker-20260815/agent_token"))
IDENTITY_PATH = STATE_DIR / "identity.json"
AUDIT_PATH = STATE_DIR / "events.jsonl"
CAPABILITIES = [
"online_agent_task_api", "task.poll", "task.claim", "task.heartbeat", "task.result",
"engineering.consumer", "engineering.execute", "workspace.worktree", "git.read", "git.diff",
"git.test", "governed.write.branch", "governed.write.file",
]
def now() -> str:
return datetime.now(timezone.utc).isoformat()
def audit(event_type: str, **fields: Any) -> None:
STATE_DIR.mkdir(mode=0o750, parents=True, exist_ok=True)
record = {"event_type": event_type, "created_at": now(), "agent_id": AGENT_ID, "credential_material_exposed": False, **fields}
with AUDIT_PATH.open("a", encoding="utf-8") as handle:
handle.write(json.dumps(record, ensure_ascii=False, sort_keys=True) + "\n")
os.chmod(AUDIT_PATH, 0o640)
print(json.dumps(record, ensure_ascii=False, sort_keys=True), flush=True)
def api_call(method: str, route: str, token: str, body: dict[str, Any] | None = None) -> tuple[int, dict[str, Any]]:
payload = None if body is None else json.dumps(body, ensure_ascii=False, sort_keys=True, separators=(",", ":")).encode("utf-8")
headers = {"Authorization": "Bearer " + token, "Accept": "application/json"}
if payload is not None:
headers["Content-Type"] = "application/json"
request = urllib.request.Request(BASE + route, data=payload, headers=headers, method=method)
try:
with urllib.request.urlopen(request, timeout=15) as response:
value = json.loads(response.read().decode("utf-8"))
return response.status, value if isinstance(value, dict) else {}
except urllib.error.HTTPError as exc:
try:
value = json.loads(exc.read(2048).decode("utf-8"))
except Exception:
value = {"error": "HTTP_ERROR"}
return exc.code, value if isinstance(value, dict) else {"error": "HTTP_ERROR"}
except (urllib.error.URLError, TimeoutError):
return 599, {"error": "AGENT_TASK_API_UNAVAILABLE"}
def read_identity() -> dict[str, Any] | None:
if not IDENTITY_PATH.is_file() or IDENTITY_PATH.is_symlink():
return None
try:
value = json.loads(IDENTITY_PATH.read_text(encoding="utf-8"))
except Exception:
return None
return value if isinstance(value, dict) else None
def identity_matches(value: dict[str, Any] | None) -> bool:
return bool(value and value.get("agent_id") == AGENT_ID and value.get("agent_type") == AGENT_TYPE and value.get("agent_version") == AGENT_VERSION and value.get("credential_projection_provider") == PROJECTION_PROVIDER and value.get("task_scope") == "dynamic")
def encrypted_credential_is_valid() -> bool:
if not CREDENTIAL_PATH.is_file() or CREDENTIAL_PATH.is_symlink():
return False
result = subprocess.run(["systemd-creds", "decrypt", "--with-key=host", str(CREDENTIAL_PATH), "/dev/null"], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, check=False, timeout=15)
return result.returncode == 0
def write_identity() -> None:
value = {"agent_id": AGENT_ID, "agent_type": AGENT_TYPE, "agent_version": AGENT_VERSION, "task_scope": "dynamic", "credential_projection_provider": PROJECTION_PROVIDER, "credential_delivery": "systemd-LoadCredentialEncrypted", "task_api_url": BASE, "task_api_schema": "aios.agent-task.v1", "execution_backend": "generic-online-agent-worker-v2", "runtime_only_secret_delivery": True, "production_write": False, "main_write": False, "force_push": False, "tag_write": False, "baseline_write": False, "writer_lease": True, "created_at": now()}
STATE_DIR.mkdir(mode=0o750, parents=True, exist_ok=True)
fd, temporary = tempfile.mkstemp(prefix="identity.", dir=str(STATE_DIR))
try:
with os.fdopen(fd, "w", encoding="utf-8") as handle:
json.dump(value, handle, ensure_ascii=False, sort_keys=True)
handle.write("\n")
os.chmod(temporary, 0o640)
os.replace(temporary, IDENTITY_PATH)
finally:
if os.path.exists(temporary):
os.unlink(temporary)
def encrypt_agent_token(agent_token: str) -> None:
CREDENTIAL_PATH.parent.mkdir(mode=0o700, parents=True, exist_ok=True)
fd, temporary = tempfile.mkstemp(prefix=".online-agent-token.", dir=str(CREDENTIAL_PATH.parent))
os.close(fd)
os.unlink(temporary)
try:
result = subprocess.run(["systemd-creds", "encrypt", "--with-key=host", "--name=agent_token", "/dev/stdin", temporary], input=(agent_token + "\n").encode("utf-8"), stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, check=False, timeout=20)
if result.returncode != 0:
raise RuntimeError("AGENT_CREDENTIAL_ENCRYPT_FAILED")
os.chmod(temporary, 0o600)
os.replace(temporary, CREDENTIAL_PATH)
os.chmod(CREDENTIAL_PATH, 0o600)
finally:
if os.path.exists(temporary):
os.unlink(temporary)
def register_or_recover() -> str:
STATE_DIR.mkdir(mode=0o750, parents=True, exist_ok=True)
existing = read_identity()
if CREDENTIAL_PATH.exists():
if not identity_matches(existing):
raise RuntimeError("AGENT_IDENTITY_BINDING_CONFLICT")
if not encrypted_credential_is_valid():
raise RuntimeError("AGENT_ENCRYPTED_CREDENTIAL_INVALID")
audit("AGENT_RECOVERED", credential_delivery="systemd-LoadCredentialEncrypted", credential_projection_provider=PROJECTION_PROVIDER, register_or_recover=True)
return "RECOVERED"
if existing is not None:
raise RuntimeError("AGENT_CREDENTIAL_MISSING_AFTER_PRIOR_REGISTRATION")
credential_directory = Path(os.environ.get("CREDENTIALS_DIRECTORY", ""))
control_path = credential_directory / "bridge_control_token"
if not control_path.is_file():
raise RuntimeError("BRIDGE_RUNTIME_CONTROL_CREDENTIAL_UNAVAILABLE")
control_token = control_path.read_text(encoding="utf-8").strip()
if len(control_token) < 32:
raise RuntimeError("BRIDGE_RUNTIME_CONTROL_CREDENTIAL_INVALID")
status, response = api_call("POST", "/v1/agents/register", control_token, {"agent_id": AGENT_ID, "agent_type": AGENT_TYPE, "agent_version": AGENT_VERSION, "capabilities": CAPABILITIES, "metadata": {"task_scope": "dynamic", "credential_projection_provider": PROJECTION_PROVIDER, "runtime_only_secret_delivery": True, "task_api_schema": "aios.agent-task.v1", "execution_backend": "generic-online-agent-worker-v2", "production_write": False, "main_write": False, "force_push": False, "tag_write": False, "baseline_write": False, "writer_lease": True}})
if status == 409 and response.get("error") == "AGENT_ALREADY_EXISTS":
raise RuntimeError("AGENT_ALREADY_EXISTS_RECOVERY_CREDENTIAL_UNAVAILABLE")
if status != 201:
raise RuntimeError("AGENT_REGISTER_FAILED_" + str(response.get("error") or status))
agent_token = response.get("agent_token")
if not isinstance(agent_token, str) or len(agent_token) < 24:
raise RuntimeError("AGENT_TOKEN_NOT_RETURNED")
encrypt_agent_token(agent_token)
write_identity()
audit("AGENT_REGISTERED", credential_delivery="systemd-LoadCredentialEncrypted", credential_projection_provider=PROJECTION_PROVIDER, register_or_recover=True)
return "REGISTERED"
if __name__ == "__main__":
try:
raise SystemExit(0 if register_or_recover() in {"REGISTERED", "RECOVERED"} else 20)
except Exception as exc:
audit("AGENT_REGISTRATION_FAILED", error=type(exc).__name__)
raise SystemExit(20)
@@ -0,0 +1,79 @@
from __future__ import annotations
import os
from pathlib import Path
from aios_online_agent_worker.worker import ExecutionEnvelope, WorkerError, sanitize # noqa: E402
BASE = "a" * 40
def payload(task_id: str, branch: str, *, base_oid: str = BASE) -> dict[str, object]:
return {
"task_id": task_id,
"metadata": {
"execution": {
"schema": "aios.online-agent.execution.v2",
"repository": "aios/aios",
"base_oid": base_oid,
"target_branch": branch,
"adapter": "deterministic",
"workspace": {
"authority_git_dir": str(Path(os.environ.get("AIOS_AUTHORITY_GIT_DIR", "/data/aios-gitops/repo.git"))),
"read_only_repository": True,
"cleanup_policy": "always",
},
"change_set": [
{
"operation": "write_file",
"path": "tests/generated_marker.py",
"content": "def test_marker():\n assert True\n",
"expected_sha256": None,
}
],
"verification": [
{"label": "compile", "argv": ["/usr/bin/python3", "-m", "py_compile", "tests/generated_marker.py"], "cwd": ".", "timeout_seconds": 60}
],
"governance": {
"task_id": f"{task_id}-writer",
"idempotency_key": f"{task_id}-governed-v1",
"commit_idempotency_key": f"{task_id}-commit-v1",
"allowed_files": ["tests/generated_marker.py"],
"main_write": False,
"force_push": False,
"production_write": False,
"tag_write": False,
"baseline_write": False,
"protected_ref_bypass": False,
},
}
},
}
def test_dynamic_payload_is_not_bound_to_task_or_base() -> None:
first = ExecutionEnvelope.from_task(payload("task-alpha-20260815", "agent/alpha-20260815"))
second = ExecutionEnvelope.from_task(payload("task-beta-20260815", "agent/beta-20260815", base_oid="b" * 40))
assert first.task_id != second.task_id
assert first.base_oid != second.base_oid
assert first.target_branch != second.target_branch
def test_protected_branch_is_rejected() -> None:
try:
ExecutionEnvelope.from_task(payload("task-main-20260815", "main"))
except WorkerError as exc:
assert exc.code == "PROTECTED_BRANCH_REJECTED"
else:
raise AssertionError("protected branch accepted")
def test_task_payload_does_not_persist_secret_values() -> None:
value = sanitize({"agent_token": "agt-secret", "nested": {"authorization": "Bearer secret"}})
assert value == {"agent_token": "[REDACTED]", "nested": {"authorization": "[REDACTED]"}}
def test_execution_digest_is_stable() -> None:
value = payload("task-digest-20260815", "agent/digest-20260815")
assert ExecutionEnvelope.from_task(value).digest == ExecutionEnvelope.from_task(value).digest
@@ -1,9 +1,8 @@
from __future__ import annotations
import hashlib,json,re,tarfile
from release_version import RELEASE_VERSION_RE
from pathlib import Path
from typing import Any
OID_RE=re.compile(r"^[0-9a-f]{40}$"); SHA_RE=re.compile(r"^[0-9a-f]{64}$"); VERSION_RE=RELEASE_VERSION_RE
OID_RE=re.compile(r"^[0-9a-f]{40}$"); SHA_RE=re.compile(r"^[0-9a-f]{64}$"); VERSION_RE=re.compile(r"^V[0-9]+R[0-9]+C[0-9]+(?:-[A-Za-z0-9_.-]+)?$")
REQUIRED_SOURCE={"schema","version","repository","base_ref","base_oid","binding_mode","release_readiness","production_deployed","tag_created","baseline_changed","production_service","production_port","rollback_metadata","controller_schema","required_checks"}
REQUEST_KEYS={"schema","version","repository","base_oid","target_oid","artifact_uri","artifact_sha256","artifact_size","evidence_closure_oid","approval_plan_id","execute","production_deployed","production_service","production_port","rollback_metadata","current_tag_oid","source_manifest_sha256"}
class ManifestValidationError(ValueError):
-1
View File
@@ -1 +0,0 @@
action-contract-integration-a
-1
View File
@@ -1 +0,0 @@
action-contract-integration-b
File diff suppressed because one or more lines are too long
-116
View File
@@ -1,116 +0,0 @@
from __future__ import annotations
import json
import sys
from pathlib import Path
import pytest
ROOT = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(ROOT))
from aios_chatgpt_bridge_v2 import ( # noqa: E402
OPERATION_REQUIRED_FIELDS,
operation_contracts,
render_openapi_contract_fragment,
)
from aios_chatgpt_bridge_v2_server import GovernedRuntime # noqa: E402
from aios_gitops_job_controller import JobState # noqa: E402
from gitea_tag_publication import validate_gitea_api_url # noqa: E402
from release_version import validate_release_version # noqa: E402
def test_canonical_release_version_is_shared_and_accepts_v2r2c1():
assert validate_release_version("V2R2C1") == "V2R2C1"
assert validate_release_version("V2R2C1-repair") == "V2R2C1-repair"
with pytest.raises(ValueError):
validate_release_version("v2r2c1")
def test_gitea_url_validation_is_startup_safe():
assert validate_gitea_api_url("https://g.cnnetmkt.top/git/api/v1") == (
"https://g.cnnetmkt.top/git/api/v1"
)
with pytest.raises(ValueError, match="HTTP"):
validate_gitea_api_url("g.cnnetmkt.top/git/api/v1")
def test_commit_files_runtime_and_published_schema_have_same_required_fields():
runtime = operation_contracts()["commitFilesGoverned"]["required_fields"]
assert runtime == list(OPERATION_REQUIRED_FIELDS["commitFilesGoverned"])
fragment = render_openapi_contract_fragment()
published = (ROOT / "docs/gitops/openapi-chatgpt-actions.yaml").read_text()
assert fragment in published
for field in runtime:
assert f" - {field}" in fragment
assert "required: [path, content_base64]" in fragment
class _Store:
def __init__(self, row):
self.row = row
self.receipt_calls = 0
def read(self, _job_id):
return self.row
def _receipt(self, row, duplicate=False):
self.receipt_calls += 1
return type("Receipt", (), {"as_dict": lambda _self: {
"job_id": row["job_id"],
"state": row["state"],
"duplicate": duplicate,
}})()
class _Controller:
def __init__(self, row):
self.store = _Store(row)
def _runtime_for(row):
runtime = object.__new__(GovernedRuntime)
controller = _Controller(row)
runtime._controller_for_job = lambda _job_id: controller
runtime._executor_from_row = lambda *_args: (_ for _ in ()).throw(
AssertionError("terminal reconcile must not reconstruct an executor")
)
return runtime, controller
@pytest.mark.parametrize(
"state",
[
JobState.COMPLETED.value,
JobState.FAILED.value,
JobState.CANCELLED.value,
JobState.MANUAL_OWNER_REVIEW_REQUIRED.value,
],
)
def test_terminal_reconcile_is_idempotent_without_durable_binding(state):
row = {
"job_id": "job-terminal",
"state": state,
"request_json": json.dumps({}),
}
runtime, controller = _runtime_for(row)
result = GovernedRuntime.job(runtime, {"job_id": row["job_id"]}, "reconcile")
assert result["duplicate"] is True
assert controller.store.receipt_calls == 1
def test_active_reconcile_still_uses_executor():
row = {
"job_id": "job-active",
"state": JobState.RECONCILIATION_REQUIRED.value,
"request_json": json.dumps({}),
}
runtime, controller = _runtime_for(row)
class Executor:
def reconcile(self, job_id):
return {"job_id": job_id, "state": "COMPLETED"}
runtime._executor_from_row = lambda *_args: Executor()
assert GovernedRuntime.job(runtime, {"job_id": row["job_id"]}, "reconcile") == {
"job_id": "job-active",
"state": "COMPLETED",
}
+201
View File
@@ -0,0 +1,201 @@
from __future__ import annotations
import importlib.util
import json
import os
from pathlib import Path
import socket
import subprocess
import sys
import time
import pytest
BROKER_PATH = Path(__file__).parents[1] / "services" / "aios-agent-runtime-broker" / "broker.py"
def load_broker():
spec = importlib.util.spec_from_file_location("broker_under_test", BROKER_PATH)
module = importlib.util.module_from_spec(spec)
assert spec.loader is not None
spec.loader.exec_module(module)
return module
def test_peer_credentials_accept_configured_uid(monkeypatch) -> None:
broker = load_broker()
left, right = socket.socketpair()
try:
monkeypatch.setattr(broker, "ALLOWED_PEER_UID", os.getuid())
monkeypatch.setattr(broker, "ALLOWED_PEER_USER", "")
broker.authorize_peer(left)
finally:
left.close()
right.close()
def test_peer_credentials_reject_other_uid(monkeypatch) -> None:
broker = load_broker()
left, right = socket.socketpair()
try:
monkeypatch.setattr(broker, "ALLOWED_PEER_UID", os.getuid() + 100000)
monkeypatch.setattr(broker, "ALLOWED_PEER_USER", "")
with pytest.raises(broker.BrokerError) as caught:
broker.authorize_peer(left)
assert caught.value.code == "PEER_UID_REJECTED"
finally:
left.close()
right.close()
def test_request_read_timeout(monkeypatch) -> None:
broker = load_broker()
left, right = socket.socketpair()
try:
monkeypatch.setattr(broker, "CONNECTION_TIMEOUT", 0.05)
with pytest.raises(broker.BrokerError) as caught:
broker.read_request(left)
assert caught.value.code == "REQUEST_TIMEOUT"
finally:
left.close()
right.close()
def test_worktree_fd_remains_pinned_after_path_replacement(tmp_path: Path, monkeypatch) -> None:
broker = load_broker()
root = tmp_path / "workspaces"
run_root = root / "run"
original = run_root / "worktree"
original.mkdir(parents=True)
(original / "marker.txt").write_text("original\n", encoding="utf-8")
request = {
"schema": "aios.online-agent.runtime-request.v1",
"adapter": "hermes",
"execution_context": {
"repository": "aios/aios",
"base_oid": "a" * 40,
"target_branch": "agent/intent/test",
"worktree": str(original),
},
"policy": {
"authoritative_repository_write": False,
"direct_git_ref_mutation": False,
"direct_git_push": False,
"direct_git_commit": False,
"main_write": False,
"force_push": False,
"production_write": False,
"tag_write": False,
"baseline_write": False,
"task_api_credential_visible": False,
"writer_lease_visible": False,
},
}
monkeypatch.setattr(broker, "ADAPTER", "hermes")
monkeypatch.setattr(broker, "WORKSPACE_ROOT", root)
_, fd = broker.validate_request(request)
try:
moved = run_root / "original-moved"
original.rename(moved)
original.mkdir()
(original / "marker.txt").write_text("replacement\n", encoding="utf-8")
pinned = Path(f"/proc/self/fd/{fd}")
(pinned / "through-fd.txt").write_text("pinned\n", encoding="utf-8")
assert (moved / "through-fd.txt").read_text(encoding="utf-8") == "pinned\n"
assert not (original / "through-fd.txt").exists()
finally:
os.close(fd)
def test_backend_receives_no_task_api_token(tmp_path: Path, monkeypatch) -> None:
broker = load_broker()
worktree = tmp_path / "worktree"
worktree.mkdir()
backend = tmp_path / "backend.py"
backend.write_text(
"""#!/usr/bin/env python3
import json, os, pathlib, sys
request = json.load(sys.stdin)
assert 'AIOS_AGENT_TASK_TOKEN' not in os.environ
worktree = pathlib.Path(request['execution_context']['worktree'])
(worktree / 'changed.txt').write_text('ok\\n', encoding='utf-8')
json.dump({
'schema':'aios.online-agent.runtime-result.v1',
'status':'COMPLETED',
'summary':'done',
'verification':[],
'evidence':{'token_visible':False, 'api_key':'runtime-secret-value'}
}, sys.stdout)
""",
encoding="utf-8",
)
backend.chmod(0o755)
monkeypatch.setattr(broker, "ADAPTER", "hermes")
monkeypatch.setattr(broker, "BACKEND_ARGV_JSON", json.dumps(["/usr/bin/python3", str(backend)]))
monkeypatch.setattr(broker, "BACKEND_TIMEOUT", 5)
monkeypatch.delenv("CREDENTIALS_DIRECTORY", raising=False)
fd = os.open(str(worktree), getattr(os, "O_PATH", os.O_RDONLY) | os.O_DIRECTORY)
try:
request = {
"schema": "aios.online-agent.runtime-request.v1",
"adapter": "hermes",
"execution_context": {"worktree": str(worktree)},
}
result = broker.execute_backend(request, fd)
finally:
os.close(fd)
assert result["status"] == "COMPLETED"
assert result["evidence"]["api_key"] == "[REDACTED]"
assert (worktree / "changed.txt").read_text(encoding="utf-8") == "ok\n"
def test_broker_backend_env_does_not_inherit_task_token(tmp_path: Path, monkeypatch) -> None:
broker = load_broker()
worktree = tmp_path / "worktree-env"
worktree.mkdir()
backend = tmp_path / "backend-env.py"
backend.write_text(
"""#!/usr/bin/env python3
import json, os, sys
assert 'AIOS_AGENT_TASK_TOKEN' not in os.environ
assert os.environ.get('AIOS_OPENCLAW_MODEL') == 'test/model'
assert os.environ.get('AIOS_OPENCLAW_BIN') == '/test/openclaw'
json.dump({
'schema':'aios.online-agent.runtime-result.v1',
'status':'COMPLETED',
'summary':'environment isolated',
'verification':[],
'evidence':{'task_token_visible':False}
}, sys.stdout)
""",
encoding="utf-8",
)
backend.chmod(0o755)
monkeypatch.setattr(broker, "ADAPTER", "openclaw")
monkeypatch.setattr(
broker,
"BACKEND_ARGV_JSON",
json.dumps(["/usr/bin/python3", str(backend)]),
)
monkeypatch.setenv("AIOS_AGENT_TASK_TOKEN", "must-not-cross")
monkeypatch.setenv("AIOS_OPENCLAW_MODEL", "test/model")
monkeypatch.setenv("AIOS_OPENCLAW_BIN", "/test/openclaw")
monkeypatch.delenv("CREDENTIALS_DIRECTORY", raising=False)
fd = os.open(str(worktree), getattr(os, "O_PATH", os.O_RDONLY) | os.O_DIRECTORY)
try:
request = {
"schema": "aios.online-agent.runtime-request.v1",
"adapter": "openclaw",
"execution_context": {"worktree": str(worktree)},
}
result = broker.execute_backend(request, fd)
finally:
os.close(fd)
assert result["status"] == "COMPLETED"
+143
View File
@@ -0,0 +1,143 @@
from __future__ import annotations
from pathlib import Path
import subprocess
import pytest
from intent_runtime import IntentError, IntentTask, is_claimable_intent, resolve
def make_bare(tmp_path: Path) -> tuple[Path, str]:
work = tmp_path / "work"
bare = tmp_path / "repo.git"
subprocess.run(["git", "init", str(work)], check=True, capture_output=True)
subprocess.run(["git", "-C", str(work), "config", "user.email", "test@example.invalid"], check=True)
subprocess.run(["git", "-C", str(work), "config", "user.name", "AIOS Test"], check=True)
(work / "README.md").write_text("base\n", encoding="utf-8")
subprocess.run(["git", "-C", str(work), "add", "README.md"], check=True)
subprocess.run(["git", "-C", str(work), "commit", "-m", "base"], check=True, capture_output=True)
subprocess.run(["git", "-C", str(work), "branch", "-M", "main"], check=True)
oid = subprocess.check_output(["git", "-C", str(work), "rev-parse", "HEAD"], text=True).strip()
subprocess.run(["git", "clone", "--bare", str(work), str(bare)], check=True, capture_output=True)
return bare, oid
def intent_task(task_id: str = "custom-gpt-intent-20260815") -> dict[str, object]:
return {
"task_id": task_id,
"goal": "Fix the repository behavior described by the task.",
"instructions": "Inspect the code, find the root cause, implement and test the smallest correct fix.",
"success_criteria": ["Relevant tests pass", "git diff --check passes"],
"required_capabilities": [
"intent.task",
"engineering.execute",
"engineering.autonomous",
"git.read",
"git.diff",
"git.test",
"governed.write.file",
"governed.write.branch",
"workspace.worktree",
"agent.runtime.hermes",
],
"metadata": {},
}
def fixed_implementation_task() -> dict[str, object]:
return {
"task_id": "implement-autonomous-intent-worker-v2-20260815",
"goal": "Implement autonomous intent worker v2.",
"instructions": "Implement the worker.",
"success_criteria": ["Implementation succeeds"],
"required_capabilities": [
"engineering.execute",
"git.diff",
"git.read",
"git.test",
"governed.write.branch",
"governed.write.file",
"workspace.worktree",
],
"metadata": {},
}
def test_fixed_bootstrap_task_is_not_claimable_by_intent_worker() -> None:
worker_caps = intent_task()["required_capabilities"]
assert is_claimable_intent(fixed_implementation_task(), worker_caps) is False
with pytest.raises(IntentError) as caught:
IntentTask.parse(fixed_implementation_task())
assert caught.value.code == "INTENT_CAPABILITY_MARKER_REQUIRED"
def test_native_custom_gpt_intent_task_is_claimable() -> None:
task = intent_task()
assert is_claimable_intent(task, task["required_capabilities"]) is True
def test_missing_runtime_capability_fails_closed() -> None:
task = intent_task()
caps = [item for item in task["required_capabilities"] if item != "agent.runtime.hermes"]
assert is_claimable_intent(task, caps) is False
def test_resolution_is_stable_across_restart(tmp_path: Path) -> None:
bare, oid = make_bare(tmp_path)
task = intent_task()
first = resolve(
task,
authority_git_dir=bare,
repository="aios/aios",
base_ref="refs/heads/main",
available_adapters=["hermes"],
default_adapter="hermes",
)
recovered = resolve(
task,
authority_git_dir=bare,
repository="aios/aios",
base_ref="refs/heads/main",
available_adapters=["hermes"],
default_adapter="hermes",
prior=first.to_json(),
)
assert first == recovered
assert recovered.base_oid == oid
assert recovered.target_branch.startswith("agent/intent/")
def test_requested_codex_is_rejected_when_unavailable(tmp_path: Path) -> None:
bare, _ = make_bare(tmp_path)
task = intent_task("codex-only-20260815")
task["required_capabilities"] = [
item for item in task["required_capabilities"] if item != "agent.runtime.hermes"
] + ["agent.runtime.codex"]
with pytest.raises(IntentError) as caught:
resolve(
task,
authority_git_dir=bare,
repository="aios/aios",
base_ref="refs/heads/main",
available_adapters=["hermes"],
default_adapter="hermes",
)
assert caught.value.code == "AGENT_RUNTIME_UNAVAILABLE"
def test_openclaw_is_a_first_class_runtime_adapter(tmp_path: Path) -> None:
bare, _ = make_bare(tmp_path)
task = intent_task("openclaw-intent-20260815")
task["required_capabilities"] = [
item for item in task["required_capabilities"] if item != "agent.runtime.hermes"
] + ["agent.runtime.openclaw"]
resolved = resolve(
task,
authority_git_dir=bare,
repository="aios/aios",
base_ref="refs/heads/main",
available_adapters=["openclaw"],
default_adapter="openclaw",
)
assert resolved.adapter == "openclaw"
+102
View File
@@ -0,0 +1,102 @@
from __future__ import annotations
import json
from pathlib import Path
import socket
import subprocess
import threading
import time
import pytest
from runtime_client import RuntimeErrorV2, collect_changes, invoke, request_payload, scan_secrets
def repo(tmp_path: Path) -> tuple[Path, str]:
work = tmp_path / "repo"
subprocess.run(["git", "init", str(work)], check=True, capture_output=True)
subprocess.run(["git", "-C", str(work), "config", "user.email", "test@example.invalid"], check=True)
subprocess.run(["git", "-C", str(work), "config", "user.name", "AIOS Test"], check=True)
(work / "a.py").write_text("VALUE = 1\n", encoding="utf-8")
subprocess.run(["git", "-C", str(work), "add", "a.py"], check=True)
subprocess.run(["git", "-C", str(work), "commit", "-m", "base"], check=True, capture_output=True)
oid = subprocess.check_output(["git", "-C", str(work), "rev-parse", "HEAD"], text=True).strip()
return work, oid
def test_collect_changes_reads_actual_git_state(tmp_path: Path) -> None:
work, oid = repo(tmp_path)
(work / "a.py").write_text("VALUE = 2\n", encoding="utf-8")
(work / "new.py").write_text("NEW = True\n", encoding="utf-8")
assert collect_changes(work, oid) == ["a.py", "new.py"]
def test_agent_commit_is_rejected(tmp_path: Path) -> None:
work, oid = repo(tmp_path)
(work / "a.py").write_text("VALUE = 2\n", encoding="utf-8")
subprocess.run(["git", "-C", str(work), "add", "a.py"], check=True)
subprocess.run(["git", "-C", str(work), "commit", "-m", "agent commit"], check=True, capture_output=True)
with pytest.raises(RuntimeErrorV2) as caught:
collect_changes(work, oid)
assert caught.value.code == "WORKTREE_HEAD_MUTATED"
def test_secret_material_is_rejected(tmp_path: Path) -> None:
work, oid = repo(tmp_path)
private_key_marker = "-----BEGIN PRIVATE " + "KEY-----"
(work / "new.txt").write_text(
private_key_marker + "\nabc\n", encoding="utf-8")
changed = collect_changes(work, oid)
with pytest.raises(RuntimeErrorV2) as caught:
scan_secrets(work, changed)
assert caught.value.code == "SECRET_MATERIAL_REJECTED"
def test_runtime_protocol_round_trip(tmp_path: Path) -> None:
sock = tmp_path / "runtime.sock"
response = {
"schema": "aios.online-agent.runtime-result.v1",
"status": "COMPLETED",
"summary": "Implemented the requested change.",
"verification": [],
"evidence": {"runtime": "fake-hermes"},
}
def server() -> None:
listener = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
listener.bind(str(sock))
listener.listen(1)
conn, _ = listener.accept()
raw = b""
while True:
chunk = conn.recv(65536)
if not chunk:
break
raw += chunk
value = json.loads(raw.decode("utf-8"))
assert value["schema"] == "aios.online-agent.runtime-request.v1"
conn.sendall(json.dumps(response).encode("utf-8"))
conn.close()
listener.close()
thread = threading.Thread(target=server)
thread.start()
for _ in range(100):
if sock.exists():
break
time.sleep(0.01)
payload = request_payload(
adapter="hermes",
task_id="intent-e2e-20260815",
goal="Change VALUE",
instructions="Inspect and implement",
success_criteria=["Tests pass"],
repository="aios/aios",
base_oid="a" * 40,
target_branch="agent/intent/example",
worktree=tmp_path,
)
result = invoke(sock, payload, 5)
thread.join(timeout=2)
assert result.status == "COMPLETED"
assert result.evidence["runtime"] == "fake-hermes"
@@ -0,0 +1,156 @@
from __future__ import annotations
import importlib.util
import json
import os
from pathlib import Path
import subprocess
import sys
import pytest
BACKEND_PATH = Path(__file__).parents[1] / "services" / "aios-agent-runtime-broker" / "openclaw_engineering_backend.py"
def load_backend():
spec = importlib.util.spec_from_file_location("openclaw_backend_under_test", BACKEND_PATH)
module = importlib.util.module_from_spec(spec)
assert spec.loader is not None
spec.loader.exec_module(module)
return module
def fake_openclaw(tmp_path: Path) -> Path:
path = tmp_path / "openclaw"
path.write_text(
"""#!/usr/bin/env python3
import json, os, pathlib, sys
args = sys.argv[1:]
assert args[:2] == ['agent', 'exec']
def value(flag):
return args[args.index(flag) + 1]
assert value('--message-file') == '-'
cwd = pathlib.Path(value('--cwd'))
assert cwd.resolve() == pathlib.Path.cwd().resolve()
assert '--auth-env-only' in args
assert '--json' in args
assert os.environ.get('OPENCLAW_EXEC_SHELL_SNAPSHOT') == '0'
assert 'AIOS_AGENT_TASK_TOKEN' not in os.environ
prompt = sys.stdin.read()
assert 'Do NOT run `git commit`' in prompt
(cwd / 'changed_by_openclaw.txt').write_text('changed\\n', encoding='utf-8')
json.dump({
'ok': True,
'status': 'ok',
'final': 'Changed the requested file and verified the result.',
'payloads': [{'text':'done'}],
'model': value('--model'),
'provider': 'test-provider',
'sessionId': 'test-session',
'assistantTurns': 2,
'codeModeEngaged': True,
'toolSummary': {'calls': 3, 'tools': ['read','write','exec'], 'totalToolTimeMs': 10}
}, sys.stdout)
""",
encoding="utf-8",
)
path.chmod(0o755)
return path
def request(worktree: Path) -> dict[str, object]:
return {
"schema": "aios.online-agent.runtime-request.v1",
"adapter": "hermes",
"task": {
"task_id": "openclaw-backend-selftest",
"goal": "Make the requested change.",
"instructions": "Inspect, implement, and test.",
"success_criteria": ["The change is correct"],
},
"execution_context": {
"repository": "aios/aios",
"base_oid": "a" * 40,
"target_branch": "agent/intent/openclaw-backend-selftest",
"worktree": str(worktree),
},
"policy": {
"authoritative_repository_write": False,
"direct_git_ref_mutation": False,
"direct_git_push": False,
"direct_git_commit": False,
"main_write": False,
"force_push": False,
"production_write": False,
"tag_write": False,
"baseline_write": False,
"task_api_credential_visible": False,
"writer_lease_visible": False,
},
}
def test_openclaw_agent_exec_contract(tmp_path: Path, monkeypatch) -> None:
backend = load_backend()
worktree = tmp_path / "worktree"
worktree.mkdir()
fake = fake_openclaw(tmp_path)
monkeypatch.setenv("AIOS_OPENCLAW_BIN", str(fake))
monkeypatch.setenv("AIOS_OPENCLAW_MODEL", "test/model")
monkeypatch.setenv("AIOS_OPENCLAW_PROVIDER_CREDENTIAL_MAP_JSON", "{}")
fd = os.open(str(worktree), getattr(os, "O_PATH", os.O_RDONLY) | os.O_DIRECTORY)
try:
value = request(worktree)
value["execution_context"]["worktree"] = f"/proc/self/fd/{fd}"
task, pinned = backend.validate_request(value)
result = backend.invoke_openclaw(task, pinned)
finally:
os.close(fd)
assert result["status"] == "COMPLETED"
assert result["evidence"]["backend"] == "openclaw-agent-exec"
assert result["evidence"]["model"] == "test/model"
assert (worktree / "changed_by_openclaw.txt").read_text(encoding="utf-8") == "changed\n"
def test_provider_credential_is_projected_only_to_openclaw_child(tmp_path: Path, monkeypatch) -> None:
backend = load_backend()
credentials = tmp_path / "credentials"
credentials.mkdir()
(credentials / "openai_api_key").write_text("provider-secret\n", encoding="utf-8")
monkeypatch.setenv("CREDENTIALS_DIRECTORY", str(credentials))
monkeypatch.setenv(
"AIOS_OPENCLAW_PROVIDER_CREDENTIAL_MAP_JSON",
json.dumps({"OPENAI_API_KEY": "openai_api_key"}),
)
env = backend.provider_environment()
assert env["OPENAI_API_KEY"] == "provider-secret"
assert "AIOS_AGENT_TASK_TOKEN" not in env
assert env["OPENCLAW_EXEC_SHELL_SNAPSHOT"] == "0"
def test_missing_model_fails_closed(tmp_path: Path, monkeypatch) -> None:
backend = load_backend()
worktree = tmp_path / "worktree"
worktree.mkdir()
monkeypatch.setenv("AIOS_OPENCLAW_BIN", str(fake_openclaw(tmp_path)))
monkeypatch.delenv("AIOS_OPENCLAW_MODEL", raising=False)
fd = os.open(str(worktree), getattr(os, "O_PATH", os.O_RDONLY) | os.O_DIRECTORY)
try:
value = request(worktree)
value["execution_context"]["worktree"] = f"/proc/self/fd/{fd}"
task, pinned = backend.validate_request(value)
with pytest.raises(backend.BackendError) as caught:
backend.invoke_openclaw(task, pinned)
finally:
os.close(fd)
assert caught.value.code == "OPENCLAW_MODEL_INVALID"
def test_pinned_worktree_is_required(tmp_path: Path) -> None:
backend = load_backend()
value = request(tmp_path)
value["execution_context"]["worktree"] = str(tmp_path)
with pytest.raises(backend.BackendError) as caught:
backend.validate_request(value)
assert caught.value.code == "PINNED_WORKTREE_REQUIRED"