Skip to content

Commit 65dc032

Browse files
vvillait88claude
andcommitted
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 <noreply@anthropic.com>
1 parent 31268a1 commit 65dc032

4 files changed

Lines changed: 48 additions & 10 deletions

File tree

agentscore_commerce/identity/sessions.py

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
import logging
88
from collections.abc import Awaitable, Callable
99
from dataclasses import dataclass
10-
from typing import Any, cast
10+
from typing import Any, Literal, cast
1111

1212
from agentscore import AgentScore, AgentScoreError
1313

@@ -46,6 +46,13 @@ class CreateSessionOnMissing:
4646
base_url: str = "https://api.agentscore.com"
4747
context: str | None = None
4848
product_name: str | None = None
49+
# Session kind sent to POST /v1/sessions. "kyc" (the API default) runs identity
50+
# verification; "sign_in" is registration-only (the buyer signs in with an AgentScore
51+
# account, no identity documents) and mints a sign_in-scoped credential. Use it when the
52+
# gate runs with an EMPTY compliance policy and only needs an account to key state on
53+
# (a prepaid balance, say): a KYC session there asks for documents nothing will check.
54+
# The denial's default error.message follows the kind.
55+
kind: Literal["kyc", "sign_in"] | None = None
4956
# Per-request override of context / product_name. Receives the framework request
5057
# object; returns a dict with optional "context" and/or "product_name" keys.
5158
get_session_options: Callable[[Any], _Hookable] | None = None
@@ -91,12 +98,22 @@ def _resolved_session_options(cfg: CreateSessionOnMissing, dynamic: Any) -> dict
9198
options["context"] = cfg.context
9299
if cfg.product_name is not None:
93100
options["product_name"] = cfg.product_name
101+
if cfg.kind is not None:
102+
options["kind"] = cfg.kind
94103
return _apply_dynamic_options(options, dynamic)
95104

96105

106+
SIGN_IN_REQUIRED_MESSAGE = (
107+
"Sign-in is required to access this resource. Visit verify_url to sign in with an "
108+
"AgentScore account (no identity documents), then poll poll_url for the operator token "
109+
"and retry."
110+
)
111+
112+
97113
def _session_denial_reason(
98114
data: dict[str, Any],
99115
extra: dict[str, Any] | None = None,
116+
kind: Literal["kyc", "sign_in"] | None = None,
100117
) -> DenialReason | None:
101118
# Validate required fields before trusting the response. A misbehaving (or
102119
# mocked-wrong) API could 200 without session_id/poll_secret/verify_url, which
@@ -116,6 +133,9 @@ def _session_denial_reason(
116133
agent_instructions = json.dumps(next_steps) if next_steps else None
117134
return DenialReason(
118135
code="identity_verification_required",
136+
# The per-code default message talks about KYC, which a sign_in session never runs;
137+
# say what this session actually asks for so a merchant's default 403 is not a lie.
138+
message=SIGN_IN_REQUIRED_MESSAGE if kind == "sign_in" else None,
119139
verify_url=data["verify_url"],
120140
session_id=data["session_id"],
121141
poll_secret=data["poll_secret"],
@@ -171,7 +191,7 @@ async def try_create_session_denial_reason(
171191
except Exception as err:
172192
logger.warning("on_before_session hook failed: %s", err)
173193

174-
return _session_denial_reason(data, extra)
194+
return _session_denial_reason(data, extra, cfg.kind)
175195
except Exception:
176196
return None
177197

@@ -219,6 +239,6 @@ def try_create_session_denial_reason_sync(
219239
except Exception as err:
220240
logger.warning("on_before_session hook failed: %s", err)
221241

222-
return _session_denial_reason(data, extra)
242+
return _session_denial_reason(data, extra, cfg.kind)
223243
except Exception:
224244
return None

pyproject.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,15 @@ build-backend = "hatchling.build"
44

55
[project]
66
name = "agentscore-commerce"
7-
version = "2.8.3"
7+
version = "2.9.0"
88
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."
99
readme = "README.md"
1010
license = "MIT"
1111
requires-python = ">=3.11"
1212
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"]
1313
dependencies = [
1414
"httpx>=0.25.0,<1.0.0",
15-
"agentscore-py>=2.6.8",
15+
"agentscore-py>=2.6.9",
1616
]
1717
classifiers = [
1818
"Development Status :: 5 - Production/Stable",

tests/test_sessions.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,24 @@ def test_forwards_context_and_product_name(self):
8181
body = json.loads(route.calls[0].request.content)
8282
assert body["context"] == "purchase_flow"
8383
assert body["product_name"] == "Example Merchant"
84+
assert "kind" not in body
85+
86+
@respx.mock
87+
def test_forwards_kind_and_swaps_the_kyc_message_for_sign_in(self):
88+
route = respx.post(SESSIONS_URL).mock(return_value=httpx.Response(200, json=SESSION_RESPONSE))
89+
reason = try_create_session_denial_reason_sync(
90+
CreateSessionOnMissing(api_key="ask_test", kind="sign_in"),
91+
user_agent="agentscore-commerce/1.0",
92+
)
93+
import json
94+
95+
body = json.loads(route.calls[0].request.content)
96+
assert body["kind"] == "sign_in"
97+
assert reason is not None
98+
assert reason.code == "identity_verification_required"
99+
assert reason.message is not None
100+
assert "sign in with an AgentScore account" in reason.message
101+
assert "KYC" not in reason.message
84102

85103
@respx.mock
86104
def test_omits_context_and_product_name_when_not_provided(self):

uv.lock

Lines changed: 5 additions & 5 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)