1054 lines
43 KiB
Python
1054 lines
43 KiB
Python
"""Credential-isolated external Agent runtime executor for Online Agent Tasks.
|
|
|
|
Task payloads never choose commands. A root-owned runtime profile chooses the executable and
|
|
I/O mode. The Online Agent worker writes a hash-bound request into an exchange directory; this
|
|
uncredentialed process executes Hermes/OpenClaw and returns a bounded, sanitized result.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
from dataclasses import dataclass
|
|
import hashlib
|
|
import json
|
|
import os
|
|
from pathlib import Path
|
|
import re
|
|
import subprocess
|
|
import tempfile
|
|
import time
|
|
from typing import Any, Mapping, Sequence
|
|
from aios_model_governance import (
|
|
ModelGovernanceError,
|
|
ModelGovernanceStore,
|
|
RouteRequest,
|
|
)
|
|
|
|
REQUEST_SCHEMA = "aios.external-agent-runtime.request.v1"
|
|
RESULT_SCHEMA = "aios.external-agent-runtime.result.v1"
|
|
PROFILE_SCHEMA = "aios.external-agent-runtime.profile.v1"
|
|
SUPPORTED_RUNTIMES = frozenset({"hermes", "openclaw"})
|
|
REQUEST_ID_RE = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._:-]{15,254}$")
|
|
RUNTIME_RE = re.compile(r"^[a-z][a-z0-9_-]{2,31}$")
|
|
SENSITIVE_RE = re.compile(
|
|
r"(?i)(authorization\s*[:=]\s*bearer\s+\S+|"
|
|
r"(?:password|passwd|secret|token|api[_-]?key)\s*[:=]\s*\S+)"
|
|
)
|
|
SECRET_ENV_MARKERS = (
|
|
"TOKEN",
|
|
"SECRET",
|
|
"PASSWORD",
|
|
"CREDENTIAL",
|
|
"AUTHORIZATION",
|
|
"PRIVATE_KEY",
|
|
"API_KEY",
|
|
)
|
|
PROFILE_ENV_NAME_RE = re.compile(r"^[A-Z][A-Z0-9_]{1,63}$")
|
|
PROFILE_CREDENTIAL_NAME_RE = re.compile(r"^[A-Za-z0-9][A-Za-z0-9_.:-]{2,127}$")
|
|
PROFILE_CREDENTIAL_KEY_RE = re.compile(r"^[A-Z][A-Z0-9_]{1,63}$")
|
|
PROVIDER_CREDENTIAL_ENV_NAMES = frozenset({"OPENAI_API_KEY"})
|
|
PROFILE_FIXED_ENV_DENY = frozenset(
|
|
{
|
|
"PATH",
|
|
"HOME",
|
|
"NO_COLOR",
|
|
"LC_ALL",
|
|
"LANG",
|
|
"LD_PRELOAD",
|
|
"LD_LIBRARY_PATH",
|
|
"PYTHONPATH",
|
|
"NODE_OPTIONS",
|
|
"BASH_ENV",
|
|
"SHELL",
|
|
"IFS",
|
|
}
|
|
)
|
|
MAX_REQUEST_BYTES = 256 * 1024
|
|
MAX_OUTPUT_BYTES = 128 * 1024
|
|
MAX_PROFILE_BYTES = 64 * 1024
|
|
MAX_CREDENTIAL_BYTES = 64 * 1024
|
|
DEFAULT_PROFILE_ROOT = Path("/etc/aios/external-agent-runtime.d")
|
|
DEFAULT_EXCHANGE_ROOT = Path("/var/lib/aios-external-agent-executor")
|
|
DEFAULT_MODEL_GOVERNANCE_DB = Path(
|
|
os.environ.get(
|
|
"AIOS_MODEL_GOVERNANCE_DB",
|
|
"/var/lib/aios-model-governance/state.sqlite3",
|
|
)
|
|
)
|
|
MODEL_GOVERNANCE_ENABLED = os.environ.get(
|
|
"AIOS_MODEL_GOVERNANCE_ENABLED", "0"
|
|
).strip().lower() in {"1", "true", "yes", "on"}
|
|
ALLOWED_EXECUTABLE_ROOTS = (
|
|
Path("/usr/bin"),
|
|
Path("/usr/local/bin"),
|
|
Path("/opt/aios/bin"),
|
|
Path("/opt/hermes"),
|
|
Path("/opt/openclaw"),
|
|
Path("/opt/aios-hermes-agent"),
|
|
Path("/opt/aios/openclaw-engineering"),
|
|
)
|
|
|
|
|
|
class ExternalAgentRuntimeError(RuntimeError):
|
|
pass
|
|
|
|
|
|
def canonical(value: object) -> bytes:
|
|
return json.dumps(
|
|
value, ensure_ascii=False, sort_keys=True, separators=(",", ":")
|
|
).encode("utf-8")
|
|
|
|
|
|
def sha256(data: bytes) -> str:
|
|
return hashlib.sha256(data).hexdigest()
|
|
|
|
|
|
def sanitize_text(value: str) -> str:
|
|
return SENSITIVE_RE.sub("[REDACTED]", str(value))
|
|
|
|
|
|
def _bounded_text(value: str, limit: int = MAX_OUTPUT_BYTES) -> str:
|
|
data = sanitize_text(value).encode("utf-8", "replace")
|
|
if len(data) <= limit:
|
|
return data.decode("utf-8", "replace")
|
|
marker = b"\n...[TRUNCATED]...\n"
|
|
half = max(1, (limit - len(marker)) // 2)
|
|
return (data[:half] + marker + data[-half:]).decode("utf-8", "replace")
|
|
|
|
|
|
def _safe_runtime(value: object) -> str:
|
|
runtime = str(value or "").strip().lower()
|
|
if RUNTIME_RE.fullmatch(runtime) is None or runtime not in SUPPORTED_RUNTIMES:
|
|
raise ExternalAgentRuntimeError("EXTERNAL_AGENT_RUNTIME_UNSUPPORTED")
|
|
return runtime
|
|
|
|
|
|
def _assert_no_secret_env() -> None:
|
|
leaked = sorted(
|
|
key
|
|
for key, value in os.environ.items()
|
|
if value and any(marker in key.upper() for marker in SECRET_ENV_MARKERS)
|
|
)
|
|
# CREDENTIALS_DIRECTORY itself is systemd metadata; the directory is expected to be hidden.
|
|
leaked = [key for key in leaked if key != "CREDENTIALS_DIRECTORY"]
|
|
if leaked:
|
|
raise ExternalAgentRuntimeError(
|
|
"EXTERNAL_AGENT_CREDENTIAL_ENV_EXPOSED:" + ",".join(leaked)
|
|
)
|
|
|
|
|
|
def _safe_executable(value: object) -> str:
|
|
if not isinstance(value, str) or not value.startswith("/"):
|
|
raise ExternalAgentRuntimeError("EXTERNAL_AGENT_EXECUTABLE_ABSOLUTE_REQUIRED")
|
|
path = Path(value)
|
|
if path.is_symlink():
|
|
resolved = path.resolve(strict=True)
|
|
else:
|
|
resolved = path.resolve(strict=True)
|
|
if not resolved.is_file() or not os.access(resolved, os.X_OK):
|
|
raise ExternalAgentRuntimeError("EXTERNAL_AGENT_EXECUTABLE_INVALID")
|
|
if not any(resolved == root or root in resolved.parents for root in ALLOWED_EXECUTABLE_ROOTS):
|
|
raise ExternalAgentRuntimeError("EXTERNAL_AGENT_EXECUTABLE_ROOT_DENY")
|
|
return str(resolved)
|
|
|
|
|
|
def _safe_profile_script(value: object) -> str:
|
|
if not isinstance(value, str) or not value.startswith("/"):
|
|
raise ExternalAgentRuntimeError("EXTERNAL_AGENT_SCRIPT_ABSOLUTE_REQUIRED")
|
|
path = Path(value)
|
|
if path.is_symlink():
|
|
resolved = path.resolve(strict=True)
|
|
else:
|
|
resolved = path.resolve(strict=True)
|
|
if not resolved.is_file():
|
|
raise ExternalAgentRuntimeError("EXTERNAL_AGENT_SCRIPT_INVALID")
|
|
if not any(resolved == root or root in resolved.parents for root in ALLOWED_EXECUTABLE_ROOTS):
|
|
raise ExternalAgentRuntimeError("EXTERNAL_AGENT_SCRIPT_ROOT_DENY")
|
|
return str(resolved)
|
|
|
|
|
|
def _profile_environment(value: object) -> tuple[tuple[str, str], ...]:
|
|
if value is None:
|
|
return ()
|
|
if not isinstance(value, Mapping):
|
|
raise ExternalAgentRuntimeError("EXTERNAL_AGENT_PROFILE_ENVIRONMENT_INVALID")
|
|
result: list[tuple[str, str]] = []
|
|
for raw_name, raw_value in value.items():
|
|
if not isinstance(raw_name, str) or PROFILE_ENV_NAME_RE.fullmatch(raw_name) is None:
|
|
raise ExternalAgentRuntimeError("EXTERNAL_AGENT_PROFILE_ENVIRONMENT_NAME_INVALID")
|
|
if raw_name in PROFILE_FIXED_ENV_DENY:
|
|
raise ExternalAgentRuntimeError("EXTERNAL_AGENT_PROFILE_ENVIRONMENT_NAME_DENY")
|
|
if any(marker in raw_name.upper() for marker in SECRET_ENV_MARKERS):
|
|
raise ExternalAgentRuntimeError("EXTERNAL_AGENT_PROFILE_SECRET_ENV_DENY")
|
|
if not isinstance(raw_value, str) or "\x00" in raw_value or "\n" in raw_value or "\r" in raw_value:
|
|
raise ExternalAgentRuntimeError("EXTERNAL_AGENT_PROFILE_ENVIRONMENT_VALUE_INVALID")
|
|
if len(raw_value) > 4096:
|
|
raise ExternalAgentRuntimeError("EXTERNAL_AGENT_PROFILE_ENVIRONMENT_VALUE_TOO_LARGE")
|
|
result.append((raw_name, raw_value))
|
|
return tuple(sorted(result))
|
|
|
|
|
|
def _profile_credential_environment(
|
|
value: object,
|
|
) -> tuple[tuple[str, str, str, str], ...]:
|
|
if value is None:
|
|
return ()
|
|
if not isinstance(value, Mapping):
|
|
raise ExternalAgentRuntimeError("EXTERNAL_AGENT_PROFILE_CREDENTIAL_ENV_INVALID")
|
|
result: list[tuple[str, str, str, str]] = []
|
|
for raw_env_name, raw_spec in value.items():
|
|
if (
|
|
not isinstance(raw_env_name, str)
|
|
or raw_env_name not in PROVIDER_CREDENTIAL_ENV_NAMES
|
|
or PROFILE_ENV_NAME_RE.fullmatch(raw_env_name) is None
|
|
):
|
|
raise ExternalAgentRuntimeError("EXTERNAL_AGENT_CREDENTIAL_ENV_NAME_DENY")
|
|
if not isinstance(raw_spec, Mapping):
|
|
raise ExternalAgentRuntimeError("EXTERNAL_AGENT_CREDENTIAL_SPEC_INVALID")
|
|
credential = raw_spec.get("credential")
|
|
fmt = str(raw_spec.get("format") or "dotenv")
|
|
key = raw_spec.get("key")
|
|
if (
|
|
not isinstance(credential, str)
|
|
or PROFILE_CREDENTIAL_NAME_RE.fullmatch(credential) is None
|
|
):
|
|
raise ExternalAgentRuntimeError("EXTERNAL_AGENT_CREDENTIAL_NAME_INVALID")
|
|
if fmt not in {"dotenv", "raw"}:
|
|
raise ExternalAgentRuntimeError("EXTERNAL_AGENT_CREDENTIAL_FORMAT_INVALID")
|
|
if fmt == "dotenv":
|
|
if not isinstance(key, str) or PROFILE_CREDENTIAL_KEY_RE.fullmatch(key) is None:
|
|
raise ExternalAgentRuntimeError("EXTERNAL_AGENT_CREDENTIAL_KEY_INVALID")
|
|
elif key is not None:
|
|
raise ExternalAgentRuntimeError("EXTERNAL_AGENT_CREDENTIAL_KEY_UNEXPECTED")
|
|
result.append((raw_env_name, credential, fmt, str(key or "")))
|
|
return tuple(sorted(result))
|
|
|
|
|
|
def _profile_model_governance(
|
|
value: object,
|
|
) -> tuple[bool, tuple[str, ...], tuple[str, ...], int, int]:
|
|
if value is None:
|
|
return False, (), (), 0, 0
|
|
if not isinstance(value, Mapping):
|
|
raise ExternalAgentRuntimeError("EXTERNAL_AGENT_MODEL_GOVERNANCE_PROFILE_INVALID")
|
|
allowed = {
|
|
"required",
|
|
"provider_ids",
|
|
"model_capabilities",
|
|
"max_output_tokens",
|
|
"input_reservation_floor_tokens",
|
|
}
|
|
if set(value) - allowed:
|
|
raise ExternalAgentRuntimeError("EXTERNAL_AGENT_MODEL_GOVERNANCE_PROFILE_FIELDS_INVALID")
|
|
required = bool(value.get("required", False))
|
|
providers = value.get("provider_ids") or []
|
|
capabilities = value.get("model_capabilities") or []
|
|
if (
|
|
not isinstance(providers, Sequence)
|
|
or isinstance(providers, (str, bytes))
|
|
or not all(isinstance(item, str) and item.strip() for item in providers)
|
|
):
|
|
raise ExternalAgentRuntimeError("EXTERNAL_AGENT_MODEL_PROVIDER_IDS_INVALID")
|
|
if (
|
|
not isinstance(capabilities, Sequence)
|
|
or isinstance(capabilities, (str, bytes))
|
|
or not all(isinstance(item, str) and item.strip() for item in capabilities)
|
|
):
|
|
raise ExternalAgentRuntimeError("EXTERNAL_AGENT_MODEL_CAPABILITIES_INVALID")
|
|
max_output_tokens = int(value.get("max_output_tokens") or 0)
|
|
input_reservation_floor_tokens = int(
|
|
value.get("input_reservation_floor_tokens") or 0
|
|
)
|
|
if max_output_tokens < 0 or max_output_tokens > 262144:
|
|
raise ExternalAgentRuntimeError("EXTERNAL_AGENT_MODEL_OUTPUT_LIMIT_INVALID")
|
|
if (
|
|
input_reservation_floor_tokens < 0
|
|
or input_reservation_floor_tokens > 262144
|
|
):
|
|
raise ExternalAgentRuntimeError(
|
|
"EXTERNAL_AGENT_MODEL_INPUT_RESERVATION_FLOOR_INVALID"
|
|
)
|
|
if required and not providers:
|
|
raise ExternalAgentRuntimeError("EXTERNAL_AGENT_MODEL_PROVIDER_IDS_REQUIRED")
|
|
if required and max_output_tokens < 1:
|
|
raise ExternalAgentRuntimeError("EXTERNAL_AGENT_MODEL_OUTPUT_LIMIT_REQUIRED")
|
|
return (
|
|
required,
|
|
tuple(sorted({item.strip() for item in providers})),
|
|
tuple(sorted({item.strip() for item in capabilities})),
|
|
max_output_tokens,
|
|
input_reservation_floor_tokens,
|
|
)
|
|
|
|
|
|
def _estimate_tokens(text: str) -> int:
|
|
# Conservative byte-based estimate; runtime-reported usage supersedes this at settlement.
|
|
data = text.encode("utf-8", "replace")
|
|
return max(1, (len(data) + 3) // 4)
|
|
|
|
|
|
def _extract_runtime_usage(
|
|
output_json: object,
|
|
) -> tuple[int | None, int | None, int | None]:
|
|
if not isinstance(output_json, Mapping):
|
|
return None, None, None
|
|
|
|
candidates: list[Mapping[str, Any]] = []
|
|
direct = output_json.get("usage")
|
|
if isinstance(direct, Mapping):
|
|
candidates.append(direct)
|
|
|
|
meta = output_json.get("meta")
|
|
if isinstance(meta, Mapping):
|
|
agent_meta = meta.get("agentMeta")
|
|
if isinstance(agent_meta, Mapping):
|
|
nested = agent_meta.get("usage")
|
|
if isinstance(nested, Mapping):
|
|
candidates.append(nested)
|
|
last_call = agent_meta.get("lastCallUsage")
|
|
if isinstance(last_call, Mapping):
|
|
candidates.append(last_call)
|
|
|
|
def number(mapping: Mapping[str, Any], *names: str) -> int | None:
|
|
for name in names:
|
|
value = mapping.get(name)
|
|
if isinstance(value, int) and not isinstance(value, bool) and value >= 0:
|
|
return value
|
|
return None
|
|
|
|
input_tokens = None
|
|
output_tokens = None
|
|
cost_micros = None
|
|
for usage in candidates:
|
|
if input_tokens is None:
|
|
input_tokens = number(usage, "input_tokens", "prompt_tokens", "input")
|
|
if output_tokens is None:
|
|
output_tokens = number(
|
|
usage, "output_tokens", "completion_tokens", "output"
|
|
)
|
|
if cost_micros is None:
|
|
cost_micros = number(usage, "cost_micros", "total_cost_micros")
|
|
|
|
if cost_micros is None and isinstance(meta, Mapping):
|
|
agent_meta = meta.get("agentMeta")
|
|
if isinstance(agent_meta, Mapping):
|
|
cost_usd = agent_meta.get("costUsd")
|
|
if (
|
|
isinstance(cost_usd, (int, float))
|
|
and not isinstance(cost_usd, bool)
|
|
and cost_usd >= 0
|
|
):
|
|
cost_micros = int(round(float(cost_usd) * 1_000_000))
|
|
|
|
return input_tokens, output_tokens, cost_micros
|
|
|
|
|
|
def _scope_from_payload(payload: Mapping[str, Any]) -> tuple[str, str | None, str | None, str]:
|
|
scope = payload.get("scope")
|
|
if not isinstance(scope, Mapping):
|
|
raise ExternalAgentRuntimeError("EXTERNAL_AGENT_MODEL_SCOPE_MISSING")
|
|
scope_type = str(scope.get("scope_type") or "legacy")
|
|
tenant_id = scope.get("tenant_id")
|
|
project_id = scope.get("project_id")
|
|
trace_id = str(payload.get("trace_id") or "").strip()
|
|
if not trace_id:
|
|
raise ExternalAgentRuntimeError("EXTERNAL_AGENT_MODEL_TRACE_ID_MISSING")
|
|
return (
|
|
scope_type,
|
|
str(tenant_id) if tenant_id not in (None, "") else None,
|
|
str(project_id) if project_id not in (None, "") else None,
|
|
trace_id,
|
|
)
|
|
|
|
|
|
def _model_replacements(decision: object | None) -> dict[str, str]:
|
|
if decision is None:
|
|
return {}
|
|
return {
|
|
"{provider_id}": str(getattr(decision, "provider_id")),
|
|
"{model_id}": str(getattr(decision, "model_id")),
|
|
"{model_version}": str(getattr(decision, "model_version")),
|
|
}
|
|
|
|
|
|
def _bind_model_values(
|
|
values: Sequence[tuple[str, str]],
|
|
decision: object | None,
|
|
) -> tuple[tuple[str, str], ...]:
|
|
replacements = _model_replacements(decision)
|
|
result: list[tuple[str, str]] = []
|
|
for key, value in values:
|
|
bound = value
|
|
for marker, replacement in replacements.items():
|
|
bound = bound.replace(marker, replacement)
|
|
result.append((key, bound))
|
|
return tuple(result)
|
|
|
|
|
|
def _runtime_model_binding_present(profile: "RuntimeProfile") -> bool:
|
|
markers = ("{model_id}", "{model_version}", "{provider_id}")
|
|
return any(marker in item for item in (*profile.argv, *(value for _, value in profile.environment)) for marker in markers)
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class RuntimeProfile:
|
|
runtime: str
|
|
argv: tuple[str, ...]
|
|
input_mode: str
|
|
output_mode: str
|
|
timeout_seconds: int
|
|
max_output_bytes: int
|
|
environment: tuple[tuple[str, str], ...]
|
|
credential_env: tuple[tuple[str, str, str, str], ...]
|
|
model_governance_required: bool
|
|
model_provider_ids: tuple[str, ...]
|
|
model_capabilities: tuple[str, ...]
|
|
model_max_output_tokens: int
|
|
model_input_reservation_floor_tokens: int
|
|
|
|
@classmethod
|
|
def load(
|
|
cls,
|
|
runtime: str,
|
|
profile_root: Path = DEFAULT_PROFILE_ROOT,
|
|
) -> "RuntimeProfile":
|
|
runtime = _safe_runtime(runtime)
|
|
root = Path(profile_root)
|
|
if root.is_symlink():
|
|
raise ExternalAgentRuntimeError("EXTERNAL_AGENT_PROFILE_ROOT_SYMLINK_DENY")
|
|
path = root / f"{runtime}.json"
|
|
if path.is_symlink() or not path.is_file() or path.stat().st_size > MAX_PROFILE_BYTES:
|
|
raise ExternalAgentRuntimeError("EXTERNAL_AGENT_PROFILE_UNAVAILABLE")
|
|
try:
|
|
value = json.loads(path.read_text(encoding="utf-8"))
|
|
except json.JSONDecodeError as exc:
|
|
raise ExternalAgentRuntimeError("EXTERNAL_AGENT_PROFILE_INVALID") from exc
|
|
if not isinstance(value, Mapping) or value.get("schema") != PROFILE_SCHEMA:
|
|
raise ExternalAgentRuntimeError("EXTERNAL_AGENT_PROFILE_INVALID")
|
|
if _safe_runtime(value.get("runtime")) != runtime:
|
|
raise ExternalAgentRuntimeError("EXTERNAL_AGENT_PROFILE_RUNTIME_MISMATCH")
|
|
argv = value.get("argv")
|
|
if (
|
|
not isinstance(argv, Sequence)
|
|
or isinstance(argv, (str, bytes))
|
|
or not argv
|
|
or not all(isinstance(item, str) and item and "\x00" not in item for item in argv)
|
|
):
|
|
raise ExternalAgentRuntimeError("EXTERNAL_AGENT_PROFILE_ARGV_INVALID")
|
|
executable = _safe_executable(argv[0])
|
|
normalized = [executable, *list(argv[1:])]
|
|
if any(item in {"sh", "bash", "-c", "--command"} for item in normalized[1:]):
|
|
raise ExternalAgentRuntimeError("EXTERNAL_AGENT_SHELL_MODE_DENY")
|
|
joined_argv = " ".join(normalized)
|
|
if re.search(r"(?i)(token|secret|password|api[_-]?key|authorization)\s*[:=]", joined_argv):
|
|
raise ExternalAgentRuntimeError("EXTERNAL_AGENT_PROFILE_SECRET_ARG_DENY")
|
|
if Path(executable).name in {"node", "nodejs"}:
|
|
if len(normalized) < 2 or normalized[1].startswith("-"):
|
|
raise ExternalAgentRuntimeError("EXTERNAL_AGENT_NODE_SCRIPT_REQUIRED")
|
|
normalized[1] = _safe_profile_script(normalized[1])
|
|
if any(item in {"-e", "--eval", "-p", "--print", "-r", "--require"} for item in normalized[1:]):
|
|
raise ExternalAgentRuntimeError("EXTERNAL_AGENT_NODE_CODE_MODE_DENY")
|
|
input_mode = str(value.get("input_mode") or "stdin_text")
|
|
output_mode = str(value.get("output_mode") or "stdout_text")
|
|
if input_mode not in {"stdin_text", "stdin_json", "file_json"}:
|
|
raise ExternalAgentRuntimeError("EXTERNAL_AGENT_INPUT_MODE_INVALID")
|
|
if output_mode not in {"stdout_text", "stdout_json", "file_json"}:
|
|
raise ExternalAgentRuntimeError("EXTERNAL_AGENT_OUTPUT_MODE_INVALID")
|
|
timeout = int(value.get("timeout_seconds") or 900)
|
|
max_output = int(value.get("max_output_bytes") or MAX_OUTPUT_BYTES)
|
|
if not 1 <= timeout <= 3600:
|
|
raise ExternalAgentRuntimeError("EXTERNAL_AGENT_TIMEOUT_INVALID")
|
|
if not 1024 <= max_output <= MAX_OUTPUT_BYTES:
|
|
raise ExternalAgentRuntimeError("EXTERNAL_AGENT_OUTPUT_LIMIT_INVALID")
|
|
environment = _profile_environment(value.get("environment"))
|
|
credential_env = _profile_credential_environment(value.get("credential_env"))
|
|
(
|
|
model_governance_required,
|
|
model_provider_ids,
|
|
model_capabilities,
|
|
model_max_output_tokens,
|
|
model_input_reservation_floor_tokens,
|
|
) = _profile_model_governance(value.get("model_governance"))
|
|
profile = cls(
|
|
runtime,
|
|
tuple(normalized),
|
|
input_mode,
|
|
output_mode,
|
|
timeout,
|
|
max_output,
|
|
environment,
|
|
credential_env,
|
|
model_governance_required,
|
|
model_provider_ids,
|
|
model_capabilities,
|
|
model_max_output_tokens,
|
|
model_input_reservation_floor_tokens,
|
|
)
|
|
if profile.model_governance_required and not _runtime_model_binding_present(profile):
|
|
raise ExternalAgentRuntimeError("EXTERNAL_AGENT_MODEL_BINDING_REQUIRED")
|
|
return profile
|
|
|
|
|
|
def validate_request(value: Mapping[str, Any]) -> dict[str, Any]:
|
|
if value.get("schema") != REQUEST_SCHEMA:
|
|
raise ExternalAgentRuntimeError("EXTERNAL_AGENT_REQUEST_SCHEMA_INVALID")
|
|
request_id = str(value.get("request_id") or "")
|
|
if REQUEST_ID_RE.fullmatch(request_id) is None:
|
|
raise ExternalAgentRuntimeError("EXTERNAL_AGENT_REQUEST_ID_INVALID")
|
|
runtime = _safe_runtime(value.get("runtime"))
|
|
task_id = str(value.get("task_id") or "")
|
|
attempt = value.get("attempt")
|
|
execution_digest = str(value.get("execution_digest") or "")
|
|
if not task_id or not re.fullmatch(r"[A-Za-z0-9][A-Za-z0-9._:-]{2,179}", task_id):
|
|
raise ExternalAgentRuntimeError("EXTERNAL_AGENT_TASK_ID_INVALID")
|
|
if (
|
|
not isinstance(attempt, int)
|
|
or isinstance(attempt, bool)
|
|
or not 1 <= attempt <= 1000
|
|
):
|
|
raise ExternalAgentRuntimeError("EXTERNAL_AGENT_ATTEMPT_INVALID")
|
|
if re.fullmatch(r"[0-9a-f]{64}", execution_digest) is None:
|
|
raise ExternalAgentRuntimeError("EXTERNAL_AGENT_EXECUTION_DIGEST_INVALID")
|
|
payload = value.get("payload")
|
|
if not isinstance(payload, Mapping):
|
|
raise ExternalAgentRuntimeError("EXTERNAL_AGENT_PAYLOAD_INVALID")
|
|
encoded = canonical(payload)
|
|
if len(encoded) > MAX_REQUEST_BYTES:
|
|
raise ExternalAgentRuntimeError("EXTERNAL_AGENT_PAYLOAD_TOO_LARGE")
|
|
goal = payload.get("goal")
|
|
instructions = payload.get("instructions")
|
|
if not isinstance(goal, str) or not goal.strip():
|
|
raise ExternalAgentRuntimeError("EXTERNAL_AGENT_GOAL_REQUIRED")
|
|
if instructions is not None and not isinstance(instructions, str):
|
|
raise ExternalAgentRuntimeError("EXTERNAL_AGENT_INSTRUCTIONS_INVALID")
|
|
return {
|
|
"schema": REQUEST_SCHEMA,
|
|
"request_id": request_id,
|
|
"runtime": runtime,
|
|
"task_id": task_id,
|
|
"attempt": attempt,
|
|
"execution_digest": execution_digest,
|
|
"payload": dict(payload),
|
|
}
|
|
|
|
|
|
def render_prompt(payload: Mapping[str, Any]) -> str:
|
|
goal = str(payload.get("goal") or "").strip()
|
|
instructions = str(payload.get("instructions") or "").strip()
|
|
criteria = payload.get("success_criteria") or []
|
|
constraints = payload.get("constraints") or {}
|
|
lines = [
|
|
"AIOS Online Agent Task",
|
|
"",
|
|
"Goal:",
|
|
goal,
|
|
]
|
|
if instructions:
|
|
lines.extend(["", "Instructions:", instructions])
|
|
if criteria:
|
|
lines.extend(["", "Success criteria:", json.dumps(criteria, ensure_ascii=False, sort_keys=True)])
|
|
if constraints:
|
|
lines.extend(["", "Constraints:", json.dumps(constraints, ensure_ascii=False, sort_keys=True)])
|
|
lines.extend([
|
|
"",
|
|
"Return the final task result only. Do not claim repository, production, credential, or control-plane side effects unless they are explicitly represented in the task payload.",
|
|
])
|
|
return "\n".join(lines)
|
|
|
|
|
|
def _argv_with_files(
|
|
profile: RuntimeProfile,
|
|
input_path: Path,
|
|
output_path: Path,
|
|
prompt: str,
|
|
model_decision: object | None = None,
|
|
) -> list[str]:
|
|
replacements = {
|
|
"{input_json}": str(input_path),
|
|
"{output_json}": str(output_path),
|
|
"{prompt}": prompt,
|
|
**_model_replacements(model_decision),
|
|
}
|
|
result = []
|
|
for item in profile.argv:
|
|
for marker, replacement in replacements.items():
|
|
item = item.replace(marker, replacement)
|
|
result.append(item)
|
|
if profile.input_mode == "file_json" and not any("{input_json}" in item for item in profile.argv):
|
|
raise ExternalAgentRuntimeError("EXTERNAL_AGENT_INPUT_PLACEHOLDER_REQUIRED")
|
|
if profile.output_mode == "file_json" and not any("{output_json}" in item for item in profile.argv):
|
|
raise ExternalAgentRuntimeError("EXTERNAL_AGENT_OUTPUT_PLACEHOLDER_REQUIRED")
|
|
return result
|
|
|
|
|
|
def _credential_environment(profile: RuntimeProfile) -> dict[str, str]:
|
|
if not profile.credential_env:
|
|
return {}
|
|
raw_root = os.environ.get("CREDENTIALS_DIRECTORY")
|
|
if not raw_root or not raw_root.startswith("/"):
|
|
raise ExternalAgentRuntimeError("EXTERNAL_AGENT_CREDENTIAL_DIRECTORY_MISSING")
|
|
root = Path(raw_root)
|
|
if root.is_symlink() or not root.is_dir():
|
|
raise ExternalAgentRuntimeError("EXTERNAL_AGENT_CREDENTIAL_DIRECTORY_INVALID")
|
|
result: dict[str, str] = {}
|
|
for env_name, credential, fmt, key in profile.credential_env:
|
|
path = root / credential
|
|
if path.is_symlink() or not path.is_file():
|
|
raise ExternalAgentRuntimeError("EXTERNAL_AGENT_CREDENTIAL_FILE_INVALID")
|
|
try:
|
|
if path.stat().st_size > MAX_CREDENTIAL_BYTES:
|
|
raise ExternalAgentRuntimeError("EXTERNAL_AGENT_CREDENTIAL_TOO_LARGE")
|
|
raw = path.read_bytes()
|
|
except OSError as exc:
|
|
raise ExternalAgentRuntimeError("EXTERNAL_AGENT_CREDENTIAL_READ_FAILED") from exc
|
|
if b"\x00" in raw:
|
|
raise ExternalAgentRuntimeError("EXTERNAL_AGENT_CREDENTIAL_CONTENT_INVALID")
|
|
try:
|
|
text = raw.decode("utf-8")
|
|
except UnicodeDecodeError as exc:
|
|
raise ExternalAgentRuntimeError("EXTERNAL_AGENT_CREDENTIAL_CONTENT_INVALID") from exc
|
|
selected = ""
|
|
if fmt == "raw":
|
|
selected = text.strip()
|
|
else:
|
|
found = False
|
|
for line in text.splitlines():
|
|
candidate = line.strip()
|
|
if not candidate or candidate.startswith("#"):
|
|
continue
|
|
if candidate.startswith("export "):
|
|
candidate = candidate[7:].lstrip()
|
|
if "=" not in candidate:
|
|
continue
|
|
candidate_key, candidate_value = candidate.split("=", 1)
|
|
if candidate_key.strip() != key:
|
|
continue
|
|
if found:
|
|
raise ExternalAgentRuntimeError("EXTERNAL_AGENT_CREDENTIAL_KEY_DUPLICATE")
|
|
selected = candidate_value.strip()
|
|
if len(selected) >= 2 and selected[0] == selected[-1] and selected[0] in {"'", '"'}:
|
|
selected = selected[1:-1]
|
|
found = True
|
|
if not found:
|
|
raise ExternalAgentRuntimeError("EXTERNAL_AGENT_CREDENTIAL_KEY_MISSING")
|
|
if not selected or "\x00" in selected or "\n" in selected or "\r" in selected:
|
|
raise ExternalAgentRuntimeError("EXTERNAL_AGENT_CREDENTIAL_VALUE_INVALID")
|
|
if len(selected) > MAX_CREDENTIAL_BYTES:
|
|
raise ExternalAgentRuntimeError("EXTERNAL_AGENT_CREDENTIAL_VALUE_TOO_LARGE")
|
|
result[env_name] = selected
|
|
return result
|
|
|
|
|
|
def _parse_output(
|
|
profile: RuntimeProfile,
|
|
stdout: str,
|
|
output_path: Path,
|
|
) -> tuple[str, object]:
|
|
if profile.output_mode == "file_json":
|
|
if output_path.is_symlink() or not output_path.is_file():
|
|
raise ExternalAgentRuntimeError("EXTERNAL_AGENT_OUTPUT_FILE_MISSING")
|
|
raw = output_path.read_bytes()
|
|
if len(raw) > profile.max_output_bytes:
|
|
raise ExternalAgentRuntimeError("EXTERNAL_AGENT_OUTPUT_TOO_LARGE")
|
|
text = raw.decode("utf-8", "replace")
|
|
try:
|
|
parsed = json.loads(text)
|
|
except json.JSONDecodeError as exc:
|
|
raise ExternalAgentRuntimeError("EXTERNAL_AGENT_OUTPUT_JSON_INVALID") from exc
|
|
return _bounded_text(text, profile.max_output_bytes), parsed
|
|
if len(stdout.encode("utf-8", "replace")) > profile.max_output_bytes:
|
|
stdout = _bounded_text(stdout, profile.max_output_bytes)
|
|
if profile.output_mode == "stdout_json":
|
|
try:
|
|
parsed = json.loads(stdout)
|
|
except json.JSONDecodeError as exc:
|
|
raise ExternalAgentRuntimeError("EXTERNAL_AGENT_OUTPUT_JSON_INVALID") from exc
|
|
return _bounded_text(stdout, profile.max_output_bytes), parsed
|
|
return _bounded_text(stdout, profile.max_output_bytes), None
|
|
|
|
|
|
def execute_request(
|
|
request_path: Path,
|
|
result_path: Path,
|
|
*,
|
|
profile_root: Path = DEFAULT_PROFILE_ROOT,
|
|
) -> None:
|
|
_assert_no_secret_env()
|
|
if request_path.is_symlink() or not request_path.is_file():
|
|
raise ExternalAgentRuntimeError("EXTERNAL_AGENT_REQUEST_FILE_INVALID")
|
|
raw_request = request_path.read_bytes()
|
|
if len(raw_request) > MAX_REQUEST_BYTES:
|
|
raise ExternalAgentRuntimeError("EXTERNAL_AGENT_REQUEST_TOO_LARGE")
|
|
try:
|
|
request = validate_request(json.loads(raw_request.decode("utf-8")))
|
|
except json.JSONDecodeError as exc:
|
|
raise ExternalAgentRuntimeError("EXTERNAL_AGENT_REQUEST_JSON_INVALID") from exc
|
|
|
|
profile = RuntimeProfile.load(request["runtime"], profile_root)
|
|
governance_store: ModelGovernanceStore | None = None
|
|
route_request: RouteRequest | None = None
|
|
decision = None
|
|
governance_attempts: list[dict[str, Any]] = []
|
|
|
|
with tempfile.TemporaryDirectory(prefix=f"aios-{request['runtime']}-") as temporary:
|
|
temp_root = Path(temporary)
|
|
(temp_root / "home").mkdir()
|
|
input_path = temp_root / "input.json"
|
|
output_path = temp_root / "output.json"
|
|
input_path.write_bytes(canonical(request["payload"]))
|
|
prompt = render_prompt(request["payload"])
|
|
|
|
if profile.model_governance_required:
|
|
if not MODEL_GOVERNANCE_ENABLED:
|
|
raise ExternalAgentRuntimeError("EXTERNAL_AGENT_MODEL_GOVERNANCE_DISABLED")
|
|
scope_type, tenant_id, project_id, trace_id = _scope_from_payload(request["payload"])
|
|
governance_store = ModelGovernanceStore(DEFAULT_MODEL_GOVERNANCE_DB)
|
|
prompt_estimated_input_tokens = _estimate_tokens(prompt)
|
|
reserved_estimated_input_tokens = max(
|
|
prompt_estimated_input_tokens,
|
|
profile.model_input_reservation_floor_tokens,
|
|
)
|
|
route_request = RouteRequest(
|
|
request_id=request["request_id"] + ":model",
|
|
task_id=request["task_id"],
|
|
trace_id=trace_id,
|
|
scope_type=scope_type,
|
|
tenant_id=tenant_id,
|
|
project_id=project_id,
|
|
required_capabilities=tuple(
|
|
sorted(
|
|
{
|
|
f"agent.runtime.{request['runtime']}",
|
|
*profile.model_capabilities,
|
|
}
|
|
)
|
|
),
|
|
estimated_input_tokens=reserved_estimated_input_tokens,
|
|
max_output_tokens=profile.model_max_output_tokens,
|
|
)
|
|
try:
|
|
decision = governance_store.route(route_request)
|
|
except ModelGovernanceError as exc:
|
|
raise ExternalAgentRuntimeError(exc.code) from exc
|
|
if profile.model_provider_ids and decision.provider_id not in profile.model_provider_ids:
|
|
raise ExternalAgentRuntimeError("EXTERNAL_AGENT_MODEL_PROVIDER_PROFILE_DENY")
|
|
governance_attempts.append(
|
|
{
|
|
"request_id": decision.request_id,
|
|
"provider_id": decision.provider_id,
|
|
"model_id": decision.model_id,
|
|
"model_version": decision.model_version,
|
|
"fallback_index": decision.fallback_index,
|
|
}
|
|
)
|
|
|
|
try:
|
|
while True:
|
|
argv = _argv_with_files(
|
|
profile,
|
|
input_path,
|
|
output_path,
|
|
prompt,
|
|
decision,
|
|
)
|
|
stdin_data = None
|
|
if profile.input_mode == "stdin_json":
|
|
stdin_data = canonical(request["payload"]).decode("utf-8")
|
|
elif profile.input_mode == "stdin_text":
|
|
stdin_data = prompt
|
|
|
|
child_env = {
|
|
"PATH": "/usr/bin:/usr/local/bin:/bin",
|
|
"LC_ALL": "C.UTF-8",
|
|
"LANG": "C.UTF-8",
|
|
"HOME": str(temp_root / "home"),
|
|
"NO_COLOR": "1",
|
|
}
|
|
child_env.update(dict(_bind_model_values(profile.environment, decision)))
|
|
child_env.update(_credential_environment(profile))
|
|
|
|
started = time.monotonic()
|
|
failure_class = None
|
|
try:
|
|
completed = subprocess.run(
|
|
argv,
|
|
input=stdin_data,
|
|
capture_output=True,
|
|
text=True,
|
|
check=False,
|
|
shell=False,
|
|
timeout=profile.timeout_seconds,
|
|
cwd=str(temp_root),
|
|
env=child_env,
|
|
)
|
|
if completed.returncode != 0:
|
|
failure_class = f"process_exit_{completed.returncode}"
|
|
except subprocess.TimeoutExpired:
|
|
completed = None
|
|
failure_class = "timeout"
|
|
|
|
if failure_class is None and completed is not None:
|
|
break
|
|
|
|
if governance_store is None or decision is None or route_request is None:
|
|
if failure_class == "timeout":
|
|
raise ExternalAgentRuntimeError("EXTERNAL_AGENT_TIMEOUT")
|
|
raise ExternalAgentRuntimeError(
|
|
f"EXTERNAL_AGENT_PROCESS_FAILED:{completed.returncode}:"
|
|
+ _bounded_text(completed.stderr, 2048)
|
|
)
|
|
|
|
try:
|
|
governance_store.record_failure(decision.request_id, failure_class)
|
|
decision = governance_store.fallback(route_request, decision.request_id)
|
|
except ModelGovernanceError as exc:
|
|
raise ExternalAgentRuntimeError(exc.code) from exc
|
|
if profile.model_provider_ids and decision.provider_id not in profile.model_provider_ids:
|
|
raise ExternalAgentRuntimeError("EXTERNAL_AGENT_MODEL_PROVIDER_PROFILE_DENY")
|
|
governance_attempts.append(
|
|
{
|
|
"request_id": decision.request_id,
|
|
"provider_id": decision.provider_id,
|
|
"model_id": decision.model_id,
|
|
"model_version": decision.model_version,
|
|
"fallback_index": decision.fallback_index,
|
|
"previous_failure_class": failure_class,
|
|
}
|
|
)
|
|
|
|
output_text, output_json = _parse_output(profile, completed.stdout, output_path)
|
|
model_governance: dict[str, Any] | None = None
|
|
if governance_store is not None and decision is not None:
|
|
input_tokens, output_tokens, actual_cost = _extract_runtime_usage(output_json)
|
|
try:
|
|
settlement = governance_store.record_success(
|
|
decision.request_id,
|
|
input_tokens=input_tokens,
|
|
output_tokens=output_tokens,
|
|
actual_cost_micros=actual_cost,
|
|
usage_source="runtime",
|
|
)
|
|
except ModelGovernanceError as exc:
|
|
raise ExternalAgentRuntimeError(exc.code) from exc
|
|
model_governance = {
|
|
"decision_id": decision.decision_id,
|
|
"request_id": decision.request_id,
|
|
"provider_id": decision.provider_id,
|
|
"model_id": decision.model_id,
|
|
"model_version": decision.model_version,
|
|
"policy_revision": decision.policy_revision,
|
|
"model_revision": decision.model_revision,
|
|
"fallback_index": decision.fallback_index,
|
|
"prompt_estimated_input_tokens": prompt_estimated_input_tokens,
|
|
"input_reservation_floor_tokens": (
|
|
profile.model_input_reservation_floor_tokens
|
|
),
|
|
"reserved_estimated_input_tokens": (
|
|
reserved_estimated_input_tokens
|
|
),
|
|
"settlement": settlement,
|
|
"attempts": governance_attempts,
|
|
"audit_chain_valid": governance_store.verify_audit_chain(),
|
|
}
|
|
|
|
result: dict[str, Any] = {
|
|
"schema": RESULT_SCHEMA,
|
|
"request_id": request["request_id"],
|
|
"request_sha256": sha256(canonical(request)),
|
|
"runtime": request["runtime"],
|
|
"task_id": request["task_id"],
|
|
"attempt": request["attempt"],
|
|
"execution_digest": request["execution_digest"],
|
|
"exit_code": completed.returncode,
|
|
"duration_ms": int((time.monotonic() - started) * 1000),
|
|
"output_text": output_text,
|
|
"output_sha256": sha256(output_text.encode("utf-8", "replace")),
|
|
"stderr_sha256": sha256(completed.stderr.encode("utf-8", "replace")),
|
|
"credential_material_exposed": False,
|
|
"control_plane_access_granted": False,
|
|
}
|
|
if output_json is not None:
|
|
result["output_json"] = output_json
|
|
if model_governance is not None:
|
|
result["model_governance"] = model_governance
|
|
result["result_sha256"] = sha256(canonical(result))
|
|
result_path.parent.mkdir(parents=True, exist_ok=True)
|
|
fd, name = tempfile.mkstemp(prefix=f".{result_path.name}.", dir=result_path.parent)
|
|
try:
|
|
os.fchmod(fd, 0o640)
|
|
with os.fdopen(fd, "w", encoding="utf-8") as handle:
|
|
json.dump(
|
|
result,
|
|
handle,
|
|
ensure_ascii=False,
|
|
sort_keys=True,
|
|
separators=(",", ":"),
|
|
)
|
|
handle.write("\n")
|
|
handle.flush()
|
|
os.fsync(handle.fileno())
|
|
os.replace(name, result_path)
|
|
finally:
|
|
if os.path.exists(name):
|
|
os.unlink(name)
|
|
finally:
|
|
if governance_store is not None:
|
|
governance_store.close()
|
|
|
|
|
|
class ExternalAgentExchange:
|
|
"""Worker-side idempotent request/result exchange."""
|
|
|
|
def __init__(self, root: Path = DEFAULT_EXCHANGE_ROOT) -> None:
|
|
self.root = Path(root)
|
|
|
|
def execute(
|
|
self,
|
|
*,
|
|
request_id: str,
|
|
runtime: str,
|
|
task_id: str,
|
|
attempt: int,
|
|
execution_digest: str,
|
|
payload: Mapping[str, Any],
|
|
timeout_seconds: int = 1800,
|
|
) -> dict[str, Any]:
|
|
request = validate_request(
|
|
{
|
|
"schema": REQUEST_SCHEMA,
|
|
"request_id": request_id,
|
|
"runtime": runtime,
|
|
"task_id": task_id,
|
|
"attempt": attempt,
|
|
"execution_digest": execution_digest,
|
|
"payload": dict(payload),
|
|
}
|
|
)
|
|
requests = self.root / "requests"
|
|
results = self.root / "results"
|
|
failed = self.root / "failed"
|
|
for directory in (requests, results, failed):
|
|
directory.mkdir(parents=True, exist_ok=True)
|
|
request_path = requests / f"{request_id}.json"
|
|
result_path = results / f"{request_id}.json"
|
|
failed_path = failed / f"{request_id}.json"
|
|
request_sha = sha256(canonical(request))
|
|
|
|
if result_path.is_file():
|
|
return self._validated_result(result_path, request, request_sha)
|
|
if failed_path.is_file():
|
|
value = json.loads(failed_path.read_text(encoding="utf-8"))
|
|
raise ExternalAgentRuntimeError(
|
|
str(value.get("error") or "EXTERNAL_AGENT_EXECUTOR_FAILED")
|
|
)
|
|
|
|
if request_path.exists():
|
|
try:
|
|
existing = json.loads(request_path.read_text(encoding="utf-8"))
|
|
except json.JSONDecodeError as exc:
|
|
raise ExternalAgentRuntimeError("EXTERNAL_AGENT_EXISTING_REQUEST_INVALID") from exc
|
|
if sha256(canonical(existing)) != request_sha:
|
|
raise ExternalAgentRuntimeError("EXTERNAL_AGENT_REQUEST_ID_CONFLICT")
|
|
else:
|
|
fd, name = tempfile.mkstemp(prefix=f".{request_path.name}.", dir=requests)
|
|
try:
|
|
os.fchmod(fd, 0o640)
|
|
with os.fdopen(fd, "w", encoding="utf-8") as handle:
|
|
json.dump(request, handle, ensure_ascii=False, sort_keys=True, separators=(",", ":"))
|
|
handle.write("\n")
|
|
handle.flush()
|
|
os.fsync(handle.fileno())
|
|
os.replace(name, request_path)
|
|
finally:
|
|
if os.path.exists(name):
|
|
os.unlink(name)
|
|
|
|
deadline = time.monotonic() + timeout_seconds
|
|
while time.monotonic() < deadline:
|
|
if result_path.is_file():
|
|
return self._validated_result(result_path, request, request_sha)
|
|
if failed_path.is_file():
|
|
try:
|
|
value = json.loads(failed_path.read_text(encoding="utf-8"))
|
|
except json.JSONDecodeError:
|
|
value = {"error": "EXTERNAL_AGENT_EXECUTOR_FAILED"}
|
|
raise ExternalAgentRuntimeError(
|
|
str(value.get("error") or "EXTERNAL_AGENT_EXECUTOR_FAILED")
|
|
)
|
|
time.sleep(0.25)
|
|
raise ExternalAgentRuntimeError("EXTERNAL_AGENT_EXCHANGE_TIMEOUT")
|
|
|
|
@staticmethod
|
|
def _validated_result(
|
|
result_path: Path,
|
|
request: Mapping[str, Any],
|
|
request_sha: str,
|
|
) -> dict[str, Any]:
|
|
try:
|
|
value = json.loads(result_path.read_text(encoding="utf-8"))
|
|
except json.JSONDecodeError as exc:
|
|
raise ExternalAgentRuntimeError("EXTERNAL_AGENT_RESULT_INVALID") from exc
|
|
if (
|
|
not isinstance(value, Mapping)
|
|
or value.get("schema") != RESULT_SCHEMA
|
|
or value.get("request_id") != request["request_id"]
|
|
or value.get("request_sha256") != request_sha
|
|
or value.get("runtime") != request["runtime"]
|
|
or value.get("task_id") != request["task_id"]
|
|
or value.get("attempt") != request["attempt"]
|
|
or value.get("execution_digest") != request["execution_digest"]
|
|
):
|
|
raise ExternalAgentRuntimeError("EXTERNAL_AGENT_RESULT_BINDING_INVALID")
|
|
material = dict(value)
|
|
declared = str(material.pop("result_sha256", ""))
|
|
if declared != sha256(canonical(material)):
|
|
raise ExternalAgentRuntimeError("EXTERNAL_AGENT_RESULT_DIGEST_INVALID")
|
|
return dict(value)
|
|
|
|
|
|
def serve(
|
|
root: Path = DEFAULT_EXCHANGE_ROOT,
|
|
profile_root: Path = DEFAULT_PROFILE_ROOT,
|
|
) -> None:
|
|
_assert_no_secret_env()
|
|
root = Path(root)
|
|
for directory in (root / "requests", root / "results", root / "failed"):
|
|
directory.mkdir(parents=True, exist_ok=True)
|
|
while True:
|
|
for request_path in sorted((root / "requests").glob("*.json")):
|
|
if request_path.is_symlink():
|
|
continue
|
|
result_path = root / "results" / request_path.name
|
|
failed_path = root / "failed" / request_path.name
|
|
try:
|
|
execute_request(request_path, result_path, profile_root=profile_root)
|
|
request_path.unlink(missing_ok=True)
|
|
except Exception as exc:
|
|
failed_path.write_text(
|
|
json.dumps(
|
|
{"error": sanitize_text(str(exc) or type(exc).__name__)},
|
|
ensure_ascii=False,
|
|
sort_keys=True,
|
|
)
|
|
+ "\n",
|
|
encoding="utf-8",
|
|
)
|
|
request_path.unlink(missing_ok=True)
|
|
time.sleep(0.25)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
import argparse
|
|
parser = argparse.ArgumentParser()
|
|
parser.add_argument("--root", default=str(DEFAULT_EXCHANGE_ROOT))
|
|
parser.add_argument("--profile-root", default=str(DEFAULT_PROFILE_ROOT))
|
|
parser.add_argument("--request")
|
|
parser.add_argument("--result")
|
|
args = parser.parse_args()
|
|
if args.request and args.result:
|
|
execute_request(
|
|
Path(args.request),
|
|
Path(args.result),
|
|
profile_root=Path(args.profile_root),
|
|
)
|
|
else:
|
|
serve(Path(args.root), Path(args.profile_root))
|