77import logging
88from collections .abc import Awaitable , Callable
99from dataclasses import dataclass
10- from typing import Any , cast
10+ from typing import Any , Literal , cast
1111
1212from 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+
97113def _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
0 commit comments