Parent: #77
`mintCapabilityToken`/`verifyCapabilityToken` (`ts/packages/core/src/domain/tokens.ts:337-396`) hardcode each narrowing check as its own `if`: bearer match, expiry-within-parent, `scopeNarrows`, capability equality, `delegations-remaining` strictly-less-than-parent's. Anything not covered by those five falls back to the claims payload's open `* tstr => any` tail, interpreted however the reading domain happens to define it -- a second, informal mechanism sitting beside the first.
Fold both into one: `token-claims` gets a `conditions: [* PredicateNode]` field, evaluated by a single `evaluatePredicate(node, resolver)` function. The current hardcoded checks become a mandated core vocabulary of predicate ops (`bearer-is`, `scope-narrows`, `expires-at`, `depth-remaining`, `capability-is`) every conforming verifier must support, sitting in the exact same list as any domain-specific condition (a message TTL, a co-presented-credential check, the `authorization` field #84 sketches for delegated revoke). One evaluator replaces five separate hardcoded checks plus whatever ad hoc reading of the extension tail a given domain invents.
trilean (ExaDev-owned, Zod-based) is the right building block: its three-valued result (`{status: "definite", value}` or `{status: "indeterminate", reason}`) maps directly onto the verifier-obligations glossary's own existing "can't resolve it, refuse it" convention -- `indeterminate` fails the token, the same as an unrecognised `kind`/`type` discriminator already does elsewhere in the spec. Its resolver pattern (`resolveValue`/`resolveLookup`/`resolveCollection`) is the exact injection point for wire-mesh's own local evaluation context (`Clock`, `RevocationView`, the presented token's own claims) -- nothing to invent, just wiring existing ports into an already-typed contract.
Deliberately kept flat/AND-only (every condition in the list must hold) -- not full boolean composition with OR/NOT. Every condition examined so far (expiry, delegation depth, a proposed TTL, the proposed revoke-authorization) has been one independent threshold; a flat list already covers that completely. Full recursive composition is a separate, harder problem (compound-expression gaming has its own attack surface) that nothing here actually needs yet -- the same restraint already applied to causal-reference composition in the design's own section C, and independent of any refactor-cost argument.
No existing tokens or external consumers to migrate -- this can be the schema from the start rather than a breaking change layered on afterward.
Update: checked directly against trilean's actual source (not assumed) rather than treating the fit as speculative. Four of five integration questions raised during design turned out already solved:
- Custom domain predicates (
bearer-is, scope-narrows, depth-remaining, capability-is, the future revoke-authorization check) are already supported natively via the delegate node ({kind: "delegate", system: string, payload: JsonValue}, evaluator.ts:1425-1441) -- a consumer-supplied resolvers.resolveDelegate(system, payload, context) handles each named system. No trilean change needed; wire-mesh just registers its five ops this way.
- Already async.
resolveDelegate (and the evaluator generally) is awaited throughout -- wire-mesh's own RevocationCheck.isRevoked(): Promise<boolean> composes directly, no synchronous-context workaround needed.
- Already fails closed, not throws, for an unhandled case. A
delegate node naming a system with no registered handler returns indeterminate("wrong-type", ...); malformed treeReference resolutions go through .safeParse() rather than a throwing .parse(). Matches wire-mesh's own verifier-obligation convention with no wrapper needed for these cases.
- Serialization is a wire-mesh-side concern, not a trilean one, and a straightforward one.
PredicateNode/ExpressionNode (including delegate's own payload) are plain JSON-value-shaped Zod schemas, no embedded functions or closures -- canonical CBOR encoding for embedding inside a signed COSE_Sign1 payload is ordinary downstream work, not something trilean needs to change to support.
One real gap confirmed and filed upstream: ExaDev/trilean#43 -- MAX_TREE_REFERENCE_DEPTH only bounds a chain of treeReference hops, not the nesting depth or node count of a single self-contained tree, which matters here because wire-mesh's own predicate trees arrive embedded in tokens minted by not-fully-trusted peers (every other trilean consumer examined assumes a trusted tree author). Track that issue as a soft prerequisite -- usable without it, but worth having before evaluating genuinely adversarial input in production.
Parent: #77
`mintCapabilityToken`/`verifyCapabilityToken` (`ts/packages/core/src/domain/tokens.ts:337-396`) hardcode each narrowing check as its own `if`: bearer match, expiry-within-parent, `scopeNarrows`, capability equality, `delegations-remaining` strictly-less-than-parent's. Anything not covered by those five falls back to the claims payload's open `* tstr => any` tail, interpreted however the reading domain happens to define it -- a second, informal mechanism sitting beside the first.
Fold both into one: `token-claims` gets a `conditions: [* PredicateNode]` field, evaluated by a single `evaluatePredicate(node, resolver)` function. The current hardcoded checks become a mandated core vocabulary of predicate ops (`bearer-is`, `scope-narrows`, `expires-at`, `depth-remaining`, `capability-is`) every conforming verifier must support, sitting in the exact same list as any domain-specific condition (a message TTL, a co-presented-credential check, the `authorization` field #84 sketches for delegated revoke). One evaluator replaces five separate hardcoded checks plus whatever ad hoc reading of the extension tail a given domain invents.
trilean (ExaDev-owned, Zod-based) is the right building block: its three-valued result (`{status: "definite", value}` or `{status: "indeterminate", reason}`) maps directly onto the verifier-obligations glossary's own existing "can't resolve it, refuse it" convention -- `indeterminate` fails the token, the same as an unrecognised `kind`/`type` discriminator already does elsewhere in the spec. Its resolver pattern (`resolveValue`/`resolveLookup`/`resolveCollection`) is the exact injection point for wire-mesh's own local evaluation context (`Clock`, `RevocationView`, the presented token's own claims) -- nothing to invent, just wiring existing ports into an already-typed contract.
Deliberately kept flat/AND-only (every condition in the list must hold) -- not full boolean composition with OR/NOT. Every condition examined so far (expiry, delegation depth, a proposed TTL, the proposed revoke-authorization) has been one independent threshold; a flat list already covers that completely. Full recursive composition is a separate, harder problem (compound-expression gaming has its own attack surface) that nothing here actually needs yet -- the same restraint already applied to causal-reference composition in the design's own section C, and independent of any refactor-cost argument.
No existing tokens or external consumers to migrate -- this can be the schema from the start rather than a breaking change layered on afterward.
Update: checked directly against trilean's actual source (not assumed) rather than treating the fit as speculative. Four of five integration questions raised during design turned out already solved:
bearer-is,scope-narrows,depth-remaining,capability-is, the futurerevoke-authorization check) are already supported natively via thedelegatenode ({kind: "delegate", system: string, payload: JsonValue},evaluator.ts:1425-1441) -- a consumer-suppliedresolvers.resolveDelegate(system, payload, context)handles each named system. No trilean change needed; wire-mesh just registers its five ops this way.resolveDelegate(and the evaluator generally) isawaited throughout -- wire-mesh's ownRevocationCheck.isRevoked(): Promise<boolean>composes directly, no synchronous-context workaround needed.delegatenode naming a system with no registered handler returnsindeterminate("wrong-type", ...); malformedtreeReferenceresolutions go through.safeParse()rather than a throwing.parse(). Matches wire-mesh's own verifier-obligation convention with no wrapper needed for these cases.PredicateNode/ExpressionNode(includingdelegate's ownpayload) are plain JSON-value-shaped Zod schemas, no embedded functions or closures -- canonical CBOR encoding for embedding inside a signed COSE_Sign1 payload is ordinary downstream work, not something trilean needs to change to support.One real gap confirmed and filed upstream: ExaDev/trilean#43 --
MAX_TREE_REFERENCE_DEPTHonly bounds a chain oftreeReferencehops, not the nesting depth or node count of a single self-contained tree, which matters here because wire-mesh's own predicate trees arrive embedded in tokens minted by not-fully-trusted peers (every other trilean consumer examined assumes a trusted tree author). Track that issue as a soft prerequisite -- usable without it, but worth having before evaluating genuinely adversarial input in production.