pi-openappa0.3.3
Thin OpenAPPA guard extension for Pi: gates tool calls through the APPA runtime. No policy logic lives here.
Untested on omp
This is a pi package. No one has reported whether it works on omp yet. If you try it, add your result below.
Reports 0
No reports yet.
Readme
pi-openappa
A thin OpenAPPA guard extension for Pi: every tool call in a protected session is checked by the APPA runtime before it runs, and every tool result before the model sees it. No policy logic lives here — the APPA runtime owns every decision; this extension only translates events and enforces the answer.
pi event ──▶ adapter ──▶ `appa hook` ──▶ APPA runtime
allow / deny / replace
pi event ◀── enforce ◀── decision ◀────────────┘
Requirements
- The
appabinary onPATH:curl -fsSL https://openappa.com/install.sh | sh(the extension installs it automatically when missing) — version 0.31.x verified; seedocs/wire-notes.mdfor the recorded contract - An APPA runtime listening on loopback (default
127.0.0.1:8787) - A policy (
appa.toml) that declares the tools your sessions may use —/appa initwrites a starter one, or copytemplates/appa.toml
Install
pi install npm:pi-openappa # published on npm; indexed by the Pi gallery
pi install ./pi-openappa # from a checkout
Smoke test
From any checkout, everything self-verifies — no runtime, no install needed:
git clone https://github.com/aemonge/pi-openappa && cd pi-openappa
just check # tests + typecheck + strict lint + the three policy gates
Then live, from anywhere:
pi install npm:pi-openappa # published on npm; indexed by the Pi gallery
pi # any session: protection on, appa auto-installs
appa --version # OK — the runtime is on PATH
pi install only registers the package — extension code runs when a session
starts, so appa appears after that first session, not before.
Protect sessions
Protection is on by default: every Pi session is guarded unless you opt out. Opt-outs, most specific first:
- Per launch:
APPA_GATE=0 pi(andAPPA_GATE=1 pito force it on). - Per project: create
<project>/.pi/no-openappa— sessions started in that directory run unguarded. - Globally:
/appa offonce (marker~/.config/pi-openappa/off) — every session everywhere runs unguarded until/appa on.
The project marker <project>/.pi/openappa names that project's policy
(absolute or cwd-relative) and re-enables protection even when globally off;
empty content falls back to APPA_CONFIG or APPA's default:
cd your-project && mkdir -p .pi && echo "appa.toml" > .pi/openappa
A protected session brings the runtime up on its own: session_start sends
appa hook --ensure-runtime (passing --config when a policy is resolved),
but does not wait for it — the boot runs on a serialized background lane,
so a cold runtime, a first-run auto-install, or a slow start never sits in
Pi's session-start path. Ordering is preserved: the SessionStart payload
always lands before the first PreToolUse, and a runtime that cannot
answer fails that first call closed with the reason (silence never means
yes). A custom
APPA_RUNTIME_URL names a runtime that is yours to start — the hook
refuses with exactly that reason instead of guessing.
When the default appa is missing from PATH, a protected session installs
it itself — once, with a UI notice — by running the same official script
(curl -fsSL https://openappa.com/install.sh | sh), then retries starting
the runtime. Sessions that name a custom APPA_HOOK_BIN are never
auto-installed.
While protected, a runtime that cannot answer blocks the call and the reason
is returned to the model — silence never means yes; /appa off always
works, even with every tool call blocked. One exception: with no policy
anywhere (APPA_CONFIG, marker content, and ~/.config/appa/appa.toml all
absent) and no runtime answering, the session runs unprotected with one
startup warning naming the fixes — an unconfigured guard must not lock you
out of your own machine. A named policy that fails stays fail-closed.
Opted-out sessions never invoke the hook.
Configuration
| Source | Default | Meaning |
|---|---|---|
APPA_GATE |
unset | 1 forces protection on for this launch; 0 forces it off |
.pi/openappa |
absent | Project marker: names that project's policy and forces protection on |
.pi/no-openappa |
absent | Project opt-out: sessions started there run unguarded |
~/.config/pi-openappa/settings.json |
absent | File settings: config, runtimeUrl, hookBin, hookTimeoutMs; env always wins, project markers beat config |
APPA_RUNTIME_URL |
http://127.0.0.1:8787 |
Runtime endpoint (loopback only) |
APPA_CONFIG |
unset | appa.toml the session auto-starts the runtime with |
APPA_HOOK_BIN |
appa |
Hook binary to invoke |
APPA_INSTALL_CMD |
curl -fsSL https://openappa.com/install.sh | sh |
Auto-install command for a missing default appa (pin a mirror or offline copy) |
APPA_HOOK_TIMEOUT_MS |
15000 |
Kill the hook after this long; the call is then blocked |
APPA_INSTALL_TIMEOUT_MS |
120000 |
Kill a stuck auto-install after this long |
Policy resolution order: APPA_CONFIG, then the project marker's content,
then settings.json's config, then APPA's own default
(~/.config/appa/appa.toml).
Status line
Protected sessions publish a footer status under the openappa key
(ctx.ui.setStatus): openappa starting while the runtime boots, then
openappa ok · gated — or openappa off · protection opted out,
openappa unreachable · <url> / · hook timeout, openappa failing · <reason>,
or openappa needs attention · no policy, session unprotected. Status surfaces
that render extension statuses (Pi's footer, pi-stardeck's shield chip) read
this text; severity is word-matched downstream, so healthy texts deliberately
avoid words like fail/down/unreachable — and silence renders as an
error, which is why the extension always publishes. The full state table,
refresh points, and the renderer contract: docs/status-line.md.
Starter policy: /appa init
templates/appa.toml declares every known Pi tool (deny-undeclared is the
one live tooth today) and carries the full fence map — the sink rules
that refuse calls from narrowed sessions: edits to existing code (docs
excepted), writes to tests/devops/infra/credential paths, git push and
gh …, memory writes, and the policy's own files.
The fences are armed but remedy-gated. The live runtime
confirmation-gates every label narrowing: the call blocks and offers a
remedy plan, accepted through the appa/execute_remedy_plan MCP tool —
which Claude Code mounts via its plugin and Pi does not have yet.
Until 0.4 exposes it as a Pi tool, the source deltas are stripped (an
unacceptable narrowing would block web research instead of labeling it),
so nothing narrows and the sinks stay inert: sessions flow everywhere
except undeclared tools. The fence map — the exact 0.4 target — is
documented at the top of templates/appa.toml, and the integration tier
pins today's truth so the change flips deliberately.
Selectors use Pi argument names (Read(path:…), not Claude Code's
file_path), and the policy is static rules only: nothing host-coupled,
nothing that can be unreachable.
Install it one of three ways:
/appa init # inside Pi: writes ~/.config/appa/appa.toml (+ settings.json)
/appa init project # writes ./appa.toml and the .pi/openappa marker
cp templates/appa.toml ~/.config/appa/appa.toml # from a checkout
init never clobbers: rerun with --force to overwrite. Subcommands
autocomplete (on, off, init, init project, status).
Policy-as-code
The policy is tested like code — no runtime needed, only the appa CLI:
just policy-check # templates/appa.toml loads
just policy-coverage # every known Pi tool declared (Day E: refusals fail)
just policy-test # replay the usage-day traces (Days A–D, F)
traces/*.appa are line-based replays (<canonical-tool> { / one arg:
JSON value per line / } / expect allow|withhold|deny); calls in one
file share a trajectory, so taint accumulates exactly as live. All three
gates run in just check.
/appa reports protection, opt-out state, and runtime health; /appa off
disables protection globally (marker ~/.config/pi-openappa/off) and
/appa on re-enables it, taking effect immediately including the
current session.
Event mapping
| Pi event | APPA event | Effect |
|---|---|---|
session_start |
SessionStart |
opens the trajectory; warns when the runtime is down |
before_agent_start |
UserPromptSubmit |
turn boundary (never gates) |
tool_call |
PreToolUse |
deny blocks the call; the reason reaches the model |
tool_result |
PostToolUse |
replace swaps the result the model sees |
turn_end |
Stop |
reported (--turn-end), never gates |
Pi built-in tool names map to Claude Code policy names (bash→Bash,
find→Glob, …); custom and MCP tools pass through under their own names and
policies declare them verbatim.
Limitations
- Remedy-gated narrowing (until 0.4): the runtime blocks every label
change until a remedy plan is accepted via
appa/execute_remedy_plan— an MCP tool Pi does not mount. The starter policy therefore ships with source deltas stripped (see the starter-policy section); the fence map lights up when the extension exposes the remedy tool. - Selectors match command text as written: a selector like
Bash(command:*.env*)matches any command mentioning that substring — including comments, docstrings, or heredocs that discuss credentials. Fail-closed, benign, and occasionally surprising (this repo's own policy work once tripped it). - One open call per host: the runtime permits one unanswered tool call
per session at a time; a second
PreToolUsebefore the previous call's result is refused (…must propose one unidentified call at a time). Parallel tool batches therefore degrade to their first call under gating. - Load order matters in theory: the runtime digests tool arguments at
PreToolUse. If another extension rewritesevent.inputafter this handler runs, the executed arguments differ from the checked ones and the runtime withholds the result (byte_mismatch). Load pi-openappa last if you combine it with argument-rewriting extensions. - Policy edits reach new sessions only: trajectories keep the policy they opened with, by runtime design. When iterating on a policy, restart the runtime (and start fresh sessions) — a stale runtime or an old trajectory will keep serving the old policy.
- Subagents: spawning is mediated as a plain tool call (deny blocks the spawn). Child trajectories are not linked into the parent's label chain yet; a gated child Pi process opens its own root trajectory.
- On by default: installing this extension guards every session and may
download and run openappa.com's install script once on first run. The
opt-outs above and
APPA_INSTALL_CMDare the escapes. - Auto-install is
curl \| sh: a protected session with the default binary missing downloads and runs the script with user privileges, at most once per session start. Pre-installappaor pinAPPA_INSTALL_CMDto avoid it. - The adapter never starts a runtime for opted-out sessions; auto-start needs
either
APPA_CONFIGor an installed APPA deployment. - OpenAPPA is Preview & RFC: wire surfaces may break without shims. The
entire wire contract lives in
src/hook-client.tsandsrc/adapter.ts(verified facts indocs/wire-notes.md), and the adapter core is runtime-agnostic so a pi-durable wiring can reuse it unchanged.
Development
just check # tests + typecheck + the three policy gates
npm test # node --test, mock-driven, no runtime needed
npm run typecheck
The adapter core (src/adapter.ts) has no Pi imports by design; the Pi
wiring is extensions/index.ts only. Contribution ground rules — including
the trace-or-don't-merge policy for changes to templates/appa.toml — are
in CONTRIBUTING.md. Notable changes per release live in
CHANGELOG.md.
Security
Suspected vulnerabilities are reported privately via GitHub security advisories — never as public issues. Scope and expectations: SECURITY.md.
Supply-chain pinning (if you do not want the runtime auto-installed from
the network): pre-install appa and point APPA_HOOK_BIN at a checksummed
binary path, and set APPA_INSTALL_CMD to an offline copy or a mirror you
control — auto-install only ever runs when the default appa is missing,
once, using that exact command. Destructive-command safety (rm -rf,
sudo, …) is out of the policy's scope by design: the OS, backups, and you
remain that guard.
License
MIT — see LICENSE.