Skip to content

Commit 5d9f219

Browse files
vvillait88claude
andauthored
feat(discovery): build_skill_md renderer for /skill.md agent surface (#5)
## Summary - Python port of node-commerce 1.2.0's \`buildSkillMd\` — renders a Claude-Skill-compatible \`/skill.md\` manifest with YAML frontmatter + markdown body. - Strictly agent-facing data: rails accepted, compatible clients per rail, identity requirements as outcomes, shipping policy, endpoints, triggers, support links. No internal posture (\`fail_open\`, mount strategy, KYC vendor, defense parameters, idempotency construction) leaks. - Per-rail compatible-clients table sources from the same SDK constant (\`compatible_clients_by_rails\`, extracted from \`challenge/agent_instructions.py\`) that drives the live 402 body's \`compatible_clients\` field — single source of truth across surfaces. - Adds \`/skill.md\` to \`DEFAULT_DISCOVERY_PATHS\` so the existing noindex middleware / discovery-path predicate auto-recognizes the new surface. - Updates \`README.md\` + \`CLAUDE.md\` to document the new builder. - Bump to 1.2.0 to match node-commerce parity. ## Test plan - [x] 35 unit tests covering every section, optional fields, output hygiene, and the no-internal-disclosure boundary - [x] \`uv run ruff check .\` clean - [x] \`uv run ruff format --check .\` clean - [x] \`uv run ty check\` clean - [x] \`uv run vulture\` clean - [x] Coverage 95.38% (over 95% threshold) - [ ] CI green --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent df32adf commit 5d9f219

9 files changed

Lines changed: 903 additions & 18 deletions

File tree

CLAUDE.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ Every helper is extracted from a real consumer, not speculated.
1010
|---|---|
1111
| `agentscore_commerce.identity.{fastapi,flask,django,aiohttp,sanic,middleware}` | Trust gate middleware (KYC, age, sanctions, jurisdiction) |
1212
| `agentscore_commerce.payment` | Networks/USDC/rails registries, paymentauth.org directive builders, `create_x402_server` (wraps official `x402[evm,svm]>=2.8` peer dep with v1+v2 dual-register + bazaar extension), `create_mppx_server` (wraps `pympp[server,tempo,stripe]>=0.6` peer dep with Tempo charge/session + Stripe SPT helpers), dispatch-by-network, signer extraction, WWW-Authenticate header, Settlement-Overrides header |
13-
| `agentscore_commerce.discovery` | Discovery probe, Bazaar wrapper, `/.well-known/mpp.json`, `llms.txt` builder, OpenAPI snippets, `NoindexNonDiscoveryMiddleware` ASGI middleware |
13+
| `agentscore_commerce.discovery` | Discovery probe, Bazaar wrapper, `/.well-known/mpp.json`, `llms.txt` builder, `skill.md` builder (Claude-Skill-compatible agent-discovery manifest), OpenAPI snippets, `NoindexNonDiscoveryMiddleware` ASGI middleware |
1414
| `agentscore_commerce.challenge` | 402-body builders: accepted_methods, identity_metadata, how_to_pay, agent_instructions, build_402_body, `build_validation_error` (4xx body builder) |
1515
| `agentscore_commerce.stripe_multichain` | Multichain PaymentIntent helper, deposit-address lookup, testnet simulator, mppx Stripe wrapper |
1616
| `agentscore_commerce.api` | Re-exports `AgentScore` from `agentscore` SDK |

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ pip install agentscore-commerce[fastapi] # or [flask], [django], [aiohttp], [s
1818
| `agentscore_commerce.identity.{fastapi,flask,django,aiohttp,sanic,middleware}` | Trust gate middleware: KYC, sanctions, age, jurisdiction. `AgentScoreGate(...)` (or `agentscore_gate(app, ...)` on Flask/Sanic), `get_assess_data(...)`, `capture_wallet(...)`, `verify_wallet_signer_match(...)`. |
1919
| `agentscore_commerce.identity` (package level) | Re-exports the denial helpers: `denial_reason_status`, `denial_reason_to_body`, `build_signer_mismatch_body`, `build_contact_support_next_steps`, `verification_agent_instructions`, `is_fixable_denial`, `FIXABLE_DENIAL_REASONS`. Also re-exports the per-product policy helpers: `PolicyBlock`, `GateResult`, `EnforcementMode`, `IdentityStatus`, `build_gate_from_policy`, `run_gate_with_enforcement`, `shipping_country_allowed`, `shipping_state_allowed` — for multi-product merchants where each product carries its own compliance config (hard gate vs soft vs none, per-product shipping allowlists). |
2020
| `agentscore_commerce.payment` | `networks`, `USDC`, `rails` registries; `payment_directive`, `build_payment_directive`, `www_authenticate_header`, `payment_required_header`, `alias_amount_fields` (v1↔v2 amount field shim — emits both `amount` and `maxAmountRequired` so v1-only x402 parsers like Coinbase awal can read v2 bodies), `settlement_override_header`, `dispatch_settlement_by_network`, `extract_payment_signer` (returns `PaymentSigner({address, network})`), `register_x402_schemes_v1_v2`; drop-in x402 helpers: `validate_x402_network_config` (boot-time guard), `verify_x402_request` (parse + validate inbound X-Payment), `process_x402_settle` (verify-then-settle with one call). |
21-
| `agentscore_commerce.discovery` | `is_discovery_probe_request`, `build_discovery_probe_response` (with optional `x402_sample` for x402-aware crawlers — `awal x402 details` etc.), `sample_x402_accept_for_network` (USDC sample-accept builder for known CAIP-2 networks), `build_well_known_mpp`, `build_llms_txt` + `llms_txt_identity_section` + `llms_txt_payment_section` (compact + verbose modes), `agentscore_openapi_snippets`, `build_bazaar_discovery_payload`, `NoindexNonDiscoveryMiddleware` (ASGI middleware that emits `X-Robots-Tag: noindex` on every path except the agent-discovery surfaces — defaults cover `/openapi.json`, `/llms.txt`, `/.well-known/{mpp.json,agent-card.json,ucp}`, `/favicon.{png,ico}`; pure helpers `is_discovery_path` + `DEFAULT_DISCOVERY_PATHS` for non-ASGI frameworks). |
21+
| `agentscore_commerce.discovery` | `is_discovery_probe_request`, `build_discovery_probe_response` (with optional `x402_sample` for x402-aware crawlers — `awal x402 details` etc.), `sample_x402_accept_for_network` (USDC sample-accept builder for known CAIP-2 networks), `build_well_known_mpp`, `build_llms_txt` + `llms_txt_identity_section` + `llms_txt_payment_section` (compact + verbose modes), `build_skill_md` (Claude-Skill-compatible `/skill.md` agent-discovery manifest — strictly agent-facing data only, no internal posture), `agentscore_openapi_snippets`, `build_bazaar_discovery_payload`, `NoindexNonDiscoveryMiddleware` (ASGI middleware that emits `X-Robots-Tag: noindex` on every path except the agent-discovery surfaces — defaults cover `/openapi.json`, `/llms.txt`, `/skill.md`, `/.well-known/{mpp.json,agent-card.json,ucp}`, `/favicon.{png,ico}`; pure helpers `is_discovery_path` + `DEFAULT_DISCOVERY_PATHS` for non-ASGI frameworks). |
2222
| `agentscore_commerce.challenge` | `build_402_body`, `build_accepted_methods`, `build_identity_metadata`, `build_how_to_pay`, `build_agent_instructions` (auto-emits per-rail `compatible_clients` — smoke-verified CLIs the agent should use; vendor override supported), `build_pricing_block` (cents → dollar-string with optional shipping/tax), `first_encounter_agent_memory` (cross-merchant hint, returns the canonical block or `None` based on a per-merchant first-seen flag), `OrderReceipt` (dataclass for the post-settlement 200 response shape); `respond_402` — drop-in 402 emit that preserves pympp's `WWW-Authenticate` and layers x402's `PAYMENT-REQUIRED`. `build_validation_error` — structured 4xx body builder (`{error: {code, message}, required_fields?, example_body?, next_steps?, ...extra}`) so vendors compose body shapes by name instead of inlining at every validation site. |
2323
| `agentscore_commerce.stripe_multichain` | `create_multichain_payment_intent`, `get_deposit_address`, `simulate_crypto_deposit`; `create_pi_cache` (TTL'd PI / deposit-address cache, Redis-backed when `redis_url` set, in-memory otherwise), `simulate_deposit_if_test_mode` (gates on `sk_test_` and looks up the PI for you), `STRIPE_TEST_TX_HASH_SUCCESS` / `STRIPE_TEST_TX_HASH_FAILED` constants. Peer dep on `stripe`. |
2424
| `agentscore_commerce.api` | Everything from `agentscore-py` re-exported in one place: `AgentScore` + `AgentScoreError`, `AGENTSCORE_TEST_ADDRESSES` + `is_agentscore_test_address`. **Don't add `agentscore-py` as a separate dep** — the two can drift versions and cause subtle type mismatches. |

agentscore_commerce/challenge/agent_instructions.py

Lines changed: 35 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
"""agent_instructions block builder for the 402 body."""
22

3+
from collections.abc import Iterable
34
from dataclasses import dataclass, field
4-
from typing import Any
5+
from typing import Any, Literal
56

67
_TEMPO_WARNING = (
78
"Do NOT use `tempo wallet transfer` to pay to the address above. That moves USDC on-chain but does not "
@@ -46,28 +47,48 @@ def _default_warnings(how_to_pay: dict[str, Any]) -> list[str]:
4647
return w
4748

4849

50+
RailKey = Literal["tempo_mpp", "x402_base", "x402_solana", "stripe"]
51+
52+
_RAIL_CLIENTS: dict[str, list[str]] = {
53+
"tempo_mpp": ["agentscore-pay", "tempo request", "x402-proxy"],
54+
"x402_base": ["agentscore-pay", "x402-proxy", "purl (omit --network flag)"],
55+
"x402_solana": ["agentscore-pay"],
56+
"stripe": ["link-cli"],
57+
}
58+
59+
60+
def compatible_clients_by_rails(rails: Iterable[str]) -> dict[str, list[str]] | None:
61+
"""Smoke-verified client list for a set of rail keys.
62+
63+
The single source of truth for "which CLIs we've verified end-to-end on each rail" —
64+
consumed both by the 402-body builder (``build_agent_instructions``) and by discovery
65+
surfaces (skill.md, llms.txt, etc.). Update here, every surface inherits.
66+
"""
67+
out: dict[str, list[str]] = {}
68+
for r in rails:
69+
clients = _RAIL_CLIENTS.get(r)
70+
if clients is not None:
71+
out[r] = list(clients)
72+
return out or None
73+
74+
4975
def _default_compatible_clients(how_to_pay: dict[str, Any]) -> dict[str, list[str]] | None:
5076
"""Default ``compatible_clients`` derived from the rails declared in ``how_to_pay``.
5177
52-
Lists clients the AgentScore team has smoke-verified end-to-end against an
53-
``agentscore-commerce`` merchant; entries appear only for rails the vendor actually
54-
offers. Vendors override this in ``BuildAgentInstructionsInput(compatible_clients=...)``
78+
Vendors override this in ``BuildAgentInstructionsInput(compatible_clients=...)``
5579
to add their own tested clients or remove entries that don't fit their endpoint.
56-
57-
Verified state as of the SDK release. The same data is also published as a docs page
58-
for humans (rationale, per-rail commands, why some clients don't fully work, last
59-
verified date) — this default keeps the merchant-side surface in sync.
80+
Verified state as of the SDK release.
6081
"""
61-
out: dict[str, list[str]] = {}
82+
rails: list[str] = []
6283
if "tempo" in how_to_pay:
63-
out["tempo_mpp"] = ["agentscore-pay", "tempo request", "x402-proxy"]
84+
rails.append("tempo_mpp")
6485
if "x402_base" in how_to_pay:
65-
out["x402_base"] = ["agentscore-pay", "x402-proxy", "purl (omit --network flag)"]
86+
rails.append("x402_base")
6687
if "x402_solana" in how_to_pay:
67-
out["x402_solana"] = ["agentscore-pay"]
88+
rails.append("x402_solana")
6889
if "stripe" in how_to_pay:
69-
out["stripe"] = ["link-cli"]
70-
return out or None
90+
rails.append("stripe")
91+
return compatible_clients_by_rails(rails)
7192

7293

7394
@dataclass

agentscore_commerce/discovery/__init__.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,16 @@
3333
install_flask_noindex,
3434
is_discovery_path,
3535
)
36+
from agentscore_commerce.discovery.skill_md import (
37+
BuildSkillMdInput,
38+
RailKey,
39+
SkillMdEndpoint,
40+
SkillMdIdentityRequirements,
41+
SkillMdLink,
42+
SkillMdShippingPolicy,
43+
build_skill_md,
44+
compatible_clients_by_rails,
45+
)
3646
from agentscore_commerce.discovery.well_known_mpp import (
3747
PaymentMethodConfig,
3848
WellKnownMppInput,
@@ -45,6 +55,7 @@
4555
"BazaarDiscoveryConfig",
4656
"BuildAgentScoreOpenApiSnippetsInput",
4757
"BuildLlmsTxtInput",
58+
"BuildSkillMdInput",
4859
"DiscoveryProbeOptions",
4960
"DiscoveryProbeResponse",
5061
"DjangoNoindexMiddleware",
@@ -53,6 +64,11 @@
5364
"LlmsTxtSection",
5465
"NoindexNonDiscoveryMiddleware",
5566
"PaymentMethodConfig",
67+
"RailKey",
68+
"SkillMdEndpoint",
69+
"SkillMdIdentityRequirements",
70+
"SkillMdLink",
71+
"SkillMdShippingPolicy",
5672
"WellKnownMppInput",
5773
"X402SampleProbe",
5874
"agentscore_denial_schemas",
@@ -62,7 +78,9 @@
6278
"build_bazaar_discovery_payload",
6379
"build_discovery_probe_response",
6480
"build_llms_txt",
81+
"build_skill_md",
6582
"build_well_known_mpp",
83+
"compatible_clients_by_rails",
6684
"install_flask_noindex",
6785
"is_discovery_path",
6886
"is_discovery_probe_request",

agentscore_commerce/discovery/robots_tag.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@
2121
{
2222
"/openapi.json",
2323
"/llms.txt",
24+
"/skill.md",
25+
"/SKILL.md",
2426
"/.well-known/mpp.json",
2527
"/.well-known/agent-card.json",
2628
"/.well-known/ucp",

0 commit comments

Comments
 (0)