AI & Agents

Agent Tool Security

The least-privilege exposure engine that decides which tools an agent session may see and call — per-session manifests, schema-hash binding, tenant policies, redaction, and decision receipts.

If an agent is a Worker, this is the governance that makes it safe to let one take work: least-privilege access to exactly the tools its manifest allows, and an exposure receipt for each evaluated call. Handing an agent the full tool catalog increases context size; authorization still depends on its grants. The agent tool exposure engine decides which registered tools the agent may see and call: on every governed tools/list it builds a per-session exposure manifest from the requested candidates, server-owned security descriptors, and a tenant policy. Every governed tools/call must present that manifest's lease and echo the exact tool it was granted. Anything that does not line up is denied. Exposure decisions produce redacted decision receipts; resource-permission refusals can occur before exposure evaluation without one. Those content-hashed manifests and decision receipts are also the evidence a Receipt draws on to prove what an agent did.

This engine sits under both the external MCP server and the in-product durable agent runtime; they draw from the same canonical tool metadata and enforce the same policies.

The manifest handshake

When exposure is enabled, tools/list no longer returns the raw catalog. It returns a manifest scoped to the caller's tenant (plus optional environment, actor, preset, session, and entity hints) along with a set of identifiers the client must carry forward:

FieldPrefixWhat it is
agent_tool_session_idats_The session lease the subsequent calls run under.
manifest_idatm_The manifest that was built for this list.
manifest_hash—A hash binding the call to this exact manifest.
receipt_idatr_The decision receipt recorded for the list.

Each tool in the list also carries a per-tool schemaHash and an exposureMode. The schemaHash is the fingerprint of the advertised schema the agent must echo when it calls that tool.

An MCP client may supply tool_names on tools/list to narrow the candidate set before policy evaluation. Exact names are sorted and deduplicated, with a limit of 64 raw entries and 256 bytes per name. Supplied null, empty, invalid, unknown, or unwired selections are refused before a manifest is created. An omitted selector retains full discovery. Selection never overrides policy: denied tools stay denied, and a tool omitted from this manifest cannot be called with its lease. All session, expiry, actor, scope, schema, and version checks continue to apply. See the buyer tool recipe.

Ductor-hosted model-only agents also retain governance. With no callable tools, admission persists an empty manifest and policy evidence; it does not inherit the full registry. Tool proposals outside that empty grant cannot execute.

match mismatch / drift / scope / not-callable tools/list Build manifest Manifestats_ · atm_ · hash · atr_tools w/ schemaHash tools/callecho tool_version + expected_schema_hashpresent session + manifest lease AuthorizeCallmatch? Execute(result redacted per grant) DENY + receipt (atr_)

What a call must carry, and what gets denied

Every tools/call under an exposed session must echo tool_version and expected_schema_hash (the values from the manifest) and present the session and manifest identifiers. Actor identity is derived from the authenticated principal; an actor_id supplied by a client is only an equality assertion and cannot choose another identity. The engine denies the call — returning a redacted receipt rather than executing — when any of these hold:

  • The echoed expected_schema_hash is missing or does not match the pinned hash.
  • The manifest has drifted (the tool's advertised schema changed since the grant).
  • The tool's required scope is not satisfied.
  • The tool is not visible or not callable in this manifest.
  • The presented session/manifest lease does not match (manifest-binding mismatch).
  • The authenticated actor does not match the actor pinned into the session and manifest.
  • The arguments do not validate against the exact narrowed schema advertised in the grant.

The schema hash binds to grant time, not call time

The expected_schema_hash check binds the call to the hash that was pinned in the manifest when the session was granted. It does not recompute the tool's live schema at execution. That is what makes a schema change detectable as drift instead of silently accepted.

Policies

A tenant policy decides how candidate tools map to exposure modes. The default policy, builtin_safe_read, is deliberately narrow: it exposes only read, non-mutating tools and hides everything else. Write, destructive, and unknown tools require an explicit policy that opts them in — least privilege is the default, not a configuration you have to remember to turn on.

Policies are richer than an allow-list. A policy carries:

  • Rules — selectors that match tools by name, prefix, family, or risk label, each assigning an exposure mode (allow, hide, deny, dry_run_only, approval_required, confirmation_required, async_job_only, staged_change) and a reason.
  • Redaction profiles — summary mode, a max-bytes cap, and allowed destinations (llm, mcp, transcript, logs) applied to tool output before the agent sees it. The built-in safe profile summarizes to at most 8 KB.
  • Invocation policy — fixed arguments, allowed enum values, and a schema overlay that narrows the advertised schema so the agent literally cannot request fields the policy forbids. A pinned connection_mode becomes a server-fixed argument; requires_idempotency adds and enforces a non-empty idempotency_key; max_duration_millis sets the execution deadline; max_result_bytes rejects oversized output before it reaches the journal, model, or MCP client; and expires_after_seconds shortens the manifest lease.

Policy fields are not accepted as decoration. A negative limit is invalid, a ceiling too large to enforce is refused, and a rate_bucket without a distributed limiter is rejected when the policy is written. A max_cost_cents ceiling is accepted — it is enforceable against the price connector pricing resolves before provider I/O — but a deployment that has not wired the spend gate refuses invocations under a capped rule rather than running them uncapped. That refusal lives at execution time, because validation cannot see runtime wiring. Either way a policy never appears stricter than the runtime can actually enforce.

Runtime authorization binding

For a Ductor-hosted agent, manifest authorization is only the first half of the boundary. Before a tool is executed, the durable runtime records an invocation bound to the tenant, agent session, turn, invocation id, canonical tool name and version, schema hash, manifest and policy receipt, and a hash of the normalized arguments. The execution receipt also snapshots effective arguments, execution mode, duration and result-size limits, and the output-redaction policy. It is attached to the resulting journal event.

This prevents a worker from substituting another tool, schema, tenant, or argument set between authorization and execution. If policy requires approval, that exact binding is parked in an input_required event. The approval response can release only that invocation; it cannot authorize a later model proposal or a modified payload.

Authorization also pins the server-resolved execution target: environment, deployment, connector connection or remote MCP instance, region, endpoint-policy id, and normalized endpoint fingerprint. A model or browser cannot supply a URL. Rotation or target drift between authorization and execution requires explicit reauthorization or reconciliation; the executor never silently retargets an approved call. Multi-request operations reuse the same target and credential context.

For Ductor-hosted chat, request-context RBAC is evaluated before the durable authorization receipt is journaled. A recovery worker executes the exact bound receipt without depending on an expired browser or HTTP context; it does not silently re-authorize as a different actor after restart.

Rejected approvals are represented as durable, protocol-valid tool results. The model can explain or choose a safer path without the original mutation being executed.

API and scopes

Server-owned mandatory permissions supplement exposure policy. In particular, bespoke work_receipt.get and work_receipt.verify require work_receipt:read on each direct or governed call. Their server and operational backend use the same canonical explicit-scope, resource-wildcard, and role-bundle authorizer, with a current nonanonymous principal and authenticated tenant. An expired or unauthorized cross-tenant principal cannot reach private lookup through an older manifest. An administrator's tenant-local role cannot cross a tenant boundary; the deployment-issued platform-administrator rule retains its intentional scope.

Receipt read permission is separate from buyer market observation. Seller bids, declines, and amendments use routing_market_bid:write; market management uses routing_market:write. Built-in operator and administrator bundles intentionally grant both writes. Exposure grants and confirmation rules do not grant missing resource permissions, worker ownership, or environment authority.

The nine bespoke market tools enforce the current principal and authenticated tenant before application lookup or effects on each direct or governed call:

Tool groupRequired permission
routing.market.preview, routing.market.get, routing.market.replayrouting_market:read
routing.market.open, routing.market.solicit, routing.market.close, routing.market.commit, routing.market.cancelrouting_market:write
routing.market.submit_bidrouting_market_bid:write

They require an explicit environment_id in their own arguments. Resolution checks current environment authority and persisted session scope; changing the argument cannot move a stored market to another environment. Receipt reads resolve stored scalar environment authority before decoding private signed payloads, and list filtering occurs before pagination. An exposure lease is additional authority evidence, not a replacement for these checks. Descriptor-generated tools inject environment from top-level invocation scope and reject it inside arguments; retain that distinction when constructing calls. See the MCP clearing contract.

The exposure engine is managed through the AgentToolExposureService (Connect/gRPC, also mounted at REST under /api/v2/agent-tool-exposure/…):

OperationPurpose
CreateAgentToolExposurePolicy / UpdateAgentToolExposurePolicyAuthor a policy (server resolves the security descriptors).
GetAgentToolExposurePolicy / ListAgentToolExposurePolicies / DeleteAgentToolExposurePolicyManage policies.
CreateAgentToolSessionBuild a manifest + session lease for a scope.
RefreshAgentToolSessionRebuild the manifest and revoke the old session — the drift remedy.
RevokeAgentToolSessionInvalidate a session lease.
GetToolExposureManifest / ExplainToolExposureManifestRead a manifest; explain expiry and repair actions.
PreviewAgentToolExposureDry-run a scope against the policy without minting a live lease.
ListToolExposureReceiptsAudit the allow/deny decisions.

Access is gated by the scopes agent_tool_exposure:read, :write, :run, and :admin.

Sessions live in the default environment when none is given, and a session lease expires after 15 minutes.

A schema mismatch means refresh, not retry

When a call is denied for a schema-hash mismatch, the manifest has drifted. Do not retry with a guessed hash — that will keep failing. Refresh the session (RefreshAgentToolSession, or a fresh tools/list) to obtain the current manifest and its pinned hashes, then call with those. ExplainToolExposureManifest reports when a manifest has expired and lists the repair action.

Award execution and final acceptance evidence

An RFQ award's admission-era tool pin is derived from its source definition; it is not an independently observed execution manifest. An award-bound AI_AGENT session runs through this same governed broker and records its actual manifest and policy receipt. Acceptance evidence signs those runtime facts alongside the original source definition and the distinct governed execution definition. A caller cannot supply inline tool/model overrides to that step. See Clear AI work and Verify a receipt.