Compare commits

...
6 changed files with 460 additions and 20 deletions
@@ -475,9 +475,16 @@ def validate_request(value: Mapping[str, Any]) -> dict[str, Any]:
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")
@@ -497,6 +504,7 @@ def validate_request(value: Mapping[str, Any]) -> dict[str, Any]:
"request_id": request_id,
"runtime": runtime,
"task_id": task_id,
"attempt": attempt,
"execution_digest": execution_digest,
"payload": dict(payload),
}
@@ -815,6 +823,7 @@ def execute_request(
"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),
@@ -865,6 +874,7 @@ class ExternalAgentExchange:
request_id: str,
runtime: str,
task_id: str,
attempt: int,
execution_digest: str,
payload: Mapping[str, Any],
timeout_seconds: int = 1800,
@@ -875,6 +885,7 @@ class ExternalAgentExchange:
"request_id": request_id,
"runtime": runtime,
"task_id": task_id,
"attempt": attempt,
"execution_digest": execution_digest,
"payload": dict(payload),
}
@@ -950,6 +961,7 @@ class ExternalAgentExchange:
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")
+87 -18
View File
@@ -583,11 +583,44 @@ class ExecutionEnvelope:
)
def external_agent_request_id(
envelope: ExecutionEnvelope,
runtime_name: str,
attempt: int,
) -> str:
if not isinstance(attempt, int) or isinstance(attempt, bool) or attempt < 1:
raise WorkerError("TASK_ATTEMPT_INVALID", phase="external_agent")
if runtime_name not in {"hermes", "openclaw"}:
raise WorkerError(
"EXTERNAL_AGENT_RUNTIME_UNSUPPORTED",
phase="external_agent",
details={"runtime": runtime_name},
)
return (
f"external-{runtime_name}-"
+ digest_json(
{
"task_id": envelope.task_id,
"job_id": envelope.job_id,
"execution_digest": envelope.digest,
"runtime": runtime_name,
"attempt": attempt,
}
)[:32]
)
class RuntimeAdapter(ABC):
name: str
@abstractmethod
def execute(self, envelope: ExecutionEnvelope, worktree: Path) -> dict[str, Any]:
def execute(
self,
envelope: ExecutionEnvelope,
worktree: Path,
*,
attempt: int,
) -> dict[str, Any]:
raise NotImplementedError
@@ -608,7 +641,13 @@ class DeterministicExecutor(RuntimeAdapter):
if os.path.exists(temporary):
os.unlink(temporary)
def execute(self, envelope: ExecutionEnvelope, worktree: Path) -> dict[str, Any]:
def execute(
self,
envelope: ExecutionEnvelope,
worktree: Path,
*,
attempt: int,
) -> dict[str, Any]:
changed: set[str] = set()
for change in envelope.change_set:
relative = str(change["path"])
@@ -663,7 +702,13 @@ class SourceCaptureExecutor(RuntimeAdapter):
"governed", "trace", "tool", "credential", "scope", "message", "handoff",
}
def execute(self, envelope: ExecutionEnvelope, worktree: Path) -> dict[str, Any]:
def execute(
self,
envelope: ExecutionEnvelope,
worktree: Path,
*,
attempt: int,
) -> dict[str, Any]:
terms = set(self.defaults)
for signal in envelope.source_signals:
terms.update(token.lower() for token in re.findall(r"[A-Za-z][A-Za-z0-9_.-]{2,63}", signal))
@@ -732,7 +777,13 @@ class CompactInterfaceMapExecutor(RuntimeAdapter):
name = "compact_interface_map"
def execute(self, envelope: ExecutionEnvelope, worktree: Path) -> dict[str, Any]:
def execute(
self,
envelope: ExecutionEnvelope,
worktree: Path,
*,
attempt: int,
) -> dict[str, Any]:
if envelope.action != "source_capture":
raise WorkerError("COMPACT_INTERFACE_MAP_ACTION_INVALID", phase="interface_map")
try:
@@ -762,7 +813,13 @@ class CompactInterfaceMapExecutor(RuntimeAdapter):
class CodexExecutor(RuntimeAdapter):
name = "codex"
def execute(self, envelope: ExecutionEnvelope, worktree: Path) -> dict[str, Any]:
def execute(
self,
envelope: ExecutionEnvelope,
worktree: Path,
*,
attempt: int,
) -> dict[str, Any]:
raise WorkerError(
"CODEX_ADAPTER_REQUIRES_EXPLICIT_POLICY",
phase="adapter",
@@ -773,7 +830,13 @@ class CodexExecutor(RuntimeAdapter):
class ExternalAgentExecutor(RuntimeAdapter):
runtime_name: str
def execute(self, envelope: ExecutionEnvelope, worktree: Path) -> dict[str, Any]:
def execute(
self,
envelope: ExecutionEnvelope,
worktree: Path,
*,
attempt: int,
) -> dict[str, Any]:
if envelope.action != "agent_runtime":
raise WorkerError(
"EXTERNAL_AGENT_ACTION_INVALID",
@@ -787,16 +850,10 @@ class ExternalAgentExecutor(RuntimeAdapter):
)
if not envelope.runtime_input:
raise WorkerError("EXTERNAL_AGENT_RUNTIME_INPUT_MISSING", phase="adapter")
request_id = (
f"external-{self.runtime_name}-"
+ digest_json(
{
"task_id": envelope.task_id,
"job_id": envelope.job_id,
"execution_digest": envelope.digest,
"runtime": self.runtime_name,
}
)[:32]
request_id = external_agent_request_id(
envelope,
self.runtime_name,
attempt,
)
exchange = ExternalAgentExchange(EXTERNAL_AGENT_EXECUTOR_ROOT)
try:
@@ -804,6 +861,7 @@ class ExternalAgentExecutor(RuntimeAdapter):
request_id=request_id,
runtime=self.runtime_name,
task_id=envelope.task_id,
attempt=attempt,
execution_digest=envelope.digest,
payload=envelope.runtime_input,
timeout_seconds=EXTERNAL_AGENT_EXECUTOR_TIMEOUT_SECONDS,
@@ -819,6 +877,7 @@ class ExternalAgentExecutor(RuntimeAdapter):
task_id=envelope.task_id,
adapter=self.runtime_name,
request_id=request_id,
attempt=attempt,
output_sha256=external.get("output_sha256"),
duration_ms=external.get("duration_ms"),
credential_material_exposed=False,
@@ -1354,7 +1413,13 @@ class IsolatedDeterministicExecutor(RuntimeAdapter):
if os.path.exists(temporary):
os.unlink(temporary)
def execute(self, envelope: ExecutionEnvelope, worktree: Path) -> dict[str, Any]:
def execute(
self,
envelope: ExecutionEnvelope,
worktree: Path,
*,
attempt: int,
) -> dict[str, Any]:
if envelope.action != "deterministic":
raise WorkerError("ISOLATED_EXECUTOR_ACTION_INVALID", phase="adapter")
root = ISOLATED_EXECUTOR_ROOT
@@ -2193,7 +2258,11 @@ class WorkerSupervisor:
save_task_state(task_id, state)
save_checkpoint(task_id, state, envelope, attempt)
adapter = adapter_for(envelope.adapter)
execution = adapter.execute(envelope, worktree)
execution = adapter.execute(
envelope,
worktree,
attempt=attempt,
)
state.update(
{
"phase": "verification_passed",
@@ -0,0 +1,123 @@
# External Agent Attempt-Scoped Request Identity
## Problem
External Agent execution previously derived its durable request ID from:
```text
task_id
job_id
execution_digest
runtime
```
The Online Task attempt was missing.
That is incorrect for retryable external side effects. A failed Model Governance route reservation is
durable and terminal for that request ID. Reusing the same external request ID on a later official
Task attempt therefore reuses the old route decision/reservation and can fail settlement with:
```text
MODEL_RESERVATION_STATE_INVALID
```
Archiving an exchange failed marker does not fix the root cause because the Model Governance
reservation remains intentionally durable.
## Contract
External request identity is now:
```text
hash(
task_id,
job_id,
execution_digest,
runtime,
attempt
)
```
The attempt is a **transient execution coordinate**. It is not added to the signed governed execution
envelope and does not alter the Task/Job binding or execution digest.
The worker gets the authoritative attempt from the successful claim response and passes it through:
```text
process_claim
→ RuntimeAdapter.execute(..., attempt=N)
→ ExternalAgentExecutor
→ ExternalAgentExchange
→ external request JSON
→ external result JSON
```
The external result must bind the same attempt.
## Idempotency
This gives the intended two-level behavior:
```text
same Task/Job/execution/runtime + same attempt
→ same external request ID
→ same external result/reservation can be replayed safely
same Task/Job/execution/runtime + next official attempt
→ different external request ID
→ different Model Governance request ID
→ new reservation
```
Old FAILED reservations are retained for audit. No Model Governance DB cleanup is required or
permitted.
## Model Governance
Model Governance already derives its route request ID from:
```text
external_request_id + ":model"
```
Therefore making the external request ID attempt-scoped automatically makes route decisions and
reservations attempt-scoped without creating another state authority.
## Recovery
For the current Hermes live Task:
```text
attempt 6
→ old request/reservation remains FAILED
official NEEDS_REWORK
→ bounded retry extension 6 → 7
attempt 7
→ new external request ID
→ new route request ID
→ new reservation
→ provider execution
→ settlement
```
No replacement Task/Job, direct DB rewrite, failed-reservation deletion or marker deletion is
required.
## Worker Availability
The repair must preserve the already verified Worker Availability runtime. Runtime switches use the
maintenance inhibit and must finish with the guard healthy and all three workers fresh.
## Verification
Required regression coverage:
- request ID stable within one attempt;
- request ID different across attempts;
- request schema requires a valid attempt;
- external result binds attempt;
- old failed Model Governance reservation remains FAILED;
- next attempt gets a new reservation and can settle;
- existing external-agent and Model Governance runtime tests still pass.
@@ -0,0 +1,232 @@
from __future__ import annotations
import json
from pathlib import Path
import sys
import pytest
ROOT=Path(__file__).resolve().parents[1]
APP=ROOT/"active-runtime/app"
sys.path.insert(0,str(APP))
import aios_online_agent_worker as worker
from aios_external_agent_runtime import (
ExternalAgentExchange,
ExternalAgentRuntimeError,
validate_request,
)
def envelope():
value=object.__new__(worker.ExecutionEnvelope)
object.__setattr__(value,"task_id","task-attempt-identity")
object.__setattr__(value,"job_id","job-attempt-identity")
object.__setattr__(value,"digest","d"*64)
return value
def test_external_request_id_is_stable_within_attempt_and_changes_across_attempts():
env=envelope()
a1=worker.external_agent_request_id(env,"hermes",6)
a1_replay=worker.external_agent_request_id(env,"hermes",6)
a2=worker.external_agent_request_id(env,"hermes",7)
assert a1==a1_replay
assert a1!=a2
assert a1.startswith("external-hermes-")
assert a2.startswith("external-hermes-")
def test_external_request_id_rejects_invalid_attempt():
env=envelope()
with pytest.raises(worker.WorkerError,match="TASK_ATTEMPT_INVALID"):
worker.external_agent_request_id(env,"hermes",0)
def test_external_request_schema_binds_attempt():
value=validate_request({
"schema":"aios.external-agent-runtime.request.v1",
"request_id":"external-hermes-0123456789abcdef",
"runtime":"hermes",
"task_id":"task-attempt-identity",
"attempt":7,
"execution_digest":"d"*64,
"payload":{
"goal":"Return ok.",
"instructions":"",
"success_criteria":[],
"constraints":{},
},
})
assert value["attempt"]==7
def test_external_request_schema_rejects_missing_attempt():
with pytest.raises(ExternalAgentRuntimeError,match="EXTERNAL_AGENT_ATTEMPT_INVALID"):
validate_request({
"schema":"aios.external-agent-runtime.request.v1",
"request_id":"external-hermes-0123456789abcdef",
"runtime":"hermes",
"task_id":"task-attempt-identity",
"execution_digest":"d"*64,
"payload":{
"goal":"Return ok.",
"instructions":"",
"success_criteria":[],
"constraints":{},
},
})
def _write_bound_result(path, request):
import hashlib
from aios_external_agent_runtime import canonical, sha256
material={
"schema":"aios.external-agent-runtime.result.v1",
"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":0,
"duration_ms":1,
"output_text":"ok",
"output_sha256":hashlib.sha256(b"ok").hexdigest(),
"stderr_sha256":hashlib.sha256(b"").hexdigest(),
"credential_material_exposed":False,
"control_plane_access_granted":False,
}
material["result_sha256"]=sha256(canonical(material))
path.write_text(json.dumps(material,sort_keys=True),encoding="utf-8")
def test_exchange_result_binding_rejects_wrong_attempt(tmp_path):
exchange=ExternalAgentExchange(tmp_path)
request={
"schema":"aios.external-agent-runtime.request.v1",
"request_id":"external-hermes-0123456789abcdef",
"runtime":"hermes",
"task_id":"task-attempt-identity",
"attempt":7,
"execution_digest":"d"*64,
"payload":{
"goal":"Return ok.",
"instructions":"",
"success_criteria":[],
"constraints":{},
},
}
request=validate_request(request)
result_path=tmp_path/"result.json"
_write_bound_result(result_path,request)
value=json.loads(result_path.read_text())
value["attempt"]=6
# Recompute digest so the binding check, not the digest check, is what fails.
from aios_external_agent_runtime import canonical,sha256
material=dict(value); material.pop("result_sha256")
value["result_sha256"]=sha256(canonical(material))
result_path.write_text(json.dumps(value),encoding="utf-8")
with pytest.raises(ExternalAgentRuntimeError,match="EXTERNAL_AGENT_RESULT_BINDING_INVALID"):
exchange._validated_result(
result_path,
request,
sha256(canonical(request)),
)
def test_model_governance_request_id_is_attempt_scoped():
env=envelope()
attempt6=worker.external_agent_request_id(env,"hermes",6)+":model"
attempt7=worker.external_agent_request_id(env,"hermes",7)+":model"
assert attempt6!=attempt7
def test_failed_previous_attempt_does_not_poison_next_model_governance_reservation(tmp_path):
from aios_model_governance import (
ModelGovernanceStore,
ModelRecord,
ProviderRecord,
RouteRequest,
ScopePolicy,
)
store=ModelGovernanceStore(tmp_path/"model-governance.sqlite3",now_fn=lambda:1_800_000_000.0)
store.register_provider(
ProviderRecord(
"oneapi",
"runtime-profile:hermes",
max_failures=5,
cooldown_seconds=60,
)
)
store.register_model(
ModelRecord(
"deepseek-v4-flash",
"oneapi",
"v1",
("agent.runtime.hermes","text"),
input_cost_micros_per_1k=1,
output_cost_micros_per_1k=1,
max_input_tokens=10000,
max_output_tokens=1000,
evaluation_sha256="a"*64,
)
)
store.set_scope_policy(
ScopePolicy(
"tenant",
"model-governance-live",
"staging",
allowed_models=("deepseek-v4-flash",),
required_capabilities=("text",),
daily_token_budget=100000,
daily_cost_budget_micros=100000,
fallback_limit=1,
)
)
def route(request_id):
return store.route(
RouteRequest(
request_id=request_id,
task_id="r42-model-governance-hermes-live-20260818",
trace_id="trace-hermes-model-governance-live",
scope_type="tenant",
tenant_id="model-governance-live",
project_id="staging",
required_capabilities=("agent.runtime.hermes",),
estimated_input_tokens=100,
max_output_tokens=50,
)
)
attempt6=route("external-hermes-attempt6:model")
store.record_failure(attempt6.request_id,"provider_process_failed")
row6=store.conn.execute(
"select state from model_governance_reservations where request_id=?",
(attempt6.request_id,),
).fetchone()
assert row6["state"]=="FAILED"
attempt7=route("external-hermes-attempt7:model")
assert attempt7.request_id!=attempt6.request_id
row7=store.conn.execute(
"select state from model_governance_reservations where request_id=?",
(attempt7.request_id,),
).fetchone()
assert row7["state"]=="RESERVED"
settled=store.record_success(
attempt7.request_id,
input_tokens=90,
output_tokens=40,
usage_source="runtime",
)
assert settled["state"]=="SETTLED"
assert store.conn.execute(
"select state from model_governance_reservations where request_id=?",
(attempt6.request_id,),
).fetchone()["state"]=="FAILED"
assert store.verify_audit_chain() is True
store.close()
+4 -1
View File
@@ -110,6 +110,7 @@ def make_text_request(tmp_path, runtime, payload=None):
"request_id":f"external-{runtime}-0123456789abcdef",
"runtime":runtime,
"task_id":f"task-{runtime}-runtime",
"attempt":1,
"execution_digest":"d"*64,
"payload":payload or {
"goal":"Return ok.",
@@ -193,6 +194,7 @@ def test_external_executor_runs_root_owned_profile_without_shell(tmp_path,monkey
"request_id":f"external-{runtime}-0123456789abcdef",
"runtime":runtime,
"task_id":f"task-{runtime}-runtime",
"attempt":1,
"execution_digest":"d"*64,
"payload":{
"goal":"Return ok.",
@@ -326,6 +328,7 @@ def test_worker_external_adapter_builds_readonly_result(monkeypatch,tmp_path,run
"request_sha256":"e"*64,
"runtime":runtime,
"task_id":kwargs["task_id"],
"attempt":kwargs["attempt"],
"execution_digest":kwargs["execution_digest"],
"exit_code":0,
"duration_ms":10,
@@ -339,7 +342,7 @@ def test_worker_external_adapter_builds_readonly_result(monkeypatch,tmp_path,run
monkeypatch.setattr(worker,"ExternalAgentExchange",FakeExchange)
monkeypatch.setattr(worker,"audit",lambda *a,**k:None)
executor=worker.HermesExecutor() if runtime=="hermes" else worker.OpenClawExecutor()
execution=executor.execute(envelope,tmp_path)
execution=executor.execute(envelope,tmp_path,attempt=1)
result=worker.build_agent_runtime_result(envelope.task_id,envelope,execution)
assert result["status"]=="SUCCEEDED"
assert result["classification"]=="EXTERNAL_AGENT_RUNTIME_SUCCEEDED"
@@ -154,7 +154,7 @@ def seed(db_path, *, legacy=False, tiny_budget=False):
store.close()
def make_external_request(projected_task, *, force_fail=False):
def make_external_request(projected_task, *, force_fail=False, attempt=1):
execution=projected_task["metadata"]["execution"]
payload=dict(execution["runtime_input"])
if force_fail:
@@ -165,6 +165,7 @@ def make_external_request(projected_task, *, force_fail=False):
"request_id":"external-model-governance-0123456789",
"runtime":"hermes",
"task_id":projected_task["task_id"],
"attempt":attempt,
"execution_digest":execution["execution_digest"],
"payload":payload,
}