You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Follow-up to #517. Both items below are against src/cmcp_runtime/catalog/approval.py as merged in #519 (ddfd9c3), which is unchanged on main since. They need a maintainer decision rather than a fix, so they were left out of #531, which covers the rest of that review.
1. Records stop verifying once their approvals expire
verify_catalog_change judges approval validity against the verification clock:
instant = int(time.time()) if now is None else now
...
if instant < approval["approved_at"] or instant >= approval["expires_at"]:
raise CatalogApprovalMismatch("approval is not currently valid")
Re-verifying a historical record after expires_at raises CatalogApprovalMismatch: approval is not currently valid. A record that was valid when the catalog was approved becomes permanently unverifiable, so the chain cannot be replayed by an auditor later. That works against the purpose stated in #517, which is to let a verifier establish independently how a catalog reached its approved state.
The underlying question is which instant the reviewer key must be valid at:
approval time, with approved_at and expires_at bounding when the signature could have been produced, and key validity checked against that window;
verification time, the current behaviour, which makes the record an authorization token with a lifetime rather than a provenance record.
Whatever is chosen, the spec doc should say it explicitly. docs/spec/catalog-approval-provenance.md does not currently state which clock governs, and the two readings have opposite consequences for audit.
Suggested resolution: keep the interval as an assertion about when the approval was made, verify key validity against an instant the caller passes in, and default that instant to the record's own approval window rather than time.time(). Revocation stays a verification-time check, since a key revoked today should not validate a record presented today.
2. schemas/catalog-approval.schema.json is never loaded, and does not ship
The schema was added in #519 but nothing reads it. The verifier reimplements structural validation by hand, and the two already disagree. #531 fixes the divergences it found by hand, but one is left deliberately failing there, as a strict xfail, because it cannot be fixed without answering this:
record with approved_at: -1
schema: rejected (-1 is less than the minimum of 0)
verifier: accepted
Hand-written validation drifting from an unused schema will keep recurring until one of them is the source of truth.
Three connected decisions:
Does the verifier load and apply the schema? loader.py refuses to load a catalog at all when catalog-entry.schema.json is missing from the installation. If approval records get the same treatment, follow that pattern; if the schema is documentation only, say so in the doc, which currently claims the record is "defined by" the schema.
If it is loaded, it has to ship. schemas/catalog-approval.schema.json is not in [tool.hatch.build.targets.wheel.force-include] in pyproject.toml, where catalog-entry.schema.json is, so it is absent from the wheel today.
The schema requires previous_record_hash on every record but defines no value for the first one in a chain. An all zero digest validates against the pattern, so a genesis record is representable, just not specified: one producer writes zeros, another writes the digest of an empty change set, and a verifier cannot distinguish "no predecessor" from a real chain link. Pick the convention, an all zero digest being the usual one, and write it into both the schema and the doc. fix(catalog): pin the approval policy verifier-side and bind the chain fields (#517) #531 asserts that convention in a test so the chain has a defined start, but the schema still does not say it.
Suggested resolution: load and apply the schema in verify_catalog_change before the hand-written checks, force-include it in the wheel, define the genesis digest, and keep the hand-written checks only for what JSON Schema cannot express, such as the runtime hash binding and the signature verification.
Tests
#531 adds an agreement test asserting that whatever the schema rejects the verifier rejects too, which is what surfaced the timestamp case above. Both decisions here need their own coverage on top of it: historical replay of an expired record under whichever clock is chosen, and a genesis record validated against the shipped schema rather than against the convention alone. Listed so the decision and its tests land together.
Not in scope
Rollback and freshness of the presented chain, which #517 already assigns to an external pin or transparency receipt, and wiring the module into startup, the CLI, or the proxy.
Follow-up to #517. Both items below are against
src/cmcp_runtime/catalog/approval.pyas merged in #519 (ddfd9c3), which is unchanged onmainsince. They need a maintainer decision rather than a fix, so they were left out of #531, which covers the rest of that review.1. Records stop verifying once their approvals expire
verify_catalog_changejudges approval validity against the verification clock:Re-verifying a historical record after
expires_atraisesCatalogApprovalMismatch: approval is not currently valid. A record that was valid when the catalog was approved becomes permanently unverifiable, so the chain cannot be replayed by an auditor later. That works against the purpose stated in #517, which is to let a verifier establish independently how a catalog reached its approved state.The underlying question is which instant the reviewer key must be valid at:
approved_atandexpires_atbounding when the signature could have been produced, and key validity checked against that window;Whatever is chosen, the spec doc should say it explicitly.
docs/spec/catalog-approval-provenance.mddoes not currently state which clock governs, and the two readings have opposite consequences for audit.Suggested resolution: keep the interval as an assertion about when the approval was made, verify key validity against an instant the caller passes in, and default that instant to the record's own approval window rather than
time.time(). Revocation stays a verification-time check, since a key revoked today should not validate a record presented today.2.
schemas/catalog-approval.schema.jsonis never loaded, and does not shipThe schema was added in #519 but nothing reads it. The verifier reimplements structural validation by hand, and the two already disagree. #531 fixes the divergences it found by hand, but one is left deliberately failing there, as a strict xfail, because it cannot be fixed without answering this:
Hand-written validation drifting from an unused schema will keep recurring until one of them is the source of truth.
Three connected decisions:
loader.pyrefuses to load a catalog at all whencatalog-entry.schema.jsonis missing from the installation. If approval records get the same treatment, follow that pattern; if the schema is documentation only, say so in the doc, which currently claims the record is "defined by" the schema.schemas/catalog-approval.schema.jsonis not in[tool.hatch.build.targets.wheel.force-include]inpyproject.toml, wherecatalog-entry.schema.jsonis, so it is absent from the wheel today.previous_record_hashon every record but defines no value for the first one in a chain. An all zero digest validates against the pattern, so a genesis record is representable, just not specified: one producer writes zeros, another writes the digest of an empty change set, and a verifier cannot distinguish "no predecessor" from a real chain link. Pick the convention, an all zero digest being the usual one, and write it into both the schema and the doc. fix(catalog): pin the approval policy verifier-side and bind the chain fields (#517) #531 asserts that convention in a test so the chain has a defined start, but the schema still does not say it.Suggested resolution: load and apply the schema in
verify_catalog_changebefore the hand-written checks, force-include it in the wheel, define the genesis digest, and keep the hand-written checks only for what JSON Schema cannot express, such as the runtime hash binding and the signature verification.Tests
#531 adds an agreement test asserting that whatever the schema rejects the verifier rejects too, which is what surfaced the timestamp case above. Both decisions here need their own coverage on top of it: historical replay of an expired record under whichever clock is chosen, and a genesis record validated against the shipped schema rather than against the convention alone. Listed so the decision and its tests land together.
Not in scope
Rollback and freshness of the presented chain, which #517 already assigns to an external pin or transparency receipt, and wiring the module into startup, the CLI, or the proxy.