pi-openappa 0.0.0-stage → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +120 -2
- package/docs/wire-notes.md +80 -0
- package/extensions/index.ts +183 -0
- package/justfile +32 -0
- package/package.json +28 -4
- package/src/adapter.ts +284 -0
- package/src/gate.ts +135 -0
- package/src/hook-client.ts +133 -0
- package/test/adapter.test.ts +207 -0
- package/test/extension.test.ts +447 -0
- package/test/fixtures/mock-appa.mjs +104 -0
- package/tsconfig.json +16 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 aemonge
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,121 @@
|
|
|
1
|
-
#
|
|
1
|
+
# pi-openappa
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
A thin [OpenAPPA](https://openappa.com) guard extension for
|
|
4
|
+
[Pi](https://pi.dev): every tool call in a protected session is checked by the
|
|
5
|
+
APPA runtime before it runs, and every tool result before the model sees it.
|
|
6
|
+
No policy logic lives here — the APPA runtime owns every decision; this
|
|
7
|
+
extension only translates events and enforces the answer.
|
|
8
|
+
|
|
9
|
+
```text
|
|
10
|
+
pi event ──▶ adapter ──▶ `appa hook` ──▶ APPA runtime
|
|
11
|
+
allow / deny / replace
|
|
12
|
+
pi event ◀── enforce ◀── decision ◀────────────┘
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Requirements
|
|
16
|
+
|
|
17
|
+
- The `appa` binary on `PATH` ([install](https://openappa.com)) — version
|
|
18
|
+
0.31.x verified; see `docs/wire-notes.md` for the recorded contract
|
|
19
|
+
- An APPA runtime listening on loopback (default `127.0.0.1:8787`)
|
|
20
|
+
- A policy (`appa.toml`) that declares the tools your sessions may use
|
|
21
|
+
|
|
22
|
+
## Install
|
|
23
|
+
|
|
24
|
+
```sh
|
|
25
|
+
pi install npm:pi-openappa # once published
|
|
26
|
+
pi install ./pi-openappa # from a checkout
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Protect sessions
|
|
30
|
+
|
|
31
|
+
Protection is opt-in, in one of three ways:
|
|
32
|
+
|
|
33
|
+
- **Project-scoped (recommended):** create `<project>/.pi/openappa` — sessions
|
|
34
|
+
started in that directory are protected, sessions elsewhere are not. The
|
|
35
|
+
marker's optional content names that project's policy (absolute or
|
|
36
|
+
cwd-relative); empty content falls back to `APPA_CONFIG` or APPA's default:
|
|
37
|
+
|
|
38
|
+
```sh
|
|
39
|
+
cd your-project && mkdir -p .pi && echo "appa.toml" > .pi/openappa
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
- **Built-in:** run `/appa on` once — every Pi session everywhere is
|
|
43
|
+
protected. `/appa off` disables.
|
|
44
|
+
- **Per launch:** `APPA_GATE=1 pi` (the `clappa`-style launcher route).
|
|
45
|
+
|
|
46
|
+
A gated session brings the runtime up on its own: `session_start` invokes
|
|
47
|
+
`appa hook --ensure-runtime`, passing `--config "$APPA_CONFIG"` when set and
|
|
48
|
+
otherwise letting APPA use its own default policy (`~/.config/appa/appa.toml`).
|
|
49
|
+
Protected sessions therefore need zero manual server management. A custom
|
|
50
|
+
`APPA_RUNTIME_URL` names a runtime that is *yours* to start — the hook
|
|
51
|
+
refuses with exactly that reason instead of guessing.
|
|
52
|
+
|
|
53
|
+
While gated, a runtime that cannot answer blocks the call and the reason is
|
|
54
|
+
returned to the model — **silence never means yes**. If no policy exists the
|
|
55
|
+
startup warning names the exact outs; `/appa off` always works, even with
|
|
56
|
+
every tool call blocked. Ungated sessions never invoke the hook.
|
|
57
|
+
|
|
58
|
+
## Configuration
|
|
59
|
+
|
|
60
|
+
| Variable | Default | Meaning |
|
|
61
|
+
|---|---|---|
|
|
62
|
+
| `APPA_GATE` | unset | `1` protects this session (read once at launch) |
|
|
63
|
+
| `.pi/openappa` | absent | Project marker: gates sessions started in that directory; optional content = policy path |
|
|
64
|
+
| `APPA_RUNTIME_URL` | `http://127.0.0.1:8787` | Runtime endpoint (loopback only) |
|
|
65
|
+
| `APPA_CONFIG` | unset | `appa.toml` the session auto-starts the runtime with |
|
|
66
|
+
| `APPA_HOOK_BIN` | `appa` | Hook binary to invoke |
|
|
67
|
+
| `APPA_HOOK_TIMEOUT_MS` | `15000` | Kill the hook after this long; the call is then blocked |
|
|
68
|
+
|
|
69
|
+
`/appa` reports protection, always-on state, and runtime health; `/appa on`
|
|
70
|
+
and `/appa off` toggle always-on protection (marker:
|
|
71
|
+
`~/.config/pi-openappa/always-on`), taking effect immediately including the
|
|
72
|
+
current session.
|
|
73
|
+
|
|
74
|
+
## Event mapping
|
|
75
|
+
|
|
76
|
+
| Pi event | APPA event | Effect |
|
|
77
|
+
|---|---|---|
|
|
78
|
+
| `session_start` | `SessionStart` | opens the trajectory; warns when the runtime is down |
|
|
79
|
+
| `before_agent_start` | `UserPromptSubmit` | turn boundary (never gates) |
|
|
80
|
+
| `tool_call` | `PreToolUse` | deny blocks the call; the reason reaches the model |
|
|
81
|
+
| `tool_result` | `PostToolUse` | replace swaps the result the model sees |
|
|
82
|
+
| `turn_end` | `Stop` | reported (`--turn-end`), never gates |
|
|
83
|
+
|
|
84
|
+
Pi built-in tool names map to Claude Code policy names (`bash`→`Bash`,
|
|
85
|
+
`find`→`Glob`, …); custom and MCP tools pass through under their own names and
|
|
86
|
+
policies declare them verbatim.
|
|
87
|
+
|
|
88
|
+
## Limitations
|
|
89
|
+
|
|
90
|
+
- **Load order matters in theory**: the runtime digests tool arguments at
|
|
91
|
+
`PreToolUse`. If another extension rewrites `event.input` after this
|
|
92
|
+
handler runs, the executed arguments differ from the checked ones and the
|
|
93
|
+
runtime withholds the result (`byte_mismatch`). Load pi-openappa last if
|
|
94
|
+
you combine it with argument-rewriting extensions.
|
|
95
|
+
- **Policy edits reach new sessions only**: trajectories keep the policy they
|
|
96
|
+
opened with, by runtime design. When iterating on a policy, restart the
|
|
97
|
+
runtime (and start fresh sessions) — a stale runtime or an old trajectory
|
|
98
|
+
will keep serving the old policy.
|
|
99
|
+
- **Subagents**: spawning is mediated as a plain tool call (deny blocks the
|
|
100
|
+
spawn). Child trajectories are not linked into the parent's label chain
|
|
101
|
+
yet; a gated child Pi process opens its own root trajectory.
|
|
102
|
+
- The adapter never starts a runtime for ungated sessions; auto-start needs
|
|
103
|
+
either `APPA_CONFIG` or an installed APPA deployment.
|
|
104
|
+
- OpenAPPA is Preview & RFC: wire surfaces may break without shims. The
|
|
105
|
+
entire wire contract lives in `src/hook-client.ts` and `src/adapter.ts`
|
|
106
|
+
(verified facts in `docs/wire-notes.md`), and the adapter core is
|
|
107
|
+
runtime-agnostic so a pi-durable wiring can reuse it unchanged.
|
|
108
|
+
|
|
109
|
+
## Development
|
|
110
|
+
|
|
111
|
+
```sh
|
|
112
|
+
npm test # node --test, mock-driven, no runtime needed
|
|
113
|
+
npm run typecheck
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
The adapter core (`src/adapter.ts`) has no Pi imports by design; the Pi
|
|
117
|
+
wiring is `extensions/index.ts` only.
|
|
118
|
+
|
|
119
|
+
## License
|
|
120
|
+
|
|
121
|
+
MIT
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
# Wire notes: `appa hook` contract (verified 2026-10-06, appa 0.31.1)
|
|
2
|
+
|
|
3
|
+
All facts below were probed live against `appa runtime` on 127.0.0.1:8791 with a
|
|
4
|
+
minimal one-tool policy. Probes: SessionStart, UserPromptSubmit, PreToolUse (allow,
|
|
5
|
+
undeclared-refuse), PostToolUse (byte-mismatch withhold), Stop --turn-end, plus
|
|
6
|
+
ungated and runtime-down probes.
|
|
7
|
+
|
|
8
|
+
## Transport
|
|
9
|
+
|
|
10
|
+
- Subprocess: `appa hook` (env inherited; gate and URL read from env)
|
|
11
|
+
- Gate: inert unless `APPA_GATE=1` — ungated always exits 0, no side effects
|
|
12
|
+
- URL: `APPA_RUNTIME_URL` (default `127.0.0.1:8787`, loopback only)
|
|
13
|
+
- Turn-end: pass `--turn-end` (non-blocking report; exit 0 even when down)
|
|
14
|
+
- Payload: Claude Code hook JSON on stdin
|
|
15
|
+
|
|
16
|
+
## Payload fields (accepted and observed)
|
|
17
|
+
|
|
18
|
+
- `session_id` — becomes trajectory `cc:<session_id>` in the runtime
|
|
19
|
+
- `hook_event_name` — `SessionStart` | `UserPromptSubmit` | `PreToolUse` |
|
|
20
|
+
`PostToolUse` | `Stop`
|
|
21
|
+
- `source` (SessionStart): `startup` | `resume`
|
|
22
|
+
- `prompt` (UserPromptSubmit)
|
|
23
|
+
- `tool_name`, `tool_input` (PreToolUse / PostToolUse)
|
|
24
|
+
- `tool_response` (PostToolUse)
|
|
25
|
+
- `cwd`
|
|
26
|
+
|
|
27
|
+
## Decision protocol (observed)
|
|
28
|
+
|
|
29
|
+
- Exit 0, no stdout (or `{}`) → proceed. SessionStart/UserPromptSubmit behave so.
|
|
30
|
+
- Exit 0 + `{"hookSpecificOutput":{"hookEventName":"PreToolUse",
|
|
31
|
+
"permissionDecision":"allow","permissionDecisionReason":"..."}}` → proceed.
|
|
32
|
+
- Exit 2 + stderr `OpenAPPA hook blocked: <reason>` → block; reason goes to the
|
|
33
|
+
model. Observed for an undeclared tool (HTTP 409 upstream, stdout carries
|
|
34
|
+
`{"error": "<reason>"}`).
|
|
35
|
+
- Exit 0 + `{"decision":"block","reason":"...",
|
|
36
|
+
"hookSpecificOutput":{"hookEventName":"PostToolUse","updatedToolOutput":{...}}}`
|
|
37
|
+
→ PostToolUse replacement: `updatedToolOutput` is what the model must see
|
|
38
|
+
(observed: withheld-result notice replacing a mismatched result).
|
|
39
|
+
- Runtime down: PreToolUse exit 2 (fail-closed, stderr names 127.0.0.1:8787);
|
|
40
|
+
Stop --turn-end exit 0 + stderr warning.
|
|
41
|
+
|
|
42
|
+
## Hard constraints
|
|
43
|
+
|
|
44
|
+
1. **Byte-exact echo**: PostToolUse `tool_input` must be byte-identical to the
|
|
45
|
+
PreToolUse `tool_input` (runtime digests it canonically; mismatch → result
|
|
46
|
+
withheld with `byte_mismatch`). The extension must remember the exact object
|
|
47
|
+
it sent per tool call id.
|
|
48
|
+
2. **Tool naming**: the runtime sees `host/claude-code/<Name>`; policies are
|
|
49
|
+
written in Claude-Code naming. Map pi built-ins: bash→Bash,
|
|
50
|
+
powershell→PowerShell, read→Read, edit→Edit, write→Write, grep→Grep,
|
|
51
|
+
find→Glob, ls→LS. Unknown/custom tools pass through verbatim; policy authors
|
|
52
|
+
declare them under the pi name.
|
|
53
|
+
3. **tool_response shape**: Claude-Code-shaped for tools where pi provides the
|
|
54
|
+
data (bash: `stdout`/`stderr`/`exitcode`/`interrupted` from details when
|
|
55
|
+
present); otherwise generic `{ "output": <joined text>, "isError": <bool> }`.
|
|
56
|
+
|
|
57
|
+
## Session mapping
|
|
58
|
+
|
|
59
|
+
pi `session_start.reason`: `startup|new` → `startup`; `resume|fork|reload` →
|
|
60
|
+
`resume`.
|
|
61
|
+
|
|
62
|
+
## Not yet observed (client parses defensively)
|
|
63
|
+
|
|
64
|
+
- `permissionDecision: "deny"` exact shape (a declared-but-denied call)
|
|
65
|
+
- `deliver_value` / remedy offers on PreToolUse
|
|
66
|
+
- Subagent (`SubagentStop`, child trajectories) — out of v1 scope
|
|
67
|
+
|
|
68
|
+
## Auto-start (verified 2026-10-06)
|
|
69
|
+
|
|
70
|
+
- `appa hook --ensure-runtime --config <path>` with nothing on the default
|
|
71
|
+
port boots a runtime on `127.0.0.1:8787`; the hook exits 0.
|
|
72
|
+
- `--ensure-runtime` with a custom `APPA_RUNTIME_URL` exits 2:
|
|
73
|
+
`the runtime could not be started: nothing answers <url>, and a runtime at
|
|
74
|
+
a URL the session named is the user's own to start` — custom URLs are
|
|
75
|
+
user-managed by design; nothing binds 8787 in that case.
|
|
76
|
+
|
|
77
|
+
## Test seam
|
|
78
|
+
|
|
79
|
+
`appa replay <dir>` checks `.appa` trace files against a policy; mock-driven unit
|
|
80
|
+
tests remain our primary seam (no runtime needed).
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* pi-openappa — Pi extension wiring for OpenAPPA.
|
|
3
|
+
*
|
|
4
|
+
* Thin by design: translate Pi events to `appa hook` invocations and enforce
|
|
5
|
+
* the answer. No policy logic lives here; the APPA runtime owns every
|
|
6
|
+
* decision. Protection is opt-in per session (APPA_GATE=1 at launch) and
|
|
7
|
+
* fail-closed while gated: if the runtime cannot answer, the call is blocked.
|
|
8
|
+
*
|
|
9
|
+
* Wire facts and constraints: docs/wire-notes.md
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
13
|
+
import {
|
|
14
|
+
parseCallDecision,
|
|
15
|
+
parseResultDecision,
|
|
16
|
+
postToolUsePayload,
|
|
17
|
+
preToolUsePayload,
|
|
18
|
+
promptPayload,
|
|
19
|
+
sessionStartPayload,
|
|
20
|
+
stopPayload,
|
|
21
|
+
toolResponseFrom,
|
|
22
|
+
} from "../src/adapter.ts";
|
|
23
|
+
import { invokeAppaHook } from "../src/hook-client.ts";
|
|
24
|
+
import { captureGate, checkHealth, setAlwaysOn, type GateState } from "../src/gate.ts";
|
|
25
|
+
|
|
26
|
+
interface TextPart {
|
|
27
|
+
type: "text";
|
|
28
|
+
text: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export default function (pi: ExtensionAPI): void {
|
|
32
|
+
/** Launch-fixed protection state; null until the session starts. */
|
|
33
|
+
let gate: GateState | null = null;
|
|
34
|
+
let sessionId = "";
|
|
35
|
+
/**
|
|
36
|
+
* toolCallId → the exact `input` object sent in the PreToolUse payload.
|
|
37
|
+
* PostToolUse must echo it byte-identically or the runtime withholds the
|
|
38
|
+
* result (byte_mismatch, see wire notes).
|
|
39
|
+
*/
|
|
40
|
+
const pendingInputs = new Map<string, Record<string, unknown>>();
|
|
41
|
+
|
|
42
|
+
const gated = (): boolean => gate?.gated === true;
|
|
43
|
+
|
|
44
|
+
pi.on("session_start", async (event, ctx) => {
|
|
45
|
+
gate = captureGate(process.env, ctx.cwd);
|
|
46
|
+
sessionId = ctx.sessionManager.getSessionId();
|
|
47
|
+
if (!gated()) return;
|
|
48
|
+
|
|
49
|
+
const outcome = await invokeAppaHook(
|
|
50
|
+
sessionStartPayload(sessionId, event.reason, ctx.cwd),
|
|
51
|
+
{
|
|
52
|
+
ensureRuntime: true,
|
|
53
|
+
...(gate.config !== undefined ? { config: gate.config } : {}),
|
|
54
|
+
},
|
|
55
|
+
);
|
|
56
|
+
if (outcome.exitCode !== 0 && ctx.hasUI) {
|
|
57
|
+
const remedy =
|
|
58
|
+
gate.config === undefined
|
|
59
|
+
? " Provide a policy (this project's .pi/openappa, APPA_CONFIG, or ~/.config/appa/appa.toml), or run /appa off."
|
|
60
|
+
: "";
|
|
61
|
+
ctx.ui.notify(
|
|
62
|
+
`OpenAPPA gated but the runtime did not answer (${gate.runtimeUrl}): ` +
|
|
63
|
+
`${outcome.stderr.trim() || `exit ${outcome.exitCode}`}.${remedy} ` +
|
|
64
|
+
"Tool calls will be blocked until it answers.",
|
|
65
|
+
"warning",
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
pi.on("before_agent_start", async (event, ctx) => {
|
|
71
|
+
if (!gated()) return;
|
|
72
|
+
// The prompt event establishes the turn boundary; it does not gate.
|
|
73
|
+
await invokeAppaHook(promptPayload(sessionId, event.prompt, ctx.cwd));
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
pi.on("tool_call", async (event, ctx) => {
|
|
77
|
+
if (!gated()) return;
|
|
78
|
+
const payload = preToolUsePayload(sessionId, event.toolName, event.input, ctx.cwd);
|
|
79
|
+
const outcome = await invokeAppaHook(payload);
|
|
80
|
+
const decision = parseCallDecision(outcome.exitCode, outcome.stdout, outcome.stderr);
|
|
81
|
+
if (decision.type === "deny") {
|
|
82
|
+
return { block: true, reason: `appa: ${decision.reason}` };
|
|
83
|
+
}
|
|
84
|
+
pendingInputs.set(event.toolCallId, event.input);
|
|
85
|
+
return undefined;
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
pi.on("tool_result", async (event, ctx) => {
|
|
89
|
+
if (!gated()) return;
|
|
90
|
+
const sentInput = pendingInputs.get(event.toolCallId);
|
|
91
|
+
pendingInputs.delete(event.toolCallId);
|
|
92
|
+
const response = toolResponseFrom({
|
|
93
|
+
text: joinContent(event.content),
|
|
94
|
+
isError: event.isError,
|
|
95
|
+
details: event.details,
|
|
96
|
+
});
|
|
97
|
+
const payload = postToolUsePayload(
|
|
98
|
+
sessionId,
|
|
99
|
+
event.toolName,
|
|
100
|
+
sentInput ?? event.input,
|
|
101
|
+
response,
|
|
102
|
+
ctx.cwd,
|
|
103
|
+
);
|
|
104
|
+
const outcome = await invokeAppaHook(payload);
|
|
105
|
+
const decision = parseResultDecision(outcome.exitCode, outcome.stdout, outcome.stderr);
|
|
106
|
+
if (decision.type === "pass") {
|
|
107
|
+
return undefined;
|
|
108
|
+
}
|
|
109
|
+
return {
|
|
110
|
+
content: [{ type: "text", text: decision.text } satisfies TextPart],
|
|
111
|
+
isError: decision.isError,
|
|
112
|
+
};
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
pi.on("turn_end", async () => {
|
|
116
|
+
if (!gated()) return;
|
|
117
|
+
// Turn completion is reported, never gated (matching --turn-end).
|
|
118
|
+
await invokeAppaHook(stopPayload(sessionId), { turnEnd: true });
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
pi.on("session_shutdown", () => {
|
|
122
|
+
pendingInputs.clear();
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
pi.registerCommand("appa", {
|
|
126
|
+
description: "Show OpenAPPA status; `appa on|off` toggles always-on protection",
|
|
127
|
+
handler: async (args, ctx) => {
|
|
128
|
+
const arg = args.trim();
|
|
129
|
+
if (arg === "on" || arg === "off") {
|
|
130
|
+
setAlwaysOn(process.env, arg === "on");
|
|
131
|
+
gate = captureGate(process.env, ctx.cwd);
|
|
132
|
+
if (ctx.hasUI) {
|
|
133
|
+
ctx.ui.notify(
|
|
134
|
+
arg === "on"
|
|
135
|
+
? "OpenAPPA always-on enabled: every future Pi session is protected."
|
|
136
|
+
: "OpenAPPA always-on disabled.",
|
|
137
|
+
"info",
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
const state = gate ?? captureGate(process.env, ctx.cwd);
|
|
143
|
+
const mode =
|
|
144
|
+
state.source === "project"
|
|
145
|
+
? "project (.pi/openappa)"
|
|
146
|
+
: state.source === "env"
|
|
147
|
+
? "launch (APPA_GATE=1)"
|
|
148
|
+
: state.source === "always-on"
|
|
149
|
+
? "always-on (/appa off to disable)"
|
|
150
|
+
: "off — /appa on enables it for every session";
|
|
151
|
+
const lines: string[] = [];
|
|
152
|
+
lines.push(state.gated ? `Protection: ON (session ${sessionId || "not started"})` : `Protection: off — ${mode}`);
|
|
153
|
+
if (state.gated) lines.push(`Mode: ${mode}`);
|
|
154
|
+
if (state.config !== undefined) lines.push(`Policy: ${state.config}`);
|
|
155
|
+
lines.push(`Runtime: ${state.runtimeUrl}`);
|
|
156
|
+
const health = await checkHealth(state.runtimeUrl);
|
|
157
|
+
lines.push(`Health: ${health.ok ? "ok" : `unreachable (${health.detail})`}`);
|
|
158
|
+
if (state.gated && !health.ok) {
|
|
159
|
+
lines.push("While the runtime is down, gated tool calls are blocked (fail-closed).");
|
|
160
|
+
}
|
|
161
|
+
const message = lines.join("\n");
|
|
162
|
+
if (ctx.hasUI) {
|
|
163
|
+
ctx.ui.notify(message, health.ok || !state.gated ? "info" : "warning");
|
|
164
|
+
}
|
|
165
|
+
},
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function joinContent(content: ReadonlyArray<unknown>): string {
|
|
170
|
+
const parts: string[] = [];
|
|
171
|
+
for (const item of content) {
|
|
172
|
+
if (
|
|
173
|
+
typeof item === "object" &&
|
|
174
|
+
item !== null &&
|
|
175
|
+
(item as { type?: unknown }).type === "text"
|
|
176
|
+
) {
|
|
177
|
+
parts.push(String((item as { text?: unknown }).text ?? ""));
|
|
178
|
+
} else {
|
|
179
|
+
parts.push("[non-text content omitted]");
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
return parts.join("\n");
|
|
183
|
+
}
|
package/justfile
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# pi-openappa — a Pi extension for OpenAPPA
|
|
2
|
+
# Just runs from the repo root; recipes are boring on purpose.
|
|
3
|
+
|
|
4
|
+
# Run everything a change should pass (default)
|
|
5
|
+
default: check
|
|
6
|
+
|
|
7
|
+
# Tests + typecheck together
|
|
8
|
+
check: test typecheck
|
|
9
|
+
|
|
10
|
+
# Mock-driven test suite (no APPA runtime needed)
|
|
11
|
+
test:
|
|
12
|
+
npm test
|
|
13
|
+
|
|
14
|
+
# TypeScript check, no emit
|
|
15
|
+
typecheck:
|
|
16
|
+
npm run typecheck
|
|
17
|
+
|
|
18
|
+
# Install this checkout as a local Pi package (loads live from this path)
|
|
19
|
+
install:
|
|
20
|
+
pi install {{justfile_directory()}}
|
|
21
|
+
|
|
22
|
+
# Reconcile Pi package installations after dependency changes
|
|
23
|
+
update:
|
|
24
|
+
pi update --extensions
|
|
25
|
+
|
|
26
|
+
# Remove this checkout from Pi's packages
|
|
27
|
+
remove:
|
|
28
|
+
pi remove {{justfile_directory()}}
|
|
29
|
+
|
|
30
|
+
# Publish to npm (requires `npm login`; the Pi gallery indexes the pi-package keyword)
|
|
31
|
+
publish: check
|
|
32
|
+
npm publish
|
package/package.json
CHANGED
|
@@ -1,6 +1,30 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-openappa",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Thin OpenAPPA guard extension for Pi: gates tool calls through the APPA runtime. No policy logic lives here.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"pi-package",
|
|
7
|
+
"openappa",
|
|
8
|
+
"appa",
|
|
9
|
+
"security",
|
|
10
|
+
"guardrails",
|
|
11
|
+
"policy"
|
|
12
|
+
],
|
|
13
|
+
"license": "MIT",
|
|
14
|
+
"type": "module",
|
|
15
|
+
"pi": {
|
|
16
|
+
"extensions": ["./extensions/index.ts"]
|
|
17
|
+
},
|
|
18
|
+
"main": "./extensions/index.ts",
|
|
19
|
+
"scripts": {
|
|
20
|
+
"test": "node --test 'test/*.test.ts'",
|
|
21
|
+
"typecheck": "tsc --noEmit"
|
|
22
|
+
},
|
|
23
|
+
"peerDependencies": {
|
|
24
|
+
"@earendil-works/pi-coding-agent": "*"
|
|
25
|
+
},
|
|
26
|
+
"devDependencies": {
|
|
27
|
+
"@earendil-works/pi-coding-agent": "^1.0.4",
|
|
28
|
+
"typescript": "^5.9.3"
|
|
29
|
+
}
|
|
30
|
+
}
|