Policy model
The PolicyEvaluator contract, three policy kinds, and dispatch evidence.
A policy in Fabric is a predicate that returns pass, warn, or block for a given action invocation. Source: packages/platform/policies/index.ts.
The evaluator contract
interface PolicyEvaluator<TDb = unknown> {
policyId: PolicyId; // "lending.credit_pull_consent.v1"
version: number;
kind?: "code";
previewSafe?: boolean;
evaluate: (ctx: PolicyContext<TDb>) => Promise<PolicyOutcome>;
}interface PolicyContext<TDb> {
tenantId: string;
spaceId: string;
actionInvocationId: string;
actionId: string;
parameters: unknown;
db: TDb;
now?: Date;
mode?: "preview" | "execute";
}The evaluator gets read-only access to db (for cross-entity checks like "has consent been recorded?") and the action's full parameters. It returns:
interface PolicyOutcome {
policyId: PolicyId;
policyVersion: number;
policyKind?: PolicyKind;
result: "pass" | "warn" | "block";
reason?: string;
metadata?: Record<string, unknown>;
dispatchEvidence?: PolicyDispatchEvidence;
}Three kinds
| Kind | Where the logic lives | When to use |
|---|---|---|
code | A registered TypeScript PolicyEvaluator. | Anything requiring database reads or non-trivial logic. |
data | A declarative DataPolicyDefinition (condition tree). | Simple parameter/context comparisons that compliance teams want to inspect/edit. |
hybrid | Data first; on a configured trigger, fall back to code. | Default-deny rules where the data check covers 90 % of cases and code handles edges. |
Data policies
interface DataPolicyDefinition {
conditions: DataPolicyCondition[];
defaultResult?: PolicyEvaluationResult;
reason?: string;
}Conditions are one of: always, parameter, comparison, all, any, not. Comparisons are limited to exists, equals, notEquals, gt, gte, lt, lte. Path access is restricted to parameters.* and a small allowlisted set of context paths (tenantId, spaceId, actionInvocationId, actionId, mode).
The bounded shape (max depth 5, max 100 conditions, max 12 path segments — see invariants) is intentional. A data policy that needs more is a code policy.
Hybrid fallback
interface PolicyFallbackDefinition {
codeEvaluatorPolicyId: PolicyId;
onResults?: PolicyEvaluationResult[]; // default: ["warn", "block"]
triggers?: PolicyFallbackTrigger[]; // default: all three triggers
reason?: string;
}Triggers:
data_result— the data outcome was inonResults.missing_data_definition— the policy is hybrid but has nodataDefinition.invalid_data_definition— the data definition failed validation.
Hybrid lets compliance own the common path declaratively while engineering owns the edge.
Dispatch evidence
Every outcome carries a dispatchEvidence blob recording:
interface PolicyDispatchEvidence {
policyKind: PolicyKind;
policyId?: PolicyId;
policyVersion?: number;
dispatchPath: Array<"data" | "code" | "fallback">;
data?: { definitionVersion, definitionStatus, conditionResults, validationErrors };
code?: { requestedPolicyId, policyId, version, registered };
fallback?: { used, trigger, reason, fromResult, definitionVersion, codeEvaluatorPolicyId };
}This is what makes the policy decision inspectable. A regulator does not have to trust the result; they can read why it was reached.
Aggregation
Multiple policies may attach to one action. The runtime evaluates each, then aggregates:
function aggregatePolicyOutcomes(outcomes: PolicyOutcome[]): PolicyOutcome | undefined {
return outcomes.find((o) => o.result === "block")
?? outcomes.find((o) => o.result === "warn");
}Any block halts. Otherwise the first warn is surfaced. Otherwise pass.
See also
- Enforcement — when and how dispatch happens.
- Policy checkpoints — pipeline placement.
- Audit trail — how dispatch evidence becomes compliance evidence.