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.
Install
npm install @trustrail/sdk-js
# Python: pip install trustrailA new workspace’s Overview walks you through the two things an agent needs and then prints this block with every value filled in — no placeholders to go and look up. Registering an agent returns its tr_agent_… credential; adding a connection gives you the last two lines.
TRUSTRAIL_URL=http://127.0.0.1:3001 TRUSTRAIL_GATEWAY_URL=http://127.0.0.1:3003 TRUSTRAIL_AGENT_CREDENTIAL=tr_agent_... TRUSTRAIL_ENVIRONMENT_ID=... TRUSTRAIL_CONNECTION_ID=... TRUSTRAIL_PROVIDER=smtp-test
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,
);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.
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.
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 type | Status | How to govern it |
|---|---|---|
payments.purchase.create | EXECUTABLE | act() — via payments-sandbox or stripe |
communications.email.send | EXECUTABLE | act() — via smtp-test |
repository.merge | DECISION_ONLY | guard() — no git adapter yet |
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, plusdescribeGoverned()so the model knows the tool is gated before it calls - MCP-capable agents — one configuration entry, no code
- Anything else —
governedExecutefor a structured refusal,governedTextExecutewhen 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: dependency-free, with the same two modes. Endpoints must be HTTPS, except plain HTTP on loopback so local development works out of the box.
Run the platform locally
Prerequisites: Docker and Node.js 22.23.1 with Corepack enabled.
git clone <your-trustrail-checkout> cd trustrail ./scripts/bootstrap.sh
Bootstrap installs the pinned workspace, starts digest-pinned infrastructure, applies forward-only migrations, regenerates the API contract, runs the full quality gate, and starts all four processes: the web dashboard on :3000, the control API on :3001, the worker on :3002, and the execution gateway on :3003.
API contract
The OpenAPI 3.1 document under packages/contracts 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
Plans meter fleet size and action volume, never safety features. Every workspace starts on the Free tier (3 governed agents); 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.
Verifying the platform
One command runs every layer of verification:
pnpm verify
# quality gate -> migrations -> four clean-database rehearsals
# -> four-process smoke test -> artifact digest verificationThe 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.