What a Policy Permits
Review a policy change by the requests it newly admits, and connect each threat to its boundary and a test that can fail.
On this page
A system that runs agents changes its policy as often as it changes its tools. A new integration needs a domain, a new task needs a directory, and each edit can quietly admit more than its author intended. Chio lets a team review a policy change by the requests it newly admits. chio policy analyze compares a candidate HushSpec policy with the one it replaces and names one request the candidate admits that the old policy denied, or shows that none exists within the fields it analyzes.
That review works because an allowlist names what a policy admits: tool names, egress domains, and path globs. A list of regular expressions names command text to refuse, and the analyzer reports each one as not_analyzed. The questions the analyzer cannot answer have their own evidence: threat records that tie an attack to the boundary that stops it, theorems about delegated authority, and replay of recorded traffic against the candidate.
Review a change by what it newly admits
The requests a policy admits form its admitted set. A candidate refines the old policy when its admitted set lies inside the old one, so the change can only remove requests. Run chio policy analyze new.yaml --against old.yaml and the report states one of three results:
| Refinement status | What the report establishes |
|---|---|
refines | No widening exists in the analyzed fields, and no changed field lies outside them. |
does_ | A refinement_failure finding carries a refinement witness: one request the candidate admits and the old policy denies. The production HushSpec evaluator confirms both decisions before the report carries it. |
inconclusive | A changed field lies outside the analyzed fields. The report records an analysis_incomplete error and never promotes the result to refines. |
The hostile comment in pull request #421 asks the reviewer to upload .env to paste.example.net. An egress allowlist that names api.github.com and *.openai.com refuses that host. Add paste.example.net to egress.allow, for a build-log upload, and the comparison reports does_not_refine. Its witness has the action_type egress and the target paste.example.net, and the finding’s rule_ref points to the new allow entry.
Edits: removed *.openai.com; added paste.example.net. The candidate does not refine; witness egress paste.example.net. Not analyzed: 1 shell pattern.
In Rust · refine.rs:19312 lines
fn confirms(
policy: &HushSpec,
against: &HushSpec,
witness: &Witness,
budget: &mut AnalysisBudget,
) -> Result<bool, AnalysisError> {
budget.consume_evaluator_confirmation()?;
Ok(
evaluate(policy, &action(witness)).decision != Decision::Deny
&& evaluate(against, &action(witness)).decision == Decision::Deny,
)
}
not_analyzed.The report names one witness, from the first widening its search reaches: tool access, argument size, egress, file paths, then computer-use and input actions. Removing **/.env.* alone yields file_read on /.env., the shortest path that only the removed pattern denied. Fix one widening and run the comparison again to find the next.
Find rules that never decide
Long policies collect entries that no longer change a decision. Run the analyzer on one policy, without --against, and it compares the entries within each block:
| Finding | What it means |
|---|---|
shadowed_, warning | An entry lies inside an entry of the opposite effect that wins. In tool_access and egress a block entry wins; in forbidden_paths an exception wins. |
unreachable_, warning | An earlier entry with the same effect already covers it. |
contradiction, error | An allow entry and a block entry match exactly the same inputs, or explicit entries cover every input so the default never applies. |
ambiguous_, notice | Entries with different effects partly overlap. |
hushspec: "0.1.0"
rules:
tool_access:
allow: ["pricing.quote"]
block: ["pricing.**"]
default: blockHere pricing.** matches pricing.quote, and a block entry wins, so the analyzer reports allow entry 0 as a shadowed_rule. The exit code is 0 when no finding reaches the --fail-on threshold (default warning), 1 when one does, and 2 when loading, validation, or a bounded analysis limit fails. Each not_analyzed entry counts as a notice. The JSON report uses the schema chio.policy-analysis.v1.
What the analyzer decides
The analyzer resolves inheritance, lowers each rule block into sets of glob patterns, numeric bounds, finite values, and flags, and compares them. Globs use the evaluator’s own semantics for *, **, and ?. It lists every configured field outside that fragment in not_analyzed, with a reason.
| Rule block | How the analyzer treats it |
|---|---|
tool_ | Decides allow, block, default, and max_args_size. Runtime attestation, workload identity, and require_confirmation are not_analyzed. |
egress, forbidden_, path_ | Decides domain and path globs, including read, write, and patch allowlists. |
computer_, remote_, input_ | Decides allowed actions, channel flags, and input types as finite sets. |
shell_, secret_, patch_ | Lists each regex as not_analyzed, a regex-valued predicate. |
velocity, human_, browser_, code_, extensions | Lists rate windows, approval workflows, and guard-only predicates as not_analyzed. |
The three-layer policy from Inherit and Merge Policies analyzes clean. Its two notices are the parts of the velocity rate window it cannot decide:
$ chio policy analyze ./policies/env/prod.yamlpolicy_sha256 e9f82ee3bfb7830a2770fe9521b9de69ac06c54e1e953c5826715890a414428b
ID SEVERITY KIND BLOCK RULE
- notice not_analyzed velocity window
stateful rate-window predicate
- notice not_analyzed velocity burst_factor
stateful rate-window predicate
summary: 0 error(s), 0 warning(s), 2 notice(s)The analyzer reads one HushSpec policy and judges one request at a time. WASM guards loaded beside the policy, session rules over sequences of calls, and information-flow labels lie outside it. A changed regex makes a comparison inconclusive even when the edit only adds a pattern: adding curl to a list that holds rm -rf refuses more commands, and the analyzer still cannot show it.
An allowlist names the requests a policy admits, so a program can compare two allowlists and produce a member of their difference. A pattern list names spellings to refuse. Whether those spellings cover every command with the same effect depends on what the commands do, and no comparison of two lists decides it. Beyond Blocklists sets out the boundaries that hold where a list cannot.
Tie each threat to a test that fails
Refinement shows that a change admits nothing new. Whether the old policy stops a given attack is a separate claim with separate evidence. A threat coverage record names the threat, the boundary that stops it, a counterexample, the mutations its test caught or missed, and the scope that remains outside the claim.
The tool_server_escape record is one example. A capability permits read_public on admitted-server. The attacker changes the tool to exec_privileged or the server to pivoted-server, and both registered connections accept every invocation, so only the kernel’s scope check can refuse. Mutation runs forced grant_matches_request in chio-kernel and grant_covers in chio-kernel-core to return Ok(true), and the test caught both. The record’s status is partial: it names WASM mutation evidence, authenticated process transport, and OS or container isolation as outside its scope.
Write new records the same way. Keep a valid neighboring request working, vary only the attacker’s input, observe the protected resource rather than the returned message, and break the defense on purpose to confirm the test fails. Adversarial testing covers the techniques, and behavioral gates shows which checks run in continuous integration and which block merging.
Proofs about delegated authority
A policy narrows what a grant permits, and the grant bounds what any policy can reach. Lean proves the properties that keep that bound, over bounded models of scopes and delegation steps:
| Theorem | Statement |
|---|---|
Chio. | When a child scope is a subset of its parent, every grant in the child is covered by a grant in the parent (P1, capability attenuation). |
Chio. | In a presented delegation chain, every grant at a link is covered by a grant at the link before it (P5). |
Chio. | An admitted delegation step narrows its parent. |
Chio. | A step past the maximum delegation depth is denied. |
Each theorem holds over its model and the assumptions the proof manifest lists. Formal Assurance traces which production functions connect to them. The policy analyzer is executable static analysis with its own bounds; the proof manifest defines the formal boundary.
Replay recorded traffic against a candidate
The analyzer covers every request in its fields. Replay covers the requests that reached the kernel, including those that shell regexes decide. Give chio replay traffic a capture with --from, the tenant key with --tenant-pubkey, and the candidate with --against. It authenticates the captured frames, evaluates each recorded invocation under the candidate in pre-output mode, and compares verdict, guard, and reason with the capture. A difference exits 10. No tool runs again.
A captured frame keeps neither the original tool-server ID nor the response bytes, so this mode rejects policies with concrete-server grants or post-invocation hooks. It also leaves the original identity, delegated authority, and runtime budget unreconstructed. Determinism and replay walks through a capture and a candidate comparison.
Choose the check by the question
| Question | Check and evidence |
|---|---|
| Does this change admit anything new? | chio policy analyze new.yaml --against old.yaml: a refinement status and, when it widens, a confirmed witness. |
| Which rules never change a decision? | chio policy analyze policy.yaml: shadowed, unreachable, and contradictory entries. |
| Does a boundary stop a named attack? | A threat coverage record: the counterexample and the mutants its test caught. |
| Can a delegate exceed its parent? | The Lean delegation theorems over the bounded model. |
| Would recorded traffic be decided differently? | chio replay traffic --against: verdict, guard, and reason drift. |