Agent Tools Dispatch
The Ironflow server’s agent_tools.Dispatcher POSTs HMAC-signed callbacks to
SDK-registered tool handlers. SDKs (@ironflow/node/agent, sdk/go/ironflow/agent)
mount the handler at /ironflow/agent-tools/dispatch.
Request
Section titled “Request”| Header | Value |
|---|---|
X-Ironflow-Timestamp | unix seconds, must be within 5 minutes past / 1 minute future |
X-Ironflow-Signature | sha256=<hex> of HMAC-SHA256(secret, "{ts}.{rawBody}") |
Body: {"qualified_name": "<agent>.<tool>", "input": {...}}
Response codes
Section titled “Response codes”| Status | Code | When |
|---|---|---|
| 200 | — | Handler ran. {output} on success, {error: {code: "HANDLER_ERROR", message}} on throw. |
| 400 | INVALID_REQUEST | Body not JSON, or qualified_name missing. |
| 400 | INPUT_SCHEMA_INVALID | Input failed Zod / Validate() schema check. |
| 401 | TIMESTAMP_SKEW | Timestamp outside replay window (5min past / 1min future), or unparseable. |
| 401 | SIGNATURE_MISMATCH | Missing/malformed sig or timestamp header, HMAC verification failed, OR qualified_name not registered. Missing-header and unknown-tool cases collapse to the same code so an unauthenticated caller cannot probe the registered tool roster (see below). |
Why unknown-tool collapses to SIGNATURE_MISMATCH
Section titled “Why unknown-tool collapses to SIGNATURE_MISMATCH”An unauthenticated caller could probe the dispatch endpoint with arbitrary
qualified_name values and distinguish 404 (unknown) from 401 (registered, bad
sig) — leaking the registered tool roster to anyone who can reach the callback
URL. The SDK collapses both cases to 401 SIGNATURE_MISMATCH "HMAC mismatch"
so the response shape is identical.
Operators distinguish legitimate stale-process drift (SDK restarted without re-registering) from attack via the SDK’s structured warn log:
ironflow.agent.dispatch unknown_tool qualified_name="<name>"Emitted once per unknown-tool dispatch. Aggregating these by source IP /
caller key in your log pipeline highlights probe attempts. The line is
diagnostic only; the wire response remains SIGNATURE_MISMATCH. The
qualified_name is rendered with Go %q / JS JSON.stringify so
embedded control chars (\n, \r, etc.) are escaped — log injection
via crafted callback payloads is blocked.