SDKs
Immiscible SDKs
Immiscible decides what your agents may spend, share and do before they do it, asks a person when it should, and signs a record of everything that crossed. The SDKs are the agent’s side of that: ask, wait, act, settle, prove.
Install with npm install @immiscible/sdk or pip install immiscible, then start with the quickstart. Each package is published at version 0.1.1, MIT licensed, with its source public at efr7-7/immiscible-sdks.
| Package | Where | Runtime | Runtime dependencies |
|---|---|---|---|
@immiscible/sdk | packages/immiscible-js | Node 18+, Deno, Bun, browsers, edge; ESM and CommonJS; typed | none |
immiscible | packages/immiscible-py | Python 3.9+; type hints | none (standard library, pure-Python Ed25519) |
@immiscible/claude-code-hook | packages/immiscible-claude-code | Node 18+ | none |
#Integrations
| Framework | Page | TypeScript | Python |
|---|---|---|---|
| OpenAI Agents SDK | openai-agents.md | guardOpenAITools, openaiToolGuardrail | guard_tools, guard_function_tool |
| LangChain and LangGraph | langchain.md | guardLangChainTools | guard_langchain_tools |
| Vercel AI SDK | vercel-ai.md | guardAiTools, immiscibleMiddleware, gateway.aiSdkOpenAI() | |
| Claude Code and the Claude Agent SDK | claude-code.md | @immiscible/claude-code-hook | |
| MCP clients (Claude Desktop, Cursor, VS Code, Claude Code) | mcp.md | configuration only | configuration only |
| Any function a framework turns into a tool | @guarded(...) |
#The surface, side by side
| TypeScript | Python | |
|---|---|---|
| client | new Immiscible({ apiKey, baseUrl }) | Immiscible(api_key, base_url) |
| a run (trace and session) | immiscible.run({ sessionId, traceparent, client }) | immiscible.run(session_id, traceparent=..., client=...) |
| ask | authorize(action, { idempotencyKey, signal }) | authorize(action, idempotency_key=...) |
| wait | waitForDecision(id, { timeoutMs, initialDelayMs, maxDelayMs, signal }) | wait_for_decision(id, timeout=, initial_delay=, max_delay=, cancel=) |
| ask and wait, or refuse | decide(action, opts) | decide(action, ...) |
| report | settle(id, { status, amount }) | settle(id, status, amount) |
| all of it | guard(action, fn, opts) | with guard(action) as d: or @guard(mapper) |
| actions | Immiscible.paymentAction, Immiscible.dataAction, toolAction | Immiscible.payment_action, Immiscible.data_action, tool_action |
| verify | verifyReceipt(token, { issuer, jwks, online, expect }) | verify_receipt(token, issuer, jwks=, online=, expect=) |
| pin keys | fetchJwks(issuer), pinJwks(jwks) | fetch_jwks(issuer), pin_jwks(jwks) |
| gateway | gateway.openai(), gateway.anthropic(), gateway.aiSdkOpenAI(), gateway.env() | gateway.openai(), gateway.openai_client(), gateway.anthropic_client(), gateway.env() |
| MCP proxy | mcpProxy(id).callWithApproval(tool, args) | mcp_proxy(id).call_with_approval(tool, args) |
| errors | ImmiscibleError, ImmiscibleDeniedError, ImmiscibleApprovalRequiredError, ImmiscibleApprovalTimeoutError | same names |
| test double | startFakeImmiscible() from @immiscible/sdk/testing | start_fake() from immiscible.testing |
Environment, every package: IMMISCIBLE_AGENT_KEY and IMMISCIBLE_URL (the older ASSAY_AGENT_KEY and ASSAY_URL are read too).
#Runs, traces and sessions
A run is one task. Everything the agent does for it shares:
- a W3C trace. Every request carries
traceparent: the run’s trace id and a fresh span id. Continue your own trace by passing the activetraceparenttorun(). The server answers with its own span in the same trace, so the gate’s decision and the gateway’s record land in your trace view, and the evidence ledger carries the trace id. - a session. The gateway records what entered the model’s context: a web page, an email, a third-party tool’s output. When the agent later asks to act and declares its provenance, the gate compares the two. A web page in the context and a request that claims to be purely the person’s idea raises
provenance_mismatch, and a person decides.
Sessions come in two kinds:
| How | Header | |
|---|---|---|
| issued (default) | leave the id out; the first gateway response carries one, and the run adopts it | x-immiscible-session: imss_... |
| chosen | run({ sessionId: 'my-run-42' }), or a Claude Code session_id | x-immiscible-client-session: my-run-42 |
An issued id is bound to your key: presented by any other key, the gateway refuses it. A chosen id lives inside your key owner’s namespace, so quoting someone else’s string joins nothing of theirs. Use one run per task: a web page read for one task should not make the next one look tainted.
#The gateway
The gateway speaks OpenAI Chat Completions at <base>/v1/chat/completions and Anthropic Messages at <base>/anthropic/v1/messages. The helpers return the options each SDK takes, including a fetch (TypeScript) or httpx event hooks (Python) that carry the run’s trace and session on every call.
import OpenAI from 'openai';
import Anthropic from '@anthropic-ai/sdk';
const run = new Immiscible().run();
const openai = new OpenAI(run.gateway.openai({ taskId: 'weekly-shop' }));
const anthropic = new Anthropic(run.gateway.anthropic());run = Immiscible().run()
openai = run.gateway.openai_client(task_id="weekly-shop") # needs openai installed
anthropic = run.gateway.anthropic_client() # needs anthropic installed
# or, with your own client: OpenAI(**run.gateway.openai(), http_client=httpx.Client(event_hooks=run.context.httpx_event_hooks()))The key the gateway meters defaults to the agent key; pass apiKey / api_key to use a person’s inference key instead. gateway.env() gives the environment a child process needs (OPENAI_BASE_URL, ANTHROPIC_BASE_URL, keys, TRACEPARENT).
#Writing actions
tool.call is the default for every integration: “Run send_invoice: amount 12, customer Acme” as the summary (the tool’s name and its arguments in words, never raw JSON), the domain from a url argument as the target, and agent provenance. Write your own mapping for anything with money or personal data in it, so the gate applies money and data rules:
mapToAction: ({ name, args }) =>
name === 'search'
? null // read-only: no check
: Immiscible.paymentAction({
amount: args.pence,
currency: 'GBP',
merchant: args.domain,
provenance: [{ source: 'user' }],
})Be honest about provenance. Anything the agent read (a page, an email, a document, a tool’s output) belongs in it as web, email, document or tool. Missing provenance is treated as untrusted, which is the safe failure.
#Tests
cd packages/immiscible-js && npm run build && npm test # unit tests on a fake, CommonJS, and the real server
cd packages/immiscible-py && python3 -m unittest discover -s tests -t .
cd packages/immiscible-claude-code && npm testpackages/immiscible-js/test/integration.test.mjs boots the real server from src/server/app.js in-process. The framework tests (test/frameworks.test.mjs, tests/test_frameworks.py) run against the real framework packages when they are installed (IMMISCIBLE_FRAMEWORKS_DIR points the TypeScript ones at a folder where they are), and skip otherwise.