pi-openappa 0.2.0 → 0.3.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/README.md +85 -66
- package/extensions/index.ts +176 -110
- package/package.json +8 -1
- package/src/gate.ts +78 -75
- package/src/hook-client.ts +1 -6
- package/templates/appa.toml +1068 -0
- package/traces/coding-loop.appa +31 -0
- package/traces/memory-poison.appa +24 -0
- package/traces/push-day.appa +21 -0
- package/traces/research-then-code.appa +53 -0
- package/traces/secret-ops.appa +23 -0
- package/justfile +0 -39
- package/src/installer.ts +0 -103
- package/test/adapter.test.ts +0 -207
- package/test/auto-install.test.ts +0 -85
- package/test/extension.test.ts +0 -667
- package/test/fixtures/mock-appa.mjs +0 -104
- package/test/gate.test.ts +0 -131
- package/tsconfig.json +0 -16
package/src/gate.ts
CHANGED
|
@@ -1,20 +1,17 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Session gate: protection is
|
|
2
|
+
* Session gate: protection is opt-in, fixed at session start.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
* -
|
|
6
|
-
*
|
|
7
|
-
* -
|
|
8
|
-
*
|
|
9
|
-
* - the global `/appa off` marker below; `/appa on` clears it.
|
|
10
|
-
* Otherwise the session is protected (the default).
|
|
4
|
+
* Three ways a session becomes protected, most specific first for reporting:
|
|
5
|
+
* - a project marker `<cwd>/.pi/openappa` (project-scoped; optional content
|
|
6
|
+
* names that project's policy, absolute or cwd-relative),
|
|
7
|
+
* - launched with APPA_GATE=1 (the launcher route, mirroring `clappa`), or
|
|
8
|
+
* - always-on mode, persisted by `/appa on` (marker file below).
|
|
11
9
|
*
|
|
12
10
|
* An explicit APPA_CONFIG always wins as the policy source; otherwise a
|
|
13
|
-
* project marker's content is used; otherwise
|
|
11
|
+
* project marker's content is used; otherwise the settings file's `config`;
|
|
12
|
+
* otherwise APPA's own default. The gate is
|
|
14
13
|
* captured once per session so a session cannot disable its own protection
|
|
15
14
|
* mid-run; `/appa on|off` are deliberate user commands and do re-resolve.
|
|
16
|
-
* The legacy `always-on` marker from opt-in days is ignored; `/appa on`
|
|
17
|
-
* removes it.
|
|
18
15
|
*/
|
|
19
16
|
|
|
20
17
|
import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
@@ -29,71 +26,92 @@ function baseConfigDir(env: NodeJS.ProcessEnv): string {
|
|
|
29
26
|
return join(env.HOME ?? "", ".config");
|
|
30
27
|
}
|
|
31
28
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
return join(baseConfigDir(env), "pi-openappa", "off");
|
|
29
|
+
export function alwaysOnMarkerPath(env: NodeJS.ProcessEnv): string {
|
|
30
|
+
return join(baseConfigDir(env), "pi-openappa", "always-on");
|
|
35
31
|
}
|
|
36
32
|
|
|
37
|
-
/**
|
|
38
|
-
export function
|
|
39
|
-
return join(baseConfigDir(env), "
|
|
33
|
+
/** The policy a gated session auto-starts the runtime with, by APPA default. */
|
|
34
|
+
export function appaDefaultConfigPath(env: NodeJS.ProcessEnv): string {
|
|
35
|
+
return join(baseConfigDir(env), "appa", "appa.toml");
|
|
40
36
|
}
|
|
41
37
|
|
|
42
|
-
|
|
43
|
-
|
|
38
|
+
/** File-backed settings for this extension (see `ExtensionSettings`). */
|
|
39
|
+
export function settingsPath(env: NodeJS.ProcessEnv): string {
|
|
40
|
+
return join(baseConfigDir(env), "pi-openappa", "settings.json");
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Keys read from `settings.json`. Every key is optional; environment
|
|
45
|
+
* variables of the same meaning always win over the file, and the file
|
|
46
|
+
* always wins over the built-in defaults. Written by `/appa init`.
|
|
47
|
+
*/
|
|
48
|
+
export interface ExtensionSettings {
|
|
49
|
+
/** Policy path passed as `--config` (the APPA_CONFIG fallback). */
|
|
50
|
+
config?: string;
|
|
51
|
+
/** Runtime endpoint (the APPA_RUNTIME_URL fallback). */
|
|
52
|
+
runtimeUrl?: string;
|
|
53
|
+
/** Hook binary (the APPA_HOOK_BIN fallback). */
|
|
54
|
+
hookBin?: string;
|
|
55
|
+
/** Hook timeout in ms (the APPA_HOOK_TIMEOUT_MS fallback). */
|
|
56
|
+
hookTimeoutMs?: number;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** Read settings.json; a missing or malformed file resolves to `{}`. */
|
|
60
|
+
export function readSettings(env: NodeJS.ProcessEnv): ExtensionSettings {
|
|
61
|
+
try {
|
|
62
|
+
const parsed: unknown = JSON.parse(readFileSync(settingsPath(env), "utf8"));
|
|
63
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
64
|
+
return {};
|
|
65
|
+
}
|
|
66
|
+
const out: ExtensionSettings = {};
|
|
67
|
+
const record = parsed as Record<string, unknown>;
|
|
68
|
+
for (const key of ["config", "runtimeUrl", "hookBin"] as const) {
|
|
69
|
+
const value = record[key];
|
|
70
|
+
if (typeof value === "string" && value !== "") out[key] = value;
|
|
71
|
+
}
|
|
72
|
+
const timeout = record["hookTimeoutMs"];
|
|
73
|
+
if (typeof timeout === "number" && Number.isFinite(timeout) && timeout > 0) {
|
|
74
|
+
out.hookTimeoutMs = timeout;
|
|
75
|
+
}
|
|
76
|
+
return out;
|
|
77
|
+
} catch {
|
|
78
|
+
return {};
|
|
79
|
+
}
|
|
44
80
|
}
|
|
45
81
|
|
|
46
|
-
export function
|
|
47
|
-
return join(cwd, ".pi", "
|
|
82
|
+
export function projectMarkerPath(cwd: string): string {
|
|
83
|
+
return join(cwd, ".pi", "openappa");
|
|
48
84
|
}
|
|
49
85
|
|
|
50
|
-
export function
|
|
51
|
-
return existsSync(
|
|
86
|
+
export function isAlwaysOn(env: NodeJS.ProcessEnv): boolean {
|
|
87
|
+
return existsSync(alwaysOnMarkerPath(env));
|
|
52
88
|
}
|
|
53
89
|
|
|
54
|
-
export function
|
|
55
|
-
const marker =
|
|
56
|
-
if (
|
|
90
|
+
export function setAlwaysOn(env: NodeJS.ProcessEnv, on: boolean): void {
|
|
91
|
+
const marker = alwaysOnMarkerPath(env);
|
|
92
|
+
if (on) {
|
|
57
93
|
mkdirSync(dirname(marker), { recursive: true });
|
|
58
94
|
writeFileSync(marker, "");
|
|
59
95
|
} else {
|
|
60
96
|
rmSync(marker, { force: true });
|
|
61
|
-
rmSync(legacyAlwaysOnMarkerPath(env), { force: true });
|
|
62
97
|
}
|
|
63
98
|
}
|
|
64
99
|
|
|
65
|
-
|
|
66
|
-
* Does APPA's own default policy exist? Heuristic mirror of the runtime's
|
|
67
|
-
* lookup: `$XDG_CONFIG_HOME/appa/appa.toml` or `~/.config/appa/appa.toml`.
|
|
68
|
-
*/
|
|
69
|
-
export function appaDefaultPolicyExists(env: NodeJS.ProcessEnv): boolean {
|
|
70
|
-
const xdg = env.XDG_CONFIG_HOME;
|
|
71
|
-
const candidates = [
|
|
72
|
-
...(xdg !== undefined && xdg !== "" ? [join(xdg, "appa", "appa.toml")] : []),
|
|
73
|
-
join(env.HOME ?? "", ".config", "appa", "appa.toml"),
|
|
74
|
-
];
|
|
75
|
-
return candidates.some((candidate) => existsSync(candidate));
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
export type GateSource =
|
|
79
|
-
| "env-on"
|
|
80
|
-
| "env-off"
|
|
81
|
-
| "project"
|
|
82
|
-
| "project-off"
|
|
83
|
-
| "global-off"
|
|
84
|
-
| "default";
|
|
100
|
+
export type GateSource = "project" | "env" | "always-on" | "off";
|
|
85
101
|
|
|
86
102
|
export interface GateState {
|
|
87
103
|
/** Protection active for this session. */
|
|
88
104
|
gated: boolean;
|
|
89
105
|
/** Most specific reason this session is (or is not) protected. */
|
|
90
106
|
source: GateSource;
|
|
91
|
-
/** Policy for auto-start:
|
|
107
|
+
/** Policy for auto-start: env, then project marker, then settings file. */
|
|
92
108
|
config?: string;
|
|
93
109
|
/** Runtime URL for health reporting (the hook binary reads it from env). */
|
|
94
110
|
runtimeUrl: string;
|
|
95
111
|
/** Hook binary used for reporting. */
|
|
96
112
|
hookBin: string;
|
|
113
|
+
/** Hook timeout in ms when settings pin one; otherwise the default applies. */
|
|
114
|
+
hookTimeoutMs?: number;
|
|
97
115
|
}
|
|
98
116
|
|
|
99
117
|
/** Read a project marker's optional policy path; empty content resolves to none. */
|
|
@@ -108,55 +126,40 @@ function projectConfig(cwd: string): string | undefined {
|
|
|
108
126
|
}
|
|
109
127
|
|
|
110
128
|
export function captureGate(env: NodeJS.ProcessEnv, cwd?: string): GateState {
|
|
129
|
+
const settings = readSettings(env);
|
|
111
130
|
const base = {
|
|
112
|
-
runtimeUrl: env.APPA_RUNTIME_URL ?? DEFAULT_RUNTIME_URL,
|
|
113
|
-
hookBin: env.APPA_HOOK_BIN ?? "appa",
|
|
131
|
+
runtimeUrl: env.APPA_RUNTIME_URL ?? settings.runtimeUrl ?? DEFAULT_RUNTIME_URL,
|
|
132
|
+
hookBin: env.APPA_HOOK_BIN ?? settings.hookBin ?? "appa",
|
|
133
|
+
...(settings.hookTimeoutMs !== undefined ? { hookTimeoutMs: settings.hookTimeoutMs } : {}),
|
|
114
134
|
};
|
|
115
135
|
const explicitConfig =
|
|
116
136
|
env.APPA_CONFIG !== undefined && env.APPA_CONFIG !== ""
|
|
117
137
|
? env.APPA_CONFIG
|
|
118
138
|
: undefined;
|
|
119
139
|
const projectGated = cwd !== undefined && existsSync(projectMarkerPath(cwd));
|
|
120
|
-
const projectOff = cwd !== undefined && existsSync(projectNoMarkerPath(cwd));
|
|
121
140
|
const projectCfg =
|
|
122
141
|
projectGated && cwd !== undefined ? projectConfig(cwd) : undefined;
|
|
142
|
+
// Precedence: launch env, then the project marker (per-project intent),
|
|
143
|
+
// then the global settings file (see readSettings).
|
|
144
|
+
const config = explicitConfig ?? projectCfg ?? settings.config;
|
|
123
145
|
|
|
124
|
-
|
|
125
|
-
if (env.APPA_GATE === "1") {
|
|
126
|
-
const config = explicitConfig ?? projectCfg;
|
|
146
|
+
if (env.APPA_GATE === "1" || projectGated) {
|
|
127
147
|
return {
|
|
128
148
|
gated: true,
|
|
129
|
-
source: "env
|
|
149
|
+
source: projectGated ? "project" : "env",
|
|
130
150
|
...base,
|
|
131
151
|
...(config !== undefined ? { config } : {}),
|
|
132
152
|
};
|
|
133
153
|
}
|
|
134
|
-
if (env
|
|
135
|
-
return { gated: false, source: "env-off", ...base };
|
|
136
|
-
}
|
|
137
|
-
// Project level: an opt-out beats the project's own opt-in.
|
|
138
|
-
if (projectOff) {
|
|
139
|
-
return { gated: false, source: "project-off", ...base };
|
|
140
|
-
}
|
|
141
|
-
if (projectGated) {
|
|
142
|
-
const config = explicitConfig ?? projectCfg;
|
|
154
|
+
if (isAlwaysOn(env)) {
|
|
143
155
|
return {
|
|
144
156
|
gated: true,
|
|
145
|
-
source: "
|
|
157
|
+
source: "always-on",
|
|
146
158
|
...base,
|
|
147
159
|
...(config !== undefined ? { config } : {}),
|
|
148
160
|
};
|
|
149
161
|
}
|
|
150
|
-
|
|
151
|
-
if (isGloballyOff(env)) {
|
|
152
|
-
return { gated: false, source: "global-off", ...base };
|
|
153
|
-
}
|
|
154
|
-
return {
|
|
155
|
-
gated: true,
|
|
156
|
-
source: "default",
|
|
157
|
-
...base,
|
|
158
|
-
...(explicitConfig !== undefined ? { config: explicitConfig } : {}),
|
|
159
|
-
};
|
|
162
|
+
return { gated: false, source: "off", ...base };
|
|
160
163
|
}
|
|
161
164
|
|
|
162
165
|
export interface HealthResult {
|
package/src/hook-client.ts
CHANGED
|
@@ -18,8 +18,6 @@ export interface HookOutcome {
|
|
|
18
18
|
stdout: string;
|
|
19
19
|
stderr: string;
|
|
20
20
|
timedOut: boolean;
|
|
21
|
-
/** The hook binary itself could not be spawned (ENOENT). */
|
|
22
|
-
binaryMissing: boolean;
|
|
23
21
|
}
|
|
24
22
|
|
|
25
23
|
export interface InvokeOptions {
|
|
@@ -96,11 +94,10 @@ export async function invokeAppaHook(
|
|
|
96
94
|
stdout,
|
|
97
95
|
stderr: `${stderr}appa hook timed out after ${timeoutMs}ms`.trim(),
|
|
98
96
|
timedOut: true,
|
|
99
|
-
binaryMissing: false,
|
|
100
97
|
});
|
|
101
98
|
return;
|
|
102
99
|
}
|
|
103
|
-
resolve({ exitCode, stdout, stderr, timedOut: false
|
|
100
|
+
resolve({ exitCode, stdout, stderr, timedOut: false });
|
|
104
101
|
};
|
|
105
102
|
|
|
106
103
|
child.on("error", (error) => {
|
|
@@ -123,13 +120,11 @@ export async function invokeAppaHook(
|
|
|
123
120
|
|
|
124
121
|
function spawnFailure(error: unknown): HookOutcome {
|
|
125
122
|
const detail = error instanceof Error ? error.message : String(error);
|
|
126
|
-
const code = (error as { code?: unknown } | null)?.code;
|
|
127
123
|
return {
|
|
128
124
|
exitCode: -1,
|
|
129
125
|
stdout: "",
|
|
130
126
|
stderr: `appa hook failed to start: ${detail}`,
|
|
131
127
|
timedOut: false,
|
|
132
|
-
binaryMissing: code === "ENOENT",
|
|
133
128
|
};
|
|
134
129
|
}
|
|
135
130
|
|