| Internet-Draft | CLC-v1 | September 2026 |
| Wei | Expires 31 March 2027 | [Page] |
This document defines the Capability Language Core (CLC), a minimal, executable
language for describing what an agent is authorized to do. It defines the
capability identifier grammar, the entailment relation between a grant and an
operation, intersection of grants from multiple sources, the constraint model,
and a deterministic decision function with stable reason codes and a
three-valued verdict (allow, deny, allow_unresolved).¶
The language is carrier-neutral: it defines what is evaluated, not how it is carried or trusted. Trust models, native verification, execution lifecycle, and receipt or token formats are out of scope (Section 11). Conformance is exercised by a published corpus of 123 vectors and 1184 property cases; three implementations (Go, Python, TypeScript) that share an author pass both. Implementation conformance and this document's claim of a conformance class are separate. An implementation conforms to CLC-A when it meets the obligations Section 12 lists for that class, and it may claim that conformance on its own, whatever other implementations exist. Section 12 additionally sets a maturity bar for this document's claim — two independent implementations agreeing on verdict and reason. That bar is not met here: as Section 12 states under "Independence of implementations", the three implementations named in the README share an author, so their agreement is a regression test for the text, not independent validation. CLC-A is still claimed by this revision as the baseline authorization class, whose obligations are implementable and exercised by a published corpus; the independent-implementation threshold is recorded as unmet. The evidence-side class CLC-E is not claimed: its relations, value grammar, reference implementation and corpus ship here.¶
This revision also folds the delegation containment relation into the
document (Section 13): Contains(parent, child) decides whether a child grant
stays inside a parent's declared boundary — the single question a delegation
chain asks at every hop that the core's entailment and intersection do not
answer. It ships with the conformance class CLC-D and its own corpus, is
strictly additive, and changes no CLC-A verdict, reason code, or vector. It
absorbs the previously separate experimental containment extension,
which is retired.¶
This Internet-Draft is submitted in full conformance with the provisions of BCP 78 and BCP 79.¶
Internet-Drafts are working documents of the Internet Engineering Task Force (IETF). Note that other groups may also distribute working documents as Internet-Drafts. The list of current Internet-Drafts is at https://datatracker.ietf.org/drafts/current/.¶
Internet-Drafts are draft documents valid for a maximum of six months and may be updated, replaced, or obsoleted by other documents at any time. It is inappropriate to use Internet-Drafts as reference material or to cite them other than as "work in progress."¶
This Internet-Draft will expire on 31 March 2027.¶
Copyright (c) 2026 IETF Trust and the persons identified as the document authors. All rights reserved.¶
This document is subject to BCP 78 and the IETF Trust's Legal Provisions Relating to IETF Documents (https://trustee.ietf.org/license-info) in effect on the date of publication of this document. Please review these documents carefully, as they describe your rights and restrictions with respect to this document.¶
CLC-v1 is a universal minimal language that can be consistently presented on either the authorization side or the evidence side.¶
The two sides share five foundational abstractions:¶
| Foundation | Authorization Side | Evidence Side |
|---|---|---|
| Action | Operation (concrete request) | ObservedAction (asserted effect) |
| Identity | CapabilityId (class-level) | ActionId (instance-level) |
| Binding | Entailment (grant ⊆ operation) | Match (evidence ↔ action) |
| Constraint | Grant params / limits | Evidence requirements / freshness |
| Verdict | allow / deny / allow_unresolved | SATISFIED / UNSATISFIED |
Conventions. The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "NOT RECOMMENDED", "MAY", and "OPTIONAL" in this document are to be interpreted as described in BCP 14 [RFC2119] [RFC8174] when, and only when, they appear in all capitals, as shown here. "UTC instant" is an [RFC3339] timestamp. "JCS" is the JSON Canonicalization Scheme [RFC8785]; "I-JSON" is [RFC7493].¶
The language structure is identical on both sides; only the direction differs:¶
The design principles behind this core — including what the language
deliberately refuses (no control flow, no mutable state, no general-purpose
policy language) — are stated in capability-language-core-principles-v1.md
(in [CLC-CORPUS]):¶
P1 minimal core · P2 no control flow · P3 immutable values · P4 domains, not types · P5 deterministic and terminating · P6 fail-closed · P7 define once, consume everywhere · P8 carriers separate from semantics · P9 local decidability · P10 bounded work · P11 composition narrows only · P12 ≥2 independent implementations¶
| Term | Definition |
|---|---|
| Action | The thing being referenced — an abstract operation class (auth) or a concrete asserted effect (evidence). |
| CapabilityId | Structured name identifying a class of actions within a scheme. |
| Operation | Concrete action request: a CapabilityId plus parameters. |
| Grant | Principal's authorization of a CapabilityId with optional params and constraints. |
| Binding | Abstract concept connecting an Identity to an Action. Entailment (auth) and Match (evidence) are concrete instances. |
| Entailment | Authorization-side binding: "Grant G covers operation O" (⊆). |
| Match | Evidence-side binding: "Evidence E is bound to exact action A". |
| Constraint | Bound on how an action may be used (auth) or what evidence is required (evidence). |
| Intersection | Combining multiple grant sources into an effective set (∩). |
| Verdict | Outcome of evaluation: allow/deny/allow_unresolved (auth) or SATISFIED/UNSATISFIED (evidence). |
| Decision | Authorization-side verdict: allow, allow_unresolved, or deny + reason (+ additive unresolved). |
| Satisfaction | Evidence-side verdict: SATISFIED or UNSATISFIED. |
Note: Binding in CLC-v1 denotes the identity↔action relation (coverage on the authorization side, match on the evidence side). It is not key binding (cnf / DPoP / mTLS sender constraint), which belongs to the native artifact's specification (see Section 11).¶
Verdicts are written lowercase on the authorization side (allow/deny/
allow_unresolved) and uppercase on the evidence side
(SATISFIED/UNSATISFIED), following the EMILIA/AEB convention.¶
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 / "-" / "_" / "." )¶
The trailing wildcard is part of the identifier grammar (the optional final
":" "*" above), so std/database-v1:query:* is a well-formed
capability-id; only that shape is a wildcard (Section 3, below).¶
Unambiguous scheme. Because product may contain -, the -v<major>
suffix is the last occurrence of -v followed by digits. A product
that itself contains the two-character sequence -v is invalid
(invalid_capability_id), so a/b-v1-v2 is rejected rather than parsed two
ways; b-v1 as a product name is not expressible. A scheme in use before
this rule that relies on such a product is out of grammar in v1.¶
A v1 identifier is scheme:action (e.g. std/database-v1:query:SELECT).
Trailing * as a complete segment matches one or more remaining segments
(it does not match the empty remainder: std/database-v1:query:* does not
cover std/database-v1:query).¶
Wildcard scope for v1: CLC-v1 core defines exactly one wildcard shape:
the complete trailing segment *. Partial (std/crm-v1:re*), bare *,
**, {a,b}, [a-z] are reserved for v2; a v1-conforming
implementation MUST reject them (unsupported_wildcard). If a capability
scheme declares its own extended grammar, that scheme's conforming
implementation may accept it; but CLC-v1 core conformance neither requires
nor authorizes those forms (see Section 12).¶
Wildcard detection precedes grammar conformance. A forbidden
wildcard shape is reported as unsupported_wildcard even when the same
string also violates the base grammar (Section 3 segment): the wildcard-shape
checks run before the generic invalid_capability_id test.¶
| Examples: std/database-v1:query:SELECT valid | database:query invalid |
|---|---|
std/database-v1:query:* valid |
std/database-v1:query:SEL* invalid |
An action is the thing being referenced. CLC-v1 defines two concrete forms:¶
The material action constructed by the effect boundary from executor-controlled facts. CLC-v1 defines the interface, not the construction algorithm.¶
An ObservedAction carries:¶
action_type: the action type name declared by the relying-party-pinned type definition (the class this projection belongs to)¶
material_fields: every field the type definition declares material¶
digest: computed over the canonical material projection (below)¶
The material projection is deterministic and normative:¶
The action type declares a material field set (required and optional-but-included). Only that set enters the digest.¶
Canonical serialization is the JSON Canonicalization Scheme (JCS) [RFC8785];
the one suite defined in v1 is jcs-sha256 (Section 4.3).¶
The projection identity is the language's own, not a CAID:
clc-action:1:<type>:<suite>:<b64url>. A CAID [CAID] covers the complete
Action Object under its own suite registry and identifies the action object,
not an occurrence; this projection covers only the declared material set. The
two are related by a relying-party-pinned Action-Mapping Profile (Section 6.4), never
by treating the strings as interchangeable.¶
A field the type does not declare as material MUST be excluded from the digest and MUST NOT affect Match: an ObservedAction carrying undeclared fields is not invalidated, but those fields carry no action identity.¶
A type-declared material field that is missing makes the
ObservedAction non-matchable: coverage MUST NOT be inferred, defaulted,
or repaired (UNSATISFIED, Section 10). This is the evidence-side mirror of
key closure (Section 6.2): the absence of a governing field is fail-closed,
never fail-open.¶
The effect boundary MUST construct the ObservedAction from facts it controls. It MUST NOT copy a requester-supplied action digest without deriving or checking the corresponding fact.¶
Two identity levels, corresponding to the two Action forms:¶
| Level | Identity | Scope | Example |
|---|---|---|---|
| Class | CapabilityId | Covers a class of actions |
std/database-v1:query:*
|
| Instance | ActionId (projection digest) | Identifies the material content of one action, not an occurrence |
clc-action:1:payment.release.1:jcs-sha256:...
|
A CapabilityId covers a class; an ActionId identifies the material content of one
action. It does not identify an occurrence: ActionId binds the declared
material content, and correlation to a particular occurrence additionally requires
an occurrence discriminator defined and checked by the consuming profile. CAID-03
Section 4.6 carries such a discriminator as the optional occurrence_id of an
action object; when a profile uses one, it MUST appear among the declared material
fields for it to affect the digest. Allocating unique occurrences and proving
one-time consumption or execution stay outside both documents (CAID-03 Section 7).
Entailment checks class coverage; Match checks content binding.¶
A grant = CapabilityId + optional params + optional constraints.¶
grant = capability-id [ params ] [ constraints ] constraint = scheme ":" type [ ":" params ]¶
Constraints are deny-when-declared: explicitly empty bound (e.g. zero max, empty allowlist) denies the class; omitted bound uses scheme default.¶
Binding is the abstract concept of connecting an Identity to an Action. CLC-v1 defines two concrete instances:¶
| Type | Rule | Example |
|---|---|---|
| number | op ≤ grant | 50 ≤ 100 (holds) |
| string | exact | "a" = "a" (holds) |
| boolean | exact | true = true (holds) |
| array | set of allowed values (enum): request scalar must equal a member; request array — every element must equal a member |
{"station":[1,2,3]} ⊇ 2 (holds); ⊇ 9 (does not hold) |
| object | every grant key in op, values recurse |
{"t":["id"]} ⊆ {"t":["id","name"]} (holds) |
| other | exact equality | — |
Boolean parameters are exact only. A boolean grant value is matched by
exact equality (true covers true only). Booleans are not numbers: an
implementation MUST NOT interpret true/false as 1/0 and MUST NOT run
them through the numeric-bound rule (op ≤ grant). They also play no role in
numeric comparisons or bounds. (Boolean GRANT-side bounds are rare in
practice; the setting that matters — an operation-side boolean value under
key closure — is pinned by undeclared-001.)¶
Table rows map to vectors: number → params-001/params-002; array scalar
member → params-009 (holds) / params-010 (fails); array element-wise →
params-011 (holds) / params-012 (fails); object recursion allow → params-015,
deny-direction → params-005; categorical guard → params-014.
Under the old (pre-v1.1) array-as-bound rule the object example read (does not hold); with the
v1.1 enum rule the request-side element id is a member of the granted set, so
the row is (holds) (see clc-v1-ambiguities.md Section 2).¶
Enum semantics of arrays (v1.1). An array-valued grant parameter is the
set of allowed values, not an order. The request MAY supply a single
scalar (must be a member of the set) or an array (every element must be a
member). Members compare by exact equality: a number inside a granted
array denotes that exact value, not a bound. This is what keeps categorical
identifiers safe — grant {"station":[3]} does not cover station 2
(failure → deny("not_in_enum")). Scheme authors SHOULD encode categorical
parameters (station, cell, batch, tool id) as arrays; a scalar number in the
grant keeps upper-bound (bound) semantics for ordered quantities
(max_rows, speed, angle).¶
An explicitly empty grant array [] denies the class
(empty_bound_denies_class).¶
Grant with no params — or with an empty {} params object — covers any
operation params (unconstrained). An absent params and "params":{} are
semantically equivalent — consistent across entailment (Section 6.3) and
intersection (Section 7 rule 6; Section 9.1).¶
A null parameter value is invalid in v1 → reject (invalid_params_null).
Absent ≠ explicitly empty (see Section 8).¶
Key closure (Plan A). A bounded grant governs its declared keys: an
operation carrying a parameter key the grant does not declare is rejected →
deny("undeclared_param") (fail-closed, Section 9.1 layer 7 request side). An
unconstrained grant (no params, or params:{}) accepts any operation
params, so no key closure applies there. Within layer 7 the missing-key check
(params_missing) resolves before the undeclared-key check
(undeclared_param); both are key-level and run before the enum/bound value
checks (layers 8–9).¶
Params representation and input normalization (v1.1). Before any Section 9.1
layer runs, the params object is normalized at the input boundary:¶
Canonical serialization. Params are normalized to the JSON
Canonicalization Scheme form (JCS, RFC 8785): object members sorted by code
unit, numbers in the ECMAScript Number::toString form, no insignificant
whitespace. The original key order, number spelling and spacing are
not preserved — canonicalization replaces them with the deterministic
form (this is what the size cap in step 4 and the material digest are
measured on). Evaluation never guesses; a lossy re-serialization (e.g. a
map that drops duplicate keys) is not used for decisions.¶
Duplicate keys. A params object with a duplicate JSON key is rejected:
deny("invalid_params_duplicate_key").¶
Number shape. A numeric param is rejected with
deny("invalid_params_number") when it is non-finite or out of IEEE-754
range (e.g. 1e400, an exponent beyond the representable maximum — it is
an overflow, not an over-precision case) or over-precision (more than 17
significant decimal digits, e.g. 1.0000000000000001). The digit count operates on the
JSON token as received — the raw digit-character sequence at the input
boundary, before it enters any storage / float / decimal representation —
so float64 and decimal/bignum implementations MUST NOT diverge: the judged
input is always the raw token text (params-* number probes in the corpus;
near-limit forms such as 1.0000000000000001 extend the probes without
changing the rule). Malformed params text that cannot be parsed as an
object is refused with the same code (see step 7).¶
Size and depth. Params whose canonical length exceeds 512 UTF-8
octets — measured in octets, never in code points or UTF-16 code units
(rev CLC-1.4) — or whose nesting depth exceeds 32, are rejected:
deny("invalid_params_size"). Nesting depth counts both objects and
arrays, with the outermost object as level 1 (31 nested arrays inside the
top-level object therefore = depth 32).¶
Order of checks. Size/depth (4) precede duplicate keys (2), which
precedes number shape (3); the first failing check wins. All five run
before Section 9.1 layer 1, so normalized params are the only view the layers see.
The size in step 4 is measured with every token in its JCS spelling
(numbers re-spelled per ECMAScript Number::toString, strings per JCS
Section 3.2.2.2) but without requiring key uniqueness, so it is defined even
when a duplicate key makes full JCS undefined; this is why an over-limit
input that also has a duplicate key reports invalid_params_size
(params-025/params-026).¶
Decoded-object path. A caller that supplies params already decoded
(no raw_params text) cannot reproduce the original byte stream; in
that case the size check (4) applies to a canonical serialization
(the JCS form of the decoded object: sorted keys, compact), and the
depth check (4) applies to the decoded structure directly.
Byte-exactness against a specific original text is guaranteed only for
the raw path; but both entry points MUST reject the caps — an
oversized/deep params object is denied whichever way it arrives.¶
Malformed Unicode is a property of the received text. A lone surrogate
escape, or an octet that is not valid UTF-8, has no JCS form (Section 3.2.2.2):
step 1 cannot normalize it, so the input is refused. The refusal reuses
invalid_params_number (Section 9.2), whose scope is "the params input is not
representable in canonical form" — a numeric param that is non-finite /
over-precision or text that is not well-formed Unicode / not parseable as
a JSON object. (One stable code, not one per malformation: introducing a
second code would change the reason for inputs already pinned to
invalid_params_number, which the additive rule forbids.) That check is defined over the octets as
received and MUST run before any decoding step, because a general-purpose
JSON decoder does not preserve the distinction. Go's encoding/json, for
example, replaces both a lone surrogate escape and an invalid octet with
U+FFFD, so {"s":"\ud800"}, {"s":"\ufffd"} and a literal invalid octet
arrive at the decision function as the same value — {"s":"\ufffd"} — and
the refusal cannot be recovered afterwards. Therefore:¶
the party that receives the input MUST run steps 1-5 on the received text, not on a re-serialization of a decoded value;¶
an implementation that exposes only a decoded-value entry point MUST NOT be described as refusing malformed Unicode: its verdict is defined over the value it was handed, which may already be a repaired one. The entry point that takes the text is the normative one, and an implementation SHOULD name the two so a caller cannot mistake one for the other;¶
two parties that must agree on the verdict MUST agree on the received text, or on a digest of it.¶
Entails(G, O) → bool:
1. G.namespace ≠ O.namespace → false (namespace = scheme + action Class, Section 9.1 layer 3)
2. G.id doesn't cover O.id → false (path coverage, Section 9.1 layer 4)
3. G declares no key → true (both `params` and `param_bounds` absent/empty
→ grant unconstrained, absent ≡ empty object)
4. O.params absent → false (bounded grant, request omits it → fail-closed,
except a declared key marked `optional`, Section 6.5)
5. params_subset(O.params, keys(params) ∪ keys(param_bounds), param_bounds)
(compare declared set incl. Section 6.5 bounds, Section 9.1 layers 5–9)
¶
Step 5 is the Section 9.1 layers 5–9 comparison over the grant's declared key set
(keys(params) ∪ keys(param_bounds), Section 6.5): params values follow Section 6.2 value
subset, and a param_bounds key additionally enforces its Bound (inclusive
min/max, step, enum cardinality, optional, nested). A grant with no
param_bounds reduces step 5 to the params-only comparison of earlier
revisions. Steps 3 and 4 read the declared key set, so a grant whose only
declarations are param_bounds is still "bounded" for presence purposes.¶
When more than one check fails, the reported reason follows the fixed resolved-reason ordering of Section 9.1. The remaining rules of this section are unchanged.¶
Rule for missing operation params: If the grant has a params bound
(step 3 does not apply) and the operation has no params field at
all (not merely a key missing, but the entire field absent), step 4
applies: Entails → false → deny("params_missing"). This covers
both "operation omits the entire params object" and "operation omits a
single bounded key" — both fail closed.¶
If a capability scheme declares a default value for a parameter, the implementation MUST apply that scheme default to O before step 4; absent such a declared default, step 4 denies.¶
Layer 6 (null) resolves before presence. If the grant's params carry a
null value (or the operation's params do), the failure is
invalid_params_null (Section 9.1 layer 6) and is reported even when the
operation omits the params field entirely — i.e. layer 6 resolves
before step 4's params_missing. This is the fixed Section 9.1 ordering; it
shadows the literal step order of the algorithm above, which lists presence
before the null checks.¶
Evidence E is bound to exact action A if:¶
E carries a valid ActionId in the language's projection form (clc-action:1:…,
Section 4.3). A CAID is a different object and relates to it only through a pinned
Action-Mapping Profile.¶
E's ActionId equals the recomputed ActionId of the ObservedAction.¶
The ActionId was computed under the relying-party-pinned suite and definition source.¶
Match is content correlation only. It does not validate a native artifact and does not authorize execution. It also does not identify an occurrence: correlation to a particular occurrence additionally requires an occurrence discriminator defined and checked by the consuming profile (CAID-03 Section 4.6), and a profile that uses one MUST pin how it is obtained and that the language sees it among the declared material fields.¶
Cross-format mapping (E's native format ≠ A's canonical form) uses an Action-Mapping Profile: a hash-identified projection pinned by the relying party, with results EQUIVALENT_UNDER_PROFILE, NOT_EQUIVALENT, or INDETERMINATE.¶
param_bounds)
params (Section 6.2) stays exactly as defined: a scalar number is an upper bound, an
array is a membership set, an object recurses per key, and there is no lower
bound, no step, no cardinality bound and no optional-key marker. This
subsection adds those as a separate, optional grant field, param_bounds,
so that no existing params input changes meaning: a grant without
param_bounds behaves exactly as before, and every params verdict is
unchanged.¶
Binding rule (one authoritative representation per key). A parameter key
MUST be declared in at most one of params and param_bounds. Declaring
the same key in both is rejected: deny("invalid_params_binding"). The
declared key set of a grant is keys(params) ∪ keys(param_bounds); key
closure (Section 9.1 layer 7) and the params:{}≡absent rule are read over that set.¶
Four distinct layers — do not collapse them in a decoder. A bare {} has
different meaning at each layer, and an implementation must keep them separate
before normalizing:¶
Container presence — "params" absent vs "params":{}. Both are
unconstrained (no declared keys); {} is not "declares nothing and
denies".¶
Declaration site — a key is declared via params or param_bounds
(never both). The site decides which value algebra applies to the key.¶
Value constraint — inside params, an empty value is a restriction
({"tables":[]} denies the class; deny-when-declared, Section 6.2); inside
param_bounds, an empty Bound {} declares the key with no value
constraint (it only participates in key closure). Same {}, opposite
reading, because the layer differs.¶
Request value — what O supplies (or a materialized default), evaluated
per the key's family at layers 7–9.¶
A decoder that maps {}→"absent" or {}→"empty set" uniformly across these
layers will disagree with the language on at least one of them.¶
Bound grammar (closed). param_bounds maps a key to a Bound object:¶
Bound = { // a closed JSON object, at most one family
// numeric family
"min": <number>, // inclusive lower bound
"max": <number>, // inclusive upper bound
"step": <number > 0>, // value must be an integer multiple of step
// enum family
"enum": [ <scalar> ], // allowed values (membership set)
"min_items": <integer >= 0>, // request cardinality lower bound
"max_items": <integer >= 0>, // request cardinality upper bound
// nested family
"nested": { <key>: Bound }, // recursion for an object-valued key
// orthogonal to all families
"optional": <boolean> // request MAY omit the key (default false)
}
¶
(Bound is JSON, not ASN.1: the members above are JSON keys and the comments are notation only.)¶
Every member is optional and the object is closed — a member outside this
set is rejected (invalid_params_binding). A Bound carries at most one
value family, plus optional which is orthogonal:¶
numeric family — any of min, max, step;¶
enum family — enum, and/or min_items/max_items;¶
nested family — nested.¶
Mixing families (e.g. enum with max, or nested with min) is rejected
(invalid_params_binding). An empty Bound {} declares the key with no
value constraint (it still participates in key closure). min > max,
step ≤ 0, min_items > max_items, or a negative min_items are rejected
(invalid_params_binding). The whole param_bounds object is subject to the
same input normalization as params (Section 6.2): canonical serialization, duplicate
keys, number shape, size (512 octets) and depth (32), checked at layer 2.¶
Entailment semantics (grant G vs operation O).¶
Presence (layer 7). A declared key with optional: true MAY be absent
from O; a declared key with optional absent or false MUST be present
(params_missing otherwise). An operation key not in the declared key set is
undeclared_param. (Unchanged behavior for grants that declare no
param_bounds, where every declared key is required.)¶
Enum family (layer 8). If enum is declared, the request value must be a
member (not_in_enum, unchanged rule): a scalar request must equal a member;
an array request must have every element equal to a member. Equal here is
JSON type-sensitive equality: two values are equal only when they have the
same JSON type and the same value — true equals neither 1 nor 0 (the
Section 6.2 rule that booleans are never numbers applies to set membership as well),
and the string "1" equals neither the number 1 nor true. Numbers are
compared after the Section 6.2 canonicalization of the input boundary (one IEEE-754
binary64 value, rendered per ECMAScript Number::toString under JCS
[RFC8785]), so 1 and 1.0 — the same value in two spellings — are
equal. The same equality is used by Section 6.2 array membership, by the Section 6.6 enum
intersection, and by Section 13.4.3 enum narrowing; implementations MUST NOT
substitute a host-language equality that coerces across JSON types (e.g. a
language where true == 1). If min_items / max_items are declared,
the request cardinality (an array's length; a
scalar counts as 1) must satisfy min_items ≤ n ≤ max_items, else
params_cardinality.¶
Numeric family (layer 9). min/max are inclusive: a numeric
request value must satisfy min ≤ v ≤ max (each bound, when declared), else
params_out_of_range. step requires the request value to be an integer
multiple of step, evaluated in IEEE-754 binary64 as
let q = v / step in q == floor(q) && q * step == v (deterministic across
implementations, since both operands arrive at the boundary as binary64), else
params_not_multiple. A numeric-family bound applied to a non-number
request value is fail-closed (params_exceed_grant).¶
Nested family (layer 8/9). nested recurses the value comparison on an
object-valued request key with the same rules, including symmetric key closure
and optional at each depth. A nested bound applied to a non-object
request value is fail-closed (params_exceed_grant).¶
Scheme defaults. A capability scheme (Section 3 scheme grammar; data/std/...) MAY
declare param_defaults, a map from a parameter key to its default value.
The consumer obligation of Section 6.3 is thereby made concrete: before evaluating
Entails(G, O), an implementation MUST materialize, for every key O omits that
the scheme declares a default for, that default into O's params; precedence is
explicit operation value > scheme default > absent. A default is applied
only when it is needed to satisfy a grant-declared key; it never adds a key the
grant does not declare (the key-closure check is unchanged).¶
optional closes the default interaction (no recursion). The presence of
the key is decided first, by the grant's optional marker, and only then is
a default consulted — so there is no "is the default needed?" loop:¶
a required declared key (no optional/optional:false) that O omits:
materialize param_defaults[key] if the scheme declares one (the key is then
present with that value), else deny("params_missing");¶
an optional:true declared key that O omits: the default is not
materialized — optional is the explicit "may be absent" marker and wins over
the default, so the key is simply absent (and satisfies presence);¶
either way, an explicitly supplied O value wins over the default.¶
Defaults are scheme configuration, not language semantics: the relation
itself takes no scheme argument, and two consumers that load the same scheme
reach the same verdict (a deployment that omits the scheme's defaults is a
different input, and the difference is attributable to the input, not to
Entails). In the containment relation a default is resolved before the
grants are compared (Section 13.8.1 obligation 1), so containment compares resolved
declared sets. A param_defaults entry is not part of a grant's declared
set and does not enter the containment lattice: Contains narrows over
keys(params) ∪ keys(param_bounds) and the optional markers alone, so a
parent's default never widens or inverts the optional→required narrowing
direction (Section 13.4.3).¶
Containment (Section 13). Section 13.4.3 narrows a child grant against its parent using
this grammar: a param_bounds bound of the child must be within the parent's
bound for the same key (numeric min raised or equal, max lowered or equal,
step refined to an integer multiple, enum a subset, cardinality bounds
tightened, optional not widened from required to optional, nested recursed).
A parameter declared in one of params/param_bounds and not the other is a
key-set difference and fails key closure (params_not_narrower).¶
BoundMeet)
Intersect (Section 7) combines the param_bounds of its sources with a meet,
over the same "value → authorization set" denotation Section 7 uses for params. The
families are those of Section 6.5; the meet is defined per key declared in
param_bounds by two or more sources.¶
optional is orthogonal and combines by conjunction: the result key is
optional only if every source marks it optional (optional := AND); a key
required by any source stays required.¶
numeric family (min/max/step): min is the greatest declared
minimum, max the least declared maximum (undeclared means unbounded).
step: if both declare one and one is an exact multiple of the other (the
Section 6.5 multiple predicate), the meet's step is the coarser (larger) of the
two — the coarser grid is a subset of the finer, so it is the meet. If
neither declared step is an exact integer multiple of the other, the meet
fails closed (invalid_params_binding). The two grids may still share
values — the common grid of step:5 and step:7 is the set of multiples of
35, so the intersection is not empty — but the meet's step must be one of
the two declared steps: neither 5 nor 7 is an integer multiple of the
other, so neither declared grid is a subset of the other, and the language
does not synthesize an undeclared grid (such as the least common multiple) —
no single step member of the two Bounds denotes both grids. One declared
step is carried through. min > max after combining → the meet is
empty → no_overlap.¶
enum family (enum/min_items/max_items): the enum member sets
intersect (exact equality — the JSON type-sensitive equality Section 6.5 layer 8
defines, so 1, true and "1" are three distinct members); if both
sources declare enum and the intersection is empty → no_overlap;
min_items is the greatest declared value, max_items the least;
max_items < min_items → no_overlap.¶
nested family (nested): the two objects MUST have the same key set,
else no_overlap (exactly as object-valued params, Section 7); the result recurses
BoundMeet per key, with optional combining as above at every depth.¶
numeric ∩ enum (either source order): refused — fails closed
(invalid_params_binding). A sound meet would have to carry both the
numeric side's shape constraint (min/max/step, which Section 6.5 layer 9
applies to numeric scalar request values only) and the enum side's member
set (which Section 6.5 layer 8 applies to scalars and to arrays whose elements
are all members) in one Bound — two families, which the closed Section 6.5 grammar
deliberately does not express. Filtering the member list by the numeric
bound (the rev CLC-1.14 rule, removed here) was broader than either
source: for numeric {min:2,max:4} ∩ enum {enum:[1,3,5]} the filtered
result enum{3} accepts the array [3] (Section 6.5 layer 8: every element is a
member), while the numeric source fail-closes that same request (Section 6.5 layer
9: a numeric-family bound applied to a non-number request value is
params_exceed_grant). Refusing the whole combination agrees with the rest
of the language: Section 6.5 rejects declaring two families in one Bound
("Mixing families … is rejected (invalid_params_binding)"), and Section 13.4.3
rejects narrowing into a family the other side does not declare ("a child
Bound that adds a family the parent does not declare … is
params_not_narrower"). The refusal covers every numeric × enum pair —
with or without a member list on the enum side (a cardinality-only enum is
the same cross-family clash), and regardless of whether any member happens
to fall inside the numeric range: the family clash is decided before any
member or range math, and it is symmetric in the two sources.¶
scalar ∩ nested (numeric or enum vs. nested), in either order: a
cross-family meet, refused with invalid_params_binding before any
value math — no single-family Bound can carry both a scalar shape (a value
presence) and the object recursion; the refusal is symmetric in the sources
and holds regardless of the nested side's contents, exactly like the
numeric × enum case (design-notes D12). A CLC-1.14 draft read this pair as
an empty meet (no_overlap, since no request value is both a scalar and an
object); rev CLC-1.15 adjudicates it to the unrepresentable-meet code so
that the empty-meet code stays reserved for genuinely empty meets within
one value family.¶
empty Bound {} is the identity for the value families ({} ∩ X = X);
its optional still participates.¶
The result always carries at most one value family, so it is a valid Section 6.5
Bound. All failure modes are the Section 9.2 codes already associated with Intersect
(no_overlap for an empty meet within one value family, such as min > max
or disjoint enums; invalid_params_binding for an unrepresentable one — every
cross-family pair in either order, including scalar ∩ nested, and
incommensurable step grids); no new reason code is introduced.¶
Key site. A key's declaration site (params vs param_bounds) must
agree across the sources of one Intersect. A key declared in params by one
source and in param_bounds by another is refused (invalid_params_binding).
This can only arise for independent authority sources, never for a delegation
chain: Section 13.4.3 already requires the sites to match at every hop (a site
difference is a key-set difference). A key that no source declares in
param_bounds stays in params and follows the Section 7 params meet unchanged.¶
P_effective = P_principal ∩ C_agent ∩ P_gateway.¶
Rules:¶
Each source provides a grant set.¶
Effective grant MUST be covered by at least one grant from every source.¶
Same-capability constraints merged by union: every source's constraint strings are kept (constraints are conjunctive — each is independently checked, so a tighter bound of the same type binds by construction). The merge is a set union of normalized constraint strings, not a meet that discards a looser one; dropping a source's constraint would drop its restriction.¶
Any source missing a capability → capability absent from effective set.¶
Zero or absent sources fail closed. An intersection over no
sources at all — an empty source list, or every source absent — has no
effective set: deny("absent_source"). (A source that exists but
carries no grant for the capability is rule 4, not rule 5.)¶
Empty params declares no constraint. A source whose grant has a
present-but-empty params object contributes no restriction. The
accumulated bound is preserved: bounded-then-empty and empty-then-bounded
must give the same result — source order MUST NOT change the outcome
(consistent with direct-grant params:{} ≡ absent, Section 6.3 step 3).¶
Identifier comparison is params-free (rule 2). The effective identifier
is the narrowest one covered by every source, chosen by the Section 6.1
identifier rules alone. It MUST NOT be resolved by routing grants through
Entails with params — a bounded grant faced with a params-less sibling
would trigger presence handling (Section 6.3 step 4) and wrongly fail this
open; the identifier is compared, params merge separately (rule 3).¶
Property: composition narrows only. If Intersect succeeds, the
result MUST be covered by every source and MUST NOT depend on the order
of the sources; otherwise it MUST deny with a normative reason code and
MUST never raise. This meet-law is what the Section 6.2 param-subset algebra
preserves across sources; it is pinned by property-cases.json (1184
cases, Section 12).¶
Notation in this section: params are JSON objects (e.g.
{"tables":["a"]}); constraints use colon-notation triples (e.g.
varwof/constraint-v1:time:window:[{"start":"00:00","end":"01:00"}]). The example tables below use
compact shorthand for readability; each row shows the relevant params or
constraints only.¶
Deny-when-declared: {"tables":[]} (explicitly empty) = deny the class.
Omitted = scheme default. A null value is invalid in v1 → reject
(invalid_params_null). Canonicalization MUST NOT broaden (segment-boundary
comparison, not lexical prefix).¶
| Source A | Source B | Result |
|---|---|---|
{"tables":["a","b"]}
|
{"tables":["a"]}
|
{"tables":["a"]} (holds) |
{"tables":["a"]}
|
{"tables":[]}
|
deny |
| (unconstrained) | (no grant) | deny |
{"limit":100}
|
{"limit":50}
|
{"limit":50} (holds) |
| (unconstrained) | constraint time:window:[{"start":"00:00","end":"01:00"}]
|
recognized, not evaluated by core (→ allow_unresolved + unresolved) |
{"tables":["a"]}
|
{"tables":["b"]}
|
deny |
The meet is over authorization sets, not JSON values. Neither Intersect
nor Section 6.2 compares JSON values directly; each value denotes an authorization
set and the meet is set intersection on that denotation. A number denotes
(-∞, v] (an upper bound), an array denotes a membership set, a string/boolean
denotes the singleton {v}, and an object denotes the product of its keys'
denotations. So {"a":1} is not the set {1} but (-∞,1], and
{"a":1} ∩ {"a":2} = (-∞,1] = {"a":1} — a minimum, not an empty set. A meet is
empty (→ deny("no_overlap")) only when the denotations are genuinely
disjoint: two enum sets with no common member (a numeric params meet is always
a minimum, since params has no lower bound), or divergent key sets (next
paragraph).¶
Object-value intersection requires identical key sets. Two object
values intersect key-by-key only when their key sets are the same; object
values with different key sets → no_overlap deny. Merging "shared keys"
would drop the keys the other source constrains, so the result would not be
covered by that source (P11 composition narrows only): {"a":1} ∩ {"b":1}
→ deny(no_overlap). When the key sets are the same the values recurse
under the Section 6.2 rules, so a numeric leaf intersects to its minimum
({"a":1} ∩ {"a":2} → {"a":1}, not a deny — matching the {"limit":100}
∩ {"limit":50} row above), and {"a":1} ∩ {"a":1} → {"a":1}.¶
param_bounds merges by a defined meet. Intersect combines each key
declared in param_bounds with the meet of Section 6.6: numeric min/max/step,
enum member intersection and cardinality, and nested recursion, with
optional combined by conjunction. An empty meet is no_overlap; an
unrepresentable one is invalid_params_binding — any cross-family pair in
either order (numeric × enum, including a cardinality-only enum and
regardless of whether any member falls inside the numeric range; and scalar ×
nested), or two step grids where neither declared step is an exact integer
multiple of the other (Section 6.6). A key declared in params by one source and
param_bounds by another is refused (invalid_params_binding, Section 6.6 "Key
site"). The Section 7.1
ConstraintUnion projection is separate and unaffected.¶
A consumer that has established a delegation chain often needs the chain's
whole constraint burden without computing an effective grant. That is
exactly the constraint projection of chained Intersect (rule 3), exposed as
a named function:¶
ConstraintUnion(chain) → string[] // chain = ordered Grant[]¶
It returns the normalized union of every constraint string carried by the
grants in chain — duplicates folded, result deterministically ordered.
"Lexically sorted" is pinned to UTF-8 byte order: the normalized strings
are compared octet by octet over their UTF-8 encodings. For well-formed
Unicode text this equals code-point order, and it is identical in every
implementation whatever the host language's native string representation is —
notably it is not UTF-16 code-unit order, which places supplementary-plane
characters (encoded as surrogate pairs, first unit U+D800–U+DBFF) before
characters in U+E000–U+FFFF, so an emoji would sort before a
private-use-area character although its code point is higher. This collation
sits after Section 6.2 canonicalization, not instead of it: JCS [RFC8785] Section 3.2.3's
UTF-16 code-unit order governs object member order inside a canonical
serialization, while this section's UTF-8 byte order governs the emitted
constraint-string list; the two artefacts keep their own pinned collations
and neither re-orders the other. The union is the same normalization
Intersect applies, so the two never disagree. The residual-obligation list
of Section 8.4 (and the ordered Resolve output of Section 8.5) re-uses this same collation;
the collation is defined here once, not restated there.¶
ConstraintUnion is a projection, not a meet: it does not compare
identifiers or parameters, does not read or validate constraint values, and
does not check containment. It asserts nothing about authority; the chain's
per-hop Contains check (Section 13) and the operation's Authorize/Resolve
(Section 9, Section 8.5) remain separate steps. Because it is not an effective-set
computation, it carries no membership semantics of its own — an empty
chain fails closed with absent_source (the same refusal Intersect
gives over zero sources, Section 7 rule 5), so a caller that lost the chain cannot
mistake "nothing to union" for "no constraints".¶
This function is why Contains stays pure (Section 13.4.4): containment answers
"is the child's declared boundary inside the parent's?", and the union
answers "what does the chain collectively require?". Folding the union into
Contains would make the relation no longer a subset on the declared tuple
and would let a null constraint check hide a broken boundary.¶
Constraints are shared between authorization and evidence sides.¶
Require specific evidence properties: freshness, consumption, quorum, initiator-exclusion, etc.¶
These are structurally identical to authorization constraints — a
type plus optional params — but evaluated against evidence artifacts
rather than operation parameters.¶
constraint = scheme ":" type [ ":" params ]¶
Both sides use the same grammar. The validator (authorization) or evidence evaluator (evidence) interprets the type-specific params.¶
A recognized constraint whose value conforms to its Section 8.1 value grammar but
for which the v1 core defines no evaluator — v1: time, network;
evaluation belongs to the declaring scheme (Section 11) — MUST NOT be silently
dropped: it must appear
in the decision's additive unresolved list (Section 9), and the verdict must be
allow_unresolved (not allow):¶
Decision = { verdict: "allow"|"deny"|"allow_unresolved",
reason, unresolved: string[] }
¶
Fail-closed boundary: allow_unresolved is an independent enum value,
never equal to allow. A consumer (PEP / profile / declaring scheme) MUST
evaluate or confirm every unresolved constraint before allowing;
if it cannot execute or confirm, it MUST deny (AAC Section 6.6: "when it cannot
perform or confirm, it should be treated as deny"). A consumer that only
writes if decision.verdict == "allow" cannot, on the literal enum, release
a residual obligation as an already-satisfied allow — a path that leaves
obligations unconfirmed must explicitly handle allow_unresolved to pass.
"the caller is supposed to check unresolved" is not sufficient defense:
the verdict itself must refuse the two-value short-circuit.¶
unresolved semantics and ordering: [] for deny and for fully evaluated
allow; for allow_unresolved the normalized constraint strings with
duplicates folded — the conditional collation order of Section 7.1
(#-separated, then by UTF-8 byte sequence), the same comparison the
ConstraintUnion payload uses (rev CLC-1.15). The order is deterministic:
the same input yields the same byte sequence in any implementation — in
particular NOT the ECMAScript default string order (UTF-16 code-unit order);
join the residual obligations across all sources and sort exactly once,
before emitting the decision.¶
Combined obligations (consumer side): multiple unresolved constraints
of the same (scheme,type) form a conjunction (AND) — satisfying A and B
satisfies all; OR, any-one, first-wins, and ignoring some entries are all
forbidden. Constraints of different (scheme,type) do not interact;
each evaluates under its own declaring scheme (P11 composition narrows only:
conjunction only narrows).¶
The core owns only the value grammar ("what it is"); "whether this
window/CIDR currently forms a boundary" ("how to evaluate") belongs to the
declaring scheme (Section 11) — but the boundary-moment semantics are part of the
grammar (Section 8.1: half-open [start, end), single segment within one day,
crossing split into segments), and a scheme MUST NOT change that
interpretation, only evaluate on top of it.¶
Resolve)
Section 8.4 delivers residual obligations but leaves the consumer's feedback loop
undefined. Resolve closes it: a consumer reports, per obligation, whether it
is satisfied, violated, or still unknown, and the core collapses the result to
a fresh decision.¶
Resolution = { constraint: string,
status: "satisfied" | "violated" | "unknown" }
Resolve(decision, resolutions, now?) → Decision
¶
decision is a Section 9 Decision; resolutions is an ordered list (possibly
empty) of Resolution; now is an optional UTC instant (RFC3339, Z).
now is the only input that lets the core tick a residual itself.¶
Algorithm, in order:¶
Terminal verdicts are fixed. If decision.verdict is deny or
allow, Resolve returns decision unchanged and ignores resolutions:
a deny is never revived and an allow carries nothing to discharge.¶
Malformed input fails closed. An entry that violates the grammar above
(unrecognized status, empty/non-string constraint) → deny with
invalid_resolution; a now that is not a valid instant →
deny with invalid_timestamp.¶
Discharge on allow_unresolved. Let O = decision.unresolved (already
normalized and sorted, Section 8.4). For each o ∈ O, a status in
{satisfied, violated, unknown} is computed by combining the sources below
under the order violated ≻ satisfied ≻ unknown (the most
restrictive source wins — a consumer that reports violated is never
overridden by a lenient one, and the core clock is never overridden by a
lenient assertion):¶
Core clock — if o is a core-recognized varwof/constraint-v1:time
obligation whose value is a Section 8.1 window array, and now is supplied,
the core evaluates it: now inside a segment → satisfied; outside every
segment → violated. This is the obligation's TTL: the discharge
horizon is the end of the segment containing now, so re-invoking
Resolve with a later now re-evaluates to violated once the window has
passed — a cached allow does not outlive its window.¶
Consumer resolution — a Resolution for o contributes its status;
the most restrictive of the two wins.¶
Absent everywhere → unknown.¶
Repeated entries for the same constraint combine under the same
violated ≻ satisfied ≻ unknown order.¶
Unrelated resolutions are ignored. A Resolution whose constraint is
not in O cannot widen the outcome; O is authoritative.¶
Result:¶
any o has status violated → deny, reason = that constraint's
{type}:violated (time:violated for a core-clock violation; otherwise
the type segment of the constraint string), unresolved = [];¶
every o is satisfied → allow, reason null, unresolved = [];¶
otherwise → allow_unresolved, reason null, unresolved = the still-
unknown subset (normalized + sorted, Section 8.4).¶
Without now, the core attaches no TTL to a time:window obligation: a
consumer may report it satisfied (its declaring scheme owns its clock), but
that discharge is only as fresh as the call, and a consumer that needs a
core-enforced horizon MUST pass now.¶
Acting on a result (caching rule). Resolve returns a plain decision; it
does not carry an expiry field, so a core-clock discharge is only valid for
the instant of the call. A consumer that acts on a Resolve result involving a
core-evaluated time:window obligation MUST either re-invoke Resolve with a
current now immediately before acting, or not cache the result beyond the
current segment's end (the discharge horizon, Section 8.5 step 3) — it MUST NOT hold a
cached allow past that horizon. Equivalently: a suite that needs a
mechanically enforceable "valid until" value derives it from the segment end
itself, not from the returned Decision. (The obligation carried on an
allow_unresolved decision remains the Section 8.4 object a consumer evaluates; this
rule is only about not outliving a clock-based discharge.)¶
Resolve is deterministic and fail-closed; it is idempotent
(Resolve(Resolve(d, r, now), r, now) = Resolve(d, r, now)) and
monotone (adding a violated never turns a deny into an allow; removing one
never turns an allow into a deny). It neither invents nor drops obligations.
It subsumes the coarser identity-level consumer gate (the reference
implementations' Discharge, which only answers "do I understand and commit
to every obligation?"): confirming an obligation is the satisfied case. It
defines no policy about how the consumer evaluates an obligation (P2) — that
is the declaring scheme's (Section 11).¶
Satisfy(evidence_set, requirement) → Satisfaction
Satisfaction = { verdict: "SATISFIED"|"UNSATISFIED", reason: string|null }
¶
Algorithm:¶
Verify each evidence artifact under its native rules.¶
For each required evidence role, check that an artifact fills it.¶
Check that each artifact is bound to the exact action via Match (Section 6.4).¶
Evaluate freshness, consumption, and role constraints. The evidence-side
value grammar is defined in this revision: varwof/evidence-v1:freshness:sec:<n>,
:consumption:once, :quorum:distinct:<n>, :exclusion:initiator|executor
(Section 8.2). A recognized constraint whose evaluation belongs to the enforcement
point (consumption) evaluates to unknown, which at the top level yields
UNSATISFIED (never SATISFIED) — the evidence side has no
allow_unresolved; unresolved is an authorization-side channel (Section 8.4,
Section 11).¶
All required roles filled and bound → SATISFIED.¶
Any role unfilled, unbound, or violated → UNSATISFIED.¶
Tri-state evaluation, binary report. A recognized evidence-side constraint
is evaluated three-valued (satisfied / violated / unknown). Satisfaction
itself is binary. A constraint that evaluates to unknown at the top level —
including one whose evaluation belongs to the enforcement point (consumption) —
MUST produce UNSATISFIED with a stable reason, never SATISFIED. unknown
is an internal evaluation result, not a third top-level verdict.¶
Properties: deterministic, fail-closed, stable reason codes.¶
CLC-v1 defines the shared minimal vocabulary and evaluation algorithms for both authorization and evidence.¶
CLC-v1 does NOT define:¶
Trust models: who signs what, issuer trust, delegation chains (belongs to AIC-JWT [AIC-JWT], OAuth, SPIFFE, etc.)¶
Native verification: signature checking, schema validation, freshness enforcement (belongs to each native artifact's spec)¶
Execution lifecycle: consumption, invocation, reconciliation, outcome classification (belongs to EMILIA AEB [EMILIA-AEB] or equivalent)¶
Receipt or token formats: the wire formats for carrying grants, evidence, or bindings (belongs to protocol-specific specs)¶
The boundary is:¶
CLC-v1 defines what to evaluate (grant ⊆ operation, evidence ↔ action)¶
Consumers define how to evaluate (native verification, trust anchors)¶
CLC-v1 defines what the output means (allow/deny/allow_unresolved,
SATISFIED/UNSATISFIED) — allow and allow_unresolved are distinct
enum values; a consumer MUST NOT treat allow_unresolved as allow
(Section 8.4)¶
Consumers define what to do with the output (invoke, record, reconcile)¶
CLC-v1 defines each known type's value grammar (what counts as a legal constraint value); the declaring scheme defines how that value is evaluated (whether this window/CIDR currently forms a boundary)¶
allow_unresolved is an authorization result, not evidence. It marks an
unresolved authorization (or policy) condition. A consumer that can evaluate
the obligation under a pinned rule may release it; one that cannot MUST refuse.
It takes on an evidence role only where a relying party separately defines one
together with the native verifier for it (AEB); the language itself makes no
such claim, and unresolved MUST NOT be read as "evidence still required"¶
A Section 6.2 refusal does not transfer to a decoded value. The input-boundary checks are defined over the text as received (Section 6.2). A deployment that must reproduce a refusal, or that applies a permit to a request it did not evaluate, therefore anchors the decision to the received octets or to a digest of them (Section 4.3) — never to a value a decoder has already normalized. A pipeline that can apply a permit to a request whose text was never checked has left the CLC boundary¶
CLC-A stays the scope language: material-action identity is referenced from CAID [CAID], heterogeneous evidence evaluation from AEC [AEC], the boundary lifecycle from AEB [EMILIA-AEB], and durable consumption/accounting from BCR [BCR]; the narrow crosswalk between them is the composition point¶
Principal/agent delegation and token attenuation stay with PAP [PAP] and AAT [AAT]; CLC consumes their authorization outcome rather than redefining it¶
CLC-v1 defines three conformance classes:¶
CLC-A (authorization side) — the v1 baseline. A conforming
implementation MUST implement: grammar (Section 3), entailment (Section 6.1),
intersection (Section 7), decision function (Section 9), rejection of non-recognized
(scheme,type) constraints (unknown_constraint, Section 8.1), rejection of
non-conforming recognized-type values (invalid_constraint, Section 8.1),
exposure of recognized-but-unevaluated constraints via the decision's
unresolved field on an independent allow_unresolved verdict (never
silently dropped, Section 8.4), multi-grant aggregation (Section 9.1), and stable
reason codes (Section 9.2).¶
CLC-D (delegation side) — the containment relation, defined in
Section 13. A conforming implementation MUST implement
Contains(parent, child) with its ordered layers, the relation's two stable
reason codes (child_exceeds_parent, params_not_narrower) and its profile
contract (Section 13.8.1), and MUST pass containment-vectors.json. The third
CLC-D code, delegation_mode_not_narrower, is produced by the binding
profile's delegation-mode pre-check (Section 13.4.5) — never by Contains, which
takes no mode argument — and belongs to the profile's obligations. A
delegation policy that requires each hop
to stay inside the previous hop's boundary reads Contains, not Section 7:
entailment and intersection over the declared sets are necessary but not
sufficient, because they do not compare a child's boundary against a parent's.
An operation fitting a grant proves nothing about a child staying inside its
parent, and where containment cannot be established a binding profile MUST NOT
authorize the delegation. Contains is a language relation over grant
values, not a wire format; the effective subset a delegation record carries is
still the profile's to define.¶
CLC-E (evidence side) — optional conformance profile, implemented and
pinned by a corpus in this revision, but NOT claimed. Section 6.4 (match) and Section 10
(satisfaction) define the evidence-side relations; this revision also defines the
evidence-side constraint value grammar (varwof/evidence-v1:freshness:sec:<n>,
:consumption:once, :quorum:distinct:<n>, :exclusion:initiator|executor) and
ships a reference implementation and a corpus.¶
The class is withheld on principle, not for lack of material: agreement is the bar, and the bar is two independent implementations (Section 12 of the principles document, P12; Section 12 here, "Independence of implementations"). Parity between implementations that share an author does not meet it. A second precondition is stewardship: the evidence-side semantics are the subject of joint review with EMILIA, so no claim is made ahead of that review.¶
A future revision that claims CLC-E would carry these obligations, and an implementation that claims it today MUST:¶
evaluate the four evidence-side types over eligible evidence facts, returning
a three-valued result (satisfied / violated / unknown) where unknown is
never read as satisfied, and report a recognized constraint whose evaluation
belongs to the enforcement point (consumption) as unknown rather than
satisfied;¶
take eligibility from integrity-protected native results only: a fact that did not reach VERIFIED, or whose protected subject identifier is absent, MUST NOT be counted for quorum or exclusion;¶
implement instance identity and binding (Section 4.2/Section 6.4): an ActionId is the digest of the JCS canonical serialization of the declared material projection, an undeclared field MUST NOT affect it, a missing declared material field makes the action non-matchable (never inferred or defaulted), and a comparison across suites or action types is INDETERMINATE — a mapping problem, never a match — unless a relying-party-pinned Action-Mapping Profile projects it;¶
implement CLC-REQUIREMENT-v1 as a closed object (an undefined member is
rejected) whose expression uses the bounded grammar — AND/OR with equal
binding strength, evaluated strictly left to right, parentheses as the only
precedence mechanism — and treat an identifier with no eligible component as
false;¶
take the requirement from relying-party configuration: a requirement supplied by the presenter MUST NOT be accepted or weakened;¶
pass evidence-vectors.json (Section 12 conformance corpora).¶
An implementation that implements only CLC-A MUST NOT claim CLC-E. CLC-E does not add a wire format: carriers that need one (e.g. an Action Evidence Envelope) profile Section 6.4/Section 10 themselves.¶
Conformance corpora. The suites below are published in the repository
tree pinned as [CLC-CORPUS]; repository paths written as capability/...
throughout this document are relative to that pinned tree, so the exact
vectors named here are retrievable. CLC-A conformance is exercised by two
machine-readable reference suites at
capability/data/_vectors/clc-v1/: vectors.json — 123 vectors mapped
to Appendix B — and property-cases.json — 1184 cases pinning the Section 7
meet-law, identifier narrowing and source-order independence. Their
syntax is defined by vectors.schema.json; offline-vectors.json is a
timestamped snapshot mirror. A conforming implementation MUST pass both
suites. CLC-D conformance (Section 13) is exercised by containment-vectors.json —
64 vectors mapped to Section 13 — together with containment-property-cases.json
(784 cases pinning the Section 13.3–Section 13.7 narrowing laws). containment-crosswalk-vectors.json
carries 44 cross-vendor vectors that map six adjacent capability
representations into CLC grants through pinned profiles and assert the
containment verdict the unchanged relation reaches (Appendix C).¶
Genericity is exercised, not asserted. crosswalk-vectors.json in the same
directory carries 13 vectors in both directions: 5 that project a CLC decision
into the members an AEB crossing record asks of a native source (the members CLC
can establish, and the ones it explicitly does not), and 8 that map five foreign
capability representations —
OAuth RAR authorization_details, an AIC-JWT delegation authorization, an
Action Evidence Graph capability class, a UCAN {with, can} capability, and a
delegation chain — into CLC grants through pinned cross-walk profiles and assert
the decision the unchanged core reaches. A profile is a few lines of mapping
written by whoever owns the foreign format; the core is not modified for any of
them.¶
The evidence side ships evidence-vectors.json in the same directory — 32 vectors covering the four evidence-side constraint types, their
value-grammar rejections, requirement-expression binding, the closed requirement
object, ActionId computation (Section 4.2: declared material projection, undeclared
fields excluded, missing material field non-matchable, suite-tagged identifiers)
and Match verdicts (Section 6.4: MATCH / NOT_EQUIVALENT / INDETERMINATE). Its
runner ships with the reference implementation (register), and its syntax
mirrors vectors.json.¶
Implementations MUST NOT: redefine semantics, accept v1-forbidden wildcards, or broaden bounds during canonicalization.¶
Independence of implementations (honest scope). The three implementations named in this repository's README (Go, Python, TypeScript) are not independent evidence: they share an author, and their agreement is a regression test for the specification, not third-party validation. An independent implementation is invited; until one exists, the parity claim in this document is scoped to "same-author, three languages, one corpus". Reviewers SHOULD treat a single-author parity claim as evidence that the text is implementable, not that it has been independently interpreted.¶
Experimental neighbours are not CLC. The WIT/WPT interop study in
varwof/aic-jwt (wit-wpt-interop/) is an experimental research artifact that
implements a different, wider wildcard surface (**, {a,b}, [a-z]) which
this revision rejects as unsupported_wildcard (Section 9.2). It is not a CLC-A
implementation and MUST NOT be cited as one; it exists to study WIT/WPT
provisioning and carries its own EXPERIMENTAL banner.¶
Every implementation declares a language revision CLC-<major>.<minor> —
this document declares CLC-1.15. A capability input (grant,
operation, or OCM) SHOULD carry the revision it was authored against; an
input without a declared revision is treated as CLC-1.0.¶
Compatible reading: an implementation MAY evaluate an input whose declared major equals its own AND whose declared minor is ≤ its own (so an implementation of CLC-1.3 reads a CLC-1.0/1.1/1.2/1.3 input, but not CLC-1.4 or CLC-2.0).¶
CLC-1.2 is additive: it adds the unresolved field to the Decision
shape and the invalid_constraint reason code without changing v1
verdicts on existing inputs; a CLC-1.1 implementation MAY claim CLC-1.1
against this document but is not CLC-A conformant (Section 12) until it exposes
unresolved and rejects non-conforming recognized-type values.¶
CLC-1.3 is additive, with one verdict value re-scoped: it adds the
allow_unresolved verdict value, closes the decision loop for residual
obligations, and re-scopes allow to mean "fully enforced" only;
allow/deny outputs on inputs with no residual obligations do not
change. A CLC-1.2 implementation MAY claim CLC-1.2 against this document
but is not CLC-A conformant (Section 12) until it emits the allow_unresolved
value and applies the Section 8.1 (scheme,type) identity and cross-midnight
window grammar.¶
CLC-1.9 is additive: it adds the containment relation and conformance
class CLC-D (Section 13) without changing any CLC-A verdict, reason code or
vector. A CLC-1.8 implementation that does not claim CLC-D MAY claim
CLC-1.8 against this document and remains CLC-A conformant; an
implementation that claims CLC-D MUST pass containment-vectors.json
(Section 12).¶
CLC-1.10 is additive with a minor gate: it adds the optional
param_bounds field and its four reason codes (Section 6.5). Every input that
declares no param_bounds is unchanged, so a CLC-1.10 implementation reads
every CLC-1.x input. An input that uses param_bounds MUST declare
CLC-1.10 or later; an implementation that does not implement Section 6.5 MUST refuse
such an input through the minor gate (unsupported_language_revision) and
MUST NOT ignore the field — silently dropping bounds would grant more than
the input declares (fail-closed).¶
CLC-1.11 is additive: it adds the Resolve function (Section 8.5) and its two
input-error reason codes (invalid_resolution, invalid_timestamp) without
changing any Authorize verdict, reason code or vector. A CLC-1.10
implementation that does not implement Resolve MAY claim CLC-1.10 against
this document and remains CLC-A conformant; Resolve is exercised only when a
consumer feeds a decision back, so no input shape is reinterpreted. A
time:window obligation is still delivered as allow_unresolved (Section 8.4) — the
core clock evaluator of Section 8.5 runs only inside Resolve with a supplied now.¶
CLC-1.12 is additive: it adds the derived function ConstraintUnion
(Section 7.1) without changing any other relation, verdict, reason code or vector.
The function is exactly the constraint projection of Intersect rule 3, so
Intersect's output is unchanged; an implementation that does not implement
ConstraintUnion MAY claim CLC-1.11 against this document and remains CLC-A
conformant.¶
CLC-1.13 is additive and CLC-D-scoped: it adds AuthorizeWithChain
(Section 13.11), a CLC-D function that fixes the order of Contains and
Authorize over an effective-chain Intersect. It changes no CLC-A
verdict, reason code or vector; an implementation claiming only CLC-A is
unaffected, and a CLC-D implementation adds it to its CLC-1.9-era
Contains surface.¶
CLC-1.14 is additive: it defines the intersection of param_bounds
(BoundMeet, Section 6.6) so that Intersect can combine a grant carrying the
bounds Section 6.5 defines. It changes no verdict, reason code or vector for an
input without param_bounds; it changes the refusal of an input with
param_bounds from invalid_params_binding to the correct meet or empty-meet
result. An implementation that does not implement Section 6.6 MUST refuse a
param_bounds input through the minor gate rather than intersect it wrongly.
Honest scope of that change (noted in rev CLC-1.15). One subset of
inputs does not evaluate identically across the CLC-1.10→1.14 minor
range: the inputs that declare param_bounds and reach a relation that
meets them — Intersect (Section 7) or AuthorizeWithChain step 3 (Section 13.11) —
with two or more sources declaring a Bound for the same key. On that
subset the direction of the change is deny → allow: what CLC-1.10–1.13
uniformly refused (invalid_params_binding) becomes the correct meet
result (usually an allow-side effective grant); where the meet is empty,
the denial's reason changes from invalid_params_binding to no_overlap.
Entailment against a single grant (Entails/Authorize), containment, and
every input without param_bounds are verdict-stable across the whole 1.x
range. The compatible-reading rule above is therefore intact — it governs
readability, not verdict stability — but verdict stability across minor
revisions does not hold for that one subset, and a consumer MUST NOT assume
it. (Rev CLC-1.15 moves the same subset again, in the opposite direction,
for cross-family meets only; see its entry below.)¶
CLC-1.15 is a corrective revision (review-driven; see the Revision
History): the cross-family numeric ∩ enum meet exception of CLC-1.14 is
removed, so every numeric × enum meet refuses with
invalid_params_binding in either source order (Section 6.6); enum membership
equal is defined as JSON type-sensitive equality (Section 6.5 layer 8);
ConstraintUnion's deterministic ordering is pinned to UTF-8 byte order
(Section 7.1); delegation_mode_not_narrower is attributed to the binding
profile's mode pre-check and moved out of the core reason commitments
(Section 13.4.5, Section 13.5, Section 13.6, Section 13.11); Contains antisymmetry is stated on
semantic equivalence classes (Section 13.3); and AuthorizeWithChain states the
caller's complete-authenticated-root-first-chain obligation (Section 13.11). It
changes no verdict, reason code or vector for an input without
param_bounds. On the Section 6.6 meet subset the direction partly reverses
CLC-1.14: a cross-family numeric × enum intersection that CLC-1.14
reduced to a filtered enum — an allow-side effective grant — now refuses
(allow → deny, invalid_params_binding), because the filtered enum was
broader than either source (Section 6.6); same-family meets are unchanged. The
same review adjudicated the scalar × nested pair (numeric or enum vs.
object, either order) to the same cross-family refusal: that pair was
already denied in CLC-1.14 (as no_overlap), so the direction there
changes the reason code only (→ invalid_params_binding), not the
verdict (Section 6.6, design-notes D12). The Section 8.4 residual-obligation list and
the Resolve output re-use the Section 7.1 UTF-8 byte-order collation; an
implementation whose native default ordering is UTF-16 code-unit order MUST
apply the pinned comparison explicitly. An implementation that does not
apply this revision MUST declare CLC-1.14 or earlier and let the minor gate
(Section 12.1) resolve any input that relies on the corrected behavior.¶
CLC-A conformance and the minor gate are the two sides of one rule.
Claiming CLC-A (this section) means implementing the CLC-A-relevant
semantics of the revision claimed — so an implementation that advertises
CLC-1.15 MUST implement param_bounds (grammar and the Section 6.6 meet), Resolve, ConstraintUnion and
the Section 6.2 canonicalization, not merely tolerate their inputs. The minor gate
is the complement for an implementation that lags: it declares an older
revision and refuses any input that uses a field or function introduced
after it. The two MUST agree — an implementation MUST NOT claim a revision
higher than it implements (which would silently evaluate newer inputs), nor
claim CLC-A while refusing a well-formed input of its own declared revision.¶
Incompatible reading MUST fail closed with
deny("unsupported_language_revision"). An implementation MUST NOT
silently evaluate under a different revision — no downgrade, no
warning-then-allow.¶
The revision check resolves before any Section 9.1 layer and yields the
single resolved reason code unsupported_language_revision.¶
Vectors: revision-001 (input CLC-1.0 against an implementation declaring
CLC-1.3 → eval normally, allow); revision-002 (input CLC-2.0 → deny
unsupported_language_revision).¶
This section defines containment, the third grant-level relation of CLC-v1 alongside entailment (Section 6.1) and intersection (Section 7), and the conformance class CLC-D that exercises it. It is additive: it changes no CLC-A verdict, reason code or vector, and a CLC-A implementation that does not claim CLC-D is unaffected. It was folded into this document from the formerly separate containment extension, which is retired.¶
The two relations the core defines answer different questions:¶
Entailment (Section 6.1): does an operation fit inside a grant?¶
Intersection (Section 7): what is the effective set when several grants cover one authority?¶
Neither answers the question a delegation chain asks of every hop: is the child's declared grant inside the parent's declared grant? Intersection delivers a set that is necessarily common to the sources, but a result fitted to two grants proves nothing that one of the grants' boundaries would not also authorize on its own — it reasons about the combined set, not about a child that must be a subset of a specific parent.¶
The AIC ecosystem needs the child-parent question at two concrete boundaries:¶
Delegation (DelegationAuthTBS vs. a principal's grant): a sub-agent's
requested capabilities, constraints and delegation mode must lie inside what
the principal authorized. Without a shared relation this obligation is the
profile's job and is re-implemented per profile, so two profiles can differ
on the same sub-agent grant.¶
Publication bound (rule signing): a rule a certificate holder publishes
must not exceed the holder's own grant. This section states the general
relation; register/ruleexec already applies the same idea in one instance
(RuleWithinSignerGrant), and is a candidate early adopter.¶
Containment is intentionally additive, not a CLC-v2 feature: it does not change any CLC-A verdict, does not touch the CLC-A corpus, and can be adopted by any implementation that claims CLC-A without breaking compatible reading of existing inputs.¶
Grant and Operation are as defined in Section 2/Section 5 (identifier per Section 3, parameters per
Section 6.2, constraints per Section 8). Contains(GP, GC) names the relation parent GP
× child GC.¶
Declared set — the parameters a grant carries as constraints on
operations (Section 6.2 value semantics: numbers bind as upper bounds, arrays as
membership sets, objects recursively per key, explicit empty []/{} deny
the class, {}≡absent).¶
Bound — a declared constraint value as interpreted by Section 6.2/Section 8.1 value semantics.¶
Narrower — a child grant is narrower than its parent when every value it declares is within the parent's declared bounds and its key set is closed by the parent's. Where the carrier defines a delegation mode, the child's mode must not widen the parent's either — that half is the binding profile's pre-check, not part of the language relation (Section 13.4.5).¶
Mode lattice — an abstract carrier-defined order over delegation modes,
exercised by the binding profile's pre-check (Section 13.4.5), never by Contains;
the AIC-JWT ordering is pinned in Section 13.8.¶
Contains(GP: Grant, GC: Grant) -> ContainmentResult
ContainmentResult = { // JSON object (not ASN.1)
"contains": <boolean>, // false <=> one of the Section 13.4 layers failed
"reason": <string> // resolved reason code (core Section 9.2 code)
}
¶
contains is true only when every layer of Section 13.4 passes.¶
reason on success is empty; on failure it carries the first failing
layer's reason code, per Section 13.4 layer order (deterministic: input order of the
two grants never influences which layer reports first).¶
The relation is antisymmetric on semantic equivalence classes, not on
Grant objects: Contains(A,B) and Contains(B,A) both hold exactly when A
and B fall in the same class — they denote the same identifier coverage,
the same declared parameter key set and the same bounds under the Section 6.2/Section 6.5
value semantics, with surface-equivalent spellings identified (params:{}
≡ absent, Section 6.2). Two syntactically different Grant objects in one class
(an equivalence, not an identity of JSON text) therefore contain each other;
containment of grants in two distinct classes in both directions is a
contradiction and MUST NOT be reported. Delegation modes are not part of
the relation (Section 13.4.5), so mode equality is neither required nor observable
here — where a carrier binds modes, the profile's pre-check owns that
comparison.¶
The relation resolves layers strictly in order; the first failure determines
the reason code. Every layer is fail-closed: any doubt yields false.¶
If a grant identifier is malformed per Section 3, Contains returns
false with the matching CLC-A syntax code (invalid_capability_id /
missing_capability_id). Validation errors are reused from the core,
not re-invented.¶
If GC.Params violates the grant-side parameter grammar (Section 6.2), the child is
not a valid grant and Contains yields false
(invalid_params_<n> codes as in the core).¶
An invalid parent grant also yields false: a boundary that cannot itself be
evaluated must not authorize a child.¶
The child identifier must be covered by the parent identifier using exactly
the CLC-v1 path-coverage relation (the rules Entails applies to identifiers,
Section 6.1/Section 6.3, with parameters excluded — the identical keyset empty-object edge is
not relevant here because parameters are excluded from this layer by
construction):¶
same namespace (scheme:action-class) — else different_namespace;¶
same segment depth with equal literal segments, OR parent trailing *
wildcard covering the child's trailing segments ("*" matches one or more
segments, never zero) — else child_exceeds_parent.¶
Mid-identifier wildcards remain unsupported_wildcard per Section 3: this section does
not enlarge the v1 wildcard surface.¶
Every parameter the child declares must be within the parent's declared
bounds, using the Section 6.2 value-subset semantics, and the key sets must be
identical (symmetric closure). The child and parent key sets are each the
union of params and param_bounds keys (Section 6.5):¶
number (from params): child value ≤ parent bound (upper bound only in
params; richer bounds are expressed in param_bounds, below);¶
string / boolean: exact equality with the parent's value;¶
array (enum): every child element must equal a member of the parent's set;
a child empty array inside a non-empty parent enum is vacuously within it
(child denotes "nothing", which is inside anything) — but a parent empty
set denies the class (deny-when-declared), and a child empty set under a
parent empty set is therefore false (params_not_narrower): the class is
denied, not "narrowed to nothing";¶
object: recursion per shared key, with the same symmetric key closure at every depth (a child object may not add a key the parent object does not declare, and may not omit one the parent declares);¶
param_bounds bound (Section 6.5): for a key with a Bound on the parent's side,
the child's Bound for that key must be within it — a numeric family bound must
have min raised or equal and max lowered or equal (min_child ≥ min_parent
/ max_child ≤ max_parent, and the child MUST declare a min/max the parent
declares), a step must be an integer multiple of the parent's step (a
coarser-or-equal grid whose values are a subset of the parent's allowed
values), an enum family must be a subset with min_items/max_items
tightened or equal, and a nested bound recurses. A parent key that is
required (optional absent or false) forces the child's key to be required;
a child MAY keep an optional key optional or make it required, but MUST NOT
turn a required parent key optional. A child Bound that adds a family the
parent does not declare, or omits a bound the parent declares, is
params_not_narrower (the child would allow a value the parent denies);¶
key closure: the child key set MUST equal the parent key set, both ways.
A child that omits a parent-declared key would allow operations the parent
denies (the missing key is params_missing in entailment); a child that adds
a key the parent does not declare allows operations the parent denies
(undeclared_param). Either failure is params_not_narrower. This is the
same symmetric closure Section 6.2 layer 7 applies to an operation vs. a grant,
carried over to grant-vs-grant comparison;¶
extra rule: the parent's {} (or absent) params object is unconstrained
and contains any child params; a child {} under a bounded parent is
false (params_not_narrower) — declaring nothing is not the same as
declaring a subset of the parent's bounds.¶
The presence semantics match Section 6.2 exactly: a grant (parent or child) whose
params are present-but-empty {} is unconstrained, identical to an absent
params object (Section 9.2/Section 6.2).¶
Constraints are not part of Contains. A grant carries constraints (Section 8),
but they are a separate axis with a different composition rule:¶
Constraints compose by union (conjunction), not by subset. Along a
delegation chain the effective constraint set is the union of every link's
constraints (Section 7 Intersect already unions them). A child therefore
need not re-declare its parent's constraints, and adding or tightening a
constraint only narrows.¶
Contains compares identifier and parameters only. It does not read,
compare, or validate the constraints field: a constraint difference never
changes the Contains verdict.¶
Constraint grammar and evaluation stay where they already are. Whether a
constraint identity is recognized, whether its value is in grammar, and
whether an operation satisfies it are the CLC-A concern of the consumer and
Intersect/the decision function (including the allow_unresolved
residual-obligation channel, Section 8.4). This section neither re-decides nor
weakens them.¶
Consequences a reviewer should take as intended: Contains(P, C) does not
by itself assert that C's operation set is inside P's when constraints are in
play — the parent's constraints are carried forward by the chain's union
(Intersect), and a consumer that uses Contains as its only gate must
compose the chain's intersections as well (Section 13.8). This is the honest reading:
containment is a relation over the declared identifier and parameter
boundary, and constraints are enforced by the union, not by this relation.¶
Where a carrier defines a delegation mode, the child's mode must not widen the
parent's. This check is a required binding-profile pre-check, not a layer
of the language relation: as with constraints (Section 13.4.4), mode is a
carrier-level concept — Grant values carry no mode, and the relation
Contains(GP, GC) takes no mode argument, so a core Contains verdict can
never express a mode decision. A binding profile (Section 13.8.1) that maps a
mode-carrying carrier MUST run the mode-lattice check itself, before or
alongside each Contains call, and MUST report
delegation_mode_not_narrower from the profile when the child's mode
widens the parent's; it MUST NOT rely on Contains for that check and MUST
NOT present a Contains verdict as evidence that the mode narrowed. The
lattice order is carrier-defined (CLC-D does not invent modes); the AIC-JWT
order is authorized < representative — a child may be authorized under a
representative parent, never the reverse. A carrier without a mode concept
has no pre-check to run.¶
CLC-D registers exactly three child-level reason codes; two of them are
returned by the relation, and everything else a Contains result carries
reuses CLC-A codes. The third is produced only by the binding-profile
delegation-mode pre-check (Section 13.4.5): the relation takes no mode argument and
MUST NOT return it. An implementation MAY collapse child_exceeds_parent
for identifier failures into the core's capability_not_authorized at a
boundary that must not reveal policy shape (Section 11), but MUST NOT collapse
params_not_narrower; a profile that maps a mode-carrying carrier MUST NOT
collapse delegation_mode_not_narrower either.¶
| Code | Produced by | Meaning |
|---|---|---|
child_exceeds_parent
|
Contains, layer 2 (Section 13.4.2) |
child identifier not covered by parent identifier |
params_not_narrower
|
Contains, layer 3 (Section 13.4.3) |
a child parameter is not within the parent's declared bounds / key set |
delegation_mode_not_narrower
|
binding-profile pre-check (Section 13.4.5) — never by Contains
|
child delegation mode (a carrier concept) widens the parent's |
There is deliberately no constraint reason code: constraints are not part of the relation (Section 13.4.4).¶
CLC-D is an optional conformance class stacked on CLC-A. A conforming implementation:¶
MUST implement Contains (Section 13.4) and the relation's two Section 13.5 reason codes,
and pass the CLC-D corpus (Section 13.7); where the implementation also ships a
binding profile for a mode-carrying carrier, that profile owns the
delegation_mode_not_narrower pre-check (Section 13.4.5);¶
MUST (rev CLC-1.13) implement AuthorizeWithChain (Section 13.11) and pass the
authorize-chain-vectors.json corpus, so the one-call chain check is exercised
by the same class that owns containment;¶
MUST NOT alter any CLC-A verdict, reason code, or the CLC-A corpus — this section is strictly additive;¶
MUST treat containment as declared-set comparison: it declares no consequence about execution lifecycle, time windows after the fact, or post-hoc bounds; where a binding profile cannot establish containment at delegation time, the profile MUST NOT authorize.¶
An implementation claiming CLC-A does not claim CLC-D. A delegation profile (a binding profile (Section 13.8), an AIC-JWT DA validator, a certificate-issuance stack) that needs the boundary check SHOULD require CLC-D of the module it delegates to, and MUST NOT substitute intersection (Section 7) for containment.¶
Implementation status (single-author parity). Contains is implemented in
all three reference implementations (Go: register/semantics.Contains;
Python: aic-capability-demo/clc_semantics.py contains; TypeScript:
ts/clc_semantics.ts contains) and mirrors the cases this section pins.
Agreement between implementations that share an author is a regression test for
this text, not independent validation — the Section 12 honesty rule applies unchanged
to CLC-D (see Section 13.7).¶
Because CLC-D is a new relation, its corpus is new and independent of the CLC-A
suites (vectors.json / property-cases.json / crosswalk-vectors.json /
evidence-vectors.json). The corpus ships as
capability/data/_vectors/clc-d/containment-vectors.json (64 vectors) and a
forward-closure property file
capability/data/_vectors/clc-d/containment-property-cases.json (784 cases
× 39 shared operations, generated by
capability/scripts/gen-contain-property-cases.py), plus a cross-walk corpus
capability/data/_vectors/clc-d/containment-crosswalk-vectors.json (44
vectors) that maps a carrier's native representation (AIC-JWT DA, OAuth RAR,
UCAN, delegation chain, and the adjacent agent drafts ATN, AAT, AIP, AAE, AOA,
AEGIS) to a grant on each side and asserts Contains (Section 13.9.3). Rev CLC-1.13
adds capability/data/_vectors/clc-d/authorize-chain-vectors.json (15
vectors) pinning the fused AuthorizeWithChain relation (Section 13.11).
The vectors cover:¶
identifier coverage (layer 2): trailing-wildcard coverage, namespace
mismatch, depth mismatch, same-length literal mismatch, unsupported_wildcard
kept stable, wildcard-requires-a-trailing-segment;¶
parameter narrowing (layer 3): number upper bound at/under/over, enum
subset / element-absent / scalar-member, empty child enum under non-empty
parent, parent empty set deny-class, key-closure violation in both directions
(child omits, child adds), nested object recursion and nested key closure,
child {} under bounded parent, child null (layer-1 grammar), unconstrained
parent {};¶
constraint non-participation (Section 13.4.4): a tighter, wider, added, dropped,
or differently-identified child constraint, and a child constraint the parent
lacks — every one MUST leave the verdict unchanged (all assert contains);¶
validity (layer 1): malformed parent/child identifiers, null params;¶
symmetry: both-directions containment identity, antisymmetry probe pair (contain-040/041), a fully-narrower combined grant.¶
The delegation-mode lattice is not part of the language-level corpus: the
relation Contains(GP, GC) takes no mode, so a profile that binds a carrier
mode (the AIC-JWT DA validator, Section 13.8) exercises it itself. The language corpus
therefore ships no mode vectors; a carrier adopting CLC-D MUST add mode vectors
to its own profile corpus.¶
The property the property corpus checks is forward closure: for every
(parent P, child C) and every operation o in the shared sample,
Contains(P, C) MUST NOT raise, and
Contains(P, C) ∧ Entails(C, o) ⟹ Entails(P, o). This is the declared-set
consequence that makes a delegation boundary sound, and the reason the Section 13.5
collapse of layer-2 failures into child_exceeds_parent cannot weaken the
boundary. On the shipped corpus the Go and Python/TS runs report the same
120 contained pairs and 30576 operation checks with zero violations.¶
The parity bar and the "independence of implementations" honesty rule of Section 12 apply unchanged to CLC-D: until two independent implementations agree, the corpus is evidence that this text is implementable, not that it has been independently interpreted.¶
This subsection is a cross-walk profile of the relation, not part of the
language. At the AIC delegation boundary the parent grant is produced from
the principal's authorization and the child grant from the
DelegationAuthTBS/AIC-JWT DA capabilities:¶
Parent grant GP: the principal's capability entry (scheme:id params),
plus principal-level authorizationConstraints projected as parent
constraints, plus the principal's own delegation mode.¶
Child grant GC: the sub-agent's requested capability
(Capability.SchemeId:CapabilityId, Parameters), its authorizationConstraints,
and its DelegationMode.¶
Boundary result: P_effective = P_principal ∩ C_agent ∩ P_gateway
keeps its intersection meaning — intersection determines the
effective set; containment (Section 13.4) is the per-child admission predicate
that runs before the intersection is composed, on each
(parent-capability, child-capability) pair. The child's constraints are not
compared by containment; they are carried forward by the intersection's union
(Section 13.4.4), so the principal's constraints remain in force in P_effective.¶
A sub-agent that requests a capability the principal did not grant fails at
layer 2 (child_exceeds_parent); a sub-agent whose requested params exceed the
principal's declared bounds fails at layer 3; a sub-agent that widens the
carrier's delegation mode fails the lattice (Section 13.4.5). Binding profiles MUST
surface the reason code into their audit trail, and MUST compose the
intersection so the principal's constraints stay in force.¶
A binding profile maps a carrier's native authorization structure to the CLC
grants Contains compares (Section 13.9.3). The profile is the carrier's, not the
language's (a carrier's field names appear in its own profile, never in the
language text). Because every Section 13.9.3 mapping is a profile, this subsection
states the obligations a conforming profile carries. It adds no CLC-A or CLC-D
verdict and changes no relation.¶
Resolve the carrier's own inheritance and defaults before mapping. The
language has no inheritance, no default values and no "absent means inherit"
rule (Section 13.10.1). A carrier that resolves an absent dimension against an
ancestor (AIP Section 4.4) MUST do so in the profile and emit the resolved declared
set. Contains is defined over the grants the profile emits, so an
unresolved default is a profile defect, not a containment result.¶
Preserve every identity dimension the carrier treats as identity. If the
carrier treats two artifacts with the same name but different metadata as
distinct capabilities (ATN Section 9.1: same id, different schema.digest), the
profile MUST carry that metadata into the mapped grant — here, a trailing
identifier segment — so a mismatch fails closed. A profile MUST NOT drop an
identity dimension and then compare the artifacts as equal.¶
Residualize semantics the relation cannot see; never drop them silently.
Dimensions the mapped grant has no field for (AEGIS allowed_roles,
environment, risk_level; AAE validity) are not compared by Contains.
The profile either models them in its own document and enforces them outside
the relation, or declares them out of scope — but it MUST NOT report
containment as if they had been checked (fail-closed: what the relation
cannot see, it does not permit).¶
Do not invent carrier semantics the carrier does not state. A profile must not widen what the carrier leaves undefined into an allow. AEGIS's dotted ids are hierarchical, but AEGIS grants capabilities individually and does not define domain-level containment, so the profile does not turn a bare domain into a namespace wildcard (ccx-042 fails closed).¶
Non-goals (recorded, not prohibitions on carriers). Three properties are deliberately outside both the relation and the profile contract:¶
union of authority sources — a sub-agent that combines narrow delegated authority with broad independent authority (AEGIS Section 5.1, AOA) composes over a set of grants, which is carrier composition governance, not a per-pair predicate;¶
chain-level verification — Contains is a per-pair predicate, not a
transitive closure; a chain check is the profile's iteration of Contains
over hops (Section 13.9.3, delegation-chain->clc-v1);¶
cross-carrier identity — CLC defines no equivalence between two carriers' capability names; a profile MAY publish its own mapping, but the language asserts none.¶
Section 13.10 records the consciously-deferred gaps surfaced in review; the items already landed as core revisions are marked closed, the rest are recorded for later discussion. Items marked candidate v1.x are candidates for the core language to adopt without breaking CLC-A inputs; items under "CLC-D" would extend containment itself.¶
Landed as Section 6.5, the optional param_bounds field:¶
Number: inclusive min/max intervals and a step multiple rule.¶
Enums: array membership plus min_items/max_items cardinality.¶
Optional keys: an optional marker exempts a declared key from the
omission half of layer 7.¶
Defaults: a param_defaults grammar with the precedence explicit >
default > absent.¶
The rejected shape is a {min,max} object inside params — it would collide
with object recursion (Section 6.2), so the bounds live in a sibling field and no
existing grant changes meaning.¶
allow_unresolved (closed in CLC-1.11)
Section 8.4 delivers residual obligations on an allow_unresolved verdict; Section 8.5
now defines the consumer's feedback loop and this item is closed:¶
Resolve(decision, resolutions, now?) -> Decision, with each unresolved
constraint reported as satisfied / violated / unknown;¶
a propagation rule for partially-evaluated sets (all satisfied → allow, any
violated → deny with {type}:violated, remainder → allow_unresolved);¶
staleness/TTL semantics for time:window residuals: with now, the core
clock evaluates the window and the discharge horizon is the current segment's
end, so a cached allow expires with the window.¶
This went beyond the original "Resolve is core, TTL is profile policy" split:
both are now core (Section 8.5). The coarser identity-level consumer gate Discharge
(the reference implementations' helper) remains as the satisfied-only case.¶
This revision makes an explicit design decision: constraints are not part of
the containment relation (Section 13.4.4). They compose by union (conjunction) along
a chain, which Intersect already implements, and no Contains layer reads
them.¶
Contains therefore stays the smaller relation — containment over
(identifier, parameters) — and the "what does the chain collectively
require?" question is answered by the separate derived function
ConstraintUnion (Section 7.1, new in CLC-1.12). Folding the union into Contains
was considered and rejected: it would make the relation no longer a pure subset
on the declared tuple, and a null constraint check would then be able to hide a
broken identifier/parameter boundary. A consumer wanting "the child's whole
authority is inside the parent's" composes the two: Contains per hop plus
ConstraintUnion over the chain.¶
Containment is folded into this document's revision stream: its changes are
recorded in the Revision History and its conformance class CLC-D is declared in
Section 12.1 in step with the language revision (this revision is CLC-1.15; CLC-D
first appeared in CLC-1.9, folded from EXT-00 rev 0).¶
A CLC-A input is unaffected by the addition of CLC-D; compatible reading of CLC-A inputs is the floor.¶
CLC-D adoption is per-implementation: an implementation may claim CLC-A without claiming CLC-D.¶
The corpus (64 containment vectors, 784 property cases, 44 cross-walk
vectors, 15 AuthorizeWithChain vectors) is a draft snapshot; the README in
capability/data/_vectors/clc-d/ maintains the live count and the date the
snapshot was generated.¶
Fail-closed: undefined/malformed/unknown → deny.¶
Deny-when-declared: empty bounds deny the class.¶
No canonical broadening: segment-boundary, not lexical prefix.¶
Composition narrows only: an intersection removes authority. Whether a
delegated grant stays inside its parent's boundary is the containment
question Section 13 answers (Contains); intersection alone (Section 7) does not answer it,
and a delegation profile MUST NOT substitute one for the other.¶
Containment is fail-closed by construction: every Contains layer
defaults to false; a grant pair any layer cannot validate is refused, with
no warning-then-allow path (Section 13.4).¶
Containment is not a constraint oracle: Contains does not read
constraints (Section 13.4.4). A consumer that uses Contains as its only gate MUST
compose the delegation chain's Intersect so the parent's constraints stay in
force; otherwise a child that omits a parent constraint could be admitted
while the constraint is unenforced. allow_unresolved is orthogonal to
containment and MUST NOT be read as a containment result.¶
Containment reason-code leakage: child_exceeds_parent reveals that a
child requested an identifier the parent does not cover. A boundary that must
not leak policy shape MAY collapse that single code into
capability_not_authorized (never params_not_narrower, Section 13.5).¶
Mode lattice must be carrier-pinned: an implementer that maps
authorized/representative the wrong way round inverts the boundary; Section 13.8
pins the AIC-JWT ordering, and other carriers MUST pin theirs in the profile that
adopts CLC-D.¶
Stable reason codes: same input → same reason across implementations.¶
Evidence binding is separate from native verification: Match checks content correlation; native verification is the consumer's responsibility.¶
Parsing divergence must not change the decision: params are normalized at the input boundary per Section 6.2 (JCS serialization, duplicate keys, non-finite/over-precision numbers, size/depth caps). A consumer that decodes into a re-orderable map and re-encodes loses duplicate keys and cannot represent non-finite numbers; two such consumers would reach different verdicts on the same raw input. Decisions are made on the boundary-validated form, not on a lossy re-serialization.¶
Recognized-but-unevaluated is not silent acceptance: a constraint
the core recognizes but cannot evaluate MUST appear in the decision's
unresolved field — never dropped (Section 8.4). The consumer must evaluate
or confirm each such constraint before acting, otherwise it MUST deny
(AAC Section 6.6).¶
A core-clock discharge is time-bounded: when Resolve (Section 8.5) evaluates a
time:window obligation with a supplied now, the discharge is valid only
for the segment containing now. A consumer MUST NOT cache the resulting
allow past that segment's end — it re-invokes Resolve with a current now
before acting, or derives the horizon from the segment end. Caching a
clock-based allow turns a window into an unbounded permit.¶
Reason-code detail suffix is diagnostic-only: everything after the
first : (e.g. the offending param name) MUST NOT change the verdict and
MUST NOT be relied upon for decisions. Consumers match on the code
prefix before the : (Section 9.2).¶
Revision mismatch is fail-closed: an incompatible language revision
(Section 12.1) yields deny("unsupported_language_revision") resolved before
any layer — never a silent downgrade or best-effort re-interpretation.¶
Resource exhaustion is bounded at the input boundary: the 512-byte
serialized-size cap and the depth-32 nesting cap (Section 6.2 step 4) apply to
raw_params as much as to every other input, keeping recursive
evaluators safe from deep-nesting and oversized-params blowup.¶
This document requests no IANA actions.¶
Constraint types (max_rows, time, network) and reason codes are defined
by this document as fixed sets. Should this work be adopted by a working
group, that group may wish to consider whether either set warrants a registry;
this revision does not propose one.¶
The containment relation (Section 13) registers three additional reason codes
(child_exceeds_parent, params_not_narrower,
delegation_mode_not_narrower — the last produced by the binding profile's
delegation-mode pre-check, Section 13.4.5, never by the relation itself); they are
part of the same fixed set, and no constraint reason code is defined for
containment because constraints are outside the relation (Section 13.4.4).¶
The language itself transports and stores nothing. Privacy exposure comes from what carriers put into it and from what evaluators report:¶
Capability identifiers and parameter values describe policy. They can reveal
organizational structure, service topology, network ranges (network
constraints), working hours (time windows), tenant names, or purposes.
Deployments should treat grants as policy-confidential material.¶
Distinct reason codes reveal the shape of a grant: the difference between
params_missing, undeclared_param and not_in_enum tells an observer what
the grant constrains. Where the requester is untrusted, a consumer should
consider collapsing reason codes at the boundary, as this specification
already does for identifier-level failures (Section 9.1).¶
Parameter values may carry personal data if a scheme defines them that way. Scheme authors should avoid personal identifiers as parameter names or values.¶
Residual obligations (unresolved, Section 8.4) and any audit record built
from decisions can persist policy and usage information; retention is the
carrier's responsibility (Section 11).¶
The reference corpus published with this document is synthetic and contains no personal data.¶
Containment reasons (Section 13) are emitted to the child's requestor at the delegation step, not to end-users, and the child receives the failure code, not the parent's declared bounds. Where bounds themselves are sensitive (e.g. network/scope declarations), a profile SHOULD log codes, not values.¶
The rows below are examples of consumers of this shared vocabulary, not required profiles: conformance to CLC-A does not depend on any of them. RAR authorization details [RFC9396] are one such carrier; CLC is a candidate evaluation language for the capabilities they declare.¶
| Consumer | Grammar | Binding | Verdict | Notes |
|---|---|---|---|---|
| AIC-JWT DA | capability[].id | Entailment (Section 6.1) | Decision (Section 9) | AIC-JWT Section 5 binding |
| EMILIA AEB | AEG capability_class | Match (Section 6.4) + Entailment (Section 6.1) | SATISFIED (Section 10) + Decision (Section 9) | AEB Section 3 decision levels (VERIFIED/MATCH/SATISFIED) + Section 5.1 ObservedAction + Section 7 AEC slots; a VERIFIED authorization artifact carries the Grant |
| RAR authorization_details | type="capability" | Entailment (Section 6.1) | Decision (Section 9) | [RFC9396] format |
| Delegation chain | each hop's declared set | Intersection (Section 7) | Decision (Section 9) | intersection over declared sets only; a hop that must stay inside its parent is checked with Containment (Section 13) |
| Delegation containment | parent and child boundaries | Contains (Section 13) | Containment verdict (Section 13) | per-hop child ⊆ parent; carrier vocabularies and reason-code mapping in Appendix C |
Grouping vs kind mapping: The appendix groups vectors by semantic
category (B.1–B.6). The machine-readable vectors.json uses a kind
field that collates these groups differently:
kind=entail (47) covers B.2 (8), the 35 params vectors that sit under
kind=entail (B.3's 39 rows minus params-028/-029/-034/-036, which are
kind=decide), and the four scheme stress-test entail vectors
(clinical-001/-002, payments-001, data-002); kind=decide (50)
covers the B.5 rows below (34), the seven combined decision vectors,
payments-002 and data-001, the four params boundary decisions that
sit under kind=decide (params-028/-029/-034/-036, all four also
listed in B.3) and the three nested key-closure vectors
(nested-001/-002/-003);
kind=intersect (17) covers B.4 (10), the four combined vectors that
call the intersect function (combined-004/-005/-008/-011) and the three
CLC-1.15 cross-type audit vectors (intersect-011/-012/-013, outside the
B.1–B.6 tables); kind=syntax (9) is exactly B.1. These counts are reproducible from the corpus itself:
every vector in vectors.json carries its kind, so the mapping is
machine-checkable rather than maintained by hand.¶
| # | Input | Expected | Derivation |
|---|---|---|---|
| S1 |
std/database-v1:query:SELECT
|
valid | literal identifier |
| S2 |
std/database-v1:query:*
|
valid | trailing wildcard |
| S3 |
*:query:SELECT
|
deny | bare * illegal |
| S4 |
std/database-v1:query:SEL*
|
deny | partial segment wildcard |
| S5 |
std/database-v1:query:{read,write}
|
deny | alternation not v1 |
| S6 |
std/database-v1:query:[a-z]
|
deny | character class not v1 |
| S7 |
database:query
|
deny(invalid_capability_id) |
Section 3 scheme grammar: no vendor "/" product "-v" major (the spec's own counter-example) |
| S8 |
bad:op
|
deny(invalid_capability_id) |
Section 3 scheme grammar: scheme bad does not match vendor/product-vN (snips the lax-intake hole) |
| S9 |
std/data-v1:fetch:item:42
|
valid | multi-segment action + conforming scheme (positive boundary) |
| # | Grant | Operation | Expected | Derivation |
|---|---|---|---|---|
| E1 |
std/database-v1:query:SELECT
|
std/database-v1:query:SELECT
|
allow | literal match |
| E2 |
std/database-v1:query:*
|
std/database-v1:query:SELECT
|
allow | trailing wildcard |
| E3 |
std/database-v1:query:*
|
std/database-v1:query:SELECT:deep
|
allow | wildcard multi-segment |
| E4 |
std/database-v1:query:*
|
std/database-v1:admin:DDL
|
deny | different namespace |
| E5 |
std/database-v1:query:*
|
std/database-v1:query
|
deny | no trailing segment |
| E6 |
std/database-v1:query:SELECT
|
std/database-v1:query:INSERT
|
deny | literal mismatch |
| E7 |
std/database-v1:*
|
std/database-v1:query:SELECT
|
deny | class-position (product-segment) wildcard is v1-forbidden: only a trailing action segment may be * (Section 3; Section 9.1 layer 3) |
| E8 |
std/database-v1:*
|
std/database-v1:admin:DDL
|
deny | same class-position wildcard; the v1-forbidden shape denies regardless of the action it faces (Section 3; Section 9.1 layer 3) |
| # | Grant | Operation | Expected | Derivation |
|---|---|---|---|---|
| P1 |
{"limit":100}
|
{"limit":50}
|
allow | 50 ≤ 100 |
| P2 |
{"limit":100}
|
{"limit":150}
|
deny | 150 > 100 |
| P3 |
{"tables":["a","b"]}
|
{"tables":["a"]}
|
allow | subset |
| P4 |
{"tables":["a"]}
|
{"tables":["a","b"]}
|
deny | "b" absent (not_in_enum) |
| P5 |
{"columns":{"t":["id"]}}
|
{"columns":{"t":["id","name"]}}
|
deny | "name" absent (not_in_enum) |
| P6 |
{}
|
{"limit":50}
|
allow | unconstrained ({} ≡ absent; the literal {} is pinned by decide-028, params-006 pins the absent form) |
| P7 |
{"tables":[]}
|
{"tables":["a"]}
|
deny | explicit empty bound denies the class |
| P8 |
{"limit":100}
|
{"limit":null}
|
deny | null invalid in v1 (invalid_params_null) |
| P9 |
{"station":[1,2,3]}
|
{"station":2}
|
allow | scalar member of array set |
| P10 |
{"station":[1,2,3]}
|
{"station":9}
|
deny | 9 ∉ allowed set (not_in_enum) |
| P11 |
{"station":[1,3]}
|
{"station":[1,3]}
|
allow | array, every element a member |
| P12 |
{"station":[1,3]}
|
{"station":[1,2,3]}
|
deny | 2 ∉ allowed set (not_in_enum) |
| P13 |
{"station":[1],"speed":0.3}
|
{"station":1,"speed":0.25}
|
allow | member + number bound unaffected |
| P14 |
{"station":[3]}
|
{"station":2}
|
deny | categorical: granting 3 does not cover 1/2 (not_in_enum) |
| P15 |
{"columns":{"t":["id","name"]}}
|
{"columns":{"t":["id"]}}
|
allow | object recursion: request element is a member of the granted set (Section 6.2 v1.1) |
| P16 |
{"limit":100}
|
raw {"limit":100,"limit":150}
|
deny(invalid_params_duplicate_key) |
duplicate JSON key rejected at input normalization (Section 6.2 step 2) |
| P17 |
{"limit":100}
|
raw {"limit":1e400}
|
deny(invalid_params_number) |
non-finite number literal (Section 6.2 step 3) |
| P18 |
{"s":"x"}
|
raw {"s":"<600 chars>"}
|
deny(invalid_params_size) |
serialized form > 512 B (Section 6.2 step 4) |
| P19 |
{"d":0}
|
raw depth-33 object | deny(invalid_params_size) |
nesting beyond depth 32 (Section 6.2 step 4) |
| P20 |
{"s":"x"}
|
raw {"s":"<512 B>"}
|
allow | serialized = exactly the 512 B limit (≤); positive boundary of P18 (Section 6.2 step 4) |
| P21 |
{"d":0}
|
raw depth-32 object | allow | depth exactly the 32 limit (≤); positive boundary of P19 (Section 6.2 step 4) |
| P22 |
{"flag":true}
|
{"flag":1}
|
deny(params_exceed_grant) |
boolean is exact and is NOT a number: 1 must not satisfy a granted true (params-022) |
| P23 |
{"flag":true}
|
{"flag":true}
|
allow | positive side of P22 (params-023) |
| P24 |
{"x":null}
|
(no params) | deny(invalid_params_null) |
layer 6 (null) precedes layer 7 (presence) (params-024) |
| P25 |
{"s":"x"}
|
raw {"s":"<512 B>","s":"dup"}
|
deny(invalid_params_size) |
multi-fault: size (4) precedes duplicate keys (2) (params-025) |
| P26 |
{"s":"x"}
|
raw {"n":1e400,"s":"<512 B>"}
|
deny(invalid_params_size) |
multi-fault: size (4) precedes number shape (3) (params-026) |
| P27 |
{"a":1}
|
raw {"a":1,"a":2,"n":1e400}
|
deny(invalid_params_duplicate_key) |
multi-fault under the size limit: duplicate keys (2) precede number shape (3) (params-027) |
| P28 |
{}
|
raw {"n":1e-6,"s":"<494 a>"}
|
deny(invalid_params_size) |
the received text is 511 octets but the JCS form writes 1e-6 as 0.000001, so the Section 6.2 step 4 size is 515 (params-033) |
| P29 |
{}
|
{"n":1e-6,"s":"<494 a>"} (decoded) |
deny(invalid_params_size) |
decoded counterpart of P28: both boundaries agree (params-034) |
| P30 |
{}
|
raw {"n":1.0,"s":"<498 a>"}
|
allow | the received text is 514 octets but the JCS form writes 1.0 as 1, so the size is 512 and inside the cap (params-035) |
| P31 |
{}
|
{"n":1.0,"s":"<498 a>"} (decoded) |
allow | decoded counterpart of P30: a raw check counting the received token would refuse it (params-036) |
| P32 |
{}
|
raw {"s":"<100×U+1F600>"}
|
allow | 100 literal astral characters are 408 JCS octets; counting UTF-16 code units would double the count and refuse (params-037) |
| P33 |
{}
|
raw {"s":"<100×\ud83d\ude00>"}
|
allow | escaped spelling of P32: literal and escaped forms of one string MUST reach the same verdict and the same size (params-038) |
| P34 |
{}
|
raw {"s":"a\nb"} (literal U+000A) |
deny(invalid_params_number) |
a literal control character is not valid JSON text; Go and Python refused it already (params-039) |
| P35 |
{}
|
{"x":"<260×é>"} (decoded) |
deny(invalid_params_size) |
260 U+00E9 code points serialize to 528 JCS octets > 512; non-ASCII sizes are measured on the canonical UTF-8 form, never in code points or UTF-16 units (params-028, rev CLC-1.4) |
| P36 |
{}
|
{"x":"<251×é>"} (decoded) |
allow | 251 U+00E9 serialize to 510 octets ≤ 512; the positive side of P35 (params-029, rev CLC-1.4) |
| P37 |
{"s":"x"}
|
raw {"s":"<251×\u00e9 escapes>"}
|
allow | the same 510-octet string spelled with \u00e9 escapes reaches the same verdict and the same size as the literal form of P36 (params-030, rev CLC-1.4) |
| P38 |
{"limit":100}
|
raw {"s":"\ud800"}
|
deny(invalid_params_number) |
a lone surrogate escape is not valid Unicode: refused at the raw boundary, never repaired to U+FFFD (RFC 8785 Section 3.2.2.2; Section 6.2 step 2; params-031, rev CLC-1.6) |
| P39 |
{}
|
raw {"s":"\ud83d\ude02"}
|
allow | a valid surrogate pair is one character (U+1F602, four UTF-8 octets) and counts as such under the size rule (params-032, rev CLC-1.6) |
Shorthand: params shown compact; constraints use colon notation.¶
| # | Source A | Source B | Expected | Derivation |
|---|---|---|---|---|
| I1 |
{"tables":["a","b"]}
|
{"tables":["a"]}
|
{"tables":["a"]}
|
overlap |
| I2 |
{"tables":["a"]}
|
{"tables":[]}
|
deny | deny-when-declared |
| I3 | (unconstrained) | (no grant) | deny | absent source |
| I4 |
{"limit":100}
|
{"limit":50}
|
{"limit":50}
|
tighter bound |
| I5 | (unconstrained) | constraint time:window:[{"start":"00:00","end":"01:00"}]
|
recognized, not evaluated → unresolved carried on allow_unresolved verdict |
constraint added |
| I6 |
{"tables":["a"]}
|
{"tables":["b"]}
|
deny | no overlap |
| I7 | (no source / null) |
— | deny(absent_source) |
zero sources: fail-closed (Section 7 rule 5) |
| I8 |
{"limit":50}
|
{}
|
{"limit":50}
|
empty params declares no constraint → bound preserved (bounded then empty, Section 7 rule 6) |
| I9 |
{}
|
{"limit":50}
|
{"limit":50}
|
source order must not matter (empty then bounded, Section 7 rule 6) |
| I10 |
{}, id query:SELECT
|
{}, id query:*
|
{}
|
narrower identifier wins; identifier comparison is params-free (Section 7 rule 2) |
| # | Scenario | Expected | Derivation |
|---|---|---|---|
| D1 | valid grant, valid op, constraints pass | allow | all checks pass |
| D2 | no matching grant | deny("capability_not_authorized") | fail-closed |
| D3 | unknown constraint type | deny("unknown_constraint") | fail-closed |
| D4 | malformed capability_id | deny("invalid_capability_id") | input validation |
| D5 | same input twice | same output | deterministic |
| D6 | constraint violation | deny("{type}:violated") | constraint fail |
| D7 | grant bounds a param, operation omits it | deny("params_missing") | Section 6.3 step 4 fail-closed |
| D8 | operation param value is null
|
deny("invalid_params_null") | Section 6.2 null rule; may carry : <param> detail (Section 9.2) |
| D9 | bounded grant, operation has no params field at all
|
deny("params_missing") | Section 6.3 step 4 fail-closed |
| D10 |
Authorize called with an absent/empty grant
|
deny("capability_not_authorized") | Section 9 fail-closed, no exception |
| D11 | input declares CLC-1.0 against a CLC-1.3 implementation | allow | same major, 1.0 ≤ 1.3 → compatible (Section 12.1) |
| D12 | input declares CLC-2.0 against a CLC-1.3 implementation | deny("unsupported_language_revision") | different major → fail-closed (Section 12.1) |
| D13 | operation carries a param key the grant does not declare (key closure) | deny("undeclared_param") | Section 6.2 key closure, Section 9.1 layer 7 request side |
| D14 | both a missing grant key and an undeclared request key | deny("params_missing") | layer-7 order: missing before undeclared |
| D15 | absent/empty grant and absent operation | deny("capability_not_authorized") | Section 9.1 pre-check resolves before any layer, incl. the absent-operation case |
| D16 | grant valid, operation has no id
|
deny("missing_capability_id") | layer 1 |
| D17 | grant carries time:window with a cross-midnight single segment (22:00→06:00) |
deny("invalid_constraint") | a single segment crossing midnight is out of grammar (decide-019) — a crossing must be split into two segments |
| D18 | grant carries network:cidr (array form, IPv4/IPv6) |
allow_unresolved, unresolved:[<constraint>]
|
recognized, no core evaluator → residual obligation (Section 8.4; decide-020) |
| D19 |
time:window scalar second form (window:3600) |
deny("invalid_constraint") | out of Section 8.1 value grammar (decide-021) |
| D20 |
network:cidr without prefix length |
deny("invalid_constraint") | out of Section 8.1 value grammar (decide-022) |
| D21 |
max_rows constraint, op carries no max_rows value |
deny("max_rows:violated") | fail-closed op-absent (decide-023) |
| D22 |
time:window split-form multi-segment window (22:00→00:00 + 00:00→06:00) |
allow_unresolved, unresolved:[<constraint>]
|
cross-midnight split-segment grammar (decide-024) |
| D23 | op scheme bad passes no vendor/product-vN |
deny("invalid_capability_id") | Section 3 scheme grammar (decide-025) |
| D24 | grant id bad:op against a valid operation |
deny("capability_not_authorized") | Section 3 fails in Entails; ID-level reason collapses (decide-026) |
| D25 |
max_rows exactly at the bound |
allow | violation is strict >; core-evaluated so no unresolved (decide-027) |
| D26 | grant params:{}, op any params → unconstrained |
allow |
{} ≡ absent (decide-028, Section 9.1) |
| D27 | multi-grant: G1 {limit:10} denies, G2 {limit:100} allows |
allow | any-one-covers authorizes (decide-029, Section 9.1) |
| D28 | multi-grant: G1 {limit:10} + G2 {limit:6}, op {limit:50}
|
deny("params_exceed_grant") | all covering grants reject → first reason in canonical order (decide-030, Section 9.1) |
| D29 | multi-grant allow_unresolved: G1 network:cidr + G2 time:window
|
allow_unresolved, unresolved:[both]
|
residual obligations union across covering grants (decide-035, Section 9.1/Section 8.4) |
| D30 | operation id uses a forbidden wildcard shape (*:query:SELECT) |
deny("unsupported_wildcard") | wildcard-shape detection precedes the base grammar, and layer 1 propagates the specific code rather than invalid_capability_id (decide-018, Section 3/Section 9.1) |
| D31 | grant bounds max_rows:10, operation carries max_rows:"garbage"
|
deny("max_rows:violated") | op-side value outside the Section 8.1 domain (finite non-negative integer) fails closed, never passes unchecked (decide-031, rev CLC-1.4) |
| D32 | grant bounds max_rows:10, operation carries max_rows:true
|
deny("max_rows:violated") | boolean is outside the Section 8.1 domain (decide-032, rev CLC-1.4) |
| D33 | grant bounds max_rows:10, operation carries max_rows:-1
|
deny("max_rows:violated") | a negative is outside the Section 8.1 domain (decide-033, rev CLC-1.4) |
| D34 | grant bounds max_rows:10, operation carries max_rows:1.5
|
deny("max_rows:violated") | a fraction is outside the Section 8.1 domain (decide-034, rev CLC-1.4) |
| # | Scenario | Expected | Derivation |
|---|---|---|---|
| C1 | wildcard grant + params within bounds | allow | E2 + P1 |
| C2 | wildcard grant + params exceed bounds | deny | E2 + P2 |
| C3 | intersection + constraint violation | deny | I4 + D6 |
| C4 | two-source intersection, both narrow | allow, narrowest | I1 + I4 |
| C5 | grant with empty constraint bound | deny | deny-when-declared |
| C6 | unknown scheme in grant | deny("unknown_constraint") | fail-closed |
| C7 | grant: std/database-v1:query:*, op: std/database-v1:query:SELECT, params mismatch |
deny | E2 + P4 |
| C8 | three-source intersection, one absent | deny | I3 |
| C9 | valid grant + valid constraint + valid op | allow | D1 |
| C10 | malformed id in operation | deny("invalid_capability_id") | D4 |
| C11 | delegation chain, intermediate hop declares empty bound | deny | deny-when-declared propagates |
Total: 123 vectors¶
Decisions D17–D28 are the corpus pin for the
residual-obligation channel unresolved / allow_unresolved, the Section 8.1
(scheme,type) identity, the invalid_constraint value grammar and the
Section 9.1 multi-grant aggregation. decide-025/-026 and syntax-007/-008
pin the Section 3 scheme grammar; decide-021/-022, decide-019/-024 pin Section 8.1
time/network value shapes; decide-019 pins the no-cross-midnight rule;
decide-028/-029/-030 pin Section 9.1 ({}≡absent, any-allow union, deterministic
deny reason); decide-035 (D29) pins multi-grant residual-obligation
union across covering grants; decide-031/-032/-033/-034 (D31–D34) pin
the Section 8.1 max_rows request-side value domain (rev CLC-1.4); decide-018
(D30) pins Section 3 wildcard-shape detection ahead of the base grammar.¶
The corpus additionally carries 6 scheme stress-test vectors
(clinical-001/-002, payments-001/-002, data-001/-002) exercised
against std/{clinical,payments,data}-v1: their enum/bound outcomes follow
the grouping rules above (→ B.3 params semantics), and payments-002
additionally exercises Section 8 fail-closed unknown_constraint for a
scheme-scoped constraint type. These vectors add no new normative rule;
they exist to record the v2 requirements evidence, not to extend v1.
undeclared-001/-002 (→ B.5 D13/D14) pin the Section 6.2 key-closure rule.
params-006/params-013 (→ B.3 P6/P13) close two previously-unmapped
rows; params-020/021 (→ P20/P21) are the positive boundary cases of
params-018/019; decide-016/017 (→ D15/D16) pin the Section 9.1 pre-check and
layer-1 paths for absent/empty grant and id-less operation;
intersect-007..010 (→ I7..I10) pin Section 7 rules 5–6 including empty-params
sources, order independence, and a params-free identifier comparison;
intersect-011/-012/-013 (rev CLC-1.15) are the cross-type audit pins of
Section 7 rule 6 (string × number merge refusal, type-sensitive enum-member
equality, 1.0 ≡ 1) and sit outside the B.1–B.6 tables, taking the
corpus total to 123. B.5 row labels Dn are semantic row numbers, not
corpus ids (D15/D16 ↔ decide-016/-017, D29 ↔ decide-035); D10
(absent/empty grant, operation present) has no dedicated vector — the
pre-check path is pinned by decide-016 (D15).¶
The delegation-containment relation (Section 13) is pinned by a separate corpus at
capability/data/_vectors/clc-d/: containment-vectors.json (64 vectors,
groups: identifier coverage, parameter narrowing, constraint
non-participation, validity, symmetry), containment-property-cases.json
(784 forward-closure cases over 39 shared operations) and
containment-crosswalk-vectors.json (44 cross-vendor vectors). It is not
re-tabulated here; the corpus README maintains the live count and the snapshot
date.¶
The Section 6.5 param_bounds grammar is pinned by
capability/data/_vectors/clc-v1/param-bounds-vectors.json (43 vectors,
groups: numeric interval/step, enum cardinality, optional keys, nested
recursion, the binding rule, malformed bounds, scheme defaults), with
param-bounds-vectors.schema.json. It is not re-tabulated here. These
vectors exercise a CLC-1.10 field; a CLC-1.9 or earlier implementation refuses
them through the Section 12.1 minor gate rather than ignoring the bounds. The JSON
type-sensitive enum equality of Section 6.5 layer 8 (rev CLC-1.15) is pinned by the
companion file param-bounds-equality-vectors.json in the same directory.¶
The Section 8.5 Resolve function is pinned by
capability/data/_vectors/clc-v1/resolve-vectors.json (26 vectors, groups:
terminal pass-through, all-satisfied → allow, partial → allow_unresolved,
violated → deny, the time:window core clock in and out of window, TTL
re-evaluation, bare assertion without now, conflict precedence and malformed
input), with resolve-vectors.schema.json. Vectors that exercise the core
clock carry now; a CLC-1.10 implementation that does not implement Resolve
is unaffected by this corpus.¶
The Section 7.1 derived ConstraintUnion projection is pinned by
capability/data/_vectors/clc-v1/constraint-union-vectors.json (12 vectors:
union over one/many grants, duplicate folding across sources, deterministic
ordering, empty constraints, and the empty-chain absent_source refusal),
with constraint-union-vectors.schema.json. The UTF-8 byte-order collation
pinned in rev CLC-1.15 (Section 7.1) — where UTF-16 code-unit order would diverge —
is exercised by the companion file constraint-union-collation-vectors.json
in the same directory.¶
The Section 6.6 intersection of param_bounds is pinned by
capability/data/_vectors/clc-v1/param-bounds-meet-vectors.json (groups:
numeric min/max/step meet including the coarser-grid and fail-closed
step cases, enum intersection and tightened cardinality, optional conjunction,
nested recursion, the empty meets (min>max, disjoint enums — empty
within one value family), the unrepresentable crosses (numeric∩enum in
either order and scalar∩nested, refused since rev CLC-1.15; cardinality-only
enum, incommensurable steps → invalid_params_binding), the
identity empty Bound, and the cross-site refusal), with
param-bounds-meet-vectors.schema.json. The rev CLC-1.15 meet invariant —
every successful meet authorizes only what every source authorizes — is
exercised by param-bounds-meet-property-cases.json in the same directory.¶
This appendix is informative. It puts a carrier's own vocabulary and its failure codes beside CLC-D's, so an adopter can explain a containment verdict in the carrier's terms — and can see exactly where the mapping is many-to-one and therefore not reversible without carrier context. The profiles named here are the pinned ones in Section 13.9.3; the profile obligations are Section 13.8.1.¶
| Carrier | Carrier term | CLC term used here | Note |
|---|---|---|---|
| CLC-D | identifier scheme:action:path
|
CapabilityId
|
the relation's left/right identity |
| CLC-D | parameter bound |
params
|
declared bound; absent or {} = unconstrained |
| CLC-D | constraint (varwof/constraint-v1:*) |
constraints
|
union axis, outside Contains (Section 13.4.4) |
| ATN |
resource_bounds, numeric conditions
|
params (upper bounds) |
Section 9.2 takes the per-dimension min
|
| ATN |
id + schema.digest
|
identifier segments | digest mismatch ⇒ distinct capability (ccx-043) |
| ATN |
preconditions
|
— (union axis) | never compared by Contains
|
| AAT |
tools(derived) ⊆ tools(parent)
|
identifier coverage | |
| AAT | argument constraints (exact/range/one_of/…) |
params
|
AAT's own contains/subset are constraint types, not the relation |
| AIP | scope, budget, expiry, domains | identifier + params
|
absent ⇒ nearest ancestor, resolved by the profile |
| AAE |
mandate.actions
|
identifier coverage | |
| AAE | CONSTRAINTS value (unwrapped) |
params
|
numeric ≤, allowlist ⊆ |
| AAE |
validity
|
— (carrier/time, CLC-A R1) | not a declared Contains input |
| AOA | operation scope string | identifier path |
* maps to a CLC trailing wildcard |
| AEGIS | dotted capability
|
identifier path | |
| AEGIS | numeric context
|
params
|
|
| AEGIS |
allowed_roles, environment, risk_level, scope
|
— (not mapped) | identity/policy dimensions with no CLC field; the profile leaves them to the carrier (C.3) |
Carrier failure codes collapse onto CLC-D's three codes. The mapping is many-to-one and is therefore not reversible without the carrier recording which of its own codes applied before mapping.¶
| Carrier | Carrier code / failure | CLC-D reason |
|---|---|---|
| ATN Section 9.1 identity rule | different id
|
different_namespace
|
| ATN Section 9.1 identity rule | different schema.digest
|
child_exceeds_parent
|
| ATN Section 9.2 | raised resource_bounds
|
params_not_narrower
|
| AAT I4 | tool outside parent set |
different_namespace
|
| AAT I4 | constraint not subsumed |
params_not_narrower
|
| AIP Section 4.4 |
aip_scope_insufficient
|
child_exceeds_parent
|
| AIP Section 4.4 |
aip_budget_exceeded
|
params_not_narrower
|
| AIP Section 4.4 |
aip_depth_exceeded
|
child_exceeds_parent
|
| AAE Section 3 | action not a subset |
different_namespace
|
| AAE Section 3/Section 7.4 | constraint not more restrictive |
params_not_narrower
|
| AOA Section 6.2 | scope not strictly narrower |
child_exceeds_parent
|
| AEGIS AIAM1-DEL-010 | authority exceeds the delegator's |
params_not_narrower
|
| AEGIS AIAM1-DEL-011 | capability the delegator lacks |
different_namespace
|
The collapse is visible in the right column: child_exceeds_parent is reached by
both AIP scope and AIP depth failures and by an ATN digest mismatch, and
params_not_narrower by every carrier's bound-widening. A carrier that needs
its own code back must keep it alongside the CLC-D reason; CLC-D guarantees the
stable language-level reason, not the carrier's.¶
Two of the Section 13.8.1 obligations are visible in the pinned vectors:¶
Identity dimensions the language cannot carry fail closed. ATN's schema
digest is carried as an identifier segment (ccx-043) rather than dropped, so a
digest mismatch is child_exceeds_parent instead of a silent match.¶
Semantics the relation cannot see are a profile limitation, not a CLC gap.
AEGIS's allowed_roles/environment/risk_level and AAE's validity have no
CLC field; the profile leaves them to the carrier and asserts nothing about
them. They are not modelled here, and a profile MUST NOT claim
containment of a dimension the relation cannot see.¶
Iman Schrock (EMILIA Protocol) reviewed the intersection and constraint
semantics against the revision 1.1 corpus and supplied the adversarial cases
that revisions 1.2 and 1.3 fix: nested partial overlap in intersection,
constraint value handling for max_rows, and the public entry-point contract.
For revision 1.4 he re-ran the Go, Python and TypeScript implementations and the
1,184 property cases, and closed the two objections he had raised against the
max_rows value domain and the UTF-8 size bound. For revision 1.5 he re-ran the
three Go suites — 105 authorization, 30 evidence and 13 crosswalk cases — and
supplied the four corrections that revision carries: the collapse from
three-valued evaluation to the binary report (Section 10), the separation of
allow_unresolved from evidence (Section 11), the scope of delegation
(Section 12), and the boundary between this projection identity and CAID
(Sections 4.2, 4.3 and 6.4). For revisions 1.6 to 1.8 he re-ran the three
implementations and reported the raw-boundary cases those revisions fix: the
HTML-escaping and key-order defects in the canonical serializer, the decoded
paths that disagreed with the raw path on malformed Unicode, and the raw size
checks that counted a number by its received spelling and a literal astral
character by UTF-16 code unit. Revision 1.9 folds the delegation-containment
relation and the CLC-D class into this document (Section 13, Appendix C),
retiring the formerly separate containment extension, and pins it against six
adjacent capability drafts (ATN, AAT, AIP, AAE, AOA, AEGIS) reviewed on
2026-09-21.¶