03 IdentifiersEN
03 · Identifiers — the capability-id grammar
Normative source:
capability-language-core-v1.md§3 (Grammar), §6.1 (Entailment, namespace), §9.1 layer 3/4 (reason ordering) This page is a readable guide. In case of conflict, the normative specification wins. Status: Preview — not for production use.
TL;DR
A capability identifier is scheme:action (e.g. std/database-v1:query:SELECT), optionally ending in a trailing wildcard segment *. v1 defines exactly one wildcard shape — the complete final segment. Everything else (*:query..., re*, **, {a,b}, [a-z]) is reserved for v2 and rejected in a v1-conforming implementation. Identifiers with different namespaces (scheme + action Class) never entail each other.
The grammar (v1, closed)
capability-id = scheme ":" action [ ":" wildcard ]
wildcard = "*"
scheme = vendor "/" product "-v" major
vendor = 1*( ALPHA / DIGIT / "-" )
product = 1*( ALPHA / DIGIT / "-" )
major = 1*DIGIT
action = segment *( ":" segment )
segment = 1*( ALPHA / DIGIT / "-" / "_" / "." )
Two structural consequences matter in practice:
- Scheme is anchored by the last
-v<digits>. Becauseproductmay itself contain-, the-vsuffix must be the last occurrence of-vfollowed by digits. A product that contains the two-character sequence-vinternally is invalid (invalid_capability_id) rather than ambiguous:a/b-v1-v2is rejected, it is never parsed two ways;b-v1as a product name is simply not expressible in v1. - The trailing wildcard is part of the grammar.
std/database-v1:query:*is a well-formed capability-id, not a malformed action. Only that complete-final-segment shape is a wildcard.
When the same string would fail both the wildcard-shape check and the generic grammar, wildcard detection runs first and reports unsupported_wildcard (§3, "before the generic invalid_capability_id test").
Three tiers of identifiers
| Tier | What it is | v1 rule | Example |
|---|---|---|---|
| Well-formed plain | full grammar, no wildcard | valid |
std/database-v1:query:SELECT |
| Well-formed wildcard | trailing * only |
valid for grant/request patterns |
std/database-v1:query:* |
| Forbidden wildcard shapes | partial / bare / nested / bracket | unsupported_wildcard (v2-reserved) |
std/crm-v1:re*, *, ** , {a,b}, [a-z] |
| Ill-formed scheme/action | missing scheme, bad major, -v ambiguity |
invalid_capability_id |
database:query, a/b-v1-v2 |
Field examples (real corpus ids)
A valid identifier — vectors.json → syntax-001
request = { "id": "std/database-v1:query:SELECT" } → valid
Default identifiers may be deeper than one action segment — vectors.json → syntax-009
request = { "id": "std/data-v1:fetch:item:42" } → valid
Forbidden wildcard shapes — each is unsupported_wildcard, never a deep-match attempt:
syntax-003 *:query:SELECT (wildcard in scheme position) → invalid unsupported_wildcard · syntax-004 std/database-v1:query:SEL* (partial action) → same · syntax-005 std/database-v1:query:{read,write} (expansion set) → same · syntax-006 std/database-v1:query:[a-z] (character class) → same. The refusal is identical as a request: decide-018. The one permitted wildcard shape is still valid as an id — syntax-002 std/database-v1:query:* → valid.
Not a v1 identifier at all — syntax-007 database:query (no scheme) → invalid invalid_capability_id; same code when the grant is well-formed but the request is not (decide-004, decide-025).
Namespace: the hard boundary
Entailment only ever happens inside one namespace, where namespace = scheme + action Class (std/database-v1) (§9.1 layer 3). If grant and operation differ by namespace, the engine answers different_namespace before looking at the path at all (§6.3 step 1):
grant = { "id": "std/database-v1:query:*" }
request = { "id": "std/database-v1:admin:DDL" } → deny different_namespace
The class-position wildcard trap
A * one segment before the end is NOT a wildcard that matches everything up front — a class-position wildcard is a v1-forbidden shape, and its request is rejected at grammar time (§9.1 layer 3, unsupported_wildcard), not silently widened at entailment time. The corpus pins both the deny-being-narrow (same action) and the deny-anyway (different action) readings:
entail-007 grant std/database-v1:* vs request std/database-v1:query:SELECT → deny different_namespace — the * at class position is not a trailing action wildcard; namespace mismatch. Class-position wildcards are v1-forbidden (§3; the requested operation’s own id would be rejected as a * in scheme position), and entailment never gets to path coverage.
entail-008 same grant vs request std/database-v1:SELECT → deny different_namespace, class mismatch.
Compare with the one full wildcard: entail-002 grant …query:* covers request …query:SELECT (allow) because the * occupies the complete trailing segment and there is at least one trailing segment. The trailing wildcard never matches the empty remainder (entail-005, wildcard_requires_trailing_segment).
Rules of thumb
- One scheme per vendor/product/
-vmajor; the major is digits only. - Grant
paramsand identifier detection happen on the segment boundary, not by lexical prefix (sostd/database-v1:query:+SELis NOTSELECT). - When the same input is both a bad wildcard and bad grammar, the wildcard error wins (reported
unsupported_wildcard, notinvalid_capability_id).
Common pitfalls
scheme:actionis mandatory. An id without a scheme isinvalid_capability_id, never "assumestd/…".-v(followed by digits) must be the last such run. A product that embeds-v1breaks;a/b-v1-v2is ungrammatical.- Trailing wildcard requires a remainder.
query:*≠query; the empty remainder is not covered. - In σ-position,
*is a defect, not a shortcut. Path coverage is strictly per-segment; unknown request segments areundeclared-family problems, not "somewhere under a wildcard".
← 02-overview.md · → 04-actions.md · related: 06-entailment.md, 09-intersection.md