Policy bundle specification
Goals
Policy bundles make guardrail behavior reusable, reviewable, deterministic, and
deployable without request-time compilation. YAML is the authoring representation;
canonical JSON is the compiled and signed representation. The initial authoring
schema is policy-bundle.schema.json.
This specification is proposed pending OD-011 through OD-013.
Authoring model
A bundle declares:
- a stable reverse-domain
nameand SemVerversion; - explicit digest-pinned
imports; - exported named profiles;
- check instances and operator-approved configuration;
- typed, pure CEL
matchpredicates; - composition and per-check error behavior;
- stable rule IDs and whether a platform rule is overridable;
- bundle tests containing synthetic input facts and expected outcomes.
Detector credentials, endpoints, trust roots, mandatory platform defaults, and audit-redaction controls are operator configuration and cannot appear in tenant bundles.
CEL profile
CEL is limited to deciding whether a rule applies. The compiler exposes a typed environment containing event metadata, trusted context labels, and content metadata—not raw detector credentials or arbitrary host objects.
Allowed expressions are side-effect-free and deterministic. The environment:
- provides an allowlist of pure functions;
- performs static type checking;
- forbids I/O, network, filesystem, reflection, secret access, and dynamic plugins;
- limits AST size and estimated/runtime cost;
- enforces an evaluation deadline;
- rejects unbounded or unsafe regular expressions and oversized literals.
Detection and transformation do not execute inside CEL.
Imports, layering, and overrides
Imports are explicit and pinned to a sha256: digest. SemVer tags assist discovery
but never select production dependencies at runtime. Shared bundles export
namespaced profiles.
The compiler applies layers in this order:
- mandatory platform baseline;
- tenant/workspace policy;
- application/deployment policy.
There is no implicit inheritance and no general deep merge. An override targets a stable rule ID and uses one typed operation:
- enable or disable an overridable rule;
- replace a schema-approved parameter;
- append a rule with a unique ID.
A lower layer cannot weaken a non-overridable platform rule. Duplicate rule IDs, unknown override targets, and ambiguous ordering are compile errors.
Each rule's overridable flag defaults to false. Only a rule marked
overridable: true can be the target of a lower layer's typed override.
Current runtime slice
The runtime loads one bundle and one profile. It has no layered compiler yet, so:
overridableis parsed, defaults tofalse, and is retained in the snapshot for the layered compiler. With a single layer there is nothing that could override a rule, so the flag cannot weaken anything.importsandoverridesare rejected with an explicit error rather than ignored, because applying neither would silently change the policy's meaning.- Bundle
testsare rejected until the compiler runs them, so a bundle cannot appear tested when it is not. matchexpressions are rejected until the CEL environment exists.check.configis accepted by the schema but not passed to built-in detectors.- Each detector referenced by several enabled rules runs once per request. Its findings are reported once, and each rule judges them and gets its own coverage entry.
Compiler contract
Compilation is all-or-nothing:
- Parse YAML and reject unknown fields.
- Validate JSON Schema and supported format version.
- Resolve the complete import DAG; reject cycles, missing digests, and name/version inconsistencies.
- Validate detector capabilities, configuration schemas, categories, stages, and parameter ranges.
- Type-check and cost-check CEL.
- Apply layers and typed overrides.
- Run bundle unit tests and corpus replay/diff.
- Emit canonical JSON, a dependency lockfile, capability requirements, and a content digest.
- Sign the artefact and provenance after the approval gate.
Canonicalization uses UTF-8 JSON with lexicographically sorted object keys, no insignificant whitespace, and unchanged array order. Number encoding must be specified before the compiled format is frozen; authoring currently avoids floating-point policy values where exact decimal handling matters.
Snapshot and activation lifecycle
Publish immutable OCI artefacts. The evaluator verifies digest, expected signer, provenance, schema compatibility, and available capabilities before installation. It never activates a partial graph. A complete snapshot is shadowed, moved through a deterministic sticky canary, and atomically promoted. Last-known-good and prior digests are retained for rollback. Signing proves provenance, not policy safety.
Composition
V1 composition is deny-wins. Each check is required or optional and declares an
on_error action of block, allow_unjudged, or continue_unjudged as permitted
by the policy schema. The exact permitted values by risk tier remain an open risk
decision. Quorum and weighted voting are deliberately deferred.
The au-baseline example
policies/examples/au-baseline.yaml is
an experimental, non-production policy that loads in the runtime. Tests check
every profile.
Decision (3 October 2026). The earlier version of this file passed schema
validation but could never load. It referenced four detectors that do not exist
(builtin.pii.au, builtin.credentials, builtin.jailbreak, and
builtin.safety), a CEL match expression, and detector config that no
built-in reads. The file was corrected rather than skipped:
- The
overridable: falseflags are kept and are now modelled by the loader. - The rules for detectors that do not exist were replaced with the two
implemented Australian structured detectors. The
matchexpression selected every phase, so removing it changes nothing. - The file now has an
observeprofile and aredact-experimentalprofile. Both userequired: falseandon_error: continue_unjudged, and neither usesminimum_confidence.
The removed families come back as rules once their detectors exist. The file is limited to the owner-approved posture recorded in section 17 of the micro-slice specification: observe or redact only up to G2, with no blocking or production enforcement.