provecart-adk 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,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 ProveCart
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,138 @@
1
+ Metadata-Version: 2.4
2
+ Name: provecart-adk
3
+ Version: 0.1.0
4
+ Summary: ProveCart pre-action hook for Google ADK: check an agent's order, booking or reservation against the customer's signed mandate before a purchase tool runs.
5
+ License: MIT
6
+ Project-URL: Homepage, https://provecart.com
7
+ Project-URL: Repository, https://github.com/provecart/provecart
8
+ Keywords: ai-agents,agent-commerce,google-adk,provecart,pre-payment-verification
9
+ Classifier: License :: OSI Approved :: MIT License
10
+ Classifier: Programming Language :: Python :: 3
11
+ Classifier: Topic :: Software Development :: Libraries
12
+ Requires-Python: >=3.9
13
+ Description-Content-Type: text/markdown
14
+ License-File: LICENSE
15
+ Provides-Extra: adk
16
+ Requires-Dist: google-adk; extra == "adk"
17
+ Provides-Extra: test
18
+ Requires-Dist: pytest; extra == "test"
19
+ Dynamic: license-file
20
+
21
+ # provecart-adk
22
+
23
+ A pre-action hook for Google ADK agents, from ProveCart.
24
+
25
+ An agent that buys for someone needs one check in the right place:
26
+ after it has built the order, and before any money moves. This
27
+ package puts that check on ADK's tool boundary. When a purchase
28
+ tool is about to run, the hook reads the pending order, booking or
29
+ reservation out of the tool arguments, compares it with the
30
+ customer's signed mandate, and only then decides whether the tool
31
+ call goes ahead.
32
+
33
+ ```bash
34
+ pip install provecart-adk
35
+ pip install "provecart-adk[adk]" # also installs google-adk
36
+ ```
37
+
38
+ ## How it works
39
+
40
+ ProveCart returns one of three verdicts:
41
+
42
+ - **MATCH** — the tool runs. The callback returns `None`, which is
43
+ ADK's signal to proceed as normal.
44
+ - **BLOCK** — the tool does not run. ADK's documented refusal path
45
+ applies: a `before_tool_callback` that returns a dict skips the
46
+ tool call and that dict becomes the tool result. The dict carries
47
+ the verdict, the record id and the corrections, so the agent can
48
+ apply a fix that stays inside the customer's existing
49
+ authorization and check again.
50
+ - **REVIEW** — the tool does not run, and the refusal dict tells
51
+ the agent to stop and ask the customer. A review is never turned
52
+ into a guess.
53
+
54
+ A check that fails, a verdict that is not recognized, and a BLOCK
55
+ whose corrections use an unknown action all escalate the same way.
56
+ Nothing falls through to the purchase tool by accident.
57
+
58
+ ## Use it two ways
59
+
60
+ As an agent callback:
61
+
62
+ ```python
63
+ from provecart_adk import ProveCartGuard, make_before_tool_callback
64
+
65
+ guard = ProveCartGuard(client=my_provecart_client)
66
+ agent = LlmAgent(
67
+ name="shopper",
68
+ tools=[place_order],
69
+ before_tool_callback=make_before_tool_callback(guard),
70
+ )
71
+ ```
72
+
73
+ Or as a plugin, registered once so it covers every agent and tool
74
+ in the app:
75
+
76
+ ```python
77
+ from provecart_adk import ProveCartPlugin
78
+
79
+ plugin = ProveCartPlugin(client=my_provecart_client)
80
+ # Runner(..., plugins=[plugin]) or App(..., plugins=[plugin])
81
+ ```
82
+
83
+ ## The client is yours to inject
84
+
85
+ This package does not hard-wire an HTTP client. Pass any object
86
+ with these methods, each returning a dict with `verdict`,
87
+ `corrections`, `record_id` and `max_attempts`:
88
+
89
+ - `check_cart(**payload)`
90
+ - `check_booking(**payload)`
91
+ - `check_reservation(**payload)`
92
+
93
+ If that object also provides
94
+ `route_verdict(response) -> (decision, detail)`, with decision one
95
+ of `proceed`, `fix` or `escalate`, the hook uses it. Otherwise the
96
+ routing built into this package applies: MATCH proceeds; BLOCK
97
+ becomes `fix` only when every correction is a known action; BLOCK
98
+ without usable corrections, REVIEW and anything unknown escalate.
99
+ A client exposing only `verify_cart` is adapted for cart checks.
100
+
101
+ ## Which tools are watched
102
+
103
+ By default the guard watches common purchase names — `place_order`,
104
+ `checkout`, `capture_payment` and friends for carts; `book_flight`,
105
+ `create_booking` for bookings; `make_reservation`, `reserve_table`
106
+ for reservations — and ignores every other tool without calling the
107
+ client. Pass your own map to watch exactly your tool names:
108
+
109
+ ```python
110
+ guard = ProveCartGuard(
111
+ client=my_provecart_client,
112
+ tool_kinds={"submit_payment": "cart", "hold_room": "booking"},
113
+ )
114
+ ```
115
+
116
+ Payloads are taken from the tool arguments as-is, or from a nested
117
+ `cart` / `booking` / `reservation` dict when the tool wraps its
118
+ order that way. Pass an `extractor(kind, args) -> dict` callable if
119
+ your tools shape their arguments differently.
120
+
121
+ ## Offline example and tests
122
+
123
+ ```bash
124
+ python examples/mock_example.py # MOCK mode, injected fake client
125
+ python -m pytest tests/ -q # offline; ADK is stubbed if absent
126
+ ```
127
+
128
+ The example defaults to mock mode and makes no network calls on
129
+ import or on a normal run. Live use is an explicit opt-in: build
130
+ your real client and inject it, as the example marks.
131
+
132
+ ## Notes
133
+
134
+ Published by ProveCart, a verification service for agent commerce
135
+ based in the Bay Area. Patent pending.
136
+
137
+ Docs: https://provecart.com/portal/docs ·
138
+ Source: https://github.com/provecart/provecart/tree/main/packages/provecart-adk
@@ -0,0 +1,118 @@
1
+ # provecart-adk
2
+
3
+ A pre-action hook for Google ADK agents, from ProveCart.
4
+
5
+ An agent that buys for someone needs one check in the right place:
6
+ after it has built the order, and before any money moves. This
7
+ package puts that check on ADK's tool boundary. When a purchase
8
+ tool is about to run, the hook reads the pending order, booking or
9
+ reservation out of the tool arguments, compares it with the
10
+ customer's signed mandate, and only then decides whether the tool
11
+ call goes ahead.
12
+
13
+ ```bash
14
+ pip install provecart-adk
15
+ pip install "provecart-adk[adk]" # also installs google-adk
16
+ ```
17
+
18
+ ## How it works
19
+
20
+ ProveCart returns one of three verdicts:
21
+
22
+ - **MATCH** — the tool runs. The callback returns `None`, which is
23
+ ADK's signal to proceed as normal.
24
+ - **BLOCK** — the tool does not run. ADK's documented refusal path
25
+ applies: a `before_tool_callback` that returns a dict skips the
26
+ tool call and that dict becomes the tool result. The dict carries
27
+ the verdict, the record id and the corrections, so the agent can
28
+ apply a fix that stays inside the customer's existing
29
+ authorization and check again.
30
+ - **REVIEW** — the tool does not run, and the refusal dict tells
31
+ the agent to stop and ask the customer. A review is never turned
32
+ into a guess.
33
+
34
+ A check that fails, a verdict that is not recognized, and a BLOCK
35
+ whose corrections use an unknown action all escalate the same way.
36
+ Nothing falls through to the purchase tool by accident.
37
+
38
+ ## Use it two ways
39
+
40
+ As an agent callback:
41
+
42
+ ```python
43
+ from provecart_adk import ProveCartGuard, make_before_tool_callback
44
+
45
+ guard = ProveCartGuard(client=my_provecart_client)
46
+ agent = LlmAgent(
47
+ name="shopper",
48
+ tools=[place_order],
49
+ before_tool_callback=make_before_tool_callback(guard),
50
+ )
51
+ ```
52
+
53
+ Or as a plugin, registered once so it covers every agent and tool
54
+ in the app:
55
+
56
+ ```python
57
+ from provecart_adk import ProveCartPlugin
58
+
59
+ plugin = ProveCartPlugin(client=my_provecart_client)
60
+ # Runner(..., plugins=[plugin]) or App(..., plugins=[plugin])
61
+ ```
62
+
63
+ ## The client is yours to inject
64
+
65
+ This package does not hard-wire an HTTP client. Pass any object
66
+ with these methods, each returning a dict with `verdict`,
67
+ `corrections`, `record_id` and `max_attempts`:
68
+
69
+ - `check_cart(**payload)`
70
+ - `check_booking(**payload)`
71
+ - `check_reservation(**payload)`
72
+
73
+ If that object also provides
74
+ `route_verdict(response) -> (decision, detail)`, with decision one
75
+ of `proceed`, `fix` or `escalate`, the hook uses it. Otherwise the
76
+ routing built into this package applies: MATCH proceeds; BLOCK
77
+ becomes `fix` only when every correction is a known action; BLOCK
78
+ without usable corrections, REVIEW and anything unknown escalate.
79
+ A client exposing only `verify_cart` is adapted for cart checks.
80
+
81
+ ## Which tools are watched
82
+
83
+ By default the guard watches common purchase names — `place_order`,
84
+ `checkout`, `capture_payment` and friends for carts; `book_flight`,
85
+ `create_booking` for bookings; `make_reservation`, `reserve_table`
86
+ for reservations — and ignores every other tool without calling the
87
+ client. Pass your own map to watch exactly your tool names:
88
+
89
+ ```python
90
+ guard = ProveCartGuard(
91
+ client=my_provecart_client,
92
+ tool_kinds={"submit_payment": "cart", "hold_room": "booking"},
93
+ )
94
+ ```
95
+
96
+ Payloads are taken from the tool arguments as-is, or from a nested
97
+ `cart` / `booking` / `reservation` dict when the tool wraps its
98
+ order that way. Pass an `extractor(kind, args) -> dict` callable if
99
+ your tools shape their arguments differently.
100
+
101
+ ## Offline example and tests
102
+
103
+ ```bash
104
+ python examples/mock_example.py # MOCK mode, injected fake client
105
+ python -m pytest tests/ -q # offline; ADK is stubbed if absent
106
+ ```
107
+
108
+ The example defaults to mock mode and makes no network calls on
109
+ import or on a normal run. Live use is an explicit opt-in: build
110
+ your real client and inject it, as the example marks.
111
+
112
+ ## Notes
113
+
114
+ Published by ProveCart, a verification service for agent commerce
115
+ based in the Bay Area. Patent pending.
116
+
117
+ Docs: https://provecart.com/portal/docs ·
118
+ Source: https://github.com/provecart/provecart/tree/main/packages/provecart-adk
@@ -0,0 +1,29 @@
1
+ [build-system]
2
+ requires = ["setuptools>=61"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "provecart-adk"
7
+ version = "0.1.0"
8
+ description = "ProveCart pre-action hook for Google ADK: check an agent's order, booking or reservation against the customer's signed mandate before a purchase tool runs."
9
+ readme = "README.md"
10
+ requires-python = ">=3.9"
11
+ license = {text = "MIT"}
12
+ keywords = ["ai-agents", "agent-commerce", "google-adk", "provecart", "pre-payment-verification"]
13
+ classifiers = [
14
+ "License :: OSI Approved :: MIT License",
15
+ "Programming Language :: Python :: 3",
16
+ "Topic :: Software Development :: Libraries",
17
+ ]
18
+ dependencies = []
19
+
20
+ [project.optional-dependencies]
21
+ adk = ["google-adk"]
22
+ test = ["pytest"]
23
+
24
+ [project.urls]
25
+ Homepage = "https://provecart.com"
26
+ Repository = "https://github.com/provecart/provecart"
27
+
28
+ [tool.setuptools.packages.find]
29
+ where = ["src"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,283 @@
1
+ """ProveCart pre-action hook for Google ADK.
2
+
3
+ Intercepts the moment before a purchase tool runs, checks the
4
+ pending order, booking or reservation against the customer's signed
5
+ mandate, and lets the tool run only on a MATCH.
6
+
7
+ Verdict routing (same semantics as the shared ProveCart client):
8
+
9
+ MATCH -> proceed (return None; ADK runs the tool)
10
+ BLOCK + known fixes -> do not run the tool; return a refusal dict
11
+ carrying the corrections, so the agent can
12
+ apply only fixes inside the customer's
13
+ existing authorization and re-check
14
+ BLOCK otherwise,
15
+ REVIEW, unknown -> do not run the tool; return a refusal dict
16
+ that escalates to the customer
17
+
18
+ In ADK, returning a dict from ``before_tool_callback`` skips the
19
+ tool call and uses that dict as the tool result. That documented
20
+ refusal path is what this package uses for every non-proceed case.
21
+
22
+ The client is injected and duck-typed. Any object with
23
+ ``check_cart`` / ``check_booking`` / ``check_reservation`` methods
24
+ returning a dict with ``verdict``, ``corrections``, ``record_id``
25
+ and ``max_attempts`` works, as does an object exposing
26
+ ``route_verdict(response) -> (decision, detail)``. If the client
27
+ supplies its own ``route_verdict`` it is used; otherwise the routing
28
+ in this module is used. A client with only the older
29
+ ``verify_cart`` shape is adapted for cart checks.
30
+
31
+ No network call happens on import.
32
+ """
33
+ from __future__ import annotations
34
+
35
+ from typing import Any, Callable, Dict, Mapping, Optional, Tuple
36
+
37
+ __version__ = "0.1.0"
38
+ __all__ = [
39
+ "KNOWN_ACTIONS",
40
+ "DEFAULT_TOOL_KINDS",
41
+ "ProveCartGuard",
42
+ "FakeProveCartClient",
43
+ "make_before_tool_callback",
44
+ "route_verdict",
45
+ ]
46
+
47
+ KNOWN_ACTIONS = {
48
+ "remove_item", "replace_item", "change_variant", "reduce_quantity",
49
+ "reduce_total", "change_currency", "correct_traveler_name",
50
+ "change_airport", "change_travel_date", "change_departure_time",
51
+ "add_itinerary", "rebuild_itinerary", "change_cabin",
52
+ "change_fare_terms", "add_hotel_stay", "change_room",
53
+ "change_provider", "change_location", "change_date", "change_time",
54
+ "change_party_size", "correct_reservation_name", "reduce_deposit",
55
+ "change_cancellation_terms",
56
+ }
57
+
58
+ # Default purchase-tool names -> check kind. Override per guard.
59
+ DEFAULT_TOOL_KINDS: Dict[str, str] = {
60
+ "place_order": "cart",
61
+ "checkout": "cart",
62
+ "create_order": "cart",
63
+ "capture_payment": "cart",
64
+ "pay": "cart",
65
+ "purchase": "cart",
66
+ "buy": "cart",
67
+ "create_booking": "booking",
68
+ "book_flight": "booking",
69
+ "book_hotel": "booking",
70
+ "book": "booking",
71
+ "make_reservation": "reservation",
72
+ "create_reservation": "reservation",
73
+ "reserve_table": "reservation",
74
+ "reserve": "reservation",
75
+ }
76
+
77
+ _CHECK_METHOD = {
78
+ "cart": "check_cart",
79
+ "booking": "check_booking",
80
+ "reservation": "check_reservation",
81
+ }
82
+ _PAYLOAD_KEYS = {"cart": "cart", "booking": "booking", "reservation": "reservation"}
83
+
84
+
85
+ def route_verdict(response: Mapping[str, Any]) -> Tuple[str, Dict[str, Any]]:
86
+ """Return (decision, detail); decision in {proceed, fix, escalate}.
87
+
88
+ 'fix' is returned only when the verdict is BLOCK and every
89
+ correction is a known action. Anything else non-MATCH escalates.
90
+ """
91
+ verdict = (response or {}).get("verdict")
92
+ if verdict == "MATCH":
93
+ return "proceed", {"record_id": response.get("record_id")}
94
+ if verdict == "BLOCK":
95
+ corrections = response.get("corrections") or []
96
+ if corrections and all(
97
+ isinstance(c, Mapping) and c.get("action") in KNOWN_ACTIONS
98
+ for c in corrections
99
+ ):
100
+ return "fix", {
101
+ "corrections": list(corrections),
102
+ "record_id": response.get("record_id"),
103
+ "max_attempts": response.get("max_attempts"),
104
+ }
105
+ return "escalate", {
106
+ "reason": "BLOCK without usable corrections",
107
+ "record_id": response.get("record_id"),
108
+ }
109
+ return "escalate", {
110
+ "reason": f"verdict {verdict} needs the buyer",
111
+ "record_id": response.get("record_id"),
112
+ }
113
+
114
+
115
+ def _tool_name(tool: Any) -> str:
116
+ if isinstance(tool, str):
117
+ return tool
118
+ return getattr(tool, "name", "") or str(tool)
119
+
120
+
121
+ class ProveCartGuard:
122
+ """Framework-agnostic core; the ADK adapters below call it."""
123
+
124
+ def __init__(
125
+ self,
126
+ client: Any,
127
+ tool_kinds: Optional[Mapping[str, str]] = None,
128
+ extractor: Optional[Callable[[str, Dict[str, Any]], Dict[str, Any]]] = None,
129
+ ):
130
+ if client is None:
131
+ raise ValueError("ProveCartGuard requires an injected client")
132
+ self.client = client
133
+ self.tool_kinds = dict(tool_kinds) if tool_kinds is not None else dict(DEFAULT_TOOL_KINDS)
134
+ self.extractor = extractor
135
+
136
+ def kind_for(self, tool: Any) -> Optional[str]:
137
+ return self.tool_kinds.get(_tool_name(tool))
138
+
139
+ def extract(self, kind: str, args: Dict[str, Any]) -> Dict[str, Any]:
140
+ if self.extractor is not None:
141
+ return dict(self.extractor(kind, dict(args)))
142
+ nested = args.get(_PAYLOAD_KEYS[kind])
143
+ if isinstance(nested, Mapping):
144
+ payload = dict(nested)
145
+ # Mandate / linkage fields may sit beside the nested payload.
146
+ for key in ("mandate_id", "previous_record_id"):
147
+ if key in args and key not in payload:
148
+ payload[key] = args[key]
149
+ return payload
150
+ return dict(args)
151
+
152
+ def check(self, kind: str, payload: Dict[str, Any]) -> Dict[str, Any]:
153
+ method_name = _CHECK_METHOD[kind]
154
+ method = getattr(self.client, method_name, None)
155
+ if callable(method):
156
+ return dict(method(**payload))
157
+ if kind == "cart" and callable(getattr(self.client, "verify_cart", None)):
158
+ return dict(self.client.verify_cart(**payload))
159
+ raise TypeError(f"client does not provide {method_name}()")
160
+
161
+ def route(self, response: Dict[str, Any]) -> Tuple[str, Dict[str, Any]]:
162
+ client_route = getattr(self.client, "route_verdict", None)
163
+ if callable(client_route):
164
+ decision, detail = client_route(response)
165
+ return str(decision), dict(detail or {})
166
+ return route_verdict(response)
167
+
168
+ def before_tool(self, tool: Any, args: Optional[Dict[str, Any]]) -> Optional[Dict[str, Any]]:
169
+ """Return None to let the tool run, or a refusal dict to skip it."""
170
+ kind = self.kind_for(tool)
171
+ if kind is None:
172
+ return None
173
+ payload = self.extract(kind, dict(args or {}))
174
+ try:
175
+ response = self.check(kind, payload)
176
+ except Exception as exc: # a failed check never lets a purchase through
177
+ return self._refusal(kind, tool, None, "escalate",
178
+ {"reason": f"check failed: {exc}"})
179
+ decision, detail = self.route(response)
180
+ if decision == "proceed":
181
+ return None
182
+ return self._refusal(kind, tool, response, decision, detail)
183
+
184
+ @staticmethod
185
+ def _refusal(kind, tool, response, decision, detail):
186
+ response = response or {}
187
+ return {
188
+ "provecart_blocked": True,
189
+ "provecart_decision": decision,
190
+ "verdict": response.get("verdict"),
191
+ "check_kind": kind,
192
+ "tool": _tool_name(tool),
193
+ "corrections": response.get("corrections") or detail.get("corrections") or [],
194
+ "record_id": response.get("record_id") or detail.get("record_id"),
195
+ "max_attempts": response.get("max_attempts") or detail.get("max_attempts"),
196
+ "detail": detail,
197
+ "error": (
198
+ "ProveCart check did not return MATCH, so the purchase "
199
+ "tool was not run. On 'fix', apply only the listed "
200
+ "corrections inside the customer's existing authorization "
201
+ "and re-check with previous_record_id. On 'escalate', "
202
+ "stop and ask the customer."
203
+ ),
204
+ }
205
+
206
+
207
+ def make_before_tool_callback(guard: ProveCartGuard):
208
+ """Build an ADK agent-level ``before_tool_callback``.
209
+
210
+ Documented ADK signature:
211
+ ``(tool, args, tool_context) -> Optional[dict]`` — returning None
212
+ lets the tool run; returning a dict skips the tool and that dict
213
+ becomes the tool result.
214
+ """
215
+ def before_tool_callback(tool=None, args=None, tool_context=None, **_kw):
216
+ return guard.before_tool(tool, args or {})
217
+
218
+ return before_tool_callback
219
+
220
+
221
+ try: # Real plugin base when google-adk is installed; stub-safe otherwise.
222
+ from google.adk.plugins.base_plugin import BasePlugin as _BasePlugin
223
+ _HAS_ADK = True
224
+ except Exception: # pragma: no cover - exercised in stubbed environments
225
+ _BasePlugin = object # type: ignore
226
+ _HAS_ADK = False
227
+
228
+
229
+ class ProveCartPlugin(_BasePlugin): # type: ignore[misc]
230
+ """ADK plugin form. Register once on the Runner / App.
231
+
232
+ Plugin signature in current ADK:
233
+ ``async before_tool_callback(*, tool, tool_args, tool_context)``
234
+ returning None to proceed or a dict to skip the tool. Older /
235
+ alternate callers may pass ``args`` instead of ``tool_args``;
236
+ both are accepted.
237
+ """
238
+
239
+ def __init__(self, guard: Optional[ProveCartGuard] = None, *,
240
+ client: Any = None, **guard_kwargs):
241
+ if _HAS_ADK:
242
+ try:
243
+ super().__init__(name="provecart")
244
+ except TypeError:
245
+ super().__init__()
246
+ self.guard = guard or ProveCartGuard(client, **guard_kwargs)
247
+
248
+ async def before_tool_callback(self, *, tool=None, tool_args=None,
249
+ args=None, tool_context=None, **_kw):
250
+ payload = tool_args if tool_args is not None else args
251
+ return self.guard.before_tool(tool, payload or {})
252
+
253
+
254
+ class FakeProveCartClient:
255
+ """Offline fake for examples and tests. No network, ever.
256
+
257
+ Pass a fixed response dict, or a callable payload -> response.
258
+ Records every call in ``.calls`` as (kind, payload).
259
+ """
260
+
261
+ def __init__(self, response=None):
262
+ self._response = response if response is not None else {
263
+ "verdict": "MATCH", "corrections": [], "record_id": "rec_mock_1",
264
+ "max_attempts": 3,
265
+ }
266
+ self.calls = []
267
+
268
+ def _handle(self, kind, **payload):
269
+ self.calls.append((kind, payload))
270
+ if callable(self._response):
271
+ return dict(self._response(payload))
272
+ return dict(self._response)
273
+
274
+ def check_cart(self, **payload):
275
+ return self._handle("cart", **payload)
276
+
277
+ def check_booking(self, **payload):
278
+ return self._handle("booking", **payload)
279
+
280
+ def check_reservation(self, **payload):
281
+ return self._handle("reservation", **payload)
282
+
283
+ # Deliberately no route_verdict: exercises this module's routing.
@@ -0,0 +1,138 @@
1
+ Metadata-Version: 2.4
2
+ Name: provecart-adk
3
+ Version: 0.1.0
4
+ Summary: ProveCart pre-action hook for Google ADK: check an agent's order, booking or reservation against the customer's signed mandate before a purchase tool runs.
5
+ License: MIT
6
+ Project-URL: Homepage, https://provecart.com
7
+ Project-URL: Repository, https://github.com/provecart/provecart
8
+ Keywords: ai-agents,agent-commerce,google-adk,provecart,pre-payment-verification
9
+ Classifier: License :: OSI Approved :: MIT License
10
+ Classifier: Programming Language :: Python :: 3
11
+ Classifier: Topic :: Software Development :: Libraries
12
+ Requires-Python: >=3.9
13
+ Description-Content-Type: text/markdown
14
+ License-File: LICENSE
15
+ Provides-Extra: adk
16
+ Requires-Dist: google-adk; extra == "adk"
17
+ Provides-Extra: test
18
+ Requires-Dist: pytest; extra == "test"
19
+ Dynamic: license-file
20
+
21
+ # provecart-adk
22
+
23
+ A pre-action hook for Google ADK agents, from ProveCart.
24
+
25
+ An agent that buys for someone needs one check in the right place:
26
+ after it has built the order, and before any money moves. This
27
+ package puts that check on ADK's tool boundary. When a purchase
28
+ tool is about to run, the hook reads the pending order, booking or
29
+ reservation out of the tool arguments, compares it with the
30
+ customer's signed mandate, and only then decides whether the tool
31
+ call goes ahead.
32
+
33
+ ```bash
34
+ pip install provecart-adk
35
+ pip install "provecart-adk[adk]" # also installs google-adk
36
+ ```
37
+
38
+ ## How it works
39
+
40
+ ProveCart returns one of three verdicts:
41
+
42
+ - **MATCH** — the tool runs. The callback returns `None`, which is
43
+ ADK's signal to proceed as normal.
44
+ - **BLOCK** — the tool does not run. ADK's documented refusal path
45
+ applies: a `before_tool_callback` that returns a dict skips the
46
+ tool call and that dict becomes the tool result. The dict carries
47
+ the verdict, the record id and the corrections, so the agent can
48
+ apply a fix that stays inside the customer's existing
49
+ authorization and check again.
50
+ - **REVIEW** — the tool does not run, and the refusal dict tells
51
+ the agent to stop and ask the customer. A review is never turned
52
+ into a guess.
53
+
54
+ A check that fails, a verdict that is not recognized, and a BLOCK
55
+ whose corrections use an unknown action all escalate the same way.
56
+ Nothing falls through to the purchase tool by accident.
57
+
58
+ ## Use it two ways
59
+
60
+ As an agent callback:
61
+
62
+ ```python
63
+ from provecart_adk import ProveCartGuard, make_before_tool_callback
64
+
65
+ guard = ProveCartGuard(client=my_provecart_client)
66
+ agent = LlmAgent(
67
+ name="shopper",
68
+ tools=[place_order],
69
+ before_tool_callback=make_before_tool_callback(guard),
70
+ )
71
+ ```
72
+
73
+ Or as a plugin, registered once so it covers every agent and tool
74
+ in the app:
75
+
76
+ ```python
77
+ from provecart_adk import ProveCartPlugin
78
+
79
+ plugin = ProveCartPlugin(client=my_provecart_client)
80
+ # Runner(..., plugins=[plugin]) or App(..., plugins=[plugin])
81
+ ```
82
+
83
+ ## The client is yours to inject
84
+
85
+ This package does not hard-wire an HTTP client. Pass any object
86
+ with these methods, each returning a dict with `verdict`,
87
+ `corrections`, `record_id` and `max_attempts`:
88
+
89
+ - `check_cart(**payload)`
90
+ - `check_booking(**payload)`
91
+ - `check_reservation(**payload)`
92
+
93
+ If that object also provides
94
+ `route_verdict(response) -> (decision, detail)`, with decision one
95
+ of `proceed`, `fix` or `escalate`, the hook uses it. Otherwise the
96
+ routing built into this package applies: MATCH proceeds; BLOCK
97
+ becomes `fix` only when every correction is a known action; BLOCK
98
+ without usable corrections, REVIEW and anything unknown escalate.
99
+ A client exposing only `verify_cart` is adapted for cart checks.
100
+
101
+ ## Which tools are watched
102
+
103
+ By default the guard watches common purchase names — `place_order`,
104
+ `checkout`, `capture_payment` and friends for carts; `book_flight`,
105
+ `create_booking` for bookings; `make_reservation`, `reserve_table`
106
+ for reservations — and ignores every other tool without calling the
107
+ client. Pass your own map to watch exactly your tool names:
108
+
109
+ ```python
110
+ guard = ProveCartGuard(
111
+ client=my_provecart_client,
112
+ tool_kinds={"submit_payment": "cart", "hold_room": "booking"},
113
+ )
114
+ ```
115
+
116
+ Payloads are taken from the tool arguments as-is, or from a nested
117
+ `cart` / `booking` / `reservation` dict when the tool wraps its
118
+ order that way. Pass an `extractor(kind, args) -> dict` callable if
119
+ your tools shape their arguments differently.
120
+
121
+ ## Offline example and tests
122
+
123
+ ```bash
124
+ python examples/mock_example.py # MOCK mode, injected fake client
125
+ python -m pytest tests/ -q # offline; ADK is stubbed if absent
126
+ ```
127
+
128
+ The example defaults to mock mode and makes no network calls on
129
+ import or on a normal run. Live use is an explicit opt-in: build
130
+ your real client and inject it, as the example marks.
131
+
132
+ ## Notes
133
+
134
+ Published by ProveCart, a verification service for agent commerce
135
+ based in the Bay Area. Patent pending.
136
+
137
+ Docs: https://provecart.com/portal/docs ·
138
+ Source: https://github.com/provecart/provecart/tree/main/packages/provecart-adk
@@ -0,0 +1,10 @@
1
+ LICENSE
2
+ README.md
3
+ pyproject.toml
4
+ src/provecart_adk/__init__.py
5
+ src/provecart_adk.egg-info/PKG-INFO
6
+ src/provecart_adk.egg-info/SOURCES.txt
7
+ src/provecart_adk.egg-info/dependency_links.txt
8
+ src/provecart_adk.egg-info/requires.txt
9
+ src/provecart_adk.egg-info/top_level.txt
10
+ tests/test_provecart_adk.py
@@ -0,0 +1,6 @@
1
+
2
+ [adk]
3
+ google-adk
4
+
5
+ [test]
6
+ pytest
@@ -0,0 +1 @@
1
+ provecart_adk
@@ -0,0 +1,137 @@
1
+ """Offline tests for provecart-adk. No network; ADK stubbed if absent."""
2
+ import asyncio
3
+ import sys
4
+ import types
5
+
6
+ # Stub google.adk plugin base if the real package is not installed,
7
+ # so the plugin class can be imported and exercised offline.
8
+ try:
9
+ import google.adk.plugins.base_plugin # noqa: F401
10
+ HAS_ADK = True
11
+ except Exception:
12
+ HAS_ADK = False
13
+ google = types.ModuleType("google")
14
+ adk = types.ModuleType("google.adk")
15
+ plugins = types.ModuleType("google.adk.plugins")
16
+ base_plugin = types.ModuleType("google.adk.plugins.base_plugin")
17
+
18
+ class BasePlugin:
19
+ def __init__(self, name=None):
20
+ self.name = name
21
+
22
+ base_plugin.BasePlugin = BasePlugin
23
+ plugins.base_plugin = base_plugin
24
+ adk.plugins = plugins
25
+ google.adk = adk
26
+ sys.modules.setdefault("google", google)
27
+ sys.modules["google.adk"] = adk
28
+ sys.modules["google.adk.plugins"] = plugins
29
+ sys.modules["google.adk.plugins.base_plugin"] = base_plugin
30
+
31
+ sys.path.insert(0, "src")
32
+
33
+ from provecart_adk import ( # noqa: E402
34
+ FakeProveCartClient, ProveCartGuard, ProveCartPlugin,
35
+ make_before_tool_callback, route_verdict,
36
+ )
37
+
38
+
39
+ class Tool:
40
+ def __init__(self, name):
41
+ self.name = name
42
+
43
+
44
+ CART_ARGS = {"mandate_id": "m1", "merchant": {"name": "Shop"},
45
+ "items": [{"name": "Chair"}], "total_price": 10}
46
+
47
+
48
+ def test_route_match_proceeds():
49
+ assert route_verdict({"verdict": "MATCH", "record_id": "r1"})[0] == "proceed"
50
+
51
+
52
+ def test_route_block_known_fix():
53
+ d, detail = route_verdict({"verdict": "BLOCK", "record_id": "r",
54
+ "corrections": [{"action": "remove_item"}],
55
+ "max_attempts": 3})
56
+ assert d == "fix" and detail["max_attempts"] == 3
57
+
58
+
59
+ def test_route_block_unknown_escalates():
60
+ d, _ = route_verdict({"verdict": "BLOCK",
61
+ "corrections": [{"action": "widen_budget"}]})
62
+ assert d == "escalate"
63
+
64
+
65
+ def test_route_review_and_unknown_escalate():
66
+ assert route_verdict({"verdict": "REVIEW"})[0] == "escalate"
67
+ assert route_verdict({"verdict": "???"})[0] == "escalate"
68
+ assert route_verdict({})[0] == "escalate"
69
+
70
+
71
+ def test_non_purchase_tool_passes_without_check():
72
+ fake = FakeProveCartClient()
73
+ guard = ProveCartGuard(fake)
74
+ assert guard.before_tool(Tool("search_catalog"), {"q": "x"}) is None
75
+ assert fake.calls == []
76
+
77
+
78
+ def test_match_lets_tool_run():
79
+ fake = FakeProveCartClient()
80
+ cb = make_before_tool_callback(ProveCartGuard(fake))
81
+ assert cb(tool=Tool("place_order"), args=CART_ARGS, tool_context=None) is None
82
+ assert fake.calls and fake.calls[0][0] == "cart"
83
+
84
+
85
+ def test_block_returns_refusal_dict_with_corrections():
86
+ fake = FakeProveCartClient({"verdict": "BLOCK", "record_id": "r9",
87
+ "max_attempts": 2,
88
+ "corrections": [{"action": "reduce_quantity"}]})
89
+ cb = make_before_tool_callback(ProveCartGuard(fake))
90
+ out = cb(tool=Tool("checkout"), args=CART_ARGS, tool_context=None)
91
+ assert out["provecart_blocked"] is True
92
+ assert out["provecart_decision"] == "fix"
93
+ assert out["corrections"] == [{"action": "reduce_quantity"}]
94
+ assert out["record_id"] == "r9"
95
+
96
+
97
+ def test_review_escalates_and_booking_and_reservation_kinds():
98
+ fake = FakeProveCartClient({"verdict": "REVIEW", "record_id": "r2",
99
+ "corrections": []})
100
+ guard = ProveCartGuard(fake)
101
+ out = guard.before_tool(Tool("book_flight"), {"booking": {"mandate_id": "m"}})
102
+ assert out["provecart_decision"] == "escalate" and out["check_kind"] == "booking"
103
+ out = guard.before_tool(Tool("reserve_table"), {"mandate_id": "m"})
104
+ assert out["check_kind"] == "reservation"
105
+ assert [c[0] for c in fake.calls] == ["booking", "reservation"]
106
+
107
+
108
+ def test_check_failure_escalates_not_proceeds():
109
+ class Boom:
110
+ def check_cart(self, **kw):
111
+ raise RuntimeError("down")
112
+ guard = ProveCartGuard(Boom())
113
+ out = guard.before_tool(Tool("place_order"), CART_ARGS)
114
+ assert out["provecart_decision"] == "escalate"
115
+
116
+
117
+ def test_client_route_verdict_is_preferred():
118
+ class Own:
119
+ def check_cart(self, **kw):
120
+ return {"verdict": "MATCH", "record_id": "x"}
121
+ def route_verdict(self, response):
122
+ return "escalate", {"reason": "house rule"}
123
+ guard = ProveCartGuard(Own())
124
+ out = guard.before_tool(Tool("place_order"), CART_ARGS)
125
+ assert out["provecart_decision"] == "escalate"
126
+
127
+
128
+ def test_plugin_callback_signature_both_arg_names():
129
+ fake = FakeProveCartClient()
130
+ plugin = ProveCartPlugin(client=fake)
131
+ run = asyncio.get_event_loop().run_until_complete
132
+ assert run(plugin.before_tool_callback(
133
+ tool=Tool("place_order"), tool_args=CART_ARGS, tool_context=None)) is None
134
+ assert run(plugin.before_tool_callback(
135
+ tool=Tool("place_order"), args=CART_ARGS, tool_context=None)) is None
136
+ assert len(fake.calls) == 2
137
+ print("HAS_REAL_ADK:", HAS_ADK)