Skip to main content

Tools

Standalone, reusable tool definitions that agents call during generation.

Overview​

A tool is one project-scoped record of type, input schema and execution config, shareable across agents and callable directly via the API. Types: http (external HTTP endpoint), client (executed locally by the calling application), mcp (proxies an MCP server), builtin (SOAT platform action), pipeline (fixed sequence of other tools as one call). Any other type is 400 VALIDATION_FAILED on create and update.

A Trigger with target_type: tool invokes a tool automatically. A Guardrail on the tool (every agent) or on the agent classifies calls and routes them to the approval queue.

See the Permissions Reference for the IAM action strings for this module.

Data Model​

FieldTypeDescription
idstringPublic ID (tool_ prefix)
project_idstringID of the owning project
namestringMachine-readable tool name sent to the model (or namespace prefix for mcp/builtin)
type"http" | "client" | "mcp" | "builtin" | "pipeline"Tool type — determines execution behaviour
descriptionstring | nullHuman-readable description sent to the model for tool selection
parametersobject | nullJSON Schema describing the tool's input. Required for http and client types.
executeobject | nullHTTP execution config (url, method, headers, body_mode, response_mode, auth). Required for http type.
execute.urlstringHTTP endpoint. Supports {paramName} and ${body.fieldName} path placeholders replaced at call time with URL-encoded argument values.
execute.methodstringHTTP method (default: POST). For GET, HEAD, DELETE the arguments become query-string parameters.
execute.headersobjectAdditional headers sent with the execution request.
execute.body_mode"json" | "multipart"How the request body is encoded for POST/PUT/PATCH (default: json). Use multipart for APIs that require multipart/form-data.
execute.response_mode"json" | "base64"How a 2xx response body is returned (default: json). base64 returns { content_type, filename, data_base64 } for binary targets. See Binary responses.
execute.authobject | nullComputed request credential, for targets whose Authorization value cannot be a static header. type is aws_sigv4 or gcp_service_account. See Computed credentials.
mcpobject | nullMCP server config (url, headers). Required for mcp type.
mcp.urlstringURL of the MCP server (SSE or Streamable HTTP transport).
mcp.headersobjectAdditional headers sent when connecting to the MCP server.
actionsstring[] | nullAllowlist of actions to expose. builtin: SOAT platform action names, e.g. ["search-knowledge"] (required). mcp: optional allowlist of MCP tool names to scope the server surface — null exposes every tool. See mcp action scoping.
denied_actionsstring[] | nullmcp only: optional denylist of MCP tool names to hide, applied after actions and taking precedence over it. null denies nothing. See mcp action scoping.
context_keysstring[] | nullAllowlist of tool_context keys forwarded to this tool as context headers. null forwards every key (the default); [] forwards none. See Scoping which context keys reach a tool.
preset_parametersobject | nullFixed parameter values pinned on every call. Keys are hidden from the model, and a pinned value wins over one the caller sends. Values accept {{context:<key>}} so a pin can be the run's value.
pipelineobject | nullPipeline definition (steps, optional output). Required for pipeline type. See pipeline.
output_mappingobject | nullJSON Logic mapping applied to the tool's raw result, for every tool type. See output mapping.
guardrail_idsarray | nullGuardrails attached at the tool scope, governing this tool wherever it is used — see Guardrails — Attachment
created_atstringISO 8601 creation timestamp
updated_atstringISO 8601 last-updated timestamp

Key Concepts​

Configs Take Only Their Own Fields​

execute, execute.auth and mcp accept the fields in the table above and nothing else: a key outside the list is 400 VALIDATION_FAILED on create and update, named by path (execute.bogus_key). execute.headers, mcp.headers, parameters, preset_parameters, pipeline and output_mapping are caller-owned bags and take any key.

Header Name Casing​

Header names in execute.headers and mcp.headers are preserved verbatim (Authorization stays Authorization).

Context Headers (X-Soat-Context-*)​

Every http and mcp call carries the generation's tool_context as X-Soat-Context-* headers (plus the auto-populated session_id, actor_id and actor_external_id), injected after the configured headers. The prefix is deployment configuration (TOOL_CONTEXT_HEADER_PREFIX), so a caller cannot overwrite a tool's own credential. A {{context:<key>}} token places a context value in a header the target expects; context_keys bounds which keys reach a tool (default: all, for http, mcp and builtin). Key→header rule and security notes: Tool Context reference.

Tool ID vs Tool Name​

A tool ID (tool_k8x2f3np) is used in tool_bindings, active_tool_ids and step_rules[].active_tool_ids; a tool name is what the model sees and what tool_choice and stop_conditions use. http/client: one ID, one name; mcp/builtin: one ID, many names.

Tool Name Resolution​

Tool typeName the model seesExample
http{name}search
client{name}read_local_file
mcp{name}_{mcpToolName}github_create_issue, github_list_repos
builtin{name}_{action}platform_get-document, platform_search-knowledge

For mcp and builtin, name is a prefix joined with _ to each sub-tool name (github_search vs jira_search).

http​

The server sends the arguments to execute.url with the configured method: as a JSON body for POST, PUT, PATCH; as query-string parameters for GET, HEAD, DELETE.

:::caution Pointing a GET tool at this API

Every leftover argument becomes a query parameter, so a GET tool at a SOAT endpoint may only take arguments that endpoint declares; an undeclared one is a 400, surfaced as TOOL_HTTP_ERROR. Prefer a builtin tool.

:::

Two placeholder syntaxes inject arguments into the URL path; a consumed argument leaves the body or query string:

  • {paramName} — the URL-encoded argument; for tools defined via the API or CLI.
  • ${body.fieldName} — the same inside formation template sub expressions, where ${...} is the interpolation syntax (url: { sub: '${AppUrl}/posts/${body.post_id}' }).

Example — a DELETE tool with path parameters:

{
"name": "delete-post",
"type": "http",
"execute": {
"url": "https://api.example.com/users/{user_id}/posts/{post_id}",
"method": "DELETE"
},
"parameters": {
"type": "object",
"properties": {
"user_id": { "type": "string" },
"post_id": { "type": "string" }
},
"required": ["user_id", "post_id"]
}
}

Called with { "user_id": "123", "post_id": "456" }, the server issues DELETE https://api.example.com/users/123/posts/456.

Secret references in execute​

GET /tools/{id} echoes execute to anyone with read access, so use a secret reference rather than a raw credential: "headers": { "Authorization": "Bearer {{secret:sec_01HXYZ}}" }.

{{secret:...}} works in execute.url and execute.headers values, resolved right before the outbound request; the stored tool and every GET/LIST keep the reference. The secret must exist in the same project (400 SECRET_NOT_FOUND at create/update).

Secret and context references are the only valid double-curly forms; any other {{...}} in execute or mcp is 400 INVALID_TEMPLATE_TOKEN at create/update. URL path placeholders use single braces ({param}). Full reference: Expressions & Templating.

Context references in headers​

A {{context:<key>}} token in execute.headers or mcp.headers is substituted per call with one key of the caller's tool_context: "headers": { "Authorization": "Bearer {{context:ocaToken}}" }.

Valid inexecute.headers and mcp.headers only. In execute.url, mcp.url, execute.auth or a body it is rejected with 400 INVALID_TEMPLATE_TOKEN — a caller-supplied value must not be able to steer the outbound URL.
Missing key at call timeThe call fails with 400 MISSING_TOOL_CONTEXT_KEY, naming the key and header, rather than sending an empty credential.
Read backGET/LIST echo the token, never the resolved value — same as {{secret:...}}.
Calling pathsEvery dispatch surface carries a bag: an agent generation, a session, an orchestration agent, tool or poll node, a pipeline step, and POST /api/v1/tools/{tool_id}/call via its own tool_context field.

Scoping which context keys reach a tool​

tool_context is forwarded in full to every http, mcp and builtin tool; context_keys bounds it per tool. With "context_keys": ["tenant"] and tool_context: { "ocaToken": "...", "tenant": "acme" }, the tool receives X-Soat-Context-tenant, not X-Soat-Context-ocaToken.

Omitted or nullEvery key is forwarded. This is the default.
[]No caller key is forwarded.
Identity keyssession_id, actor_id, actor_external_id and calling_project_id are server-derived and always forwarded regardless of the list.
{{context:<key>}} tokensSubstituted regardless of the list: the tool declared that header itself. A key used only in a token need not be listed.
builtin toolsThe list also bounds the tool_context propagated in the action's request body, so a nested generation inherits only the listed keys.
MatchingCase-insensitive (a key names a header).
Invalid entryAn entry outside the key grammar is rejected at write time with 400 INVALID_TOOL_CONTEXT_KEY.

The allowlist is per tool, not per agent.

Computed credentials (execute.auth)​

execute.auth authenticates targets a static header cannot: AWS (per-request Signature Version 4 HMAC) and Google (short-lived OAuth 2.0 token from a signed service account assertion). It is a strategy on the http transport, not a tool type; parameters, path placeholders, body_mode, output_mapping, preset_parameters, guardrails, approvals, pipeline steps and 502 TOOL_HTTP_ERROR mapping are unchanged.

auth.typeRequired fieldsOptional fieldsWhat is sent
aws_sigv4region, service, access_key_id, secret_access_keysession_tokenAuthorization: AWS4-HMAC-SHA256 …, X-Amz-Date, plus X-Amz-Security-Token and X-Amz-Content-Sha256 when applicable
gcp_service_accountcredentials (service account key file JSON, as a string), scopes—Authorization: Bearer <access token>

Credential values take secret references, stored and returned as-is, resolved only before signing:

{
"name": "get-s3-object",
"type": "http",
"description": "Reads an object from an S3 bucket",
"parameters": {
"type": "object",
"properties": { "key": { "type": "string" } },
"required": ["key"]
},
"execute": {
"url": "https://my-bucket.s3.us-east-1.amazonaws.com/{key}",
"method": "GET",
"auth": {
"type": "aws_sigv4",
"region": "us-east-1",
"service": "s3",
"access_key_id": "{{secret:sec_01HAWSKEYID}}",
"secret_access_key": "{{secret:sec_01HAWSSECRET}}"
}
}
}
  • Signing happens last, over the final method, URL, headers and body. Only SOAT-controlled headers are signed (host, content-type, x-amz-*); context headers and Idempotency-Key go unsigned (AWS permits it).
  • aws_sigv4 with body_mode: "multipart" is 400 VALIDATION_FAILED at create/update: the body bytes are unknown at signing time.
  • Path encoding follows the service: segments are URI-encoded twice except for s3 (once), per the SigV4 spec.
  • GCP tokens are cached per service account, token endpoint and scope set, refreshed shortly before expiry.
  • service and region are signed, not just routing; a mismatch is rejected by the target.
  • Credential failures are 502 TOOL_AUTH_FAILED (distinct from 502 TOOL_HTTP_ERROR, the target rejecting the call); a token endpoint's status and body land in error meta as upstream_status and upstream_body.

Every auth field is validated at create/update (400 VALIDATION_FAILED) and by validate-formation.

Request body encoding (body_mode)​

The caller's input is the request body verbatim (keys never case-transformed). POST, PUT and PATCH default to JSON (Content-Type: application/json); execute.body_mode set to "multipart" sends multipart/form-data:

  • Scalars (string, number, boolean) become form fields.
  • A { content_type, filename, data_base64 } field (the shape an ingestion rule passes for the uploaded file) is base64-decoded and attached as a file part with that filename and content type.
  • Content-Type is left unset so fetch generates the boundary (any Content-Type in execute.headers is dropped).

Binary responses (response_mode)​

By default a 2xx body is parsed as JSON, else returned as text, which corrupts binary content. execute.response_mode: "base64" returns the body as the same file shape multipart accepts:

{ "content_type": "audio/ogg", "filename": "voice.ogg", "data_base64": "T2dnUwACAAAA…" }
  • content_type is the response's media type without parameters (application/octet-stream when absent).
  • filename comes from Content-Disposition (filename* over filename) and is omitted when the target names none.
  • A body over TOOL_RESPONSE_MAX_BYTES is 502 TOOL_RESPONSE_TOO_LARGE, refused while it streams.
  • A non-2xx answer is 502 TOOL_HTTP_ERROR with its text, as in json mode.
  • Traces and transcripts record the object with data_base64 replaced by its size.

A pipeline step can hand the object to a multipart tool as one field:

{
"steps": [
{ "id": "download", "tool_id": "tool_download", "input": { "media_id": { "var": "input.media_id" } } },
{ "id": "upload", "tool_id": "tool_upload", "input": { "file": { "var": "steps.download" } } }
]
}

client​

Client tools have no server-side execute. A call pauses the generation with status: "requires_action" and the pending tool calls; the caller executes locally and submits results via POST /agents/{agent_id}/generate/{generation_id}/tool-outputs, which answers with a final result or another requires_action. Each pause accepts outputs once: a second submission, or one racing the first, is 409 GENERATION_NOT_AWAITING_TOOL_OUTPUTS.

Resuming continues the paused turn: the agent's tool_choice applies, step_rules keep counting from the turn's first step, and only what remains of max_steps can be spent.

Response when a client tool is called:

{
"status": "requires_action",
"generation_id": "gen_abc123",
"required_action": {
"type": "submit_tool_outputs",
"tool_calls": [
{
"tool_call_id": "call_xyz",
"tool_name": "read_local_file",
"args": { "path": "/tmp/data.csv" }
}
]
}
}

Submitting tool outputs:

POST /agents/{agent_id}/generate/{generation_id}/tool-outputs

{
"tool_outputs": [
{
"tool_call_id": "call_xyz",
"output": "col1,col2\n1,2\n3,4"
}
]
}

mcp​

An mcp tool proxies a Model Context Protocol server: at generation time its tools are discovered and registered with the model (one tool ID, many names, each with the advertised name, description and parameters); each call is forwarded and the result fed back.

mcp.url and mcp.headers accept secret references (resolved before connecting, as for http headers); mcp.headers also accepts {{context:<key>}}.

What the protocol carries​

Discovery follows nextCursor until the catalogue is exhausted, so a paginated server is registered in full. A tool's title, annotations (destructiveHint, readOnlyHint, …) and output_schema are carried onto the registered tool — not sent to the model, which sees name, description and input schema, but readable from the tool's call parts. A page that fails after an earlier one succeeded resolves nothing: a partial catalogue is a turn missing tools, which is reported rather than run silently.

A call's structured_content is the result when the server sends one; otherwise the content blocks, text blocks joined and parsed as JSON when they are JSON, and handed over as the block array when any block is not text (an image is never flattened away).

A call the server answers with is_error, or with a JSON-RPC error, is a failed tool call: the model is told the tool failed and the failure is logged. Through POST /tools/{id}/call it is 502 MCP_TOOL_ERROR, with the tool name and server URL in meta. Neither is a transport failure — an unreachable server is TOOL_EGRESS_BLOCKED or a resolution failure instead.

Scoping an MCP tool to a subset of actions​

By default an mcp tool exposes the whole server surface. actions allowlists MCP tool names:

{
"name": "oneclick",
"type": "mcp",
"mcp": { "url": "https://mcp.oneclick.example/sse" },
"actions": ["list_campaigns", "get_campaign"]
}

Only allowlisted tools are registered with the model; POST /tools/{id}/call and pipeline steps reject an action outside it with 400 VALIDATION_FAILED before contacting the server.

Omitted or null exposes everything; [] nothing. MCP names are discovered at runtime, not validated at create/update (unlike builtin); an unadvertised name is never exposed.

denied_actions is a denylist (for a read+write server where enumerating every read tool would drift), enforced at the same points, applied after actions and winning: a name in both is denied. Omitted or null denies nothing.

A server that defers its tools behind search / describe / call (SOAT's own MCP server among them) lists only those three, so both lists name the tools behind call, never the proxies. With actions, the named tools are described and registered under their own names and the proxies are not exposed; with only denied_actions, the proxies are exposed and call refuses a denied tool before contacting the server. POST /tools/{id}/call takes the tool's own name as action and sends it through call; an action of call is held to the lists by the tool it names.

builtin​

A builtin tool exposes the SOAT platform actions listed in actions, named as the platform's MCP tools (get-document, search-knowledge, create-file), not REST operationIds (search-knowledge, not searchKnowledge). Worked example: Orchestrate a Sonnet - Step 4 (Create the fixed write tool).

Create/update validates every entry against the registry: an unknown name is 400 VALIDATION_FAILED; a camelCase name matching a kebab-cased action gets a suggestion ("searchKnowledge" (did you mean "search-knowledge"?)).

A non-2xx action response is 502 TOOL_HTTP_ERROR with the real status in meta.tool_status_code. A 204 No Content answer (every delete-*) yields null.

Operations whose response cannot be a tool result are 400 VALIDATION_FAILED at create: download-file (raw bytes; use download-file-base64) and export-audit-entries (unbounded NDJSON; use list-audit-entries); both remain over REST, SDK and CLI. create-agent-generation has no stream and returns the completed generation.

Actions an agent may not be given​

These actions are withheld from agents whatever the caller's credential allows; naming one in actions is 400 VALIDATION_FAILED on create and update:

ActionsWhy
approve-approval, reject-approvalAn approval exists to put a person between an agent and an action; an agent that settles its own has removed them
create-api-key, update-api-key, delete-api-key, get-api-key, login-user, bootstrap-userMint or return a credential
get-trigger-secret, rotate-trigger-secret, get-webhook-secret, rotate-webhook-secretReturn or replace signing secrets
create-secret, update-secret, delete-secretWrite secret material. get-secret stays available: it answers has_value, never the value
attach-user-policies, create-policy, update-policy, delete-policy, create-user, delete-user, list-users, get-userChange or enumerate who may do what
update-ai-provider-prices, update-project-pricesRewrite the price book its own spend is metered against. The get-* twins stay available

Over MCP these are ordinary operations (the caller is a person acting as themselves). A tool row created before an action joined the list keeps it stored; the agent's surface leaves it out. An mcp tool pointed at this deployment's own MCP endpoint reaches them again; prefer a builtin tool.

The project a builtin action acts on​

On every action that takes project_id the server pins it to the agent's own project and removes it from the model's schema. The pin outranks a preset parameter naming another project and applies to agent tool calls only; a direct POST /tools/{tool_id}/call is unaffected.

How a builtin action is executed​

The action runs in-process through the same middleware stack and route handler as a client request (no network hop): per-call permission check against the caller's policies, strict field validation, audit logging, metering and quotas, and the snake_case response contract all apply. No ambient authority: an action the credential cannot perform fails with 502 TOOL_HTTP_ERROR / meta.tool_status_code: 403. A call not settled within SOAT_TOOL_CALL_TIMEOUT_MS (default 300000) fails with a timeout error. From an orchestration the tool acts as the run's own identity — Run identity.

Mid-turn agent calls inject tool_context, parent_trace_id, root_trace_id and max_call_depth only for actions whose REST schema declares them (currently only create-agent-generation).

pipeline​

A pipeline tool runs a fixed, ordered sequence of other tools as one call (compute → persist) server-side, with no model reasoning between steps; callable by orchestration tool nodes and directly via the API.

pipeline has steps and an optional output:

  • steps[] — each step names a tool by tool_id or an inline tool (Create Tool body minus project_id, no Tool row, not itself pipeline), never both; in a formation template tool_id may be { "ref": "ResourceName" } to another template tool, resolved at deploy. Optional action for builtin/mcp step tools. input maps keys to JSON Logic expressions over { input, steps }:
    • { "var": "input.<field>" } reads the pipeline's own input; { "var": "steps.<id>.<path>" } an earlier step's output.
    • Literals pass through; transforms (cat, +, if, map, filter, reduce, …) resolve recursively at any depth.
    • A value is an expression only when it is a single-key object whose key is a real JSON Logic operator; preserve returns its argument unevaluated for a literal that looks like one: { "preserve": { "var": "some.var" } }.
  • output (optional) — a JSON Logic expression building the return value (default: the last step's raw output); may be a bare scalar ({ "var": "steps.<id>.<path>" }) or an object with nested expressions.

Each step's output is captured under steps.<id>; a step may reference only earlier steps (forward references rejected at create). The first failing step aborts with PIPELINE_STEP_FAILED. Nested pipeline targets are bounded by a maximum depth (PIPELINE_DEPTH_EXCEEDED). Steps cannot target client tools.

Case convention. Structural keys are snake_case (tool_id, steps, input, output). A step's input keys are preserved verbatim as the sub-tool's arguments; author them, and var paths, in the casing of the data.

For LLM-decided multi-step flows see Orchestrations, which share the JSON Logic mapping model.

Validation. POST /tools, PATCH /tools/:id and validate-formation check the structure: every step needs a tool_id or an inline tool with a name, and a tool_id names a tool in the pipeline's own project (400 PIPELINE_INVALID_STEP otherwise), which is also where it resolves at call time, whoever calls the pipeline; a { "ref": … } tool_id is accepted in a formation template only (direct POST/PATCH need a literal string). validate-formation also warns (not errors) on a parameters property no step input or output reads via { "var": "input.<name>" }.

Output Mapping​

output_mapping is a JSON Logic mapping over { "output": <raw result>, "input": <merged input> }, applied to every tool type's raw result: { "var": "output.text" } extracts a scalar without a wrapping pipeline, { "var": "input.title" } echoes a request field, an object reshapes:

{ "transcript": { "var": "output.text" }, "language": { "var": "output.language" } }

input is the caller-supplied input with preset_parameters pinned over it.

  • pipeline tools. The top-level output_mapping runs after the pipeline's own output mapping, over its final result.
  • client tools. Applied when the submitted output is materialized back into the generation, keyed by tool name.
  • input is populated for a direct call (POST /tools/{id}/call), a pipeline step or a workflow/orchestration tool dispatch; {} for a tool the model calls from an agent's loop and for a client tool's mapping.
  • A var path resolving to null (commonly a missing output. prefix) emits a debug log entry; the result is unchanged.

Without output_mapping the raw result is returned unchanged.

Preset Parameters​

preset_parameters pins fixed values: a key matching an input-schema field is removed from the schema shown to the model and merged into every call before dispatch. Several tools can target different resources from one action: two builtin tools binding update-document, one with "preset_parameters": { "id": "doc_abc123" }, one with { "id": "doc_xyz789" }. End to end: Agent SOAT Tools and Preset Parameters - Step 6 (Create builtin tools).

A preset is a pin, not a default: it wins. A value the call supplies for a pinned key (a model naming the hidden field, or an input to POST /tools/{id}/call) is replaced by the preset; unpinned keys come from the call. No tool schema sets additionalProperties: false, so a hidden key is not rejected; merge order is the guarantee.

Holds on every tool type (builtin, http, mcp, client, pipeline) and dispatch surface (agent tool loop, direct call, pipeline step, orchestration tool node, converter invocation). A guardrail evaluates the pinned arguments (args.* in a guard condition, arguments on an approval item); a client tool receives them pinned at the requires_action boundary. On an mcp binding presets apply to every tool the server exposes, as to every action a builtin binding lists; use a separate binding for a key meant for one.

Pinning to a per-run value ({{context:}})​

A preset value may be a {{context:<key>}} token, resolved per call from the caller's tool_context:

{
"type": "mcp",
"mcp": { "headers": { "Authorization": "Bearer {{context:ocaToken}}" } },
"context_keys": ["ocaToken", "ocaAdAccountId"],
"preset_parameters": { "adAccountId": "{{context:ocaAdAccountId}}" }
}

Credential and scope both come from the run, neither from the model. A key missing from the call's tool_context is 400 MISSING_TOOL_CONTEXT_KEY (the literal placeholder is never sent); a resolved value is retyped to the parameter's declared schema type. {{secret:...}} is not resolved in a preset (secrets are headers-only).

Presets and model arguments reach the action wherever the OpenAPI operation declares the parameter (path, query, body): a list-* action's project_id, filters and pagination are query parameters, so { "project_id": "proj_abc123" } locks a builtin tool to one project. An omitted argument is left out, never sent empty.

Literal Credentials Are Masked on Read​

A tool is readable by anyone holding tools:GetTool, so a literal credential is stored and sent on every call but never echoed: secret_access_key, session_token and credentials under execute.auth, and any credential-named headers value in execute or mcp (Authorization, Cookie, or a name containing api-key / token / secret / password) read back as {"no_echo": true}. The mask is an object so a read-edit-write round trip fails the schema's type: string check instead of storing the placeholder as the credential.

A {{secret:...}} reference stays readable; it is the wiring, not the credential.

Where a Tool May Reach (Egress)​

An http or mcp tool's target is bounded by the deployment, not the tool definition. Default: any publicly routable address. Loopback, 10/8, 172.16/12, 192.168/16, link-local 169.254/16 (cloud metadata), CGNAT and IPv6 ULA fail with 403 TOOL_EGRESS_BLOCKED, the refused address in meta.tool_address.

The resolved address is checked on the first request and every redirect hop; credential headers (Authorization, Cookie) are dropped when a redirect changes origin. A public hostname resolving to 169.254.169.254 and a 302 Location: http://169.254.169.254/… are both refused.

The operator allows internal services in TOOL_EGRESS_ALLOWED_HOSTS, deployment-wide, not per project. For SOAT's own API use a builtin tool: in-process, under the caller's permissions.

Calling a Tool Directly​

POST /api/v1/tools/{tool_id}/call invokes a tool without an agent. Body: action (required for builtin and mcp; ignored for pipeline), input (the pipeline input for pipeline), tool_context. With an output_mapping the response is its result (Output Mapping).

  • A target neither publicly routable nor in TOOL_EGRESS_ALLOWED_HOSTS is 403 TOOL_EGRESS_BLOCKED before any connection is opened — Where a Tool May Reach.
  • A non-2xx target response is 502 TOOL_HTTP_ERROR; error meta carries tool_status_code, tool_response_body, tool_url and tool_method.
  • execute.auth failing to produce the credential is 502 TOOL_AUTH_FAILED.
  • A 2xx body that is not valid JSON is returned as raw text; an empty result (a builtin action answering 204) is 200 with a JSON null body.
  • tool_context reaches a tool declaring a {{context:<key>}} token; with no session on this route, session_id, actor_id and actor_external_id are dropped — Calling a context-dependent tool directly.

Sharing a tool​

A tool can be offered to another project with a share. The grantee names it by id wherever a reference to a tool can be shared, or calls it on its own route with a key scoped to the grantee project; the call runs on this tool's endpoint and secrets, carries the grantee's id as calling_project_id, and is metered in the grantee's project. The grantee reads the tool's id, name, description and parameters only. See Using a shared tool.

Pricing a tool​

A project prices a tool it owns with resource price rows: each adds a component to the tool's tool_execution events, its quantity read off the call's input, response (after output_mapping), outcome or duration_ms. A call through a share is priced from the owner's rows. A pipeline or client tool has no execution of its own to price.

Deleting a tool​

DELETE /api/v1/tools/{tool_id} refuses a tool that is a decider's backend with 409 TOOL_HAS_DEPENDENTS; meta.decider_count counts the deciders to delete or repoint first. A tool another project accepted a share of is refused the same way, with meta.accepted_share_count, until force=true revokes the shares. So is a tool one of its own project's ingestion rules converts with, with meta.ingestion_rule_count, until force=true.

Who may act on a tool​

Every route that acts on one tool — read it, change it, delete it, or call it — is authorized against that tool's SRN, srn:<project_id>:tool:<tool_id>, not against the project. A policy may therefore name the tools it covers:

{
"statement": [
{
"effect": "Allow",
"action": ["tools:GetTool", "tools:CallTool"],
"resource": ["srn:proj_V1StGXR8Z5jdHi6B:tool:tool_V1StGXR8Z5jdHi6B"]
}
]
}

This is the same check approvals have always made for tools:CallTool, so a per-tool grant now means the same thing whether the call arrives directly or through an approval.

Refusals keep the shapes IAM defines: a read the caller may not perform is 404 (a tool it may not see does not announce itself), a write or a call is 403, and a credential scoped to another project is 403 API_KEY_PROJECT_SCOPE. A write on a resource in a project none of the caller's policies name is 404 too — the same answer their read would get, so a refusal never confirms existence across a tenant boundary.

Listing tools stays project-scoped: GET /api/v1/tools asks whether the caller may list tools in a project at all, so a policy that names individual tools grants no listing.

Configuration​

Environment VariableRequiredDescription
TOOL_RESPONSE_MAX_BYTESNoCeiling on an http tool's response_mode: base64 body, in bytes. Defaults to FILE_UPLOAD_MAX_BYTES (25 MB), so a downloaded file can be uploaded. A larger body is 502 TOOL_RESPONSE_TOO_LARGE.

Examples​

Create an HTTP tool​

soat create-tool \
--project-id "$PROJECT_ID" \
--name "get-weather" \
--type http \
--description "Fetches current weather for a city" \
--execute '{"url":"https://api.weather.example/v1/current?city={city}","method":"GET"}' \
--parameters '{"type":"object","properties":{"city":{"type":"string"}},"required":["city"]}'

Create a pipeline tool​

Step one computes a sum, step two persists it, the first output mapped into the second input ($CALC_TOOL_ID and $SAVE_TOOL_ID are existing tool IDs).

soat create-tool \
--project-id "$PROJECT_ID" \
--name "compute-and-save" \
--type pipeline \
--description "Computes a sum and persists the result" \
--parameters '{"type":"object","properties":{"x":{"type":"number"},"y":{"type":"number"}},"required":["x","y"]}' \
--pipeline '{"steps":[{"id":"compute","tool_id":"'"$CALC_TOOL_ID"'","action":"add","input":{"a":{"var":"input.x"},"b":{"var":"input.y"}}},{"id":"persist","tool_id":"'"$SAVE_TOOL_ID"'","input":{"value":{"var":"steps.compute.sum"}}}],"output":{"saved_id":{"var":"steps.persist.id"}}}'

Call a tool directly​

soat call-tool --tool-id "$TOOL_ID" --input '{"city":"São Paulo"}'