Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions agentscore_commerce/identity/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
build_a2a_agent_card,
ucp_a2a_extension,
)
from agentscore_commerce.identity.client import GateClient
from agentscore_commerce.identity.core import AgentScoreCore
from agentscore_commerce.identity.policy import (
EnforcementMode,
GateResult,
Expand Down Expand Up @@ -106,14 +106,14 @@ def _load_asgi_middleware() -> tuple[Any, Any]:
"A2AAgentSkill",
"AgentIdentity",
"AgentMemoryHint",
"AgentScoreCore",
"AgentScoreGate",
"AgentScoreGatePolicy",
"AssessResult",
"CreateSessionOnMissing",
"DenialCode",
"DenialReason",
"EnforcementMode",
"GateClient",
"GateResult",
"GeneratedUCPKey",
"IdentityStatus",
Expand Down
6 changes: 3 additions & 3 deletions agentscore_commerce/identity/aiohttp.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,8 @@
build_missing_identity_reason,
denial_reason_to_body,
)
from agentscore_commerce.identity.client import (
GateClient,
from agentscore_commerce.identity.core import (
AgentScoreCore,
InvalidCredentialError,
PaymentRequiredError,
QuotaExceededError,
Expand Down Expand Up @@ -163,7 +163,7 @@ def agentscore_gate_middleware(
"""
from aiohttp import web

client = GateClient(
client = AgentScoreCore(
api_key=api_key,
require_kyc=require_kyc,
require_sanctions_clear=require_sanctions_clear,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@
DEFAULT_CACHE_SECONDS = 300


class GateClient:
class AgentScoreCore:
"""Shared client for calling the AgentScore assess API.

Manages caching and policy construction. Used by all framework adapters.
Expand Down
6 changes: 3 additions & 3 deletions agentscore_commerce/identity/django.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,8 @@
build_missing_identity_reason,
denial_reason_to_body,
)
from agentscore_commerce.identity.client import (
GateClient,
from agentscore_commerce.identity.core import (
AgentScoreCore,
InvalidCredentialError,
PaymentRequiredError,
QuotaExceededError,
Expand Down Expand Up @@ -129,7 +129,7 @@ def __init__(self, get_response: Any) -> None:

config: dict[str, Any] = getattr(settings, "AGENTSCORE_GATE", {})

self._client = GateClient(
self._client = AgentScoreCore(
api_key=config.get("api_key", ""),
require_kyc=config.get("require_kyc"),
require_sanctions_clear=config.get("require_sanctions_clear"),
Expand Down
6 changes: 3 additions & 3 deletions agentscore_commerce/identity/fastapi.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,8 @@
build_missing_identity_reason,
denial_reason_to_body,
)
from agentscore_commerce.identity.client import (
GateClient,
from agentscore_commerce.identity.core import (
AgentScoreCore,
InvalidCredentialError,
PaymentRequiredError,
QuotaExceededError,
Expand Down Expand Up @@ -181,7 +181,7 @@ def __init__(
on_denied: Callable[[Request, DenialReason], tuple[dict[str, Any], int]] | None = None,
create_session_on_missing: CreateSessionOnMissing | None = None,
) -> None:
self._client = GateClient(
self._client = AgentScoreCore(
api_key=api_key,
require_kyc=require_kyc,
require_sanctions_clear=require_sanctions_clear,
Expand Down
6 changes: 3 additions & 3 deletions agentscore_commerce/identity/flask.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,8 @@
build_missing_identity_reason,
denial_reason_to_body,
)
from agentscore_commerce.identity.client import (
GateClient,
from agentscore_commerce.identity.core import (
AgentScoreCore,
InvalidCredentialError,
PaymentRequiredError,
QuotaExceededError,
Expand Down Expand Up @@ -167,7 +167,7 @@ def agentscore_gate(
from flask import g, jsonify
from flask import request as flask_request

client = GateClient(
client = AgentScoreCore(
api_key=api_key,
require_kyc=require_kyc,
require_sanctions_clear=require_sanctions_clear,
Expand Down
6 changes: 3 additions & 3 deletions agentscore_commerce/identity/middleware.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,8 @@
build_missing_identity_reason,
denial_reason_to_body,
)
from agentscore_commerce.identity.client import (
GateClient,
from agentscore_commerce.identity.core import (
AgentScoreCore,
InvalidCredentialError,
PaymentRequiredError,
QuotaExceededError,
Expand Down Expand Up @@ -165,7 +165,7 @@ def __init__(
create_session_on_missing: CreateSessionOnMissing | None = None,
) -> None:
self.app = app
self._client = GateClient(
self._client = AgentScoreCore(
api_key=api_key,
require_kyc=require_kyc,
require_sanctions_clear=require_sanctions_clear,
Expand Down
2 changes: 1 addition & 1 deletion agentscore_commerce/identity/policy.py
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ def build_gate_from_policy(

Use a fresh gate per request rather than constructing once at module scope
when policy varies per resource (e.g. per product). The gate is cheap to
instantiate; AgentScore's response cache lives on :class:`GateClient`
instantiate; AgentScore's response cache lives on :class:`AgentScoreCore`
inside the gate, scoped to the lifetime of this gate instance.
"""
if policy is None:
Expand Down
6 changes: 3 additions & 3 deletions agentscore_commerce/identity/sanic.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,8 @@
build_missing_identity_reason,
denial_reason_to_body,
)
from agentscore_commerce.identity.client import (
GateClient,
from agentscore_commerce.identity.core import (
AgentScoreCore,
InvalidCredentialError,
PaymentRequiredError,
QuotaExceededError,
Expand Down Expand Up @@ -164,7 +164,7 @@ def agentscore_gate(
"""
from sanic import response

client = GateClient(
client = AgentScoreCore(
api_key=api_key,
require_kyc=require_kyc,
require_sanctions_clear=require_sanctions_clear,
Expand Down
2 changes: 1 addition & 1 deletion agentscore_commerce/identity/signer.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
(``Authorization: Payment``), not x402, so they don't arrive at this helper. If a
non-AgentScore merchant does receive a legacy x402 SVM payload, this function returns
``None``; custom adapters that recover the Solana signer themselves should pass
``signer={address, network}`` directly to ``GateClient.check``/``acheck``.
``signer={address, network}`` directly to ``AgentScoreCore.check``/``acheck``.

Tempo MPP signer extraction is also caller-supplied; there's no pip-installable
equivalent of the node ``mppx`` library today.
Expand Down
2 changes: 1 addition & 1 deletion agentscore_commerce/identity/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,7 @@ class VerifyWalletSignerResult:

@dataclass
class SignerVerdict:
"""Combined wallet-signer verdict surfaced by :meth:`GateClient.get_signer_verdict`.
"""Combined wallet-signer verdict surfaced by :meth:`AgentScoreCore.get_signer_verdict`.

Both ``signer_match`` and ``signer_sanctions`` come through the gate's primary
``/v1/assess`` call (single round trip). ``signer_match`` describes the wallet-
Expand Down
2 changes: 1 addition & 1 deletion examples/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ AgentScore Commerce handles the agent commerce protocol layer; everything else i

Python wraps `x402[evm]` and `pympp[server,tempo,stripe]` as peer deps; `@solana/mpp` has no Python equivalent today. Two implications:

1. **`extract_payment_signer` returns EVM only.** Solana SPL Token payer recovery requires a Solana SDK (`solders` / `solana-py`) which isn't bundled. Custom adapters that wire Solana signer recovery should pass `signer={address, network}` directly to `GateClient.acheck()`; the API returns the wallet-binding + sanctions verdicts on the same response.
1. **`extract_payment_signer` returns EVM only.** Solana SPL Token payer recovery requires a Solana SDK (`solders` / `solana-py`) which isn't bundled. Custom adapters that wire Solana signer recovery should pass `signer={address, network}` directly to `AgentScoreCore.acheck()`; the API returns the wallet-binding + sanctions verdicts on the same response.
2. **Streaming session payments (variable_cost_merchant.py)** sketches the protocol but doesn't ship a working tempo session implementation; there's no pip-installable `mppx` equivalent. The example shows the response shape; vendors using session payments today should check the [tempo session protocol docs](https://mpp.dev/guides/streamed-payments) and bind to a Solana wallet library directly.

For Python merchants on x402 alone (Base or Solana), every helper (`create_x402_server`, `create_mppx_server`, directives, headers, dispatch, settle-overrides, signer extraction for EVM, accepted_methods, agent_instructions, how_to_pay) is fully native.
6 changes: 3 additions & 3 deletions tests/test_aiohttp.py
Original file line number Diff line number Diff line change
Expand Up @@ -236,7 +236,7 @@ async def _snoop(request: web.Request) -> web.Response:
return web.json_response({k: v for k, v in state.items() if k != "client"})

with patch(
"agentscore_commerce.identity.aiohttp.GateClient.acheck_identity",
"agentscore_commerce.identity.aiohttp.AgentScoreCore.acheck_identity",
side_effect=httpx.TimeoutException("read timeout"),
):
client = await _client(_make_app(handler=_snoop, fail_open=True))
Expand All @@ -254,7 +254,7 @@ async def _snoop(request: web.Request) -> web.Response:
return web.json_response({k: v for k, v in state.items() if k != "client"})

with patch(
"agentscore_commerce.identity.aiohttp.GateClient.acheck_identity",
"agentscore_commerce.identity.aiohttp.AgentScoreCore.acheck_identity",
side_effect=RuntimeError("oops"),
):
client = await _client(_make_app(handler=_snoop, fail_open=True))
Expand Down Expand Up @@ -431,7 +431,7 @@ async def test_no_ops_when_gate_did_not_run(self):
# Handler wired without the gate middleware — capture_wallet must silently no-op.
app = web.Application()
app.router.add_post("/", _capture_handler)
with patch("agentscore_commerce.identity.client.GateClient.acapture_wallet", new=AsyncMock()) as mock_cap:
with patch("agentscore_commerce.identity.core.AgentScoreCore.acapture_wallet", new=AsyncMock()) as mock_cap:
client = await _client(app)
async with client:
resp = await client.post("/")
Expand Down
34 changes: 17 additions & 17 deletions tests/test_client.py → tests/test_core.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
"""Direct unit tests for GateClient internals."""
"""Direct unit tests for AgentScoreCore internals."""

from __future__ import annotations

Expand All @@ -9,13 +9,13 @@
import pytest
import respx

from agentscore_commerce.identity.client import GateClient, PaymentRequiredError
from agentscore_commerce.identity.core import AgentScoreCore, PaymentRequiredError


def _make_client(**kwargs) -> GateClient:
def _make_client(**kwargs) -> AgentScoreCore:
defaults = {"api_key": "ask_test_key"}
defaults.update(kwargs)
return GateClient(**defaults)
return AgentScoreCore(**defaults)


class TestHeaders:
Expand Down Expand Up @@ -195,7 +195,7 @@ class TestParseResponseStatusCodes:
"""

def test_429_raises_quota_exceeded(self):
from agentscore_commerce.identity.client import QuotaExceededError
from agentscore_commerce.identity.core import QuotaExceededError

client = _make_client()
resp = MagicMock(spec=httpx.Response)
Expand All @@ -204,7 +204,7 @@ def test_429_raises_quota_exceeded(self):
client._parse_response(resp)

def test_401_token_expired_raises_token_denied(self):
from agentscore_commerce.identity.client import TokenDeniedError
from agentscore_commerce.identity.core import TokenDeniedError

client = _make_client()
resp = MagicMock(spec=httpx.Response)
Expand All @@ -220,7 +220,7 @@ def test_401_token_expired_raises_token_denied(self):
assert info.value.body.get("verify_url") == "https://x"

def test_401_invalid_credential_raises_invalid_credential(self):
from agentscore_commerce.identity.client import InvalidCredentialError
from agentscore_commerce.identity.core import InvalidCredentialError

client = _make_client()
resp = MagicMock(spec=httpx.Response)
Expand Down Expand Up @@ -342,7 +342,7 @@ def test_check_raises_on_api_error(self):
def test_check_raises_quota_exceeded_on_429(self):
"""429 must be distinguishable from generic 5xx so adapters surface
``infra_reason='quota_exceeded'`` separately when fail_open=True."""
from agentscore_commerce.identity.client import QuotaExceededError
from agentscore_commerce.identity.core import QuotaExceededError

client = _make_client(fail_open=False)
respx.post(ASSESS_URL).mock(return_value=httpx.Response(429))
Expand All @@ -355,7 +355,7 @@ def test_check_raises_quota_exceeded_on_typed_429(self):
"""SDK emits typed QuotaExceededError when body has error.code='quota_exceeded' —
commerce wraps it so callers get the gate's QuotaExceededError sentinel.
"""
from agentscore_commerce.identity.client import QuotaExceededError
from agentscore_commerce.identity.core import QuotaExceededError

client = _make_client(fail_open=False)
respx.post(ASSESS_URL).mock(
Expand All @@ -380,7 +380,7 @@ def test_check_raises_payment_required_on_402(self):
@respx.mock
def test_check_raises_token_denied_on_typed_401(self):
"""401 with error.code='token_expired' surfaces TokenDeniedError carrying body."""
from agentscore_commerce.identity.client import TokenDeniedError
from agentscore_commerce.identity.core import TokenDeniedError

client = _make_client(fail_open=False)
respx.post(ASSESS_URL).mock(
Expand All @@ -402,7 +402,7 @@ def test_check_raises_token_denied_on_typed_401(self):
@respx.mock
def test_check_raises_invalid_credential_on_typed_401(self):
"""401 with error.code='invalid_credential' surfaces InvalidCredentialError."""
from agentscore_commerce.identity.client import InvalidCredentialError
from agentscore_commerce.identity.core import InvalidCredentialError

client = _make_client(fail_open=False)
respx.post(ASSESS_URL).mock(
Expand Down Expand Up @@ -734,7 +734,7 @@ class TestInvalidCredential:

@respx.mock
def test_raises_invalid_credential_on_401_invalid_credential(self):
from agentscore_commerce.identity.client import InvalidCredentialError
from agentscore_commerce.identity.core import InvalidCredentialError

respx.post(ASSESS_URL).mock(
return_value=httpx.Response(
Expand Down Expand Up @@ -768,7 +768,7 @@ def test_logs_when_401_body_isnt_valid_json(self):
client.check(operator_token="opc_x")

def test_build_invalid_credential_reason_carries_action_copy(self):
from agentscore_commerce.identity.client import build_invalid_credential_reason
from agentscore_commerce.identity.core import build_invalid_credential_reason

reason = build_invalid_credential_reason()
assert reason.code == "invalid_credential"
Expand All @@ -790,7 +790,7 @@ class TestAcheckTypedErrors:
@pytest.mark.asyncio
@respx.mock
async def test_acheck_raises_quota_exceeded_on_typed_429(self):
from agentscore_commerce.identity.client import QuotaExceededError
from agentscore_commerce.identity.core import QuotaExceededError

client = _make_client()
respx.post(ASSESS_URL).mock(
Expand All @@ -807,7 +807,7 @@ async def test_acheck_raises_quota_exceeded_on_typed_429(self):
@respx.mock
async def test_acheck_raises_quota_exceeded_on_untyped_429(self):
"""Mirrors the sync defensive 429-fallback path."""
from agentscore_commerce.identity.client import QuotaExceededError
from agentscore_commerce.identity.core import QuotaExceededError

client = _make_client()
respx.post(ASSESS_URL).mock(return_value=httpx.Response(429))
Expand All @@ -827,7 +827,7 @@ async def test_acheck_raises_payment_required_on_402(self):
@pytest.mark.asyncio
@respx.mock
async def test_acheck_raises_token_denied_on_typed_401(self):
from agentscore_commerce.identity.client import TokenDeniedError
from agentscore_commerce.identity.core import TokenDeniedError

client = _make_client()
respx.post(ASSESS_URL).mock(
Expand All @@ -849,7 +849,7 @@ async def test_acheck_raises_token_denied_on_typed_401(self):
@pytest.mark.asyncio
@respx.mock
async def test_acheck_raises_invalid_credential_on_typed_401(self):
from agentscore_commerce.identity.client import InvalidCredentialError
from agentscore_commerce.identity.core import InvalidCredentialError

client = _make_client()
respx.post(ASSESS_URL).mock(
Expand Down
Loading