Govern a tool in one wrapper

Start by gating a tool you already have, then move the actions that matter inside the execution boundary. Both modes are one import.

The fastest path — one config line, no code

Point any MCP client at TrustRail and every tool call becomes a governed decision on the way through. This is the whole integration:

{ "mcpServers": { "trustrail": { "command": "npx", "args": ["-y", "@trustrail/mcp-server"],
    "env": { "TRUSTRAIL_AGENT_CREDENTIAL": "trb1_..." } } } }

If your agents already run on this computer (Claude Code, Claude Desktop, Cursor, Windsurf), the desktop app finds them.

Download the desktop appIt opens straight into the Setup Assistant — no Docker, no terminal.
Scan and chooseIt finds the agents and provider keys on this machine, shows you everything it found, and imports only what you tick.
Approve your first actionThe console walks you through the rest: your agent asks, you tap Approve, and the receipt is already in the ledger.

Prefer code? Everything below is the SDK route — both modes are one import.

Install

Both SDKs are published as a beta: the JavaScript SDK and the MCP server on npm, the Python SDK on PyPI as trustrail-sdk (imported as trustrail; the name trustrail on PyPI belongs to an unrelated project).

npm install @trustrail/sdk-js
# MCP: npx -y @trustrail/mcp-server (see Coding agents below)

pip install --pre trustrail-sdk

A new workspace’s Overview walks you through the two things an agent needs and then prints the block to set. Quick registration returns a trb1_… credential bundle that carries the URLs and the environment, and the default connection when one already existed. The Overview registers the agent before it adds the connection, so it prints the connection beside the bundle:

TRUSTRAIL_AGENT_CREDENTIAL=trb1_...
TRUSTRAIL_CONNECTION_ID=...
TRUSTRAIL_PROVIDER=ledger

The six separate variables still work and win when set (TRUSTRAIL_URL, TRUSTRAIL_GATEWAY_URL, TRUSTRAIL_ENVIRONMENT_ID, TRUSTRAIL_CONNECTION_ID, TRUSTRAIL_PROVIDER, and a bare tr_agent_… credential); they are the deprecated way to say the same thing.

Those six names are why setup is one line: agentFromEnvironment() in TypeScript and agent_from_environment() in Python read exactly this set.

Every action names a connection, so one must exist before anything can be evaluated. For Decision mode a decision-only connection is a single click: it holds no provider credential and can never execute, which is exactly the guarantee guard() already makes. Actions that really move money need a reviewed provider integration instead — that is Enforce mode, where the gateway holds the credential.

Decision mode — gate the tool you have

guard() wraps an existing implementation. It keeps the original signature, so adopting it is a one-line change at the definition site, and the wrapped function runs only on an allowing decision.

import { agentFromEnvironment } from "@trustrail/sdk-js";

const agent = agentFromEnvironment();

const sendEmail = agent.guard(
  {
    actionType: "communications.email.send",
    resource: { type: "mailbox", id: "support@example.com" },
    purpose: (args) => `Email ${args.to.join(", ")}: ${args.subject}`,
  },
  myExistingSender,
);

When policy asks a person, the call waits up to 120 seconds, silently, for someone to approve it in the console's Approval inbox; set waitForApprovalSeconds on the spec to change that.

A denial throws TrustRailDeniedError, and a wait that runs out throws TrustRailApprovalRequiredError. A spent monthly action quota throws TrustRailQuotaExceededError (TrustRailQuotaExceeded in Python) — distinct from a denial because policy never saw the action, and distinct from a transport failure because retrying cannot help; the fix is an owner upgrading the plan. All three carry text that is safe to hand to a model verbatim, because it tells the model the action did not happen and that finding another route is not the fix.

Decision mode gates your code; it does not hold your credentials. A compromised or buggy agent process could call the provider directly. Do not describe it as enforcement.

Enforce mode — the agent never holds the credential

act() drops the local implementation. TrustRail evaluates the action, waits for a human if policy requires one, mints a one-use Ed25519 authorization, and spends it at the governed gateway, so the provider call happens inside the boundary with credentials the agent process never sees.

Your own agent's Enforce needs a real provider: a GitHub App, or a Stripe test key on a deployment that has one. There is no sandbox provider you can connect yet. To see the whole path run today — custody, the grace hold, the approval and the receipt — use the console's try-it-now demo, which runs it against a simulated provider.

The example below is the hard case on purpose: a purchase is CRITICAL, so it needs a recorded request from a person and two approvers, as described under actions that need a person's request.

const result = await agent.act({
  purpose: "Reorder the stock the planner flagged",
  actionType: "payments.purchase.create",
  resource: { type: "payment", id: "order-4821" },
  target: { type: "merchant", id: "supplier-acme" },   // who gets paid
  parameters: { lineItems: [{ sku: "WIDGET-1", quantity: 12 }] },
  financial: { amountMinor: "4999", currency: "USD" },      // how much
  waitForApprovalSeconds: 120,
});

// result.status: "EXECUTED" | "DENIED" | "AWAITING_APPROVAL"

The counterparty and the amount are target and financial, not parameters. Policy and risk read who is being paid and how much from those fields directly, rather than parsing an action-specific payload — so a spend rule works the same across every action type that moves money.

act() never throws on a policy outcome. A denial is a returned result, because refusing an action is a normal answer — and code that treats a refusal as a crash tends to work around it.

Actions that need a person's request

These actions are refused with TRACEABLE_HUMAN_INTENT_REQUIRED unless the agent shows that a person asked for them:

  • deployment.create
  • package.publish
  • payments.payout.create
  • payments.purchase.create
  • payments.refund.issue
  • repository.branch.delete
  • repository.merge
  • repository.push.force
  • repository.release.publish
  • secret.read

Record the request in the console, under Agents, with “Record a request for your agent”. It returns an id that is good for one action, for an hour, and you pass it with that action:

const result = await agent.act({
  // …the action, as above
  intentProvenance: {
    origin: "HUMAN",
    initiatedAt: "2026-10-07T09:00:00.000Z",   // when you recorded it
    humanIntentId: "0e1d2c3b-4a59-4687-8796-a5b4c3d2e1f0",   // the id the console gave you
  },
});

A request is not an approval. The purchase above is CRITICAL, so it also needs two approvers, and neither may be the person who registered the agent or the person who recorded the request; any action in a production environment needs the same. A workspace of one cannot complete it alone. To watch Enforce run end to end first, use the console's try-it-now demo.

Which actions can execute

Enforce mode needs a reviewed provider adapter, so the action registry states plainly which actions have one. A build-time check fails the release if the registry and the adapter table ever disagree, in either direction.

Action typeStatusHow to govern it
communications.email.sendEXECUTABLEact() — via smtp-test, google
payments.purchase.createEXECUTABLEact() — via payments-sandbox, stripe
payments.refund.issueEXECUTABLEact() — via payments-sandbox
payments.payout.createEXECUTABLEact() — via stripe
payments.subscription.cancelEXECUTABLEact() — via stripe
invoice.sendEXECUTABLEact() — via stripe
ledger.entry.postDECISION_ONLYguard() — decided by TrustRail, executed by your code
repository.mergeEXECUTABLEact() — via git-sandbox, github
repository.pull_request.createEXECUTABLEact() — via github
repository.pushEXECUTABLEact() — via git-sandbox, github
repository.push.forceEXECUTABLEact() — via git-sandbox, github
repository.branch.deleteEXECUTABLEact() — via git-sandbox, github
repository.tag.createEXECUTABLEact() — via git-sandbox, github
repository.release.publishEXECUTABLEact() — via github
ci.workflow.dispatchEXECUTABLEact() — via github
package.publishDECISION_ONLYguard() — decided by TrustRail, executed by your code
deployment.createEXECUTABLEact() — via github
secret.readEXECUTABLEact() — via openbao
communications.slack.message.postEXECUTABLEact() — via slack
calendar.event.createEXECUTABLEact() — via google
calendar.event.updateEXECUTABLEact() — via google
calendar.event.deleteEXECUTABLEact() — via google
communications.slack.channel.inviteEXECUTABLEact() — via slack
communications.slack.channel.archiveEXECUTABLEact() — via slack
storage.object.deleteEXECUTABLEact() — via object-storage
network.http.requestEXECUTABLEact() — via http
kubernetes.workload.scaleEXECUTABLEact() — via kubernetes
kubernetes.workload.restartEXECUTABLEact() — via kubernetes
kubernetes.workload.image.setEXECUTABLEact() — via kubernetes
database.statement.executeDECISION_ONLYguard() — decided by TrustRail, executed by your code
cloud.compute.instance.startEXECUTABLEact() — via aws
cloud.compute.instance.stopEXECUTABLEact() — via aws
cloud.compute.instance.terminateEXECUTABLEact() — via aws
tool.callDECISION_ONLYguard() — decided by TrustRail, executed by your code
mcp.tool.callEXECUTABLEact() — via mcp-upstream

Submitting an unregistered action type, or a type paired with a provider the registry does not list for it, is rejected at evaluation. The catalogue is the contract.

Framework adapters

The adapters match each framework's shape structurally and import nothing, so there are no peer dependencies and nothing to keep in step across major releases. Their real job is failure shape: most frameworks treat a thrown error as a run failure instead of feeding it back to the model, so these convert a governance outcome into a tool result.

  • Vercel AI SDK — governedAiTool(agent, spec, tool)
  • LangChain / LangGraph — governedLangChainTool(...), or the @agent.guard(...) decorator in Python
  • OpenAI / Anthropic tool loops — governedHandler(agent, spec, handler) per dispatch entry, plus describeGoverned() so the model knows the tool is gated before it calls
  • MCP-capable agents — one configuration entry, no code
  • Claude Agent SDK — governedPreToolUse(agent, specs) in TypeScript, governed_pre_tool_use(agent, specs) in Python
  • Anything else — governedExecute for a structured refusal, governedTextExecute when tool output must be text

SDKs

  • JavaScript/TypeScript — @trustrail/sdk-js: the governed loop, the framework adapters, and a full control-plane and gateway client generated from the OpenAPI contract. Browser and Node.
  • Python — trustrail-sdk (imported as trustrail): dependency-free, with the same two modes. Endpoints must be HTTPS, except plain HTTP on loopback so local development works out of the box.
  • Go — not yet installable: the module is not published, so there is no go get path today. It is standard library only, with Act for the gateway path and Guard for gating your own code.

Coding agents

A hook that asks TrustRail before a coding agent pushes, merges or publishes is not yet available outside TrustRail. Until it is, govern a coding agent through the MCP server above, or wrap the tools it calls with an SDK.

API contract

TrustRail's OpenAPI 3.1 contract is the source of truth, covering identity, agents, actions, approvals, policies, integrations, executions, delegation, MCP, the agent network, federation, and operations. Generated SDK code is never hand-edited, and the contract check rejects any mutating operation that lacks an idempotency key.

Plans & billing

Decisions are free, and a plan charges for enforced actions, never for safety features. Every workspace starts on the Free tier (3 governed agents), which stops at its included actions; a paid plan includes some and bills each further one. An organization owner upgrades from the console's Billing view, which opens a hosted Stripe Checkout page — TrustRail itself never sees card details. Payment failures keep paid entitlements while Stripe retries; entitlements drop to the free tier only when collection stops.

Self-hosting? Set STRIPE_SECRET_KEY, STRIPE_PUBLISHABLE_KEY, and STRIPE_BILLING_WEBHOOK_SECRET on the API service, and point a Stripe webhook endpoint subscribed to customer.subscription.* events at POST /api/v1/billing/stripe/webhook. Developer mode enforces test-mode keys, so no real card is ever charged from a development deployment.

How TrustRail is verified

Inside TrustRail, one command runs every layer of verification on every change:

pnpm verify
# quality gate -> migrations -> four clean-database rehearsals
# -> four-process smoke test -> artifact digest verification

The rehearsals create throwaway databases, replay the full migration history, and exercise raced approvals, kill switches mid-flight, webhook quarantine, delegation escalation attempts, and cross-tenant probes — proving the invariants, not asserting them.