What this is
This is the Python twin of #225, which documents the identical defect in
the Node reference server (reached there via #162 and #179) and already
traces this Python instance to our own merged #122 and #169.
That issue already names this companion; this is it.
What I found
The Python sample server declares UCP_VERSION 2026-04-08
(routes/discovery_profile.json). At that pin,
source/discovery/profile_schema.json $defs/base requires ucp and
separately declares signing_keys as a top level sibling of ucp. That
schema defines no keys field anywhere, nested or otherwise.
discovery.py publishes signing_keys[] correctly at the top level, but
also mirrors the same key into a nested ucp.keys[] that has no basis in
the schema at any pin (rest/python/server/routes/discovery.py:73). Its own
verifier, _extract_keys() in ucp_signing.py, reads only that nested
field (rest/python/server/ucp_signing.py:821):
ucp = document.get("ucp", document)
value = ucp.get("keys") if isinstance(ucp, dict) else None
Every real profile document carries ucp (the schema requires it), so
this reader always takes that branch and looks inside ucp for keys.
It can never see a top level sibling field on any real document. A peer
that publishes only the schema correct top level signing_keys[], with
nothing extra, fails key discovery against this very server.
Observed against main (00333a8)
Fresh clone at main (00333a8), ucp-sdk pinned to 0.4.6 locally (a
fresh uv sync currently resolves 0.5.0 and the server fails to import,
tracked separately in #221/#222). A direct call against _extract_keys
confirms the read side:
_extract_keys({"ucp": {"version": "2026-04-08"}, "signing_keys": [{"kid": "k"}]})
# => [] -- the schema correct shape returns nothing
_extract_keys({"ucp": {"keys": [{"kid": "k"}]}})
# => [{"kid": "k"}] -- only the nonstandard nested shape is read
The booted server confirms the write side matches (curl
/.well-known/ucp): signing_keys[] at the top level, keys[] mirrored
under ucp, both carrying the same webhook JWK.
Expected
signing_keys[] (the 2026-04-08 pin this server declares) published at
the top level, and read from the top level.
Where it comes from
#122 (request signature verification) introduced _extract_keys reading
only a nested keys[], with a comment pre-adopting the ucp#566 rename
that only applies to 2026-08-25 and later, and getting the location wrong
too (ucp#566 places keys as a top level sibling of ucp, never nested).
#169 (webhook signing) then added the top level signing_keys[]
publication and, to satisfy the #122 reader, the nested ucp.keys[]
mirror.
Why CI did not catch it
Every test that exercises key discovery constructed its own profile
document rather than reading the real served output through both the
write and read paths at once. The shared "good" fixture in
signature_integration_test.py was shaped {"ucp": {"keys": [...]}},
matching the actual (wrong) expectation of _extract_keys rather than
the schema. This fixture is a bare dict literal, not an attribute access,
so it was missed by an initial grep for attribute-style .keys usage.
A fix is attached as a companion PR.
What this is
This is the Python twin of #225, which documents the identical defect in
the Node reference server (reached there via #162 and #179) and already
traces this Python instance to our own merged #122 and #169.
That issue already names this companion; this is it.
What I found
The Python sample server declares UCP_VERSION 2026-04-08
(routes/discovery_profile.json). At that pin,
source/discovery/profile_schema.json $defs/base requires
ucpandseparately declares
signing_keysas a top level sibling ofucp. Thatschema defines no
keysfield anywhere, nested or otherwise.discovery.py publishes
signing_keys[]correctly at the top level, butalso mirrors the same key into a nested
ucp.keys[]that has no basis inthe schema at any pin (rest/python/server/routes/discovery.py:73). Its own
verifier,
_extract_keys()in ucp_signing.py, reads only that nestedfield (rest/python/server/ucp_signing.py:821):
Every real profile document carries
ucp(the schema requires it), sothis reader always takes that branch and looks inside
ucpforkeys.It can never see a top level sibling field on any real document. A peer
that publishes only the schema correct top level
signing_keys[], withnothing extra, fails key discovery against this very server.
Observed against main (00333a8)
Fresh clone at main (00333a8),
ucp-sdkpinned to0.4.6locally (afresh
uv synccurrently resolves 0.5.0 and the server fails to import,tracked separately in #221/#222). A direct call against
_extract_keysconfirms the read side:
The booted server confirms the write side matches (curl
/.well-known/ucp):
signing_keys[]at the top level,keys[]mirroredunder
ucp, both carrying the same webhook JWK.Expected
signing_keys[](the 2026-04-08 pin this server declares) published atthe top level, and read from the top level.
Where it comes from
#122 (request signature verification) introduced
_extract_keysreadingonly a nested
keys[], with a comment pre-adopting the ucp#566 renamethat only applies to 2026-08-25 and later, and getting the location wrong
too (ucp#566 places
keysas a top level sibling ofucp, never nested).#169 (webhook signing) then added the top level
signing_keys[]publication and, to satisfy the #122 reader, the nested
ucp.keys[]mirror.
Why CI did not catch it
Every test that exercises key discovery constructed its own profile
document rather than reading the real served output through both the
write and read paths at once. The shared "good" fixture in
signature_integration_test.py was shaped
{"ucp": {"keys": [...]}},matching the actual (wrong) expectation of
_extract_keysrather thanthe schema. This fixture is a bare dict literal, not an attribute access,
so it was missed by an initial grep for attribute-style
.keysusage.A fix is attached as a companion PR.