Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions docs/contracts/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ The execution engine owns run execution and talks only to the control plane and
intersects `platform_functions`, `allowed_tools`, and `tool_specs`. The engine
maps the alias back to the canonical control-plane ID and sends the original
model call ID; missing, duplicate, and invalid mappings fail closed.
The `acornops_fetch` alias for `http.fetch.get` uses this same platform-function
route; execution-engine does not make the external HTTP request itself.
Provider-native `web_search` remains the only declaration sent through
`native_tools`, while target and MCP tools retain their existing route.
- Execution-engine never calls target agents, management-console, or external MCP servers directly.
Expand Down
1 change: 1 addition & 0 deletions docs/contracts/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@
"toolCallCompletedArtifactFields": ["id", "expires_at", "sha256", "uncompressed_bytes", "compressed_bytes", "content_type"],
"toolCallCompletedResultMaxBytes": 12288,
"platformNativeToolRouting": "A platform function is exposed under its provider-safe model_alias only when that alias intersects bootstrap platform_functions, allowed_tools, and tool_specs. Execution routes the alias back to the canonical control-plane ID with the stable tool-call ID. Missing, duplicate, or invalid mappings fail closed. native_tools and gateway JWT allowed_native_tools contain provider-native tools only; target MCP tools remain gateway/target-adapter owned.",
"fetchRouting": "http.fetch.get is exposed as acornops_fetch and dispatches through the existing platform-function client to the control plane; execution-engine never performs the external HTTP request. The platform-function read deadline exceeds Fetch's control-plane-owned 15-second deadline so timeout outcomes remain producer-authoritative. Durable tool-start events redact the URL, and completion or fallback events retain only status, content type, response size, retrieval time, and the untrusted-data marker, never the URL, query, or response body.",
"commitStatusValues": ["completed", "failed", "cancelled"],
"commitAssistantMessageFields": ["content", "format"],
"commitUsageFields": ["input_tokens", "output_tokens", "tool_calls", "reasoning_tokens?"],
Expand Down
7 changes: 7 additions & 0 deletions execution_engine/orchestrator_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
)

INTERNAL_CONTROL_PLANE_PREFIX = "/internal/v1"
PLATFORM_NATIVE_TOOL_READ_TIMEOUT_SECONDS = 20.0


def _retryable_orchestrator_error(exc: BaseException) -> bool:
Expand Down Expand Up @@ -244,6 +245,12 @@ async def call_platform_native_tool(
response = await self.client.post(
url,
json={"toolCallId": call_id, "arguments": arguments},
timeout=httpx.Timeout(
connect=5.0,
read=PLATFORM_NATIVE_TOOL_READ_TIMEOUT_SECONDS,
write=10.0,
pool=5.0,
),
)
response.raise_for_status()
orchestrator_requests_total.labels(endpoint="platform_native_tool", result="success").inc()
Expand Down
13 changes: 9 additions & 4 deletions execution_engine/worker.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,12 @@
start_event_manager,
write_result_outcome_unknown,
)
from execution_engine.worker_tool_artifacts import persist_tool_result_artifact, tool_result_event_payload
from execution_engine.worker_tool_artifacts import (
persist_tool_result_artifact,
tool_call_event_arguments,
tool_result_event_payload,
tool_result_event_summary,
)
from execution_engine.worker_tool_authority import build_runtime_tool_client, provider_native_tools
from execution_engine.worker_tool_sanitizer import sanitize_tool_spec_for_llm

Expand Down Expand Up @@ -304,7 +309,7 @@ def finish_cancelled_run() -> None:
emit_event("tool_call_started", {
"call_id": pending_call_id,
"tool": pending_tool_name,
"arguments": pending_arguments,
"arguments": tool_call_event_arguments(pending_tool_name, pending_arguments),
})
tool_result = await tool_client.call_tool(
pending_tool_name,
Expand Down Expand Up @@ -466,7 +471,7 @@ async def load_skill_context(skill_ref: str) -> dict[str, object]:
emit_event("tool_call_started", {
"call_id": chunk["call_id"],
"tool": chunk["tool"],
"arguments": chunk["arguments"]
"arguments": tool_call_event_arguments(chunk["tool"], chunk["arguments"]),
})
elif chunk["type"] == "tool_result":
artifact, artifact_unavailable = await persist_tool_result_artifact(
Expand All @@ -475,7 +480,7 @@ async def load_skill_context(skill_ref: str) -> dict[str, object]:
tool_result_events.append(
{
"tool": chunk["tool"],
"result": chunk["result"],
"result": tool_result_event_summary(chunk["tool"], chunk["result"]),
"is_error": bool(chunk["is_error"]),
}
)
Expand Down
28 changes: 27 additions & 1 deletion execution_engine/worker_tool_artifacts.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,32 @@
UUID_V4_RE = re.compile(r"^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$", re.I)
SHA256_RE = re.compile(r"^[0-9a-f]{64}$")
RFC3339_RE = re.compile(r"^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?Z$")
FETCH_MODEL_ALIAS = "acornops_fetch"


def tool_call_event_arguments(tool_name: object, arguments: object) -> object:
"""Remove Fetch URL paths and queries from durable tool-call events."""
if str(tool_name) == FETCH_MODEL_ALIAS:
return {"url": "[redacted]"}
return arguments


def tool_result_event_summary(tool_name: object, supplied_result: object) -> object:
"""Remove Fetch URLs and response bodies from durable events and fallbacks."""
if str(tool_name) != FETCH_MODEL_ALIAS:
return supplied_result
structured = supplied_result.get("structuredContent") if isinstance(supplied_result, dict) else None
structured = structured if isinstance(structured, dict) else {}
return {
"status": structured.get("status"),
"contentType": structured.get("contentType"),
"responseSizeBytes": structured.get("responseSizeBytes"),
"retrievedAt": structured.get("retrievedAt"),
"untrustedExternalData": True,
} if structured else {
"code": supplied_result.get("code") if isinstance(supplied_result, dict) else None,
"untrustedExternalData": True,
}


def _valid_artifact_metadata(value: Any) -> bool:
Expand Down Expand Up @@ -69,7 +95,7 @@ def tool_result_event_payload(
chunk: dict[str, Any], artifact: dict[str, Any] | None, artifact_unavailable: bool
) -> dict[str, Any]:
"""Build the compact-only durable tool completion event."""
supplied_result = chunk.get("result")
supplied_result = tool_result_event_summary(chunk.get("tool"), chunk.get("result"))
result = compact_tool_context(supplied_result)
supplied_meta = chunk.get("context_meta")
meta = supplied_meta if isinstance(supplied_meta, dict) else {}
Expand Down
87 changes: 84 additions & 3 deletions tests/test_supporting_components.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,12 @@
from execution_engine.agent.tools import GatewayToolClient, PlatformToolClient, ToolClientStub
from execution_engine.gateway_client import GatewayLlmClient
from execution_engine.readiness import DependencyStatus
from execution_engine.worker_tool_artifacts import persist_tool_result_artifact, tool_result_event_payload
from execution_engine.worker_tool_artifacts import (
persist_tool_result_artifact,
tool_call_event_arguments,
tool_result_event_payload,
tool_result_event_summary,
)
from execution_engine.worker_tool_authority import (
build_authorized_tool_routing,
platform_function_mappings,
Expand Down Expand Up @@ -62,15 +67,20 @@ def test_tool_routing_requires_an_exact_authorized_reference():

def test_platform_functions_require_all_snapshot_authorities():
assert platform_function_mappings(
["acornops_generate_pdf_report", "web_search"],
["acornops_fetch", "acornops_generate_pdf_report", "web_search"],
[
{"id": "http.fetch.get", "model_alias": "acornops_fetch"},
{"id": "reports.pdf.generate", "model_alias": "acornops_generate_pdf_report"},
],
[
{"name": "acornops_fetch", "input_schema": {"type": "object"}},
{"name": "acornops_generate_pdf_report", "input_schema": {"type": "object"}},
{"name": "web_search"},
],
) == {"acornops_generate_pdf_report": "reports.pdf.generate"}
) == {
"acornops_fetch": "http.fetch.get",
"acornops_generate_pdf_report": "reports.pdf.generate",
}


@pytest.mark.parametrize(
Expand Down Expand Up @@ -164,6 +174,34 @@ async def test_platform_tool_client_calls_control_plane_with_stable_call_id():
assert result["full_result"]["structuredContent"]["reportId"] == "report-1"


@pytest.mark.asyncio
async def test_fetch_uses_the_existing_platform_function_client():
orchestrator = MagicMock()
orchestrator.call_platform_native_tool = AsyncMock(return_value={
"content": [{"type": "text", "text": "{\"status\": 200}"}],
"structuredContent": {"status": 200, "data": {"ok": True}},
"isError": False,
})
client = PlatformToolClient(
ToolClientStub(), orchestrator, "run-1",
{"acornops_fetch": "http.fetch.get"},
)

result = await client.call_tool(
"acornops_fetch",
{"url": "https://status.example.com/api/health"},
call_id="call-fetch-1",
)

orchestrator.call_platform_native_tool.assert_awaited_once_with(
"run-1",
"http.fetch.get",
{"url": "https://status.example.com/api/health"},
call_id="call-fetch-1",
)
assert result["is_error"] is False


@pytest.mark.asyncio
async def test_platform_tool_client_requires_call_id():
client = PlatformToolClient(
Expand Down Expand Up @@ -1096,6 +1134,49 @@ async def test_artifact_failure_keeps_full_result_out_of_durable_event():
assert "must-not-enter-events" not in str(payload)


def test_fetch_event_omits_url_query_and_response_body():
chunk = {
"call_id": "call-fetch-1",
"tool": "acornops_fetch",
"result": {
"content": [{"type": "text", "text": "secret response body"}],
"structuredContent": {
"url": "https://api.example.com/search?secret=query-value",
"status": 200,
"contentType": "application/json",
"data": {"secret": "response body"},
"responseSizeBytes": 42,
"retrievedAt": "2026-07-24T00:00:00.000Z",
},
"isError": False,
},
"full_result": {"secret": "artifact body"},
"context_meta": {"context_bytes": 200},
"artifact_eligible": False,
"is_error": False,
}

payload = tool_result_event_payload(chunk, None, False)

assert payload["result"] == {
"status": 200,
"contentType": "application/json",
"responseSizeBytes": 42,
"retrievedAt": "2026-07-24T00:00:00.000Z",
"untrustedExternalData": True,
}
assert "query-value" not in str(payload)
assert "response body" not in str(payload)

assert tool_call_event_arguments(
"acornops_fetch",
{"url": "https://api.example.com/search?secret=query-value"},
) == {"url": "[redacted]"}
fallback_result = tool_result_event_summary("acornops_fetch", chunk["result"])
assert "query-value" not in str(fallback_result)
assert "response body" not in str(fallback_result)


@pytest.mark.asyncio
async def test_invalid_artifact_receipt_is_reported_as_unavailable():
orchestrator = MagicMock()
Expand Down
24 changes: 24 additions & 0 deletions tests/test_unit.py
Original file line number Diff line number Diff line change
Expand Up @@ -622,6 +622,30 @@ async def handler(request: httpx.Request) -> httpx.Response:
await client.close()


@pytest.mark.asyncio
async def test_orchestrator_client_allows_platform_native_tool_deadline_to_finish():
async def handler(request: httpx.Request) -> httpx.Response:
assert request.url.path == "/internal/v1/runs/r1/native-tools/http.fetch.get/call"
assert request.extensions["timeout"]["read"] == 20.0
return httpx.Response(200, json={"structuredContent": {"status": 200}})

client = OrchestratorClient()
await client.close()
client.base_url = "http://orchestrator"
client.client = httpx.AsyncClient(transport=httpx.MockTransport(handler))

try:
result = await client.call_platform_native_tool(
"r1",
"http.fetch.get",
{"url": "https://status.example.com/health"},
call_id="call-1",
)
assert result["structuredContent"]["status"] == 200
finally:
await client.close()


@pytest.mark.asyncio
async def test_orchestrator_client_encodes_skill_snapshot_path_params():
async def handler(request: httpx.Request) -> httpx.Response:
Expand Down
Loading