Skip to content

Substrate catch-up to the Server 0.6 triple, and CC 1.0-rc5 licensure conformance - #140

Merged
emooreatx merged 3 commits into
mainfrom
fold-prep/substrate-catchup
Oct 1, 2026
Merged

emooreatx merged 3 commits into
mainfrom
fold-prep/substrate-catchup

Conversation

@emooreatx

Copy link
Copy Markdown
Contributor

Two commits, independently reviewable. The first is routine; the second fixes a defect the constitution named this Registry for.

4fc9c96 — substrate catch-up

persist v40.0.0 → v42.1.0, edge v20.1.1 → v21.1.0, verify v14.1.0 → v15.0.0.

This is CIRISServer 0.5.204's exact triple, which is the point: registry-core folds into that workspace, so a divergence here becomes a duplicate substrate rev in the composed build. The set is internally coherent — edge 21.1.0 pins persist 42.1.0, and edge and persist both pin the verify family at 15.0.0 — so there is one verify in the graph, not two. Confirmed in the lockfile: crypto, keyring and verify-core all resolve to exactly 15.0.0.

Zero source changes. KeyRecord / Attestation / Revocation / ScrubSig are field-identical across the gap, and every symbol this crate imports survives (put_attestation, list_attestations_by, list_keys_by_identity_type, lookup_public_key, federation_directory, ThresholdMember with fields unchanged, ThresholdSignature, verify_threshold_signatures, jcs::canonicalize).

verify v15.0.0 is a major — FedCode becomes non_exhaustive, admission unconstructible unless verified (CIRISVerify#274). Registry consumes neither, so the break does not reach us. MSRV stays 1.86.

Re-verified the two pins that bite in production rather than CI: a single libsqlite3-sys 0.28 (the bundled union holds), and no persist federation type is serialized into an HTTP response.

4679dd3 — CC 1.0-rc5 licensure conformance

rc5 ratified the authority triad (CC 2.4.1.2.1, from CIRISConstitution#100) and named this Registry twice while doing it. Both citations were live defects on GET /v1/partner/{key_id} — a public, unauthenticated surface.

authority_id was the tax id

part_3 §3.3.9 rules the authority_id is the organisation's org_id UUID, and explicitly not "the legal registration number the reference Registry's PartnerRecord.organization_id carries (jurisdiction-scoped, mutable, PII-adjacent)."

The emit site was authority_id: row.organization_id.clone() — the column the schema comments as Tax ID / Registration number. So the surface published a tax id and named the wrong thing as the licence's provenance.

The correct id was one join away and unused: organizations.org_id, reachable through organizations.partner_id REFERENCES partners(partner_id), covered by idx_organizations_partner. New db::org_id_for_partner does that reverse join, taking the oldest row by created_at with an org_id tiebreak — the column is indexed but not unique, and an authority_id that flipped between rows would be worse than a wrong one: unstable licence provenance.

No org row now yields no licence entry rather than a guess. Emitting a licence whose authority we cannot name asserts provenance we do not have.

suspended and revoked were the same string

rc5: "suspended is reversible; revoked is terminal — the one operational distinction a consumer MUST honour, and a status a consumer MUST NOT infer from the other."

The mapping was if status == 1 { "active" } else { "inactive" }, collapsing PARTNER_SUSPENDED(2) and PARTNER_REVOKED(3) into one token. A consumer reading "inactive" could not tell whether a partner may return — the exact inference rc5 forbids, destroyed at the emitter. "active" was also outside the canonical vocabulary.

Now 1 → issued, 2 → suspended, 3 → revoked. UNSPECIFIED(0) and unrecognised codes map to a deliberately non-canonical "unspecified" rather than issued: the row exists so the licence is not absent, but nothing licensed it, and a consumer that does not recognise the token withholds. Mapping unknown to issued would escalate "we do not know" to "valid".

⚠ Wire change

/v1/partner/{key_id} → licensure[].authority_id changes value (tax id → org UUID) and licensure[].status changes vocabulary (active/inactive → issued/suspended/revoked/unspecified). Both are the fix; anything keyed on the old strings sees new ones. A partner with no organisation row now returns an empty licensure[].

Three tests pin the rules, each of which regressed once: suspended and revoked are distinguishable; emitted statuses are canonical or deliberately not, and unknown never borrows a canonical one; authority_id parses as a UUID while the tax-id shapes it used to carry do not.

Green: 111 lib + 19 crypto_properties + 26 capability_properties.

Refs CIRISConstitution#100, #76, #62

🤖 Generated with Claude Code

https://claude.ai/code/session_01Arv1jQhQPe5Ab92mJfyLLG

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 4679dd3f28

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +107 to +111
let row: Option<(uuid::Uuid,)> = sqlx::query_as(
"SELECT org_id FROM organizations WHERE partner_id = $1 \
ORDER BY created_at ASC, org_id ASC LIMIT 1",
)
.bind(uuid)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Query the TEXT identifiers as strings

When a UUID-shaped partner has a linked organization, this lookup cannot return it successfully: migration 001_initial_schema.sql defines both organizations.partner_id and organizations.org_id as PostgreSQL TEXT, but the query binds a uuid::Uuid and attempts to decode the selected TEXT as uuid::Uuid. PostgreSQL/SQLx will report a parameter or column type mismatch; partner_composition catches that error and silently emits an empty licensure array for every affected partner. Bind partner_id as &str and decode the row as String.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Correct, and already fixed on the current head. This review ran against 4679dd3; 10d14f2 binds partner_id as text and decodes org_id as String, and the emit site now checks the value parses as a UUID before publishing it as an authority_id (a non-UUID org id is withheld with a warning).

The same defect was found independently by booting the build against Postgres 16 and seeding partners: every licensure array came back empty. After the fix, seeded ACTIVE / SUSPENDED / REVOKED partners serve a UUID authority and three distinct statuses.

emooreatx and others added 3 commits September 27, 2026 20:23
…d into revoked (CC 1.0-rc5)

CC 1.0-rc5 ratified the authority triad (CC 2.4.1.2.1, from CIRISConstitution#100)
and, in doing so, named this Registry twice. Both citations were defects here.

## authority_id was the tax id

part_3 §3.3.9: when a licensing authority is an organisation, the `authority_id`
segment of `licensure:{authority_id}` is its `org_id` — the UUID
`org_membership.org_id` joins on, through which the organisation's keys resolve —
and explicitly NOT "the legal registration number the reference Registry's
`PartnerRecord.organization_id` carries (jurisdiction-scoped, mutable,
PII-adjacent)."

`/v1/partner/{key_id}` emitted exactly that: `partners.organization_id`, which
the schema comments as "Tax ID / Registration number". So a public, unauthenticated
surface was publishing a tax id AND naming the wrong thing as the licence's
provenance.

The correct id was one join away and unused: `organizations.org_id`, reachable
via `organizations.partner_id REFERENCES partners(partner_id)` (covered by
`idx_organizations_partner`). New `db::org_id_for_partner` does that reverse
join. The column is indexed but not unique, so it takes the oldest by
`created_at` with an `org_id` tiebreak — an authority_id that flipped between
rows would be worse than a wrong one: unstable licence provenance.

No org row now yields NO licence entry rather than a guess. Emitting a licence
whose authority we cannot name asserts provenance we do not have; absent is both
honest and fail-secure.

## suspended and revoked were the same string

rc5: "`suspended` is reversible; `revoked` is terminal — the one operational
distinction a consumer MUST honour, and a status a consumer MUST NOT infer from
the other."

The mapping was `if status == 1 { "active" } else { "inactive" }`, collapsing
PARTNER_SUSPENDED(2) and PARTNER_REVOKED(3) into one token. A consumer reading
`"inactive"` could not tell whether a partner may return — the exact inference
rc5 forbids, destroyed at the emitter. `"active"` was also outside the canonical
vocabulary.

Now: 1 → `issued`, 2 → `suspended`, 3 → `revoked`. UNSPECIFIED(0) and any
unrecognised code map to a deliberately NON-canonical `"unspecified"`, not to
`issued`: the row exists so the licence is not absent, but nothing licensed it,
and a consumer that does not recognise the token withholds — the correct
outcome. Mapping unknown to `issued` would escalate "we do not know" to "valid".

Three tests pin both rules, each of which regressed once: suspended and revoked
are distinguishable; emitted statuses are canonical or deliberately not, and
unknown never borrows a canonical one; authority_id parses as a UUID while the
tax-id shapes it used to carry do not.

Green: 111 lib + 19 crypto_properties + 26 capability_properties.

Refs CIRISConstitution#100, CC 2.4.1.2.1, CC part_3 §3.3.9

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Arv1jQhQPe5Ab92mJfyLLG
…kup binds TEXT (#139)

Found by booting against a fresh Postgres 16 and seeding partners.

ebe637a added responsible_party and public_contact_email to every partner
query but put the ALTER only in the legacy database/migrations/ tree, never in
the sqlx tree the binary runs. On a database this binary migrated itself,
lookup_partner fails with 'column responsible_party does not exist': gRPC
LookupPartner errors and /v1/partner/{key_id} serves an empty composition,
because the handler discards the error. Migration 031 adds both columns with
ADD COLUMN IF NOT EXISTS, so a database that already has them is untouched.

The same boot showed org_id_for_partner was wrong as first written: it bound
and decoded UUIDs, but organizations.org_id and .partner_id are TEXT in the
schema this binary migrates (the UUID types are the legacy tree's). It now
binds text, and the emit site checks the org id parses as a UUID before
publishing it as an authority_id; a non-UUID value is withheld with a warning.

Exercised end to end over seeded ACTIVE / SUSPENDED / REVOKED partners: UUID
authority, three distinct statuses, no tax id in any response, and 031 applied
over existing rows.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VhhR4ntuczmABtdh6RifqF
…218 substrate)

Substrate to CIRISServer 0.5.218's exact triple: persist v52.0.1, edge v38.1.0,
verify v18.0.0. The set is coherent (edge pins persist 52.0.1, both pin verify
18.0.0), and the lock carries one of each. No source change was needed for the
bump; the one new upstream field (Revocation) is on a type this crate never
constructs.

CEG-Version header 1.0-rc4 -> 1.0-rc5. rc6 is open upstream but not cut, so
this claims the last released text, whose authority triad the licensure
surface implements.

MAJOR because /v1/partner/{key_id} changes on the wire (authority_id value,
status vocabulary) and the persist migrations through V166 set a rollback
floor: a v3.0.0 image will not boot against a database this release migrated.

Verified: 117 lib + 19 + 26 + 3 fold_router; the no-default-features fold build
passes with sqlx / tonic / tokio-postgres absent; booted against Postgres 16
with persist on Postgres (161 migrations to V166, sqlx to 031, 6/6 identity
keys).

Closes #139

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VhhR4ntuczmABtdh6RifqF
@emooreatx
emooreatx force-pushed the fold-prep/substrate-catchup branch from 4679dd3 to f7d80a6 Compare October 1, 2026 14:03
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.
To continue using code reviews, you can upgrade your account or add credits to your account and enable them for code reviews in your settings.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant