pi-openappa 0.0.0-stage → 0.2.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.
@@ -0,0 +1,104 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Scripted `appa hook` stand-in for tests. Behavior is selected with
4
+ * MOCK_MODE; every received payload is appended to MOCK_RECORD as JSONL so
5
+ * tests can assert byte-exact echoes across PreToolUse/PostToolUse.
6
+ */
7
+ import { appendFileSync } from "node:fs";
8
+
9
+ let raw = "";
10
+ process.stdin.on("data", (chunk) => {
11
+ raw += chunk;
12
+ });
13
+ process.stdin.on("end", () => {
14
+ let payload = {};
15
+ try {
16
+ payload = JSON.parse(raw);
17
+ } catch {
18
+ payload = {};
19
+ }
20
+ if (process.env.MOCK_RECORD) {
21
+ appendFileSync(process.env.MOCK_RECORD, `${JSON.stringify(payload)}\n`);
22
+ appendFileSync(
23
+ `${process.env.MOCK_RECORD}.argv`,
24
+ `${JSON.stringify(process.argv.slice(2))}\n`,
25
+ );
26
+ }
27
+ const event = payload.hook_event_name;
28
+ const mode = process.env.MOCK_MODE ?? "allow";
29
+
30
+ if (mode === "sleep") {
31
+ setTimeout(() => process.exit(0), 2000);
32
+ return;
33
+ }
34
+ if (mode === "crash") {
35
+ console.error("appa hook failed to start: mock crash");
36
+ process.exit(-1);
37
+ return;
38
+ }
39
+
40
+ if (event === "PreToolUse") {
41
+ if (mode === "deny-stderr") {
42
+ console.error("OpenAPPA hook blocked: not declared in this policy");
43
+ process.exit(2);
44
+ return;
45
+ }
46
+ if (mode === "deny-json") {
47
+ process.stdout.write(JSON.stringify({ error: "tool host/claude-code/X is not declared" }));
48
+ console.error("OpenAPPA hook blocked: tool host/claude-code/X is not declared");
49
+ process.exit(2);
50
+ return;
51
+ }
52
+ if (mode === "deny-permission") {
53
+ process.stdout.write(
54
+ JSON.stringify({
55
+ hookSpecificOutput: {
56
+ hookEventName: "PreToolUse",
57
+ permissionDecision: "deny",
58
+ permissionDecisionReason: "appa: this command may not run here",
59
+ },
60
+ }),
61
+ );
62
+ process.exit(0);
63
+ return;
64
+ }
65
+ process.stdout.write(
66
+ JSON.stringify({
67
+ hookSpecificOutput: {
68
+ hookEventName: "PreToolUse",
69
+ permissionDecision: "allow",
70
+ permissionDecisionReason: "appa: the call is released",
71
+ },
72
+ }),
73
+ );
74
+ process.exit(0);
75
+ return;
76
+ }
77
+
78
+ if (event === "PostToolUse" && mode === "replace-result") {
79
+ process.stdout.write(
80
+ JSON.stringify({
81
+ decision: "block",
82
+ reason: "this outcome does not match the open dispatch; it is not reported",
83
+ hookSpecificOutput: {
84
+ hookEventName: "PostToolUse",
85
+ updatedToolOutput: {
86
+ exitcode: 0,
87
+ stdout: "[appa] the tool result was withheld by policy",
88
+ },
89
+ },
90
+ }),
91
+ );
92
+ process.exit(0);
93
+ return;
94
+ }
95
+
96
+ if (event === "PostToolUse" && mode === "deny-result") {
97
+ console.error("OpenAPPA hook blocked: result may not cross");
98
+ process.exit(2);
99
+ return;
100
+ }
101
+
102
+ process.stdout.write("{}");
103
+ process.exit(0);
104
+ });
@@ -0,0 +1,131 @@
1
+ import assert from "node:assert/strict";
2
+ import { after, describe, it } from "node:test";
3
+ import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
4
+ import { tmpdir } from "node:os";
5
+ import { join } from "node:path";
6
+ import {
7
+ appaDefaultPolicyExists,
8
+ captureGate,
9
+ globalOffMarkerPath,
10
+ legacyAlwaysOnMarkerPath,
11
+ } from "../src/gate.ts";
12
+
13
+ /** Environment keys this suite mutates; restored after every test. */
14
+ const KEYS = ["APPA_GATE", "APPA_CONFIG", "XDG_CONFIG_HOME", "HOME"];
15
+ const saved: Record<string, string | undefined> = {};
16
+ for (const key of KEYS) saved[key] = process.env[key];
17
+
18
+ after(() => {
19
+ for (const key of KEYS) {
20
+ const value = saved[key];
21
+ if (value === undefined) delete process.env[key];
22
+ else process.env[key] = value;
23
+ }
24
+ });
25
+
26
+ const workDirs: string[] = [];
27
+ after(() => {
28
+ for (const dir of workDirs) rmSync(dir, { recursive: true, force: true });
29
+ });
30
+
31
+ /** Fresh env with isolated HOME/XDG (no markers, no default policy) + project dir. */
32
+ function fresh(): { env: Record<string, string>; proj: string } {
33
+ const dir = mkdtempSync(join(tmpdir(), "pi-openappa-gate-"));
34
+ workDirs.push(dir);
35
+ return {
36
+ env: { HOME: join(dir, "home"), XDG_CONFIG_HOME: join(dir, "xdg") },
37
+ proj: join(dir, "proj"),
38
+ };
39
+ }
40
+
41
+ function projectMarker(env: Record<string, string>, proj: string, content = ""): void {
42
+ mkdirSync(join(proj, ".pi"), { recursive: true });
43
+ writeFileSync(join(proj, ".pi", "openappa"), content);
44
+ }
45
+
46
+ describe("default-on gate resolution", () => {
47
+ it("is gated by default with no markers or env", () => {
48
+ const { env, proj } = fresh();
49
+ const state = captureGate(env, proj);
50
+ assert.equal(state.gated, true);
51
+ assert.equal(state.source, "default");
52
+ assert.equal(state.config, undefined);
53
+ });
54
+
55
+ it("APPA_GATE=0 opts out for one launch; APPA_GATE=1 forces on", () => {
56
+ const { env, proj } = fresh();
57
+ env.APPA_GATE = "0";
58
+ assert.equal(captureGate(env, proj).source, "env-off");
59
+ env.APPA_GATE = "1";
60
+ const on = captureGate(env, proj);
61
+ assert.equal(on.gated, true);
62
+ assert.equal(on.source, "env-on");
63
+ });
64
+
65
+ it("an explicit launch choice beats every project marker", () => {
66
+ const { env, proj } = fresh();
67
+ projectMarker(env, proj, "appa.toml");
68
+ mkdirSync(join(proj, ".pi"), { recursive: true });
69
+ writeFileSync(join(proj, ".pi", "no-openappa"), "");
70
+ env.APPA_GATE = "1";
71
+ assert.equal(captureGate(env, proj).source, "env-on");
72
+ env.APPA_GATE = "0";
73
+ assert.equal(captureGate(env, proj).source, "env-off");
74
+ });
75
+
76
+ it(".pi/openappa gates the project and names its policy", () => {
77
+ const { env, proj } = fresh();
78
+ projectMarker(env, proj, "appa.toml\n");
79
+ const state = captureGate(env, proj);
80
+ assert.equal(state.gated, true);
81
+ assert.equal(state.source, "project");
82
+ assert.equal(state.config, join(proj, "appa.toml"));
83
+ });
84
+
85
+ it(".pi/no-openappa opts the project out, beating .pi/openappa", () => {
86
+ const { env, proj } = fresh();
87
+ projectMarker(env, proj, "appa.toml");
88
+ writeFileSync(join(proj, ".pi", "no-openappa"), "");
89
+ const state = captureGate(env, proj);
90
+ assert.equal(state.gated, false);
91
+ assert.equal(state.source, "project-off");
92
+ });
93
+
94
+ it("the /appa off marker opts out globally; APPA_CONFIG still names a policy", () => {
95
+ const { env, proj } = fresh();
96
+ mkdirSync(join(env.XDG_CONFIG_HOME!, "pi-openappa"), { recursive: true });
97
+ writeFileSync(globalOffMarkerPath(env), "");
98
+ const state = captureGate(env, proj);
99
+ assert.equal(state.gated, false);
100
+ assert.equal(state.source, "global-off");
101
+ env.APPA_CONFIG = "/tmp/policy.toml";
102
+ env.APPA_GATE = "1";
103
+ assert.equal(captureGate(env, proj).config, "/tmp/policy.toml");
104
+ });
105
+
106
+ it("ignores the legacy always-on marker (default is already on)", () => {
107
+ const { env, proj } = fresh();
108
+ mkdirSync(join(env.XDG_CONFIG_HOME!, "pi-openappa"), { recursive: true });
109
+ writeFileSync(legacyAlwaysOnMarkerPath(env), "");
110
+ assert.equal(captureGate(env, proj).source, "default");
111
+ });
112
+ });
113
+
114
+ describe("appaDefaultPolicyExists heuristic", () => {
115
+ it("finds nothing in a clean environment", () => {
116
+ const { env } = fresh();
117
+ assert.equal(appaDefaultPolicyExists(env), false);
118
+ });
119
+
120
+ it("finds ~/.config/appa/appa.toml and the XDG equivalent", () => {
121
+ const home = fresh();
122
+ mkdirSync(join(home.env.HOME!, ".config", "appa"), { recursive: true });
123
+ writeFileSync(join(home.env.HOME!, ".config", "appa", "appa.toml"), "");
124
+ assert.equal(appaDefaultPolicyExists(home.env), true);
125
+
126
+ const xdg = fresh();
127
+ mkdirSync(join(xdg.env.XDG_CONFIG_HOME!, "appa"), { recursive: true });
128
+ writeFileSync(join(xdg.env.XDG_CONFIG_HOME!, "appa", "appa.toml"), "");
129
+ assert.equal(appaDefaultPolicyExists(xdg.env), true);
130
+ });
131
+ });
package/tsconfig.json ADDED
@@ -0,0 +1,16 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2023",
4
+ "module": "NodeNext",
5
+ "moduleResolution": "NodeNext",
6
+ "lib": ["ES2023"],
7
+ "types": ["node"],
8
+ "strict": true,
9
+ "noUncheckedIndexedAccess": true,
10
+ "exactOptionalPropertyTypes": true,
11
+ "noEmit": true,
12
+ "skipLibCheck": true,
13
+ "allowImportingTsExtensions": true
14
+ },
15
+ "include": ["extensions/**/*.ts", "src/**/*.ts", "test/**/*.ts"]
16
+ }