feat(identity): UCP profile signing helpers (1.3.7)#14
Open
vvillait88 wants to merge 32 commits intomainfrom
Open
feat(identity): UCP profile signing helpers (1.3.7)#14vvillait88 wants to merge 32 commits intomainfrom
vvillait88 wants to merge 32 commits intomainfrom
Conversation
Add four helpers for UCP §6 trust-mode profiles: generate_ucp_signing_key, sign_ucp_profile, verify_ucp_profile, build_jwks_response. Profiles are signed with JWS Compact Serialization over a JCS-canonicalized body; verifiers look up the kid in the merchant's JWKS and validate against the canonical body of the presented profile. Both EdDSA (Ed25519) and ES256 are supported. Cross-language byte parity with @agent-score/commerce Node SDK: profiles signed by either flavor verify in the other. Bumps to 1.3.7. joserfc is an optional extra (`pip install agentscore-commerce[ucp]`); vendors install it only when publishing signed UCP profiles. /.well-known/jwks.json is added to the default discovery path allowlist. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- markdown-it-py 4.1.0 → 4.2.0 - pydantic-settings 2.14.0 → 2.14.1 - types-requests 2.33.0.20260503 → 2.33.0.20260508 - urllib3 2.6.3 → 2.7.0 Full suite green (737 passed, 3 skipped). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Reviewer audit surfaced three real issues + several gaps. Fixes:
Security:
- verify_ucp_profile pre-deserializes the JWS protected header and
enforces kid + typ='ucp-profile+jws' + alg in {EdDSA, ES256} BEFORE
passing to joserfc, closing the missing-kid bypass where joserfc's
KeySet-based verify silently iterates every key.
- Rejects duplicate kids in JWKS.
- _canonicalize_profile rejects float values at sign-time
(cross-language float canonicalization is not stable).
Ergonomics:
- New UCPVerificationError (ValueError subclass) with discriminated
`code` attribute. Wraps joserfc's BadSignatureError / DecodeError so
consumers don't import joserfc internals.
- New UCPSigningKey.from_jwk() classmethod fixes the broken
README/Mintlify example: `UCPSigningKey(**key.public_jwk)` raises
TypeError because dataclass has fixed fields. The classmethod routes
known fields + captures `x`/`y` into `extras`.
Docs:
- README documents kid/typ/alg enforcement, error codes, float defense,
HSM/KMS via joserfc, key rotation runbook.
- Mintlify python-commerce.mdx fixed.
- New examples/signed_ucp_merchant.py demonstrates the full sign+verify
flow with env-var-loaded key + asyncio.Lock-protected cache + cache
headers + alg auto-detection from JWK shape.
Tests:
- Added 14 new tests covering kid-less JWS rejection, wrong typ,
alg-confusion (HS256 hostile key), dup-kid, body mismatch, no_signature,
tampered signature segment, malformed JWS, EdDSA determinism, ES256
non-determinism, float rejection, UCPSigningKey.from_jwk round-trip.
- New cross-language fixture corpus (tests/fixtures/cross-lang/) with 6
signed profiles (3 Node + 3 Python) that both SDKs verify, locking in
byte parity at CI level.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Mirrors node-commerce hardening for cross-language parity.
- verify_ucp_profile: wrap UnsupportedHeaderError (RFC 7515 §4.1.11
unrecognized `crit`) into UCPVerificationError('unrecognized_critical_header').
- verify_ucp_profile: malformed JWKS shape (non-dict, missing keys array)
now emits UCPVerificationError('malformed_jwks') instead of cryptic
AttributeError. Same for non-dict signed_profile input.
- verify_ucp_profile: reject JWKs with use != 'sig' as 'unusable_key'
(RFC 7517 §4.2).
- sign_ucp_profile: enforce kid in profile.signing_keys[] at sign-time.
- UCPSigningKey.from_jwk: reject `oct` symmetric keys + missing
`kid`/`kty` with typed ValueError instead of bare KeyError.
- Drop 3 redundant `del joserfc` statements.
- Cross-language fixture corpus: 6 → 12 fixtures (minimal, ES256-rails,
extras-int, capability, unicode, multi-key per language).
- Fix vacuous key-order-independence test (json.dumps preserves order on
Python 3.7+); now hand-constructs reverse insertion-order dict.
- README KMS claim rewritten — `OKPKey`/`ECKey` import key MATERIAL, they
don't wrap remote signers.
Tests: 773 pass, 3 skipped. ruff + ty clean.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
HIGH:
- verify_ucp_profile: _peek_jws_header now asserts the decoded header is a
dict; non-dict JWS header (e.g. `null`, list, number) was raising
AttributeError instead of UCPVerificationError(malformed_jws). Parity
with Node.
Other:
- UCPProfile.to_dict: extras-collision filter rejects keys matching
reserved profile fields (signing_keys, services, etc.) so
`extras={'signing_keys': [...]}` can't silently destroy the explicit
field.
- generate_ucp_signing_key, sign_ucp_profile, verify_ucp_profile: wrap
joserfc calls in a context manager that suppresses the SecurityWarning
emitted by joserfc on every EdDSA op (RFC 9864 deprecation, but UCP §6
explicitly mandates EdDSA support). pyproject.toml ships an additional
pytest filterwarnings rule for tests that hand-construct joserfc calls.
- sign_ucp_profile: kid must be a non-empty string. Empty kid silently
passed the in-signing_keys check before.
Tests: 773 pass, 3 skipped. ruff + ty clean.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ADME codes) Tighten the EdDSA SecurityWarning suppression to the exact joserfc message and class so a future, unrelated EdDSA-vulnerability warning is no longer swallowed: scope is now `joserfc.errors.SecurityWarning` with literal regex `^EdDSA is deprecated via RFC 9864$` in both the helper and the pyproject `filterwarnings`. Add explicit canonicalization tests for NaN, positive-infinity, and negative-infinity floats (matching node-commerce parity). Add coverage for `unusable_key` (JWK with `use=enc`), non-string `signature` field (int / None / list / dict), non-dict JWKS entries, and JWS protected headers that decode to a JSON array. Extend the README error-code list with `unusable_key`, `malformed_jwks`, and `unrecognized_critical_header`. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…non-ASCII keys fixture) verify_ucp_profile now rejects matched JWKs whose declared alg disagrees with the JWS protected header alg (RFC 7517 §4.4: a JWK with `alg` constrains its use to that algorithm). Closes a small security gap where a JWKS publisher advertising the wrong alg on a key would silently let mismatched signatures through joserfc's verify path. _suppress_joserfc_eddsa_warning docstring corrected. The previous text claimed joserfc emits the RFC-9864 SecurityWarning at every call including key generation; empirically the warning fires only at JWS sign/verify (in the JWS registry), not at OKPKey.generate_key. Removed the redundant suppression wrapper around generate_key in generate_ucp_signing_key — it was a no-op. New cross-language fixture py-emoji-keys.json mirrors the node-emoji-keys.json fixture landing in node-commerce. Locks codepoint-aware key sort: Python's default sorted() orders by Unicode codepoint, JS default sort orders by UTF-16 code units which diverges for supplementary-plane chars (e.g. U+1F377). The fixture covers BMP CJK Compatibility (U+8C48), non-BMP wine glass (U+1F377), and ASCII so both languages must explicitly sort by codepoint to maintain byte parity. Tests: 786 pass + 3 skipped, 95.17% coverage. ruff + ty clean. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ject set) Tighten cross-lang fixture corpus + extras-collision defenses + _reject_floats walking so cross-language byte parity has airtight regression coverage. Cross-lang fixture corpus: previous py-emoji-keys.json used chars (cafe, JP, wine glass, CJK Compat) where UTF-16 first-unit sort and Unicode codepoint sort produce identical canonical bodies, so the codepoint-sort fix was silently untested in the verifier-side parity check. Regenerates py-emoji-keys.json with a key set that genuinely distinguishes the two: BMP private use (U+E000, codepoint 57344) alongside supplementary-plane wine glass (U+1F377, codepoint 127863, UTF-16 first unit 55356). Codepoint sort puts U+E000 BEFORE U+1F377; UTF-16 first-unit sort reverses that. Both repos now ship both node-emoji-keys.json and py-emoji-keys.json in their fixture corpus so each repo's verifier validates the OTHER language's canonical body. A regression in either language's key sort surfaces here. UCPProfile.to_dict reserved set adds __class__, __dict__, __init__ for symmetry with node-commerce's prototype-pollution defense. Python's runtime model doesn't have JS-style __proto__ pollution but the reserved-name check guards against bidirectional vendor data passing through both SDKs and surprising downstream consumers. _reject_floats now walks set / frozenset in addition to list / tuple. A profile containing a set with a float would otherwise crash later in json.dumps with an untyped TypeError; catching it at the same surface as the list/tuple path keeps the error message consistent. README joserfc note adds tested-version pin (joserfc>=1.0.0,<2) mirroring node-commerce's "tested against jose v5.x; pin jose@^5". Tests: 800 pass + 3 skipped, 95.22% coverage. ruff + ty clean. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- verify_ucp_profile: compare signed payload against canonical body using hmac.compare_digest for constant-time equality, matching the node-commerce sibling. - UCPProfile.to_dict reserved set: replace Python dunders with __proto__ / constructor / prototype so the reserved set is byte-identical across the Node and Python SDKs (Node-signed profiles carrying those keys are rejected by both). - README: tell publishers to set Cache-Control: public, max-age=300 on /.well-known/jwks.json and wait at least that long before removing the old JWK during rotation. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Close cross-language byte-parity hole: Python's _reject_floats only walked for
float, while Node's stableStringify hard-rejects integers outside Number.MAX_SAFE_INTEGER
(2^53 - 1). A Python-signed profile with an oversized int produced a valid
self-verifying envelope that Node could not parse. Sign-time rejection on the
Python side now matches Node, plus a checked-in cross-lang fixture
(int-boundary) covers the safe-edge integer for both languages.
Renames _reject_floats to _reject_unsafe_numbers (private; not exported) and
extends it to raise ValueError for any int whose magnitude exceeds 2^53 - 1.
bool subclass of int still allowed; container walking (dict, list, tuple, set,
frozenset) preserved.
Tests: 7 new cases under TestUnsafeNumberRejection (max_safe boundary accept,
min_safe boundary accept, 2^53 reject, 2^60 reject, -(2^53) reject, nested
2^60 reject, bool accept). Existing float rejection tests kept intact under
the broader class name.
Cross-lang fixture: tests/fixtures/cross-lang/{py,node}-int-boundary.json
exercise max_safe_int / min_safe_int / small_int / neg_small_int / zero. Both
fixtures verify in both languages. node-int-boundary.json was generated by the
node-commerce companion script and copied here.
scripts/generate_int_boundary_fixture.py: one-shot regenerator.
Tests: 809 pass + 3 skipped, 95.22% coverage. ruff + ty clean.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…y parity)
Wrap verifier-side _canonicalize_profile call so JCS-incompatible
numbers in a received profile emit a typed UCPVerificationError
(body_mismatch) instead of a raw ValueError. Symmetric with the Node
sibling fix.
Reorder verify_ucp_profile shape checks profile-first so verify(None,
malformed_jwks) returns no_signature, matching node-commerce.
Walk dict keys in _reject_unsafe_numbers so {2**60: "a"} raises at
sign-time. String dict keys (including numeric-looking ones like
"1.5") remain allowed; bool keys remain allowed (bool is int).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ity)
Move the JWS `crit` header check ahead of the JWKS kid lookup in
`verify_ucp_profile` so a crit-violating JWS with a missing/duplicate/unusable
kid surfaces `unrecognized_critical_header` instead of `kid_not_found` /
`duplicate_kid` / `unusable_key`.
Matches node-commerce's manual peek order: typ -> alg -> kid -> crit ->
kid_lookup. Without this, joserfc only enforces `crit` after its own kid
lookup, so the two SDKs diverged on inputs like `{kid: 'real',
crit: ['unknown']}` against a JWKS missing 'real'.
Adds a regression-guard test that hand-crafts a JWS carrying both a crit
violation AND a missing kid; existing crit + kid_not_found single-fault
tests still pass because the new check fires only when `crit` is present.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Round-17 added a `crit` precedence check gated on `if crit is not None`, which let JSON null fall through to joserfc's iter and surface a raw TypeError instead of the typed UCPVerificationError. Switch to a key-presence gate (`if "crit" in header`) so null hits the shape branch and emits malformed_jws, matching node-commerce's behavior. Adds regression tests for crit=null, crit=[], and crit="string" (all RFC 7515 §4.1.11 violations); the existing crit=['fakething'] coverage keeps unrecognized_critical_header. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Mirrors the rejection in core/api/src/lib/canonicalize.ts so the SDK canonicalizer stays byte-symmetric across the Node and Python siblings on any pre-ES2019 V8 (where JSON.stringify still escapes these codepoints) or browser-side verifier path; today's V8 emits them raw, so the divergence is theoretical but the contract is now consistent with the upstream check. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The example used `UCPSigningKey(**key.public_jwk)`, which raises TypeError because UCPSigningKey only accepts kid/kty/alg/use/crv/extras (the JWK `x`/`y` fields belong in extras). Switch to the `UCPSigningKey.from_jwk(...)` classmethod, matching the runnable example in examples/signed_ucp_merchant.py. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ode sibling The API can return account_verification with either null or empty-string for un-set fields depending on the row state. The python builder used dict.get(k, DEFAULT) for age_bracket/jurisdiction, which returns None verbatim when the key is present-but-null instead of falling through to the default. The node sibling used `??` (collapse null/undefined only, pass empty-string verbatim). For the same AssessResult shape, that meant the two SDKs emitted different canonical claims blocks, so a profile signed in one language failed verify in the other. Switch python to `dict.get(k) or DEFAULT` for age_bracket and jurisdiction so null AND empty-string both fall through to the schema default. The kyc_level and verified_at branches already used `or` semantics; verified and left as-is. Node sibling switched to `||` in the same round. Adds a data-driven-claims cross-lang fixture that, unlike the rest of the corpus, exercises build_ucp_profile's actual data path (constructs a synthetic AssessResult with the API "missing" sentinels and lets the builder coalesce). Both languages now emit identical canonical bytes for the input. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…+ set/frozenset reject Three reviewer LOW findings on the UCP profile path: * `build_ucp_profile` now falls back to the typed `AssessResult.operator_verification` (and ad-hoc `account_verification`) field when `data.raw` doesn't carry the block. Production callers populate `raw`, but a hand-constructed `AssessResult(raw=None)` was silently dropping the operator verification block, diverging from the node sibling's typed-field read path. * `UCPService`, `UCPCapability`, and `UCPSigningKey` now mirror `UCPProfile.to_dict`'s reserved-key collision guard so vendor `extras` can't silently overwrite a canonical declared field via `out.update(extras)`. `UCPPaymentHandler` has no `extras` attribute and is unaffected. * `_reject_unsafe_numbers` now rejects `set` / `frozenset` outright with a typed `ValueError` (mirroring node's `stableStringify: Set values are not allowed`). Previously an empty set or a set-of-valid-strings fell through cleanly and surfaced a raw `TypeError` from `json.dumps` later. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
verify_ucp_profile now treats explicit JSON null on JWK.use / JWK.alg as absent (skip-on-null) rather than rejecting via joserfc's stricter KeySet.import_key_set validation; both fields are optional per RFC 7517 and the Node sibling now uses ``!= null`` semantics, so a JWK with ``"use": null`` / ``"alg": null`` should pass language-symmetric verify. The ``is not None`` checks above already skip null; we now also drop the explicit-null entries before handing the JWK to joserfc so its key parameter registry doesn't reject them downstream. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…FC 7515 RFC 7515 §4.1.11 requires crit array entries to be strings. The previous shape check accepted arrays like [42] or [42, "valid"] because it only guarded against non-list and empty-list shapes. Node-commerce already rejects these with malformed_jws; Python now matches. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two cross-language parity fixes from round-29 SDK review: LOW-1: build_ucp_profile now reads operator_verification and account_verification from the typed AssessResult fields first, falling back to data.raw only when the typed field is missing. Previously raw won, diverging from node-commerce which reads typed fields directly. With raw and typed in disagreement, both languages now pick the same source so a profile signed by one verifies in the other. LOW-2: _reject_unsafe_numbers now rejects bytes / bytearray with a typed ValueError before json.dumps can raise its raw "Object of type bytes is not JSON serializable" TypeError. Mirrors the node sibling's "typed arrays are not allowed" rejection in stableStringify. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ield The UCP profile builder reads the typed AssessResult.operator_verification first then falls back to data.raw, but the typed account_verification branch was unreachable because the field wasn't declared on the dataclass. Add the optional account_verification: dict[str, Any] | None field for symmetry with operator_verification (mirrors node-commerce AgentScoreData.account_verification), populate it in AgentScoreClient._project, and adjust the UCP builder to read the typed field directly. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…nt_handler config
Three cross-language parity fixes for build_ucp_profile / UCPPaymentHandler:
1. `data.account_verification == {}` (and `data.operator_verification == {}`)
means "API returned the block with no populated values" and now wins over
the `data.raw` fallback. The previous `if not account_verification:` check
treated empty-dict as falsy and bled raw values through. Mirrors the Node
sibling, which reads the typed field directly without consulting raw.
2. `UCPPaymentHandler.to_dict()` always emits `config` (even when empty).
TypeScript serializes `{name: 'tempo', config: {}}` with `config`
preserved; the dataclass default is `field(default_factory=dict)` so the
field is always a dict. The previous `if self.config:` truthy gate
produced byte divergence on explicit `config={}` callers.
3. New `typed-claims` cross-lang fixture exercises the typed-field-only read
path (`AssessResult(account_verification={...}, raw=None)`) that the
existing `data-driven-claims` fixture didn't cover (it uses `raw=`).
Both languages must produce byte-identical canonical bytes for the typed
path or cross-lang verify silently drifts in production.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The previous comment claimed parity with node-commerce read order, but Node has no raw fallback at all. Rewrite to accurately describe the behavior: typed fields are canonical; raw fallback is a Python-only hatch for hand-constructed AssessResult instances and may not verify cross-language. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Run `uv lock --upgrade`. Transitive bump only: - propcache 0.4.1 -> 0.5.2 All declared direct deps remain at their latest versions within existing constraint ranges. joserfc held at 1.x. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Replace em dashes with periods, semicolons, colons, or parens across README.md, CLAUDE.md, CONTRIBUTING.md, examples/README.md. - examples/README.md: remove stale claim that Python lacks `create_x402_server` / `create_mppx_server` factories; both have shipped (in `agentscore_commerce.payment`) and are now native. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…y import CodeQL flagged the `/_selftest/ucp` route returning `str(exc)` to the HTTP response body as information exposure. Match the round-26 sanitization pattern used in core/store: log the full exception server-side via logger.exception, expose only `type(exc).__name__` + the structured verification code to the caller. The error class name is enough for an operator to triage without revealing internal verification machinery. Also drop the unused `Any` import from tests/test_ucp.py; the only cast() target gets retyped to `OperatorVerification` (the actual field type), which keeps the typed-empty-wins-over-raw test intent intact. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
….4.0) UCP §6 does not define a profile-as-JWS typ, and UCP capability names must follow reverse-DNS namespacing (``^[a-z][a-z0-9]*(?:\.[a-z][a-z0-9_]*)+$``). The previous ``typ: ucp-profile+jws`` and ``agentscore-identity`` capability name implied UCP-canonical signing/slots they aren't. Renamed to vendor-namespaced honest forms: - JWS protected header ``typ``: ``ucp-profile+jws`` -> ``agentscore-profile+jws`` (matches the ``agentscore-risk-signal+jws`` pattern AP2 already uses). - Capability name: ``agentscore-identity`` -> ``sh.agentscore.identity`` (passes the UCP regex; namespace authority ``agentscore.sh``). - Schema URL path: ``agentscore-identity.v1.json`` -> ``sh-agentscore-identity-v1.json`` (path matches the new capability name; URL hosted under namespace authority ``agentscore.sh`` per UCP convention). All 10 cross-lang fixture scenarios re-signed via the new ``scripts/regenerate_cross_lang_fixtures.py`` orchestrator. Hand-crafted ``*-capability.json`` fixtures bumped to the new name to keep the corpus honest about what callers should publish. 871 tests pass at 95.20% coverage. Cross-lang verify against the node sibling's regenerated ``node-*`` corpus passes byte-identically for the data-driven and typed-claims fixtures. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The capability scenario in the cross-lang orchestrator hand-crafted the old `https://agentscore.sh/schema/identity/1` URL while the SDK's build_ucp_profile auto-injects the new `https://agentscore.sh/schemas/ucp/sh-agentscore-identity-v1.json`. The inconsistency made the corpus dishonest about what callers should publish. Update the orchestrator script to use the canonical URL and regenerate all 20 fixtures so canonical bytes stay byte-identical between Node and Python siblings. Also bump the README stability section to reflect the current 1.4.0 version. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ndary cross-lang regenerator
The Python regenerator passed `extras={"extras": {...}}` for the emoji-keys
and int-boundary scenarios, so the test keys ended up nested under a literal
"extras" key in the profile body. The Node sibling passes `extras={...}`
directly and `buildUCPProfile` flattens via `Object.assign(profile, extras)`,
so node fixtures had the keys at profile root interleaved with canonical
fields. Net: a regression in either language's top-level key-sort that fires
only when non-ASCII keys interleave with canonical fields would have surfaced
in node fixtures but not py fixtures, defeating the cross-lang corpus.
Also clarify the typed-empty-wins-over-raw behavior in `build_ucp_profile`
as Python-only (Node has no raw fallback at all, so the asymmetry covers
both directions: raw fallback AND the empty-dict-suppresses-fallback rule).
Both flavors of py-emoji-keys.json and py-int-boundary.json now match
node-* canonical bodies byte-for-byte (modulo per-run keypair material).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…onal convention
Two cross-language parity fixes:
1. UCPPaymentHandler.to_dict() now omits the `config` key when empty. Node's
`UCPPaymentHandler.config` is a TypeScript optional property and
`buildUCPProfile` passes the array verbatim, so a Node caller writing
`{ name: 'tempo' }` shipped a wire profile WITHOUT `config`. Python was
force-emitting `config: {}` for that same input, producing different
canonical bytes between SDKs for the same logical input. Explicit
`config={}` is semantically identical to absent and follows the same
omit rule.
2. Standalone int-boundary fixture script now uses build_ucp_profile(extras=...)
instead of hand-building a profile dict with a top-level `extras` key.
The orchestrator was already correct; the standalone version (used for
one-off debug regen of a single fixture) was producing canonical bytes
with a nested `extras` block while the orchestrator-produced fixture was
flat. Mirrors how the data-driven-claims and typed-claims standalone
scripts already work.
Cross-lang fixture corpus regenerated; new tests cover the omit behavior.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…estrator The cross-lang fixture orchestrator (regenerate_cross_lang_fixtures.py) already covers int-boundary, data-driven-claims, and typed-claims; the per-scenario one-shots duplicate the work with subtly different json.dumps flags (no ensure_ascii=False), which would silently drift the on-disk fixture if a future unicode field landed at the relevant levels. Drop them so the orchestrator is the single source of truth. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…arity
Python's chained `or` returns the last falsy value, so
`account_verification.get("verified_at") or operator_verification.get("verified_at")`
returned `""` when both source values were empty strings. The Node sibling
returned `null` via `a || b || null`. Cross-language byte-parity divergence
on a corner the doc comment explicitly calls out (API may emit either null
or "" for un-set fields).
Add a trailing `or None` to match Node, plus a regression test covering
the both-empty corner. Also add a per-element shape parity note near the
dataclasses describing how Node's TypeScript-interface + flat-extras
approach reaches the same net contract as Python's dataclass-with-extras-
slot + runtime collision guard.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
generate_ucp_signing_key,sign_ucp_profile,verify_ucp_profile,build_jwks_response. Profiles are JWS-signed (Compact Serialization) over a JCS-canonicalized body. Both EdDSA (Ed25519, default) and ES256 supported.@agent-score/commerceNode SDK 1.3.4 — profiles signed by either SDK verify in the other.joserfcis an optional extra (pip install agentscore-commerce[ucp]); vendors install it only when publishing signed UCP profiles. Unsignedbuild_ucp_profilekeeps working./.well-known/jwks.jsonadded to the default discovery-path allowlist.Test plan
kidInvalidKeyIdError🤖 Generated with Claude Code