Constraints
Scopes name what an agent is allowed to do. Constraints describe where, when, and on which version it’s allowed to do it. The two are checked separately by the verifier — both must pass for a bundle to be considered valid.
┌─────────────────────────────────────┐ │ DelegationCert │ │ │ │ scope: [physical:move] │ ← what │ │ │ constraints: [ │ │ { type: geo_circle, │ ← where │ lat: 37.77, lon: -122.41, │ │ radius_m: 500 }, │ │ │ │ { type: temporal, │ ← when │ valid_hours: [6, 22], │ │ days: [1,2,3,4,5] }, │ │ │ │ { type: version, │ ← which version │ min: "1.2.0" } │ │ ] │ └─────────────────────────────────────┘Three constraint families in v1
Section titled “Three constraint families in v1”| Family | Type values | Purpose |
|---|---|---|
| Geographic | geo_circle, geo_polygon | Agent can only act inside a region |
| Temporal | temporal | Agent can only act during specified hours / days |
| Version | version | Agent must be running a specific version range |
The constraint families are intentionally narrow. They cover the three things every adapter
asked for in v1 design discussions; everything else fits in custom: scopes or application-level
policy.
Geo constraints
Section titled “Geo constraints”{ "type": "geo_circle", "lat": 37.7749, "lon": -122.4194, "radius_m": 500}The agent must be inside a 500-meter circle around (37.7749, -122.4194). The verifier needs to know the agent’s claimed location at verification time:
const result = await verifyBundle(bundle, { required_scope: "physical:move", context: { location: { lat: 37.7751, lon: -122.4190 }, // 30m from center → ✓ },});Or polygon for non-circular regions (e.g. a warehouse footprint):
{ "type": "geo_polygon", "points": [ { "lat": 37.7755, "lon": -122.4200 }, { "lat": 37.7755, "lon": -122.4180 }, { "lat": 37.7745, "lon": -122.4180 }, { "lat": 37.7745, "lon": -122.4200 } ]}The verifier uses ray-casting for polygon-inside checks. Reference implementations in all four SDKs are byte-identical (verified by conformance fixture 47–52).
Who provides the location?
Section titled “Who provides the location?”The presenter (the agent) provides location in the verification context. The verifier
trusts the presenter to report its true location. This is by design: the protocol doesn’t
solve “is this agent telling the truth about where it is?” — that’s a layer above, typically
handled by hardware attestation (TPM/secure enclave) or out-of-band signals (Wi-Fi MAC,
cell-tower triangulation, GPS time-of-flight).
If you need stronger geo assurance, the runtime context should be vouched for by something the verifier already trusts: a TPM quote, a vehicle’s CAN-bus reading, a fleet management system’s record. The constraint is then “agent claims X, system records Y, do they match?”
Temporal constraints
Section titled “Temporal constraints”{ "type": "temporal", "valid_hours": [6, 22], "days": [1, 2, 3, 4, 5]}The agent can only act between 6:00 and 22:00 (in the agent’s local time, derived from the
context’s now and timezone), Monday through Friday (days uses ISO 8601 day numbering;
Monday=1).
const result = await verifyBundle(bundle, { required_scope: "system:execute", context: { now: new Date("2026-05-11T10:30:00-08:00"), // Monday 10:30 PT → ✓ timezone: "America/Los_Angeles", },});If timezone is unspecified, UTC is assumed.
Common patterns
Section titled “Common patterns”- Business hours only:
valid_hours: [9, 17], days: [1,2,3,4,5] - No weekend access:
days: [1,2,3,4,5], novalid_hours - Maintenance window:
valid_hours: [2, 4](2am–4am) - Always: no
temporalconstraint at all — the default
Version constraints
Section titled “Version constraints”{ "type": "version", "min": "1.2.0", "max": "2.0.0", "exclude": ["1.4.2", "1.4.3"]}The agent must be running a version that satisfies the range. SemVer rules apply (greater-than-or-equal
to min, less-than max, not in exclude).
const result = await verifyBundle(bundle, { required_scope: "vehicle:drive", context: { version: "1.3.5", // ≥1.2.0, <2.0.0, not excluded → ✓ },});Use case: revoke a fleet of agents running a known-vulnerable firmware version without revoking individual cert IDs. Just push a version-constraint update.
Interaction between scopes and constraints
Section titled “Interaction between scopes and constraints”Both must pass for a bundle to be valid:
verify_bundle(bundle, options) returns valid iff:
1. All signature checks pass (signature) 2. Freshness check passes (challenge_at within window) 3. No cert is expired (issued_at ≤ now < expires_at) 4. Chain is well-formed (subjects = next issuers) 5. Effective scope covers required (scope intersection) 6. No cert is revoked (revocation list check) 7. All constraints pass (for each cert in chain, for each constraint, the context satisfies it)A constraint that the context can’t satisfy is a rejection, not a partial match. If the
delegation requires geo_circle and the verification context has no location, the verifier
rejects with identity_status: constraint_violation and error_reason: "location required".
Chained constraints
Section titled “Chained constraints”Sub-delegation constraints are additive: if Alice constrains Agent-A to a geo region, and Agent-A sub-delegates to Agent-B with a different geo region, Agent-B must satisfy both.
Alice → Agent-A constraint: geo_circle (warehouse)
Agent-A → Agent-B constraint: temporal (6am–8am)
Agent-B presenting a proof bundle must be: ✓ Inside the warehouse (from Alice's constraint) ✓ At 6am–8am (from Agent-A's constraint)
If both pass: bundle is valid. If either fails: identity_status = constraint_violation.Constraints never weaken down the chain. They only accumulate.
// Goimport "github.com/identities-ai/ratify-protocol"
result := ratify.VerifyBundle(bundle, ratify.VerifyOptions{ RequiredScope: "physical:move", Context: &ratify.VerifyContext{ Location: &ratify.GeoPoint{Lat: 37.7751, Lon: -122.4190}, Now: time.Now(), Version: "1.3.5", },})# Pythonresult = verify_bundle( bundle, required_scope="physical:move", context=VerifyContext( location=GeoPoint(lat=37.7751, lon=-122.4190), now=datetime.now(), version="1.3.5", ),)// Rustlet result = verify_bundle(&bundle, &VerifyOptions { required_scope: Some("physical:move".into()), context: Some(VerifyContext { location: Some(GeoPoint { lat: 37.7751, lon: -122.4190 }), now: Some(SystemTime::now()), version: Some("1.3.5".into()), }), ..Default::default()})?;// TypeScriptconst result = await verifyBundle(bundle, { required_scope: "physical:move", context: { location: { lat: 37.7751, lon: -122.4190 }, now: Date.now(), version: "1.3.5", },});Source
Section titled “Source”Constraint encoding and evaluation rules are normative: SPEC.md §10. The full constraint vocabulary is frozen for v1.x; new constraint families require a minor bump.
Where to next
Section titled “Where to next”- Scopes — the things constraints further restrict
- Physical AI guide — geo constraints in production
- Delegate, Present, Verify — how constraints fit into the verifier