Skip to content

Commit b4140a7

Browse files
vvillait88claude
andcommitted
fix: remove jurisdiction_restricted from FIXABLE_DENIAL_REASONS
The API only emits jurisdiction_restricted AFTER KYC is verified — meaning the user's KYC'd country is in the merchant's blocked list (or absent from the allowed list). Re-doing KYC won't change the country, so it's permanent. Same shape as sanctions_flagged / age_insufficient — should surface contact_support, not bootstrap a doomed verification session. Also flips empty/None reasons to return False (don't bootstrap on unknown deny — default to bare denial). Updates the canonical wallet_not_trusted instructions copy and all six adapter comments to spell out the API-side rationale. Tests updated: jurisdiction_restricted now in the unfixable bucket alongside sanctions/age, empty reasons returns False. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent a44bd4b commit b4140a7

9 files changed

Lines changed: 85 additions & 53 deletions

File tree

agentscore_commerce/identity/_denial.py

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -23,26 +23,35 @@
2323

2424
from agentscore_commerce.identity.types import DenialReason, VerifyWalletSignerResult
2525

26+
# Compliance denial reasons that can be resolved by re-completing KYC. The API emits these
27+
# when KYC is missing/pending/failed; the user can re-verify and retry.
28+
#
29+
# `jurisdiction_restricted` is NOT in this set — the API only emits it AFTER KYC is verified,
30+
# meaning the user's KYC'd country is in the merchant's blocked list (or absent from the
31+
# allowed list). Re-doing KYC won't change the country, so it's permanent. Same shape as
32+
# `sanctions_flagged` and `age_insufficient` — surface contact_support, don't waste a
33+
# /v1/sessions mint.
2634
FIXABLE_DENIAL_REASONS: frozenset[str] = frozenset(
2735
{
2836
"kyc_required",
2937
"kyc_pending",
3038
"kyc_failed",
31-
"jurisdiction_restricted",
3239
}
3340
)
3441

3542

3643
def is_fixable_denial(reasons: Iterable[str] | None) -> bool:
37-
"""Return True when every reason is fixable (or reasons is empty/None).
44+
"""Return True when every reason is fixable via KYC re-verification.
3845
39-
Sanctions and age failures are permanent — any of those in the list returns False.
46+
False when any reason is permanent (sanctions, age, jurisdiction_restricted) OR when
47+
reasons is empty/None — without a known reason we can't promise a fix, so default to
48+
the bare denial path.
4049
"""
4150
if not reasons:
42-
return True
51+
return False
4352
reasons_list = list(reasons)
4453
if not reasons_list:
45-
return True
54+
return False
4655
return all(r in FIXABLE_DENIAL_REASONS for r in reasons_list)
4756

4857

agentscore_commerce/identity/_response.py

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -142,10 +142,13 @@
142142
),
143143
(
144144
"Fixable compliance reasons (`kyc_required`, `kyc_pending`, "
145-
"`kyc_failed`, `jurisdiction_required` without explicit restriction) "
146-
"do NOT land on this code — the gate auto-mints a verification session "
147-
"for those and returns `identity_verification_required` with poll "
148-
"endpoints, same shape as `missing_identity`."
145+
"`kyc_failed`) do NOT land on this code — the gate auto-mints a "
146+
"verification session for those and returns "
147+
"`identity_verification_required` with poll endpoints, same shape as "
148+
"`missing_identity`. `jurisdiction_restricted` IS in the unfixable "
149+
"bucket because the API only emits it after KYC is verified (the "
150+
"user's KYC'd country is in the blocked list — re-doing KYC won't "
151+
"change the country)."
149152
),
150153
],
151154
"user_message": (

agentscore_commerce/identity/aiohttp.py

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -179,12 +179,14 @@ async def _agentscore_middleware(
179179
request["agentscore"] = result.raw
180180
return await handler(request)
181181

182-
# Fixable compliance denials (kyc_required, kyc_pending, kyc_failed,
183-
# jurisdiction_required when not explicitly restricted) get the same UX as
184-
# missing_identity: the gate mints a fresh verification session, the agent
185-
# polls until status=verified, gets a fresh opc_..., and retries with
186-
# X-Operator-Token. Unfixable reasons (sanctions, age, jurisdiction_restricted)
187-
# keep the bare wallet_not_trusted denial — re-verification won't fix them.
182+
# Fixable compliance denials (kyc_required, kyc_pending, kyc_failed) get the
183+
# same UX as missing_identity: the gate mints a fresh verification session,
184+
# the agent polls until status=verified, gets a fresh opc_..., and retries
185+
# with X-Operator-Token. Unfixable reasons (sanctions_flagged, age_insufficient,
186+
# jurisdiction_restricted) keep the bare wallet_not_trusted denial.
187+
# `jurisdiction_restricted` is unfixable: the API only emits it after KYC is
188+
# verified (the user's KYC'd country is in the blocked list — re-doing KYC
189+
# won't change the country).
188190
if is_fixable_denial(result.reasons) and create_session_on_missing is not None:
189191
session_reason = await try_create_session_denial_reason(
190192
create_session_on_missing,

agentscore_commerce/identity/django.py

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -170,12 +170,14 @@ def __call__(self, request: HttpRequest) -> Any:
170170
setattr(request, "agentscore", result.raw) # noqa: B010 — dynamic attribute attach on HttpRequest
171171
return self.get_response(request)
172172

173-
# Fixable compliance denials (kyc_required, kyc_pending, kyc_failed,
174-
# jurisdiction_required when not explicitly restricted) get the same UX as
175-
# missing_identity: the gate mints a fresh verification session, the agent
176-
# polls until status=verified, gets a fresh opc_..., and retries with
177-
# X-Operator-Token. Unfixable reasons (sanctions, age, jurisdiction_restricted)
178-
# keep the bare wallet_not_trusted denial — re-verification won't fix them.
173+
# Fixable compliance denials (kyc_required, kyc_pending, kyc_failed) get the
174+
# same UX as missing_identity: the gate mints a fresh verification session,
175+
# the agent polls until status=verified, gets a fresh opc_..., and retries
176+
# with X-Operator-Token. Unfixable reasons (sanctions_flagged, age_insufficient,
177+
# jurisdiction_restricted) keep the bare wallet_not_trusted denial.
178+
# `jurisdiction_restricted` is unfixable: the API only emits it after KYC is
179+
# verified (the user's KYC'd country is in the blocked list — re-doing KYC
180+
# won't change the country).
179181
if is_fixable_denial(result.reasons) and self._create_session_on_missing is not None:
180182
session_reason = try_create_session_denial_reason_sync(
181183
self._create_session_on_missing,

agentscore_commerce/identity/fastapi.py

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -208,13 +208,14 @@ async def __call__(self, request: Request) -> None:
208208
setattr(request.state, ASSESS_STATE_KEY, result.raw)
209209
return
210210

211-
# Fixable compliance denials (kyc_required, kyc_pending, kyc_failed,
212-
# jurisdiction_required when not explicitly restricted) get the same UX as
213-
# missing_identity: the gate mints a fresh verification session, the agent
214-
# polls until status=verified, gets a fresh opc_..., and retries with
211+
# Fixable compliance denials (kyc_required, kyc_pending, kyc_failed) get the
212+
# same UX as missing_identity: the gate mints a fresh verification session, the
213+
# agent polls until status=verified, gets a fresh opc_..., and retries with
215214
# X-Operator-Token. No "go to verify_url and tell us when done" gap.
216-
# Unfixable reasons (sanctions, age, jurisdiction_restricted) keep the
217-
# bare wallet_not_trusted denial — re-verification won't fix them.
215+
# Unfixable reasons (sanctions_flagged, age_insufficient, jurisdiction_restricted)
216+
# keep the bare wallet_not_trusted denial — re-verification won't fix them.
217+
# `jurisdiction_restricted` is unfixable because the API only emits it AFTER KYC
218+
# is verified (the user's KYC'd country is in the blocked list).
218219
if is_fixable_denial(result.reasons) and self._create_session_on_missing is not None:
219220
session_reason = await try_create_session_denial_reason(
220221
self._create_session_on_missing,

agentscore_commerce/identity/flask.py

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -184,12 +184,14 @@ def _agentscore_check() -> Response | tuple[Response, int] | None:
184184
g.agentscore = result.raw
185185
return None
186186

187-
# Fixable compliance denials (kyc_required, kyc_pending, kyc_failed,
188-
# jurisdiction_required when not explicitly restricted) get the same UX as
189-
# missing_identity: the gate mints a fresh verification session, the agent
190-
# polls until status=verified, gets a fresh opc_..., and retries with
191-
# X-Operator-Token. Unfixable reasons (sanctions, age, jurisdiction_restricted)
192-
# keep the bare wallet_not_trusted denial — re-verification won't fix them.
187+
# Fixable compliance denials (kyc_required, kyc_pending, kyc_failed) get the
188+
# same UX as missing_identity: the gate mints a fresh verification session,
189+
# the agent polls until status=verified, gets a fresh opc_..., and retries
190+
# with X-Operator-Token. Unfixable reasons (sanctions_flagged, age_insufficient,
191+
# jurisdiction_restricted) keep the bare wallet_not_trusted denial.
192+
# `jurisdiction_restricted` is unfixable: the API only emits it after KYC is
193+
# verified (the user's KYC'd country is in the blocked list — re-doing KYC
194+
# won't change the country).
193195
if is_fixable_denial(result.reasons) and create_session_on_missing is not None:
194196
session_reason = try_create_session_denial_reason_sync(
195197
create_session_on_missing,

agentscore_commerce/identity/middleware.py

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -191,12 +191,14 @@ async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
191191
await self.app(scope, receive, send)
192192
return
193193

194-
# Fixable compliance denials (kyc_required, kyc_pending, kyc_failed,
195-
# jurisdiction_required when not explicitly restricted) get the same UX as
196-
# missing_identity: the gate mints a fresh verification session, the agent
197-
# polls until status=verified, gets a fresh opc_..., and retries with
198-
# X-Operator-Token. Unfixable reasons (sanctions, age, jurisdiction_restricted)
199-
# keep the bare wallet_not_trusted denial — re-verification won't fix them.
194+
# Fixable compliance denials (kyc_required, kyc_pending, kyc_failed) get the
195+
# same UX as missing_identity: the gate mints a fresh verification session,
196+
# the agent polls until status=verified, gets a fresh opc_..., and retries
197+
# with X-Operator-Token. Unfixable reasons (sanctions_flagged, age_insufficient,
198+
# jurisdiction_restricted) keep the bare wallet_not_trusted denial.
199+
# `jurisdiction_restricted` is unfixable: the API only emits it after KYC is
200+
# verified (the user's KYC'd country is in the blocked list — re-doing KYC
201+
# won't change the country).
200202
if is_fixable_denial(result.reasons) and self._create_session_on_missing is not None:
201203
session_reason = await try_create_session_denial_reason(
202204
self._create_session_on_missing,

agentscore_commerce/identity/sanic.py

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -181,12 +181,14 @@ async def _agentscore_check(request: Request) -> HTTPResponse | None:
181181
request.ctx.agentscore = result.raw
182182
return None
183183

184-
# Fixable compliance denials (kyc_required, kyc_pending, kyc_failed,
185-
# jurisdiction_required when not explicitly restricted) get the same UX as
186-
# missing_identity: the gate mints a fresh verification session, the agent
187-
# polls until status=verified, gets a fresh opc_..., and retries with
188-
# X-Operator-Token. Unfixable reasons (sanctions, age, jurisdiction_restricted)
189-
# keep the bare wallet_not_trusted denial — re-verification won't fix them.
184+
# Fixable compliance denials (kyc_required, kyc_pending, kyc_failed) get the
185+
# same UX as missing_identity: the gate mints a fresh verification session,
186+
# the agent polls until status=verified, gets a fresh opc_..., and retries
187+
# with X-Operator-Token. Unfixable reasons (sanctions_flagged, age_insufficient,
188+
# jurisdiction_restricted) keep the bare wallet_not_trusted denial.
189+
# `jurisdiction_restricted` is unfixable: the API only emits it after KYC is
190+
# verified (the user's KYC'd country is in the blocked list — re-doing KYC
191+
# won't change the country).
190192
if is_fixable_denial(result.reasons) and create_session_on_missing is not None:
191193
session_reason = await try_create_session_denial_reason(
192194
create_session_on_missing,

tests/test_denial.py

Lines changed: 17 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -40,20 +40,29 @@ def test_returns_403_for_everything_else(self, code):
4040

4141
class TestIsFixableDenial:
4242
def test_known_fixable_reasons_in_set(self):
43-
for r in ("kyc_required", "kyc_pending", "kyc_failed", "jurisdiction_restricted"):
43+
for r in ("kyc_required", "kyc_pending", "kyc_failed"):
4444
assert r in FIXABLE_DENIAL_REASONS
4545

46-
def test_empty_or_none_treated_as_fixable(self):
47-
assert is_fixable_denial(None)
48-
assert is_fixable_denial([])
46+
def test_jurisdiction_restricted_is_unfixable(self):
47+
# The API only emits jurisdiction_restricted AFTER KYC is verified, meaning the
48+
# user's KYC'd country is in the merchant's blocked list. Re-doing KYC won't
49+
# change the country — same shape as sanctions_flagged / age_insufficient.
50+
assert "jurisdiction_restricted" not in FIXABLE_DENIAL_REASONS
51+
52+
def test_empty_or_none_returns_false(self):
53+
# Without a known reason we can't promise a fix — default to bare denial.
54+
assert not is_fixable_denial(None)
55+
assert not is_fixable_denial([])
4956

5057
def test_all_fixable_returns_true(self):
51-
assert is_fixable_denial(["kyc_required", "jurisdiction_restricted"])
58+
assert is_fixable_denial(["kyc_required", "kyc_pending"])
5259

5360
def test_any_permanent_returns_false(self):
54-
assert not is_fixable_denial(["sanctions_not_clear"])
55-
assert not is_fixable_denial(["age_not_verified"])
56-
assert not is_fixable_denial(["kyc_required", "sanctions_not_clear"])
61+
assert not is_fixable_denial(["sanctions_flagged"])
62+
assert not is_fixable_denial(["age_insufficient"])
63+
assert not is_fixable_denial(["jurisdiction_restricted"])
64+
assert not is_fixable_denial(["kyc_required", "sanctions_flagged"])
65+
assert not is_fixable_denial(["kyc_required", "jurisdiction_restricted"])
5766

5867

5968
class TestBuildSignerMismatchBody:

0 commit comments

Comments
 (0)