prae-cli
v0.10.25
Published
Operator policy for agent harnesses: validate packs, explain a decision, test a pack against security invariants, verify a decision log, and provision a governed DSH profile.
Maintainers
Readme
prae-cli
Command line for prae-gate:
validate an operator policy pack, ask it why a call would be refused, verify a
decision log, and provision a governed profile.
npx prae explain ./acme-baseline.prae.json write path=/etc/hostsDENIED write (fs:write)
PRAE-001 · writes outside ./src, ./tests, and ./docs are not granted in this pack
PRAE-001 deny writes outside ./src, ./tests, and ./docs are not granted in this pack
acme-baseline 1.0.0 · 6 rules consideredCommands
prae check <pack.json>
Validate a pack and summarise what it enforces. Unknown fields are errors, so a mistyped matcher fails here rather than loading clean and enforcing nothing.
prae explain <pack.json> <tool> [key=value ...]
Judge one call and print the trace. Nothing is executed and no harness is involved — "why was this denied" is the first question anyone asks of a policy engine, and answering it should not require running an agent.
It also says when no capability claims the named tool, so a reader cannot infer coverage that is not there.
prae scan <file> [--pack <pack.json>] [--json]
Inspect a file for credentials the way the gate does before a read: provider
formats plus entropy, placeholders dropped. Findings name the type, rule,
line and confidence — structurally never the secret, so the output is safe to
paste anywhere. With --pack, also prints the verdict that pack would reach.
prae test <suite.json> [--pack <pack.json>] [--json]
Run security invariants against a pack and exit non-zero if any stops holding. A suite says what an agent attempts and what the pack must decide about it:
{
"pack": "./acme.prae.json",
"scenarios": [
{
"name": "Reads outside the workspace are refused",
"given": "the agent is scoped to ./src and ./docs",
"when": { "tool": "read", "args": { "file_path": "/etc/shadow" } },
"then": { "decision": "deny", "rule": "ACME-001" }
}
]
}then also takes secretTypes, asserting which credential classes the
inspector found before the verdict — so a rule about secrets is tested on the
detection as well as the decision. when.repeat: 8 asserts the eighth call of
that capability, which is how a session budget is written down and how the gate
counts it.
A failure prints the invariant in full — given, when, then, and what it got
instead — because the useful thing about a red security test is the sentence
describing what was supposed to hold. --pack overrides the suite's own, which
is how one suite is run against a candidate pack before it is adopted.
This evaluates the same pack through the same evaluator and the same content
inspector the gate uses at runtime. It is not the harness: the adapter that
turns a live tool call into facts is covered by prae-gate's own suite.
prae mcp --root <dir> [--pack <pack.json>] [--ledger <file>]
Serve gated filesystem access over MCP stdio. Any MCP-capable IDE or agent
mounts this as its file tool; every read_file is inspected before its
contents are returned, refusals are structured JSON with "retry": false,
and every decision lands in the ledger. Reads are confined to --root. With
no --pack, the bundled secret-catcher pack mounts. See
docs/secret-catcher.md for client configuration.
prae verify <ledger.jsonl>
Re-derive every hash in a decision log and report any break. Prints the limit next to the result rather than in a footnote: a removed, edited, or reordered row breaks the chain; a rewrite of the whole file by someone holding this code does not.
prae report <ledger.jsonl> [--out <file.html>] [--pack <pack.json>]
Render the decision record as a printable evidence report. With --pack,
each clause also names the framework risk the rule cites — OWASP Top 10 for
LLM Applications 2026 today — so a reviewer reads risk → control → decision →
seal in one row. Without the pack the clause id stands alone rather than a
name guessed from it.
prae console <ledger.jsonl> --out <dir> [--pack <file>]
Write the control plane as linked pages: Overview, Policy packs, Playground,
Audit ledger, Compliance map. The Playground is written only when --pack is
given, since an empty one teaches nothing.
The Playground inlines the real kernel and runs it in the page, so a rule that denies there denies in a live profile. A playground running an approximation would be worse than none — it would teach a policy language that does not exist, and the difference would surface at the worst moment.
Static files rather than a server. The views are a read of local evidence, and a governance tool that opens a port to show you its own audit trail has added an attack surface to solve a rendering problem.
The Fleet view the rail names is shown dim and marked SOON. It needs a
control plane that does not exist yet, and a working-looking Fleet page over
one machine would be a lie told in the most expensive place to tell one.
The Compliance map states what each control asks for against what the profile actually produces — evidence toward a control, never compliance with it. Whether that evidence satisfies the obligation is an auditor's judgement.
prae init [options]
Compose the overlay row that mounts the plugin with a pack.
| Flag | Meaning |
| --- | --- |
| --pack <file> | The policy pack to embed. Required. |
| --profile <dir> | Profile directory. Defaults to $DSH_HOME/profiles/web. |
| --ledger <file> | Where decisions are appended. |
| --approver <name> | Account this profile runs as, recorded against approvals. |
| --apply | Actually write. Without it, the composed overlay is printed and nothing changes. |
It defaults to not writing, because the file it edits decides whether the
harness boots at all. A row naming a package the profile cannot resolve is not a
disabled feature — Cordis fails module resolution during boot, the harness does
not start, and every tool you would debug it with is gone. So init refuses
outright when prae-gate is not installed in the profile, snapshots the
file before writing, and checks the overlay has not changed since you reviewed
the diff.
None of that safety is reimplemented here: it comes from
dsh-overlay-check, which was
hardened against a real gate review — including a symlink escape that lexical
containment let through.
The pack is embedded in the row rather than referenced by path. A row pointing at a file that later moves is a policy nobody can reconstruct from the profile alone, and reconstructing what was in force is the entire point.
Status
Pre-release. Do not pin a production system to it.
Licence
MIT.
