Chio/Docs
LOGIN · JOIN

Policy Compilation

Compile a HushSpec document, inspect the scope, guards, and output hook it produces, and see where the host installs each.

Concept chio@5f63e6e

On this page

One HushSpec document configures three runtime controls. The compiler turns it into the capability scope a host signs authority from, the request guards the kernel runs before dispatch, and the output hook that processes a tool result before the agent receives it.

chio policy explain prints what a document compiles to without writing Rust, and the policy inspector, a Rust program built on the same compiler, prints the complete serialized result. Write rules with HushSpec Policy Format; run the same controls against a tool in the document application.

One document compiles into three controls the host installs

The compiler validates the resolved document, then builds each control from the rule blocks that configure it.

Compiled controlsChoose a control.

The host signs authority from the compiled scope

tool_access compiles into default_scope: one grant to invoke quote on server *, constrained by MaxArgsSize(4096). The scope is unsigned. The host issues a capability token from it, and the agent presents that token with each request. Capability reference.

The policy inspector's document policy: base.yaml and policy.yaml resolve into one HushSpec, which compiles into three controls. Threshold approval and detection guards compile the same way and are left out; the grant's operation (invoke) and server (*) are omitted from the labels.

Compilation builds the controls in memory. The host installs the request pipeline and the output pipeline, and signs a capability token from the scope, before an agent can use any of them. The control-plane loader installs both pipelines, builds the default capability configuration, and binds the policy's identity for runtime use. Compiler output describes constructed controls; the signed kernel receipt is the record that a call was decided.

Validation errors stop the build; warnings do not. A document with no configured rule compiles to no request guards and one wildcard default grant, so an explicit tool_access block is how a policy limits authority.

One command prints what a document compiles to

Download the complete policy-inspector project and unpack it anywhere. base.yaml allows quote with at most 4,096 bytes of serialized arguments, and policy.yaml extends it with a custom secret pattern. chio policy explain prints what a document compiles to without writing Rust:

Terminal · in the unpacked policy-inspectorbash
chio policy explain policies/policy.yaml
Output
policy     policies/policy.yaml  extends policies/base.yaml
grants     quote on server *  max_args_size 4096
guards     1 mcp-tool
           2 secret-leak
output     1 hook
approvals  none

The policy-inspector project below calls the same compiler from the library and prints the complete serialized scope. It is the starting point for a host that inspects policies in its own code.

The project carries its source, policies, tests, and the Cargo lockfile. Its Cargo.toml takes the chio-policy crate as a git dependency pinned to one public revision.

policies/base.yaml · completeyaml
hushspec: "0.1.0"
name: document-policy
rules:
  tool_access:
    enabled: true
    default: block
    allow: [quote]
    max_args_size: 4096
policies/policy.yaml · completeyaml
hushspec: "0.1.0"
extends: base.yaml
rules:
  secret_patterns:
    enabled: true
    patterns:
      - name: project-token
        pattern: 'DEMO_[A-Z0-9]{8}'
        severity: error
Terminal · in policy-inspectorbash
cargo run --locked --quiet -- policies/policy.yaml
Compiled output · complete scope
{
  "default_scope": {
    "grants": [
      {
        "constraints": [
          {
            "type": "max_args_size",
            "value": 4096
          }
        ],
        "operations": [
          "invoke"
        ],
        "server_id": "*",
        "tool_name": "quote"
      }
    ]
  },
  "guard_names": [
    "mcp-tool",
    "secret-leak"
  ],
  "output_hooks": 1,
  "threshold_approval": false
}

You should see two request guards, mcp-tool and secret-leak, one output hook, and one default grant: invoke quote on server * with a max_args_size of 4096. The child inherited the parent's tool_access block whole. The output is the complete serialized default_scope, with the guard names and hook count beside it. Constrain the server when you issue authority for your own application.

A stricter child narrows the grant and leaves the guards alone

attested.yaml requires an attested runtime and lowers the argument limit to 2,048 bytes. It repeats the allowlist, because a child's tool_access block replaces its parent's whole block.

policies/attested.yaml · completeyaml
hushspec: "0.1.0"
extends: policy.yaml
rules:
  tool_access:
    enabled: true
    default: block
    allow: [quote]
    max_args_size: 2048
    require_runtime_assurance_tier: attested
Terminalbash
cargo run --locked --quiet -- policies/attested.yaml
Stricter compiled output
{
  "default_scope": {
    "grants": [
      {
        "constraints": [
          {
            "type": "max_args_size",
            "value": 2048
          },
          {
            "type": "minimum_runtime_assurance",
            "value": "attested"
          }
        ],
        "operations": [
          "invoke"
        ],
        "server_id": "*",
        "tool_name": "quote"
      }
    ]
  },
  "guard_names": [
    "mcp-tool",
    "secret-leak"
  ],
  "output_hooks": 1,
  "threshold_approval": false
}

You should see the same guard names and hook count. The grant now carries two constraints, MaxArgsSize at 2048 and MinimumRuntimeAssurance at attested. The inspector reports the requirement only: it supplies no attestation evidence and evaluates no request.

An empty allowlist removes the grant on purpose. blocked.yaml keeps default: block and allows nothing.

policies/blocked.yaml · completeyaml
hushspec: "0.1.0"
extends: policy.yaml
rules:
  tool_access:
    enabled: true
    default: block
    allow: []
Terminalbash
cargo run --locked --quiet -- policies/blocked.yaml
Empty default scope
{
  "default_scope": {},
  "guard_names": [
    "mcp-tool",
    "secret-leak"
  ],
  "output_hooks": 1,
  "threshold_approval": false
}

You should see default_scope serialized as {}. It supplies no default tool authority, and its serializer omits the empty grants list. Both guards still compile. An empty pipeline and an empty scope are different results, so check for each.

The scope compiler emits only the grants it can represent

Enabled tool_access rules and applicable human_in_loop approval settings become grants. A combination the scope cannot express produces no default grant, so a successful compile can return an empty scope, which is a result to inspect and never a parse error.

  • No tool_access block, or a disabled one. One wildcard server and tool grant for Invoke, with no constraints. Omitting rules has the same result.
  • default: block with an allowlist. One wildcard-server Invoke grant per allowed tool pattern that overlaps no blocked pattern. An empty allowlist gives no grants.
  • default: allow. With both lists empty, one wildcard grant that can carry max_args_size, a required assurance tier, all-tool confirmation, or an approval threshold. A nonempty allow or block list, or a confirmation that covers only some tools, gives no grants, because a requirement on some tools cannot widen into a wildcard grant.
  • Workload identity. An enabled block with require_workload_identity or prefer_workload_identity gives no grants from this path; the scope does not encode identity conditions.

Constraints map one to one. max_args_size becomes MaxArgsSize, and require_runtime_assurance_tier becomes MinimumRuntimeAssurance; a preferred tier adds nothing. A matching require_confirmation becomes RequireApprovalAbove with threshold 0. Otherwise an enabled human_in_loop.approve_above supplies the threshold.

Overlap resolves conservatively. Allowing payments.* while requiring confirmation for payments.charge attaches the approval requirement to the compiled payments.* grant. An allow pattern that overlaps a blocked pattern is dropped, and an invalid pattern fails the compile.

Read more in Capability reference.

A failed compile stops before any control exists

The project includes a policy that declares an unsupported HushSpec version, so you can watch a failure without breaking an integration.

policies/invalid.yaml · complete failing inputyaml
hushspec: "9.9.9"
rules:
  tool_access:
    enabled: true
    default: block
    allow: [quote]
Terminalbash
cargo run --locked --quiet -- policies/invalid.yaml
Compiler failure · exit 1
Could not compile policy: invalid policy: HushSpec validation failed: unsupported hushspec version: 9.9.9

You should see the validation error and exit status 1. An unreadable pattern database, a regex or glob that cannot build, and threshold approvers without a directory stop the compile the same way; Compilation failures gives the action for each.

A compile can also succeed while a setting has no mapping on this runtime path: egress.default, jailbreak.warn_threshold, and a secret pattern's severity and description are examples. Schema acceptance alone establishes no enforcement, so test the exact controls your deployment relies on through your configured application.

Read more in Testing Guards & Policies.

Compiled fields, rule mappings, and limits

CompiledPolicy fields

FieldHoldsHost action
guards
The request GuardPipeline
Install before dispatch
post_invocation
The output pipeline
Install after the tool
default_scope
Generated tool grants and constraints
Sign capability authority from it
threshold_approval
An optional policy-owned threshold requirement
Install it; approvers need a directory
guard_names
Ordered runtime guard names
Inspect; a name can repeat

Rule, detection, and budget passes add guards to guards in that order. In this compiler, secret_patterns adds its SanitizerHook to post_invocation. default_scope is unsigned. Configured threshold approvers need an authenticated directory at compilation. guard_names can hold the same name twice when separate passes add the same guard type.

Compiler orchestration source

The compiler lives in crates/guards/chio-policy/src/compiler.rs, with the rule passes in compiler/rules.rs and scope generation in compiler/scope.rs.

crates/guards/chio-policy/src/compiler.rs149-173rust
fn compile_policy_with_options(
    policy: &HushSpec,
    source_path: Option<&Path>,
    budget: &MemoryBudgetConfig,
    approver_directory: Option<&dyn ApproverDirectory>,
) -> Result<CompiledPolicy, CompileError> {
    ensure_compilable_policy(policy)?;

    let mut builder = PipelineBuilder::new();
    let mut post_invocation = PostInvocationPipeline::new();
    let source_dir = source_path.and_then(|path| path.parent());
    compile_rule_guards(policy, &mut builder, &mut post_invocation, budget)?;
    compile_detection_guards(policy, &mut builder, source_dir)?;
    compile_budget_guards(policy, &mut builder, budget)?;
    let default_scope = compile_scope(policy)?;
    let threshold_approval = compile_threshold_approval_requirement(policy, approver_directory)?;
    let (guards, guard_names) = builder.finish();
    Ok(CompiledPolicy {
        guards,
        post_invocation,
        default_scope,
        threshold_approval,
        guard_names,
    })
}

Policy rule to runtime guard

Present, enabled rule blocks construct their guards; the three detection blocks default to enabled when enabled is omitted. The pipeline holds only what this compile adds and does not inherit the default pipeline.

Policy keyGuard runtime nameCondition
rules.forbidden_paths
forbidden-path
Empty patterns uses built-in defaults without exceptions; a non-empty list carries both, and a bad glob fails the compile.
rules.velocity
velocity
Only with max_invocations_per_window or max_spend_per_window. window_secs clamps up to 1.
rules.velocity
agent-velocity
Only with max_requests_per_agent or max_requests_per_session; with neither, no guard.
rules.shell_commands
shell-command
Empty forbidden_patterns keeps the guard defaults.
rules.egress
egress-allowlist, internal-network
One block, two guards: the allowlist catches unknown domains, the SSRF companion raw private and metadata addresses.
rules.tool_access
mcp-tool
Carries allow, block, default, and max_args_size. Approval and assurance constraints compile into the scope.
rules.secret_patterns
secret-leak plus a post-invocation hook
The one block that compiles onto two pipelines.
rules.patch_integrity
patch-integrity
Every field maps across; an invalid forbidden_patterns regex fails the compile.
rules.path_allowlist
path-allowlist
read, write, patch become the three allow lists.
rules.computer_use
computer-use
observe, guardrail, fail_closed map onto EnforcementMode.
rules.remote_desktop_channels
remote-desktop-side-channel
Four toggles travel. Session share, printing, and transfer size stay at the guard’s permissive defaults.
rules.input_injection
input-injection-capability
Allowed types plus require_postcondition_probe.
rules.browser_automation
browser-automation
Domain lists, verb list, credential detection, extra credential patterns.
rules.code_execution
code-execution
Language allowlist, module denylist, network access, execution time, optional scan-byte cap.
extensions.detection.prompt_injection
prompt-injection
Levels become score thresholds: safe 0.1, suspicious 0.4, high 0.8, critical 1.0. max_scan_bytes clamps up to 1.
extensions.detection.jailbreak
jailbreak
block_threshold is an integer percentage, capped at 100 and divided by 100. max_input_bytes becomes the scan cap.
extensions.detection.threat_intel
embedding-anomaly
A missing or empty pattern_db fails validation; an unresolvable or unparsable file fails the compile.
extensions.origins.profiles[].budgets
agent-velocity
The tightest tool_calls across profiles becomes one per-agent ceiling over a fixed 60-second window.

A burst_factor that is not finite and positive falls back to 1.0. Empty egress allow and block lists take the allowlist guard's built-in lists, and egress.default does not travel: the guard refuses any unlisted host whatever the block says. Workload-identity settings on tool_access produce no default grants.

For prompt_injection, a max_scan_bytes of zero would make the scanner a no-op that allows everything, hence the clamp. For jailbreak, a conversion overflow to u32 uses 0, and warn_threshold has no guard mapping. For threat_intel, similarity_threshold and top_k override the guard defaults.

Velocity guards sit after forbidden-path and before shell-command. Origin budgets take the smallest tool_calls limit across profiles, saturating values above u32::MAX. Combined with rules.velocity, the pipeline can hold two agent-velocity entries, and guard_names preserves both.

Secret pattern compilation

secret-leak checks supported outbound writes. SanitizerHook processes tool results before delivery to the agent. The custom pattern in the example reaches both, with different configurations.

SettingWrite guardOutput hook
Pattern text
Added to the built-in secret patterns
Replaces the denylist pattern list; other detectors keep defaults
Pattern name
Preserved for the custom pattern
Not passed
skip_paths
The exemption list; empty removes the default exemptions
Not used
severity, description
Not mapped
Not mapped

A write exemption does not exempt content in a returned result. The output hook keeps the sanitizer's built-in secret, PII, internal-data, and entropy detection defaults and its 1,000,000-byte scan cap, so it can redact patterns beyond your custom list. Response Sanitization covers result handling and scan limits.

Source for the two secret-check pipelines
crates/guards/chio-policy/src/compiler/rules.rs125-146rust
// 5. secret_patterns -> SecretLeakGuard
//
// SecretLeakGuard handles the write path (detect secrets in outbound
// file writes) while the post-invocation SanitizerHook handles the read
// path (redact secrets in tool results before the agent sees them).
if let Some(sp) = &rules.secret_patterns {
    if sp.enabled {
        let config = chio_guards::secret_leak::SecretLeakConfig {
            enabled: true,
            skip_paths: sp.skip_paths.clone(),
            custom_patterns: compile_custom_secret_patterns(sp),
        };
        builder.add(
            SecretLeakGuard::with_config(config)
                .map_err(|error| CompileError::Invalid(error.to_string()))?,
        );
        post_invocation.add(Box::new(
            SanitizerHook::with_config(compile_output_sanitizer_config(sp))
                .map_err(|error| CompileError::Invalid(error.to_string()))?,
        ));
    }
}

Compiler entry points

FunctionSource pathApprover directoryMemory budget
compile_policy
No
No
Process default
compile_policy_with_source
Optional
No
Process default
compile_policy_with_memory_budget
Yes
No
Configured MemoryBudgetConfig
compile_policy_with_approver_directory
No
Authenticated lookup
Process default
compile_policy_with_source_and_approver_directory
Yes
Authenticated lookup
Process default

The source path gives relative auxiliary assets a base directory. The inspector calls compile_policy_with_source; the control-plane loader calls compile_policy_with_source_and_approver_directory. MemoryBudgetConfig includes velocity_bucket_cap.

Threshold approvers must resolve to the requested identities from one nonempty directory version. The requirement checks quorum, unique identifiers and keys, at most 32 approvers, and a timeout from 1 to 3,600 seconds (default 900). The inspector supplies no directory, so a document that declares threshold approvers fails with the explicit directory requirement. Approval & HITL covers the approval contract.

Resolution limits and merge behavior

resolve_from_path returns one merged HushSpec document, and compilation is a separate operation. Relative extends paths resolve from the referring document's directory; absolute paths are used as written. The filesystem loader reads local files only and does not confine reads to a policy root.

Setting or resultBehaviorAction
max_document_bytes
4 MiB per document, checked before and during the read
Shrink the file or set a positive limit
max_extends_depth
32 documents, counting the start; zero is invalid
Flatten the chain or set a positive depth
Cycle
A repeated canonical source path reports the closing chain
Remove the circular extends
merge, deep_merge
Replace each supplied rules block whole; differ on extensions
Repeat required settings when overriding
replace
Discards the parent, keeps the child
Supply every control to retain
HTTP extends
Treated as a path; the read fails
Use a local file; no network timeout applies

Set deliberate limits with resolve_from_path_with_limits. A loader you pass to resolve_with_loader controls its own loading behavior and canonical source identifiers; the filesystem behavior above belongs to resolve_from_path. Full merge rules are in HushSpec Policy Format.

crates/guards/chio-policy/src/resolve.rs11-12rust
pub const DEFAULT_MAX_POLICY_DOCUMENT_BYTES: usize = 4 * 1024 * 1024;
pub const DEFAULT_MAX_EXTENDS_DEPTH: usize = 32;

Policy hashes and decision records

RecordComputed overKernel signature
compute_policy_hash
SHA-256 of the parsed policy serialized as JSON
No
source_hash
Loaded root-file bytes, plus auxiliary asset digests
No
runtime_hash
Selected fields of the control-plane runtime fingerprint
No
DecisionReceipt
One policy interpreter evaluation
No
Kernel receipt
One mediated decision
Yes

The compiler first uses compute_policy_hash for a threshold-approval requirement. source_hash covers the root file, never every inherited source file. runtime_hash includes the generated capability configuration and auxiliary assets, never the complete HushSpec. DecisionReceipt is the interpreter's structured record, with no proof of tool dispatch.

The loader rebinds a compiled threshold requirement to runtime_hash before installing it. That fingerprint covers seven listed rule blocks and the reputation settings and omits some other compiled rule and detection fields, so equal runtime hashes alone do not show that every configured control is identical. For signed runtime records, inspect the kernel receipt and its verification result in Receipt Contract.

Policy interpreter receipt and hash implementation

evaluate_audited wraps evaluate with optional timing and hashing. AuditConfig.enabled=false returns zero duration and an empty content hash. redact_content records a flag when content was present; ActionSummary carries no content field. hushspec_version identifies the library version, while policy.version copies the document version.

crates/guards/chio-policy/src/receipt.rs129-134rust
pub fn compute_policy_hash(spec: &HushSpec) -> String {
    let json = serde_json::to_string(spec).unwrap_or_default();
    let mut hasher = Sha256::new();
    hasher.update(json.as_bytes());
    format!("{:x}", hasher.finalize())
}

The helper falls back to an empty string if serialization fails, so its digest alone does not establish that serialization succeeded.

Compilation failures

SymptomMeaningNext action
Validation failed
The compiler stopped before building controls
Correct the reported version, field, or constraint; warnings alone pass
Pattern database read or parse failure
Enabled threat_intel needs a readable, valid JSON database
Supply pattern_db and pass the policy source path
Invalid regex or glob
A guard or scope cannot build its matcher
Correct the named pattern; test intended matches and non-matches
Threshold approvers require a directory
Approver identities need authentication
Use an approver-directory entry point; check its identities and version
Empty default_scope.grants
No default authority, including for unrepresentable shapes
Inspect tool_access; keep the empty scope over a blanket grant

Inspector source

The executable calls the Rust library directly. resolve_from_path loads inheritance, and compile_policy_with_source validates and constructs the result. Passing the policy path also gives relative threat-intelligence assets a base directory.

policy-inspector/Cargo.tomlL1–10toml
[package]
name = "policy-inspector"
version = "0.1.0"
edition = "2021"

[workspace]

[dependencies]
chio-policy = { git = "https://github.com/backbay-labs/chio", rev = "5b8bec41d32f3838b880576fe6123c983ecebf8d" }
serde_json = "1"

Open the complete source file

policy-inspector/src/lib.rsL1–15rust
use chio_policy::{compile_policy_with_source, resolve_from_path};
use serde_json::{json, Value};
use std::{error::Error, path::Path};

/// Inspect compiler output. This does not issue a capability or invoke a tool.
pub fn inspect(path: &Path) -> Result<Value, Box<dyn Error>> {
    let policy = resolve_from_path(path)?;
    let compiled = compile_policy_with_source(&policy, Some(path))?;
    Ok(json!({
        "guard_names": compiled.guard_names,
        "output_hooks": compiled.post_invocation.len(),
        "default_scope": compiled.default_scope,
        "threshold_approval": compiled.threshold_approval.is_some()
    }))
}

Open the complete source file

policy-inspector/src/main.rsL1–25rust
use std::{env, path::Path, process::ExitCode};

fn main() -> ExitCode {
    let args: Vec<_> = env::args_os().skip(1).collect();
    if args.len() != 1 {
        eprintln!("Usage: policy-inspector <policy.yaml>");
        return ExitCode::from(2);
    }
    match policy_inspector::inspect(Path::new(&args[0])) {
        Ok(output) => match serde_json::to_string_pretty(&output) {
            Ok(text) => {
                println!("{text}");
                ExitCode::SUCCESS
            }
            Err(error) => {
                eprintln!("Could not serialize compiler output: {error}");
                ExitCode::FAILURE
            }
        },
        Err(error) => {
            eprintln!("Could not compile policy: {error}");
            ExitCode::FAILURE
        }
    }
}

Open the complete source file

The contract tests check inherited guard construction, changed constraints, empty grants, whole-block replacement, resolver and compiler failures, and the permissive result of omitting tool_access. Run them after each policy change.

Terminalbash
cargo test --locked --test compilation --quiet
Contract test results

running 8 tests
........
test result: ok. 8 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.07s
policy-inspector/tests/compilation.rsL1–112rust
use chio_policy::{
    compile_policy, resolve_from_path, resolve_from_path_with_limits, HushSpec, ResolveLimits,
};
use policy_inspector::inspect;
use serde_json::json;
use std::path::Path;

#[test]
fn inherited_policy_builds_both_pipelines_and_one_grant() {
    let out = inspect(Path::new("policies/policy.yaml")).unwrap();
    assert_eq!(out["guard_names"], json!(["mcp-tool", "secret-leak"]));
    assert_eq!(out["output_hooks"], 1);
    assert_eq!(out["threshold_approval"], false);
    let grants = out["default_scope"]["grants"].as_array().unwrap();
    assert_eq!(grants.len(), 1);
    assert_eq!(grants[0]["server_id"], "*");
    assert_eq!(grants[0]["tool_name"], "count_words");
    assert_eq!(grants[0]["constraints"].as_array().unwrap().len(), 1);
}

#[test]
fn stricter_policy_preserves_secret_checks_and_adds_a_constraint() {
    let original = inspect(Path::new("policies/policy.yaml")).unwrap();
    let strict = inspect(Path::new("policies/attested.yaml")).unwrap();
    assert_eq!(strict["guard_names"], original["guard_names"]);
    assert_eq!(strict["output_hooks"], 1);
    let policy = resolve_from_path("policies/attested.yaml").unwrap();
    let tool_access = policy.rules.unwrap().tool_access.unwrap();
    assert_eq!(tool_access.max_args_size, Some(2048));
    assert_eq!(
        strict["default_scope"]["grants"][0]["constraints"]
            .as_array()
            .unwrap()
            .len(),
        2
    );
    assert_ne!(strict["default_scope"], original["default_scope"]);
}

#[test]
fn empty_allowlist_emits_no_default_grants() {
    let out = inspect(Path::new("policies/blocked.yaml")).unwrap();
    assert_eq!(out["default_scope"], json!({}));
    let policy = resolve_from_path("policies/blocked.yaml").unwrap();
    assert!(compile_policy(&policy)
        .unwrap()
        .default_scope
        .grants
        .is_empty());
    assert_eq!(out["guard_names"], json!(["mcp-tool", "secret-leak"]));
    assert_eq!(out["output_hooks"], 1);
}

#[test]
fn child_rule_replaces_the_entire_parent_block() {
    let parent = HushSpec::parse(include_str!("../policies/base.yaml")).unwrap();
    let child =
        HushSpec::parse("hushspec: '0.1.0'\nrules:\n  tool_access:\n    allow: [count_words]\n")
            .unwrap();
    let merged = chio_policy::merge(&parent, &child);
    assert_eq!(
        merged.rules.unwrap().tool_access.unwrap().max_args_size,
        None
    );
}

#[test]
fn depth_limit_counts_the_starting_document() {
    let error = resolve_from_path_with_limits(
        "policies/policy.yaml",
        ResolveLimits {
            max_document_bytes: 4096,
            max_extends_depth: 1,
        },
    )
    .unwrap_err();
    assert!(error.to_string().contains("maximum depth 1"));
}

#[test]
fn document_size_limit_rejects_before_compilation() {
    let error = resolve_from_path_with_limits(
        "policies/base.yaml",
        ResolveLimits {
            max_document_bytes: 8,
            max_extends_depth: 32,
        },
    )
    .unwrap_err();
    assert!(error
        .to_string()
        .contains("exceeding maximum document size 8"));
}

#[test]
fn invalid_version_is_rejected_by_the_compiler() {
    let policy = resolve_from_path("policies/invalid.yaml").unwrap();
    assert!(compile_policy(&policy)
        .err()
        .unwrap()
        .to_string()
        .contains("unsupported hushspec version"));
}

#[test]
fn missing_tool_access_is_a_wildcard_not_a_denial() {
    let policy = HushSpec::parse("hushspec: '0.1.0'\n").unwrap();
    let compiled = compile_policy(&policy).unwrap();
    assert!(compiled.guard_names.is_empty());
    assert_eq!(compiled.default_scope.grants.len(), 1);
    assert_eq!(compiled.default_scope.grants[0].tool_name, "*");
}

Open the complete source file

Next: test the controls your policy compiles