Skip to content

First Integration

This page walks through integrating an application with Caracal using the TypeScript, Python, or Go SDK. By the end you will have an agent that opens a session, obtains a mandate, and calls a resource through the Gateway.

The instructions assume you have a running stack and a caracal.toml from the Quickstart.

The SDK wraps the token exchange and agent lifecycle behind three primitives:

  • spawn() opens an agent session with the Coordinator, obtains an ambient mandate from the STS, and runs your code inside that session context.
  • headers() returns the HTTP headers that propagate the agent’s identity to downstream services (Authorization, traceparent, baggage).
  • transport() wraps fetch (TS/Python) or *http.Client (Go) to automatically inject those headers on every outbound request.

When spawn() exits — normally or on error — the session is terminated in the Coordinator and the ambient mandate can no longer be used.

All three SDKs read configuration from the same four environment variables:

VariableSourceDescription
CARACAL_COORDINATOR_URLStackURL of the Coordinator service
CARACAL_ZONE_IDcaracal.tomlZone the application belongs to
CARACAL_APPLICATION_IDcaracal.tomlApplication ID in that zone
CARACAL_SUBJECT_TOKENObtained at runtimeAmbient mandate for this agent

The subject token is not a static secret — it is an ambient mandate obtained by exchanging the application’s client credentials with the STS. The SDK’s fromEnv() factory handles this transparently when used with caracal run.

Two additional optional variables:

VariableDescription
CARACAL_GATEWAY_URLURL of the Gateway (needed when routing tool calls through it)
CARACAL_RESOURCESComma-separated resourceId=https://upstream/prefix pairs

The easiest way to get valid env vars into a process is caracal run. It reads caracal.toml, exchanges credentials with the STS, and injects the resulting tokens as environment variables before launching your process:

Terminal window
caracal run -- node my-agent.js
caracal run -- python agent.py
caracal run -- ./my-go-agent

Inside the launched process, CARACAL_SUBJECT_TOKEN is set to a 60-minute ambient mandate and the resource tokens declared in caracal.toml are set under their configured env var names (e.g. RESOURCE_TOKEN).

Install the SDK:

Terminal window
npm install @caracalai/sdk

Minimal example — spawn an agent and call a resource through the Gateway:

import { Caracal, AgentKind } from "@caracalai/sdk";
const caracal = Caracal.fromEnv();
await caracal.spawn(async () => {
const ctx = caracal.current();
console.log("Agent session:", ctx?.agentSessionId);
// All fetch calls through transport() carry the mandate headers.
const client = caracal.transport();
const response = await client("https://gateway.example.com/my-resource/endpoint", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ action: "summarize" }),
});
const result = await response.json();
console.log(result);
}, { kind: AgentKind.Ephemeral });

Caracal.fromEnv() throws if any of the four required env vars are missing. Wrap your process startup in caracal run to inject them automatically.

To protect an incoming HTTP server rather than an outgoing agent, use the Express connector:

Terminal window
npm install @caracalai/sdk @caracalai/mcp-express
import express from "express";
import { Caracal } from "@caracalai/sdk";
import { caracalMiddleware } from "@caracalai/mcp-express";
const app = express();
const caracal = Caracal.fromEnv();
app.use(caracalMiddleware(caracal));
app.post("/tool", (req, res) => {
const ctx = caracal.current();
// ctx.zoneId, ctx.agentSessionId, ctx.scope are available here
res.json({ ok: true });
});

The middleware extracts the agent’s mandate from the Authorization header and W3C baggage, verifies it, and binds the CaracalContext to the request.

Install the SDK:

Terminal window
pip install caracalai-sdk

Minimal example using async with spawn:

import asyncio
from caracalai_sdk import Caracal, AgentKind
caracal = Caracal.from_env()
async def main():
async with caracal.spawn(kind=AgentKind.EPHEMERAL) as ctx:
print("Agent session:", ctx.agent_session_id)
# transport() returns an httpx.AsyncClient pre-configured with mandate headers.
async with caracal.transport() as client:
response = await client.post(
"https://gateway.example.com/my-resource/endpoint",
json={"action": "summarize"},
)
print(response.json())
asyncio.run(main())

To protect a FastAPI or Starlette service:

Terminal window
pip install caracalai-sdk
from fastapi import FastAPI
from caracalai_sdk import Caracal
app = FastAPI()
caracal = Caracal.from_env()
app.add_middleware(caracal.middleware().__class__, caracal=caracal)
@app.post("/tool")
async def tool():
ctx = caracal.current()
# ctx.zone_id, ctx.agent_session_id, ctx.scope are available
return {"ok": True}

The ASGI middleware extracts the agent’s mandate from inbound request headers and binds the CaracalContext to the request scope.

Install the SDK:

Terminal window
go get github.com/garudex-labs/caracal/sdk

Minimal example:

package main
import (
"context"
"fmt"
"io"
"net/http"
caracal "github.com/garudex-labs/caracal/sdk"
)
func main() {
c, err := caracal.FromEnv()
if err != nil {
panic(err)
}
err = c.Spawn(context.Background(), func(ctx context.Context) error {
cc, _ := c.Current(ctx)
fmt.Println("Agent session:", cc.AgentSessionID)
// Transport injects mandate headers on every request.
client := c.Transport(nil)
resp, err := client.Post(
"https://gateway.example.com/my-resource/endpoint",
"application/json",
nil,
)
if err != nil {
return err
}
defer resp.Body.Close()
body, _ := io.ReadAll(resp.Body)
fmt.Println(string(body))
return nil
})
if err != nil {
panic(err)
}
}
Terminal window
go get github.com/garudex-labs/caracal/connectors/nethttp
import (
caracal "github.com/garudex-labs/caracal/sdk"
"github.com/garudex-labs/caracal/connectors/nethttp"
)
c, _ := caracal.FromEnv()
mux := http.NewServeMux()
mux.HandleFunc("/tool", func(w http.ResponseWriter, r *http.Request) {
ctx, _ := c.Current(r.Context())
fmt.Fprintln(w, "Agent session:", ctx.AgentSessionID)
})
http.ListenAndServe(":8090", c.Middleware(mux))

An agent can delegate a subset of its authority to a child agent. The parent creates a delegation edge with the Coordinator, and the child receives its own mandate scoped to whatever the edge permits.

// TypeScript example
await caracal.spawn(async () => {
// Delegate to a child agent session with narrowed scope.
await caracal.delegate(
{
to: childAgentSessionId,
toApplicationId: "app2",
scopes: ["resource://example:read"],
constraints: { ttlSeconds: 300 },
},
async () => {
// Within this block, the delegated context is active.
const child = caracal.current();
console.log("Delegation edge:", child?.delegationEdgeId);
}
);
});

Revoking the delegation edge from the CLI or admin SDK terminates the child session and all of its descendants immediately.

What happens if the policy denies the exchange

Section titled “What happens if the policy denies the exchange”

If the OPA policy denies the token exchange, the STS returns HTTP 403 and the SDK throws an error. No mandate is issued. The denial is recorded in the audit ledger with the determining policies and diagnostics that caused it. Use caracal audit tail or caracal explain to inspect the decision.

You now have a working integration. Read Key Ideas at a Glance for a concise reference of everything the system does, or jump into Concepts to understand the model in depth.