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 a decision receipt for every call. Handing an agent the full tool catalog is the same mistake as handing a service account every scope. The agent tool exposure engine is the authorization layer that prevents it: on every tools/list it builds a per-session exposure manifest from server-owned security descriptors and a tenant policy, and every tools/call must present that manifest's lease and echo the exact tool it was granted. Anything that does not line up is denied with a redacted decision receipt — no partial trust, no guessing. Those signed 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_hashA 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.

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

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.