Skip to main content

immiscible

Govern what your AI agents spend, share and do, from Python. Before a tool runs, the agent asks Immiscible; Immiscible checks the mandates a person wrote, asks that person when it should, and hands back a signed receipt. Spend control, approvals and TokenOps for the OpenAI Agents SDK, LangChain, LangGraph and MCP. Standard library only: urllib, json, hashlib and a pure-Python Ed25519 verifier. Python 3.9+. Type hints throughout (py.typed). Fails closed.

pip install immiscible
from immiscible import Immiscible, tool_action

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

with run.guard(tool_action("deploy", {"service": "api"}, domain="mycompany.com")):
    deploy()

guard asks, waits for a person if one is asked, runs your block only if allowed, and settles completed (or failed if it raised). A refusal raises ImmiscibleDeniedError with plain-English reasons; your block never ran. As a decorator it takes a function that maps the call to an action, and works on async def too.

Set up a project in one command with npx immiscible init (it writes IMMISCIBLE_URL and IMMISCIBLE_AGENT_KEY to .env). Quickstart: immiscible.fly.dev/docs/quickstart. Answers to common questions: immiscible.fly.dev/docs/answers. SDKs: immiscible.fly.dev/docs/sdks. API: immiscible.fly.dev/docs/api. JavaScript: @immiscible/sdk on npm. MIT licence.

The client

Call Does
Immiscible(api_key, base_url, timeout=30, max_retries=2, session_id=, traceparent=) Defaults from IMMISCIBLE_AGENT_KEY and IMMISCIBLE_URL (or ASSAY_AGENT_KEY, ASSAY_URL), then https://immiscible.fly.dev, the hosted service (set IMMISCIBLE_URL for your own server).
immiscible.run(session_id=None, client="custom", traceparent=None) A client for a new run.
authorize(action, idempotency_key=None) Ask. Returns a Decision (a dict with .allowed, .receipt, .reasons, .approval_url, ...).
wait_for_decision(id, timeout=600, initial_delay=0.5, max_delay=8, factor=1.6, cancel=None, on_poll=None) Poll with backoff and jitter. cancel is a threading.Event.
decide(action, ...) Authorize and wait; returns an allow or raises a refusal.
settle(id, status="completed", amount=None) Record what happened.
guard(action_or_mapper, wait=True, on_approval_required=None, settle=True, settle_amount=None, ...) All of it: with block or decorator.
pay(...), request_data(...), Immiscible.payment_action(...), Immiscible.data_action(...), tool_action(...) Build and send actions.
outcome(task_id, status) TokenOps.
mcp_proxy(upstream_id) MCP proxy client: list_tools, call, call_with_approval, retry_after_approval.
gateway.openai(), gateway.anthropic(), gateway.openai_client(), gateway.anthropic_client(), gateway.env() Model SDKs through the gateway, inside the run.
context The run: trace_id, session_id, headers(), traceparent(), httpx_event_hooks(), last_server_traceparent.

Errors

Everything the SDK raises on purpose is an ImmiscibleError, with status, type, body and request_id (quote it when asking for help). HTTP failures use a subclass by status: ImmiscibleAuthenticationError (401), ImmiscibleInvalidRequestError (400 or 422, with errors per field), ImmiscibleRateLimitError (429, with retry_after in seconds), ImmiscibleIdempotencyConflictError (409, the same key with a different body) and ImmiscibleConnectionError (unreachable or timed out). Refusals are ImmiscibleDeniedError, ImmiscibleApprovalRequiredError and ImmiscibleApprovalTimeoutError.

Integrations

from immiscible.integrations import guard_tools, guard_langchain_tools, guarded
guard_tools(tools, client=, map_to_action=) OpenAI Agents SDK function tools
guard_langchain_tools(tools, client=, map_to_action=) LangChain tools, for ToolNode, bind_tools, create_react_agent
@guarded(map_to_action, client=) any function a framework turns into a tool; signature and docstring kept

Setups for each framework: immiscible.fly.dev/docs/sdks.

Receipts

from immiscible import fetch_jwks, verify_receipt

jwks = fetch_jwks("https://immiscible.example")                  # pin: fetch once, store with your config
r = verify_receipt(token, "https://immiscible.example", jwks=jwks, online=True,
                   expect={"amount": 6420, "currency": "GBP", "merchant": "ocado.com"})

The same checks as the TypeScript SDK, with a pure-Python Ed25519 verifier checked against RFC 8032 and against Node's signatures.

Testing

python3 -m unittest discover -s tests -t .

immiscible.testing.start_fake() is a stdlib fake server (real Ed25519 receipts, a fake gateway and model, the MCP proxy, a person's approvals) for your own tests. tests/test_frameworks.py runs against the real openai, openai-agents, langchain-core and langgraph when they are installed; tests/test_cross_language.py runs against the TypeScript SDK's fake when Node is available.

Crypto payments: decide, then sign

Immiscible never holds keys or signs. decide_then_sign asks first and calls your wallet only after an allow whose signed receipt covers the exact transfer; x402_request does the same for HTTP 402 (x402 v1 and v2) resources.

from immiscible import Immiscible
from immiscible.crypto import decide_then_sign, x402_request

decide_then_sign(Immiscible(), {"asset": "USDC", "network": "base", "amount": "12.50", "recipient": "0x..."},
                 lambda decision: {"txHash": wallet.send()})
status, headers, body = x402_request(Immiscible(), "https://api.example.com/report", pay=my_x402_signer)

Amounts are decimal strings, never floats. See crypto payments and x402.

Metadata

Release files for immiscible 0.1.1

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for immiscible 0.1.1
File Size Uploaded
immiscible-0.1.1.tar.gz 61.9 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for immiscible 0.1.1
File Interpreter ABI Platform
immiscible-0.1.1-py3-none-any.whl Python 3 none any Details

Total release size: 113.0 kB

Release files / immiscible-0.1.1.tar.gz

Download URL immiscible-0.1.1.tar.gz
Size 61.9 kB
Tags Source
SHA-256 checksum
How to use checksums
f8866d3571e5c70e1e454cb5a49c3fb697201dd8400e3ff48a7db4c79e148c9a
BLAKE2b-256 checksum
How to use checksums
252e5fcb5646d17b049bea8c6c10b5b9a4f389d648934e34a95acf04fac71861
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.13.5

Release files / immiscible-0.1.1-py3-none-any.whl

Download URL immiscible-0.1.1-py3-none-any.whl
Size 51.0 kB
Tags Python 3
SHA-256 checksum
How to use checksums
7c1982d514418ffa06e32c65f491c5947d35614a7d36e2c5fb318e0433646371
BLAKE2b-256 checksum
How to use checksums
5238e2d94bf61e1f90e7b6e2a0440f92180be780d5ef94bf27ca763a9edeaf48
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.13.5

Release history Release notifications | RSS feed

This release

0.1.1 This release

2 release files

0.1.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page