Skip to content

Govern Composio tool execution

OpenFirma can govern Composio tool execution without running another gateway. The existing Sidecar HTTPS boundary decodes Composio requests, maps tools to canonical action classes, evaluates Cedar, and audits one event per logical tool action.

Composio governance is not opt-in: the pinned catalogs and the protocol decoder load with every Sidecar, whether or not the composio mapping pack is installed. Installing the pack adds the HTTPS interception hosts and the startup coverage warning below; removing it does not turn Composio decoding off.

Add the built-in mapping to an existing project:

Terminal window
firma config --profile generic --mapping composio

The generated configuration enables strict HTTPS interception for app.composio.dev and backend.composio.dev. Do not add those hosts to bypass_hosts; opaque CONNECT traffic cannot be decoded at Layer 7.

The Sidecar cross-checks this at startup: when the mapping rules reference the Composio hosts but the HTTPS MITM configuration leaves them bypassed, unintercepted, or non-strict, it logs a warning per affected host so a misconfiguration cannot silently downgrade governance to opaque tunnels. When HTTPS MITM is off altogether, that becomes a single combined warning naming both hosts. Wildcard and catch-all rule hosts count as referencing the Composio hosts, because such rules do govern that traffic at runtime.

The check only runs in the HTTP-proxy interceptor mode. Under any other interceptor.mode it is silently skipped, so verify interception coverage by hand there.

Continue to run your chosen command:

Terminal window
firma run --profile generic -- your-agent

firma run does not start Composio or PAI. It starts or reuses the normal OpenFirma components, configures the proxy and CA, and executes the command you provided.

PAI reaches Composio through two independent paths:

  • the runtime calls the hosted MCP endpoint;
  • the backend calls direct execution and Tool Router endpoints.

Both processes must use the OpenFirma HTTP/HTTPS proxy and trust the Firma CA. Production PAI startup fails when Composio is enabled without both settings. A failed proxied request is not retried directly.

PAI keeps its pai-assistant:<bot_uuid> user value. The account value carried by a request may be an identifier or alias; audit records preserve that selector but do not claim it is a provider-confirmed identity.

Each decoded tool uses a transport-independent resource:

composio://gmail/GMAIL_SEND_EMAIL

The connector still dispatches the original HTTPS request. The logical resource is used only for capability scope, Cedar, provenance, session history, and audit.

An exact-resource Cedar rule can block one tool:

forbid (
principal,
action == Firma::Action::"communication.external.send",
resource == Firma::Resource::"composio://gmail/GMAIL_SEND_EMAIL"
);

Policies can also use optional context fields:

forbid (
principal,
action,
resource
) when {
context has composio_toolkit &&
context.composio_toolkit == "gmail" &&
context.composio_batch_size > 1
};

The context includes toolkit, exact slug, user selector, account selector, session identifier, and batch position when those values are present.

Linking or removing a connected account changes what an agent can reach, so those requests are governed like tool calls instead of passing through. Writes (POST, PATCH, PUT, DELETE) to connected_accounts and auth_configs, POST to the Tool Router session link route — the only method Composio defines there, since it only initiates an OAuth link flow — and PATCH, PUT, and DELETE on a Tool Router session/{id} resource (under /api/v3 and /api/v3.1), decode into one account.permission.change action with a synthetic resource:

composio://composio/COMPOSIO_CREATE_CONNECTED_ACCOUNT

Creating a Tool Router session (POST to the session collection) is governed the same way. The creation payload binds the connected accounts every later session call executes within, and Composio resolves an omitted downstream account selector from that stored state, so the creation decodes into one account.permission.change action per selected account (resource composio://composio/COMPOSIO_CREATE_SESSION); a creation that selects no accounts decodes into a single unbound action. A session update (PATCH or PUT on session/{id}) can rebind those accounts through the same connected_accounts field, so its selection decodes into per-account COMPOSIO_UPDATE_SESSION actions the same way, an explicit connected_accounts: null clears the selection and decodes unbound, and an update body that cannot be parsed fails closed. Account policy therefore meets each account before the session perimeter exists or changes. One request may select at most 50 accounts.

A capability must grant account.permission.change for these requests to succeed, and Cedar can deny them like any other action. Grant it in two scopes: the backend session that runs OAuth flows needs it for the connected_accounts and auth_configs writes, and whichever principal creates Tool Router sessions — often the agent runtime itself in the stock Composio flow — needs it for session creation and updates. Withholding the class from agent runtimes entirely denies every session they try to create; constrain them with Cedar conditions on composio_account instead, so an agent can open sessions but only over the accounts it is allowed to use.

One sharp edge: a session write that selects no accounts carries no composio_account at all, so a condition that only matches the attribute’s value admits it. For the “only these accounts” guarantee, also forbid session writes whose context lacks composio_account (a forbid ... unless context has composio_account pattern). Even then, Composio resolves an omitted account selector on a later session execution from server-side state such as the user’s default account, which no per-call policy sees; the account decision is made at the governed session writes or not at all.

Reads of the same two families are governed too. GET, HEAD, and OPTIONS on connected_accounts and auth_configs disclose which integrations exist and how they authenticate, so they decode into one credential.read action with a synthetic resource such as composio://composio/COMPOSIO_LIST_CONNECTED_ACCOUNT (LIST for the collection, GET for a single item). Discovery routes (tools, toolkits), Tool Router session reads, and MCP session streams still pass through.

These reads are the one governed shape allowed to carry a query string, so GET /api/v3/connected_accounts?cursor=... keeps paginating: a cursor picks a page of the same listing rather than changing which action is classified. The logical resource stays query-free and the cursor is restored onto the dispatched request. Every other governed Composio request still denies a query string outright.

COMPOSIO_MULTI_EXECUTE_TOOL is all-or-nothing. OpenFirma decodes every child before dispatch and evaluates them in input order.

  • If every child is allowed, the original request is dispatched once.
  • If any child blocks, the request is not dispatched.
  • Allowed siblings are audited as aborted for batch atomicity.
  • Monitor mode dispatches once and retains each would-block reason. This extends to protocol-level denials: malformed payloads, unknown tools, and protocol upgrades are forwarded in monitor mode with a monitor_mode: audit annotation instead of being blocked. When every child would have blocked, the forwarded request carries no injected credentials, so the observed upstream response may differ from what an admitted batch would see.

OpenFirma never splits, reorders, or partially forwards the batch.

The Sidecar ships reviewed catalogs for Gmail (63 tools), Google Calendar (49), and Slack (167), pinned at toolkit version 20260721_00, plus Notion (56) pinned at 20260730_00. They are compiled into the binary, so enforcement never queries Composio on the hot path and a tool is governed the same way on every host.

Each slug carries a manually assigned canonical class, so policies stay transport-independent:

ToolAction class
GMAIL_FETCH_EMAILScommunication.external.read
GMAIL_SEND_EMAILcommunication.external.send
GOOGLECALENDAR_FIND_EVENTcalendar.read
GOOGLECALENDAR_DELETE_EVENTcalendar.delete
SLACK_SEND_MESSAGEcommunication.external.send
SLACK_INVITE_USER_TO_CHANNELaccount.permission.change
SLACK_CREATE_CANVASdocument.write
NOTION_CREATE_NOTION_PAGEdocument.write
NOTION_ARCHIVE_NOTION_PAGEdocument.delete

Refreshing a toolkit is a maintainer task, not an operator one: see Composio enforcement for the refresh and review loop.

Unknown toolkits, missing slugs, mismatched versions, malformed execution payloads, custom tools, raw proxy execution, and shell or workbench tools fail closed. Direct execution requires the toolkit version in the payload and denies unpinned_tool without one. The Tool Router session routes (execute, execute_meta) and hosted MCP define no version field in Composio’s API, so a call without one is admitted and classified from the pinned snapshot, while a version a client does attach is still checked against the pin. Governed requests carrying a query string are also denied: the query never participates in the policy decision, so it must not ride along on an admitted dispatch. Hosted MCP URLs deny query strings uniformly, discovery included, so a query-carrying MCP URL fails at the handshake with a clear denial instead of breaking only on tool calls. Recognized routes accept only read methods (plus DELETE for MCP session teardown and POST on the Tool Router session execution routes); anything else fails closed, including a POST to a discovery route such as /api/v3/toolkits or to an existing session/{id} resource.

Three sharp edges are worth knowing before writing policy.

NOTION_REPLACE_PAGE_CONTENT overwrites a whole page but is classified document.write, matching how filesystem.write covers “create or overwrite”. A policy meant to block destructive edits must cover document.write or name that slug; document.delete alone does not catch it.

GOOGLECALENDAR_BATCH_EVENTS performs a mixed batch of create, update, and delete operations in one call. It is classified at the highest applicable tier, calendar.delete, rather than split per operation, so granting that class for this slug also permits event creation and modification in the same call.

Slack’s canvas-specific read, list, and delete tools are deprecated upstream in favor of generic file tools. Those replacements — SLACK_RETRIEVE_DETAILED_INFORMATION_ABOUT_A_FILE, SLACK_LIST_FILES_WITH_FILTERS_IN_SLACK, and SLACK_DELETE_FILE — stay under communication.external.* because they act on Slack files in general, not canvases. A policy meant to block canvas access entirely must name those three as well; Composio exposes no canvas-only equivalent of them.

Several Composio API families the SDK can call are not yet decoded and fail closed as unsupported_route. This is deliberate — an unrecognized route must never pass — but it means enabling these SDK features breaks against the Sidecar today:

  • automatic file upload and download (POST /api/v3/files/upload/request and the files routes), used whenever a tool parameter is file-backed;
  • session file mounts (session/{id}/mounts/...);
  • the manual Tool Router discovery call (POST session/{id}/search) — the equivalent COMPOSIO_SEARCH_TOOLS meta-tool through execute_meta or hosted MCP passes through instead;
  • inline custom tools (POST session/{id}/attach), consistent with custom tools being unsupported on the execution routes;
  • triggers (trigger_instances, triggers_types), including their read-only listings;
  • MCP server management (/api/{v3,v3.1}/mcp/servers and related routes). One consequence: DELETE /api/{v3,v3.1}/mcp/{id} is indistinguishable by shape from hosted MCP transport teardown, so an MCP server-configuration delete passes through with the teardown allowance. Deleting only narrows the reachable surface, but treat MCP server management as an operator-side task outside agent traffic. Standalone MCP servers on mcp.composio.dev are outside the protected hosts entirely and fall to generic mapping.

Each logical action emits its own signed audit event. The canonical class and the composio://<toolkit>/<tool_slug> resource identify the tool without changing the shared protobuf contract. The event also records the decision and shared dispatch outcome.

The event does not include API keys, OAuth tokens, authorization headers, cookies, complete tool arguments, request selectors, or provider response bodies.

See Read and verify the audit log for sink and signature details.