Access policies
A deterministic, side-effect-free rules engine. Rules are sorted by descending priority; the first matching rule wins; the defaultEffect applies when nothing matches. No I/O of any kind.
Defining a policy
ts
import { definePolicy } from "@corri/sdk/server";
const policy = definePolicy({
id: "demo-publisher",
version: "1",
defaultEffect: "deny", // when no rule matches
rules: [
{ id: "require-verified-agent", priority: 100,
when: { principalKinds: ["unknown"] },
effect: "challenge",
reason: "A signed agent identity is required" },
{ id: "deny-training", priority: 90,
when: { purposes: ["training"] },
effect: "deny",
reason: "Training is not permitted" },
{ id: "charge-summarization", priority: 80,
when: { principalKinds: ["verified_agent"], actions: ["read"],
purposes: ["summarization"] },
effect: "charge",
entitlement: { type: "any", providers: ["api-key", "x402"] },
payment: { amount: "0.01", currency: "USD" },
obligations: [
{ type: "attribution", required: true },
{ type: "prohibit-republication" }
] }
]
});
definePolicy validates the definition and returns an evaluatable Policy; a malformed definition throws PolicyValidationError with a list of issues. Because the returned object carries a validation marker, raw unvalidated definitions cannot be passed where a Policy is required.
Effects
| Effect | HTTP | Behavior |
|---|---|---|
allow | 200 | Handler runs immediately; a receipt is issued. |
challenge | 401 | The agent is told how to authenticate (Web Bot Auth challenge body). |
charge | 402 → 200 | Entitlement providers are consulted. Satisfied → handler runs. Missing → 402 with payment requirements. |
deny | 403 | Hard stop with the rule's reason. Payment cannot override a deny. |
Matching
Every field present in a rule's when clause must match; absent fields match anything.
| Field | Matches | Example |
|---|---|---|
principalKinds | verified_agent, api_client, unknown | ["verified_agent"] |
principalIds | Specific agent identities | ["demo-agent.corri.dev"] |
operatorIds | The organization operating the agent | ["operator:acme"] |
actions | Declared actions | ["read"] |
purposes | Declared purposes | ["training", "summarization"] |
resources | Resource ids, with a trailing * wildcard segment | ["report:*"] matches report:grid-brief |
resourceAttributes | Resource attribute equality | { tier: "premium" } |
Charge rules
A charge rule must say how it can be satisfied and what it costs:
entitlement,{ type: "any" | "all", providers: [...] }. With"any", one satisfied provider is enough; with"all", every listed provider must be satisfied.payment,{ amount: "0.01", currency: "USD" }, a decimal string and an ISO 4217 / stablecoin code.obligations, terms attached to the grant (e.g.attribution). They travel into the 402 body, the receipt, and the RSL document.
Evaluating directly
The engine is pure, useful for tests, previews, and admin UIs:
ts
import { evaluatePolicy, matchesResourcePattern } from "@corri/sdk";
const decision = evaluatePolicy(definition, {
principal: { id: "demo-agent.corri.dev", kind: "verified_agent", trust: "verified" },
resource: { id: "report:grid-brief", version: "2026-07-21" },
action: "read",
purpose: "summarization"
});
// decision: { effect: "charge", policyId, policyVersion, ruleId: "charge-summarization",
// entitlementRequirement, payment, obligations }
matchesResourcePattern("report:*", "report:grid-brief"); // true
Determinism matters: the same request against the same policy always yields the same decision, which is what makes decisions auditable, receipts meaningful, and the RSL export an honest rendering of your actual behavior.