Constraints
Scopes name what an agent is allowed to do. A constraint rides alongside a scope and
names when, where, or how much — geofence, time-of-day, speed cap, amount cap, rate cap.
The verifier evaluates every constraint against the runtime VerifierContext you supply.
Fail-closed by design:
- If the constraint’s value is outside the allowed range →
constraint_denied. - If the runtime context required to decide is not supplied →
constraint_unverifiable. - If the constraint’s
typeis not recognized by this SDK build →constraint_unknown.
In other words: the verifier never falls back to “allow because I’m not sure.”
The v1 constraint vocabulary
Section titled “The v1 constraint vocabulary”Seven first-class types are built into every SDK. They are byte-for-byte identical across Go, TypeScript, Python, and Rust — the canonical wire-format fixtures cover all seven.
type | What it bounds | Required VerifierContext |
|---|---|---|
geo_circle | Position inside a haversine-radius circle | current_lat, current_lon |
geo_polygon | Position inside a polygon (≥3 vertices) | current_lat, current_lon |
geo_bbox | Position inside a lat/lon (+ optional alt) box | current_lat, current_lon (+ current_alt_m if set) |
time_window | Local wall-clock time in [start, end] | (none — verifier clock is enough) |
max_speed_mps | Current velocity ≤ max_mps | current_speed_mps |
max_amount | Requested amount ≤ max_amount in currency | requested_amount, requested_currency |
max_rate | ≤ count invocations per rolling window_s | rate-counter callback |
Unknown types are rejected (constraint_unknown). New types require either a SPEC bump or an
extension ConstraintEvaluator (SPEC §17.7) — see Provider architecture.
Geofence: geo_circle
Section titled “Geofence: geo_circle”{ "type": "geo_circle", "lat": 37.7749, "lon": -122.4194, "radius_m": 500}Haversine distance on WGS-84. Valid only when the agent is within radius_m meters of
(lat, lon).
ctx := ratify.VerifierContext{ HasLocation: true, CurrentLat: 37.7751, // ~30 m from center CurrentLon: -122.4190,}result := ratify.Verify(bundle, ratify.VerifyOptions{ RequiredScope: "drone:deliver", Context: ctx,})If HasLocation == false the result is constraint_unverifiable — not denied, unverifiable.
Geofence: geo_polygon
Section titled “Geofence: geo_polygon”{ "type": "geo_polygon", "points": [ [37.7755, -122.4200], [37.7755, -122.4180], [37.7745, -122.4180], [37.7745, -122.4200] ]}points is a list of [lat, lon] pairs. At least 3 vertices required; winding order is
irrelevant. Inclusion is tested with ray-casting in equirectangular projection.
v1 limitation (fail-closed): polygons whose longitude span exceeds 180° (i.e., that cross
the anti-meridian) are rejected with constraint_denied rather than silently returning a wrong
answer. For very large or anti-meridian-crossing regions, model the boundary with a sequence of
sub-polygons or use a ConstraintEvaluator extension (geodesic semantics are planned for v2).
Geofence: geo_bbox
Section titled “Geofence: geo_bbox”{ "type": "geo_bbox", "min_lat": 37.7700, "max_lat": 37.7800, "min_lon": -122.4250, "max_lon": -122.4150, "min_alt_m": 0, "max_alt_m": 120}Rectangular bounding box. Altitude bounds are only enforced if either min_alt_m or
max_alt_m is non-zero — set both to zero to ignore altitude entirely.
geo_bbox is the only geo type with anti-meridian semantics built in: if min_lon > max_lon,
the box is interpreted as wrapping the 180° meridian. For example,
{"min_lon": 170, "max_lon": -170} means “from 170°E eastward across the date line to 170°W.”
Time of day: time_window
Section titled “Time of day: time_window”{ "type": "time_window", "tz": "America/Los_Angeles", "start": "06:00", "end": "22:00"}Local wall-clock window in the IANA-named timezone tz. Inclusive at both ends. No
VerifierContext input is required — the verifier uses its own clock.
Wrapping is supported: start: "22:00", end: "06:00" means “10 PM through 6 AM the next day.”
# Cert says "weekdays 6am–10pm Pacific"; the verifier's own clock decidesresult = verify_bundle( bundle, VerifyOptions(required_scope="physical:move"),)Times are validated as HH:MM 24-hour clock. Malformed values fail closed.
Velocity cap: max_speed_mps
Section titled “Velocity cap: max_speed_mps”{ "type": "max_speed_mps", "max_mps": 5.0}The agent’s current velocity must be ≤ max_mps meters per second (SI units — no mph
hidden conversions).
let mut ctx = VerifierContext::default();ctx.current_speed_mps = Some(3.2); // ~7 mph
let result = verify_bundle(&bundle, &VerifyOptions { required_scope: "robot:operate".into(), context: ctx, ..VerifyOptions::default()});Without current_speed_mps, the result is constraint_unverifiable.
Transaction amount: max_amount
Section titled “Transaction amount: max_amount”{ "type": "max_amount", "max_amount": 500.0, "currency": "USD"}The requested amount must be ≤ max_amount and the currency must match exactly (ISO 4217
codes; case-sensitive on the wire). A USD constraint will deny a EUR request with
constraint_denied: currency mismatch.
const result = await verifyBundle(bundle, { required_scope: "payment:execute", context: { requested_amount: 75.0, requested_currency: "USD", },});The protocol does no FX conversion. If you grant max_amount: 500 USD and the agent presents
a EUR payment, the cert is denied — even if 75 EUR < 500 USD by today’s rate. This is
intentional: FX feeds are out of scope for an authorization protocol.
Rate cap: max_rate
Section titled “Rate cap: max_rate”{ "type": "max_rate", "count": 10, "window_s": 3600}At most count exercises of this specific cert (matched by cert_id) within a rolling
window_s seconds. Both must be positive integers — malformed values fail closed.
max_rate requires you to supply a callback that answers “how many times has this cert
been used in the last N seconds?”:
ctx := ratify.VerifierContext{ InvocationsInWindow: func(certID string, windowS int64) int { return myRateLimiter.Count(certID, windowS) },}This is the one constraint that needs persistent state across verifications — the rest are
pure functions of (constraint, context, now). Most implementations back the counter with
Redis or a database keyed on (cert_id, window_bucket).
Without the callback, the result is constraint_unverifiable.
VerifierContext reference
Section titled “VerifierContext reference”The full set of fields the seven built-in constraints can read:
| Field | Type | Read by |
|---|---|---|
current_lat, current_lon | float64 | geo_circle, geo_polygon, geo_bbox |
current_alt_m | float64 | geo_bbox (when altitude bounds set) |
current_speed_mps | float64 | max_speed_mps |
requested_amount | float64 | max_amount |
requested_currency | string (ISO 4217) | max_amount |
invocations_in_window(cert_id, window_s) -> int | callback | max_rate |
In Go and Rust, presence is signaled by boolean flags (HasLocation, HasSpeed,
HasAmount); in TypeScript and Python, by leaving the field undefined / None. The
canonical context hash (used inside PolicyVerdict, SPEC §17.6) treats “flag unset” and
“field is None” identically across SDKs — so a verdict signed by a Go verifier is consumable
by a Rust verifier on the exact same context.
Chained constraints are additive
Section titled “Chained constraints are additive”A sub-delegation chain compounds constraints — every cert’s constraints must pass.
Alice → Agent-A constraint: geo_bbox (warehouse footprint)Agent-A → Agent-B constraint: time_window (06:00–08:00 PT)
Agent-B's bundle is valid iff: ✓ inside the warehouse (Alice's constraint) ✓ at 6 AM–8 AM PT (Agent-A's constraint)Constraints never weaken down the chain. If you want a wider envelope downstream, the upstream cert has to grant it explicitly.
What the verifier returns
Section titled “What the verifier returns”| Outcome | identity_status | error_reason prefix |
|---|---|---|
| All constraints satisfied | authorized_agent | (empty) |
| A value is out of range | constraint_denied | constraint[i] (geo_circle): outside allowed radius: 612.3m > 500.0m |
| A required input is missing | constraint_unverifiable | constraint[i] (max_speed_mps): no current speed in context |
The type is unknown to this SDK | constraint_unknown | constraint[i] (foo): constraint_unknown: unknown constraint type "foo" |
error_reason always carries the cert chain index and the constraint type, so logs are
machine-parsable.
Extension constraint types (SPEC §17.7)
Section titled “Extension constraint types (SPEC §17.7)”If you need a constraint vocabulary the v1 set doesn’t cover (e.g., org_membership,
device_attestation), register a ConstraintEvaluator in VerifyOptions.ConstraintEvaluators:
opts := ratify.VerifyOptions{ RequiredScope: "internal:do_thing", ConstraintEvaluators: map[string]ratify.ConstraintEvaluator{ "org_membership": myOrgMembershipEvaluator, },}The built-in seven always win — registering a geo_circle evaluator is a no-op. Unknown
types are routed to your registry; if no entry matches, the verifier returns
constraint_unknown (fail-closed).
See Provider architecture for the full evaluator interface and lifecycle.
Where to next
Section titled “Where to next”- Scopes — the verbs that constraints further restrict
- Delegate → Present → Verify — where constraints sit in the verification pipeline
- Provider architecture —
ConstraintEvaluatorand the other §17 hooks - Physical AI guide — geofences and speed caps in production