From 3efb648cb07aea5827aca5ce8e5c104e21d867b7 Mon Sep 17 00:00:00 2001 From: vvillait88 Date: Wed, 12 Aug 2026 05:05:13 -0700 Subject: [PATCH 1/2] Surface the pairwise operator handle through the Gate Python half of the same capability as the node library, kept level with it. Merchants keying durable state on identity (prepaid balances first) need a value that outlives a credential: an opc_ lives 24h and rotates silently off a 90-day refresh, so state keyed on the token instance is stranded daily and revoking a leaked token would forfeit the balance it held. It rides the /v1/assess response the gate already fetches, so reading it is a synchronous read like get_signer_verdict, costs no second round trip on a merchant's hot path, and meters nothing extra. project_operator_handle in identity/core.py is the single derivation both the six adapters and Checkout call. Written twice it would drift, and the failure is silent: one spelling's handle misses the other's rows and a buyer quietly grows a second balance. Stashed BEFORE the allow/deny branch so it is available on denials too: this is identity rather than a verdict, and a merchant recording a denial against the buyer needs it where its handler never runs. --- CLAUDE.md | 12 ++ agentscore_commerce/checkout.py | 16 +++ agentscore_commerce/identity/aiohttp.py | 27 +++++ agentscore_commerce/identity/core.py | 38 ++++++ agentscore_commerce/identity/django.py | 27 +++++ agentscore_commerce/identity/fastapi.py | 27 +++++ agentscore_commerce/identity/flask.py | 35 ++++++ agentscore_commerce/identity/middleware.py | 27 +++++ agentscore_commerce/identity/sanic.py | 28 +++++ tests/test_operator_handle.py | 135 +++++++++++++++++++++ 10 files changed, 372 insertions(+) create mode 100644 tests/test_operator_handle.py diff --git a/CLAUDE.md b/CLAUDE.md index 8b49e89..2e093f6 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -87,6 +87,18 @@ Two identity types: wallet (`X-Wallet-Address`) and operator-token (`X-Operator- `get_signer_verdict(request)` (per-adapter) returns the cached `signer_match` + `signer_sanctions` verdicts the gate composed on its primary `/v1/assess` call (single round trip; merchants build a 403 with `build_signer_mismatch_body(result=verdict.signer_match)` when `kind != "pass"`). +### Operator handle: what durable merchant state keys on + +`get_operator_handle(request)` (per-adapter; Flask takes no argument and reads `g`; `ctx.operator_handle` inside `Checkout` hooks) returns the stable pairwise `oph_...` handle for the ACCOUNT behind the request's operator token. + +**Key state on this, never on the token.** An `opc_` lives 24h and rotates silently off a 90-day refresh, so anything keyed on the token instance is stranded daily, and revoking a leaked token would forfeit a prepaid balance. The handle derives from the account, so rotation, expiry and revocation are all free. It is pairwise per consuming merchant, so the same buyer presents an unrelated handle at every store and handles never correlate across them. + +It rides the gate's existing `/v1/assess` response, so reading it costs **no extra round trip and nothing extra against the merchant's quota**. That is why the accessor is synchronous like `get_signer_verdict` rather than doing a lookup of its own. + +Returns `None` when the gate did not run, on wallet-authenticated paths (there is no operator token to resolve), or when the API has no handle salt configured. Available on **denied** requests too, so a merchant recording a denial against a buyer can still key it. It carries no compliance meaning: a registration-only (`sign_in`) credential resolves exactly like a KYC-backed one, so read the decision fields for policy. + +Anything that is not a well-formed `oph_` string reads as absent rather than being passed through, so a half-configured API can never hand a merchant a value it would write balance rows against. `project_operator_handle(raw)` in `identity/core.py` is the single derivation both the adapters and `Checkout` call. + Captured wallets: `capture_wallet(...)` is fire-and-forget. Reads `operator_token` stashed during gating and POSTs to `/v1/credentials/wallets`. No-ops for wallet-authenticated requests. Wallet-signer-match + signer-sanctions: the gate adapter calls `extract_payment_signer(x402_header)` pre-evaluate and passes `signer={address, network}` to the SDK's `assess`. The API returns both `signer_match` (wallet-binding) and `signer_sanctions` (OFAC SDN wallet-address) on the same response; commerce caches the raw body alongside the projected verdicts so `get_signer_verdict` is a pure cache read. **Wallet-OFAC SDN enforcement on the `signer` block is unconditional** whenever a signer is present — no `policy.require_sanctions_clear` opt-in required. An SDN hit (or `sanctions_check_unavailable`) flips `decision -> deny` and the gate returns 403 before the handler runs. diff --git a/agentscore_commerce/checkout.py b/agentscore_commerce/checkout.py index 10fe9e6..02cefa4 100644 --- a/agentscore_commerce/checkout.py +++ b/agentscore_commerce/checkout.py @@ -339,6 +339,17 @@ class CheckoutContext: """Merchant-supplied per-request state, populated by :attr:`Checkout.pre_validate`. Other hooks read from here (e.g. ``ctx.state["product"]`` after pre_validate resolved it). Stays empty when no pre_validate is configured.""" + operator_handle: str | None = None + """Stable pairwise handle for the ACCOUNT behind this request's operator token. + + Set by Checkout's internal gate from the same ``/v1/assess`` response it already + fetched, so it costs no extra round trip and nothing extra against quota. + + This is what durable merchant state should key on, prepaid balances above all: it + survives the token rotating, expiring or being revoked, whereas state keyed on the token + instance is stranded every time one rotates. ``None`` when no gate is configured, on + wallet or AIT paths, on anonymous discovery legs, or when the API has no handle salt. + """ capture_wallet: Callable[..., Any] | None = None """Capture the signer wallet under the operator credential the gate resolved for this request. Set by Checkout's internal gate after a successful allow when @@ -2188,6 +2199,11 @@ async def _run_gate(self, ctx: CheckoutContext) -> CheckoutResult | None: if signer_denial is not None: return signer_denial + # The pairwise account handle rides the same assess response the gate just used. + from agentscore_commerce.identity.core import project_operator_handle + + ctx.operator_handle = project_operator_handle(ctx.request.assess) + # Stash ctx.capture_wallet so on_settled can bind the signer wallet to # the operator credential without needing a framework-specific context. # No-op when the request was wallet-authenticated (no operator_token). diff --git a/agentscore_commerce/identity/aiohttp.py b/agentscore_commerce/identity/aiohttp.py index 18e6b77..d62846a 100644 --- a/agentscore_commerce/identity/aiohttp.py +++ b/agentscore_commerce/identity/aiohttp.py @@ -266,6 +266,13 @@ async def _agentscore_middleware( return await handler(request) return _deny_response(request, DenialReason(code="api_error")) + # The pairwise account handle rides this same assess response. Stash it BEFORE the + # allow/deny branch: it is identity rather than a verdict, so a merchant recording a + # denial against the buyer needs it on the path where its handler never runs. + _handle_state = request.get(GATE_STATE_KEY) + if isinstance(_handle_state, dict): + _handle_state["operator_handle"] = client.project_operator_handle(result.raw) + if result.allow: request["agentscore"] = result.raw state = request.get(GATE_STATE_KEY) @@ -462,3 +469,23 @@ def conditional_aip_gate_middleware(**kwargs: Any) -> Any: kwargs["condition"] = has_agent_identity_header return aip_gate_middleware(**kwargs) + + +def get_operator_handle(request: web.Request) -> str | None: + """Read the stable pairwise operator handle for the account behind this request's token. + + This is what durable merchant state (prepaid balances first) should key on: it survives + the token rotating, expiring or being revoked, whereas anything keyed on the token + instance is stranded every time one rotates. + + Synchronous and free. The handle rides the gate's existing ``/v1/assess`` call, so + reading it costs no extra round trip and nothing extra against the merchant's quota. + + Returns ``None`` when the gate did not run, when no operator token was presented (wallet + or AIT paths), or when the API has no handle salt configured. Available on denied + requests too, so a merchant recording a denial against a buyer can still key it. + """ + state = request.get(GATE_STATE_KEY) + if not isinstance(state, dict): + return None + return state.get("operator_handle") diff --git a/agentscore_commerce/identity/core.py b/agentscore_commerce/identity/core.py index 8f33a95..6cc495d 100644 --- a/agentscore_commerce/identity/core.py +++ b/agentscore_commerce/identity/core.py @@ -57,6 +57,26 @@ DEFAULT_CACHE_SECONDS = 300 +def project_operator_handle(raw: dict[str, Any] | None) -> str | None: + """Project the stable pairwise operator handle from a raw ``/v1/assess`` response. + + Pure, and the single derivation both the gate adapters and ``Checkout`` call: written + twice it would drift, and the failure is silent, since one spelling's handle simply + misses the other's rows and a buyer quietly grows a second balance. + + Narrowed rather than cast. The field is absent whenever the request carried no operator + token, and absent is also what the API emits when its handle salt is unconfigured, so + anything that is not a usable ``oph_`` string must read as "no handle" instead of + becoming a state key. + """ + if not isinstance(raw, dict): + return None + value = raw.get("operator_handle") + if isinstance(value, str) and value.startswith("oph_"): + return value + return None + + class AgentScoreCore: """Shared client for calling the AgentScore assess API. @@ -525,6 +545,24 @@ def _stash_signer_raw( ): self._last_signer_raw[normalize_address(address)] = raw + def project_operator_handle(self, raw: dict[str, Any] | None) -> str | None: + """Project the stable pairwise operator handle from a raw ``/v1/assess`` response. + + The handle is the identity durable merchant state (prepaid balances first) should + key on, because it survives the token rotating, expiring or being revoked: an + ``opc_`` lives 24h and rotates silently off a 90-day refresh, so anything keyed on + the token instance is stranded daily. + + It rides the assess response the gate already fetches, so reading it costs no extra + round trip and nothing extra against the merchant's quota. + + Narrowed rather than cast: the field is absent whenever the request carried no + operator token, and absent is also what the API emits when its handle salt is + unconfigured. Anything that is not a usable ``oph_`` string must read as "no handle" + instead of becoming a state key. + """ + return project_operator_handle(raw) + def project_signer_verdict(self, raw: dict[str, Any] | None, claimed_address: str) -> SignerVerdict | None: """Project ``signer_match`` + ``signer_sanctions`` from a SPECIFIC raw assess response. diff --git a/agentscore_commerce/identity/django.py b/agentscore_commerce/identity/django.py index d023433..67cdede 100644 --- a/agentscore_commerce/identity/django.py +++ b/agentscore_commerce/identity/django.py @@ -247,6 +247,13 @@ def __call__(self, request: HttpRequest) -> Any: return self.get_response(request) return self._on_denied(request, DenialReason(code="api_error")) + # The pairwise account handle rides this same assess response. Stash it BEFORE the + # allow/deny branch: it is identity rather than a verdict, so a merchant recording a + # denial against the buyer needs it on the path where its handler never runs. + _handle_state = getattr(request, "_agentscore_gate", None) + if isinstance(_handle_state, dict): + _handle_state["operator_handle"] = self._client.project_operator_handle(result.raw) + if result.allow: setattr(request, "agentscore", result.raw) # noqa: B010 — dynamic attribute attach on HttpRequest state = getattr(request, "_agentscore_gate", None) @@ -472,3 +479,23 @@ def __init__(self, get_response: Any) -> None: super().__init__(get_response) self._condition = lambda request: has_agent_identity_header_parts(dict(request.headers)) + + +def get_operator_handle(request: HttpRequest) -> str | None: + """Read the stable pairwise operator handle for the account behind this request's token. + + This is what durable merchant state (prepaid balances first) should key on: it survives + the token rotating, expiring or being revoked, whereas anything keyed on the token + instance is stranded every time one rotates. + + Synchronous and free. The handle rides the gate's existing ``/v1/assess`` call, so + reading it costs no extra round trip and nothing extra against the merchant's quota. + + Returns ``None`` when the gate did not run, when no operator token was presented (wallet + or AIT paths), or when the API has no handle salt configured. Available on denied + requests too, so a merchant recording a denial against a buyer can still key it. + """ + state = getattr(request, "_agentscore_gate", None) + if not isinstance(state, dict): + return None + return state.get("operator_handle") diff --git a/agentscore_commerce/identity/fastapi.py b/agentscore_commerce/identity/fastapi.py index c304375..1cb72d9 100644 --- a/agentscore_commerce/identity/fastapi.py +++ b/agentscore_commerce/identity/fastapi.py @@ -341,6 +341,13 @@ async def __call__(self, request: Request) -> None: return self._deny(request, DenialReason(code="api_error")) + # The pairwise account handle rides this same assess response. Stash it BEFORE the + # allow/deny branch: it is identity rather than a verdict, so a merchant recording a + # denial against the buyer needs it on the path where its handler never runs. + _handle_state = getattr(request.state, GATE_STATE_KEY, None) + if isinstance(_handle_state, dict): + _handle_state["operator_handle"] = self._client.project_operator_handle(result.raw) + if result.allow: setattr(request.state, ASSESS_STATE_KEY, result.raw) state = getattr(request.state, GATE_STATE_KEY, None) @@ -598,3 +605,23 @@ async def checkout(ait = Depends(get_verified_ait)): ... """ return getattr(request.state, AIT_STATE_KEY, None) + + +def get_operator_handle(request: Request) -> str | None: + """Read the stable pairwise operator handle for the account behind this request's token. + + This is what durable merchant state (prepaid balances first) should key on: it survives + the token rotating, expiring or being revoked, whereas anything keyed on the token + instance is stranded every time one rotates. + + Synchronous and free. The handle rides the gate's existing ``/v1/assess`` call, so + reading it costs no extra round trip and nothing extra against the merchant's quota. + + Returns ``None`` when the gate did not run, when no operator token was presented (wallet + or AIT paths), or when the API has no handle salt configured. Available on denied + requests too, so a merchant recording a denial against a buyer can still key it. + """ + state = getattr(request.state, GATE_STATE_KEY, None) + if not isinstance(state, dict): + return None + return state.get("operator_handle") diff --git a/agentscore_commerce/identity/flask.py b/agentscore_commerce/identity/flask.py index c2adabd..6137745 100644 --- a/agentscore_commerce/identity/flask.py +++ b/agentscore_commerce/identity/flask.py @@ -270,6 +270,14 @@ def _agentscore_check() -> Response | tuple[Response, int] | None: try: result = client.check_identity(identity, chain_override, signer=signer_payload) + # The pairwise account handle rides this same assess response. Stash it BEFORE + # the allow/deny branch: it is identity rather than a verdict, so a merchant + # recording a denial against the buyer needs it on the path where its handler + # never runs. + _handle_state = getattr(g, "_agentscore_gate", None) + if isinstance(_handle_state, dict): + _handle_state["operator_handle"] = client.project_operator_handle(result.raw) + if result.allow: g.agentscore = result.raw state = getattr(g, "_agentscore_gate", None) @@ -527,3 +535,30 @@ def _has_header(request: Request) -> bool: kwargs["condition"] = _has_header aip_gate(app, **kwargs) + + +def get_operator_handle() -> str | None: + """Read the stable pairwise operator handle for the account behind this request's token. + + This is what durable merchant state (prepaid balances first) should key on: it survives + the token rotating, expiring or being revoked, whereas anything keyed on the token + instance is stranded every time one rotates. + + Synchronous and free. The handle rides the gate's existing ``/v1/assess`` call, so + reading it costs no extra round trip and nothing extra against the merchant's quota. + + Returns ``None`` when the gate did not run, when no operator token was presented (wallet + or AIT paths), or when the API has no handle salt configured. Available on denied + requests too, so a merchant recording a denial against a buyer can still key it. + """ + from flask import g + + try: + state = getattr(g, "_agentscore_gate", None) + except RuntimeError: + # No application context (called outside a request). Same posture as the sibling + # accessor: absent state reads as no handle, never as an error. + return None + if not isinstance(state, dict): + return None + return state.get("operator_handle") diff --git a/agentscore_commerce/identity/middleware.py b/agentscore_commerce/identity/middleware.py index b4c972c..37c4cb4 100644 --- a/agentscore_commerce/identity/middleware.py +++ b/agentscore_commerce/identity/middleware.py @@ -298,6 +298,13 @@ async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None: await response(scope, receive, send) return + # The pairwise account handle rides this same assess response. Stash it BEFORE the + # allow/deny branch: it is identity rather than a verdict, so a merchant recording a + # denial against the buyer needs it on the path where its handler never runs. + _handle_state = getattr(request.state, GATE_STATE_KEY, None) + if isinstance(_handle_state, dict): + _handle_state["operator_handle"] = self._client.project_operator_handle(result.raw) + if result.allow: scope["state"] = {**scope.get("state", {}), "agentscore": result.raw} state = scope["state"].get(GATE_STATE_KEY) @@ -513,3 +520,23 @@ class ConditionalAipGate(AipGate): def __init__(self, app: ASGIApp, **kwargs: Any) -> None: kwargs["condition"] = has_agent_identity_header super().__init__(app, **kwargs) + + +def get_operator_handle(request: Request) -> str | None: + """Read the stable pairwise operator handle for the account behind this request's token. + + This is what durable merchant state (prepaid balances first) should key on: it survives + the token rotating, expiring or being revoked, whereas anything keyed on the token + instance is stranded every time one rotates. + + Synchronous and free. The handle rides the gate's existing ``/v1/assess`` call, so + reading it costs no extra round trip and nothing extra against the merchant's quota. + + Returns ``None`` when the gate did not run, when no operator token was presented (wallet + or AIT paths), or when the API has no handle salt configured. Available on denied + requests too, so a merchant recording a denial against a buyer can still key it. + """ + state = getattr(request.state, GATE_STATE_KEY, None) + if not isinstance(state, dict): + return None + return state.get("operator_handle") diff --git a/agentscore_commerce/identity/sanic.py b/agentscore_commerce/identity/sanic.py index dfb5292..aa4eef9 100644 --- a/agentscore_commerce/identity/sanic.py +++ b/agentscore_commerce/identity/sanic.py @@ -238,6 +238,14 @@ async def _agentscore_check(request: Request) -> HTTPResponse | None: try: result = await client.acheck_identity(identity, chain_override, signer=signer_payload) + # The pairwise account handle rides this same assess response. Stash it BEFORE + # the allow/deny branch: it is identity rather than a verdict, so a merchant + # recording a denial against the buyer needs it on the path where its handler + # never runs. + _handle_state = getattr(request.ctx, GATE_STATE_ATTR, None) + if isinstance(_handle_state, dict): + _handle_state["operator_handle"] = client.project_operator_handle(result.raw) + if result.allow: request.ctx.agentscore = result.raw state = getattr(request.ctx, GATE_STATE_ATTR, None) @@ -454,3 +462,23 @@ def conditional_aip_gate(app: Sanic, **kwargs: Any) -> None: kwargs["condition"] = has_agent_identity_header aip_gate(app, **kwargs) + + +def get_operator_handle(request: Request) -> str | None: + """Read the stable pairwise operator handle for the account behind this request's token. + + This is what durable merchant state (prepaid balances first) should key on: it survives + the token rotating, expiring or being revoked, whereas anything keyed on the token + instance is stranded every time one rotates. + + Synchronous and free. The handle rides the gate's existing ``/v1/assess`` call, so + reading it costs no extra round trip and nothing extra against the merchant's quota. + + Returns ``None`` when the gate did not run, when no operator token was presented (wallet + or AIT paths), or when the API has no handle salt configured. Available on denied + requests too, so a merchant recording a denial against a buyer can still key it. + """ + state = getattr(request.ctx, GATE_STATE_ATTR, None) + if not isinstance(state, dict): + return None + return state.get("operator_handle") diff --git a/tests/test_operator_handle.py b/tests/test_operator_handle.py new file mode 100644 index 0000000..9540dd0 --- /dev/null +++ b/tests/test_operator_handle.py @@ -0,0 +1,135 @@ +"""The operator handle: the identity durable merchant state keys on. + +The whole reason it exists is that an ``opc_`` token is the WRONG key. It lives 24h and +rotates silently off a 90-day refresh, so anything keyed on the token instance is stranded +daily, and revoking a leaked token would forfeit a prepaid balance. The handle derives from +the account behind the token instead, and is pairwise per merchant. + +It rides the gate's existing ``/v1/assess`` response rather than a lookup of its own, so +these tests pin the projection (which must refuse anything that is not a usable handle) and +the per-adapter read path across all six adapters. +""" + +from __future__ import annotations + +from unittest.mock import MagicMock + +import pytest + +HANDLE = "oph_" + "a" * 40 + + +# --------------------------------------------------------------------------- +# Projection: what counts as a usable handle +# --------------------------------------------------------------------------- + + +def _client(): + from agentscore_commerce.identity.core import AgentScoreCore + + return AgentScoreCore(api_key="as_test_key") + + +def test_projects_a_well_formed_handle() -> None: + assert _client().project_operator_handle({"operator_handle": HANDLE}) == HANDLE + + +@pytest.mark.parametrize( + "raw", + [ + None, + {}, + # Wallet path: no operator token was presented, so there is no account handle. + {"decision": "allow"}, + # An unsalted or half-configured API must never hand back something that merely + # looks usable. Anything that is not an `oph_` string reads as absent, because the + # alternative is a merchant writing balance rows against a junk key. + {"operator_handle": ""}, + {"operator_handle": "not_a_handle"}, + {"operator_handle": 12345}, + {"operator_handle": None}, + ], +) +def test_refuses_anything_that_is_not_a_usable_handle(raw: object) -> None: + assert _client().project_operator_handle(raw) is None # type: ignore[arg-type] + + +# --------------------------------------------------------------------------- +# Per-adapter read path +# --------------------------------------------------------------------------- + + +def test_asgi_reads_the_stashed_handle() -> None: + from agentscore_commerce.identity.middleware import GATE_STATE_KEY, get_operator_handle + + request = MagicMock() + request.state = MagicMock() + setattr(request.state, GATE_STATE_KEY, {"operator_handle": HANDLE}) + assert get_operator_handle(request) == HANDLE + + +def test_asgi_returns_none_without_gate_state() -> None: + from agentscore_commerce.identity.middleware import GATE_STATE_KEY, get_operator_handle + + request = MagicMock() + request.state = MagicMock(spec=[]) + assert get_operator_handle(request) is None + assert GATE_STATE_KEY # key exists for the stash side + + +def test_fastapi_reads_the_stashed_handle() -> None: + from agentscore_commerce.identity.fastapi import GATE_STATE_KEY, get_operator_handle + + request = MagicMock() + request.state = MagicMock() + setattr(request.state, GATE_STATE_KEY, {"operator_handle": HANDLE}) + assert get_operator_handle(request) == HANDLE + + +def test_aiohttp_reads_the_stashed_handle() -> None: + from agentscore_commerce.identity.aiohttp import GATE_STATE_KEY, get_operator_handle + + request = {GATE_STATE_KEY: {"operator_handle": HANDLE}} + assert get_operator_handle(request) == HANDLE # type: ignore[arg-type] + + +def test_sanic_reads_the_stashed_handle() -> None: + from agentscore_commerce.identity.sanic import GATE_STATE_ATTR, get_operator_handle + + request = MagicMock() + request.ctx = MagicMock() + setattr(request.ctx, GATE_STATE_ATTR, {"operator_handle": HANDLE}) + assert get_operator_handle(request) == HANDLE + + +def test_django_reads_the_stashed_handle() -> None: + from agentscore_commerce.identity.django import get_operator_handle + + request = MagicMock(spec=["_agentscore_gate"]) + request._agentscore_gate = {"operator_handle": HANDLE} + assert get_operator_handle(request) == HANDLE + + +def test_django_returns_none_without_gate_state() -> None: + from agentscore_commerce.identity.django import get_operator_handle + + assert get_operator_handle(MagicMock(spec=[])) is None + + +def test_flask_returns_none_outside_an_application_context() -> None: + # Same posture as the sibling signer-verdict accessor: no app context reads as "no + # handle" rather than raising, so a merchant calling it off-request never 500s. + from agentscore_commerce.identity.flask import get_operator_handle + + assert get_operator_handle() is None + + +def test_flask_reads_the_stashed_handle_in_context() -> None: + flask = pytest.importorskip("flask") + + from agentscore_commerce.identity.flask import get_operator_handle + + app = flask.Flask(__name__) + with app.test_request_context("/"): + flask.g._agentscore_gate = {"operator_handle": HANDLE} + assert get_operator_handle() == HANDLE From d1c411a98a9657475adb13fe599195d8fe7bff42 Mon Sep 17 00:00:00 2001 From: vvillait88 Date: Wed, 12 Aug 2026 05:09:53 -0700 Subject: [PATCH 2/2] Bump to 2.6.0 Minor rather than patch: this adds exported surface (get_operator_handle on every adapter, ctx.operator_handle on Checkout, project_operator_handle) rather than only changing behavior. --- pyproject.toml | 2 +- uv.lock | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 67b9158..966afac 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "hatchling.build" [project] name = "agentscore-commerce" -version = "2.5.18" +version = "2.6.0" description = "Agentic commerce SDK for Python: identity middleware (FastAPI, Flask, Django, AIOHTTP, Sanic, ASGI) + payment helpers + 402 builders + discovery + Stripe multichain. The full merchant-side toolkit for AgentScore-powered agentic commerce." readme = "README.md" license = "MIT" diff --git a/uv.lock b/uv.lock index 5322d8b..33ba13f 100644 --- a/uv.lock +++ b/uv.lock @@ -13,7 +13,7 @@ constraints = [{ name = "fastapi", specifier = "!=0.136.3" }] [[package]] name = "agentscore-commerce" -version = "2.5.18" +version = "2.6.0" source = { editable = "." } dependencies = [ { name = "agentscore-py" },