README.md
agent-policy-engine
A policy engine for Pi. It evaluates tool operations in this order:
- Host prechecks
- Deterministic policy rules
- Decision cache
- LLM review
allow permits an operation. deny blocks it. ask opens the host permission UI. Pi blocks ask when it has no UI.
Install
Pi
pi install /path/to/agent-policy-engine
For development:
bun install
pi -e ./src/pi/index.ts
Reviewer configuration
Pi reads global configuration from $PI_CODING_AGENT_DIR/policy-engine.json, then merges .pi/policy-engine.json from the current project. Project settings override global settings.
Use a Pi-configured model. This uses Pi's provider transport and authentication, including OAuth. It never reads OAuth credentials directly.
{
"reviewer": {
"kind": "pi",
"provider": "openai-codex",
"model": "gpt-5.6-luna",
"reasoningEffort": "minimal",
"promptCacheKey": "policy-engine-v1"
}
}
Pi reviewers accept none, minimal, low, medium, high, xhigh, and max. The selected Pi model determines the supported levels. promptCacheKey is forwarded to Pi as its session ID, which Pi sends to OpenAI as prompt_cache_key for cache routing.
promptCacheKey improves cache routing for Pi models that support it. It does not guarantee a cache hit.
Disable LLM reviews. Requests that reach this stage ask for approval. Static rules, deterministic rules, and cached decisions still apply.
{
"reviewer": {
"kind": "none"
}
}
Use local llama.cpp. This backend does not use authentication.
{
"reviewer": {
"kind": "llama.cpp",
"baseUrl": "http://127.0.0.1:9931",
"model": "reviewer",
"enableThinking": true
}
}
enableThinking defaults to false. llama.cpp always uses the deepseek reasoning format.
LLM review sends the complete tool input to the configured provider. Do not use a cloud reviewer when this is not acceptable.
Pi permissions
Pi uses a root-level tools map and externalDirectories list. Any tool name can be configured. An omitted tool defaults to allow; set a tool to check for LLM review.
{
"tools": {
"bash": "check",
"my_extension_tool": "check"
},
"externalDirectories": ["/tmp/pi"]
}
externalDirectories applies to Pi path tools. Each entry is a non-empty directory path: an external path is allowed only when it is the entry or nested below it by path components. Sibling paths with the same string prefix are not allowed. Its entries are also included in the LLM reviewer prompt with the same semantics.
Pi behavior
Pi keeps /pe-allow-bash, Pi static permissions, and Herdr herdr:blocked events around its confirmation dialog. /pe-skip-permissions skips LLM review for the current extension instance. Static permissions, deterministic rules, and cached decisions still apply; uncached requests allow. Use /pe-skip-permissions clear to disable it. The setting is not restored after an extension reload. Pi bypasses MCP tools at the Pi adapter boundary.
For Bash, Pi applies its session override and static permissions to the raw command. Pi and raw core calls then use the same tree-sitter parser and async deterministic evaluator. Policy checks evaluate each command independently using decoded arguments, assignments, and redirects. Commands joined by ;, &&, ||, pipes, or newlines use the same deterministic rules. Every command must pass for the full input to be allowed. Whole-root and whole-home searches with find, rg, or recursive grep require confirmation, including when the search root defaults to the request's working directory. Path checks and bounded, metadata-only glob expansion use the request's working directory; git -C is handled within its own command. Earlier commands do not change how later commands are checked. This does not model changes to the directory, environment, or filesystem, so it can miss sensitive paths reached through those changes. Unknown expansions and unsupported syntax still fall back to review of the full input. Parse errors cannot receive a deterministic allow. If parser assets are unavailable, Bash is blocked.
Bash cache keys preserve the exact source text and working directory. Cached Bash allows are neither read nor written, so an old filesystem snapshot cannot approve a later request. Cached asks and denies still apply.
PowerShell remains one LLM-reviewed operation.
Development
bun test
bunx eslint src test
bunx tsc --noEmit
Run the opt-in llama.cpp policy evaluation:
POLICY_EVAL_BASE_URL=http://127.0.0.1:9931 POLICY_EVAL_MODEL=reviewer bun test test/core/llm.test.ts
Run the same policy evaluation through Pi OAuth. Pi resolves the configured OAuth session; no API key is needed.
POLICY_EVAL_BACKEND=pi \
POLICY_EVAL_PROVIDER=openai-codex \
POLICY_EVAL_MODEL=gpt-5.6-luna \
POLICY_EVAL_REASONING_EFFORT=minimal \
bun test test/core/llm.test.ts
Pi evaluations using Pi's OpenAI Responses APIs use policy-engine-evaluation-v1 as the cache key; set POLICY_EVAL_PROMPT_CACHE_KEY to override it.
Tests redirect Pi state to a temporary directory, except opt-in Pi OAuth evaluations, which use the existing Pi OAuth session.