Scope grammar
A scope is a compact declarative statement of the form:
<product>:<verb>(<constraint-list>)
Scopes are the protocol's contract for "what may this agent do, and on what terms." They are parseable, comparable, and composable across OrangeCheck sub-products.
Shape
scope := product ":" verb [ "(" constraint-list ")" ]
product := lowercase-ident
verb := lowercase-ident
constraint-list := constraint ( "," constraint )*
constraint := key op value
key := lowercase-ident
op := "=" | "!=" | "<" | "<=" | ">" | ">=" | "*"
value := quoted-string | bare-token
No whitespace is permitted inside a scope string. * is the wildcard op and takes no value.
MVP scope registry
Implementations MUST accept these products and verbs.
| Scope | Registered keys |
|---|---|
lock:seal | recipient, mime, max_bytes |
lock:chat | recipient, max_bytes_per_msg, max_msgs |
stamp:sign | mime, max_bytes, content_hash_prefix |
vote:cast | poll_id, choice |
nostr:publish | kind, relay, max_bytes |
http:request | origin, method, max_rps, max_bytes_out |
ln:send | max_sats, node, max_fee_sats |
mcp:invoke | server, tool, max_invocations |
In strict mode (default), verifiers reject unknown products / verbs / constraint keys. In permissive mode, verifiers accept unknowns but never treat them as wider — an unknown key is ignored as a constraint, not inflated to a wildcard.
Canonical form
To make sub-scope comparison deterministic, scopes are canonicalized before use:
- Constraints sorted lexicographically by key.
- Bare tokens lowercased unless the registered key is case-sensitive.
- No whitespace.
ln:send(node=03abc,max_sats<=1000) canonicalizes to ln:send(max_sats<=1000,node=03abc). The canonical form is what gets written to scopes and scope_exercised.
Sub-scope containment
An exercised scope S_x is a sub-scope of a granted scope S_g iff:
productandverbare identical.- For every constraint
(k, op, v)inS_g:op = *— no requirement onS_x.op = =—S_xmust contain(k, =, v)exactly.op = !=—S_xmust contain a value that isn'tv(via=or another!=).- Ordered ops (
<,<=,>,>=) —S_x's implied numeric range must be a subset ofS_g's.
S_xmay add constraints on keys not present inS_g, as long as they're registered for thatproduct:verb.
Examples
| Granted | Exercised | Result |
|---|---|---|
lock:seal(recipient=bc1qalice) | lock:seal(recipient=bc1qalice) | ✓ exact match |
ln:send(max_sats<=1000) | ln:send(max_sats=500,node=03abc) | ✓ tighter bound + extra key |
stamp:sign(mime=text/markdown) | stamp:sign(mime=application/pdf) | ✗ = value mismatch |
http:request(origin=https://api.example.com) | http:request(origin=https://api.evil.com) | ✗ = value mismatch |
http:request(method!=POST) | http:request(method=GET) | ✓ admissible under != |
http:request(method!=POST) | http:request(method=POST) | ✗ disallowed under != |
ln:send(max_sats<=1000) | ln:send(max_sats=5000) | ✗ wider range |
http:request(origin=*) | http:request(origin=https://anything) | ✓ wildcard grants everything |
Wildcards
A delegation MAY grant product:verb(key=*) to express "any value for key." Wildcards widen — use them deliberately. An unbonded delegation with http:request(*) (no constraints, blanket verb) is maximally permissive; verifiers MAY reject such delegations by policy.
Adding new scopes
New products, verbs, and constraint keys are allocated by PR to the oc-agent-protocol repository. Vendor-specific experimental values SHOULD use an x-vendor/ prefix until standardized.
Implementing the sub-scope check
The reference TypeScript implementation lives in @orangecheck/agent-core:
import { isSubScope, parseScope } from '@orangecheck/agent-core';
const granted = parseScope('ln:send(max_sats<=1000,node=03abc)');
const exercised = parseScope('ln:send(max_sats=500,node=03abc,max_fee_sats=5)');
if (!isSubScope(exercised, granted)) {
throw new Error('scope escalation attempt');
}
The algorithm is pure and deterministic — same inputs on any compliant implementation yield the same accept/reject.