Skip to content

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 rangeconstraint_denied.
  • If the runtime context required to decide is not suppliedconstraint_unverifiable.
  • If the constraint’s type is not recognized by this SDK build → constraint_unknown.

In other words: the verifier never falls back to “allow because I’m not sure.”

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.

typeWhat it boundsRequired VerifierContext
geo_circlePosition inside a haversine-radius circlecurrent_lat, current_lon
geo_polygonPosition inside a polygon (≥3 vertices)current_lat, current_lon
geo_bboxPosition inside a lat/lon (+ optional alt) boxcurrent_lat, current_lon (+ current_alt_m if set)
time_windowLocal wall-clock time in [start, end](none — verifier clock is enough)
max_speed_mpsCurrent velocity ≤ max_mpscurrent_speed_mps
max_amountRequested amount ≤ max_amount in currencyrequested_amount, requested_currency
max_ratecount invocations per rolling window_srate-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.

{
"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.

{
"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).

{
"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.”

{
"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 decides
result = verify_bundle(
bundle,
VerifyOptions(required_scope="physical:move"),
)

Times are validated as HH:MM 24-hour clock. Malformed values fail closed.

{
"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.

{
"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.

{
"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.

The full set of fields the eight built-in constraints can read:

FieldTypeRead by
current_lat, current_lonfloat64geo_circle, geo_polygon, geo_bbox
current_alt_mfloat64geo_bbox (when altitude bounds set)
current_speed_mpsfloat64max_speed_mps
requested_amountfloat64max_amount
requested_currencystring (ISO 4217)max_amount
invocations_in_window(cert_id, window_s) -> intcallbackmax_rate
requested_resource_idstringresource_path
requested_pathstringresource_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.

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.

Outcomeidentity_statuserror_reason prefix
All constraints satisfiedauthorized_agent(empty)
A value is out of rangeconstraint_deniedconstraint[i] (geo_circle): outside allowed radius: 612.3m > 500.0m
A required input is missingconstraint_unverifiableconstraint[i] (max_speed_mps): no current speed in context
The type is unknown to this SDKconstraint_unknownconstraint[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.

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.