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: 0 additions & 4 deletions agentscore/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,6 @@
AccountVerification,
AgentMemoryHint,
AgentMemoryIdentityPaths,
AipProvenance,
AipSignatureMaterial,
AssessResponse,
AssociateWalletResponse,
CredentialCreateErrorNextSteps,
Expand Down Expand Up @@ -61,8 +59,6 @@
"AgentMemoryIdentityPaths",
"AgentScore",
"AgentScoreError",
"AipProvenance",
"AipSignatureMaterial",
"AssessResponse",
"AssociateWalletResponse",
"CredentialCreateErrorNextSteps",
Expand Down
22 changes: 1 addition & 21 deletions agentscore/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,6 @@ def _build_error_from_response(response: httpx.Response) -> AgentScoreError:
from collections.abc import Awaitable, Callable

from agentscore.types import (
AipSignatureMaterial,
AssessResponse,
AssociateWalletResponse,
CredentialCreateResponse,
Expand Down Expand Up @@ -254,8 +253,6 @@ def assess(
policy: DecisionPolicy | None = None,
operator_token: str | None = None,
signer: Signer | None = None,
aip_token: str | None = None,
aip_signature: AipSignatureMaterial | None = None,
) -> AssessResponse:
"""Assess a wallet or operator against a compliance policy.

Expand All @@ -264,22 +261,12 @@ def assess(
``signer`` opts into server-side wallet-signer-match: when supplied,
the API resolves the signer wallet against the claimed ``address`` and emits
a ``signer_match`` block on the response. See :class:`Signer`.

``aip_token`` (+ ``aip_signature``) supplies an AIP Agent Identity Token in place of
``address`` / ``operator_token``: the API re-verifies the issuer signature, the RFC 9421
proof-of-possession, and the attested claims, then evaluates policy against them.
"""
body: dict[str, Any] = {}
if address:
body["address"] = address
if operator_token:
body["operator_token"] = operator_token
# AIP Agent Identity Token path: the API re-verifies the IdP signature + claims
# server-side and evaluates policy against the attested identity.
if aip_token:
body["aip_token"] = aip_token
if aip_signature is not None:
body["aip_signature"] = dict(aip_signature)
if chain:
body["chain"] = chain
if refresh is not None:
Expand Down Expand Up @@ -404,25 +391,18 @@ async def aassess(
policy: DecisionPolicy | None = None,
operator_token: str | None = None,
signer: Signer | None = None,
aip_token: str | None = None,
aip_signature: AipSignatureMaterial | None = None,
) -> AssessResponse:
"""Assess a wallet or operator against a compliance policy.

``refresh`` is deprecated: the API ignores it, and every assess is evaluated live.

``signer`` opts into server-side wallet-signer-match; ``aip_token`` (+ ``aip_signature``)
supplies an AIP Agent Identity Token. Async mirror of :meth:`assess`.
``signer`` opts into server-side wallet-signer-match. Async mirror of :meth:`assess`.
"""
body: dict[str, Any] = {}
if address:
body["address"] = address
if operator_token:
body["operator_token"] = operator_token
if aip_token:
body["aip_token"] = aip_token
if aip_signature is not None:
body["aip_signature"] = dict(aip_signature)
if chain:
body["chain"] = chain
if refresh is not None:
Expand Down
40 changes: 1 addition & 39 deletions agentscore/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -132,43 +132,10 @@ class SignerSanctionsUnavailable(TypedDict):
SignerSanctions = SignerSanctionsClear | SignerSanctionsHit | SignerSanctionsUnavailable


class AipSignatureMaterial(TypedDict):
"""RFC 9421 HTTP Message Signature material proving possession of the AIT-bound ``cnf`` key.

Forwarded alongside ``aip_token`` so ``/v1/assess`` can re-verify proof-of-possession
authoritatively; the API never sees the original agent-to-merchant request itself.
"""

method: str # HTTP method of the original agent-to-merchant request (``@method``)
authority: str # Authority/host the agent signed (``@authority``)
path: str # Request path the agent signed (``@path``)
signature_input: str # Raw ``Signature-Input`` header value the agent sent
signature: str # Raw ``Signature`` header value the agent sent


class _AipProvenanceRequired(TypedDict):
issuer: str # Canonical issuer URL of the AIT
subject: str # The token's ``sub``: the IdP's subject identifier for the verified human


class AipProvenance(_AipProvenanceRequired, total=False):
"""Provenance block returned when the identity input was an AIP Agent Identity Token.

Surfaces which issuer attested the identity and the trust level it asserted.
"""

trust_level: Literal["autonomous", "human_present", "human_confirmed"]
agent_provider: str
# True when /v1/assess re-verified the RFC 9421 proof-of-possession. Always true on a
# success response: the API fail-closes with an HTTP 400/401 error (not a 200 deny)
# when possession can't be proven.
pop_verified: bool


class _AssessResponseRequired(TypedDict):
decision: str | None
decision_reasons: list[str]
identity_method: Literal["wallet", "operator_token", "aip_token"]
identity_method: Literal["wallet", "operator_token"]


class PolicyExplanation(TypedDict, total=False):
Expand Down Expand Up @@ -223,8 +190,6 @@ class AssessResponse(_AssessResponseRequired, total=False):
# Server-side OFAC SDN wallet-address verdict, returned only when the request supplied
# ``signer``. Empty otherwise.
signer_sanctions: NotRequired[SignerSanctions]
# Issuer provenance, returned only when ``identity_method == "aip_token"``.
aip: NotRequired[AipProvenance]
# Quota state for this account, captured from response headers on the success path.
# Use to monitor approach-to-cap proactively (warn at 80%, alert at 95%) before 429.
quota: NotRequired[QuotaInfo]
Expand Down Expand Up @@ -446,7 +411,6 @@ class AssociateWalletResponse(TypedDict):
class AgentMemoryIdentityPaths(TypedDict):
wallet: str
operator_token: str
agent_identity: NotRequired[str]


class AgentMemoryHint(TypedDict):
Expand All @@ -463,8 +427,6 @@ class AgentMemoryHint(TypedDict):
identity_check_endpoint: str
list_wallets_endpoint: NotRequired[str]
identity_paths: AgentMemoryIdentityPaths
# Issuer allowlist a merchant accepts for AIP Agent Identity Tokens, when advertised.
aip_trusted_issuers: NotRequired[list[str]]
bootstrap: str
do_not_persist_in_memory: list[str]
persist_in_credential_store: list[str]
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ build-backend = "hatchling.build"

[project]
name = "agentscore-py"
version = "2.7.1"
version = "2.8.0"
description = "Python client for the AgentScore APIs"
readme = "README.md"
license = "MIT"
Expand Down
101 changes: 0 additions & 101 deletions tests/test_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -353,107 +353,6 @@ def test_assess_signer_raises_token_expired_with_signer():
assert body["signer"] == {"address": "0xs", "network": "evm"}


# ---------------------------------------------------------------------------
# assess: AIP Agent Identity Token (aip_token + aip_signature)
# ---------------------------------------------------------------------------

AIP_TOKEN = "eyJhbGciOiJFZERTQSJ9.ait.payload"

AIP_SIGNATURE = {
"method": "POST",
"authority": "merchant.example.com",
"path": "/premium/report",
"signature_input": 'sig1=("@method" "@authority" "@path");keyid="agent-cnf-key";alg="ed25519"',
"signature": "sig1=:dGVzdC1zaWduYXR1cmU=:",
}

AIP_ASSESS_PAYLOAD = {
**ASSESS_PAYLOAD,
"identity_method": "aip_token",
"aip": {
"issuer": "https://www.agentscore.com",
"subject": "user_2abc",
"trust_level": "human_present",
"agent_provider": "openai",
"pop_verified": True,
},
}


@respx.mock
def test_assess_forwards_aip_token_and_signature_in_body():
"""aip_token + all 5 RFC 9421 PoP fields of aip_signature land in the request body."""
route = respx.post(f"{BASE_URL}/v1/assess").mock(return_value=httpx.Response(200, json=AIP_ASSESS_PAYLOAD))
client = AgentScore(api_key=API_KEY)
client.assess(aip_token=AIP_TOKEN, aip_signature=AIP_SIGNATURE)
body = json.loads(route.calls.last.request.content)
assert body["aip_token"] == AIP_TOKEN
assert body["aip_signature"] == {
"method": "POST",
"authority": "merchant.example.com",
"path": "/premium/report",
"signature_input": 'sig1=("@method" "@authority" "@path");keyid="agent-cnf-key";alg="ed25519"',
"signature": "sig1=:dGVzdC1zaWduYXR1cmU=:",
}
assert "address" not in body
assert "operator_token" not in body


@respx.mock
def test_assess_returns_aip_provenance():
"""The aip block (incl. pop_verified) and identity_method round-trip on the response."""
respx.post(f"{BASE_URL}/v1/assess").mock(return_value=httpx.Response(200, json=AIP_ASSESS_PAYLOAD))
client = AgentScore(api_key=API_KEY)
result = client.assess(aip_token=AIP_TOKEN, aip_signature=AIP_SIGNATURE)
assert result["identity_method"] == "aip_token"
assert result["aip"]["issuer"] == "https://www.agentscore.com"
assert result["aip"]["subject"] == "user_2abc"
assert result["aip"]["trust_level"] == "human_present"
assert result["aip"]["agent_provider"] == "openai"
assert result["aip"]["pop_verified"] is True


@respx.mock
def test_assess_omits_aip_fields_when_not_provided():
route = respx.post(f"{BASE_URL}/v1/assess").mock(return_value=httpx.Response(200, json=ASSESS_PAYLOAD))
client = AgentScore(api_key=API_KEY)
client.assess(ADDRESS)
body = json.loads(route.calls.last.request.content)
assert "aip_token" not in body
assert "aip_signature" not in body


@pytest.mark.asyncio
@respx.mock
async def test_aassess_forwards_aip_token_and_signature_in_body():
route = respx.post(f"{BASE_URL}/v1/assess").mock(return_value=httpx.Response(200, json=AIP_ASSESS_PAYLOAD))
client = AgentScore(api_key=API_KEY)
await client.aassess(aip_token=AIP_TOKEN, aip_signature=AIP_SIGNATURE)
body = json.loads(route.calls.last.request.content)
assert body["aip_token"] == AIP_TOKEN
assert body["aip_signature"] == {
"method": "POST",
"authority": "merchant.example.com",
"path": "/premium/report",
"signature_input": 'sig1=("@method" "@authority" "@path");keyid="agent-cnf-key";alg="ed25519"',
"signature": "sig1=:dGVzdC1zaWduYXR1cmU=:",
}
assert "address" not in body
await client.aclose()


@pytest.mark.asyncio
@respx.mock
async def test_aassess_returns_aip_provenance():
respx.post(f"{BASE_URL}/v1/assess").mock(return_value=httpx.Response(200, json=AIP_ASSESS_PAYLOAD))
client = AgentScore(api_key=API_KEY)
result = await client.aassess(aip_token=AIP_TOKEN, aip_signature=AIP_SIGNATURE)
assert result["identity_method"] == "aip_token"
assert result["aip"]["pop_verified"] is True
assert result["aip"]["issuer"] == "https://www.agentscore.com"
await client.aclose()


# ---------------------------------------------------------------------------
# Async request handling
# ---------------------------------------------------------------------------
Expand Down
2 changes: 1 addition & 1 deletion uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading