@codyswann/lisa 4.17.5 → 4.17.7
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/all/copy-contents/gitignore +6 -0
- package/dist/cli/doctor-reconciliation.d.ts +15 -0
- package/dist/cli/doctor-reconciliation.d.ts.map +1 -0
- package/dist/cli/doctor-reconciliation.js +123 -0
- package/dist/cli/doctor-reconciliation.js.map +1 -0
- package/dist/cli/doctor.d.ts.map +1 -1
- package/dist/cli/doctor.js +7 -0
- package/dist/cli/doctor.js.map +1 -1
- package/dist/core/lisa.d.ts.map +1 -1
- package/dist/core/lisa.js +15 -1
- package/dist/core/lisa.js.map +1 -1
- package/dist/core/reconciliation-report.d.ts +100 -0
- package/dist/core/reconciliation-report.d.ts.map +1 -0
- package/dist/core/reconciliation-report.js +178 -0
- package/dist/core/reconciliation-report.js.map +1 -0
- package/dist/core/upstream-evidence-manifest.d.ts.map +1 -1
- package/dist/core/upstream-evidence-manifest.js +17 -1
- package/dist/core/upstream-evidence-manifest.js.map +1 -1
- package/dist/utils/postinstall-trampoline-report-source.d.ts +50 -0
- package/dist/utils/postinstall-trampoline-report-source.d.ts.map +1 -0
- package/dist/utils/postinstall-trampoline-report-source.js +113 -0
- package/dist/utils/postinstall-trampoline-report-source.js.map +1 -0
- package/dist/utils/postinstall-trampoline-source.d.ts +65 -0
- package/dist/utils/postinstall-trampoline-source.d.ts.map +1 -0
- package/dist/utils/postinstall-trampoline-source.js +312 -0
- package/dist/utils/postinstall-trampoline-source.js.map +1 -0
- package/dist/utils/postinstall-trampoline.d.ts +4 -2
- package/dist/utils/postinstall-trampoline.d.ts.map +1 -1
- package/dist/utils/postinstall-trampoline.js +5 -197
- package/dist/utils/postinstall-trampoline.js.map +1 -1
- package/package.json +3 -2
- package/plugins/lisa/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-agy/plugin.json +1 -1
- package/plugins/lisa-cdk/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-cdk/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-cdk-agy/plugin.json +1 -1
- package/plugins/lisa-cdk-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-cdk-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-expo/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-expo/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-expo-agy/plugin.json +1 -1
- package/plugins/lisa-expo-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-expo-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-harper-fabric/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-harper-fabric/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-harper-fabric-agy/plugin.json +1 -1
- package/plugins/lisa-harper-fabric-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-harper-fabric-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-nestjs/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-nestjs/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-nestjs-agy/plugin.json +1 -1
- package/plugins/lisa-nestjs-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-nestjs-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-openclaw/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-openclaw/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-openclaw-agy/plugin.json +1 -1
- package/plugins/lisa-openclaw-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-openclaw-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-phaser/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-phaser/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-phaser-agy/plugin.json +1 -1
- package/plugins/lisa-phaser-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-phaser-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-rails/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-rails/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-rails-agy/plugin.json +1 -1
- package/plugins/lisa-rails-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-rails-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-typescript/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-typescript/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-typescript-agy/plugin.json +1 -1
- package/plugins/lisa-typescript-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-typescript-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-wiki/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-wiki/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-wiki-agy/plugin.json +1 -1
- package/plugins/lisa-wiki-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-wiki-cursor/.claude-plugin/plugin.json +1 -1
- package/scripts/check-shell-guard-refusal-coverage.mjs +233 -0
- package/scripts/lib/shell-guard-refusal-coverage.mjs +315 -0
- package/scripts/lib/shell-guard-trace.mjs +256 -0
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* shell-guard-trace — record which shipped `.sh` guards a test run actually
|
|
3
|
+
* EXECUTES, and with what exit status (CodySwannGT/lisa#3190).
|
|
4
|
+
*
|
|
5
|
+
* @remarks
|
|
6
|
+
* ## Why a runtime trace rather than a scan of the tests
|
|
7
|
+
*
|
|
8
|
+
* The question this module answers is "which guard did the suite run", and
|
|
9
|
+
* every static spelling of that question has been tried here and is wrong in
|
|
10
|
+
* the direction that produces silence:
|
|
11
|
+
*
|
|
12
|
+
* - **By filename.** `foo.test.ts` next to `foo.sh` proves nothing. A test
|
|
13
|
+
* named after a guard that never runs it is the deceptive case
|
|
14
|
+
* CodySwannGT/lisa#3190 was filed about, and a scan keyed on names reports
|
|
15
|
+
* it as covered.
|
|
16
|
+
* - **By the path appearing in the test source.** Measured on this tree:
|
|
17
|
+
* 258 of 259 tracked `.sh` files are named somewhere inside a test file that
|
|
18
|
+
* also spawns something — inventories, manifests, and parity tables mention
|
|
19
|
+
* every shipped artifact. The signal is noise.
|
|
20
|
+
* - **By resolving the spawn argument statically.** Precise where it works
|
|
21
|
+
* (26 guards found) and blind exactly where this repository's suites are
|
|
22
|
+
* most interesting: the dominant idiom READS a guard's bytes, writes them
|
|
23
|
+
* into a fixture project under a different name, and executes THAT. The
|
|
24
|
+
* shipped path never reaches a spawn argument at all.
|
|
25
|
+
*
|
|
26
|
+
* The trace has none of those failure modes because it observes the process
|
|
27
|
+
* table instead of arguing about it, and it handles the write-a-copy idiom by
|
|
28
|
+
* matching on **content hash**: the copy is byte-identical to the guard, so it
|
|
29
|
+
* is the guard.
|
|
30
|
+
*
|
|
31
|
+
* ## What it does not see, stated rather than hidden
|
|
32
|
+
*
|
|
33
|
+
* - A helper reached by `source` rather than by `bash` is not a child process
|
|
34
|
+
* and never appears here. `lisa-edit-gate.sh` is the live example — every
|
|
35
|
+
* on-edit hook sources it. Such a helper enters the population only when a
|
|
36
|
+
* suite executes it directly, which is how CodySwannGT/lisa#3190's roster
|
|
37
|
+
* covers it.
|
|
38
|
+
* - Asynchronous child starts are not patched. Every child start in this
|
|
39
|
+
* test tree is synchronous by rule — see `tests/helpers/unbounded-spawn-scan`
|
|
40
|
+
* — so patching the three synchronous entry points covers the population,
|
|
41
|
+
* and a suite that started a guard asynchronously would be a rule violation
|
|
42
|
+
* caught by that scan first.
|
|
43
|
+
* - A suite that mocks `node:child_process` wholesale never reaches this
|
|
44
|
+
* module, and nothing real ran, so there is nothing to record.
|
|
45
|
+
*
|
|
46
|
+
* ## Loaded with `--import`, not as a vitest setup file
|
|
47
|
+
*
|
|
48
|
+
* `--import` runs before any user module links, so the patch lands on the CJS
|
|
49
|
+
* `child_process` exports that node's ESM facade for the builtin is later
|
|
50
|
+
* generated from. Measured: a vitest case using `import { spawnSync } from
|
|
51
|
+
* "node:child_process"` is intercepted. A vitest `setupFiles` entry runs after
|
|
52
|
+
* linking and would miss every already-bound import, and it would also collide
|
|
53
|
+
* with the suites that `vi.mock` the module.
|
|
54
|
+
*
|
|
55
|
+
* It NEVER throws and never changes a return value: a tracer that can break the
|
|
56
|
+
* suite it observes would be turned off, and then this whole control is prose.
|
|
57
|
+
*
|
|
58
|
+
* @module scripts/lib/shell-guard-trace
|
|
59
|
+
*/
|
|
60
|
+
import { appendFileSync, readFileSync, statSync } from "node:fs";
|
|
61
|
+
import { createHash } from "node:crypto";
|
|
62
|
+
import { createRequire } from "node:module";
|
|
63
|
+
import path from "node:path";
|
|
64
|
+
import process from "node:process";
|
|
65
|
+
|
|
66
|
+
/** Largest file the tracer will hash, in bytes. */
|
|
67
|
+
const MAX_HASHED_BYTES = 2_000_000;
|
|
68
|
+
|
|
69
|
+
/** Longest argv element the tracer will treat as a possible path. */
|
|
70
|
+
const MAX_PATH_LENGTH = 4096;
|
|
71
|
+
|
|
72
|
+
const traceFile = process.env["LISA_SHELL_GUARD_TRACE"];
|
|
73
|
+
const indexFile = process.env["LISA_SHELL_GUARD_INDEX"];
|
|
74
|
+
|
|
75
|
+
if (traceFile && indexFile) install(traceFile, indexFile);
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Patch the synchronous child starts so every guard execution is recorded.
|
|
79
|
+
* @param {string} trace - Path of the JSONL trace to append to.
|
|
80
|
+
* @param {string} index - Path of the guard index JSON to read.
|
|
81
|
+
* @returns {void}
|
|
82
|
+
*/
|
|
83
|
+
function install(trace, index) {
|
|
84
|
+
/** @type {{ byHash: Record<string, string[]>, sizes: number[] }} */
|
|
85
|
+
let inventory;
|
|
86
|
+
try {
|
|
87
|
+
inventory = JSON.parse(readFileSync(index, "utf8"));
|
|
88
|
+
} catch {
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
const sizes = new Set(inventory.sizes);
|
|
92
|
+
const seen = new Set();
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Append one observation, at most once per guard/status pair per process.
|
|
96
|
+
* @param {string} script - Repository-relative guard path.
|
|
97
|
+
* @param {number | null} status - The guard's exit status.
|
|
98
|
+
* @param {string} origin - Test frame that started it.
|
|
99
|
+
* @returns {void}
|
|
100
|
+
*/
|
|
101
|
+
const record = (script, status, origin) => {
|
|
102
|
+
const key = `${script}:${String(status)}`;
|
|
103
|
+
if (seen.has(key)) return;
|
|
104
|
+
seen.add(key);
|
|
105
|
+
try {
|
|
106
|
+
appendFileSync(trace, `${JSON.stringify({ script, status, origin })}\n`);
|
|
107
|
+
} catch {
|
|
108
|
+
/* A trace that cannot be written must not fail the suite. */
|
|
109
|
+
}
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Whether one child start RUNS its shell operand rather than inspecting it.
|
|
114
|
+
*
|
|
115
|
+
* This distinction is load-bearing and was found by measurement, not by
|
|
116
|
+
* reasoning. Without it, `bash -n script.sh` — a syntax check that never
|
|
117
|
+
* executes a line — reported fourteen `scripts/*.sh` as "driven, only ever
|
|
118
|
+
* exit 0", which is a false accusation of exactly the defect this control
|
|
119
|
+
* looks for. `shellcheck`, `cat` and `cp` name a guard in argv for the same
|
|
120
|
+
* reason and run none of it.
|
|
121
|
+
*
|
|
122
|
+
* So the operand counts only under an interpreter that will execute it, with
|
|
123
|
+
* POSIX `-n` (noexec) absent.
|
|
124
|
+
* @param {string} command - The command the child start was given.
|
|
125
|
+
* @param {string[]} argv - Its argument vector.
|
|
126
|
+
* @returns {boolean} Whether the operand is being executed.
|
|
127
|
+
*/
|
|
128
|
+
const executes = (command, argv) => {
|
|
129
|
+
const runner = path.basename(command.split(/\s+/u)[0] ?? "");
|
|
130
|
+
if (!["bash", "sh", "dash", "ksh", "zsh", "env"].includes(runner)) {
|
|
131
|
+
return false;
|
|
132
|
+
}
|
|
133
|
+
return !argv.includes("-n");
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Every tracked guard whose bytes appear in one child start's argv.
|
|
138
|
+
* @param {unknown} command - First argument of the child start.
|
|
139
|
+
* @param {unknown} args - Second argument: an argv array, or the options.
|
|
140
|
+
* @param {unknown} third - Third argument, when the second was an argv array.
|
|
141
|
+
* @returns {string[]} Repository-relative guard paths.
|
|
142
|
+
*/
|
|
143
|
+
const matched = (command, args, third) => {
|
|
144
|
+
if (typeof command !== "string") return [];
|
|
145
|
+
const argv = Array.isArray(args)
|
|
146
|
+
? args.filter(entry => typeof entry === "string")
|
|
147
|
+
: [];
|
|
148
|
+
// A guard invoked as the command itself is executing itself; anything else
|
|
149
|
+
// is executing only when the interpreter says so.
|
|
150
|
+
const tokens = executes(command, argv)
|
|
151
|
+
? [...command.split(/\s+/u), ...argv]
|
|
152
|
+
: [command];
|
|
153
|
+
const options = Array.isArray(args) ? third : args;
|
|
154
|
+
const cwd =
|
|
155
|
+
options !== null &&
|
|
156
|
+
typeof options === "object" &&
|
|
157
|
+
typeof (/** @type {{cwd?: unknown}} */ (options).cwd) === "string"
|
|
158
|
+
? /** @type {{cwd: string}} */ (options).cwd
|
|
159
|
+
: process.cwd();
|
|
160
|
+
const hits = new Set();
|
|
161
|
+
for (const token of tokens) {
|
|
162
|
+
if (!token || token.length > MAX_PATH_LENGTH) continue;
|
|
163
|
+
const absolute = path.isAbsolute(token)
|
|
164
|
+
? token
|
|
165
|
+
: path.resolve(cwd, token);
|
|
166
|
+
let size = -1;
|
|
167
|
+
try {
|
|
168
|
+
const stats = statSync(absolute);
|
|
169
|
+
if (!stats.isFile()) continue;
|
|
170
|
+
size = stats.size;
|
|
171
|
+
} catch {
|
|
172
|
+
continue;
|
|
173
|
+
}
|
|
174
|
+
// Hash only what could be a guard. Every tracked guard's size is known,
|
|
175
|
+
// so a `git`, a `node`, or a fixture source is rejected by an integer
|
|
176
|
+
// comparison rather than by reading it.
|
|
177
|
+
if (!sizes.has(size) || size > MAX_HASHED_BYTES) continue;
|
|
178
|
+
try {
|
|
179
|
+
const digest = createHash("sha256")
|
|
180
|
+
.update(readFileSync(absolute))
|
|
181
|
+
.digest("hex");
|
|
182
|
+
for (const guard of inventory.byHash[digest] ?? []) hits.add(guard);
|
|
183
|
+
} catch {
|
|
184
|
+
continue;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
return [...hits];
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* The first stack frame inside the test tree, for the failure report.
|
|
192
|
+
* @returns {string} A `file:line` frame, or an empty string.
|
|
193
|
+
*/
|
|
194
|
+
const origin = () => {
|
|
195
|
+
const frames = (new Error("trace").stack ?? "")
|
|
196
|
+
.split("\n")
|
|
197
|
+
.filter(line => line.includes("/tests/"));
|
|
198
|
+
// The suite, not the helper it reached through. Every bounded spawn in this
|
|
199
|
+
// tree goes through `tests/helpers/io-latency-budget`, so naming the first
|
|
200
|
+
// `tests/` frame would name that helper for nearly every observation and
|
|
201
|
+
// the report would say nothing about where to look.
|
|
202
|
+
const frame = frames.find(line => line.includes(".test.ts")) ?? frames[0];
|
|
203
|
+
return (frame ?? "").trim().replace(/^at\s+/u, "");
|
|
204
|
+
};
|
|
205
|
+
|
|
206
|
+
const require = createRequire(import.meta.url);
|
|
207
|
+
const childProcess = require("node:child_process");
|
|
208
|
+
|
|
209
|
+
const originalSpawnSync = childProcess.spawnSync;
|
|
210
|
+
childProcess.spawnSync = function tracedSpawnSync(command, args, options) {
|
|
211
|
+
const result = originalSpawnSync.call(this, command, args, options);
|
|
212
|
+
try {
|
|
213
|
+
const guards = matched(command, args, options);
|
|
214
|
+
if (guards.length > 0) {
|
|
215
|
+
const where = origin();
|
|
216
|
+
for (const guard of guards) record(guard, result.status, where);
|
|
217
|
+
}
|
|
218
|
+
} catch {
|
|
219
|
+
/* Never let observation change the observed. */
|
|
220
|
+
}
|
|
221
|
+
return result;
|
|
222
|
+
};
|
|
223
|
+
|
|
224
|
+
for (const name of ["execFileSync", "execSync"]) {
|
|
225
|
+
const original = childProcess[name];
|
|
226
|
+
childProcess[name] = function tracedExec(command, args, options) {
|
|
227
|
+
/**
|
|
228
|
+
* Record one outcome of the wrapped call.
|
|
229
|
+
* @param {number | null} status - Its exit status.
|
|
230
|
+
* @returns {void}
|
|
231
|
+
*/
|
|
232
|
+
const observe = status => {
|
|
233
|
+
try {
|
|
234
|
+
const guards = matched(command, args, options);
|
|
235
|
+
if (guards.length === 0) return;
|
|
236
|
+
const where = origin();
|
|
237
|
+
for (const guard of guards) record(guard, status, where);
|
|
238
|
+
} catch {
|
|
239
|
+
/* Never let observation change the observed. */
|
|
240
|
+
}
|
|
241
|
+
};
|
|
242
|
+
try {
|
|
243
|
+
const result = original.call(this, command, args, options);
|
|
244
|
+
observe(0);
|
|
245
|
+
return result;
|
|
246
|
+
} catch (error) {
|
|
247
|
+
observe(
|
|
248
|
+
typeof (/** @type {{status?: unknown}} */ (error).status) === "number"
|
|
249
|
+
? /** @type {{status: number}} */ (error).status
|
|
250
|
+
: null
|
|
251
|
+
);
|
|
252
|
+
throw error;
|
|
253
|
+
}
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
}
|