Until 2026-10-23 a principal's id was any string: protocol.ncl typed Subject.id, Holders' holders and HatsOf's principal as String; signatiod's PutPrincipal and `signatio bootstrap` checked nothing; Cedar takes the id as a JSON string literal, so it takes anything. That a ULID or a short name worked was a fact of the code, not something a consumer could rely on.
A consumer measured the gap on 2026-10-22: wuwei-observer's view writes each act's Party as a reference `<kind>:<id>` and names its persons by a ULID that is to become the principal's name in PutPrincipal when the view links with the authority. It asked whether signatio or Cedar restrict anything a ULID does not meet. Nothing did — and nothing promised that nothing would. The principal decided to fix the grammar in the protocol.
A PRINCIPAL'S ID HAS ONE GRAMMAR, AND IT IS THE PROTOCOL'S. schemas/protocol.ncl publishes PrincipalId: 1 to 128 of A-Z a-z 0-9 . _ -, the first a letter or a digit (`^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$`). It types every place the protocol names a principal — Subject.id, Holders' holders, HatsOf's principal — and a consumer imports it as it imports the rest of the protocol (adr-001).
NO ':' AND NO '/'. A consumer's reference to a principal puts the kind before the id with ':' (`person:<id>`); a hat's and a document's ids join the project to the name with '/' (`<project>/<hat>`, cedar.rs). An id holding either could not be told apart from a reference or a scoped name.
REFUSED WHERE A PRINCIPAL IS CREATED. signatiod refuses PutPrincipal with an id outside the grammar, writing nothing; `signatio bootstrap` refuses its administrator before it opens the store. Every other op that names a principal (PutClient, Assign, SetKeyStatus, HatsOf) names one that must already exist.
THE KIND IS NOT IN THE ID. A principal's kind is its own field (PutPrincipal's `kind`, Subject's `kind`), and it never changes; a consumer that writes `person:<id>` maps it to `{ id, kind = Person }` and sends the bare id.
WHAT THIS DOES NOT DECIDE: a grammar for client ids, project ids or hat names; checking a Subject's id on the client side (signatio-attest, signatio-person); a migration of stores holding ids outside the grammar — the installed authority holds none, by the principal's statement of 2026-10-23, not by a measurement (adr-002 forbids reading the store).
protocol.ncl types Subject.id with PrincipalId, whose grammar is 1 to 128 of A-Z a-z 0-9 . _ -, the first a letter or a digit
signatiod's PutPrincipal and signatio bootstrap refuse an id outside the grammar
Keep any string and let each consumer choose its own ids
Rejected: Nothing a consumer could rely on: the same code that accepts a ULID today may refuse it tomorrow without a change to the contract.
Lower case only, 1 to 64 (`^[a-z0-9][a-z0-9-]{0,63}$`)
Rejected: Refuses a ULID in its canonical upper case, which wuwei-observer had already frozen in its contract v4.
Refuse an id outside the grammar on every op that reads a principal
Rejected: An id created before the decision would lose its token; refusing at creation keeps new ids inside without breaking existing ones.