Skip to content

SDKs

Quickstart: from key to first governed tool call

Five minutes, in TypeScript or Python. By the end, your agent asks Immiscible before a tool runs, waits for a person when one is asked, runs the tool only if allowed, records what happened, and hands over a receipt anyone can check.

Both SDKs have no runtime dependencies. Both fail closed: if Immiscible cannot be reached, the tool does not run.

#1. Get an agent key (one minute)

The quickest way is one command in your project: npx immiscible init signs you in, creates the agent and its rule, and writes both variables below to .env. See the CLI guide. To do it by hand, in the console:

  1. Agents, then Add an agent. Pick what it does (for this guide, Something else), its limits, and who approves; give it a name you will recognise on your phone.
  2. Open the agent’s setup page and choose Collect the agent’s key. The agent key (ask_...) is shown once. It can ask for permission and nothing else.
  3. Agents, then Agent limits, then Add a rule, from a template. For this guide take Coding agent (tool calls; anything that reaches the network may only go to the domains you name) and Groceries (payments at tesco.com, sainsburys.co.uk, ocado.com and three more; asks you above £80).

A new agent starts at the workspace’s starting tier: intern unless an owner has chosen junior in the console. An intern has a person sign off every payment, data release and outside action until it has earned more autonomy, so expect its first calls to ask for approval; that is the system working.

Shell
export IMMISCIBLE_URL=https://immiscible.fly.dev     # the hosted service; your own server if you run one
export IMMISCIBLE_AGENT_KEY=ask_...

No server yet? Every example below runs against a built-in fake with --demo.

#2. Install (thirty seconds)

TypeScript (@immiscible/sdk on npm: Node 18+, Deno, Bun, browsers and edge; ESM and CommonJS):

Shell
npm install @immiscible/sdk

Python (immiscible on PyPI: Python 3.9+, standard library only), in a virtual environment:

Shell
python3 -m venv .venv && . .venv/bin/activate
pip install immiscible

#3. Ask before a tool runs (two minutes)

A run is one task: one trace and one session shared by everything the agent does for it. guard asks, waits for a person if asked, runs your function only if allowed, then settles.

TypeScript

TypeScript
import { Immiscible, toolAction } from '@immiscible/sdk';

const immiscible = new Immiscible();          // IMMISCIBLE_AGENT_KEY and IMMISCIBLE_URL
const run = immiscible.run();                 // one trace, one session, per task

const page = await run.guard(
  toolAction('fetch_page', { url: 'https://github.com/acme/api' }),   // a tool.call action
  async () => fetchPage('https://github.com/acme/api'),               // runs only if allowed
);

Python

Python
from immiscible import Immiscible, tool_action

immiscible = Immiscible()                     # IMMISCIBLE_AGENT_KEY and IMMISCIBLE_URL
run = immiscible.run()                        # one trace, one session, per task

with run.guard(tool_action("fetch_page", {"url": "https://github.com/acme/api"})) as decision:
    page = fetch_page("https://github.com/acme/api")                  # runs only if allowed

If the answer is no, the TypeScript call throws ImmiscibleDeniedError and Python raises it; err.reasons says why in plain English and err.signals lists the risk signals. Your code never ran.

#4. When a person must approve (one minute)

Above a mandate’s threshold, the answer is approval_required and a person is asked on their phone. guard waits, backing off between polls (500 ms growing to 8 s, with jitter), for up to ten minutes by default.

TypeScript

TypeScript
const order = await run.pay(
  { amount: 9500, currency: 'GBP', merchant: 'ocado.com', provenance: [{ source: 'user', detail: 'weekly shop' }] },
  (decision) => checkout(cart, decision.receipt),
  { onApprovalRequired: (d) => notify(`Approve at ${d.approval.url}`), timeoutMs: 5 * 60_000 },
);

Python

Python
payment = Immiscible.payment_action(9500, "GBP", "ocado.com", provenance=[{"source": "user", "detail": "weekly shop"}])
with run.guard(payment, on_approval_required=lambda d: notify(f"Approve at {d.approval_url}"), timeout=300) as decision:
    checkout(cart, decision.receipt)

Amounts are in minor units: 9500 is £95.00. To wait yourself, call authorize, then waitForDecision(id, { signal }) / wait_for_decision(id, cancel=event); an abort signal or a cancel event stops the wait without touching the approval.

#5. Check the receipt (thirty seconds)

An allowed action carries a receipt: a compact JWS signed with Ed25519. Whoever receives the order checks it, offline against keys you pinned, and once online for single use.

TypeScript

TypeScript
import { fetchJwks, verifyReceipt } from '@immiscible/sdk';

const jwks = await fetchJwks(process.env.IMMISCIBLE_URL);     // once; store it with your config
const r = await verifyReceipt(receipt, { jwks, issuer: process.env.IMMISCIBLE_URL, online: true,
  expect: { amount: 9500, currency: 'GBP', merchant: 'ocado.com' } });
if (!r.valid) refuse(r.message);                               // r.reason: expired, bad_signature, replayed, ...

Python

Python
from immiscible import fetch_jwks, verify_receipt

jwks = fetch_jwks(url)                                         # once; store it with your config
r = verify_receipt(receipt, url, jwks=jwks, online=True, expect={"amount": 9500, "currency": "GBP", "merchant": "ocado.com"})
if not r.valid:
    refuse(r.message)

#6. Run it

The demo ships with both SDKs from version 0.1.1, so it runs from an install, against a built-in fake:

Shell
npx -p @immiscible/sdk immiscible-demo
python3 -m immiscible.demo        # with the virtual environment active

Add --live to run the same steps against your own workspace (IMMISCIBLE_URL and IMMISCIBLE_AGENT_KEY). In a clone of the repository, the same code is packages/immiscible-js/examples/quickstart.mjs and packages/immiscible-py/examples/quickstart.py.

Both print an allowed tool call, a payment a person approved, a valid receipt, and a prompt-injected lookalike (0cado.com) refused before any code ran.

#What the SDK does for you

IdempotencyEvery authorize carries an idempotency key, in the body and the Idempotency-Key header, reused on every retry. A retry never authorises twice. Framework integrations key on the model’s tool call id.
RetriesNetwork errors, 429 and 5xx are retried twice with backoff (honouring Retry-After). A settle retried after a lost answer returns the settled action, not a 409.
TraceEvery request carries a W3C traceparent: the run’s trace id, a fresh span. Pass traceparent to run() to continue your own OpenTelemetry trace. The server’s span comes back in run.context.lastServerTraceparent.
SessionModel calls through the gateway and action requests share one session, so the gate can compare what the agent declares with what entered the model’s context. Leave the id out and the gateway issues one (x-immiscible-session, bound to your key).
Fail closedNo answer means no action. Do not catch the error and carry on.

#Next