Skip to content

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.

PackageWhereRuntimeRuntime dependencies
@immiscible/sdkpackages/immiscible-jsNode 18+, Deno, Bun, browsers, edge; ESM and CommonJS; typednone
immisciblepackages/immiscible-pyPython 3.9+; type hintsnone (standard library, pure-Python Ed25519)
@immiscible/claude-code-hookpackages/immiscible-claude-codeNode 18+none

#Integrations

FrameworkPageTypeScriptPython
OpenAI Agents SDKopenai-agents.mdguardOpenAITools, openaiToolGuardrailguard_tools, guard_function_tool
LangChain and LangGraphlangchain.mdguardLangChainToolsguard_langchain_tools
Vercel AI SDKvercel-ai.mdguardAiTools, immiscibleMiddleware, gateway.aiSdkOpenAI()
Claude Code and the Claude Agent SDKclaude-code.md@immiscible/claude-code-hook
MCP clients (Claude Desktop, Cursor, VS Code, Claude Code)mcp.mdconfiguration onlyconfiguration only
Any function a framework turns into a tool@guarded(...)

#The surface, side by side

TypeScriptPython
clientnew Immiscible({ apiKey, baseUrl })Immiscible(api_key, base_url)
a run (trace and session)immiscible.run({ sessionId, traceparent, client })immiscible.run(session_id, traceparent=..., client=...)
askauthorize(action, { idempotencyKey, signal })authorize(action, idempotency_key=...)
waitwaitForDecision(id, { timeoutMs, initialDelayMs, maxDelayMs, signal })wait_for_decision(id, timeout=, initial_delay=, max_delay=, cancel=)
ask and wait, or refusedecide(action, opts)decide(action, ...)
reportsettle(id, { status, amount })settle(id, status, amount)
all of itguard(action, fn, opts)with guard(action) as d: or @guard(mapper)
actionsImmiscible.paymentAction, Immiscible.dataAction, toolActionImmiscible.payment_action, Immiscible.data_action, tool_action
verifyverifyReceipt(token, { issuer, jwks, online, expect })verify_receipt(token, issuer, jwks=, online=, expect=)
pin keysfetchJwks(issuer), pinJwks(jwks)fetch_jwks(issuer), pin_jwks(jwks)
gatewaygateway.openai(), gateway.anthropic(), gateway.aiSdkOpenAI(), gateway.env()gateway.openai(), gateway.openai_client(), gateway.anthropic_client(), gateway.env()
MCP proxymcpProxy(id).callWithApproval(tool, args)mcp_proxy(id).call_with_approval(tool, args)
errorsImmiscibleError, ImmiscibleDeniedError, ImmiscibleApprovalRequiredError, ImmiscibleApprovalTimeoutErrorsame names
test doublestartFakeImmiscible() from @immiscible/sdk/testingstart_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 active traceparent to run(). 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:

HowHeader
issued (default)leave the id out; the first gateway response carries one, and the run adopts itx-immiscible-session: imss_...
chosenrun({ sessionId: 'my-run-42' }), or a Claude Code session_idx-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.

TypeScript
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());
Python
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:

TypeScript
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

Shell
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 test

packages/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.