omp packages

pi-openappa0.3.3

Thin OpenAPPA guard extension for Pi: gates tool calls through the APPA runtime. No policy logic lives here.

aemonge Published 6 days ago MIT ~572 downloads/mo

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.

Did it work for you?

Result

One report per person. A new report replaces your old one.

Reports 0

No reports yet.

Readme

pi-openappa

npm version CI license: MIT

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 appa binary on PATH: curl -fsSL https://openappa.com/install.sh | sh (the extension installs it automatically when missing) — version 0.31.x verified; see docs/wire-notes.md for 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 init writes a starter one, or copy templates/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 (and APPA_GATE=1 pi to force it on).
  • Per project: create <project>/.pi/no-openappa — sessions started in that directory run unguarded.
  • Globally: /appa off once (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 PreToolUse before 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 rewrites event.input after 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_CMD are 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-install appa or pin APPA_INSTALL_CMD to avoid it.
  • The adapter never starts a runtime for opted-out sessions; auto-start needs either APPA_CONFIG or an installed APPA deployment.
  • OpenAPPA is Preview & RFC: wire surfaces may break without shims. The entire wire contract lives in src/hook-client.ts and src/adapter.ts (verified facts in docs/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.