bitcoinstratigraphy-agent-yield 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.
@@ -0,0 +1,67 @@
1
+ # See https://docs.github.com/en/get-started/getting-started-with-git/ignoring-files for more about ignoring files.
2
+
3
+ # compiled output
4
+ dist
5
+ tmp
6
+ out-tsc
7
+ *.tsbuildinfo
8
+ .expo
9
+ .expo-shared
10
+
11
+ # dependencies
12
+ node_modules
13
+
14
+ # IDEs and editors
15
+ /.idea
16
+ .project
17
+ .classpath
18
+ .c9/
19
+ *.launch
20
+ .settings/
21
+ *.sublime-workspace
22
+
23
+ # IDE - VSCode
24
+ .vscode/*
25
+ !.vscode/settings.json
26
+ !.vscode/tasks.json
27
+ !.vscode/launch.json
28
+ !.vscode/extensions.json
29
+
30
+ # misc
31
+ /.sass-cache
32
+ /connect.lock
33
+ /coverage
34
+ /libpeerconnection.log
35
+ npm-debug.log
36
+ yarn-error.log
37
+ testem.log
38
+ /typings
39
+
40
+ # System Files
41
+ .DS_Store
42
+ Thumbs.db
43
+
44
+ .cursor/rules/nx-rules.mdc
45
+ .github/instructions/nx.instructions.md
46
+
47
+ # Replit
48
+ .cache/
49
+ .local/
50
+ .data/
51
+
52
+ # Private enterprise audit evidence; never commit trace data.
53
+ **/.agent-yield-state/
54
+ /audit_trail.jsonl
55
+ /audit_trail.jsonl.*
56
+
57
+ # Enterprise SDK build output and Python bytecode.
58
+ /sdk/python/build/
59
+ /sdk/python/*.egg-info/
60
+ __pycache__/
61
+ *.pyc
62
+
63
+ # Runtime provenance archives and generated verification artifacts.
64
+ /generated-artifacts/
65
+
66
+ # Private tool-integrity signing keys (standalone deployments only).
67
+ **/.keys/
@@ -0,0 +1,12 @@
1
+ Copyright (c) 2026 Charles R. Strogish. All Rights Reserved.
2
+
3
+ This software is dual-licensed under GNU AGPL version 3 only
4
+ (SPDX: AGPL-3.0-only), or a separately executed Commercial License from
5
+ Charles R. Strogish (SPDX: LicenseRef-Commercial).
6
+
7
+ The AGPLv3 text is available at https://www.gnu.org/licenses/agpl-3.0.html.
8
+ The repository's LICENSE.md contains the full license text and licensing rider:
9
+ https://github.com/CharlesStrogish/bitcoin-stratigraphy-mcp/blob/main/LICENSE.md
10
+
11
+ This notice does not grant commercial licensing rights. AGPLv3 permits
12
+ commercial use when its conditions are met.
@@ -0,0 +1,195 @@
1
+ Metadata-Version: 2.5
2
+ Name: bitcoinstratigraphy-agent-yield
3
+ Version: 0.1.0
4
+ Summary: One-call MCP discovery, budgeted L402 settlement and fiduciary tools for Python agents.
5
+ Project-URL: Homepage, https://bitcoinstratigraphy.com
6
+ Project-URL: Repository, https://github.com/CharlesStrogish/bitcoin-stratigraphy-mcp
7
+ License-Expression: AGPL-3.0-only OR LicenseRef-Commercial
8
+ License-File: LICENSE.md
9
+ Classifier: Development Status :: 3 - Alpha
10
+ Classifier: Programming Language :: Python :: 3
11
+ Classifier: Programming Language :: Python :: 3.10
12
+ Classifier: Programming Language :: Python :: 3.13
13
+ Classifier: Typing :: Typed
14
+ Requires-Python: >=3.10
15
+ Requires-Dist: bech32==1.2.0
16
+ Requires-Dist: coincurve==21.0.0
17
+ Requires-Dist: httpx==0.28.1
18
+ Provides-Extra: test
19
+ Requires-Dist: pytest==9.2.0; extra == 'test'
20
+ Description-Content-Type: text/markdown
21
+
22
+ # bitcoinstratigraphy-agent-yield
23
+
24
+ One-call Python onboarding for Bitcoin Stratigraphy MCP tools: HTTPX discovery,
25
+ automatic bounded L402 retries, durable spending reservations, and the same
26
+ stderr terminal receipts as `@bitcoinstratigraphy/agent-yield`.
27
+
28
+ Install from PyPI:
29
+
30
+ ```sh
31
+ pip install bitcoinstratigraphy-agent-yield
32
+ ```
33
+
34
+ For local development, install from this checkout:
35
+
36
+ ```sh
37
+ pip install ./packages/python-agent-yield
38
+ ```
39
+
40
+ ## Two-line quick starts
41
+
42
+ Start with your existing `agent_config`. Its `fiduciary` policy is the explicit
43
+ wallet authorization shown below. No wallet is created, connected or funded
44
+ by installing or importing this package.
45
+
46
+ ### LangChain
47
+
48
+ ```python
49
+ from bitcoinstratigraphy_agent_yield import with_fiduciary_yield
50
+
51
+ agent = with_fiduciary_yield({**agent_config, "framework": "langchain"})
52
+ ```
53
+
54
+ Pass `agent["tools"]` to `langchain.agents.create_agent(model=..., tools=...)`.
55
+ Existing tools are preserved; discovered tools are native `StructuredTool`s
56
+ with sync `.invoke()` and async `.ainvoke()`.
57
+
58
+ ### CrewAI
59
+
60
+ ```python
61
+ from bitcoinstratigraphy_agent_yield import with_fiduciary_yield
62
+
63
+ agent = with_fiduciary_yield({**agent_config, "framework": "crewai"})
64
+ ```
65
+
66
+ Pass `agent["tools"]` to `crewai.Agent(role=..., goal=..., backstory=..., tools=...)`.
67
+ They are native CrewAI `BaseTool`s. You can also wrap an existing public-tools
68
+ CrewAI agent with `with_fiduciary_yield(existing_agent, fiduciary=policy)`;
69
+ this returns a copy, never mutating the original agent.
70
+
71
+ ### AutoGen
72
+
73
+ ```python
74
+ from bitcoinstratigraphy_agent_yield import with_fiduciary_yield
75
+
76
+ agent = with_fiduciary_yield({**agent_config, "framework": "autogen"})
77
+ ```
78
+
79
+ Pass `agent["tools"]` to `autogen_agentchat.agents.AssistantAgent(..., tools=...)`.
80
+ These are native `autogen_core.tools.BaseTool`s, not OpenAI descriptors requiring
81
+ a separate dispatcher. Cancellation is checked before starting execution.
82
+
83
+ Framework imports are lazy. Install your chosen framework separately; the base
84
+ wheel has **only three direct runtime dependencies**, all exactly pinned:
85
+ HTTPX, bech32 and coincurve. No LangChain, CrewAI, AutoGen, NWC or MCP framework
86
+ is forced on other consumers. Native adapters target the current public
87
+ LangChain Core 1.x, CrewAI 1.x and AutoGen Core 0.7 APIs.
88
+
89
+ ## Explicit wallet and spending policy
90
+
91
+ ```python
92
+ policy = {
93
+ "wallet": authorized_wallet,
94
+ "max_payment_sats": 500,
95
+ "max_total_sats": 5000,
96
+ "fee_reserve_sats": 10,
97
+ "state_directory": ".agent-yield-state",
98
+ }
99
+ agent_config = {"tools": existing_tools, "fiduciary": policy}
100
+ ```
101
+
102
+ `authorized_wallet.pay_invoice(bolt11)` is a **synchronous** wallet adapter
103
+ returning `{"preimage": "<64 hex characters>", "fees_paid": 0}`.
104
+ `fees_paid` is optional integer millisatoshis. Use your wallet provider's
105
+ authorized SDK inside the adapter; secrets never belong in prompts, source,
106
+ HTTP headers, terminal receipts, or tool arguments. This scaffold does **not**
107
+ implement NWC wallet transport.
108
+
109
+ Without a wallet and explicit positive budgets, discovery and free tools work;
110
+ paid execution fails closed. Optional `make_invoice(amount_msats)` returns
111
+ `{"invoice": "<receiving BOLT11>"}` to enable automatic yield withdrawals.
112
+ Wallet adapters must enforce their own operation timeouts and fee ceiling:
113
+ Python cannot cancel an external payment already submitted to a wallet.
114
+
115
+ `agent["fiduciary"]` exposes `.list_tools()`, `.call_tool(name, arguments)`,
116
+ `await .acall_tool(name, arguments)`, `.recover_claim(payment_hash)`, and
117
+ `.close()`. Framework tools are added to a configuration before constructing a
118
+ graph/AssistantAgent; this does not retrofit private internals of running agents.
119
+ Async execution offloads synchronous HTTP/wallet work to a worker thread.
120
+ Cancelling the awaiting task does not reverse an in-progress wallet operation.
121
+ Close the config's client at application shutdown. For public-tools object
122
+ wrapping, supply an application-owned `http_client` and close it yourself.
123
+
124
+ ## Connection and payment guarantees
125
+
126
+ - Discovers the live paginated catalog at `https://bitcoinstratigraphy.com/mcp`.
127
+ Handles MCP `2026-07-28`, JSON and SSE, bounded response sizes, timeout and
128
+ one bounded Hashcash admission retry. Discovery never pays a wallet.
129
+ - Validates mainnet BOLT11 checksum, recoverable signature, explicit payee,
130
+ amount, expiry and payment hash against the HTTP JSON quote before spending.
131
+ - Reserves invoice principal plus fee allowance in private SQLite storage
132
+ **before** contacting the wallet. Per-invoice and cumulative limits are
133
+ integer satoshis. Reservations survive threads, processes and restarts.
134
+ Reported excessive/invalid fees block future new payments.
135
+ - Verifies every preimage against its invoice hash, then retries the identical
136
+ MCP body/ID exactly once with `Authorization: L402 macaroon:preimage`.
137
+ Credentials are bound to the exact invoice/request, not globally reused
138
+ across unrelated tools. A repeated 402 never triggers a second payment.
139
+ - Never follows redirects or replaces caller-supplied payment credentials.
140
+ Supply `identity_headers(url, method, body_bytes)` for fresh NIP-98 headers;
141
+ it runs again for admission and paid retries. Valid Genesis Grant calls incur
142
+ no payment, paid-history credit, yield allocation or earned receipt.
143
+ - Unknown payments and invalid returned proofs stay reserved. **Do not delete
144
+ the state directory or change directories to retry a payment.** Operators
145
+ must establish the payment outcome before recovery. Use private persistent
146
+ storage, exclude it from Git, and use one directory per wallet and policy.
147
+
148
+ The SQLite ledger contains sensitive payment proofs, though never wallet keys.
149
+ The folder is created with mode 0700 and the database with 0600. Protect and
150
+ back up this directory. Existing directories must already be private.
151
+ This is client-side accounting, not a replacement for server-side settlement
152
+ ledgers or a wallet-enforced spending limit.
153
+
154
+ ## Terminal receipts
155
+
156
+ On a successful paid execution with a receiving wallet:
157
+
158
+ ```text
159
+ [Bitcoin Stratigraphy] ⚡ Thermodynamic Yield Allocated: 21 Sats (settlement pending).
160
+ [Bitcoin Stratigraphy] ⚡ Thermodynamic Yield Earned: 21 Sats routed to agent wallet.
161
+ ```
162
+
163
+ Allocated is yellow; earned is green. An earned receipt appears **only** after
164
+ the claim API confirms `SETTLED` **and** `payment_sent: true`, never on
165
+ allocation, free grants, pending claims, failed tools or failed payouts.
166
+ Millisatoshi precision is preserved. Logging goes exclusively to `sys.stderr`
167
+ to preserve MCP JSON-RPC stdout. `QUIET_STRATIGRAPHY=true` disables receipts at
168
+ event time; logging failures never change successful tool results.
169
+
170
+ Claims are best effort, not guaranteed income. `on_error({"code": ...})`
171
+ reports recovery needs without exposing wallet/server error text.
172
+ `on_yield_claim({"status": ..., "amount_msats": ...})` observes claim status.
173
+ `.recover_claim(payment_hash)` replays only a persisted withdrawal request and
174
+ never buys a new tool invoice. Recovery after a crash during receiving-invoice
175
+ creation needs operator intervention; it does not create a second invoice.
176
+ Claims run synchronously after paid execution, bounded by the HTTP timeout.
177
+
178
+ ## Development / packaging (no publication)
179
+
180
+ From the repository root, using its Python environment:
181
+
182
+ ```sh
183
+ uv run --no-sync python -m pytest packages/python-agent-yield
184
+ cd packages/python-agent-yield
185
+ uv run --no-sync python -m hatchling build -d ../../generated-artifacts/python-agent-yield
186
+ ```
187
+
188
+ Or install `.[test]` inside this package, then run `python -m pytest`.
189
+ The dedicated tests use signed synthetic invoices, `httpx.MockTransport`,
190
+ temporary state, and fake wallets. They do not contact mainnet or move funds.
191
+ Native LangChain/AutoGen integration tests run when those frameworks are
192
+ installed; the CrewAI adapter has a public-API contract fixture without pulling
193
+ CrewAI's full dependency tree into this standalone package.
194
+ Wheel/sdist build output belongs in `generated-artifacts/`, never source control.
195
+ Building never uploads to PyPI; publication is an explicit release step.
@@ -0,0 +1,174 @@
1
+ # bitcoinstratigraphy-agent-yield
2
+
3
+ One-call Python onboarding for Bitcoin Stratigraphy MCP tools: HTTPX discovery,
4
+ automatic bounded L402 retries, durable spending reservations, and the same
5
+ stderr terminal receipts as `@bitcoinstratigraphy/agent-yield`.
6
+
7
+ Install from PyPI:
8
+
9
+ ```sh
10
+ pip install bitcoinstratigraphy-agent-yield
11
+ ```
12
+
13
+ For local development, install from this checkout:
14
+
15
+ ```sh
16
+ pip install ./packages/python-agent-yield
17
+ ```
18
+
19
+ ## Two-line quick starts
20
+
21
+ Start with your existing `agent_config`. Its `fiduciary` policy is the explicit
22
+ wallet authorization shown below. No wallet is created, connected or funded
23
+ by installing or importing this package.
24
+
25
+ ### LangChain
26
+
27
+ ```python
28
+ from bitcoinstratigraphy_agent_yield import with_fiduciary_yield
29
+
30
+ agent = with_fiduciary_yield({**agent_config, "framework": "langchain"})
31
+ ```
32
+
33
+ Pass `agent["tools"]` to `langchain.agents.create_agent(model=..., tools=...)`.
34
+ Existing tools are preserved; discovered tools are native `StructuredTool`s
35
+ with sync `.invoke()` and async `.ainvoke()`.
36
+
37
+ ### CrewAI
38
+
39
+ ```python
40
+ from bitcoinstratigraphy_agent_yield import with_fiduciary_yield
41
+
42
+ agent = with_fiduciary_yield({**agent_config, "framework": "crewai"})
43
+ ```
44
+
45
+ Pass `agent["tools"]` to `crewai.Agent(role=..., goal=..., backstory=..., tools=...)`.
46
+ They are native CrewAI `BaseTool`s. You can also wrap an existing public-tools
47
+ CrewAI agent with `with_fiduciary_yield(existing_agent, fiduciary=policy)`;
48
+ this returns a copy, never mutating the original agent.
49
+
50
+ ### AutoGen
51
+
52
+ ```python
53
+ from bitcoinstratigraphy_agent_yield import with_fiduciary_yield
54
+
55
+ agent = with_fiduciary_yield({**agent_config, "framework": "autogen"})
56
+ ```
57
+
58
+ Pass `agent["tools"]` to `autogen_agentchat.agents.AssistantAgent(..., tools=...)`.
59
+ These are native `autogen_core.tools.BaseTool`s, not OpenAI descriptors requiring
60
+ a separate dispatcher. Cancellation is checked before starting execution.
61
+
62
+ Framework imports are lazy. Install your chosen framework separately; the base
63
+ wheel has **only three direct runtime dependencies**, all exactly pinned:
64
+ HTTPX, bech32 and coincurve. No LangChain, CrewAI, AutoGen, NWC or MCP framework
65
+ is forced on other consumers. Native adapters target the current public
66
+ LangChain Core 1.x, CrewAI 1.x and AutoGen Core 0.7 APIs.
67
+
68
+ ## Explicit wallet and spending policy
69
+
70
+ ```python
71
+ policy = {
72
+ "wallet": authorized_wallet,
73
+ "max_payment_sats": 500,
74
+ "max_total_sats": 5000,
75
+ "fee_reserve_sats": 10,
76
+ "state_directory": ".agent-yield-state",
77
+ }
78
+ agent_config = {"tools": existing_tools, "fiduciary": policy}
79
+ ```
80
+
81
+ `authorized_wallet.pay_invoice(bolt11)` is a **synchronous** wallet adapter
82
+ returning `{"preimage": "<64 hex characters>", "fees_paid": 0}`.
83
+ `fees_paid` is optional integer millisatoshis. Use your wallet provider's
84
+ authorized SDK inside the adapter; secrets never belong in prompts, source,
85
+ HTTP headers, terminal receipts, or tool arguments. This scaffold does **not**
86
+ implement NWC wallet transport.
87
+
88
+ Without a wallet and explicit positive budgets, discovery and free tools work;
89
+ paid execution fails closed. Optional `make_invoice(amount_msats)` returns
90
+ `{"invoice": "<receiving BOLT11>"}` to enable automatic yield withdrawals.
91
+ Wallet adapters must enforce their own operation timeouts and fee ceiling:
92
+ Python cannot cancel an external payment already submitted to a wallet.
93
+
94
+ `agent["fiduciary"]` exposes `.list_tools()`, `.call_tool(name, arguments)`,
95
+ `await .acall_tool(name, arguments)`, `.recover_claim(payment_hash)`, and
96
+ `.close()`. Framework tools are added to a configuration before constructing a
97
+ graph/AssistantAgent; this does not retrofit private internals of running agents.
98
+ Async execution offloads synchronous HTTP/wallet work to a worker thread.
99
+ Cancelling the awaiting task does not reverse an in-progress wallet operation.
100
+ Close the config's client at application shutdown. For public-tools object
101
+ wrapping, supply an application-owned `http_client` and close it yourself.
102
+
103
+ ## Connection and payment guarantees
104
+
105
+ - Discovers the live paginated catalog at `https://bitcoinstratigraphy.com/mcp`.
106
+ Handles MCP `2026-07-28`, JSON and SSE, bounded response sizes, timeout and
107
+ one bounded Hashcash admission retry. Discovery never pays a wallet.
108
+ - Validates mainnet BOLT11 checksum, recoverable signature, explicit payee,
109
+ amount, expiry and payment hash against the HTTP JSON quote before spending.
110
+ - Reserves invoice principal plus fee allowance in private SQLite storage
111
+ **before** contacting the wallet. Per-invoice and cumulative limits are
112
+ integer satoshis. Reservations survive threads, processes and restarts.
113
+ Reported excessive/invalid fees block future new payments.
114
+ - Verifies every preimage against its invoice hash, then retries the identical
115
+ MCP body/ID exactly once with `Authorization: L402 macaroon:preimage`.
116
+ Credentials are bound to the exact invoice/request, not globally reused
117
+ across unrelated tools. A repeated 402 never triggers a second payment.
118
+ - Never follows redirects or replaces caller-supplied payment credentials.
119
+ Supply `identity_headers(url, method, body_bytes)` for fresh NIP-98 headers;
120
+ it runs again for admission and paid retries. Valid Genesis Grant calls incur
121
+ no payment, paid-history credit, yield allocation or earned receipt.
122
+ - Unknown payments and invalid returned proofs stay reserved. **Do not delete
123
+ the state directory or change directories to retry a payment.** Operators
124
+ must establish the payment outcome before recovery. Use private persistent
125
+ storage, exclude it from Git, and use one directory per wallet and policy.
126
+
127
+ The SQLite ledger contains sensitive payment proofs, though never wallet keys.
128
+ The folder is created with mode 0700 and the database with 0600. Protect and
129
+ back up this directory. Existing directories must already be private.
130
+ This is client-side accounting, not a replacement for server-side settlement
131
+ ledgers or a wallet-enforced spending limit.
132
+
133
+ ## Terminal receipts
134
+
135
+ On a successful paid execution with a receiving wallet:
136
+
137
+ ```text
138
+ [Bitcoin Stratigraphy] ⚡ Thermodynamic Yield Allocated: 21 Sats (settlement pending).
139
+ [Bitcoin Stratigraphy] ⚡ Thermodynamic Yield Earned: 21 Sats routed to agent wallet.
140
+ ```
141
+
142
+ Allocated is yellow; earned is green. An earned receipt appears **only** after
143
+ the claim API confirms `SETTLED` **and** `payment_sent: true`, never on
144
+ allocation, free grants, pending claims, failed tools or failed payouts.
145
+ Millisatoshi precision is preserved. Logging goes exclusively to `sys.stderr`
146
+ to preserve MCP JSON-RPC stdout. `QUIET_STRATIGRAPHY=true` disables receipts at
147
+ event time; logging failures never change successful tool results.
148
+
149
+ Claims are best effort, not guaranteed income. `on_error({"code": ...})`
150
+ reports recovery needs without exposing wallet/server error text.
151
+ `on_yield_claim({"status": ..., "amount_msats": ...})` observes claim status.
152
+ `.recover_claim(payment_hash)` replays only a persisted withdrawal request and
153
+ never buys a new tool invoice. Recovery after a crash during receiving-invoice
154
+ creation needs operator intervention; it does not create a second invoice.
155
+ Claims run synchronously after paid execution, bounded by the HTTP timeout.
156
+
157
+ ## Development / packaging (no publication)
158
+
159
+ From the repository root, using its Python environment:
160
+
161
+ ```sh
162
+ uv run --no-sync python -m pytest packages/python-agent-yield
163
+ cd packages/python-agent-yield
164
+ uv run --no-sync python -m hatchling build -d ../../generated-artifacts/python-agent-yield
165
+ ```
166
+
167
+ Or install `.[test]` inside this package, then run `python -m pytest`.
168
+ The dedicated tests use signed synthetic invoices, `httpx.MockTransport`,
169
+ temporary state, and fake wallets. They do not contact mainnet or move funds.
170
+ Native LangChain/AutoGen integration tests run when those frameworks are
171
+ installed; the CrewAI adapter has a public-API contract fixture without pulling
172
+ CrewAI's full dependency tree into this standalone package.
173
+ Wheel/sdist build output belongs in `generated-artifacts/`, never source control.
174
+ Building never uploads to PyPI; publication is an explicit release step.
@@ -0,0 +1,38 @@
1
+ [build-system]
2
+ requires = ["hatchling==1.33.0"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "bitcoinstratigraphy-agent-yield"
7
+ version = "0.1.0"
8
+ description = "One-call MCP discovery, budgeted L402 settlement and fiduciary tools for Python agents."
9
+ readme = "README.md"
10
+ requires-python = ">=3.10"
11
+ license = "AGPL-3.0-only OR LicenseRef-Commercial"
12
+ license-files = ["LICENSE.md"]
13
+ dependencies = ["httpx==0.28.1", "bech32==1.2.0", "coincurve==21.0.0"]
14
+ classifiers = [
15
+ "Development Status :: 3 - Alpha",
16
+ "Programming Language :: Python :: 3",
17
+ "Programming Language :: Python :: 3.10",
18
+ "Programming Language :: Python :: 3.13",
19
+ "Typing :: Typed",
20
+ ]
21
+
22
+ [project.optional-dependencies]
23
+ test = ["pytest==9.2.0"]
24
+
25
+ [project.urls]
26
+ Homepage = "https://bitcoinstratigraphy.com"
27
+ Repository = "https://github.com/CharlesStrogish/bitcoin-stratigraphy-mcp"
28
+
29
+ [tool.hatch.build.targets.wheel]
30
+ packages = ["src/bitcoinstratigraphy_agent_yield"]
31
+
32
+ [tool.hatch.build.targets.sdist]
33
+ include = ["/src", "/tests", "/README.md", "/LICENSE.md", "/pyproject.toml"]
34
+
35
+ [tool.pytest.ini_options]
36
+ testpaths = ["tests"]
37
+ pythonpath = ["src"]
38
+ addopts = "-ra"
@@ -0,0 +1,14 @@
1
+ """Frictionless, explicitly budgeted Bitcoin Stratigraphy tools for Python agents."""
2
+
3
+ from .adapters import AgentTool, with_fiduciary_yield
4
+ from .client import DEFAULT_MCP_URL, FiduciaryClient
5
+ from .errors import FiduciaryError
6
+
7
+ __all__ = [
8
+ "AgentTool",
9
+ "DEFAULT_MCP_URL",
10
+ "FiduciaryClient",
11
+ "FiduciaryError",
12
+ "with_fiduciary_yield",
13
+ ]
14
+ __version__ = "0.1.0"
@@ -0,0 +1,172 @@
1
+ """Lazy, native framework adapters. No framework dependencies in the base wheel."""
2
+
3
+ import copy
4
+ import re
5
+ from collections.abc import Mapping
6
+ from dataclasses import dataclass
7
+ from typing import Any
8
+
9
+ from .client import FiduciaryClient
10
+ from .errors import FiduciaryError
11
+
12
+
13
+ @dataclass(frozen=True)
14
+ class AgentTool:
15
+ name: str
16
+ canonical_name: str
17
+ description: str
18
+ input_schema: dict
19
+ client: FiduciaryClient
20
+
21
+ def invoke(self, arguments=None, **kwargs):
22
+ return self.client.call_tool(
23
+ self.canonical_name, arguments if arguments is not None else kwargs
24
+ )
25
+
26
+ async def ainvoke(self, arguments=None, **kwargs):
27
+ return await self.client.acall_tool(
28
+ self.canonical_name, arguments if arguments is not None else kwargs
29
+ )
30
+
31
+
32
+ def _argument_model(tool):
33
+ from pydantic import ConfigDict, create_model
34
+
35
+ # Forward the original JSON Schema to the model provider without flattening
36
+ # enums, nested objects, unions, or refs. MCP remains the authoritative
37
+ # validator; framework-side parsing must not coerce or drop arguments.
38
+ schema = copy.deepcopy(tool.input_schema)
39
+ required = schema.get("required", [])
40
+ fields = {
41
+ name: (Any, ... if name in required else None)
42
+ for name in schema.get("properties", {})
43
+ }
44
+ model = create_model(
45
+ f"{tool.name}_Args", __config__=ConfigDict(extra="allow"), **fields
46
+ )
47
+ model.model_json_schema = classmethod(
48
+ lambda cls, *args, **kwargs: copy.deepcopy(schema)
49
+ )
50
+ return model
51
+
52
+
53
+ def _langchain(tool):
54
+ from langchain_core.tools import StructuredTool
55
+
56
+ return StructuredTool.from_function(
57
+ func=lambda **kwargs: tool.invoke(kwargs),
58
+ coroutine=lambda **kwargs: tool.ainvoke(kwargs),
59
+ name=tool.name,
60
+ description=tool.description,
61
+ args_schema=copy.deepcopy(tool.input_schema),
62
+ infer_schema=False,
63
+ )
64
+
65
+
66
+ def _crewai(tool):
67
+ from crewai.tools import BaseTool
68
+
69
+ class FiduciaryCrewTool(BaseTool):
70
+ def _run(self, **kwargs):
71
+ return tool.invoke(kwargs)
72
+
73
+ async def _arun(self, **kwargs):
74
+ return await tool.ainvoke(kwargs)
75
+
76
+ return FiduciaryCrewTool(
77
+ name=tool.name, description=tool.description, args_schema=_argument_model(tool)
78
+ )
79
+
80
+
81
+ def _autogen(tool):
82
+ from autogen_core.tools import BaseTool
83
+
84
+ class FiduciaryAutoGenTool(BaseTool):
85
+ def __init__(self):
86
+ super().__init__(_argument_model(tool), object, tool.name, tool.description)
87
+
88
+ @property
89
+ def schema(self):
90
+ return {
91
+ "name": tool.name,
92
+ "description": tool.description,
93
+ "parameters": copy.deepcopy(tool.input_schema),
94
+ }
95
+
96
+ async def run(self, args, cancellation_token):
97
+ if cancellation_token.is_cancelled():
98
+ raise FiduciaryError("MCP_REQUEST_CANCELLED")
99
+ return await tool.ainvoke(args.model_dump(exclude_unset=True))
100
+
101
+ return FiduciaryAutoGenTool()
102
+
103
+
104
+ def with_fiduciary_yield(agent, *, fiduciary=None, framework=None):
105
+ """Copy an agent config or a public-tools agent, discover, and append tools.
106
+
107
+ Configs return a dict with a ``fiduciary`` client for lifetime management.
108
+ Public-tools/Pydantic agents return a model_copy; their original is untouched.
109
+ Already-built graph/private-internals agents must be wrapped at config time.
110
+ """
111
+ is_config = isinstance(agent, Mapping)
112
+ if is_config:
113
+ options = fiduciary if fiduciary is not None else agent.get("fiduciary", {})
114
+ selected = framework or agent.get("framework", "custom")
115
+ existing = list(agent.get("tools", []))
116
+ else:
117
+ if not callable(getattr(agent, "model_copy", None)) or not hasattr(
118
+ agent, "tools"
119
+ ):
120
+ raise FiduciaryError("WRAP_AGENT_CONFIGURATION_NOT_PRIVATE_INTERNALS")
121
+ if fiduciary is None:
122
+ raise FiduciaryError("FIDUCIARY_CONFIGURATION_REQUIRED")
123
+ options = fiduciary
124
+ selected = framework or (
125
+ "crewai" if type(agent).__module__.startswith("crewai") else "custom"
126
+ )
127
+ existing = list(agent.tools or [])
128
+ if selected not in ("custom", "langchain", "crewai", "autogen"):
129
+ raise FiduciaryError("UNSUPPORTED_FRAMEWORK")
130
+ if not isinstance(options, Mapping):
131
+ raise FiduciaryError("FIDUCIARY_CONFIGURATION_REQUIRED")
132
+ client = FiduciaryClient(**options)
133
+ try:
134
+ tools = []
135
+ for index, entry in enumerate(client.list_tools()):
136
+ safe = f"fiduciary_{index}_{re.sub(r'[^a-zA-Z0-9_-]', '_', entry['name'])}"[
137
+ :64
138
+ ]
139
+ if any(
140
+ (
141
+ item.get("name")
142
+ if isinstance(item, dict)
143
+ else getattr(item, "name", None)
144
+ )
145
+ == safe
146
+ for item in existing
147
+ ):
148
+ raise FiduciaryError("MCP_TOOL_NAME_COLLISION")
149
+ tool = AgentTool(
150
+ safe,
151
+ entry["name"],
152
+ entry.get("description") or entry["name"],
153
+ copy.deepcopy(entry["inputSchema"]),
154
+ client,
155
+ )
156
+ adapter = {
157
+ "langchain": _langchain,
158
+ "crewai": _crewai,
159
+ "autogen": _autogen,
160
+ }.get(selected)
161
+ tools.append(adapter(tool) if adapter else tool)
162
+ if is_config:
163
+ return {**agent, "tools": existing + tools, "fiduciary": client}
164
+ # CrewAI agents expose public tools but do not accept arbitrary client
165
+ # fields. Tool closures retain the client; no global monkeypatching.
166
+ return agent.model_copy(update={"tools": existing + tools})
167
+ except ImportError:
168
+ client.close()
169
+ raise FiduciaryError(f"INSTALL_{selected.upper()}_FOR_NATIVE_ADAPTER") from None
170
+ except BaseException:
171
+ client.close()
172
+ raise