immiscible 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (35) hide show
  1. immiscible-0.1.0/LICENSE +21 -0
  2. immiscible-0.1.0/PKG-INFO +124 -0
  3. immiscible-0.1.0/README.md +89 -0
  4. immiscible-0.1.0/immiscible/__init__.py +36 -0
  5. immiscible-0.1.0/immiscible/client.py +507 -0
  6. immiscible-0.1.0/immiscible/crypto.py +235 -0
  7. immiscible-0.1.0/immiscible/ed25519.py +119 -0
  8. immiscible-0.1.0/immiscible/errors.py +70 -0
  9. immiscible-0.1.0/immiscible/gateway.py +89 -0
  10. immiscible-0.1.0/immiscible/integrations/__init__.py +10 -0
  11. immiscible-0.1.0/immiscible/integrations/core.py +193 -0
  12. immiscible-0.1.0/immiscible/integrations/langchain.py +92 -0
  13. immiscible-0.1.0/immiscible/integrations/openai_agents.py +51 -0
  14. immiscible-0.1.0/immiscible/proxy.py +195 -0
  15. immiscible-0.1.0/immiscible/py.typed +0 -0
  16. immiscible-0.1.0/immiscible/testing.py +570 -0
  17. immiscible-0.1.0/immiscible/trace.py +182 -0
  18. immiscible-0.1.0/immiscible/verify.py +347 -0
  19. immiscible-0.1.0/immiscible.egg-info/PKG-INFO +124 -0
  20. immiscible-0.1.0/immiscible.egg-info/SOURCES.txt +33 -0
  21. immiscible-0.1.0/immiscible.egg-info/dependency_links.txt +1 -0
  22. immiscible-0.1.0/immiscible.egg-info/requires.txt +7 -0
  23. immiscible-0.1.0/immiscible.egg-info/top_level.txt +1 -0
  24. immiscible-0.1.0/pyproject.toml +49 -0
  25. immiscible-0.1.0/setup.cfg +4 -0
  26. immiscible-0.1.0/tests/test_client.py +207 -0
  27. immiscible-0.1.0/tests/test_cross_language.py +58 -0
  28. immiscible-0.1.0/tests/test_crypto.py +141 -0
  29. immiscible-0.1.0/tests/test_ed25519.py +94 -0
  30. immiscible-0.1.0/tests/test_frameworks.py +138 -0
  31. immiscible-0.1.0/tests/test_integrations.py +155 -0
  32. immiscible-0.1.0/tests/test_proxy.py +55 -0
  33. immiscible-0.1.0/tests/test_style.py +22 -0
  34. immiscible-0.1.0/tests/test_trace.py +120 -0
  35. immiscible-0.1.0/tests/test_verify.py +102 -0
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Eóin Forker
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,124 @@
1
+ Metadata-Version: 2.4
2
+ Name: immiscible
3
+ Version: 0.1.0
4
+ Summary: Govern what AI agents spend, share and do: authorize tool calls, wait for a person, settle, and verify signed receipts. Standard library only.
5
+ Author: Eóin Forker
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://immiscible.fly.dev
8
+ Project-URL: Documentation, https://immiscible.fly.dev/docs/sdks
9
+ Project-URL: Source, https://github.com/efr7-7/immiscible-sdks/tree/main/packages/immiscible-py
10
+ Project-URL: Issues, https://github.com/efr7-7/immiscible-sdks/issues
11
+ Keywords: ai agents,agent governance,spend control,finops,tokenops,mcp,claude code,ai,agents,guardrails,authorization,approvals,receipts,openai-agents,langchain,langgraph
12
+ Classifier: Development Status :: 3 - Alpha
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3 :: Only
16
+ Classifier: Programming Language :: Python :: 3.9
17
+ Classifier: Programming Language :: Python :: 3.10
18
+ Classifier: Programming Language :: Python :: 3.11
19
+ Classifier: Programming Language :: Python :: 3.12
20
+ Classifier: Programming Language :: Python :: 3.13
21
+ Classifier: Operating System :: OS Independent
22
+ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
23
+ Classifier: Topic :: Security
24
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
25
+ Classifier: Typing :: Typed
26
+ Requires-Python: >=3.9
27
+ Description-Content-Type: text/markdown
28
+ License-File: LICENSE
29
+ Provides-Extra: openai-agents
30
+ Requires-Dist: openai-agents>=0.2; extra == "openai-agents"
31
+ Provides-Extra: langchain
32
+ Requires-Dist: langchain-core>=0.3; extra == "langchain"
33
+ Requires-Dist: langgraph>=0.2; extra == "langchain"
34
+ Dynamic: license-file
35
+
36
+ # immiscible
37
+
38
+ 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.
39
+ Spend control, approvals and TokenOps for the OpenAI Agents SDK, LangChain, LangGraph and MCP.
40
+ Standard library only: urllib, json, hashlib and a pure-Python Ed25519 verifier. Python 3.9+. Type hints throughout (`py.typed`). Fails closed.
41
+
42
+ ```shell
43
+ pip install immiscible
44
+ ```
45
+
46
+ ```python
47
+ from immiscible import Immiscible, tool_action
48
+
49
+ immiscible = Immiscible() # IMMISCIBLE_AGENT_KEY, IMMISCIBLE_URL
50
+ run = immiscible.run() # one trace and one session per task
51
+
52
+ with run.guard(tool_action("deploy", {"service": "api"}, domain="mycompany.com")):
53
+ deploy()
54
+ ```
55
+
56
+ `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.
57
+
58
+ Quickstart: [immiscible.fly.dev/docs/quickstart](https://immiscible.fly.dev/docs/quickstart). SDKs: [immiscible.fly.dev/docs/sdks](https://immiscible.fly.dev/docs/sdks). API: [immiscible.fly.dev/docs/api](https://immiscible.fly.dev/docs/api). JavaScript: [`@immiscible/sdk` on npm](https://www.npmjs.com/package/@immiscible/sdk). MIT licence.
59
+
60
+ ## The client
61
+
62
+ | Call | Does |
63
+ |---|---|
64
+ | `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`). |
65
+ | `immiscible.run(session_id=None, client="custom", traceparent=None)` | A client for a new run. |
66
+ | `authorize(action, idempotency_key=None)` | Ask. Returns a `Decision` (a dict with `.allowed`, `.receipt`, `.reasons`, `.approval_url`, ...). |
67
+ | `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`. |
68
+ | `decide(action, ...)` | Authorize and wait; returns an allow or raises a refusal. |
69
+ | `settle(id, status="completed", amount=None)` | Record what happened. |
70
+ | `guard(action_or_mapper, wait=True, on_approval_required=None, settle=True, settle_amount=None, ...)` | All of it: `with` block or decorator. |
71
+ | `pay(...)`, `request_data(...)`, `Immiscible.payment_action(...)`, `Immiscible.data_action(...)`, `tool_action(...)` | Build and send actions. |
72
+ | `outcome(task_id, status)` | TokenOps. |
73
+ | `mcp_proxy(upstream_id)` | MCP proxy client: `list_tools`, `call`, `call_with_approval`, `retry_after_approval`. |
74
+ | `gateway.openai()`, `gateway.anthropic()`, `gateway.openai_client()`, `gateway.anthropic_client()`, `gateway.env()` | Model SDKs through the gateway, inside the run. |
75
+ | `context` | The run: `trace_id`, `session_id`, `headers()`, `traceparent()`, `httpx_event_hooks()`, `last_server_traceparent`. |
76
+
77
+ ## Integrations
78
+
79
+ ```python
80
+ from immiscible.integrations import guard_tools, guard_langchain_tools, guarded
81
+ ```
82
+
83
+ | | |
84
+ |---|---|
85
+ | `guard_tools(tools, client=, map_to_action=)` | OpenAI Agents SDK function tools |
86
+ | `guard_langchain_tools(tools, client=, map_to_action=)` | LangChain tools, for `ToolNode`, `bind_tools`, `create_react_agent` |
87
+ | `@guarded(map_to_action, client=)` | any function a framework turns into a tool; signature and docstring kept |
88
+
89
+ Setups for each framework: [immiscible.fly.dev/docs/sdks](https://immiscible.fly.dev/docs/sdks).
90
+
91
+ ## Receipts
92
+
93
+ ```python
94
+ from immiscible import fetch_jwks, verify_receipt
95
+
96
+ jwks = fetch_jwks("https://immiscible.example") # pin: fetch once, store with your config
97
+ r = verify_receipt(token, "https://immiscible.example", jwks=jwks, online=True,
98
+ expect={"amount": 6420, "currency": "GBP", "merchant": "ocado.com"})
99
+ ```
100
+
101
+ The same checks as the TypeScript SDK, with a pure-Python Ed25519 verifier checked against RFC 8032 and against Node's signatures.
102
+
103
+ ## Testing
104
+
105
+ ```shell
106
+ python3 -m unittest discover -s tests -t .
107
+ ```
108
+
109
+ `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.
110
+
111
+ ## Crypto payments: decide, then sign
112
+
113
+ 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.
114
+
115
+ ```python
116
+ from immiscible import Immiscible
117
+ from immiscible.crypto import decide_then_sign, x402_request
118
+
119
+ decide_then_sign(Immiscible(), {"asset": "USDC", "network": "base", "amount": "12.50", "recipient": "0x..."},
120
+ lambda decision: {"txHash": wallet.send()})
121
+ status, headers, body = x402_request(Immiscible(), "https://api.example.com/report", pay=my_x402_signer)
122
+ ```
123
+
124
+ Amounts are decimal strings, never floats. See [crypto payments](https://immiscible.fly.dev/docs/guides/crypto-payments) and [x402](https://immiscible.fly.dev/docs/guides/x402).
@@ -0,0 +1,89 @@
1
+ # immiscible
2
+
3
+ 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.
4
+ Spend control, approvals and TokenOps for the OpenAI Agents SDK, LangChain, LangGraph and MCP.
5
+ Standard library only: urllib, json, hashlib and a pure-Python Ed25519 verifier. Python 3.9+. Type hints throughout (`py.typed`). Fails closed.
6
+
7
+ ```shell
8
+ pip install immiscible
9
+ ```
10
+
11
+ ```python
12
+ from immiscible import Immiscible, tool_action
13
+
14
+ immiscible = Immiscible() # IMMISCIBLE_AGENT_KEY, IMMISCIBLE_URL
15
+ run = immiscible.run() # one trace and one session per task
16
+
17
+ with run.guard(tool_action("deploy", {"service": "api"}, domain="mycompany.com")):
18
+ deploy()
19
+ ```
20
+
21
+ `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.
22
+
23
+ Quickstart: [immiscible.fly.dev/docs/quickstart](https://immiscible.fly.dev/docs/quickstart). SDKs: [immiscible.fly.dev/docs/sdks](https://immiscible.fly.dev/docs/sdks). API: [immiscible.fly.dev/docs/api](https://immiscible.fly.dev/docs/api). JavaScript: [`@immiscible/sdk` on npm](https://www.npmjs.com/package/@immiscible/sdk). MIT licence.
24
+
25
+ ## The client
26
+
27
+ | Call | Does |
28
+ |---|---|
29
+ | `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`). |
30
+ | `immiscible.run(session_id=None, client="custom", traceparent=None)` | A client for a new run. |
31
+ | `authorize(action, idempotency_key=None)` | Ask. Returns a `Decision` (a dict with `.allowed`, `.receipt`, `.reasons`, `.approval_url`, ...). |
32
+ | `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`. |
33
+ | `decide(action, ...)` | Authorize and wait; returns an allow or raises a refusal. |
34
+ | `settle(id, status="completed", amount=None)` | Record what happened. |
35
+ | `guard(action_or_mapper, wait=True, on_approval_required=None, settle=True, settle_amount=None, ...)` | All of it: `with` block or decorator. |
36
+ | `pay(...)`, `request_data(...)`, `Immiscible.payment_action(...)`, `Immiscible.data_action(...)`, `tool_action(...)` | Build and send actions. |
37
+ | `outcome(task_id, status)` | TokenOps. |
38
+ | `mcp_proxy(upstream_id)` | MCP proxy client: `list_tools`, `call`, `call_with_approval`, `retry_after_approval`. |
39
+ | `gateway.openai()`, `gateway.anthropic()`, `gateway.openai_client()`, `gateway.anthropic_client()`, `gateway.env()` | Model SDKs through the gateway, inside the run. |
40
+ | `context` | The run: `trace_id`, `session_id`, `headers()`, `traceparent()`, `httpx_event_hooks()`, `last_server_traceparent`. |
41
+
42
+ ## Integrations
43
+
44
+ ```python
45
+ from immiscible.integrations import guard_tools, guard_langchain_tools, guarded
46
+ ```
47
+
48
+ | | |
49
+ |---|---|
50
+ | `guard_tools(tools, client=, map_to_action=)` | OpenAI Agents SDK function tools |
51
+ | `guard_langchain_tools(tools, client=, map_to_action=)` | LangChain tools, for `ToolNode`, `bind_tools`, `create_react_agent` |
52
+ | `@guarded(map_to_action, client=)` | any function a framework turns into a tool; signature and docstring kept |
53
+
54
+ Setups for each framework: [immiscible.fly.dev/docs/sdks](https://immiscible.fly.dev/docs/sdks).
55
+
56
+ ## Receipts
57
+
58
+ ```python
59
+ from immiscible import fetch_jwks, verify_receipt
60
+
61
+ jwks = fetch_jwks("https://immiscible.example") # pin: fetch once, store with your config
62
+ r = verify_receipt(token, "https://immiscible.example", jwks=jwks, online=True,
63
+ expect={"amount": 6420, "currency": "GBP", "merchant": "ocado.com"})
64
+ ```
65
+
66
+ The same checks as the TypeScript SDK, with a pure-Python Ed25519 verifier checked against RFC 8032 and against Node's signatures.
67
+
68
+ ## Testing
69
+
70
+ ```shell
71
+ python3 -m unittest discover -s tests -t .
72
+ ```
73
+
74
+ `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.
75
+
76
+ ## Crypto payments: decide, then sign
77
+
78
+ 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.
79
+
80
+ ```python
81
+ from immiscible import Immiscible
82
+ from immiscible.crypto import decide_then_sign, x402_request
83
+
84
+ decide_then_sign(Immiscible(), {"asset": "USDC", "network": "base", "amount": "12.50", "recipient": "0x..."},
85
+ lambda decision: {"txHash": wallet.send()})
86
+ status, headers, body = x402_request(Immiscible(), "https://api.example.com/report", pay=my_x402_signer)
87
+ ```
88
+
89
+ Amounts are decimal strings, never floats. See [crypto payments](https://immiscible.fly.dev/docs/guides/crypto-payments) and [x402](https://immiscible.fly.dev/docs/guides/x402).
@@ -0,0 +1,36 @@
1
+ """immiscible: the control layer for AI agents, from the agent's side.
2
+
3
+ from immiscible import Immiscible
4
+ immiscible = Immiscible() # IMMISCIBLE_AGENT_KEY, IMMISCIBLE_URL
5
+ run = immiscible.run() # one trace and one session per task
6
+ with run.guard(action) as decision: # authorize, wait, run, settle
7
+ do_the_thing(decision.receipt)
8
+
9
+ Standard library only, including the Ed25519 verifier.
10
+ """
11
+
12
+ from .client import DEFAULT_BASE_URL, SDK_VERSION, Decision, Immiscible, new_idempotency_key, normalise_domain, tool_action
13
+ from .errors import (
14
+ ImmiscibleApprovalRequiredError, ImmiscibleApprovalTimeoutError, ImmiscibleDeniedError, ImmiscibleError, is_refusal,
15
+ )
16
+ from .gateway import Gateway
17
+ from .proxy import MCP_PROTOCOL, McpProxy, approval_from_result, approval_meta, mcp_proxy_url, mcp_server_url
18
+ from .trace import (
19
+ ISSUED_SESSION_HEADER, SESSION_HEADER, TRACEPARENT_HEADER, RunContext, format_traceparent, is_valid_session_id,
20
+ new_span_id, new_trace_id, parse_traceparent,
21
+ )
22
+ from .verify import (
23
+ JWKS_PATH, REASONS, RECEIPT_TYP, VerifyResult, clear_jwks_cache, decode_receipt_unverified, fetch_jwks, pin_jwks,
24
+ verify_online, verify_receipt,
25
+ )
26
+
27
+ __version__ = SDK_VERSION
28
+ __all__ = [
29
+ "Immiscible", "Decision", "tool_action", "normalise_domain", "new_idempotency_key", "DEFAULT_BASE_URL", "SDK_VERSION",
30
+ "ImmiscibleError", "ImmiscibleDeniedError", "ImmiscibleApprovalTimeoutError", "ImmiscibleApprovalRequiredError", "is_refusal",
31
+ "Gateway", "McpProxy", "mcp_proxy_url", "mcp_server_url", "approval_from_result", "approval_meta", "MCP_PROTOCOL",
32
+ "RunContext", "parse_traceparent", "format_traceparent", "new_trace_id", "new_span_id", "is_valid_session_id",
33
+ "SESSION_HEADER", "ISSUED_SESSION_HEADER", "TRACEPARENT_HEADER",
34
+ "verify_receipt", "verify_online", "fetch_jwks", "pin_jwks", "VerifyResult", "clear_jwks_cache", "decode_receipt_unverified",
35
+ "RECEIPT_TYP", "JWKS_PATH", "REASONS",
36
+ ]