From 65dc032e2636a48c431ff8a3cb4d0a92b22f361c Mon Sep 17 00:00:00 2001 From: vvillait88 Date: Tue, 8 Sep 2026 20:10:37 -0400 Subject: [PATCH] Let CreateSessionOnMissing mint a sign_in session A kind field on CreateSessionOnMissing rides through to POST /v1/sessions. A gate running an empty compliance policy that only needs an account to key state on could previously mint only the KYC kind, which asks the buyer for documents nothing checks. For sign_in the denial's default message says what the session actually asks for instead of the KYC copy. Parity with the node library. Takes agentscore-py 2.6.9, which carries the option. Co-Authored-By: Claude Fable 5.1 --- agentscore_commerce/identity/sessions.py | 26 +++++++++++++++++++++--- pyproject.toml | 4 ++-- tests/test_sessions.py | 18 ++++++++++++++++ uv.lock | 10 ++++----- 4 files changed, 48 insertions(+), 10 deletions(-) diff --git a/agentscore_commerce/identity/sessions.py b/agentscore_commerce/identity/sessions.py index f63b5f5..28d5a63 100644 --- a/agentscore_commerce/identity/sessions.py +++ b/agentscore_commerce/identity/sessions.py @@ -7,7 +7,7 @@ import logging from collections.abc import Awaitable, Callable from dataclasses import dataclass -from typing import Any, cast +from typing import Any, Literal, cast from agentscore import AgentScore, AgentScoreError @@ -46,6 +46,13 @@ class CreateSessionOnMissing: base_url: str = "https://api.agentscore.com" context: str | None = None product_name: str | None = None + # Session kind sent to POST /v1/sessions. "kyc" (the API default) runs identity + # verification; "sign_in" is registration-only (the buyer signs in with an AgentScore + # account, no identity documents) and mints a sign_in-scoped credential. Use it when the + # gate runs with an EMPTY compliance policy and only needs an account to key state on + # (a prepaid balance, say): a KYC session there asks for documents nothing will check. + # The denial's default error.message follows the kind. + kind: Literal["kyc", "sign_in"] | None = None # Per-request override of context / product_name. Receives the framework request # object; returns a dict with optional "context" and/or "product_name" keys. get_session_options: Callable[[Any], _Hookable] | None = None @@ -91,12 +98,22 @@ def _resolved_session_options(cfg: CreateSessionOnMissing, dynamic: Any) -> dict options["context"] = cfg.context if cfg.product_name is not None: options["product_name"] = cfg.product_name + if cfg.kind is not None: + options["kind"] = cfg.kind return _apply_dynamic_options(options, dynamic) +SIGN_IN_REQUIRED_MESSAGE = ( + "Sign-in is required to access this resource. Visit verify_url to sign in with an " + "AgentScore account (no identity documents), then poll poll_url for the operator token " + "and retry." +) + + def _session_denial_reason( data: dict[str, Any], extra: dict[str, Any] | None = None, + kind: Literal["kyc", "sign_in"] | None = None, ) -> DenialReason | None: # Validate required fields before trusting the response. A misbehaving (or # mocked-wrong) API could 200 without session_id/poll_secret/verify_url, which @@ -116,6 +133,9 @@ def _session_denial_reason( agent_instructions = json.dumps(next_steps) if next_steps else None return DenialReason( code="identity_verification_required", + # The per-code default message talks about KYC, which a sign_in session never runs; + # say what this session actually asks for so a merchant's default 403 is not a lie. + message=SIGN_IN_REQUIRED_MESSAGE if kind == "sign_in" else None, verify_url=data["verify_url"], session_id=data["session_id"], poll_secret=data["poll_secret"], @@ -171,7 +191,7 @@ async def try_create_session_denial_reason( except Exception as err: logger.warning("on_before_session hook failed: %s", err) - return _session_denial_reason(data, extra) + return _session_denial_reason(data, extra, cfg.kind) except Exception: return None @@ -219,6 +239,6 @@ def try_create_session_denial_reason_sync( except Exception as err: logger.warning("on_before_session hook failed: %s", err) - return _session_denial_reason(data, extra) + return _session_denial_reason(data, extra, cfg.kind) except Exception: return None diff --git a/pyproject.toml b/pyproject.toml index b9ac8fe..1f14c40 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "hatchling.build" [project] name = "agentscore-commerce" -version = "2.8.3" +version = "2.9.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" @@ -12,7 +12,7 @@ requires-python = ">=3.11" keywords = ["agentscore", "agent-commerce", "agentic-payments", "402", "x402", "mpp", "machine-payments-protocol", "fastapi", "starlette", "flask", "django", "aiohttp", "sanic", "middleware", "trust", "reputation", "kyc", "identity", "stripe", "tempo", "solana", "base", "ai-agent"] dependencies = [ "httpx>=0.25.0,<1.0.0", - "agentscore-py>=2.6.8", + "agentscore-py>=2.6.9", ] classifiers = [ "Development Status :: 5 - Production/Stable", diff --git a/tests/test_sessions.py b/tests/test_sessions.py index f1b8e44..6d200ad 100644 --- a/tests/test_sessions.py +++ b/tests/test_sessions.py @@ -81,6 +81,24 @@ def test_forwards_context_and_product_name(self): body = json.loads(route.calls[0].request.content) assert body["context"] == "purchase_flow" assert body["product_name"] == "Example Merchant" + assert "kind" not in body + + @respx.mock + def test_forwards_kind_and_swaps_the_kyc_message_for_sign_in(self): + route = respx.post(SESSIONS_URL).mock(return_value=httpx.Response(200, json=SESSION_RESPONSE)) + reason = try_create_session_denial_reason_sync( + CreateSessionOnMissing(api_key="ask_test", kind="sign_in"), + user_agent="agentscore-commerce/1.0", + ) + import json + + body = json.loads(route.calls[0].request.content) + assert body["kind"] == "sign_in" + assert reason is not None + assert reason.code == "identity_verification_required" + assert reason.message is not None + assert "sign in with an AgentScore account" in reason.message + assert "KYC" not in reason.message @respx.mock def test_omits_context_and_product_name_when_not_provided(self): diff --git a/uv.lock b/uv.lock index 130c97d..d1e1561 100644 --- a/uv.lock +++ b/uv.lock @@ -22,7 +22,7 @@ wheels = [ [[package]] name = "agentscore-commerce" -version = "2.8.3" +version = "2.9.0" source = { editable = "." } dependencies = [ { name = "agentscore-py" }, @@ -96,7 +96,7 @@ dev = [ [package.metadata] requires-dist = [ - { name = "agentscore-py", specifier = ">=2.6.8" }, + { name = "agentscore-py", specifier = ">=2.6.9" }, { name = "aiohttp", marker = "extra == 'aiohttp'", specifier = ">=3.8.0" }, { name = "cdp-sdk", marker = "extra == 'coinbase'", specifier = ">=1.0,<2" }, { name = "django", marker = "extra == 'django'", specifier = ">=4.0" }, @@ -140,14 +140,14 @@ dev = [ [[package]] name = "agentscore-py" -version = "2.6.8" +version = "2.6.9" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "httpx" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/67/3f/10fcd23b00dfc4ed4a701dca48f9b64d5d2c895734c42244dbdede84cb07/agentscore_py-2.6.8.tar.gz", hash = "sha256:e650bc8107c81e02f5ea056f829f4dcc6b4c0feffccd5bcff61448e77b801124", size = 67559, upload-time = "2026-09-05T00:34:10.821Z" } +sdist = { url = "https://files.pythonhosted.org/packages/79/4a/5963efd290f3f8cd2dcd5c06ffa955fccdfa955bf774540e5c640bc68ac3/agentscore_py-2.6.9.tar.gz", hash = "sha256:8606753735560062722a6910191ddc9dfd5673f2e64c0556a2be51513170e3ff", size = 67815, upload-time = "2026-09-09T00:07:49.229Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/40/83/05d9d74b926ea5ee6228e7b08c07e5f2b9c2690f8402c2b1468ea15adb2e/agentscore_py-2.6.8-py3-none-any.whl", hash = "sha256:0f217280fd85d8bc95847d5918e41565596af71e8c0794a1437946d8bed4ab67", size = 22379, upload-time = "2026-09-05T00:34:09.444Z" }, + { url = "https://files.pythonhosted.org/packages/b8/1f/0ee13b46eecb4e27a6c8b0b7c91dd63df7ae31742f8e321228ee8c2eadc8/agentscore_py-2.6.9-py3-none-any.whl", hash = "sha256:9f584515742288f7dec4e0f19b869ae5b89b2e77fba97a97154c83a6d2ca140d", size = 22650, upload-time = "2026-09-09T00:07:47.931Z" }, ] [[package]]