merge(task-plane): add governed multi-agent shadow control plane

Source: patch/aios-agent-task-control-plane-20260724
Source commit: 2458fa5f28
Target base: a8e9786897
Production execution: disabled
Production deployment: not authorized
Production tag operations: not authorized
This commit was merged in pull request #14.
This commit is contained in:
2026-07-24 13:57:01 +08:00
38 changed files with 6670 additions and 1 deletions
@@ -88,7 +88,10 @@ BASE_MODELS = [
{'id':'base.gpt-5.5','provider':'openai','model':'gpt-5.5','display_name':'GPT-5.5','scope':'global'},
]
SECRET_RE = re.compile(r'(?i)(api[_-]?key|secret[_-]?access[_-]?key|secret|password|token)\s*[:=]\s*["\']?([A-Za-z0-9_./+=:-]{16,})')
SECRET_VAL_RE = re.compile(r'(sk-[A-Za-z0-9_-]{20,}|HPUA[A-Z0-9]{10,}|SDC[A-Za-z0-9]{20,})')
SECRET_VAL_RE = re.compile(
r'(?<![A-Za-z0-9_])'
r'(sk-[A-Za-z0-9_-]{20,}|HPUA[A-Z0-9]{10,}|SDC[A-Za-z0-9]{20,})'
)
READ_ONLY_GET_PATHS = frozenset({
'/', '/health', '/api/health', '/api/openapi.yaml',
@@ -0,0 +1,179 @@
"""Task-scoped hardened wrapper around the existing Action API."""
from __future__ import annotations
import json
import os
import subprocess
from pathlib import Path
import aios_chatgpt_actions_api as api
import aios_isolated_writer_policy as policy
_reject = api.reject_non_read_http
_write_file = api.write_file
_sync_branch = api.sync_branch
def reject_non_read_http(handler, method: str, path: str) -> bool:
if _reject(handler, method, path):
return True
try:
policy.validate_endpoint(method, path, api.WS, api.BARE_REPO)
return False
except PermissionError as exc:
handler.sendj({"error": str(exc)}, 403)
return True
def checkout(branch, base="main", create=False):
api.ensure_git()
target = api.branch_name(branch)
policy.validate_branch_request({"branch": target, "base": base})
exists = api.sh(["rev-parse", "--verify", target])["code"] == 0
if exists:
if api.sh(["rev-parse", "--abbrev-ref", "HEAD"], True)["stdout"] != target:
api.sh(["checkout", target], True)
elif create:
api.sh(["checkout", "-b", target, base], True)
else:
raise PermissionError("isolated_writer_branch_missing")
policy.require_service_invariants(api.WS, api.BARE_REPO)
return target
def write_file(path, content):
policy.validate_path(path, api.WS)
return _write_file(path, content)
def sync_branch(branch: str):
policy.validate_sync_branch(branch)
local = _sync_branch(branch)
remote = os.getenv("AIOS_GITOPS_GITEA_PUSH_REMOTE", "").strip()
if remote != "gitea-http":
raise PermissionError("isolated_writer_gitea_remote_rejected")
head = api.sh(["rev-parse", "HEAD"], True)["stdout"]
pushed = subprocess.run(
["git", "push", "--porcelain", remote, f"HEAD:refs/heads/{branch}"],
cwd=api.WS,
text=True,
capture_output=True,
)
if pushed.returncode != 0:
raise RuntimeError("isolated_writer_gitea_push_failed")
verified = subprocess.run(
["git", "ls-remote", "--heads", remote, f"refs/heads/{branch}"],
cwd=api.WS,
text=True,
capture_output=True,
)
if verified.returncode != 0:
raise RuntimeError("isolated_writer_gitea_verification_failed")
fields = verified.stdout.strip().split()
if len(fields) != 2 or fields[0] != head or fields[1] != f"refs/heads/{branch}":
raise RuntimeError("isolated_writer_gitea_oid_mismatch")
return {
"status": "verified",
"target": "authoritative-bare+gitea",
"branch": branch,
"oid": head,
"local": local.get("status"),
}
def commit(paths, msg):
policy.validate_commit_paths(paths, api.WS)
api.ensure_git()
selected = [api.relpath(p) for p in paths] if paths else ["."]
api.sh(["add", *selected], True)
policy.validate_staged_paths(api.WS)
branch = api.sh(["rev-parse", "--abbrev-ref", "HEAD"], True)["stdout"]
if not api.sh(["status", "--short"])["stdout"]:
return {"status": "no_changes", "branch": branch, "sync": sync_branch(branch)}
out = api.sh(["commit", "-m", msg], True)
branch = api.sh(["rev-parse", "--abbrev-ref", "HEAD"], True)["stdout"]
return {
"status": "committed",
"branch": branch,
"rev": api.sh(["rev-parse", "--short", "HEAD"], True)["stdout"],
"git": out,
"sync": sync_branch(branch),
}
def audit(actor, action, target, payload):
policy.append_audit_record(
{
"at": api.now(),
"actor": actor,
"action": action,
"target": target,
"payload": api.sanitize(payload),
}
)
def _validate_request(self, data, validate):
try:
validate(data)
return True
except PermissionError as exc:
self.sendj({"error": str(exc)}, 403)
return False
def _checked_branch(self, data):
if not _validate_request(self, data, policy.validate_branch_request):
return
return _branch(self, data)
def _checked_file(self, data):
data = dict(data)
data.setdefault("branch", policy.allowed_branch())
if not _validate_request(self, data, policy.validate_branch_request):
return
if not _validate_request(
self,
{"path": data.get("path", "")},
lambda item: policy.validate_path(item["path"], api.WS),
):
return
return _file(self, data)
def _checked_commit(self, data):
data = dict(data)
data.setdefault("branch", policy.allowed_branch())
if not _validate_request(self, data, policy.validate_branch_request):
return
return _commit(self, data)
def _checked_patch(self, data):
data = dict(data)
data["branch"] = policy.allowed_branch()
if not _validate_request(self, data, policy.validate_branch_request):
return
return _patch(self, data)
_branch, _file, _commit, _patch = api.H.branch, api.H.file, api.H.commit, api.H.patch
api.reject_non_read_http = reject_non_read_http
api.checkout, api.write_file, api.sync_branch, api.commit, api.audit = (
checkout,
write_file,
sync_branch,
commit,
audit,
)
api.H.branch, api.H.file, api.H.commit, api.H.patch = (
_checked_branch,
_checked_file,
_checked_commit,
_checked_patch,
)
if __name__ == "__main__":
policy.require_service_invariants(api.WS, api.BARE_REPO)
api.ThreadingHTTPServer((api.HOST, api.PORT), api.H).serve_forever()
@@ -0,0 +1,280 @@
"""Fail-closed policy used only by the task-scoped Action writer."""
from __future__ import annotations
import os
import json
import stat
import subprocess
import re
from pathlib import Path, PurePosixPath, PureWindowsPath
from urllib.parse import unquote
WRITE_ENDPOINTS = frozenset({
"/api/branch", "/api/create-branch", "/api/file", "/api/commit-file",
"/api/commit", "/api/patch", "/api/create-patch",
})
ALLOWED_ROOTS = (
"active-runtime",
"docs",
"patches",
"reports",
"scripts",
"test",
"tests",
)
DENIED_ROOTS = ("baseline", "config", "deploy", "deployments", "ops", "runtime", "services")
_TRUE = frozenset({"1", "true", "yes", "on"})
_FALSE = frozenset({"0", "false", "no", "off"})
_DENY_FLAGS = (
"AIOS_GITOPS_ALLOW_MAIN_WRITE",
"AIOS_GITOPS_ALLOW_TAG_WRITE",
"AIOS_GITOPS_ALLOW_BASELINE_WRITE",
"AIOS_GITOPS_ALLOW_REGISTRY_WRITE",
"AIOS_GITOPS_ALLOW_ROLLBACK",
"AIOS_GITOPS_ALLOW_DEPLOYMENT",
)
def enforcement_required() -> bool:
return os.getenv("AIOS_GITOPS_ISOLATED_WRITER_ENFORCEMENT", "") == "required"
def allowed_branch() -> str:
value = os.getenv("AIOS_GITOPS_ALLOWED_BRANCH", "").strip()
if not value:
raise PermissionError("isolated_writer_missing_allowed_branch")
return value
def allowed_base_oid() -> str:
value = os.getenv("AIOS_GITOPS_ALLOWED_BASE_OID", "").strip().lower()
if len(value) != 40 or any(ch not in "0123456789abcdef" for ch in value):
raise PermissionError("isolated_writer_invalid_allowed_base_oid")
return value
def _git(workspace: Path, *args: str) -> str:
result = subprocess.run(["git", *args], cwd=workspace, text=True, capture_output=True)
if result.returncode != 0:
raise PermissionError("isolated_writer_git_validation_failed")
return result.stdout.strip()
def require_service_invariants(workspace: Path, bare_repo: Path) -> None:
if not enforcement_required():
return
if os.getenv("AIOS_GITOPS_WRITES_ENABLED", "").strip().lower() not in _TRUE:
raise PermissionError("isolated_writer_write_gate_required")
if os.getenv("AIOS_GITOPS_RUNTIME_ROLE", "") != "isolated-writer":
raise PermissionError("isolated_writer_role_required")
if os.getenv("AIOS_GITOPS_WORKSPACE_ROLE", "") != "isolated-writer":
raise PermissionError("isolated_writer_workspace_role_required")
for name in _DENY_FLAGS:
if os.getenv(name, "").strip().lower() not in _FALSE:
raise PermissionError("isolated_writer_forbidden_capability_flag")
if os.getenv("AIOS_GITOPS_REPOSITORY_IDENTITY", "") != "aios/aios":
raise PermissionError("isolated_writer_repository_identity_rejected")
configured_bare_value = os.getenv("AIOS_GITOPS_AUTHORITATIVE_GIT_DIR", "").strip()
if not configured_bare_value:
raise PermissionError("isolated_writer_authoritative_repo_rejected")
configured_bare = Path(configured_bare_value)
if configured_bare.resolve(strict=True) != bare_repo.resolve(strict=True):
raise PermissionError("isolated_writer_authoritative_repo_rejected")
writer_root_value = os.getenv("AIOS_GITOPS_APPROVED_WRITER_ROOT", "").strip()
if not writer_root_value:
raise PermissionError("isolated_writer_workspace_outside_approved_root")
writer_root = Path(writer_root_value).resolve(strict=True)
lexical_workspace = Path(os.path.abspath(str(workspace)))
canonical_workspace = lexical_workspace.resolve(strict=True)
if lexical_workspace != canonical_workspace:
raise PermissionError("isolated_writer_workspace_symlink_rejected")
try:
canonical_workspace.relative_to(writer_root)
except (ValueError, FileNotFoundError):
raise PermissionError("isolated_writer_workspace_outside_approved_root") from None
if _git(workspace, "rev-parse", "--abbrev-ref", "HEAD") != allowed_branch():
raise PermissionError("isolated_writer_current_branch_rejected")
approved_base = allowed_base_oid()
try:
_git(workspace, "cat-file", "-e", f"{approved_base}^{{commit}}")
subprocess.run(
["git", "merge-base", "--is-ancestor", approved_base, "HEAD"],
cwd=workspace,
check=True,
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
)
except (PermissionError, subprocess.CalledProcessError):
raise PermissionError("isolated_writer_approved_base_not_ancestor") from None
if _git(workspace, "merge-base", approved_base, "HEAD") != approved_base:
raise PermissionError("isolated_writer_merge_base_rejected")
common = Path(_git(workspace, "rev-parse", "--git-common-dir"))
if not common.is_absolute():
common = (workspace / common).resolve()
if common.resolve() != bare_repo.resolve():
raise PermissionError("isolated_writer_common_repo_rejected")
def approved_audit_log_path() -> Path:
root_value = os.getenv("AIOS_GITOPS_APPROVED_AUDIT_ROOT", "").strip()
log_value = os.getenv("AIOS_GITOPS_AUDIT_LOG", "").strip()
if not root_value or not log_value:
raise PermissionError("isolated_writer_audit_path_missing")
lexical_root = Path(os.path.abspath(root_value))
lexical_log = Path(os.path.abspath(log_value))
root = lexical_root.resolve(strict=True)
if lexical_root != root or root.is_symlink() or not root.is_dir():
raise PermissionError("isolated_writer_audit_root_rejected")
try:
lexical_log.relative_to(root)
except ValueError:
raise PermissionError("isolated_writer_audit_path_outside_approved_root") from None
if lexical_log.parent.resolve(strict=True) != lexical_log.parent:
raise PermissionError("isolated_writer_audit_parent_symlink_rejected")
if lexical_log.exists() and lexical_log.is_symlink():
raise PermissionError("isolated_writer_audit_log_symlink_rejected")
os.chmod(root, 0o750)
os.chmod(lexical_log.parent, 0o750)
return lexical_log
def append_audit_record(record: dict) -> None:
path = approved_audit_log_path()
flags = os.O_WRONLY | os.O_APPEND | os.O_CREAT
if hasattr(os, "O_NOFOLLOW"):
flags |= os.O_NOFOLLOW
descriptor = os.open(path, flags, 0o600)
try:
info = os.fstat(descriptor)
if not stat.S_ISREG(info.st_mode):
raise PermissionError("isolated_writer_audit_log_not_regular")
os.fchmod(descriptor, 0o600)
line = json.dumps(
_audit_sanitize(record),
ensure_ascii=False,
sort_keys=True,
separators=(",", ":"),
).encode("utf-8") + b"\n"
os.write(descriptor, line)
os.fsync(descriptor)
finally:
os.close(descriptor)
parent_fd = os.open(path.parent, os.O_RDONLY | getattr(os, "O_DIRECTORY", 0))
try:
os.fsync(parent_fd)
finally:
os.close(parent_fd)
def _audit_sanitize(value):
if isinstance(value, dict):
return {
key: (
"[REDACTED]"
if any(
marker in str(key).lower()
for marker in (
"authorization",
"credential",
"password",
"secret",
"token",
"private_key",
)
)
else _audit_sanitize(inner)
)
for key, inner in value.items()
}
if isinstance(value, list):
return [_audit_sanitize(item) for item in value]
if isinstance(value, str) and re.search(
r"(?i)(authorization\s*:\s*bearer|(?:token|password|secret)\s*[:=])",
value,
):
return "[REDACTED]"
return value
def validate_endpoint(method: str, path: str, workspace: Path, bare_repo: Path) -> None:
if not enforcement_required():
return
if method != "POST" or path not in WRITE_ENDPOINTS:
raise PermissionError("isolated_writer_operation_forbidden")
require_service_invariants(workspace, bare_repo)
def validate_branch_request(data: dict) -> None:
if not enforcement_required():
return
branch = allowed_branch() if "branch" not in data else str(data.get("branch") or "").strip()
if branch != allowed_branch():
raise PermissionError("isolated_writer_branch_rejected")
if str(data.get("base", "main")).strip() != "main":
raise PermissionError("isolated_writer_base_rejected")
if data.get("force") or data.get("force_push"):
raise PermissionError("isolated_writer_force_rejected")
def validate_path(path: str, workspace: Path | None = None) -> str:
if not isinstance(path, str):
raise PermissionError("isolated_writer_path_rejected")
value = path
if not value or value != value.strip() or "\x00" in value or "\\" in value:
raise PermissionError("isolated_writer_path_rejected")
try:
decoded = unquote(value, errors="strict")
except (UnicodeDecodeError, ValueError):
raise PermissionError("isolated_writer_path_rejected") from None
if decoded != value:
raise PermissionError("isolated_writer_path_rejected")
windows = PureWindowsPath(value)
posix = PurePosixPath(value)
parts = value.split("/")
if (
posix.is_absolute()
or windows.is_absolute()
or bool(windows.drive)
or any(part in {"", ".", ".."} for part in parts)
):
raise PermissionError("isolated_writer_path_rejected")
first = parts[0]
if first in DENIED_ROOTS:
raise PermissionError("isolated_writer_protected_path_rejected")
if value != "README.md" and first not in ALLOWED_ROOTS:
raise PermissionError("isolated_writer_path_outside_allowed_roots")
if workspace is not None:
lexical_root = Path(os.path.abspath(str(workspace)))
root = lexical_root.resolve(strict=True)
if lexical_root != root:
raise PermissionError("isolated_writer_workspace_symlink_rejected")
current = root
for part in parts:
current = current / part
if current.is_symlink():
raise PermissionError("isolated_writer_path_symlink_rejected")
candidate = (root / value).resolve(strict=False)
try:
candidate.relative_to(root)
except ValueError:
raise PermissionError("isolated_writer_path_escapes_workspace") from None
return value
def validate_commit_paths(paths: list[str], workspace: Path | None = None) -> None:
if enforcement_required():
for path in paths:
if str(path).strip() != ".":
validate_path(str(path), workspace)
def validate_staged_paths(workspace: Path) -> None:
if not enforcement_required():
return
for path in _git(workspace, "diff", "--cached", "--name-only").splitlines():
validate_path(path, workspace)
def validate_sync_branch(branch: str) -> None:
if enforcement_required() and branch != allowed_branch():
raise PermissionError("isolated_writer_sync_branch_rejected")
+21
View File
@@ -0,0 +1,21 @@
"""Task Plane runtime package."""
from .models import (
EvidenceEnvelope,
ProbeEvidence,
ProbeOutcome,
TaskPlan,
TaskRecord,
TaskStatus,
TaskStateTransitionError,
)
__all__ = [
"EvidenceEnvelope",
"ProbeEvidence",
"ProbeOutcome",
"TaskPlan",
"TaskRecord",
"TaskStatus",
"TaskStateTransitionError",
]
@@ -0,0 +1,17 @@
"""Adapters for executing tasks via worker backends."""
from .base import WorkerAdapter
from .openclaw import OpenClawAdapter
from .codex import CodexAdapter
from .hermes import HermesAdapter
from .a2a import A2AAdapter
from .systemd_worker import SystemdWorkerAdapter
__all__ = [
"WorkerAdapter",
"OpenClawAdapter",
"CodexAdapter",
"HermesAdapter",
"A2AAdapter",
"SystemdWorkerAdapter",
]
@@ -0,0 +1,31 @@
from __future__ import annotations
from .base import WorkerAdapter
class A2AAdapter(WorkerAdapter):
worker_name = "a2a"
def capabilities(self) -> list[str]:
return ["task.exchange", "agent.handoff"]
def claim(self, task):
return {"worker": self.worker_name, "task_id": task.task_id, "status": "claimed"}
def execute(self, task, claim):
return {"worker": self.worker_name, "task_id": task.task_id, "status": "SHADOW_VERIFIED", "claim_id": claim.claim_id, "execution_mode": "shadow", "production_side_effects": False}
def heartbeat(self, claim):
return {"worker": self.worker_name, "claim_id": claim.claim_id, "status": "alive"}
def report_progress(self, claim, progress):
return {"worker": self.worker_name, "claim_id": claim.claim_id, "progress": progress}
def probe(self, claim):
return {"worker": self.worker_name, "claim_id": claim.claim_id, "status": claim.status}
def cancel(self, claim):
return {"worker": self.worker_name, "claim_id": claim.claim_id, "status": "cancelled"}
def collect_result(self, claim):
return {"worker": self.worker_name, "claim_id": claim.claim_id, "result": "not_collected"}
@@ -0,0 +1,41 @@
"""Base worker adapter contract."""
from __future__ import annotations
import abc
class WorkerAdapter(abc.ABC):
worker_name = "base"
@abc.abstractmethod
def capabilities(self) -> list[str]:
raise NotImplementedError
@abc.abstractmethod
def claim(self, task):
raise NotImplementedError
@abc.abstractmethod
def execute(self, task, claim):
raise NotImplementedError
@abc.abstractmethod
def heartbeat(self, claim):
raise NotImplementedError
@abc.abstractmethod
def report_progress(self, claim, progress):
raise NotImplementedError
@abc.abstractmethod
def probe(self, claim):
raise NotImplementedError
@abc.abstractmethod
def cancel(self, claim):
raise NotImplementedError
@abc.abstractmethod
def collect_result(self, claim):
raise NotImplementedError
@@ -0,0 +1,31 @@
from __future__ import annotations
from .base import WorkerAdapter
class CodexAdapter(WorkerAdapter):
worker_name = "codex"
def capabilities(self) -> list[str]:
return ["code.read", "code.write.patch", "test.python", "review.security"]
def claim(self, task):
return {"worker": self.worker_name, "task_id": task.task_id, "status": "claimed"}
def execute(self, task, claim):
return {"worker": self.worker_name, "task_id": task.task_id, "status": "SHADOW_VERIFIED", "claim_id": claim.claim_id, "execution_mode": "shadow", "production_side_effects": False}
def heartbeat(self, claim):
return {"worker": self.worker_name, "claim_id": claim.claim_id, "status": "alive"}
def report_progress(self, claim, progress):
return {"worker": self.worker_name, "claim_id": claim.claim_id, "progress": progress}
def probe(self, claim):
return {"worker": self.worker_name, "claim_id": claim.claim_id, "status": claim.status}
def cancel(self, claim):
return {"worker": self.worker_name, "claim_id": claim.claim_id, "status": "cancelled"}
def collect_result(self, claim):
return {"worker": self.worker_name, "claim_id": claim.claim_id, "result": "not_collected"}
@@ -0,0 +1,31 @@
from __future__ import annotations
from .base import WorkerAdapter
class HermesAdapter(WorkerAdapter):
worker_name = "hermes"
def capabilities(self) -> list[str]:
return ["architecture.review", "policy.review", "threat.model"]
def claim(self, task):
return {"worker": self.worker_name, "task_id": task.task_id, "status": "claimed"}
def execute(self, task, claim):
return {"worker": self.worker_name, "task_id": task.task_id, "status": "SHADOW_VERIFIED", "claim_id": claim.claim_id, "execution_mode": "shadow", "production_side_effects": False}
def heartbeat(self, claim):
return {"worker": self.worker_name, "claim_id": claim.claim_id, "status": "alive"}
def report_progress(self, claim, progress):
return {"worker": self.worker_name, "claim_id": claim.claim_id, "progress": progress}
def probe(self, claim):
return {"worker": self.worker_name, "claim_id": claim.claim_id, "status": claim.status}
def cancel(self, claim):
return {"worker": self.worker_name, "claim_id": claim.claim_id, "status": "cancelled"}
def collect_result(self, claim):
return {"worker": self.worker_name, "claim_id": claim.claim_id, "result": "not_collected"}
@@ -0,0 +1,31 @@
from __future__ import annotations
from .base import WorkerAdapter
class OpenClawAdapter(WorkerAdapter):
worker_name = "openclaw"
def capabilities(self) -> list[str]:
return ["code.read", "code.write.patch", "test.execute", "runtime.shadow"]
def claim(self, task):
return {"worker": self.worker_name, "task_id": task.task_id, "status": "claimed"}
def execute(self, task, claim):
return {"worker": self.worker_name, "task_id": task.task_id, "status": "SHADOW_VERIFIED", "claim_id": claim.claim_id, "execution_mode": "shadow", "production_side_effects": False}
def heartbeat(self, claim):
return {"worker": self.worker_name, "claim_id": claim.claim_id, "status": "alive"}
def report_progress(self, claim, progress):
return {"worker": self.worker_name, "claim_id": claim.claim_id, "progress": progress}
def probe(self, claim):
return {"worker": self.worker_name, "claim_id": claim.claim_id, "status": claim.status}
def cancel(self, claim):
return {"worker": self.worker_name, "claim_id": claim.claim_id, "status": "cancelled"}
def collect_result(self, claim):
return {"worker": self.worker_name, "claim_id": claim.claim_id, "result": "not_collected"}
@@ -0,0 +1,31 @@
from __future__ import annotations
from .base import WorkerAdapter
class SystemdWorkerAdapter(WorkerAdapter):
worker_name = "systemd_worker"
def capabilities(self) -> list[str]:
return ["systemd.exec", "runtime.shell"]
def claim(self, task):
return {"worker": self.worker_name, "task_id": task.task_id, "status": "claimed"}
def execute(self, task, claim):
return {"worker": self.worker_name, "task_id": task.task_id, "status": "SHADOW_VERIFIED", "claim_id": claim.claim_id, "execution_mode": "shadow", "production_side_effects": False}
def heartbeat(self, claim):
return {"worker": self.worker_name, "claim_id": claim.claim_id, "status": "alive"}
def report_progress(self, claim, progress):
return {"worker": self.worker_name, "claim_id": claim.claim_id, "progress": progress}
def probe(self, claim):
return {"worker": self.worker_name, "claim_id": claim.claim_id, "status": claim.status}
def cancel(self, claim):
return {"worker": self.worker_name, "claim_id": claim.claim_id, "status": "cancelled"}
def collect_result(self, claim):
return {"worker": self.worker_name, "claim_id": claim.claim_id, "result": "not_collected"}
@@ -0,0 +1,27 @@
"""Agent registry abstraction."""
from __future__ import annotations
from .errors import NotFoundError
class AgentRegistry:
def __init__(self):
self._agents: dict[str, dict] = {}
def register(self, agent_id: str, metadata: dict) -> None:
self._agents[agent_id] = {"agent_id": agent_id, **metadata}
def unregister(self, agent_id: str) -> None:
if agent_id not in self._agents:
raise NotFoundError("agent")
del self._agents[agent_id]
def list(self) -> list[dict]:
return list(self._agents.values())
def get(self, agent_id: str) -> dict:
try:
return self._agents[agent_id]
except KeyError:
raise NotFoundError("agent")
+553
View File
@@ -0,0 +1,553 @@
"""Task Plane HTTP API implementation (stdlib-only)."""
from __future__ import annotations
import json
import os
import time
import uuid
import urllib.parse
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
from typing import Any, Callable
from .delegation import DelegationEngine
from .errors import (
ConflictError,
ForbiddenError,
NotFoundError,
PayloadTooLargeError,
TaskPlaneHTTPError,
UnauthorizedError,
ValidationError,
)
from .evidence import EvidenceService
from .journal import TaskJournal
from .lease import LeaseManager
from .models import (
EvidenceEnvelope,
ResultEnvelope,
TaskPlan,
TaskRecord,
iso8601_now,
contains_secret_text,
validate_identifier,
)
from .policy import PolicyEngine
from .store import TaskPlaneStore
from .worker_registry import WorkerRegistry
def _json_payload(data: dict[str, Any], status: int = 200) -> tuple[int, bytes]:
body = json.dumps(data, ensure_ascii=False).encode("utf-8")
return status, body
def _bounded_int(value: Any, name: str, minimum: int, maximum: int) -> int:
try:
number = int(value)
except (TypeError, ValueError) as exc:
raise ValidationError(f"invalid {name}") from exc
if not minimum <= number <= maximum:
raise ValidationError(f"invalid {name}")
return number
class TaskPlaneService:
def __init__(self, store: TaskPlaneStore, policy: PolicyEngine | None = None):
self.store = store
self.policy = policy or PolicyEngine(
execution_enabled=False,
identity_capabilities={
"anon": {"task.creator", "task.read", "task.audit"}
},
)
self.journal = TaskJournal(store)
self.lease = LeaseManager(store)
self.workers = WorkerRegistry(store)
self.evidence = EvidenceService(store)
self.delegation = DelegationEngine(store)
self.store.probe_attestation_verifier = self.policy.probe_attestation_verifier
def create_task(self, body: dict[str, Any], idempotency_key: str | None = None) -> dict[str, Any]:
if body.get("parent_task_id"):
raise ForbiddenError("child tasks must use the delegate endpoint")
issuer = body.get("issuer", {})
actor = issuer.get("id", "anon") if isinstance(issuer, dict) else "anon"
self.policy.assert_allowed("create", actor)
try:
plan = TaskPlan.from_dict(body)
except (TypeError, ValueError) as exc:
raise ValidationError("invalid task plan") from exc
task = self.store.create_task_with_event(plan, idempotency_key=idempotency_key)
return task.to_dict()
def list_tasks(self, actor: str | None = None) -> list[dict[str, Any]]:
if actor is not None:
self.policy.assert_allowed("read", actor)
return [task.to_dict(include_task=False) for task in self.store.list_tasks()]
def get_task(self, task_id: str, actor: str | None = None) -> dict[str, Any]:
if actor is not None:
self.policy.assert_allowed("read", actor, task=self.store.get_task(task_id))
return self.store.get_task(task_id).to_dict()
def get_worker(self, worker_id: str, actor: str | None = None) -> dict[str, Any]:
if actor is not None:
self.policy.assert_allowed("read", actor)
return self.workers.get(worker_id).to_dict()
def list_workers(self, actor: str | None = None) -> list[dict[str, Any]]:
if actor is not None:
self.policy.assert_allowed("read", actor)
return [w.to_dict() for w in self.workers.list()]
def register_worker(self, body: dict[str, Any]) -> dict[str, Any]:
actor = str(body.get("actor_identity") or body.get("identity") or "")
is_admin = actor in self.policy.admin_identities
try:
existing = self.store.get_worker(str(body.get("worker_id") or ""))
except NotFoundError:
existing = None
if existing is not None and str(body.get("identity") or "") != existing.identity:
raise ConflictError("worker identity takeover rejected")
self.policy.assert_allowed(
"worker.admin" if is_admin else "worker.register", actor
)
try:
worker = self.workers.register(
body,
actor_identity=actor,
is_admin=is_admin,
expected_revision=body.get("expected_revision"),
)
except (TypeError, ValueError, KeyError) as exc:
raise ValidationError("invalid worker registration") from exc
return worker.to_dict()
def worker_heartbeat(self, worker_id: str, payload: dict[str, Any]) -> dict[str, Any]:
interval = payload.get("heartbeat_interval_seconds")
actor = str(payload.get("actor_identity") or "")
self.policy.assert_allowed("worker.register", actor)
return self.workers.heartbeat(
worker_id,
heartbeat_interval_seconds=interval,
actor_identity=actor,
expected_revision=payload.get("expected_revision"),
).to_dict()
def disable_worker(self, worker_id: str, payload: dict[str, Any]) -> dict[str, Any]:
actor = str(payload.get("actor_identity") or "")
self.policy.assert_allowed("worker.admin", actor)
return self.store.disable_worker(
worker_id,
actor_identity=actor,
expected_revision=int(payload.get("expected_revision")),
reason=str(payload.get("reason") or "disabled by administrator"),
).to_dict()
def restore_worker(self, worker_id: str, payload: dict[str, Any]) -> dict[str, Any]:
actor = str(payload.get("actor_identity") or "")
self.policy.assert_allowed("worker.admin", actor)
return self.store.restore_worker(
worker_id,
actor_identity=actor,
expected_revision=int(payload.get("expected_revision")),
).to_dict()
def claim(self, task_id: str, body: dict[str, Any]) -> dict[str, Any]:
actor = body.get("actor_identity") or body.get("worker_identity") or "worker"
self.policy.assert_allowed("claim", actor)
worker_id = body.get("worker_id")
if not worker_id:
raise ValidationError("worker_id required")
task = self.store.get_task(task_id)
self.policy.assert_allowed("claim", actor, task=task)
lease_seconds = _bounded_int(
body.get("lease_seconds", 30), "lease_seconds", 1, 3600
)
claim = self.lease.claim(
task_id=task_id,
worker_id=worker_id,
worker_identity=actor,
lease_seconds=lease_seconds,
expected_revision=task.revision,
operation_digest=task.task_plan.operation_digest(),
)
return {"claim_id": claim.claim_id, **claim.to_dict()}
def heartbeat(self, task_id: str, body: dict[str, Any]) -> dict[str, Any]:
claim_id = body.get("claim_id")
if not claim_id:
raise ValidationError("claim_id required")
actor = body.get("actor_identity") or body.get("worker_identity") or "worker"
self.policy.assert_allowed("heartbeat", actor)
task = self.store.get_task(task_id)
self.policy.assert_allowed("heartbeat", actor, task=task)
claim = self.store.get_claim(claim_id)
if claim.worker_identity != actor:
raise ForbiddenError("claim identity mismatch")
lease_seconds = _bounded_int(
body.get("lease_seconds", 30), "lease_seconds", 1, 3600
)
claim = self.lease.heartbeat(task_id, claim_id, lease_seconds)
return {"claim_id": claim_id, "lease_until": claim.lease_until}
def progress(self, task_id: str, body: dict[str, Any]) -> dict[str, Any]:
claim_id = body.get("claim_id")
if not claim_id:
raise ValidationError("claim_id required")
actor = body.get("actor_identity") or body.get("worker_identity") or "worker"
self.policy.assert_allowed("progress", actor)
progress = body.get("progress")
stage = body.get("stage", "RUNNING")
message = body.get("message")
if progress is None:
raise ValidationError("progress required")
task = self.store.get_task(task_id)
self.policy.assert_allowed("progress", actor, task=task)
if task.task_plan.data_classification == "credential-secret":
if message not in {None, ""} or contains_secret_text(stage):
raise ValidationError(
"credential-secret progress must contain metadata only"
)
lease_seconds = _bounded_int(
body.get("lease_seconds", 30), "lease_seconds", 1, 3600
)
claim = self.store.record_progress(
task_id,
claim_id,
actor,
progress=progress,
stage=str(stage),
message=None if message is None else str(message),
lease_seconds=lease_seconds,
)
return {"claim_id": claim.claim_id, "status": claim.status, "progress": progress}
def complete(self, task_id: str, body: dict[str, Any]) -> dict[str, Any]:
claim_id = body.get("claim_id")
if not claim_id:
raise ValidationError("claim_id required")
actor = body.get("actor_identity") or body.get("worker_identity") or "worker"
self.policy.assert_allowed("result", actor)
task = self.store.get_task(task_id)
self.policy.assert_allowed("result", actor, task=task)
status = body.get("status", "COMPLETED")
outputs = body.get("outputs", [])
evidences = body.get("evidence", [])
if not isinstance(outputs, list):
raise ValidationError("outputs must be list")
if not isinstance(evidences, list):
raise ValidationError("evidence must be list")
if any(not isinstance(ev, dict) for ev in evidences):
raise ValidationError("evidence items must be objects")
claim = self.store.get_claim(claim_id)
if claim.worker_identity != actor:
raise ForbiddenError("claim identity mismatch")
if task.task_plan.data_classification == "credential-secret":
allowed_result_keys = {
"kind",
"hash",
"sha256",
"count",
"status",
"error_code",
"signature",
"attestation_ref",
}
for entry in [*outputs, *evidences]:
if (
not isinstance(entry, dict)
or set(entry) - allowed_result_keys
or contains_secret_text(entry)
):
raise ValidationError(
"credential-secret result must contain hash/count/status only"
)
result = ResultEnvelope(
task_id=task_id,
claim_id=claim_id,
worker_identity=actor,
status=status,
outputs=outputs,
evidence=evidences,
started_at=str(body.get("started_at") or iso8601_now()),
completed_at=str(body.get("completed_at") or iso8601_now()),
result_digest=body.get("result_digest"),
signature=body.get("signature"),
attestation_ref=body.get("attestation_ref"),
operation_digest=claim.operation_digest,
)
evidence_decision = self.policy.evidence_verifier.verify(task, evidences)
if not evidence_decision.allowed:
raise ForbiddenError(evidence_decision.reason)
result_decision = self.policy.verify_result(result, task)
if not result_decision.allowed:
raise ForbiddenError(result_decision.reason)
result.result_digest = result.computed_digest()
previous = self.store._last_evidence_hash(task_id)
evidence_entries = []
for evidence in evidences:
envelope = EvidenceEnvelope.create(
task_id=task_id,
claim_id=claim_id,
kind=str(evidence.get("kind") or evidence.get("type") or "generic"),
worker_identity=actor,
payload=evidence,
previous_evidence_hash=previous,
signature=evidence.get("signature"),
attestation_ref=evidence.get("attestation_ref"),
)
evidence_entries.append(envelope)
previous = envelope.evidence_digest
self.store.complete_claim_with_evidence_and_event(
task_id,
claim_id,
status
if status
in {"COMPLETED", "FAILED_FINAL", "FAILED_RECOVERABLE", "CANCELLED"}
else "COMPLETED",
result.to_dict(),
evidence_entries,
)
return result.to_dict()
def fail(self, task_id: str, body: dict[str, Any]) -> dict[str, Any]:
claim_id = body.get("claim_id")
if not claim_id:
raise ValidationError("claim_id required")
reason = body.get("reason", "failed")
actor = body.get("actor_identity") or body.get("worker_identity") or "worker"
self.policy.assert_allowed("fail", actor)
task = self.store.get_task(task_id)
self.policy.assert_allowed("fail", actor, task=task)
claim = self.store.get_claim(claim_id)
if claim.worker_identity != actor:
raise ForbiddenError("claim identity mismatch")
status = "FAILED_RECOVERABLE" if body.get("recoverable", False) else "FAILED_FINAL"
self.store.fail_claim_with_event(task_id, claim_id, status, str(reason))
return {"task_id": task_id, "status": status, "reason": reason}
def cancel(self, task_id: str, body: dict[str, Any]) -> dict[str, Any]:
actor = body.get("actor_identity") or body.get("worker_identity") or "owner"
self.policy.assert_allowed("cancel", actor)
reason = body.get("reason", "cancelled")
task = self.store.get_task(task_id)
self.policy.assert_allowed("cancel", actor, task=task)
updated = self.store.cancel_task_with_event(
task_id, task.revision, reason=reason
)
return updated.to_dict()
def delegate(self, task_id: str, body: dict[str, Any]) -> dict[str, Any]:
actor = body.get("actor_identity") or "owner"
self.policy.assert_allowed("delegate", actor)
parent = self.store.get_task(task_id)
self.policy.assert_allowed("delegate", actor, task=parent)
if "task" in body:
try:
child_plan = TaskPlan.from_dict(body["task"])
except (TypeError, ValueError) as exc:
raise ValidationError("invalid child task plan") from exc
else:
plan_body = dict(body)
plan_body.pop("actor_identity", None)
try:
child_plan = TaskPlan.from_dict(plan_body)
except (TypeError, ValueError) as exc:
raise ValidationError("invalid child task plan") from exc
child_plan.parent_task_id = task_id
child = self.delegation.delegate(task_id, child_plan, caller=actor)
return child.to_dict()
def get_events(self, task_id: str, actor: str | None = None) -> list[dict[str, Any]]:
if actor is not None:
self.policy.assert_allowed("audit", actor, task=self.store.get_task(task_id))
self.store.get_task(task_id)
return self.store.list_events(task_id)
def get_evidence(self, task_id: str, actor: str | None = None) -> list[dict[str, Any]]:
if actor is not None:
self.policy.assert_allowed("audit", actor, task=self.store.get_task(task_id))
self.store.get_task(task_id)
return self.evidence.list(task_id)
def get_children(self, task_id: str, actor: str | None = None) -> list[str]:
if actor is not None:
self.policy.assert_allowed("read", actor, task=self.store.get_task(task_id))
self.store.get_task(task_id)
return self.store.list_children(task_id)
class TaskPlaneRequestHandler(BaseHTTPRequestHandler):
service: TaskPlaneService | None = None
authenticator: Callable[[str], str | None] | None = None
max_body_bytes = 1_000_000
def log_message(self, format: str, *args: Any) -> None:
# BaseHTTPRequestHandler logs the raw request target; suppress it so
# attacker-controlled paths or query strings cannot leak credentials.
return
def _json(self) -> tuple[str | None, dict[str, Any]]:
try:
length = int(self.headers.get("Content-Length", "0"))
except ValueError as exc:
raise ValidationError("invalid content length") from exc
if length < 0:
raise ValidationError("invalid content length")
if length > self.max_body_bytes:
raise PayloadTooLargeError("payload exceeds limit")
raw = self.rfile.read(length) if length > 0 else b""
if not raw:
return None, {}
try:
decoded = raw.decode("utf-8")
payload = json.loads(decoded)
if not isinstance(payload, dict):
raise ValidationError("JSON payload must be an object")
return decoded, payload
except (UnicodeDecodeError, json.JSONDecodeError) as exc:
raise ValidationError("invalid JSON payload") from exc
def _response(self, status: int, payload: Any) -> None:
encoded = json.dumps(payload, ensure_ascii=False).encode("utf-8")
self.send_response(status)
self.send_header("Content-Type", "application/json; charset=utf-8")
self.send_header("Content-Length", str(len(encoded)))
self.end_headers()
self.wfile.write(encoded)
def _authenticated_identity(self) -> str:
authenticator = type(self).authenticator
if authenticator is None:
raise UnauthorizedError("authentication unavailable")
header = self.headers.get("Authorization", "")
if not header.startswith("Bearer "):
raise UnauthorizedError("bearer authentication required")
token = header[7:]
if not token or any(ord(ch) < 33 for ch in token):
raise UnauthorizedError("invalid bearer token")
identity = authenticator(token)
if not identity:
raise UnauthorizedError("invalid credentials")
return str(identity)
def _handle(self, handler):
try:
if self.service is None:
raise TaskPlaneHTTPError("service not configured")
result = handler()
if isinstance(result, tuple):
status, payload = result
else:
status, payload = 200, result
self._response(status, payload)
except TaskPlaneHTTPError as exc:
self._response(exc.status_code, exc.to_payload())
except Exception as exc: # defensive
if isinstance(exc, TaskPlaneHTTPError):
raise
self._response(
500,
{"error": "internal_error", "message": "internal server error"},
)
def do_GET(self): # noqa: N802
def route():
path = urllib.parse.urlparse(self.path).path
parts = [p for p in path.split("/") if p]
for part in parts[1:]:
if part not in {"events", "evidence", "children", "heartbeat"}:
try:
validate_identifier(part, "resource identifier")
except ValueError as exc:
raise ValidationError("invalid resource identifier") from exc
identity = self._authenticated_identity()
if len(parts) == 1 and parts[0] == "tasks":
return 200, self.service.list_tasks(identity)
if len(parts) == 2 and parts[0] == "tasks":
task_id = parts[1]
return 200, self.service.get_task(task_id, identity)
if len(parts) == 3 and parts[0] == "tasks" and parts[2] == "events":
return 200, self.service.get_events(parts[1], identity)
if len(parts) == 3 and parts[0] == "tasks" and parts[2] == "evidence":
return 200, self.service.get_evidence(parts[1], identity)
if len(parts) == 3 and parts[0] == "tasks" and parts[2] == "children":
return 200, self.service.get_children(parts[1], identity)
if len(parts) == 1 and parts[0] == "workers":
return 200, self.service.list_workers(identity)
if len(parts) == 2 and parts[0] == "workers":
return 200, self.service.get_worker(parts[1], identity)
raise NotFoundError("unknown endpoint")
self._handle(route)
def do_POST(self): # noqa: N802
def route():
body_text, body = self._json()
identity = self._authenticated_identity()
path = urllib.parse.urlparse(self.path).path
parts = [p for p in path.split("/") if p]
headers = {k.lower(): v for k, v in self.headers.items()}
if len(parts) == 1 and parts[0] == "tasks":
issuer = body.get("issuer")
if issuer and (
not isinstance(issuer, dict) or issuer.get("id") != identity
):
raise ForbiddenError("issuer identity mismatch")
body["issuer"] = {"type": "authenticated", "id": identity}
return 201, self.service.create_task(body or {}, headers.get("idempotency-key"))
if body.get("actor_identity") not in {None, identity}:
raise ForbiddenError("actor identity mismatch")
body["actor_identity"] = identity
if len(parts) == 3 and parts[0] == "tasks" and parts[2] == "claim":
return 200, self.service.claim(parts[1], body or {})
if len(parts) == 3 and parts[0] == "tasks" and parts[2] == "heartbeat":
return 200, self.service.heartbeat(parts[1], body or {})
if len(parts) == 3 and parts[0] == "tasks" and parts[2] == "progress":
return 200, self.service.progress(parts[1], body or {})
if len(parts) == 3 and parts[0] == "tasks" and parts[2] == "result":
return 200, self.service.complete(parts[1], body or {})
if len(parts) == 3 and parts[0] == "tasks" and parts[2] == "fail":
return 200, self.service.fail(parts[1], body or {})
if len(parts) == 3 and parts[0] == "tasks" and parts[2] == "cancel":
return 200, self.service.cancel(parts[1], body or {})
if len(parts) == 3 and parts[0] == "tasks" and parts[2] == "delegate":
return 200, self.service.delegate(parts[1], body or {})
if len(parts) == 2 and parts[0] == "workers" and parts[1] == "register":
if (
body.get("identity") != identity
and identity not in self.service.policy.admin_identities
):
raise ForbiddenError("worker identity mismatch")
return 201, self.service.register_worker(body or {})
if len(parts) == 3 and parts[0] == "workers" and parts[2] == "heartbeat":
return 200, self.service.worker_heartbeat(parts[1], body or {})
if len(parts) == 3 and parts[0] == "workers" and parts[2] == "disable":
return 200, self.service.disable_worker(parts[1], body or {})
if len(parts) == 3 and parts[0] == "workers" and parts[2] == "restore":
return 200, self.service.restore_worker(parts[1], body or {})
raise NotFoundError("unknown endpoint")
self._handle(route)
def run_server(
host: str,
port: int,
service: TaskPlaneService,
*,
authenticator: Callable[[str], str | None] | None = None,
):
handler = TaskPlaneRequestHandler
handler.service = service
handler.authenticator = authenticator
server = ThreadingHTTPServer((host, port), handler)
server.serve_forever()
@@ -0,0 +1,36 @@
"""Capability matcher helpers."""
from __future__ import annotations
from fnmatch import fnmatch
def _normalize_capability(capability: str) -> str:
return capability.strip().lower()
def capability_matches(granted: str, required: str) -> bool:
granted = _normalize_capability(granted)
required = _normalize_capability(required)
if granted == required:
return True
# A granted wildcard such as "test.*" should match required "test.unit"
if "*" in granted:
return fnmatch(required, granted)
# A required wildcard is treated as a suffix/prefix requirement for callers.
if "*" in required:
return fnmatch(granted, required)
return False
def has_capabilities(granted_caps: list[str], required_caps: list[str]) -> bool:
granted = [_normalize_capability(c) for c in granted_caps]
for required in required_caps:
required = _normalize_capability(required)
if not any(capability_matches(g, required) for g in granted):
return False
return True
def normalize_capabilities(caps: list[str]) -> list[str]:
return sorted({_normalize_capability(cap) for cap in caps})
@@ -0,0 +1,87 @@
"""Delegation guardrails."""
from __future__ import annotations
from .errors import ConflictError, ForbiddenError, NotFoundError
from .models import Budget, TaskPlan, TaskRecord
from .store import TaskPlaneStore
class DelegationEngine:
def __init__(self, store: TaskPlaneStore):
self.store = store
def can_delegate(self, parent_id: str, child_plan: TaskPlan) -> bool:
parent = self.store.get_task(parent_id)
if parent.status == "COMPLETED":
raise ConflictError("cannot delegate completed task")
if parent.task_plan.max_delegation_depth <= 0:
raise ConflictError("max delegation depth not configured")
depth = self._depth(parent_id)
if depth >= parent.task_plan.max_delegation_depth:
raise ConflictError("max delegation depth reached")
if child_plan.parent_task_id not in {None, parent_id}:
raise ConflictError("delegation parent mismatch")
current = parent_id
while current:
if child_plan.task_id and current == child_plan.task_id:
raise ConflictError("delegation cycle")
current = self.store.parent_of(current)
return True
def delegate(self, parent_id: str, child_plan: TaskPlan, *, caller: str) -> TaskRecord:
self.can_delegate(parent_id, child_plan)
parent = self.store.get_task(parent_id)
if child_plan.required_capabilities and not set(child_plan.required_capabilities).issubset(
set(parent.task_plan.required_capabilities or [])
):
raise ForbiddenError("child capabilities exceed parent constraints")
risk_rank = {"low": 1, "medium": 2, "high": 3}
if risk_rank.get(child_plan.risk, 99) > risk_rank.get(parent.task_plan.risk, -1):
raise ForbiddenError("child risk exceeds parent constraints")
child_plan.parent_task_id = parent_id
child_plan.correlation_id = parent.task_plan.correlation_id or parent.task_id
child_plan.preferred_workers = parent.task_plan.preferred_workers
child_plan.max_delegation_depth = min(
child_plan.max_delegation_depth,
parent.task_plan.max_delegation_depth,
)
child_plan.approval_policy = parent.task_plan.approval_policy
parent_budget = self.store.get_task_budget(parent_id)
required = Budget()
for name in ("time_seconds", "attempts", "tokens", "cost"):
available = getattr(parent_budget, name)
requested = getattr(child_plan.budgets, name)
if name == "attempts" and requested is None:
requested = 1
if available is not None:
requested = 0 if requested is None else requested
if requested > available:
raise ConflictError(f"budget exceeded for {name}")
setattr(child_plan.budgets, name, requested)
setattr(required, name, requested)
# A retried identical delegation is a read, not a second budget charge.
if child_plan.task_id:
try:
existing = self.store.get_task(child_plan.task_id)
except NotFoundError:
existing = None
if existing is not None:
if (
existing.task_plan.parent_task_id == parent_id
and existing.task_plan.operation_digest()
== child_plan.operation_digest()
):
return existing
raise ConflictError("delegation task_id conflict")
return self.store.delegate_task_with_budget_and_events(
parent_id, child_plan, caller=caller
)
def _depth(self, task_id: str) -> int:
return self.store._compute_depth(task_id)
+71
View File
@@ -0,0 +1,71 @@
"""Error types for task plane operations."""
from __future__ import annotations
class TaskPlaneError(Exception):
"""Base exception for API-compatible failures."""
status_code = 500
error_code = "task_plane_error"
def __init__(self, message: str, *, code: str | None = None, details: dict | None = None):
super().__init__(message)
self.message = message
self.details = details or {}
if code:
self.error_code = code
def to_payload(self) -> dict:
return {"error": self.error_code, "message": self.message, "details": self.details}
class TaskPlaneHTTPError(TaskPlaneError):
"""Base class with explicit HTTP status."""
status_code = 500
class ValidationError(TaskPlaneHTTPError):
status_code = 400
error_code = "validation_error"
class UnauthorizedError(TaskPlaneHTTPError):
status_code = 401
error_code = "unauthorized"
class ForbiddenError(TaskPlaneHTTPError):
status_code = 403
error_code = "forbidden"
class NotFoundError(TaskPlaneHTTPError):
status_code = 404
error_code = "not_found"
class ConflictError(TaskPlaneHTTPError):
status_code = 409
error_code = "conflict"
class PayloadTooLargeError(TaskPlaneHTTPError):
status_code = 413
error_code = "payload_too_large"
class FailedDependencyError(TaskPlaneHTTPError):
status_code = 424
error_code = "failed_dependency"
class LockedError(TaskPlaneHTTPError):
status_code = 423
error_code = "locked"
class ServiceUnavailableError(TaskPlaneHTTPError):
status_code = 503
error_code = "service_unavailable"
+108
View File
@@ -0,0 +1,108 @@
"""CloudEvents-compatible event envelope and durable outbox helpers."""
from __future__ import annotations
from dataclasses import dataclass
from datetime import datetime, timezone
from typing import Callable, Protocol
from .store import TaskPlaneStore
@dataclass
class CloudEvent:
specversion: str = "1.0"
id: str = ""
type: str = ""
source: str = "spiffe://aios/task-plane"
subject: str = ""
datacontenttype: str = "application/json"
data: dict | None = None
@property
def time(self) -> str:
return datetime.now(timezone.utc).replace(microsecond=0).isoformat().replace("+00:00", "Z")
def asdict(self) -> dict:
return {
"specversion": self.specversion,
"id": self.id,
"type": self.type,
"source": self.source,
"subject": self.subject,
"time": self.time,
"datacontenttype": self.datacontenttype,
"data": self.data or {},
}
NOT_DELIVERED = "NOT_DELIVERED"
DELIVERED = "DELIVERED"
FAILED_DELIVERY = "FAILED"
class NotificationAdapter(Protocol):
name: str
def deliver(self, envelope: dict) -> str | None:
"""Return a provider receipt, or None when delivery is unconfirmed."""
class CallbackNotificationAdapter:
"""Adapter seam for webhook, NATS, GPT, dashboard, and WeChat transports."""
def __init__(self, name: str, callback: Callable[[dict], str | None]):
self.name = name
self._callback = callback
def deliver(self, envelope: dict) -> str | None:
return self._callback(envelope)
class WebhookAdapter(CallbackNotificationAdapter):
def __init__(self, callback: Callable[[dict], str | None]):
super().__init__("webhook", callback)
class NatsJetStreamAdapter(CallbackNotificationAdapter):
def __init__(self, callback: Callable[[dict], str | None]):
super().__init__("nats-jetstream", callback)
class GPTResultCallbackAdapter(CallbackNotificationAdapter):
def __init__(self, callback: Callable[[dict], str | None]):
super().__init__("gpt-result-callback", callback)
class DashboardAdapter(CallbackNotificationAdapter):
def __init__(self, callback: Callable[[dict], str | None]):
super().__init__("dashboard", callback)
class WeChatGatewayAdapter(CallbackNotificationAdapter):
def __init__(self, callback: Callable[[dict], str | None]):
super().__init__("wechat-gateway", callback)
class DurableOutboxDispatcher:
def __init__(self, store: TaskPlaneStore):
self.store = store
def dispatch(self, message_id: str, adapter: NotificationAdapter) -> dict:
row = self.store.get_outbox_message(message_id)
import json
envelope = json.loads(row["payload"])
try:
receipt = adapter.deliver(envelope)
except Exception:
return self.store.record_outbox_delivery(
message_id,
receipt=None,
error=f"{adapter.name} delivery failed",
)
return self.store.record_outbox_delivery(
message_id,
receipt=str(receipt) if receipt else None,
error=None if receipt else f"{adapter.name} receipt unavailable",
)
+31
View File
@@ -0,0 +1,31 @@
"""Evidence helpers."""
from __future__ import annotations
import hashlib
import json
from .store import TaskPlaneStore
def evidence_digest(payload: dict) -> str:
body = json.dumps(payload, sort_keys=True, separators=(",", ":"))
return hashlib.sha256(body.encode("utf-8")).hexdigest()
class EvidenceService:
def __init__(self, store: TaskPlaneStore):
self.store = store
def add(self, task_id: str, kind: str, payload: dict, worker_identity: str) -> str:
return self.store.add_evidence(task_id, kind, payload, worker_identity)
def list(self, task_id: str) -> list[dict]:
return self.store.list_evidence(task_id)
def verify(self, task_id: str, required_kinds: list[str]) -> bool:
evidences = self.list(task_id)
observed = {e["kind"] for e in evidences}
return set(required_kinds).issubset(
observed
) and self.store.verify_evidence_integrity(task_id)
+22
View File
@@ -0,0 +1,22 @@
"""Event journaling helpers."""
from __future__ import annotations
from .errors import ValidationError
from .store import TaskPlaneStore
class TaskJournal:
def __init__(self, store: TaskPlaneStore):
self.store = store
def append(self, task_id: str, event_type: str, data: dict, *, subject: str | None = None) -> None:
if not event_type:
raise ValidationError("event_type required")
self.store.append_event(task_id, event_type, data, subject=subject)
def list(self, task_id: str) -> list[dict]:
return self.store.list_events(task_id)
def check_chain(self, task_id: str) -> bool:
return self.store.verify_event_chain(task_id)
+32
View File
@@ -0,0 +1,32 @@
"""Claim and lease management helpers."""
from __future__ import annotations
from .errors import LockedError
from .models import ProbeEvidence
from .store import TaskPlaneStore
class LeaseManager:
def __init__(self, store: TaskPlaneStore):
self.store = store
def claim(self, task_id: str, worker_id: str, worker_identity: str, lease_seconds: int, expected_revision: int, operation_digest: str):
return self.store.claim_task(task_id, worker_id, worker_identity, lease_seconds, expected_revision, operation_digest)
def heartbeat(self, task_id: str, claim_id: str, lease_seconds: int):
claim = self.store.heartbeat_claim(task_id, claim_id, lease_seconds)
return claim
def probe(self, claim_id: str):
claim = self.store.get_claim(claim_id)
if claim.status != "ACTIVE":
raise LockedError("claim not active")
return claim
def recover(self, task_id: str, claim_id: str, probe: ProbeEvidence):
return self.store.recover_expired_claim(
task_id,
claim_id,
probe,
)
+829
View File
@@ -0,0 +1,829 @@
"""Core data models for the task plane."""
from __future__ import annotations
import hashlib
import json
import re
import time
import uuid
import urllib.parse
from enum import StrEnum
from dataclasses import dataclass, field
from datetime import datetime, timezone
from typing import Any
SENSITIVE_KEYS = {
"secret",
"password",
"token",
"api_key",
"access_token",
"refresh_token",
"authorization",
"private_key",
"client_secret",
"credential",
}
IDENTIFIER_RE = re.compile(r"[A-Za-z0-9][A-Za-z0-9._:-]{0,127}")
TYPE_RE = re.compile(r"[A-Za-z0-9][A-Za-z0-9._-]{0,127}")
RISK_LEVELS = {"low", "medium", "high"}
DATA_CLASSIFICATIONS = {
"public",
"internal",
"confidential",
"credential-secret",
}
SECRET_TEXT_PATTERNS = (
re.compile(r"(?i)\bAuthorization\s*:\s*Bearer\s+\S+"),
re.compile(r"(?i)\b(?:password|passwd|api[_-]?key|access[_-]?token|secret)\s*[:=]\s*\S+"),
re.compile(r"\bsk-[A-Za-z0-9_-]{12,}\b"),
re.compile(r"\bAKIA[0-9A-Z]{16}\b"),
re.compile(r"-----BEGIN (?:RSA |EC |OPENSSH )?PRIVATE KEY-----"),
)
TASK_PLAN_KEYS = {
"schema_version",
"task_id",
"task_type",
"issuer",
"intent",
"required_capabilities",
"preferred_workers",
"risk",
"approval_policy",
"dependencies",
"max_delegation_depth",
"budgets",
"evidence_requirements",
"parent_task_id",
"correlation_id",
"metadata",
"data_classification",
"execution_zone",
"network_policy",
"logging_policy",
"allowed_input_paths",
"allowed_output_paths",
"output_policy",
}
def _now_ts() -> float:
return time.time()
def canonical_json(value: Any) -> str:
return json.dumps(value, ensure_ascii=False, sort_keys=True, separators=(",", ":"))
def compute_sha256_hex(value: Any) -> str:
return hashlib.sha256(canonical_json(value).encode("utf-8")).hexdigest()
def contains_secret_text(value: Any) -> bool:
if isinstance(value, str):
return any(pattern.search(value) for pattern in SECRET_TEXT_PATTERNS)
if isinstance(value, dict):
return any(contains_secret_text(item) for item in value.values())
if isinstance(value, (list, tuple)):
return any(contains_secret_text(item) for item in value)
return False
def validate_relative_paths(values: Any, name: str) -> list[str]:
paths = _normalize_list(values)
for value in paths:
decoded = urllib.parse.unquote(value)
parts = value.replace("\\", "/").split("/")
if (
decoded != value
or "\\" in value
or value.startswith("/")
or re.match(r"^[A-Za-z]:", value)
or any(part in {"", ".", ".."} for part in parts)
):
raise ValueError(f"invalid {name}")
return paths
def validate_identifier(value: Any, name: str) -> str:
text = str(value or "")
if (
not IDENTIFIER_RE.fullmatch(text)
or urllib.parse.unquote(text) != text
or any(ord(ch) < 32 or ord(ch) == 127 for ch in text)
):
raise ValueError(f"invalid {name}")
return text
def validate_type(value: Any, name: str) -> str:
text = str(value or "")
if not TYPE_RE.fullmatch(text):
raise ValueError(f"invalid {name}")
return text
def redact_value(value: Any) -> Any:
if isinstance(value, dict):
redacted = {}
for key, inner in value.items():
lowered = str(key).lower()
if lowered in SENSITIVE_KEYS or lowered.endswith(
("_password", "_secret", "_access_token", "_refresh_token", "_private_key")
):
redacted[key] = "[REDACTED]"
else:
redacted[key] = redact_value(inner)
return redacted
if isinstance(value, list):
return [redact_value(v) for v in value]
if isinstance(value, tuple):
return tuple(redact_value(v) for v in value)
return value
class TaskStateTransitionError(ValueError):
pass
class TaskStatus(StrEnum):
CREATED = "CREATED"
QUEUED = "QUEUED"
CLAIMED = "CLAIMED"
RUNNING = "RUNNING"
WAITING_CHILD = "WAITING_CHILD"
VERIFYING = "VERIFYING"
COMPLETED = "COMPLETED"
FAILED_RECOVERABLE = "FAILED_RECOVERABLE"
FAILED_FINAL = "FAILED_FINAL"
CANCELLED = "CANCELLED"
MANUAL_REVIEW_REQUIRED = "MANUAL_REVIEW_REQUIRED"
TASK_STATES = {
status.value for status in TaskStatus
}
TASK_NEXT_STATES = {
"CREATED": {"QUEUED", "CLAIMED", "CANCELLED", "MANUAL_REVIEW_REQUIRED"},
"QUEUED": {"CLAIMED", "CANCELLED", "FAILED_RECOVERABLE", "FAILED_FINAL"},
"CLAIMED": {"RUNNING", "FAILED_RECOVERABLE", "FAILED_FINAL", "CANCELLED"},
"RUNNING": {"VERIFYING", "WAITING_CHILD", "COMPLETED", "FAILED_RECOVERABLE", "FAILED_FINAL", "CANCELLED"},
"WAITING_CHILD": {"RUNNING", "VERIFYING", "FAILED_RECOVERABLE", "FAILED_FINAL", "CANCELLED"},
"VERIFYING": {"COMPLETED", "FAILED_RECOVERABLE", "FAILED_FINAL", "CANCELLED"},
"COMPLETED": set(),
"FAILED_RECOVERABLE": {"CLAIMED", "RUNNING", "CANCELLED"},
"FAILED_FINAL": set(),
"CANCELLED": set(),
"MANUAL_REVIEW_REQUIRED": {"RUNNING", "CANCELLED"},
}
@dataclass
class Budget:
time_seconds: int | None = None
attempts: int | None = None
tokens: int | None = None
cost: float | None = None
@classmethod
def from_dict(cls, data: dict[str, Any] | None) -> "Budget":
if not data:
return cls()
if not isinstance(data, dict) or set(data) - {
"time_seconds",
"attempts",
"tokens",
"cost",
}:
raise ValueError("invalid budget")
for name in ("time_seconds", "attempts", "tokens", "cost"):
value = data.get(name)
if value is not None:
try:
if float(value) < 0:
raise ValueError("budget values must be non-negative")
except (TypeError, ValueError) as exc:
raise ValueError("invalid budget value") from exc
return cls(
time_seconds=_to_positive_int(data.get("time_seconds")),
attempts=_to_positive_int(data.get("attempts")),
tokens=_to_positive_int(data.get("tokens")),
cost=_to_float(data.get("cost")),
)
def to_dict(self) -> dict[str, Any]:
return {
"time_seconds": self.time_seconds,
"attempts": self.attempts,
"tokens": self.tokens,
"cost": self.cost,
}
def merge(self, other: "Budget") -> "Budget":
return Budget(
time_seconds=_first_non_none(self.time_seconds, other.time_seconds),
attempts=_first_non_none(self.attempts, other.attempts),
tokens=_first_non_none(self.tokens, other.tokens),
cost=_first_non_none(self.cost, other.cost),
)
@dataclass
class TaskPlan:
schema_version: int = 1
task_id: str | None = None
task_type: str = "code.change"
issuer: dict[str, Any] = field(default_factory=dict)
intent: str | None = None
required_capabilities: list[str] = field(default_factory=list)
preferred_workers: list[str] = field(default_factory=list)
risk: str = "medium"
approval_policy: str = "patch-only"
dependencies: list[str] = field(default_factory=list)
max_delegation_depth: int = 5
budgets: Budget = field(default_factory=Budget)
evidence_requirements: list[str] = field(default_factory=list)
parent_task_id: str | None = None
correlation_id: str | None = None
metadata: dict[str, Any] = field(default_factory=dict)
data_classification: str = "internal"
execution_zone: str = "isolated-development"
network_policy: str = "controlled"
logging_policy: str = "redacted"
allowed_input_paths: list[str] = field(default_factory=list)
allowed_output_paths: list[str] = field(default_factory=list)
output_policy: str = "standard"
@classmethod
def from_dict(cls, data: dict[str, Any] | None) -> "TaskPlan":
if not isinstance(data, dict):
raise ValueError("task plan payload must be an object")
unknown = set(data) - TASK_PLAN_KEYS
if unknown:
raise ValueError("unknown task plan fields")
if "task_id" not in data or data["task_id"] is None:
data = dict(data)
data["task_id"] = f"task-{uuid.uuid4()}"
risk = str(data.get("risk", "medium"))
if risk not in RISK_LEVELS:
raise ValueError("invalid risk")
issuer = data.get("issuer", {})
if not isinstance(issuer, dict):
raise ValueError("issuer must be an object")
schema_version = int(data.get("schema_version", 1))
if schema_version != 1:
raise ValueError("unsupported schema_version")
max_depth = _to_positive_int(data.get("max_delegation_depth"), default=5)
if max_depth is None or max_depth > 32:
raise ValueError("invalid max_delegation_depth")
classification = str(data.get("data_classification", "internal"))
if classification not in DATA_CLASSIFICATIONS:
raise ValueError("invalid data_classification")
execution_zone = validate_type(
data.get("execution_zone", "isolated-development"), "execution_zone"
)
network_policy = validate_type(
data.get("network_policy", "controlled"), "network_policy"
)
logging_policy = validate_type(
data.get("logging_policy", "redacted"), "logging_policy"
)
output_policy = validate_type(
data.get("output_policy", "standard"), "output_policy"
)
if contains_secret_text(
{
"intent": data.get("intent"),
"metadata": data.get("metadata", {}),
}
):
raise ValueError("task content contains secret material")
if classification == "credential-secret":
if execution_zone != "secure-file-transformer":
raise ValueError("credential-secret requires secure-file-transformer")
if network_policy != "denied":
raise ValueError("credential-secret requires network denied")
if logging_policy != "metadata-only":
raise ValueError("credential-secret requires metadata-only logging")
if output_policy != "hash-count-status-only":
raise ValueError("credential-secret requires restricted output")
return cls(
schema_version=schema_version,
task_id=validate_identifier(data["task_id"], "task_id"),
task_type=validate_type(data.get("task_type", "code.change"), "task_type"),
issuer=issuer,
intent=data.get("intent"),
required_capabilities=_normalize_list(data.get("required_capabilities", [])),
preferred_workers=_normalize_list(data.get("preferred_workers", [])),
risk=risk,
approval_policy=validate_type(
data.get("approval_policy", "patch-only"), "approval_policy"
),
dependencies=_normalize_list(data.get("dependencies", [])),
max_delegation_depth=max_depth,
budgets=Budget.from_dict(data.get("budgets")),
evidence_requirements=_normalize_list(data.get("evidence_requirements", [])),
parent_task_id=data.get("parent_task_id"),
correlation_id=data.get("correlation_id"),
metadata=data.get("metadata", {}) if isinstance(data.get("metadata", {}), dict) else {},
data_classification=classification,
execution_zone=execution_zone,
network_policy=network_policy,
logging_policy=logging_policy,
allowed_input_paths=validate_relative_paths(
data.get("allowed_input_paths", []), "allowed_input_paths"
),
allowed_output_paths=validate_relative_paths(
data.get("allowed_output_paths", []), "allowed_output_paths"
),
output_policy=output_policy,
)
def to_dict(self, *, redact: bool = False) -> dict[str, Any]:
payload = {
"schema_version": self.schema_version,
"task_id": self.task_id,
"task_type": self.task_type,
"issuer": self.issuer,
"intent": self.intent,
"required_capabilities": list(self.required_capabilities),
"preferred_workers": list(self.preferred_workers),
"risk": self.risk,
"approval_policy": self.approval_policy,
"dependencies": list(self.dependencies),
"max_delegation_depth": self.max_delegation_depth,
"budgets": self.budgets.to_dict(),
"evidence_requirements": list(self.evidence_requirements),
"parent_task_id": self.parent_task_id,
"correlation_id": self.correlation_id,
"metadata": dict(self.metadata),
"data_classification": self.data_classification,
"execution_zone": self.execution_zone,
"network_policy": self.network_policy,
"logging_policy": self.logging_policy,
"allowed_input_paths": list(self.allowed_input_paths),
"allowed_output_paths": list(self.allowed_output_paths),
"output_policy": self.output_policy,
}
if redact:
payload = redact_value(payload)
return payload
def operation_digest(self) -> str:
return compute_sha256_hex(self.to_dict())
@dataclass
class TaskRecord:
task_id: str
status: str
revision: int
task_plan: TaskPlan
created_at: float
updated_at: float
root_task_id: str
idempotency_key: str | None = None
failure_reason: str | None = None
def with_status(self, status: str) -> "TaskRecord":
allowed = TASK_NEXT_STATES.get(self.status, set())
if status != self.status and (not allowed and self.status not in {"COMPLETED", "FAILED_FINAL", "CANCELLED"}):
raise TaskStateTransitionError(f"Cannot transition {self.status} -> {status}")
if status not in TASK_STATES:
raise TaskStateTransitionError(f"Invalid status {status}")
if status != self.status and self.status in TASK_NEXT_STATES and status not in TASK_NEXT_STATES[self.status]:
raise TaskStateTransitionError(f"Cannot transition {self.status} -> {status}")
return TaskRecord(
task_id=self.task_id,
status=status,
revision=self.revision + 1,
task_plan=self.task_plan,
created_at=self.created_at,
updated_at=_now_ts(),
root_task_id=self.root_task_id,
idempotency_key=self.idempotency_key,
failure_reason=self.failure_reason,
)
def to_dict(self, *, include_task=True, redact_payload: bool = True) -> dict[str, Any]:
payload = {
"task_id": self.task_id,
"status": self.status,
"revision": self.revision,
"root_task_id": self.root_task_id,
"parent_task_id": self.task_plan.parent_task_id,
"created_at": _fmt_ts(self.created_at),
"updated_at": _fmt_ts(self.updated_at),
"failure_reason": self.failure_reason,
}
if include_task:
payload["plan"] = self.task_plan.to_dict(redact=redact_payload)
if self.idempotency_key:
payload["idempotency_key"] = self.idempotency_key
return payload
@dataclass
class WorkerRegistration:
worker_id: str
worker_type: str
identity: str
capabilities: list[str]
trust_zone: str
max_concurrency: int
heartbeat_interval_seconds: int
supported_protocols: list[str]
registered_at: float
last_heartbeat: float
status: str = "active"
disabled: bool = False
owner_identity: str = ""
registration_revision: int = 1
registration_policy: str = "self"
created_at: float = 0.0
updated_at: float = 0.0
disabled_reason: str | None = None
@classmethod
def from_dict(cls, data: dict[str, Any]) -> "WorkerRegistration":
if not isinstance(data, dict):
raise ValueError("worker registration payload must be an object")
now = _now_ts()
worker_id = validate_identifier(data.get("worker_id"), "worker_id")
worker_type = validate_type(data.get("worker_type"), "worker_type")
identity = str(data.get("identity") or "").strip()
if not identity or any(ord(ch) < 32 for ch in identity):
raise ValueError("invalid identity")
max_concurrency = _to_positive_int(data.get("max_concurrency"), default=1)
heartbeat_interval = _to_positive_int(
data.get("heartbeat_interval_seconds"), default=15
)
if not max_concurrency:
raise ValueError("max_concurrency must be positive")
if not heartbeat_interval:
raise ValueError("heartbeat_interval_seconds must be positive")
return cls(
worker_id=worker_id,
worker_type=worker_type,
identity=identity,
capabilities=_normalize_list(data.get("capabilities", [])),
trust_zone=str(data.get("trust_zone") or "default"),
max_concurrency=max_concurrency,
heartbeat_interval_seconds=heartbeat_interval,
supported_protocols=_normalize_list(
data.get("supported_protocols", ["aios-task-v1"])
),
registered_at=float(data.get("registered_at") or now),
last_heartbeat=float(data.get("last_heartbeat") or now),
status=str(data.get("status") or "active"),
disabled=bool(data.get("disabled", False)),
owner_identity=str(data.get("owner_identity") or identity),
registration_revision=int(data.get("registration_revision") or 1),
registration_policy=str(data.get("registration_policy") or "self"),
created_at=float(data.get("created_at") or data.get("registered_at") or now),
updated_at=float(data.get("updated_at") or now),
disabled_reason=data.get("disabled_reason"),
)
def to_dict(self, redact_payload: bool = True) -> dict[str, Any]:
payload = {
"worker_id": self.worker_id,
"worker_type": self.worker_type,
"identity": self.identity,
"capabilities": list(self.capabilities),
"trust_zone": self.trust_zone,
"max_concurrency": self.max_concurrency,
"heartbeat_interval_seconds": self.heartbeat_interval_seconds,
"supported_protocols": list(self.supported_protocols),
"registered_at": _fmt_ts(self.registered_at),
"last_heartbeat": _fmt_ts(self.last_heartbeat),
"status": self.status,
"disabled": self.disabled,
"owner_identity": self.owner_identity,
"registration_revision": self.registration_revision,
"registration_policy": self.registration_policy,
"created_at": _fmt_ts(self.created_at or self.registered_at),
"updated_at": _fmt_ts(self.updated_at or self.last_heartbeat),
"disabled_reason": self.disabled_reason,
}
if redact_payload:
payload = redact_value(payload)
return payload
@dataclass(frozen=True)
class EvidenceEnvelope:
schema_version: int
evidence_id: str
task_id: str
claim_id: str
kind: str
worker_identity: str
payload: dict[str, Any]
created_at: float
previous_evidence_hash: str
evidence_digest: str
signature: str | None = None
attestation_ref: str | None = None
@classmethod
def create(
cls,
*,
task_id: str,
claim_id: str,
kind: str,
worker_identity: str,
payload: dict[str, Any],
previous_evidence_hash: str,
signature: str | None = None,
attestation_ref: str | None = None,
evidence_id: str | None = None,
created_at: float | None = None,
) -> "EvidenceEnvelope":
envelope = cls(
schema_version=1,
evidence_id=evidence_id or f"evd-{uuid.uuid4()}",
task_id=validate_identifier(task_id, "task_id"),
claim_id=validate_identifier(claim_id, "claim_id"),
kind=validate_type(kind, "evidence kind"),
worker_identity=str(worker_identity),
payload=redact_value(payload),
created_at=float(created_at or _now_ts()),
previous_evidence_hash=str(previous_evidence_hash or ""),
evidence_digest="",
signature=signature,
attestation_ref=attestation_ref,
)
return dataclass_replace(
envelope, evidence_digest=compute_sha256_hex(envelope.digest_payload())
)
def digest_payload(self) -> dict[str, Any]:
return {
"schema_version": self.schema_version,
"evidence_id": self.evidence_id,
"task_id": self.task_id,
"claim_id": self.claim_id,
"kind": self.kind,
"worker_identity": self.worker_identity,
"payload": self.payload,
"created_at": self.created_at,
"previous_evidence_hash": self.previous_evidence_hash,
"attestation_ref": self.attestation_ref,
}
def verify(
self,
*,
expected_task_id: str,
expected_claim_id: str,
expected_previous_hash: str,
attestation_verifier: Any = None,
) -> bool:
if (
self.schema_version != 1
or self.task_id != expected_task_id
or self.claim_id != expected_claim_id
or self.previous_evidence_hash != expected_previous_hash
or self.evidence_digest != compute_sha256_hex(self.digest_payload())
):
return False
if self.signature or self.attestation_ref:
return True if attestation_verifier is None else bool(attestation_verifier(self))
return True
def to_dict(self) -> dict[str, Any]:
return {
**self.digest_payload(),
"evidence_digest": self.evidence_digest,
"signature": self.signature,
}
class ProbeOutcome(StrEnum):
NOT_APPLIED = "NOT_APPLIED"
APPLIED_VERIFIED = "APPLIED_VERIFIED"
INDETERMINATE = "INDETERMINATE"
CONFLICT = "CONFLICT"
@dataclass(frozen=True)
class ProbeEvidence:
schema_version: int
probe_id: str
task_id: str
claim_id: str
worker_id: str
worker_identity: str
operation_digest: str
outcome: ProbeOutcome
observed_at: float
expires_at: float
probe_executor_identity: str
evidence_digest: str
signature: str | None = None
attestation_ref: str | None = None
@classmethod
def create(cls, **data: Any) -> "ProbeEvidence":
outcome = data["outcome"]
if not isinstance(outcome, ProbeOutcome):
outcome = ProbeOutcome(str(outcome))
probe = cls(
schema_version=1,
probe_id=data.get("probe_id") or f"probe-{uuid.uuid4()}",
task_id=validate_identifier(data["task_id"], "task_id"),
claim_id=validate_identifier(data["claim_id"], "claim_id"),
worker_id=validate_identifier(data["worker_id"], "worker_id"),
worker_identity=str(data["worker_identity"]),
operation_digest=str(data["operation_digest"]),
outcome=outcome,
observed_at=float(data["observed_at"]),
expires_at=float(data["expires_at"]),
probe_executor_identity=str(data["probe_executor_identity"]),
evidence_digest="",
signature=data.get("signature"),
attestation_ref=data.get("attestation_ref"),
)
return dataclass_replace(
probe, evidence_digest=compute_sha256_hex(probe.digest_payload())
)
def digest_payload(self) -> dict[str, Any]:
return {
"schema_version": self.schema_version,
"probe_id": self.probe_id,
"task_id": self.task_id,
"claim_id": self.claim_id,
"worker_id": self.worker_id,
"worker_identity": self.worker_identity,
"operation_digest": self.operation_digest,
"outcome": self.outcome.value,
"observed_at": self.observed_at,
"expires_at": self.expires_at,
"probe_executor_identity": self.probe_executor_identity,
"attestation_ref": self.attestation_ref,
}
def verify(self, *, now: float, attestation_verifier: Any) -> bool:
if (
self.schema_version != 1
or self.expires_at <= now
or self.observed_at > now
or self.evidence_digest != compute_sha256_hex(self.digest_payload())
or not (self.signature or self.attestation_ref)
):
return False
return bool(attestation_verifier and attestation_verifier(self))
def to_dict(self) -> dict[str, Any]:
return {
**self.digest_payload(),
"evidence_digest": self.evidence_digest,
"signature": self.signature,
}
def dataclass_replace(value: Any, **changes: Any) -> Any:
from dataclasses import replace
return replace(value, **changes)
@dataclass
class WorkerClaim:
claim_id: str
task_id: str
worker_id: str
worker_identity: str
claimed_at: float
lease_until: float
attempt: int
task_revision: int
operation_digest: str
status: str = "ACTIVE"
def to_dict(self, redact_payload: bool = True) -> dict[str, Any]:
payload = {
"claim_id": self.claim_id,
"task_id": self.task_id,
"worker_id": self.worker_id,
"worker_identity": self.worker_identity,
"claimed_at": _fmt_ts(self.claimed_at),
"lease_until": _fmt_ts(self.lease_until),
"attempt": self.attempt,
"task_revision": self.task_revision,
"operation_digest": self.operation_digest,
"status": self.status,
}
if redact_payload:
payload = redact_value(payload)
return payload
@dataclass
class ResultEnvelope:
task_id: str
claim_id: str
worker_identity: str
status: str
outputs: list[dict[str, Any]]
evidence: list[dict[str, Any]]
started_at: str
completed_at: str
result_digest: str | None = None
signature: str | None = None
attestation_ref: str | None = None
operation_digest: str | None = None
trust_status: str = "untrusted_shadow_result"
def digest_payload(self) -> dict[str, Any]:
return {
"task_id": self.task_id,
"claim_id": self.claim_id,
"worker_identity": self.worker_identity,
"status": self.status,
"outputs": self.outputs,
"evidence": self.evidence,
"started_at": self.started_at,
"completed_at": self.completed_at,
"operation_digest": self.operation_digest,
"attestation_ref": self.attestation_ref,
}
def computed_digest(self) -> str:
return compute_sha256_hex(self.digest_payload())
def to_dict(self, redact_payload: bool = True) -> dict[str, Any]:
payload = {
"task_id": self.task_id,
"claim_id": self.claim_id,
"worker_identity": self.worker_identity,
"status": self.status,
"outputs": self.outputs,
"evidence": self.evidence,
"started_at": self.started_at,
"completed_at": self.completed_at,
"result_digest": self.result_digest,
"signature": self.signature,
"attestation_ref": self.attestation_ref,
"operation_digest": self.operation_digest,
"trust_status": self.trust_status,
}
if redact_payload:
payload = redact_value(payload)
return payload
def _normalize_list(value: Any) -> list[str]:
if not isinstance(value, list):
raise ValueError("expected list")
if len(value) > 128 or any(
not isinstance(v, str)
or not v
or len(v) > 256
or any(ord(ch) < 32 for ch in v)
for v in value
):
raise ValueError("invalid list item")
return list(value)
def _to_positive_int(value: Any, default: int | None = None) -> int | None:
if value is None:
return default
try:
number = int(value)
except (TypeError, ValueError):
return default
return number if number >= 0 else default
def _to_float(value: Any) -> float | None:
if value is None:
return None
try:
return float(value)
except (TypeError, ValueError):
return None
def _first_non_none(primary: Any, fallback: Any) -> Any:
if primary is not None:
return primary
return fallback
def iso8601_now() -> str:
return datetime.now(timezone.utc).replace(microsecond=0).isoformat().replace("+00:00", "Z")
def _fmt_ts(value: float | None) -> str | None:
if value is None:
return None
return datetime.fromtimestamp(value, tz=timezone.utc).replace(microsecond=0).isoformat().replace("+00:00", "Z")
+166
View File
@@ -0,0 +1,166 @@
"""Policy and verification logic (fail-closed)."""
from __future__ import annotations
from dataclasses import dataclass
from typing import Callable
from .errors import ForbiddenError, LockedError
from .models import ProbeEvidence, ResultEnvelope, TaskRecord
RISK_RANK = {"low": 1, "medium": 2, "high": 3}
ALL_ACTION_CAPABILITIES = frozenset(
{
"task.creator",
"worker.self_register",
"worker.admin",
"task.claim",
"task.cancel",
"task.delegate",
"task.read",
"task.audit",
}
)
@dataclass
class PolicyDecision:
allowed: bool
reason: str = ""
class IdentityVerifier:
def __init__(self, trusted_identities: set[str] | None = None):
self.trusted_identities = trusted_identities
def verify(self, identity: str) -> bool:
if self.trusted_identities is None:
return True
return identity in self.trusted_identities
class EvidenceVerifier:
def __init__(self, mandatory: list[str] | None = None):
self.mandatory = set(mandatory or [])
def verify(self, task: TaskRecord, evidences: list[dict]) -> PolicyDecision:
required = set(task.task_plan.evidence_requirements or [])
if not required and not self.mandatory:
return PolicyDecision(True)
observed = {e.get("kind") for e in evidences}
missing = required.union(self.mandatory) - observed
if missing:
return PolicyDecision(False, f"missing evidence: {', '.join(sorted(missing))}")
return PolicyDecision(True)
class PolicyEngine:
"""
Fail-closed policy engine with minimal but explicit decisions.
"""
def __init__(
self,
*,
identity_verifier: IdentityVerifier | None = None,
evidence_verifier: EvidenceVerifier | None = None,
execution_enabled: bool = False,
max_risk: str = "medium",
result_signature_verifier: Callable[[ResultEnvelope], bool] | None = None,
probe_attestation_verifier: Callable[[ProbeEvidence], bool] | None = None,
identity_capabilities: dict[str, set[str]] | None = None,
admin_identities: set[str] | None = None,
) -> None:
if max_risk not in RISK_RANK:
raise ValueError("invalid max_risk")
self.identity_verifier = identity_verifier or IdentityVerifier()
self.evidence_verifier = evidence_verifier or EvidenceVerifier()
self.execution_enabled = execution_enabled
self.max_risk = max_risk
self.result_signature_verifier = result_signature_verifier
self.probe_attestation_verifier = probe_attestation_verifier
self.identity_capabilities = {
identity: set(capabilities)
for identity, capabilities in (identity_capabilities or {}).items()
}
self.admin_identities = set(admin_identities or ())
def verify_result(
self, result: ResultEnvelope, task: TaskRecord | None = None
) -> PolicyDecision:
computed = result.computed_digest()
if result.result_digest and result.result_digest != computed:
return PolicyDecision(False, "result digest mismatch")
sensitive = bool(
task
and (
task.task_plan.data_classification
in {"confidential", "credential-secret"}
or any(
capability.startswith(
("code.write", "deploy", "secret", "production")
)
for capability in task.task_plan.required_capabilities
)
)
)
if sensitive and not (result.signature or result.attestation_ref):
return PolicyDecision(False, "trusted result proof required")
if result.signature is not None or result.attestation_ref is not None:
if self.result_signature_verifier is None:
return PolicyDecision(False, "result signature verifier unavailable")
if not self.result_signature_verifier(result):
return PolicyDecision(False, "result signature invalid")
result.trust_status = "trusted_result"
else:
result.trust_status = "untrusted_shadow_result"
return PolicyDecision(True)
def authorize(self, action: str, actor: str, task: TaskRecord | None = None, context: dict | None = None) -> PolicyDecision:
context = context or {}
if not self.identity_verifier.verify(actor):
return PolicyDecision(False, "identity untrusted")
action_capabilities = {
"create": "task.creator",
"read": "task.read",
"audit": "task.audit",
"claim": "task.claim",
"result": "task.claim",
"fail": "task.claim",
"progress": "task.claim",
"heartbeat": "task.claim",
"cancel": "task.cancel",
"delegate": "task.delegate",
"worker.register": "worker.self_register",
"worker.admin": "worker.admin",
}
required_capability = action_capabilities.get(action)
if required_capability:
granted = set(
self.identity_capabilities.get(
actor, self.identity_capabilities.get("*", set())
)
)
if required_capability not in granted:
return PolicyDecision(False, f"action {action} not permitted")
if action in {"claim", "result", "fail", "cancel", "progress", "delegate", "heartbeat"} and not self.execution_enabled:
return PolicyDecision(False, "execution disabled")
if action == "delegate" and task and task.status not in {"COMPLETED", "FAILED_FINAL", "CANCELLED"}:
risk = str(context.get("risk", task.task_plan.risk))
if RISK_RANK.get(risk, 2) > RISK_RANK.get(self.max_risk, 1):
return PolicyDecision(False, f"risk {risk} exceeds policy")
return PolicyDecision(True)
def assert_allowed(self, action: str, actor: str, task: TaskRecord | None = None, context: dict | None = None):
decision = self.authorize(action, actor, task, context=context)
if not decision.allowed:
if decision.reason == "identity untrusted":
raise ForbiddenError(decision.reason)
if decision.reason == "execution disabled":
raise LockedError("execution disabled")
raise ForbiddenError(decision.reason)
@@ -0,0 +1,76 @@
"""Task scheduler primitives."""
from __future__ import annotations
import time
from .errors import NotFoundError
from .capabilities import has_capabilities
from .models import WorkerClaim
from .store import TaskPlaneStore
class TaskScheduler:
def __init__(self, store: TaskPlaneStore):
self.store = store
def next_queued_task(self, worker_id: str):
worker = self.store.get_worker(worker_id)
tasks = sorted(
self.store.list_tasks(),
key=lambda task: (
-int(task.task_plan.metadata.get("priority", 0)),
task.created_at,
task.task_id,
),
)
for task in tasks:
if task.status not in {"CREATED", "QUEUED", "FAILED_RECOVERABLE"}:
continue
if task.task_plan.task_type == "worker.registry.audit":
continue
if task.status == "FAILED_RECOVERABLE" and not self.store.task_has_retryable_probe(
task.task_id
):
continue
if not has_capabilities(
worker.capabilities, task.task_plan.required_capabilities
):
continue
if any(
self.store.get_task(dependency).status != "COMPLETED"
for dependency in task.task_plan.dependencies
):
continue
if (
task.task_plan.data_classification == "credential-secret"
and worker.worker_type != "secure_file_transformer"
and "secure_file_transformer" not in worker.capabilities
):
continue
return task
return None
def schedule_for_worker(self, worker_id: str, lease_seconds: int = 15) -> WorkerClaim:
worker = self.store.get_worker(worker_id)
if worker.disabled:
from .errors import LockedError
raise LockedError("worker disabled")
if time.time() > worker.last_heartbeat + (
worker.heartbeat_interval_seconds * 3
):
from .errors import LockedError
raise LockedError("worker offline")
task = self.next_queued_task(worker_id)
if task is None:
raise NotFoundError("no queued tasks available")
claim = self.store.claim_task(
task.task_id,
worker_id,
worker.identity,
lease_seconds=lease_seconds,
expected_revision=task.revision,
operation_digest=task.task_plan.operation_digest(),
)
return claim
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,68 @@
"""Worker registry abstraction and read helpers."""
from __future__ import annotations
import time
from .capabilities import has_capabilities
from .errors import NotFoundError
from .models import WorkerRegistration
from .store import TaskPlaneStore
class WorkerRegistry:
def __init__(self, store: TaskPlaneStore):
self.store = store
def register(
self,
payload: dict,
*,
actor_identity: str,
is_admin: bool,
expected_revision: int | None,
) -> WorkerRegistration:
reg = WorkerRegistration.from_dict(payload)
return self.store.register_worker(
reg,
actor_identity=actor_identity,
is_admin=is_admin,
expected_revision=expected_revision,
)
def heartbeat(
self,
worker_id: str,
heartbeat_interval_seconds: int | None = None,
*,
actor_identity: str,
expected_revision: int | None,
) -> WorkerRegistration:
return self.store.heartbeat_worker(
worker_id,
heartbeat_interval_seconds=heartbeat_interval_seconds,
actor_identity=actor_identity,
expected_revision=expected_revision,
)
def list(self):
return self.store.list_workers()
def get(self, worker_id: str) -> WorkerRegistration:
try:
return self.store.get_worker(worker_id)
except NotFoundError:
raise
def can_execute(self, worker_id: str, required_capabilities: list[str]) -> bool:
worker = self.store.get_worker(worker_id)
return has_capabilities(worker.capabilities, required_capabilities)
def is_online(self, worker_id: str, *, now: float | None = None) -> bool:
worker = self.store.get_worker(worker_id)
deadline = worker.last_heartbeat + (worker.heartbeat_interval_seconds * 3)
return not worker.disabled and (time.time() if now is None else now) <= deadline
def stale_workers(self, *, now: float | None = None) -> list[WorkerRegistration]:
current = time.time() if now is None else now
return [worker for worker in self.list() if not self.is_online(worker.worker_id, now=current)]
+42
View File
@@ -0,0 +1,42 @@
# AIOS Task Plane
该模块是与任务发起方、Worker 类型和通知后端解耦的持久化任务控制平面。当前交付仅用于隔离开发与 Shadow 验收;默认 `execution_enabled=false`,不会修改生产、tag、Baseline 或 Registry。
## 架构
- `TaskPlaneService`:统一服务边界和 HTTP 路由。
- `TaskPlaneStore`:SQLite WAL、revision/CAS、串行事务、租约、hash-chained journal、evidence 和 durable outbox。
- `WorkerRegistry` / `LeaseManager`:身份绑定、capability、并发额度、心跳、过期探测后恢复。
- `DelegationEngine`:父子 DAG、全局深度、capability/risk/approval/budget 单调约束和幂等重放。
- `PolicyEngine`:默认关闭执行;身份、证据、结果摘要和可插拔签名验签均 fail-closed。
- `WorkerAdapter`:OpenClaw、Codex、Hermes、A2A 和 systemd Worker 的统一契约。默认实现只返回 `SHADOW_VERIFIED`,不执行外部命令。
- `DurableOutboxDispatcher`:Webhook、NATS JetStream、GPT callback、Dashboard 和 WeChat 的回执式通知接口。没有真实回执时始终保留 `NOT_DELIVERED`。
## 状态与恢复
正常路径为 `CREATED → QUEUED/CLAIMED → RUNNING → VERIFYING/COMPLETED`;失败路径使用 `FAILED_RECOVERABLE`、`FAILED_FINAL`、`CANCELLED` 或 `MANUAL_REVIEW_REQUIRED`。每次状态事件都进入同一事务中的 append-only hash chain。
租约过期不会自动重放副作用。控制平面必须先调用 Worker probe,只有明确确认原执行已停止,才可将 claim 标记为 `EXPIRED` 并把任务转为 `FAILED_RECOVERABLE`。结果必须匹配 task、claim、Worker identity、未过期 lease 和 operation digest;重复结果返回冲突。
## 安全模型
- task/worker/claim/message ID 使用 allowlist pattern,拒绝 URL 编码差异、路径分隔符和控制字符。
- HTTP body 最大 1 MB,非对象 JSON、异常 Content-Length 和非法参数返回 400/413。
- 内部异常响应不回显路径、payload 或凭据;默认 HTTP access log 不记录原始 request target。
- secret-like 字段在 durable task、event、evidence 和 outbox 写入前脱敏。
- 结果签名存在但没有配置验签器时拒绝;摘要、evidence 或 event 被篡改时验收失败。
- SQLite 文件创建为 0600;新建状态目录使用 0700。
## 验收
```bash
PYTHONPATH=active-runtime \
bash scripts/task-plane/run_task_plane_acceptance.sh
PYTHONPATH=active-runtime \
python3 scripts/task-plane/create_shadow_tasks.py \
--database reports/task-plane-shadow.sqlite3 \
--report reports/task-plane-shadow.json
```
上述脚本只使用本地 SQLite 和 Shadow adapter,不触发真实 OpenClaw、Codex、Hermes、微信或生产执行。
+314
View File
@@ -0,0 +1,314 @@
openapi: 3.0.3
info:
title: AIOS Task Plane API
version: 1.0.0
description: >
Durable control-plane APIs for task orchestration with fail-closed policy.
Every POST operation requires bearerAuth; read-only GET operations may be
exposed according to the surrounding runtime policy.
servers:
- url: /
paths:
/tasks:
get:
summary: List tasks
responses:
"200":
description: Task list
post:
security: [ { bearerAuth: [] } ]
summary: Create task
parameters:
- in: header
name: Idempotency-Key
required: false
schema:
type: string
minLength: 1
maxLength: 128
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/TaskPlan"
responses:
"201": { description: Created }
"400": { description: Bad request }
"401": { description: Unauthorized }
"403": { description: Forbidden }
"409": { description: Conflict }
"413": { description: Payload too large }
"423": { description: Execution or resource locked }
"503": { description: Authentication or service unavailable }
/tasks/{task_id}:
get:
summary: Get task
parameters:
- $ref: "#/components/parameters/TaskId"
responses:
"200": { description: OK }
"404": { description: Not found }
/tasks/{task_id}/claim:
post:
security: [ { bearerAuth: [] } ]
summary: Claim a task
parameters:
- $ref: "#/components/parameters/TaskId"
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/TaskClaimRequest"
responses:
"200": { description: Claimed }
"401": { description: Unauthorized }
"403": { description: Forbidden }
"404": { description: Not found }
"409": { description: Conflict }
"423": { description: Execution disabled }
/tasks/{task_id}/heartbeat:
post:
security: [ { bearerAuth: [] } ]
summary: Renew task claim lease
parameters: [ $ref: "#/components/parameters/TaskId" ]
responses:
"200": { description: Leased }
"400": { description: Bad request }
"401": { description: Unauthorized }
"403": { description: Forbidden }
"404": { description: Task or claim not found }
"409": { description: Claim conflict }
"413": { description: Payload too large }
"423": { description: Lease or execution locked }
"503": { description: Service unavailable }
/tasks/{task_id}/progress:
post:
security: [ { bearerAuth: [] } ]
summary: Report task progress
parameters: [ $ref: "#/components/parameters/TaskId" ]
responses:
"200": { description: Progress saved }
"400": { description: Bad request }
"401": { description: Unauthorized }
"403": { description: Forbidden }
"404": { description: Task or claim not found }
"409": { description: Claim conflict }
"413": { description: Payload too large }
"423": { description: Lease or execution locked }
"503": { description: Service unavailable }
/tasks/{task_id}/result:
post:
security: [ { bearerAuth: [] } ]
summary: Submit task result
parameters: [ $ref: "#/components/parameters/TaskId" ]
responses:
"200": { description: Result accepted }
"400": { description: Bad request }
"401": { description: Unauthorized }
"403": { description: Result policy rejected }
"404": { description: Task or claim not found }
"409": { description: Duplicate or mismatched result }
"413": { description: Payload too large }
"423": { description: Lease or execution locked }
"503": { description: Verification service unavailable }
/tasks/{task_id}/fail:
post:
security: [ { bearerAuth: [] } ]
summary: Mark task failed
parameters: [ $ref: "#/components/parameters/TaskId" ]
responses:
"200": { description: Failure recorded }
"400": { description: Bad request }
"401": { description: Unauthorized }
"403": { description: Forbidden }
"404": { description: Task or claim not found }
"409": { description: Claim conflict }
"413": { description: Payload too large }
"423": { description: Lease or execution locked }
"503": { description: Service unavailable }
/tasks/{task_id}/cancel:
post:
security: [ { bearerAuth: [] } ]
summary: Cancel task
parameters: [ $ref: "#/components/parameters/TaskId" ]
responses:
"200": { description: Cancelled }
"400": { description: Bad request }
"401": { description: Unauthorized }
"403": { description: Forbidden }
"404": { description: Task not found }
"409": { description: Revision or state conflict }
"413": { description: Payload too large }
"423": { description: Resource locked }
"503": { description: Service unavailable }
/tasks/{task_id}/delegate:
post:
security: [ { bearerAuth: [] } ]
summary: Delegate task
parameters: [ $ref: "#/components/parameters/TaskId" ]
responses:
"200": { description: Delegated }
"400": { description: Bad request }
"401": { description: Unauthorized }
"403": { description: Forbidden }
"404": { description: Parent task not found }
"409": { description: Conflict }
"413": { description: Payload too large }
"423": { description: Execution locked }
"503": { description: Service unavailable }
/workers/register:
post:
security: [ { bearerAuth: [] } ]
summary: Register worker
responses:
"201": { description: Worker registered }
"400": { description: Bad request }
"401": { description: Unauthorized }
"403": { description: Worker identity mismatch }
"409": { description: Registration conflict }
"413": { description: Payload too large }
"423": { description: Resource locked }
"503": { description: Service unavailable }
/workers/{worker_id}/heartbeat:
post:
security: [ { bearerAuth: [] } ]
summary: Worker heartbeat
parameters: [ $ref: "#/components/parameters/WorkerId" ]
responses:
"200": { description: Heartbeat accepted }
"400": { description: Bad request }
"401": { description: Unauthorized }
"403": { description: Forbidden }
"404": { description: Worker not found }
"409": { description: Worker state conflict }
"413": { description: Payload too large }
"423": { description: Worker locked }
"503": { description: Service unavailable }
/workers:
get:
summary: List workers
responses:
"200": { description: Worker list }
/workers/{worker_id}:
get:
summary: Get worker
parameters: [ $ref: "#/components/parameters/WorkerId" ]
responses:
"200": { description: Worker details }
/tasks/{task_id}/events:
get:
summary: Task events
parameters: [ $ref: "#/components/parameters/TaskId" ]
responses:
"200": { description: Event list }
/tasks/{task_id}/evidence:
get:
summary: Task evidence
parameters: [ $ref: "#/components/parameters/TaskId" ]
responses:
"200": { description: Evidence list }
/tasks/{task_id}/children:
get:
summary: Task child tasks
parameters: [ $ref: "#/components/parameters/TaskId" ]
responses:
"200": { description: Child list }
components:
securitySchemes:
bearerAuth:
type: http
scheme: bearer
bearerFormat: opaque
parameters:
TaskId:
name: task_id
in: path
required: true
schema:
type: string
minLength: 1
maxLength: 128
pattern: '^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$'
WorkerId:
name: worker_id
in: path
required: true
schema:
type: string
minLength: 1
maxLength: 128
pattern: '^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$'
schemas:
TaskPlan:
type: object
additionalProperties: false
properties:
schema_version: { type: integer, enum: [1] }
task_id:
type: string
minLength: 1
maxLength: 128
pattern: '^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$'
task_type:
type: string
minLength: 1
maxLength: 128
pattern: '^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$'
issuer: { type: object }
intent: { type: string, maxLength: 16384 }
required_capabilities:
type: array
maxItems: 128
items: { type: string }
preferred_workers:
type: array
maxItems: 64
items: { type: string }
risk: { type: string, enum: [low, medium, high] }
approval_policy:
type: string
maxLength: 128
dependencies:
type: array
maxItems: 128
items: { type: string }
max_delegation_depth: { type: integer, minimum: 0, maximum: 32 }
budgets:
type: object
additionalProperties: false
properties:
time_seconds: { type: integer, minimum: 0 }
attempts: { type: integer, minimum: 0 }
tokens: { type: integer, minimum: 0 }
cost: { type: number, minimum: 0 }
evidence_requirements:
type: array
maxItems: 128
items: { type: string }
parent_task_id:
type: string
maxLength: 128
correlation_id:
type: string
maxLength: 128
metadata:
type: object
TaskClaimRequest:
type: object
additionalProperties: false
required: [worker_id, actor_identity]
properties:
worker_id:
type: string
maxLength: 128
pattern: '^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$'
lease_seconds:
type: integer
minimum: 1
maximum: 3600
actor_identity:
type: string
minLength: 1
maxLength: 512
@@ -0,0 +1,9 @@
{
"type": "branch",
"branch": "patch/aios-agent-task-control-plane-20260724",
"base": "main",
"created_by": "agent-task-plane-writer",
"created_at": "2026-07-24T02:20:56Z",
"direct_main_push": false,
"owner_approval_required": true
}
+10
View File
@@ -0,0 +1,10 @@
{
"execution_status": "PARTIALLY_COMPLETED",
"tests_run": 19,
"failures": 0,
"errors": 0,
"skipped": 0,
"multi_agent_shadow_verified": true,
"production_side_effects": false,
"notification_delivery_status": "NOT_DELIVERED"
}
+53
View File
@@ -0,0 +1,53 @@
{
"root_task_id": "AIOS-AGENT-TASK-CONTROL-PLANE-20260724-SHADOW",
"execution_mode": "shadow_adapter",
"production_side_effects": false,
"tasks": [
{
"task_id": "AIOS-TASK-PLANE-OPENCLAW-ACCEPTANCE",
"parent_task_id": "AIOS-AGENT-TASK-CONTROL-PLANE-20260724-SHADOW",
"task_status": "COMPLETED",
"worker_result": {
"worker": "openclaw",
"task_id": "AIOS-TASK-PLANE-OPENCLAW-ACCEPTANCE",
"status": "SHADOW_VERIFIED",
"claim_id": "claim-093cbf4c-11d5-47a3-97dc-a373a8a53940",
"execution_mode": "shadow",
"production_side_effects": false
},
"result_digest": "b3a8ed4e0a60a0a9364d1eea1a6d30a403da1fb5ff79ff38a489a52227f4116e"
},
{
"task_id": "AIOS-TASK-PLANE-CODEX-REVIEW",
"parent_task_id": "AIOS-AGENT-TASK-CONTROL-PLANE-20260724-SHADOW",
"task_status": "COMPLETED",
"worker_result": {
"worker": "codex",
"task_id": "AIOS-TASK-PLANE-CODEX-REVIEW",
"status": "SHADOW_VERIFIED",
"claim_id": "claim-3cfe4031-27c7-409c-9841-cd5c0d26218d",
"execution_mode": "shadow",
"production_side_effects": false
},
"result_digest": "65fb825869b9b32d923b387796839e57b9ca7053716720861988ceb8d7e9c93a"
},
{
"task_id": "AIOS-TASK-PLANE-HERMES-ARCHITECTURE",
"parent_task_id": "AIOS-AGENT-TASK-CONTROL-PLANE-20260724-SHADOW",
"task_status": "COMPLETED",
"worker_result": {
"worker": "hermes",
"task_id": "AIOS-TASK-PLANE-HERMES-ARCHITECTURE",
"status": "SHADOW_VERIFIED",
"claim_id": "claim-fd7ba481-ab71-47dc-8e91-3655cbef1c40",
"execution_mode": "shadow",
"production_side_effects": false
},
"result_digest": "09c9ce563424b6490b7be1e854d06de786bd243a48b099137a7a2e9cb7035265"
}
],
"event_chains_valid": true,
"notification_delivery_status": [
"NOT_DELIVERED"
]
}
+1
View File
@@ -0,0 +1 @@
{"schema_version":1,"task_id":"AIOS-AGENT-TASK-CONTROL-PLANE-20260724","writer_status":"READY","production_side_effects":false}
@@ -0,0 +1,64 @@
#!/usr/bin/env python3
"""Deterministic, local-only fault injection for Task Plane recovery."""
from __future__ import annotations
import argparse
import json
import time
from unittest import mock
from app.task_plane.api import TaskPlaneService
from app.task_plane.policy import PolicyEngine
from app.task_plane.store import TaskPlaneStore
def main() -> int:
parser = argparse.ArgumentParser()
parser.add_argument("--database", required=True)
args = parser.parse_args()
store = TaskPlaneStore(args.database)
service = TaskPlaneService(
store, policy=PolicyEngine(execution_enabled=True)
)
identity = "spiffe://aios/agent/codex/fault-injection"
service.register_worker(
{
"worker_id": "codex-fault-injection",
"worker_type": "codex",
"identity": identity,
"capabilities": ["test.execute"],
"trust_zone": "isolated-shadow",
"max_concurrency": 1,
"heartbeat_interval_seconds": 1,
"supported_protocols": ["aios-task-v1"],
}
)
task = service.create_task(
{"task_id": "task-fault-expired-lease", "required_capabilities": ["test.execute"]}
)
claim = service.claim(
task["task_id"],
{
"worker_id": "codex-fault-injection",
"actor_identity": identity,
"lease_seconds": 1,
},
)
with mock.patch("app.task_plane.store._now", return_value=time.time() + 5):
recovered = store.recover_expired_claim(
task["task_id"], claim["claim_id"], probe_confirms_stopped=True
)
output = {
"fault": "expired_lease",
"recovered_status": recovered.status,
"event_chain_valid": store.verify_event_chain(task["task_id"]),
"production_side_effects": False,
}
print(json.dumps(output, sort_keys=True))
store.close()
return 0
if __name__ == "__main__":
raise SystemExit(main())
+168
View File
@@ -0,0 +1,168 @@
#!/usr/bin/env python3
"""Create and complete the three authorized no-side-effect Shadow tasks."""
from __future__ import annotations
import argparse
import json
from pathlib import Path
from app.task_plane.adapters.codex import CodexAdapter
from app.task_plane.adapters.hermes import HermesAdapter
from app.task_plane.adapters.openclaw import OpenClawAdapter
from app.task_plane.api import TaskPlaneService
from app.task_plane.policy import PolicyEngine
from app.task_plane.store import TaskPlaneStore
TASKS = (
(
"AIOS-TASK-PLANE-OPENCLAW-ACCEPTANCE",
"platform.acceptance",
"openclaw",
"spiffe://aios/agent/openclaw/shadow-01",
["test.execute", "runtime.shadow"],
OpenClawAdapter,
),
(
"AIOS-TASK-PLANE-CODEX-REVIEW",
"code.review",
"codex",
"spiffe://aios/agent/codex/shadow-01",
["code.read", "review.security", "test.python"],
CodexAdapter,
),
(
"AIOS-TASK-PLANE-HERMES-ARCHITECTURE",
"architecture.review",
"hermes",
"spiffe://aios/agent/hermes/shadow-01",
["architecture.review", "policy.review", "threat.model"],
HermesAdapter,
),
)
def main() -> int:
parser = argparse.ArgumentParser()
parser.add_argument("--database", required=True)
parser.add_argument("--report", required=True)
args = parser.parse_args()
store = TaskPlaneStore(args.database)
service = TaskPlaneService(
store, policy=PolicyEngine(execution_enabled=True, max_risk="medium")
)
union_capabilities = sorted({cap for item in TASKS for cap in item[4]})
root = service.create_task(
{
"task_id": "AIOS-AGENT-TASK-CONTROL-PLANE-20260724-SHADOW",
"task_type": "platform.shadow-orchestration",
"required_capabilities": union_capabilities,
"risk": "medium",
"approval_policy": "patch-only",
"max_delegation_depth": 1,
"budgets": {"attempts": 3},
"metadata": {"production_side_effects": False},
},
idempotency_key="shadow-orchestration-v1",
)
results = []
for task_id, task_type, worker_type, identity, capabilities, adapter_type in TASKS:
service.register_worker(
{
"worker_id": f"{worker_type}-shadow-01",
"worker_type": worker_type,
"identity": identity,
"capabilities": capabilities,
"trust_zone": "isolated-shadow",
"max_concurrency": 1,
"heartbeat_interval_seconds": 15,
"supported_protocols": ["aios-task-v1"],
}
)
child = service.delegate(
root["task_id"],
{
"task_id": task_id,
"task_type": task_type,
"required_capabilities": capabilities,
"risk": "low",
"approval_policy": "patch-only",
"max_delegation_depth": 0,
"budgets": {"attempts": 1},
"evidence_requirements": ["shadow-report"],
"metadata": {"production_side_effects": False},
"actor_identity": "spiffe://aios/owner/shadow-controller",
},
)
claim = service.claim(
task_id,
{
"worker_id": f"{worker_type}-shadow-01",
"actor_identity": identity,
"lease_seconds": 60,
},
)
service.progress(
task_id,
{
"claim_id": claim["claim_id"],
"actor_identity": identity,
"progress": 100,
"stage": "shadow-verification",
"message": "adapter contract verified without external execution",
},
)
adapter_result = adapter_type().execute(
store.get_task(task_id).task_plan, store.get_claim(claim["claim_id"])
)
completed = service.complete(
task_id,
{
"claim_id": claim["claim_id"],
"actor_identity": identity,
"status": "COMPLETED",
"outputs": [adapter_result],
"evidence": [
{
"kind": "shadow-report",
"execution_mode": "shadow_adapter",
"production_side_effects": False,
}
],
},
)
results.append(
{
"task_id": task_id,
"parent_task_id": child["parent_task_id"],
"task_status": store.get_task(task_id).status,
"worker_result": adapter_result,
"result_digest": completed["result_digest"],
}
)
report = {
"root_task_id": root["task_id"],
"execution_mode": "shadow_adapter",
"production_side_effects": False,
"tasks": results,
"event_chains_valid": all(store.verify_event_chain(item[0]) for item in TASKS),
"notification_delivery_status": sorted(
{row["delivery_status"] for row in store.list_outbox()}
),
}
report_path = Path(args.report)
report_path.parent.mkdir(parents=True, exist_ok=True)
report_path.write_text(
json.dumps(report, ensure_ascii=False, indent=2) + "\n", encoding="utf-8"
)
store.close()
print(f"shadow_report={report_path}")
return 0
if __name__ == "__main__":
raise SystemExit(main())
@@ -0,0 +1,40 @@
#!/usr/bin/env bash
set -euo pipefail
REPORT_PATH="${1:-reports/task-plane-acceptance.json}"
mkdir -p "$(dirname "${REPORT_PATH}")"
export TASKPLANE_REPORT="${REPORT_PATH}"
export PYTHONPATH="${PYTHONPATH:-}:$(pwd)/active-runtime"
python3 - <<'PY'
import io
import json
import sys
import unittest
from pathlib import Path
from contextlib import redirect_stdout
report_path = Path(__import__('os').environ.get("TASKPLANE_REPORT", "reports/task-plane-acceptance.json"))
stream = io.StringIO()
loader = unittest.TestLoader()
suite = loader.discover("tests/task_plane/public_interface", pattern="test_*.py")
runner = unittest.TextTestRunner(stream=sys.stdout, verbosity=2)
result = runner.run(suite)
payload = {
"execution_status": "PARTIALLY_COMPLETED" if result.wasSuccessful() else "FAILED",
"tests_run": result.testsRun,
"failures": len(result.failures),
"errors": len(result.errors),
"skipped": len(result.skipped),
"multi_agent_shadow_verified": result.wasSuccessful(),
"production_side_effects": False,
"notification_delivery_status": "NOT_DELIVERED",
}
report_path.parent.mkdir(parents=True, exist_ok=True)
report_path.write_text(json.dumps(payload, indent=2) + "\n", encoding="utf-8")
print(f"acceptance_report={report_path}")
sys.exit(0 if result.wasSuccessful() else 1)
PY
@@ -0,0 +1,127 @@
import os
import sys
import unittest
ROOT = os.getcwd()
if os.path.join(ROOT, "active-runtime") not in sys.path:
sys.path.append(os.path.join(ROOT, "active-runtime"))
from app.task_plane.api import TaskPlaneService
from app.task_plane.policy import ALL_ACTION_CAPABILITIES, PolicyEngine
def _policy(**kwargs):
return PolicyEngine(
identity_capabilities={"*": set(ALL_ACTION_CAPABILITIES)}, **kwargs
)
from app.task_plane.store import TaskPlaneStore
class TaskPlaneAPIBehaviorTests(unittest.TestCase):
def setUp(self):
self.store = TaskPlaneStore(":memory:")
self.service = TaskPlaneService(self.store, policy=_policy(execution_enabled=False))
def test_task_crud_and_events_chain(self):
task = self.service.create_task(
{
"task_type": "code.change",
"intent": "verify core",
"required_capabilities": ["code.read", "test.python"],
}
)
self.assertIn("task_id", task)
read = self.service.get_task(task["task_id"])
self.assertEqual("CREATED", read["status"])
events = self.service.get_events(task["task_id"])
self.assertEqual(len(events), 1)
def test_execution_disabled_blocks_claim(self):
task = self.service.create_task({"task_type": "code.change"})
self.store.register_worker(
{
"worker_id": "codex-dev-01",
"worker_type": "codex",
"identity": "spiffe://aios/agent/codex/dev-01",
"capabilities": ["code.read", "test.python"],
"trust_zone": "isolated-development",
"max_concurrency": 1,
"heartbeat_interval_seconds": 15,
"supported_protocols": ["aios-task-v1"],
}
)
with self.assertRaises(Exception):
self.service.claim(task["task_id"], {"worker_id": "codex-dev-01", "lease_seconds": 20})
def test_workers_and_events_outbox(self):
policy = _policy(execution_enabled=True)
service = TaskPlaneService(TaskPlaneStore(":memory:"), policy=policy)
service.register_worker(
{
"worker_id": "codex-dev-01",
"worker_type": "codex",
"identity": "spiffe://aios/agent/codex/dev-01",
"capabilities": ["code.read", "test.python", "code.write.patch"],
"trust_zone": "isolated-development",
"max_concurrency": 2,
"heartbeat_interval_seconds": 15,
"supported_protocols": ["aios-task-v1"],
}
)
task = service.create_task({"required_capabilities": ["code.read"], "task_type": "code.change"})
task_record = service.store.get_task(task["task_id"])
identity = "spiffe://aios/agent/codex/dev-01"
claim = service.claim(task["task_id"], {"worker_id": "codex-dev-01", "lease_seconds": 15, "actor_identity": identity})
service.heartbeat(task["task_id"], {"claim_id": claim["claim_id"], "lease_seconds": 20, "actor_identity": identity})
service.progress(task["task_id"], {"claim_id": claim["claim_id"], "progress": 40, "stage": "tests", "message": "running", "actor_identity": identity})
service.complete(task["task_id"], {"claim_id": claim["claim_id"], "status": "COMPLETED", "outputs": [], "evidence": [], "actor_identity": identity})
events = service.get_events(task["task_id"])
self.assertGreater(len(events), 1)
self.assertTrue(service.store.verify_event_chain(task["task_id"]))
self.assertEqual(service.get_children(task["task_id"]), [])
class DelegationGuardTests(unittest.TestCase):
def setUp(self):
self.store = TaskPlaneStore(":memory:")
self.policy = _policy(execution_enabled=True)
self.service = TaskPlaneService(self.store, policy=self.policy)
self.parent = self.service.create_task(
{
"task_type": "code.change",
"required_capabilities": ["code.read"],
"budgets": {"attempts": 3, "tokens": 100, "time_seconds": 1000},
"max_delegation_depth": 2,
}
)
def test_delegation_budget_and_depth(self):
child = self.service.delegate(
self.parent["task_id"],
{
"task_type": "code.change",
"required_capabilities": ["code.read"],
"budgets": {"attempts": 1},
"actor_identity": "owner",
},
)
self.assertEqual(child["plan"]["parent_task_id"], self.parent["task_id"])
grandchild = self.service.delegate(
child["task_id"],
{
"task_type": "code.change",
"required_capabilities": ["code.read"],
"budgets": {"attempts": 1},
"actor_identity": "owner",
},
)
with self.assertRaises(Exception):
self.service.delegate(
grandchild["task_id"],
{
"task_type": "code.change",
"required_capabilities": ["code.read"],
"budgets": {"attempts": 1},
"actor_identity": "owner",
},
)
@@ -0,0 +1,381 @@
import json
import http.client
import os
import tempfile
import threading
import time
import unittest
from http.server import ThreadingHTTPServer
from unittest import mock
from app.task_plane.adapters.codex import CodexAdapter
from app.task_plane.adapters.hermes import HermesAdapter
from app.task_plane.adapters.openclaw import OpenClawAdapter
from app.task_plane.api import TaskPlaneRequestHandler, TaskPlaneService
from app.task_plane.errors import ConflictError, LockedError, ValidationError
from app.task_plane.events import DurableOutboxDispatcher, WeChatGatewayAdapter
from app.task_plane.models import ProbeEvidence, ProbeOutcome, TaskPlan
from app.task_plane.policy import ALL_ACTION_CAPABILITIES, PolicyEngine
from app.task_plane.store import TaskPlaneStore
IDENTITY = "spiffe://aios/agent/codex/dev-01"
def _policy(**kwargs):
return PolicyEngine(
identity_capabilities={"*": set(ALL_ACTION_CAPABILITIES)}, **kwargs
)
def register_worker(service):
return service.register_worker(
{
"worker_id": "codex-dev-01",
"worker_type": "codex",
"identity": IDENTITY,
"capabilities": ["code.read", "code.write.patch", "test.python"],
"trust_zone": "isolated-development",
"max_concurrency": 2,
"heartbeat_interval_seconds": 15,
"supported_protocols": ["aios-task-v1"],
}
)
class DurableStoreTests(unittest.TestCase):
def test_idempotency_replay_and_payload_conflict(self):
service = TaskPlaneService(TaskPlaneStore(":memory:"))
first = service.create_task(
{"task_id": "task-idempotent", "intent": "same"}, "idem-1"
)
replay = service.create_task(
{"task_id": "task-idempotent", "intent": "same"}, "idem-1"
)
self.assertEqual(first["task_id"], replay["task_id"])
with self.assertRaises(ConflictError):
service.create_task(
{"task_id": "task-other", "intent": "different"}, "idem-1"
)
def test_cas_rejects_stale_revision(self):
store = TaskPlaneStore(":memory:")
task = store.create_task(TaskPlan.from_dict({"task_id": "task-cas"}))
store.update_task_status(task.task_id, task.revision, "QUEUED")
with self.assertRaises(ConflictError):
store.update_task_status(task.task_id, task.revision, "CANCELLED")
def test_crash_recovery_preserves_task_events_and_outbox(self):
with tempfile.TemporaryDirectory() as directory:
path = os.path.join(directory, "task-plane.sqlite3")
first = TaskPlaneService(TaskPlaneStore(path))
task = first.create_task({"task_id": "task-restart"})
first.store.close()
second = TaskPlaneService(TaskPlaneStore(path))
self.assertEqual("CREATED", second.get_task(task["task_id"])["status"])
self.assertTrue(second.store.verify_event_chain(task["task_id"]))
self.assertEqual(1, len(second.store.list_outbox()))
second.store.close()
def test_event_and_evidence_tamper_are_detected(self):
store = TaskPlaneStore(":memory:")
service = TaskPlaneService(store)
task = service.create_task({"task_id": "task-tamper"})
evidence_id = store.add_evidence(
task["task_id"], "test-report", {"passed": True}, IDENTITY
)
store.conn.execute(
"UPDATE events SET event_data=? WHERE task_id=?",
(json.dumps({"changed": True}), task["task_id"]),
)
store.conn.execute(
"UPDATE evidence SET payload=? WHERE evidence_id=?",
(json.dumps({"passed": False}), evidence_id),
)
self.assertFalse(store.verify_event_chain(task["task_id"]))
self.assertFalse(store.verify_evidence_integrity(task["task_id"]))
def test_secrets_are_redacted_at_rest_and_in_outbox(self):
store = TaskPlaneStore(":memory:")
service = TaskPlaneService(store)
secret = "never-store-this-value"
task = service.create_task(
{
"task_id": "task-redaction",
"metadata": {"access_token": secret, "token_count": 12},
}
)
raw_task = store.conn.execute(
"SELECT task_json FROM tasks WHERE task_id=?", (task["task_id"],)
).fetchone()[0]
raw_outbox = store.conn.execute(
"SELECT payload FROM outbox WHERE task_id=?", (task["task_id"],)
).fetchone()[0]
self.assertNotIn(secret, raw_task + raw_outbox)
self.assertIn("[REDACTED]", raw_task)
self.assertIn("token_count", raw_task)
class ClaimLeaseTests(unittest.TestCase):
def setUp(self):
self.store = TaskPlaneStore(":memory:")
self.service = TaskPlaneService(
self.store, policy=_policy(execution_enabled=True)
)
register_worker(self.service)
def test_only_one_worker_can_claim_concurrently(self):
task = self.service.create_task({"task_id": "task-race"})
revision = self.store.get_task(task["task_id"]).revision
outcomes = []
barrier = threading.Barrier(2)
def attempt():
barrier.wait()
try:
self.store.claim_task(
task["task_id"],
"codex-dev-01",
IDENTITY,
30,
revision,
self.store.get_task(task["task_id"]).task_plan.operation_digest(),
)
outcomes.append("claimed")
except ConflictError:
outcomes.append("conflict")
threads = [threading.Thread(target=attempt) for _ in range(2)]
for thread in threads:
thread.start()
for thread in threads:
thread.join()
self.assertCountEqual(["claimed", "conflict"], outcomes)
def test_expired_lease_requires_probe_before_reassignment(self):
task = self.service.create_task({"task_id": "task-expiry"})
claim = self.service.claim(
task["task_id"],
{
"worker_id": "codex-dev-01",
"actor_identity": IDENTITY,
"lease_seconds": 1,
},
)
future = time.time() + 5
with mock.patch("app.task_plane.store._now", return_value=future):
self.store.probe_attestation_verifier = lambda probe: (
probe.attestation_ref == "trusted://probe/recovery"
)
probe = ProbeEvidence.create(
task_id=task["task_id"],
claim_id=claim["claim_id"],
worker_id="codex-dev-01",
worker_identity=IDENTITY,
operation_digest=claim["operation_digest"],
outcome=ProbeOutcome.NOT_APPLIED,
observed_at=future,
expires_at=future + 60,
probe_executor_identity="spiffe://aios/admin/recovery",
attestation_ref="trusted://probe/recovery",
)
recovered = self.store.recover_claim_with_probe_and_event(
task["task_id"],
claim["claim_id"],
probe,
)
self.assertEqual("FAILED_RECOVERABLE", recovered.status)
retry = self.service.claim(
task["task_id"],
{
"worker_id": "codex-dev-01",
"actor_identity": IDENTITY,
"lease_seconds": 30,
},
)
self.assertEqual(2, retry["attempt"])
def test_wrong_claim_identity_and_duplicate_result_are_rejected(self):
task = self.service.create_task({"task_id": "task-result"})
claim = self.service.claim(
task["task_id"],
{"worker_id": "codex-dev-01", "actor_identity": IDENTITY},
)
with self.assertRaises(Exception):
self.service.complete(
task["task_id"],
{
"claim_id": claim["claim_id"],
"actor_identity": "spiffe://aios/agent/codex/other",
},
)
self.service.progress(
task["task_id"],
{
"claim_id": claim["claim_id"],
"actor_identity": IDENTITY,
"progress": 10,
"stage": "running",
},
)
self.service.complete(
task["task_id"],
{"claim_id": claim["claim_id"], "actor_identity": IDENTITY},
)
with self.assertRaises(ConflictError):
self.service.complete(
task["task_id"],
{"claim_id": claim["claim_id"], "actor_identity": IDENTITY},
)
def test_tampered_result_digest_and_unverifiable_signature_fail_closed(self):
for suffix, extra in (
("digest", {"result_digest": "0" * 64}),
("signature", {"signature": "tampered"}),
):
task = self.service.create_task({"task_id": f"task-{suffix}"})
claim = self.service.claim(
task["task_id"],
{"worker_id": "codex-dev-01", "actor_identity": IDENTITY},
)
self.service.progress(
task["task_id"],
{
"claim_id": claim["claim_id"],
"actor_identity": IDENTITY,
"progress": 10,
"stage": "running",
},
)
with self.assertRaises(Exception):
self.service.complete(
task["task_id"],
{
"claim_id": claim["claim_id"],
"actor_identity": IDENTITY,
**extra,
},
)
self.assertEqual(
"ACTIVE", self.store.get_claim(claim["claim_id"]).status
)
class DelegationAndNotificationTests(unittest.TestCase):
def setUp(self):
self.service = TaskPlaneService(
TaskPlaneStore(":memory:"),
policy=_policy(execution_enabled=True, max_risk="high"),
)
self.parent = self.service.create_task(
{
"task_id": "task-parent",
"task_type": "code.review",
"required_capabilities": ["code.read"],
"risk": "medium",
"approval_policy": "patch-only",
"max_delegation_depth": 1,
"budgets": {"attempts": 1, "tokens": 10},
}
)
def test_capability_risk_budget_and_cycle_escalation_are_rejected(self):
base = {
"actor_identity": "owner",
"task_type": "code.review",
"budgets": {"attempts": 1},
}
for payload in (
{**base, "required_capabilities": ["git.force_push"]},
{**base, "required_capabilities": ["code.read"], "risk": "high"},
{**base, "required_capabilities": ["code.read"], "budgets": {"tokens": 11}},
{
**base,
"task_id": "task-parent",
"required_capabilities": ["code.read"],
},
):
with self.assertRaises(Exception):
self.service.delegate(self.parent["task_id"], payload)
def test_missing_gateway_receipt_remains_not_delivered(self):
message = self.service.store.list_outbox()[0]
dispatcher = DurableOutboxDispatcher(self.service.store)
updated = dispatcher.dispatch(
message["message_id"], WeChatGatewayAdapter(lambda envelope: None)
)
self.assertEqual("NOT_DELIVERED", updated["delivery_status"])
delivered = self.service.store.record_outbox_delivery(
message["message_id"], receipt="gateway-receipt-1"
)
replay = self.service.store.record_outbox_delivery(
message["message_id"], receipt="gateway-receipt-1"
)
self.assertEqual("DELIVERED", delivered["delivery_status"])
self.assertEqual(delivered["receipt"], replay["receipt"])
def test_worker_offline_detection_is_fail_closed(self):
register_worker(self.service)
worker = self.service.store.get_worker("codex-dev-01")
after_deadline = (
worker.last_heartbeat + worker.heartbeat_interval_seconds * 3 + 1
)
self.assertFalse(
self.service.workers.is_online("codex-dev-01", now=after_deadline)
)
def test_path_and_parameter_injection_are_rejected(self):
for value in ("../task", "task/child", "%2e%2e", "task\ninjected", ""):
with self.assertRaises(ValidationError):
self.service.store.get_task(value)
def test_shadow_adapters_only_return_descriptions(self):
task = TaskPlan.from_dict({"task_id": "task-shadow"})
claim = type("Claim", (), {"claim_id": "claim-shadow", "status": "ACTIVE"})()
for adapter in (OpenClawAdapter(), CodexAdapter(), HermesAdapter()):
result = adapter.execute(task, claim)
self.assertEqual("SHADOW_VERIFIED", result["status"])
self.assertFalse(result["production_side_effects"])
self.assertNotIn("command", result)
def test_http_rejects_encoded_path_and_non_object_json(self):
TaskPlaneRequestHandler.service = self.service
TaskPlaneRequestHandler.authenticator = None
server = ThreadingHTTPServer(("127.0.0.1", 0), TaskPlaneRequestHandler)
thread = threading.Thread(target=server.serve_forever, daemon=True)
thread.start()
try:
connection = http.client.HTTPConnection(
"127.0.0.1", server.server_address[1], timeout=3
)
connection.request("GET", "/tasks/%2e%2e")
response = connection.getresponse()
payload = response.read().decode()
self.assertEqual(400, response.status)
self.assertNotIn("Traceback", payload)
connection.request(
"POST",
"/tasks",
body="[]",
headers={"Content-Type": "application/json"},
)
response = connection.getresponse()
response.read()
self.assertEqual(400, response.status)
connection.request(
"POST",
"/tasks",
body=json.dumps({"task_id": "task-http-unauthorized"}),
headers={"Content-Type": "application/json"},
)
response = connection.getresponse()
response.read()
self.assertEqual(401, response.status)
finally:
server.shutdown()
server.server_close()
thread.join(timeout=3)
if __name__ == "__main__":
unittest.main()
@@ -0,0 +1,892 @@
import dataclasses
import http.client
import json
import multiprocessing
import os
import tempfile
import threading
import time
import unittest
from http.server import ThreadingHTTPServer
from app.task_plane.api import TaskPlaneRequestHandler, TaskPlaneService
from app.task_plane.errors import (
ConflictError,
ForbiddenError,
LockedError,
ValidationError,
)
from app.task_plane.models import (
EvidenceEnvelope,
ProbeEvidence,
ProbeOutcome,
TaskPlan,
)
from app.task_plane.policy import ALL_ACTION_CAPABILITIES, PolicyEngine
from app.task_plane.scheduler import TaskScheduler
from app.task_plane.store import TaskPlaneStore
WORKER_ID = "codex-secure-01"
WORKER_IDENTITY = "spiffe://aios/agent/codex/secure-01"
ADMIN_IDENTITY = "spiffe://aios/admin/task-plane"
def _worker_payload(**overrides):
payload = {
"worker_id": WORKER_ID,
"worker_type": "codex",
"identity": WORKER_IDENTITY,
"owner_identity": WORKER_IDENTITY,
"capabilities": ["code.read", "test.python"],
"trust_zone": "isolated-development",
"max_concurrency": 1,
"heartbeat_interval_seconds": 15,
"supported_protocols": ["aios-task-v1"],
"registration_policy": "self",
"actor_identity": WORKER_IDENTITY,
}
payload.update(overrides)
return payload
def _service(store=None, **policy_kwargs):
grants = {
ADMIN_IDENTITY: {
"task.creator",
"task.read",
"task.audit",
"task.cancel",
"task.delegate",
"task.claim",
"worker.admin",
"worker.self_register",
},
WORKER_IDENTITY: {
"task.read",
"task.claim",
"worker.self_register",
},
}
return TaskPlaneService(
store or TaskPlaneStore(":memory:"),
policy=PolicyEngine(
execution_enabled=True,
identity_capabilities=grants,
admin_identities={ADMIN_IDENTITY},
**policy_kwargs,
),
)
def _claim_for_service(service, *, task_id="task-secure", plan=None):
plan = plan or {
"task_id": task_id,
"issuer": {"type": "agent", "id": ADMIN_IDENTITY},
"required_capabilities": ["code.read"],
"risk": "low",
}
service.register_worker(
_worker_payload(
capabilities=sorted(
{"code.read", "test.python", *plan.get("required_capabilities", [])}
)
)
)
task = service.create_task(plan)
claim = service.claim(
task["task_id"],
{"worker_id": WORKER_ID, "actor_identity": WORKER_IDENTITY},
)
return task, claim
def _process_claim(db_path, task_id, expected_revision, digest, queue):
store = TaskPlaneStore(db_path)
try:
store.claim_task(
task_id,
WORKER_ID,
WORKER_IDENTITY,
30,
expected_revision,
digest,
)
queue.put("claimed")
except Exception as exc:
queue.put(type(exc).__name__)
finally:
store.close()
class EvidenceEnvelopeContractTests(unittest.TestCase):
def setUp(self):
self.base = EvidenceEnvelope.create(
task_id="task-evidence",
claim_id="claim-evidence",
kind="test-report",
worker_identity=WORKER_IDENTITY,
payload={"passed": True},
previous_evidence_hash="",
attestation_ref="trusted://shadow/test-report",
)
def test_digest_binds_complete_envelope_and_order(self):
self.assertTrue(
self.base.verify(
expected_task_id="task-evidence",
expected_claim_id="claim-evidence",
expected_previous_hash="",
)
)
mutations = {
"kind": "secret-scan",
"worker_identity": "spiffe://attacker",
"task_id": "task-other",
"claim_id": "claim-other",
"created_at": self.base.created_at + 1,
"payload": {"passed": False},
"previous_evidence_hash": "f" * 64,
}
for field, value in mutations.items():
with self.subTest(field=field):
self.assertFalse(
dataclasses.replace(self.base, **{field: value}).verify(
expected_task_id="task-evidence",
expected_claim_id="claim-evidence",
expected_previous_hash="",
)
)
def test_store_rejects_cross_task_required_evidence_and_chain_tamper(self):
store = TaskPlaneStore(":memory:")
store.create_task_with_event(
TaskPlan.from_dict({"task_id": "task-evidence"})
)
store.add_evidence_envelope(self.base)
self.assertTrue(store.verify_evidence_integrity("task-evidence"))
store.conn.execute(
"UPDATE evidence SET envelope_json=json_set(envelope_json,'$.task_id','task-other')"
)
self.assertFalse(store.verify_evidence_integrity("task-evidence"))
class AtomicMutationContractTests(unittest.TestCase):
def _fault(self, point):
def inject(observed):
if observed == point:
raise RuntimeError("injected fault")
return inject
def test_create_cancel_and_delegate_roll_back_at_event_boundaries(self):
store = TaskPlaneStore(":memory:")
store.fault_injector = self._fault("create.after_task_before_event")
with self.assertRaises(RuntimeError):
store.create_task_with_event(TaskPlan.from_dict({"task_id": "task-create"}))
self.assertEqual([], store.list_tasks())
store.fault_injector = None
parent = store.create_task_with_event(
TaskPlan.from_dict(
{
"task_id": "task-parent-atomic",
"required_capabilities": ["code.read"],
"budgets": {"attempts": 2},
}
)
)
store.fault_injector = self._fault("cancel.after_state_before_event")
with self.assertRaises(RuntimeError):
store.cancel_task_with_event(parent.task_id, parent.revision, "stop")
self.assertEqual("CREATED", store.get_task(parent.task_id).status)
store.fault_injector = self._fault("delegate.after_budget_before_child")
with self.assertRaises(RuntimeError):
store.delegate_task_with_budget_and_events(
parent.task_id,
TaskPlan.from_dict(
{
"task_id": "task-child-atomic",
"required_capabilities": ["code.read"],
"budgets": {"attempts": 1},
}
),
caller=ADMIN_IDENTITY,
)
self.assertEqual(2, store.get_task_budget(parent.task_id).attempts)
self.assertEqual([], store.list_children(parent.task_id))
def test_complete_fail_and_recover_roll_back_wholly(self):
service = _service()
task, claim = _claim_for_service(service, task_id="task-complete-atomic")
service.progress(
task["task_id"],
{
"claim_id": claim["claim_id"],
"actor_identity": WORKER_IDENTITY,
"progress": 10,
"stage": "running",
},
)
service.store.fault_injector = self._fault(
"complete.after_evidence_before_state"
)
envelope = EvidenceEnvelope.create(
task_id=task["task_id"],
claim_id=claim["claim_id"],
kind="test-report",
worker_identity=WORKER_IDENTITY,
payload={"passed": True},
previous_evidence_hash="",
attestation_ref="trusted://shadow/test",
)
with self.assertRaises(RuntimeError):
service.store.complete_claim_with_evidence_and_event(
task["task_id"],
claim["claim_id"],
"COMPLETED",
{"status": "COMPLETED"},
[envelope],
)
self.assertEqual([], service.store.list_evidence(task["task_id"]))
self.assertEqual("RUNNING", service.store.get_task(task["task_id"]).status)
def test_fail_rolls_back_claim_state_and_event(self):
service = _service()
task, claim = _claim_for_service(service, task_id="task-fail-atomic")
service.store.fault_injector = self._fault("fail.after_state_before_commit")
with self.assertRaises(RuntimeError):
service.store.fail_claim_with_event(
task["task_id"], claim["claim_id"], "FAILED_FINAL", "injected"
)
self.assertEqual("ACTIVE", service.store.get_claim(claim["claim_id"]).status)
self.assertEqual("CLAIMED", service.store.get_task(task["task_id"]).status)
self.assertNotIn(
"TASK_FAILED",
{
event["type"]
for event in service.store.list_events(task["task_id"])
},
)
def test_recovery_rolls_back_probe_state_and_event(self):
service = _service(
probe_attestation_verifier=lambda probe: probe.attestation_ref
== "trusted://probe/atomic"
)
task, claim = _claim_for_service(service, task_id="task-recover-atomic")
service.store.conn.execute(
"UPDATE claims SET lease_until=? WHERE claim_id=?",
(time.time() - 10, claim["claim_id"]),
)
probe = ProbeEvidence.create(
task_id=task["task_id"],
claim_id=claim["claim_id"],
worker_id=WORKER_ID,
worker_identity=WORKER_IDENTITY,
operation_digest=claim["operation_digest"],
outcome=ProbeOutcome.NOT_APPLIED,
observed_at=time.time(),
expires_at=time.time() + 60,
probe_executor_identity=ADMIN_IDENTITY,
attestation_ref="trusted://probe/atomic",
)
service.store.fault_injector = self._fault(
"recover.after_probe_before_state"
)
with self.assertRaises(RuntimeError):
service.store.recover_claim_with_probe_and_event(
task["task_id"], claim["claim_id"], probe
)
self.assertEqual(
0,
service.store.conn.execute(
"SELECT COUNT(*) FROM probe_evidence WHERE probe_id=?",
(probe.probe_id,),
).fetchone()[0],
)
self.assertEqual("ACTIVE", service.store.get_claim(claim["claim_id"]).status)
self.assertEqual("CLAIMED", service.store.get_task(task["task_id"]).status)
class WorkerOwnershipContractTests(unittest.TestCase):
def test_identity_takeover_escalation_and_disabled_reregistration_rejected(self):
service = _service()
registered = service.register_worker(_worker_payload())
revision = registered["registration_revision"]
with self.assertRaises(ConflictError):
service.register_worker(
_worker_payload(
identity="spiffe://aios/attacker",
owner_identity="spiffe://aios/attacker",
actor_identity="spiffe://aios/attacker",
expected_revision=revision,
)
)
with self.assertRaises(ForbiddenError):
service.register_worker(
_worker_payload(
capabilities=["code.read", "deploy.production"],
expected_revision=revision,
)
)
disabled = service.disable_worker(
WORKER_ID,
{
"actor_identity": ADMIN_IDENTITY,
"expected_revision": revision,
"reason": "security review",
},
)
with self.assertRaises(LockedError):
service.register_worker(
_worker_payload(expected_revision=disabled["registration_revision"])
)
def test_worker_cannot_heartbeat_other_identity_and_all_updates_use_cas(self):
service = _service()
worker = service.register_worker(_worker_payload())
with self.assertRaises(ForbiddenError):
service.worker_heartbeat(
WORKER_ID,
{
"actor_identity": "spiffe://aios/attacker",
"expected_revision": worker["registration_revision"],
},
)
with self.assertRaises(ConflictError):
service.worker_heartbeat(
WORKER_ID,
{
"actor_identity": WORKER_IDENTITY,
"expected_revision": worker["registration_revision"] + 1,
},
)
class RecoveryProbeContractTests(unittest.TestCase):
def _expired_claim(self, suffix):
service = _service(
probe_attestation_verifier=lambda probe: probe.attestation_ref
== "trusted://probe/test"
)
task, claim = _claim_for_service(service, task_id=f"task-probe-{suffix}")
service.store.conn.execute(
"UPDATE claims SET lease_until=? WHERE claim_id=?",
(time.time() - 10, claim["claim_id"]),
)
return service, task, claim
def _probe(self, task, claim, outcome, **overrides):
data = {
"task_id": task["task_id"],
"claim_id": claim["claim_id"],
"worker_id": WORKER_ID,
"worker_identity": WORKER_IDENTITY,
"operation_digest": claim["operation_digest"],
"outcome": outcome,
"observed_at": time.time(),
"expires_at": time.time() + 60,
"probe_executor_identity": ADMIN_IDENTITY,
"attestation_ref": "trusted://probe/test",
}
data.update(overrides)
return ProbeEvidence.create(**data)
def test_four_probe_outcomes_have_distinct_fail_closed_transitions(self):
expected = {
ProbeOutcome.NOT_APPLIED: "FAILED_RECOVERABLE",
ProbeOutcome.APPLIED_VERIFIED: "COMPLETED",
ProbeOutcome.INDETERMINATE: "FAILED_RECOVERABLE",
ProbeOutcome.CONFLICT: "MANUAL_REVIEW_REQUIRED",
}
for outcome, status in expected.items():
with self.subTest(outcome=outcome):
service, task, claim = self._expired_claim(outcome.value.lower())
recovered = service.store.recover_claim_with_probe_and_event(
task["task_id"],
claim["claim_id"],
self._probe(task, claim, outcome),
)
self.assertEqual(status, recovered.status)
if outcome == ProbeOutcome.INDETERMINATE:
self.assertFalse(
service.store.task_has_retryable_probe(task["task_id"])
)
def test_forged_expired_or_mismatched_probe_is_rejected(self):
service, task, claim = self._expired_claim("invalid")
valid = self._probe(task, claim, ProbeOutcome.NOT_APPLIED)
for probe in (
dataclasses.replace(valid, evidence_digest="0" * 64),
self._probe(
task,
claim,
ProbeOutcome.NOT_APPLIED,
expires_at=time.time() - 1,
),
self._probe(
task,
claim,
ProbeOutcome.NOT_APPLIED,
operation_digest="f" * 64,
),
):
with self.assertRaises((ForbiddenError, ConflictError, LockedError)):
service.store.recover_claim_with_probe_and_event(
task["task_id"], claim["claim_id"], probe
)
class SchedulerAndClassificationTests(unittest.TestCase):
def test_scheduler_skips_incompatible_and_dependency_blocked_tasks(self):
service = _service()
service.register_worker(_worker_payload())
service.create_task(
{
"task_id": "task-incompatible",
"issuer": {"type": "agent", "id": ADMIN_IDENTITY},
"required_capabilities": ["gpu.train"],
}
)
service.create_task(
{
"task_id": "task-dependency",
"issuer": {"type": "agent", "id": ADMIN_IDENTITY},
}
)
service.create_task(
{
"task_id": "task-blocked",
"issuer": {"type": "agent", "id": ADMIN_IDENTITY},
"required_capabilities": ["code.read"],
"dependencies": ["task-dependency"],
}
)
service.create_task(
{
"task_id": "task-eligible",
"issuer": {"type": "agent", "id": ADMIN_IDENTITY},
"required_capabilities": ["code.read"],
"metadata": {"priority": 100},
}
)
claim = TaskScheduler(service.store).schedule_for_worker(WORKER_ID)
self.assertEqual("task-eligible", claim.task_id)
self.assertEqual(WORKER_IDENTITY, claim.worker_identity)
def test_credential_secret_rejects_generic_worker_and_redacts_free_text(self):
service = _service()
service.register_worker(_worker_payload())
task = service.create_task(
{
"task_id": "task-credential-secret",
"issuer": {"type": "agent", "id": ADMIN_IDENTITY},
"required_capabilities": ["secret.transform"],
"data_classification": "credential-secret",
"execution_zone": "secure-file-transformer",
"network_policy": "denied",
"logging_policy": "metadata-only",
"output_policy": "hash-count-status-only",
}
)
with self.assertRaises(ForbiddenError):
service.claim(
task["task_id"],
{"worker_id": WORKER_ID, "actor_identity": WORKER_IDENTITY},
)
with self.assertRaises(ValueError):
TaskPlan.from_dict(
{
"task_id": "task-secret-leak",
"intent": "Authorization: Bearer super-secret-value",
"data_classification": "credential-secret",
}
)
class ReadRBACAndResultProofTests(unittest.TestCase):
def test_get_evidence_requires_authentication(self):
service = _service()
service.create_task(
{
"task_id": "task-http-read",
"issuer": {"type": "agent", "id": ADMIN_IDENTITY},
}
)
TaskPlaneRequestHandler.service = service
TaskPlaneRequestHandler.authenticator = lambda token: (
ADMIN_IDENTITY if token == "valid" else None
)
server = ThreadingHTTPServer(("127.0.0.1", 0), TaskPlaneRequestHandler)
thread = threading.Thread(target=server.serve_forever, daemon=True)
thread.start()
try:
connection = http.client.HTTPConnection(
"127.0.0.1", server.server_address[1], timeout=3
)
connection.request("GET", "/tasks/task-http-read/evidence")
response = connection.getresponse()
response.read()
self.assertEqual(401, response.status)
connection.request(
"GET",
"/tasks/task-http-read/evidence",
headers={"Authorization": "Bearer valid"},
)
response = connection.getresponse()
response.read()
self.assertEqual(200, response.status)
finally:
server.shutdown()
server.server_close()
thread.join(timeout=3)
def test_sensitive_result_requires_worker_proof_but_low_shadow_is_marked(self):
service = _service(result_signature_verifier=lambda result: False)
task, claim = _claim_for_service(
service,
task_id="task-write-proof",
plan={
"task_id": "task-write-proof",
"issuer": {"type": "agent", "id": ADMIN_IDENTITY},
"required_capabilities": ["code.read", "code.write.patch"],
"risk": "medium",
},
)
service.progress(
task["task_id"],
{
"claim_id": claim["claim_id"],
"actor_identity": WORKER_IDENTITY,
"progress": 100,
"stage": "done",
},
)
with self.assertRaises(ForbiddenError):
service.complete(
task["task_id"],
{
"claim_id": claim["claim_id"],
"actor_identity": WORKER_IDENTITY,
"signature": "invalid",
},
)
class SQLiteAndEventIntegrityTests(unittest.TestCase):
def test_event_hash_binds_metadata(self):
store = TaskPlaneStore(":memory:")
task = store.create_task_with_event(
TaskPlan.from_dict({"task_id": "task-event-metadata"})
)
self.assertTrue(store.verify_event_chain(task.task_id))
for column, value in (
("event_id", "evt-forged"),
("subject", "task/other"),
("created_at", 1.0),
):
with self.subTest(column=column):
original = dict(
store.conn.execute(
"SELECT * FROM events WHERE task_id=?", (task.task_id,)
).fetchone()
)
store.conn.execute(
f"UPDATE events SET {column}=? WHERE task_id=?",
(value, task.task_id),
)
self.assertFalse(store.verify_event_chain(task.task_id))
store.conn.execute(
f"UPDATE events SET {column}=? WHERE task_id=?",
(original[column], task.task_id),
)
def test_sqlite_high_assurance_schema_integrity_checkpoint_and_backup(self):
with tempfile.TemporaryDirectory() as directory:
path = os.path.join(directory, "task-plane.sqlite3")
store = TaskPlaneStore(path, high_assurance=True)
self.assertGreaterEqual(store.schema_version(), 2)
self.assertEqual(5000, store.conn.execute("PRAGMA busy_timeout").fetchone()[0])
self.assertEqual(2, store.conn.execute("PRAGMA synchronous").fetchone()[0])
self.assertEqual("ok", store.integrity_check())
store.create_task_with_event(
TaskPlan.from_dict({"task_id": "task-backup"})
)
backup = os.path.join(directory, "backup.sqlite3")
store.checkpoint_and_backup(backup)
self.assertTrue(os.path.exists(backup))
store.close()
def test_multi_process_claim_race_has_single_winner(self):
with tempfile.TemporaryDirectory() as directory:
path = os.path.join(directory, "task-plane.sqlite3")
store = TaskPlaneStore(path)
service = _service(store)
service.register_worker(_worker_payload(max_concurrency=2))
task = service.create_task(
{
"task_id": "task-multiprocess",
"issuer": {"type": "agent", "id": ADMIN_IDENTITY},
"required_capabilities": ["code.read"],
}
)
record = store.get_task(task["task_id"])
queue = multiprocessing.Queue()
processes = [
multiprocessing.Process(
target=_process_claim,
args=(
path,
task["task_id"],
record.revision,
record.task_plan.operation_digest(),
queue,
),
)
for _ in range(2)
]
for process in processes:
process.start()
for process in processes:
process.join(10)
outcomes = [queue.get(timeout=3) for _ in processes]
self.assertEqual(1, outcomes.count("claimed"))
store.close()
class ExplicitSecurityMatrixTests(unittest.TestCase):
def test_evidence_kind_tamper_is_rejected(self):
envelope = EvidenceEnvelope.create(
task_id="task-kind",
claim_id="claim-kind",
kind="test-report",
worker_identity=WORKER_IDENTITY,
payload={"passed": True},
previous_evidence_hash="",
)
self.assertFalse(
dataclasses.replace(envelope, kind="forged").verify(
expected_task_id="task-kind",
expected_claim_id="claim-kind",
expected_previous_hash="",
)
)
def test_worker_admin_restore_is_cas_and_journaled(self):
service = _service()
worker = service.register_worker(_worker_payload())
disabled = service.disable_worker(
WORKER_ID,
{
"actor_identity": ADMIN_IDENTITY,
"expected_revision": worker["registration_revision"],
"reason": "test",
},
)
restored = service.restore_worker(
WORKER_ID,
{
"actor_identity": ADMIN_IDENTITY,
"expected_revision": disabled["registration_revision"],
},
)
self.assertFalse(restored["disabled"])
events = service.store.list_events("task-worker-registry")
self.assertIn("WORKER_RESTORED", {event["type"] for event in events})
def test_disabled_worker_heartbeat_is_rejected(self):
service = _service()
worker = service.register_worker(_worker_payload())
disabled = service.disable_worker(
WORKER_ID,
{
"actor_identity": ADMIN_IDENTITY,
"expected_revision": worker["registration_revision"],
"reason": "test",
},
)
with self.assertRaises(LockedError):
service.worker_heartbeat(
WORKER_ID,
{
"actor_identity": WORKER_IDENTITY,
"expected_revision": disabled["registration_revision"],
},
)
def test_low_risk_unsigned_result_is_explicitly_untrusted(self):
service = _service()
task, claim = _claim_for_service(service, task_id="task-low-shadow")
service.progress(
task["task_id"],
{
"claim_id": claim["claim_id"],
"actor_identity": WORKER_IDENTITY,
"progress": 100,
"stage": "done",
},
)
result = service.complete(
task["task_id"],
{
"claim_id": claim["claim_id"],
"actor_identity": WORKER_IDENTITY,
},
)
self.assertEqual("untrusted_shadow_result", result["trust_status"])
def test_boolean_recovery_contract_is_rejected(self):
service = _service()
task, claim = _claim_for_service(service, task_id="task-no-bool-probe")
service.store.conn.execute(
"UPDATE claims SET lease_until=? WHERE claim_id=?",
(time.time() - 10, claim["claim_id"]),
)
with self.assertRaises(ValidationError):
service.store.recover_expired_claim(
task["task_id"], claim["claim_id"], True
)
def test_scheduler_rejects_offline_worker(self):
service = _service()
service.register_worker(_worker_payload())
service.create_task(
{
"task_id": "task-offline",
"issuer": {"type": "agent", "id": ADMIN_IDENTITY},
"required_capabilities": ["code.read"],
}
)
service.store.conn.execute(
"UPDATE workers SET last_heartbeat=? WHERE worker_id=?",
(time.time() - 1000, WORKER_ID),
)
with self.assertRaises(LockedError):
TaskScheduler(service.store).schedule_for_worker(WORKER_ID)
def test_schedule_decision_event_is_single_for_claim(self):
service = _service()
service.register_worker(_worker_payload())
service.create_task(
{
"task_id": "task-schedule-event",
"issuer": {"type": "agent", "id": ADMIN_IDENTITY},
"required_capabilities": ["code.read"],
}
)
TaskScheduler(service.store).schedule_for_worker(WORKER_ID)
events = service.store.list_events("task-schedule-event")
self.assertEqual(
1, sum(event["type"] == "SCHEDULE_DECISION" for event in events)
)
def _credential_service_and_claim(self):
identity = "spiffe://aios/agent/secure-file-transformer/01"
service = TaskPlaneService(
TaskPlaneStore(":memory:"),
policy=PolicyEngine(
execution_enabled=True,
identity_capabilities={"*": set(ALL_ACTION_CAPABILITIES)},
),
)
service.register_worker(
{
"worker_id": "secure-transformer-01",
"worker_type": "secure_file_transformer",
"identity": identity,
"capabilities": ["secret.transform", "secure_file_transformer"],
"trust_zone": "secure",
"max_concurrency": 1,
"heartbeat_interval_seconds": 15,
"supported_protocols": ["aios-task-v1"],
}
)
task = service.create_task(
{
"task_id": "task-secure-output",
"required_capabilities": ["secret.transform"],
"data_classification": "credential-secret",
"execution_zone": "secure-file-transformer",
"network_policy": "denied",
"logging_policy": "metadata-only",
"output_policy": "hash-count-status-only",
}
)
claim = service.claim(
task["task_id"],
{"worker_id": "secure-transformer-01", "actor_identity": identity},
)
return service, task, claim, identity
def test_credential_progress_free_text_is_rejected(self):
service, task, claim, identity = self._credential_service_and_claim()
with self.assertRaises(ValidationError):
service.progress(
task["task_id"],
{
"claim_id": claim["claim_id"],
"actor_identity": identity,
"progress": 10,
"stage": "running",
"message": "Authorization: Bearer forbidden",
},
)
def test_credential_result_content_is_rejected(self):
service, task, claim, identity = self._credential_service_and_claim()
service.progress(
task["task_id"],
{
"claim_id": claim["claim_id"],
"actor_identity": identity,
"progress": 100,
"stage": "done",
},
)
with self.assertRaises(ValidationError):
service.complete(
task["task_id"],
{
"claim_id": claim["claim_id"],
"actor_identity": identity,
"outputs": [{"content": "forbidden"}],
},
)
def test_action_rbac_denies_read_without_task_read(self):
store = TaskPlaneStore(":memory:")
service = TaskPlaneService(
store,
policy=PolicyEngine(
identity_capabilities={"spiffe://reader/none": set()}
),
)
store.create_task_with_event(TaskPlan.from_dict({"task_id": "task-rbac"}))
with self.assertRaises(ForbiddenError):
service.get_task("task-rbac", "spiffe://reader/none")
def test_default_service_policy_denies_authenticated_read(self):
service = TaskPlaneService(TaskPlaneStore(":memory:"))
service.create_task({"task_id": "task-default-deny"})
with self.assertRaises(ForbiddenError):
service.get_task("task-default-deny", "spiffe://reader/unconfigured")
def test_fail_path_emits_one_task_failed_event(self):
service = _service()
task, claim = _claim_for_service(service, task_id="task-single-fail")
service.fail(
task["task_id"],
{
"claim_id": claim["claim_id"],
"actor_identity": WORKER_IDENTITY,
"reason": "expected test failure",
},
)
events = service.store.list_events(task["task_id"])
self.assertEqual(1, sum(event["type"] == "TASK_FAILED" for event in events))
if __name__ == "__main__":
unittest.main()
+170
View File
@@ -0,0 +1,170 @@
import os
import sys
import tempfile
import subprocess
import unittest
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "active-runtime" / "app"))
import aios_isolated_writer_policy as policy
class IsolatedWriterPolicyTests(unittest.TestCase):
def setUp(self):
self.saved = dict(os.environ)
os.environ["AIOS_GITOPS_ISOLATED_WRITER_ENFORCEMENT"] = "required"
os.environ["AIOS_GITOPS_ALLOWED_BRANCH"] = "patch/aios-agent-task-control-plane-20260724"
os.environ["AIOS_GITOPS_ALLOWED_BASE_OID"] = "a8e97868976b0dce17b7cb8487eadc1d9c687622"
def tearDown(self):
os.environ.clear()
os.environ.update(self.saved)
def test_only_expected_branch_is_accepted(self):
policy.validate_branch_request({
"branch": os.environ["AIOS_GITOPS_ALLOWED_BRANCH"],
"base": "main",
})
for branch in ("patch/other", "main", ""):
with self.subTest(branch=branch), self.assertRaises(PermissionError):
policy.validate_branch_request({"branch": branch, "base": "main"})
def test_write_paths_are_scoped_and_canonical(self):
with tempfile.TemporaryDirectory() as root:
workspace = Path(root)
self.assertEqual(
policy.validate_path("active-runtime/app/x.py", workspace),
"active-runtime/app/x.py",
)
self.assertEqual(policy.validate_path("docs/x.md", workspace), "docs/x.md")
self.assertEqual(
policy.validate_path("scripts/task-plane/run.sh", workspace),
"scripts/task-plane/run.sh",
)
self.assertEqual(
policy.validate_path("reports/task-plane.json", workspace),
"reports/task-plane.json",
)
rejected = (
"",
"../docs/x.md",
"/docs/x.md",
"docs/../x.md",
"%2e%2e/docs/x.md",
"%2Fdocs/x.md",
r"C:\temp\x",
r"\\server\share\x",
"./docs/x.md",
"docs//x.md",
"docs/./x.md",
"docs/x.md\x00tail",
"baseline/current.json",
)
for value in rejected:
with self.subTest(value=value), self.assertRaises(PermissionError):
policy.validate_path(value, workspace)
@unittest.skipIf(os.name == "nt", "symlink creation requires Windows privilege")
def test_external_symlink_is_rejected(self):
with tempfile.TemporaryDirectory() as root, tempfile.TemporaryDirectory() as outside:
workspace = Path(root)
(workspace / "docs").symlink_to(Path(outside), target_is_directory=True)
with self.assertRaises(PermissionError):
policy.validate_path("docs/escape.md", workspace)
def test_force_and_forbidden_endpoints_are_rejected(self):
with self.assertRaises(PermissionError):
policy.validate_branch_request({
"branch": os.environ["AIOS_GITOPS_ALLOWED_BRANCH"],
"force": True,
})
for endpoint in (
"/api/tag",
"/api/rollback",
"/api/model",
"/api/model-priority",
"/api/baseline",
"/api/registry",
"/api/deploy",
):
self.assertNotIn(endpoint, policy.WRITE_ENDPOINTS)
@unittest.skipIf(os.name == "nt", "authoritative writer runs on POSIX")
def test_approved_base_is_ancestor_even_when_main_drifts(self):
with tempfile.TemporaryDirectory() as root:
workspace = Path(root) / "writer"
workspace.mkdir()
subprocess.run(["git", "init"], cwd=workspace, check=True, capture_output=True)
subprocess.run(
["git", "config", "user.email", "task-plane@example.invalid"],
cwd=workspace,
check=True,
)
subprocess.run(
["git", "config", "user.name", "Task Plane Test"],
cwd=workspace,
check=True,
)
(workspace / "README.md").write_text("base\n", encoding="utf-8")
subprocess.run(["git", "add", "README.md"], cwd=workspace, check=True)
subprocess.run(["git", "commit", "-m", "base"], cwd=workspace, check=True, capture_output=True)
base = subprocess.check_output(
["git", "rev-parse", "HEAD"], cwd=workspace, text=True
).strip()
branch = os.environ["AIOS_GITOPS_ALLOWED_BRANCH"]
subprocess.run(["git", "checkout", "-b", branch], cwd=workspace, check=True, capture_output=True)
(workspace / "README.md").write_text("branch\n", encoding="utf-8")
subprocess.run(["git", "commit", "-am", "branch"], cwd=workspace, check=True, capture_output=True)
os.environ.update(
{
"AIOS_GITOPS_ALLOWED_BASE_OID": base,
"AIOS_GITOPS_WRITES_ENABLED": "true",
"AIOS_GITOPS_RUNTIME_ROLE": "isolated-writer",
"AIOS_GITOPS_WORKSPACE_ROLE": "isolated-writer",
"AIOS_GITOPS_REPOSITORY_IDENTITY": "aios/aios",
"AIOS_GITOPS_AUTHORITATIVE_GIT_DIR": str(workspace / ".git"),
"AIOS_GITOPS_APPROVED_WRITER_ROOT": str(Path(root)),
}
)
for name in policy._DENY_FLAGS:
os.environ[name] = "false"
policy.require_service_invariants(workspace, workspace / ".git")
@unittest.skipIf(os.name == "nt", "POSIX audit mode and O_NOFOLLOW contract")
def test_audit_path_is_contained_durable_and_redacted(self):
with tempfile.TemporaryDirectory() as root:
approved = Path(root) / "approved"
approved.mkdir(mode=0o750)
log = approved / "writer.jsonl"
os.environ["AIOS_GITOPS_APPROVED_AUDIT_ROOT"] = str(approved)
os.environ["AIOS_GITOPS_AUDIT_LOG"] = str(log)
policy.append_audit_record(
{
"action": "commit",
"Authorization": "Bearer must-not-appear",
"payload": "token=must-not-appear",
}
)
text = log.read_text(encoding="utf-8")
self.assertNotIn("must-not-appear", text)
self.assertEqual(0o600, log.stat().st_mode & 0o777)
os.environ["AIOS_GITOPS_AUDIT_LOG"] = str(Path(root) / "escape.jsonl")
with self.assertRaises(PermissionError):
policy.approved_audit_log_path()
class SecretScannerBoundaryTests(unittest.TestCase):
def test_task_control_plane_branch_is_not_an_api_key(self):
import aios_chatgpt_actions_api as api
payload = (
'{"branch":"patch/aios-agent-task-control-plane-20260724",'
'"created_by":"agent-task-plane-writer"}'
)
self.assertFalse(api.content_has_secret(payload))
def test_real_openai_style_key_is_still_rejected(self):
import aios_chatgpt_actions_api as api
payload = '{"value":"sk-abcdefghijklmnopqrstuvwxyz123456"}'
self.assertTrue(api.content_has_secret(payload))