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 eight 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 |
requested_resource_id | string | resource_path |
requested_path | string | resource_path when path_prefix is present |
In Go and Rust, presence is signaled by boolean flags (HasLocation, HasSpeed,
HasAmount, HasResource); 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, define a reverse-domain name
under a domain you control and register a ConstraintEvaluator in
VerifyOptions.ConstraintEvaluators:
opts := ratify.VerifyOptions{ RequiredScope: "internal:do_thing", ConstraintEvaluators: map[string]ratify.ConstraintEvaluator{ "com.example.org_membership": myOrgMembershipEvaluator, },}The eight built-in types 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).
An extension constraint can carry a params object containing null, booleans, strings, safe
integers, arrays, and objects under the restricted model in SPEC §5.7.1. Both the type and
parameters are covered by the principal’s signature. Every downstream verifier must register
the same exact type name before it can evaluate the constraint.
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