08 ConstraintsEN
08 · Constraints, residual obligations, and Resolve
Normative source:
capability-language-core-v1.md§8 This page is a readable guide. In case of conflict, the normative specification wins. Status: Preview — not for production use.
TL;DR
Constraints limit how a capability may be used. v1 core recognizes exactly three (scheme, type) pairs; only max_rows has a core evaluator. A recognized-but-unevaluated constraint is carried on the verdict as allow_unresolved + unresolved — never silently dropped. Resolve closes the consumer's feedback loop per obligation.
1. Constraint identity: (scheme, type)
Constraints use colon-notation triples scheme:type[:params]; the identity is the pair (scheme, type), never the type name alone (§8.1).
varwof/constraint-v1 : max_rows (core evaluator exists)
varwof/constraint-v1 : time (validated, not evaluated)
varwof/constraint-v1 : network (validated, not evaluated)
std/database-v1:max_rowsis not recognized — scheme-frame matters (§8.1). A non-recognized pair →deny("unknown_constraint"), fail-closed (decide-003unknown:constraint:type;payments-002a scheme-scopedpayments:quota:dailywhich is out of the v1 known set).- A recognized pair must conform to that type's value grammar; non-conforming →
deny("invalid_constraint").
2. Value grammars (§8.1)
| type | legal value (v1) | core behavior |
|---|---|---|
max_rows |
strict non-negative integer | evaluated against op params |
time:window |
JSON array (≤ 32), each element {"start":"HH:MM[:SS]","end":"HH:MM[:SS]"}, UTC segments; segments ascending, non-overlapping, half-open [start, end), no single segment crossing midnight |
recognized only |
network:cidr |
JSON array (≤ 32) of legal IPv4/IPv6 CIDR strings | recognized only |
Grammar traps (each pinned by a vector):
- Scalar time:window:3600 is not a legal time:window value → invalid_constraint (decide-021).
- CIDR without a prefix length → invalid_constraint (decide-022).
- A single segment crossing midnight (22:00→06:00) must be split into 22:00→00:00 + 00:00→06:00 (decide-019 → invalid_constraint). The end value "00:00" is read as 86400 (next-day midnight): 22:00→00:00 is legal because comparison is on seconds-of-day, not lexical string order (§8.1).
Operation-side max_rows domain (rev CLC-1.4): absent, string, boolean, negative, fractional or non-finite → max_rows:violated (cannot-be-shown-conforming = fail-closed). Pinned by decide-023 (absent), decide-031 ("garbage"), decide-032 (true), decide-033 (-1), decide-034 (1.5), all → max_rows:violated.
Core recognizes by pair, evaluates max_rows only. Evaluation of time/network is the declaring scheme's responsibility (§11); core's non-recognition of other pairs is fail-closed, killing cross-scheme pollution: a scheme that defines its own max_rows cannot be hijacked by Core's evaluator (§8.1).
3. The residual-obligation channel (§8.4)
A recognized constraint whose value conforms to its §8.1 grammar but for which v1 core defines no evaluator (time, network) MUST NOT be dropped: it appears on the decision's additive unresolved list, and the verdict is allow_unresolved — an independent enum value, never equal to allow (decide-020 network:cidr; decide-024 split cross-midnight window, both recognized-only).
Fail-closed boundary: a consumer (PEP / profile / declaring scheme) MUST evaluate or confirm every unresolved constraint before allowing; if it cannot, it MUST deny. A consumer that only checks if verdict == "allow" cannot, on the literal enum, release a residual as an already-satisfied allow (§8.4).
unresolvedordering (§8.4, rev CLC-1.15): normalized strings, duplicates folded, ordered by UTF-8 byte sequence — the §7.1 collation, not the ECMAScript default (UTF-16 code-unit) order. Join once and sort exactly once before emitting.- Combined obligations (consumer side): multiple
unresolvedof the same(scheme,type)form a conjunction (AND) — satisfying A and B satisfies all; OR / any-one / first-wins / ignoring some are forbidden. Different(scheme,type)do not interact (§8.4).
4. Resolve — the consumer's feedback loop (§8.5)
Resolution = { constraint: string, status: "satisfied" | "violated" | "unknown" }
Resolve(decision, resolutions, now?) → Decision
In order: (1) terminal verdicts fixed — deny/allow return unchanged, resolutions ignored; (2) malformed input fails closed — bad status/constraint → invalid_resolution, bad now → invalid_timestamp; (3) discharge — for each obligation combine core clock + consumer resolutions under violated ≻ satisfied ≻ unknown; (4) repeated entries combine the same way; (5) unrelated resolutions are ignored (O is authoritative); (6) any violated → deny({type}:violated); all satisfied → allow; else allow_unresolved with the still-unknown subset.
Core clock: when now is supplied, a varwof/constraint-v1:time obligation whose value is a §8.1 window array is evaluated by the core: inside a segment → satisfied, outside → time:violated (resolve-015: now=21:00 outside [22:00,24:00); malformed now refused in resolve-024). Pinned elsewhere: partial satisfaction → allow_unresolved with the remainder (resolve-006); any violated → deny with the offending {type}:violated (resolve-009); a resolution outside O is ignored (resolve-013); unrecognized status → invalid_resolution (resolve-023).
No caching beyond the horizon: a core-clock discharge is valid for the instant of the call. A consumer acting on a Resolve result must either re-invoke with current now immediately before acting or not cache past the current segment's end — a cached allow must not outlive its window (§8.5).
Resolve is deterministic, fail-closed, idempotent (Resolve(Resolve(d,r,now),r,now) = Resolve(d,r,now)) and monotone. It neither invents nor drops obligations.
Prevent-mixup checklist
- [ ] Identity is
(scheme,type):foo/database-v1:max_rowsisunknown_constraint, never Core's evaluator. - [ ] Recognized-but-unevaluated ⇒
allow_unresolved+unresolved—allow_unresolvedis notallow. - [ ] Grammar refusals are
invalid_constraint(value out of grammar) — distinct fromunknown_constraint(pair not recognized). - [ ]
time:windowcrossing midnight is split into two same-day segments;"00:00"inend= 86400. - [ ]
Resolvenever revives adeny, never widens via an unrelated resolution, and resolvesnow-starvedtimeobligations tounknown(no TTL).
Common pitfalls
- Silently skipping an unevaluated constraint — conformance bug; residual obligations are additive (§8.4).
- Trusting
time/networkas evaluated — core recognizes the value grammar only; evaluation belongs to the declaring scheme. - Caching a
Resolveallow past the segment end — outlives its discharge horizon (§8.5).
← 07-parameters.md · → 09-intersection.md · related: 10-decisions.md, 11-reason-codes.md