@codyswann/lisa 4.4.6 → 4.4.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/dist/core/upstream-evidence-manifest.d.ts.map +1 -1
- package/dist/core/upstream-evidence-manifest.js +28 -15
- package/dist/core/upstream-evidence-manifest.js.map +1 -1
- package/package.json +1 -1
- package/plugins/lisa/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa/.codex-plugin/skills/lisa-remote-dispatch/scripts/dispatch.mjs +4 -3
- package/plugins/lisa/.codex-plugin/skills/lisa-secrets-access/scripts/providers.mjs +11 -5
- package/plugins/lisa/.codex-plugin/skills/lisa-secrets-access/scripts/sync-secret-to-ci.mjs +3 -2
- package/plugins/lisa/.codex-plugin/skills/lisa-setup-remote-env/scripts/setup-remote-env.mjs +36 -23
- package/plugins/lisa/.codex-plugin/skills/lisa-setup-remote-env/scripts/verify-remote-env.mjs +7 -3
- package/plugins/lisa/.codex-plugin/skills/lisa-setup-workstation/scripts/bounded-child.mjs +159 -0
- package/plugins/lisa/.codex-plugin/skills/lisa-setup-workstation/scripts/workstation.mjs +5 -4
- package/plugins/lisa/scripts/design-source-gate.mjs +19 -1
- package/plugins/lisa/scripts/plugin-sync-explain.mjs +23 -1
- package/plugins/lisa/scripts/project-ideation-idempotency-harness.mjs +20 -1
- package/plugins/lisa/skills/lisa-remote-dispatch/scripts/dispatch.mjs +4 -3
- package/plugins/lisa/skills/lisa-secrets-access/scripts/providers.mjs +11 -5
- package/plugins/lisa/skills/lisa-secrets-access/scripts/sync-secret-to-ci.mjs +3 -2
- package/plugins/lisa/skills/lisa-setup-remote-env/scripts/setup-remote-env.mjs +36 -23
- package/plugins/lisa/skills/lisa-setup-remote-env/scripts/verify-remote-env.mjs +7 -3
- package/plugins/lisa/skills/lisa-setup-workstation/scripts/bounded-child.mjs +159 -0
- package/plugins/lisa/skills/lisa-setup-workstation/scripts/workstation.mjs +5 -4
- package/plugins/lisa-agy/plugin.json +1 -1
- package/plugins/lisa-agy/scripts/design-source-gate.mjs +19 -1
- package/plugins/lisa-agy/scripts/plugin-sync-explain.mjs +23 -1
- package/plugins/lisa-agy/scripts/project-ideation-idempotency-harness.mjs +20 -1
- package/plugins/lisa-agy/skills/lisa-remote-dispatch/scripts/dispatch.mjs +4 -3
- package/plugins/lisa-agy/skills/lisa-secrets-access/scripts/providers.mjs +11 -5
- package/plugins/lisa-agy/skills/lisa-secrets-access/scripts/sync-secret-to-ci.mjs +3 -2
- package/plugins/lisa-agy/skills/lisa-setup-remote-env/scripts/setup-remote-env.mjs +36 -23
- package/plugins/lisa-agy/skills/lisa-setup-remote-env/scripts/verify-remote-env.mjs +7 -3
- package/plugins/lisa-agy/skills/lisa-setup-workstation/scripts/bounded-child.mjs +159 -0
- package/plugins/lisa-agy/skills/lisa-setup-workstation/scripts/workstation.mjs +5 -4
- 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-copilot/scripts/design-source-gate.mjs +19 -1
- package/plugins/lisa-copilot/scripts/plugin-sync-explain.mjs +23 -1
- package/plugins/lisa-copilot/scripts/project-ideation-idempotency-harness.mjs +20 -1
- package/plugins/lisa-copilot/skills/lisa-remote-dispatch/scripts/dispatch.mjs +4 -3
- package/plugins/lisa-copilot/skills/lisa-secrets-access/scripts/providers.mjs +11 -5
- package/plugins/lisa-copilot/skills/lisa-secrets-access/scripts/sync-secret-to-ci.mjs +3 -2
- package/plugins/lisa-copilot/skills/lisa-setup-remote-env/scripts/setup-remote-env.mjs +36 -23
- package/plugins/lisa-copilot/skills/lisa-setup-remote-env/scripts/verify-remote-env.mjs +7 -3
- package/plugins/lisa-copilot/skills/lisa-setup-workstation/scripts/bounded-child.mjs +159 -0
- package/plugins/lisa-copilot/skills/lisa-setup-workstation/scripts/workstation.mjs +5 -4
- package/plugins/lisa-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-cursor/scripts/design-source-gate.mjs +19 -1
- package/plugins/lisa-cursor/scripts/plugin-sync-explain.mjs +23 -1
- package/plugins/lisa-cursor/scripts/project-ideation-idempotency-harness.mjs +20 -1
- package/plugins/lisa-cursor/skills/lisa-remote-dispatch/scripts/dispatch.mjs +4 -3
- package/plugins/lisa-cursor/skills/lisa-secrets-access/scripts/providers.mjs +11 -5
- package/plugins/lisa-cursor/skills/lisa-secrets-access/scripts/sync-secret-to-ci.mjs +3 -2
- package/plugins/lisa-cursor/skills/lisa-setup-remote-env/scripts/setup-remote-env.mjs +36 -23
- package/plugins/lisa-cursor/skills/lisa-setup-remote-env/scripts/verify-remote-env.mjs +7 -3
- package/plugins/lisa-cursor/skills/lisa-setup-workstation/scripts/bounded-child.mjs +159 -0
- package/plugins/lisa-cursor/skills/lisa-setup-workstation/scripts/workstation.mjs +5 -4
- 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/hooks/enforce-config-extensions.mjs +55 -0
- 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-copilot/hooks/enforce-config-extensions.mjs +55 -0
- package/plugins/lisa-harper-fabric-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-harper-fabric-cursor/hooks/enforce-config-extensions.mjs +55 -0
- 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/scripts/diff-guard.mjs +3 -2
- package/plugins/lisa-wiki/scripts/ensure-wiki.mjs +3 -2
- package/plugins/lisa-wiki/scripts/ingest-git.mjs +12 -6
- package/plugins/lisa-wiki/scripts/lib/bounded-child.mjs +159 -0
- package/plugins/lisa-wiki/scripts/verify-migration.mjs +3 -2
- package/plugins/lisa-wiki/scripts/verify-wiki-safety.mjs +15 -5
- package/plugins/lisa-wiki-agy/plugin.json +1 -1
- package/plugins/lisa-wiki-agy/scripts/diff-guard.mjs +3 -2
- package/plugins/lisa-wiki-agy/scripts/ensure-wiki.mjs +3 -2
- package/plugins/lisa-wiki-agy/scripts/ingest-git.mjs +12 -6
- package/plugins/lisa-wiki-agy/scripts/lib/bounded-child.mjs +159 -0
- package/plugins/lisa-wiki-agy/scripts/verify-migration.mjs +3 -2
- package/plugins/lisa-wiki-agy/scripts/verify-wiki-safety.mjs +15 -5
- package/plugins/lisa-wiki-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-wiki-copilot/scripts/diff-guard.mjs +3 -2
- package/plugins/lisa-wiki-copilot/scripts/ensure-wiki.mjs +3 -2
- package/plugins/lisa-wiki-copilot/scripts/ingest-git.mjs +12 -6
- package/plugins/lisa-wiki-copilot/scripts/lib/bounded-child.mjs +159 -0
- package/plugins/lisa-wiki-copilot/scripts/verify-migration.mjs +3 -2
- package/plugins/lisa-wiki-copilot/scripts/verify-wiki-safety.mjs +15 -5
- package/plugins/lisa-wiki-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-wiki-cursor/scripts/diff-guard.mjs +3 -2
- package/plugins/lisa-wiki-cursor/scripts/ensure-wiki.mjs +3 -2
- package/plugins/lisa-wiki-cursor/scripts/ingest-git.mjs +12 -6
- package/plugins/lisa-wiki-cursor/scripts/lib/bounded-child.mjs +159 -0
- package/plugins/lisa-wiki-cursor/scripts/verify-migration.mjs +3 -2
- package/plugins/lisa-wiki-cursor/scripts/verify-wiki-safety.mjs +15 -5
- package/plugins/src/base/scripts/design-source-gate.mjs +19 -1
- package/plugins/src/base/scripts/plugin-sync-explain.mjs +23 -1
- package/plugins/src/base/scripts/project-ideation-idempotency-harness.mjs +20 -1
- package/plugins/src/base/skills/lisa-remote-dispatch/scripts/dispatch.mjs +4 -3
- package/plugins/src/base/skills/lisa-secrets-access/scripts/providers.mjs +11 -5
- package/plugins/src/base/skills/lisa-secrets-access/scripts/sync-secret-to-ci.mjs +3 -2
- package/plugins/src/base/skills/lisa-setup-remote-env/scripts/setup-remote-env.mjs +36 -23
- package/plugins/src/base/skills/lisa-setup-remote-env/scripts/verify-remote-env.mjs +7 -3
- package/plugins/src/base/skills/lisa-setup-workstation/scripts/bounded-child.mjs +159 -0
- package/plugins/src/base/skills/lisa-setup-workstation/scripts/workstation.mjs +5 -4
- package/plugins/src/harper-fabric/hooks/enforce-config-extensions.mjs +55 -0
- package/plugins/src/wiki/scripts/diff-guard.mjs +3 -2
- package/plugins/src/wiki/scripts/ensure-wiki.mjs +3 -2
- package/plugins/src/wiki/scripts/ingest-git.mjs +12 -6
- package/plugins/src/wiki/scripts/lib/bounded-child.mjs +159 -0
- package/plugins/src/wiki/scripts/verify-migration.mjs +3 -2
- package/plugins/src/wiki/scripts/verify-wiki-safety.mjs +15 -5
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
* must include the advisory run fields project-ideation promises to write.
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
|
+
import { spawnSync } from "node:child_process";
|
|
12
13
|
import {
|
|
13
14
|
existsSync,
|
|
14
15
|
mkdirSync,
|
|
@@ -17,7 +18,20 @@ import {
|
|
|
17
18
|
rmSync,
|
|
18
19
|
} from "node:fs";
|
|
19
20
|
import { dirname, resolve } from "node:path";
|
|
20
|
-
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Hang-detector deadline for a child of this script, in milliseconds.
|
|
24
|
+
*
|
|
25
|
+
* Written inline rather than imported. The shared `bounded-child.mjs` lives in
|
|
26
|
+
* the `skills/` tree because the built Codex artifact copies `skills/` and
|
|
27
|
+
* nothing else — and these plugin-ROOT scripts are not in that artifact at all,
|
|
28
|
+
* so reaching across into `../skills/` would invent a dependency between two
|
|
29
|
+
* halves of the plugin for no gain.
|
|
30
|
+
*
|
|
31
|
+
* A ceiling, not a budget: `git` through PATH on macOS goes via Apple's `xcrun`
|
|
32
|
+
* shim, measured over 20s under load (CodySwannGT/lisa#2887).
|
|
33
|
+
*/
|
|
34
|
+
const CHILD_BUDGET_MS = 30_000;
|
|
21
35
|
|
|
22
36
|
const args = parseArgs(process.argv.slice(2));
|
|
23
37
|
|
|
@@ -112,6 +126,9 @@ function requireValue(input, key) {
|
|
|
112
126
|
*/
|
|
113
127
|
function runCommand(label, command) {
|
|
114
128
|
const result = spawnSync(command, {
|
|
129
|
+
killSignal: "SIGKILL",
|
|
130
|
+
// The harness drives a whole ideation run; a hang detector, not a budget.
|
|
131
|
+
timeout: 30 * 60 * 1000,
|
|
115
132
|
shell: true,
|
|
116
133
|
stdio: "inherit",
|
|
117
134
|
env: process.env,
|
|
@@ -272,6 +289,8 @@ function memoryContainsRunEntry(memoryFile, expected) {
|
|
|
272
289
|
function runJson(command, argv) {
|
|
273
290
|
const result = spawnSync(command, argv, {
|
|
274
291
|
encoding: "utf8",
|
|
292
|
+
killSignal: "SIGKILL",
|
|
293
|
+
timeout: CHILD_BUDGET_MS,
|
|
275
294
|
stdio: ["ignore", "pipe", "pipe"],
|
|
276
295
|
});
|
|
277
296
|
|
|
@@ -19,7 +19,6 @@
|
|
|
19
19
|
* @module dispatch
|
|
20
20
|
*/
|
|
21
21
|
|
|
22
|
-
import { execFileSync } from "node:child_process";
|
|
23
22
|
import {
|
|
24
23
|
existsSync,
|
|
25
24
|
mkdirSync,
|
|
@@ -30,6 +29,8 @@ import {
|
|
|
30
29
|
import { dirname, join, resolve } from "node:path";
|
|
31
30
|
import { fileURLToPath } from "node:url";
|
|
32
31
|
|
|
32
|
+
import { boundedChildOutput } from "../../lisa-setup-workstation/scripts/bounded-child.mjs";
|
|
33
|
+
|
|
33
34
|
/** Surfaces this dispatcher knows how to reach. `local` means "do not dispatch". */
|
|
34
35
|
export const EXECUTION_ENVS = new Set(["local", "codex-cloud", "claude-web"]);
|
|
35
36
|
|
|
@@ -249,7 +250,7 @@ function dispatchCodexCloud(block, prompt, payload) {
|
|
|
249
250
|
const args = buildCodexArgs(block, prompt);
|
|
250
251
|
let output;
|
|
251
252
|
try {
|
|
252
|
-
output =
|
|
253
|
+
output = boundedChildOutput("codex", args, {
|
|
253
254
|
encoding: "utf8",
|
|
254
255
|
stdio: ["ignore", "pipe", "pipe"],
|
|
255
256
|
});
|
|
@@ -432,7 +433,7 @@ function resolveBearerToken(block) {
|
|
|
432
433
|
"resolve-secret.mjs"
|
|
433
434
|
);
|
|
434
435
|
try {
|
|
435
|
-
return
|
|
436
|
+
return boundedChildOutput("node", [resolver, "get", name], {
|
|
436
437
|
encoding: "utf8",
|
|
437
438
|
stdio: ["ignore", "pipe", "pipe"],
|
|
438
439
|
}).trim();
|
|
@@ -9,10 +9,13 @@
|
|
|
9
9
|
* @module providers
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
|
-
import { execFileSync } from "node:child_process";
|
|
13
|
-
|
|
14
12
|
import { readBootstrapFile } from "./bootstrap-store.mjs";
|
|
15
13
|
|
|
14
|
+
import {
|
|
15
|
+
boundedChildOutput,
|
|
16
|
+
rethrowIfChildTimeout,
|
|
17
|
+
} from "../../lisa-setup-workstation/scripts/bounded-child.mjs";
|
|
18
|
+
|
|
16
19
|
/**
|
|
17
20
|
* A provider key becomes a shell variable name on materializing surfaces, so
|
|
18
21
|
* only names valid in every POSIX-like shell are accepted. Anything else stays
|
|
@@ -154,12 +157,15 @@ function fromKeychain(key) {
|
|
|
154
157
|
// `bootstrapToken` has no handler — so a raw filesystem error would
|
|
155
158
|
// replace the curated "not found in: ..." message with a stack trace.
|
|
156
159
|
if (process.platform !== "darwin") return readBootstrapFile(key);
|
|
157
|
-
return
|
|
160
|
+
return boundedChildOutput(
|
|
158
161
|
"security",
|
|
159
162
|
["find-generic-password", "-s", key, "-a", process.env.USER ?? "", "-w"],
|
|
160
163
|
{ encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }
|
|
161
164
|
).trim();
|
|
162
|
-
} catch {
|
|
165
|
+
} catch (error) {
|
|
166
|
+
// A killed child must not read as "this provider has no such
|
|
167
|
+
// secret" — that is a claim about the vault, and the caller acts on it.
|
|
168
|
+
rethrowIfChildTimeout(error);
|
|
163
169
|
return "";
|
|
164
170
|
}
|
|
165
171
|
}
|
|
@@ -223,7 +229,7 @@ export function fetchRaw(cfg) {
|
|
|
223
229
|
* @returns {string} Captured stdout.
|
|
224
230
|
*/
|
|
225
231
|
function run(bin, args, env) {
|
|
226
|
-
return
|
|
232
|
+
return boundedChildOutput(bin, args, {
|
|
227
233
|
encoding: "utf8",
|
|
228
234
|
env,
|
|
229
235
|
stdio: ["ignore", "pipe", "pipe"],
|
|
@@ -47,13 +47,14 @@
|
|
|
47
47
|
* @module sync-secret-to-ci
|
|
48
48
|
*/
|
|
49
49
|
|
|
50
|
-
import { execFileSync } from "node:child_process";
|
|
51
50
|
import { fileURLToPath } from "node:url";
|
|
52
51
|
import { realpathSync } from "node:fs";
|
|
53
52
|
|
|
54
53
|
import { ENV_KEY, fetchAll } from "./providers.mjs";
|
|
55
54
|
import { readConfig } from "./surfaces.mjs";
|
|
56
55
|
|
|
56
|
+
import { boundedChildOutput } from "../../lisa-setup-workstation/scripts/bounded-child.mjs";
|
|
57
|
+
|
|
57
58
|
/**
|
|
58
59
|
* How many secrets to ask for per listing page.
|
|
59
60
|
*
|
|
@@ -266,7 +267,7 @@ export function confirmPresent(payload, dest) {
|
|
|
266
267
|
*/
|
|
267
268
|
function gh(args, options = {}) {
|
|
268
269
|
try {
|
|
269
|
-
return
|
|
270
|
+
return boundedChildOutput("gh", args, {
|
|
270
271
|
encoding: "utf8",
|
|
271
272
|
stdio: ["pipe", "pipe", "pipe"],
|
|
272
273
|
...options,
|
|
@@ -28,7 +28,6 @@
|
|
|
28
28
|
* @module setup-remote-env
|
|
29
29
|
*/
|
|
30
30
|
|
|
31
|
-
import { execFileSync } from "node:child_process";
|
|
32
31
|
import { createHash } from "node:crypto";
|
|
33
32
|
import {
|
|
34
33
|
chmodSync,
|
|
@@ -51,6 +50,8 @@ import { pathDirs } from "../../lisa-setup-workstation/scripts/catalogue.mjs";
|
|
|
51
50
|
|
|
52
51
|
import { assertPinned, extractVersion, planToolchain } from "./toolchain.mjs";
|
|
53
52
|
|
|
53
|
+
import { boundedChildOutput } from "../../lisa-setup-workstation/scripts/bounded-child.mjs";
|
|
54
|
+
|
|
54
55
|
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
55
56
|
|
|
56
57
|
/**
|
|
@@ -117,7 +118,7 @@ export function readRemoteEnvConfig(cwd = process.cwd()) {
|
|
|
117
118
|
* @param {Function} [exec] Command runner, for tests.
|
|
118
119
|
* @returns {{version: string|null, present: boolean}} Probe result.
|
|
119
120
|
*/
|
|
120
|
-
export function probe(name, exec =
|
|
121
|
+
export function probe(name, exec = boundedChildOutput) {
|
|
121
122
|
try {
|
|
122
123
|
const out = exec(name, ["--version"], {
|
|
123
124
|
encoding: "utf8",
|
|
@@ -169,19 +170,23 @@ function installReleaseZip(tool, binDir) {
|
|
|
169
170
|
mkdirSync(temporary, { recursive: true });
|
|
170
171
|
try {
|
|
171
172
|
const archive = join(temporary, "download.zip");
|
|
172
|
-
|
|
173
|
+
boundedChildOutput("curl", ["-fsSL", tool.url, "-o", archive], {
|
|
173
174
|
stdio: "inherit",
|
|
174
175
|
});
|
|
175
176
|
// Verify before unpacking, not after. An unexpected archive must fail
|
|
176
177
|
// before any of its contents reach a directory that is on PATH.
|
|
177
178
|
verifyChecksum(archive, tool.sha256, tool.name);
|
|
178
|
-
|
|
179
|
+
boundedChildOutput("unzip", ["-q", "-o", archive, "-d", temporary], {
|
|
179
180
|
stdio: "inherit",
|
|
180
181
|
});
|
|
181
182
|
const binary = join(temporary, tool.binary ?? tool.name);
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
183
|
+
boundedChildOutput(
|
|
184
|
+
"install",
|
|
185
|
+
["-m", "0755", binary, join(binDir, tool.name)],
|
|
186
|
+
{
|
|
187
|
+
stdio: "inherit",
|
|
188
|
+
}
|
|
189
|
+
);
|
|
185
190
|
} finally {
|
|
186
191
|
rmSync(temporary, { recursive: true, force: true });
|
|
187
192
|
}
|
|
@@ -202,19 +207,23 @@ function installReleaseTar(tool, binDir) {
|
|
|
202
207
|
mkdirSync(temporary, { recursive: true });
|
|
203
208
|
try {
|
|
204
209
|
const archive = join(temporary, "download.tar.gz");
|
|
205
|
-
|
|
210
|
+
boundedChildOutput("curl", ["-fsSL", tool.url, "-o", archive], {
|
|
206
211
|
stdio: "inherit",
|
|
207
212
|
});
|
|
208
213
|
verifyChecksum(archive, tool.sha256, tool.name);
|
|
209
|
-
|
|
214
|
+
boundedChildOutput("tar", ["-xzf", archive, "-C", temporary], {
|
|
210
215
|
stdio: "inherit",
|
|
211
216
|
});
|
|
212
217
|
// Release tarballs usually nest under a versioned directory, so `binary` is
|
|
213
218
|
// a path within the archive rather than a bare name.
|
|
214
219
|
const binary = join(temporary, tool.binary ?? tool.name);
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
220
|
+
boundedChildOutput(
|
|
221
|
+
"install",
|
|
222
|
+
["-m", "0755", binary, join(binDir, tool.name)],
|
|
223
|
+
{
|
|
224
|
+
stdio: "inherit",
|
|
225
|
+
}
|
|
226
|
+
);
|
|
218
227
|
} finally {
|
|
219
228
|
rmSync(temporary, { recursive: true, force: true });
|
|
220
229
|
}
|
|
@@ -260,7 +269,7 @@ function installReleaseTree(tool, binDir) {
|
|
|
260
269
|
mkdirSync(temporary, { recursive: true });
|
|
261
270
|
try {
|
|
262
271
|
const archive = join(temporary, "download.archive");
|
|
263
|
-
|
|
272
|
+
boundedChildOutput("curl", ["-fsSL", tool.url, "-o", archive], {
|
|
264
273
|
stdio: "inherit",
|
|
265
274
|
});
|
|
266
275
|
// Verify before unpacking, as everywhere else here: an unexpected archive
|
|
@@ -273,11 +282,11 @@ function installReleaseTree(tool, binDir) {
|
|
|
273
282
|
rmSync(prefix, { recursive: true, force: true });
|
|
274
283
|
mkdirSync(prefix, { recursive: true });
|
|
275
284
|
if (tool.url.endsWith(".zip")) {
|
|
276
|
-
|
|
285
|
+
boundedChildOutput("unzip", ["-q", "-o", archive, "-d", prefix], {
|
|
277
286
|
stdio: "inherit",
|
|
278
287
|
});
|
|
279
288
|
} else {
|
|
280
|
-
|
|
289
|
+
boundedChildOutput("tar", ["-xzf", archive, "-C", prefix], {
|
|
281
290
|
stdio: "inherit",
|
|
282
291
|
});
|
|
283
292
|
}
|
|
@@ -330,7 +339,7 @@ function installReleaseBinary(tool, binDir) {
|
|
|
330
339
|
mkdirSync(temporary, { recursive: true });
|
|
331
340
|
try {
|
|
332
341
|
const artifact = join(temporary, tool.name);
|
|
333
|
-
|
|
342
|
+
boundedChildOutput("curl", ["-fsSL", tool.url, "-o", artifact], {
|
|
334
343
|
stdio: "inherit",
|
|
335
344
|
});
|
|
336
345
|
// Verify before it reaches a directory on PATH. For this kind that ordering
|
|
@@ -338,9 +347,13 @@ function installReleaseBinary(tool, binDir) {
|
|
|
338
347
|
// executable, so a wrong artifact placed first is a wrong artifact that can
|
|
339
348
|
// be run.
|
|
340
349
|
verifyChecksum(artifact, tool.sha256, tool.name);
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
350
|
+
boundedChildOutput(
|
|
351
|
+
"install",
|
|
352
|
+
["-m", "0755", artifact, join(binDir, tool.name)],
|
|
353
|
+
{
|
|
354
|
+
stdio: "inherit",
|
|
355
|
+
}
|
|
356
|
+
);
|
|
344
357
|
} finally {
|
|
345
358
|
rmSync(temporary, { recursive: true, force: true });
|
|
346
359
|
}
|
|
@@ -351,7 +364,7 @@ function installReleaseBinary(tool, binDir) {
|
|
|
351
364
|
* @param {object} tool Manifest entry.
|
|
352
365
|
*/
|
|
353
366
|
function installNpmGlobal(tool) {
|
|
354
|
-
|
|
367
|
+
boundedChildOutput(
|
|
355
368
|
"npm",
|
|
356
369
|
["install", "--global", `${tool.package}@${tool.version}`],
|
|
357
370
|
{
|
|
@@ -495,7 +508,7 @@ function runHook(hook, dryRun) {
|
|
|
495
508
|
console.log(`\nProject hook: ${hook}`);
|
|
496
509
|
if (dryRun) return;
|
|
497
510
|
chmodSync(path, 0o755);
|
|
498
|
-
|
|
511
|
+
boundedChildOutput("bash", [path], { stdio: "inherit" });
|
|
499
512
|
}
|
|
500
513
|
|
|
501
514
|
/** Phases this runner can execute, in the order they must happen. */
|
|
@@ -1181,7 +1194,7 @@ export function emitContainer({
|
|
|
1181
1194
|
* skills has no detector, and that is not a reason to refuse to provision.
|
|
1182
1195
|
* @param {Function} [exec] Seam for tests.
|
|
1183
1196
|
*/
|
|
1184
|
-
function reportUndeclaredTooling(exec =
|
|
1197
|
+
function reportUndeclaredTooling(exec = boundedChildOutput) {
|
|
1185
1198
|
let script;
|
|
1186
1199
|
try {
|
|
1187
1200
|
script = siblingScript("lisa-detect-tooling", "detect-tooling.mjs");
|
|
@@ -1489,7 +1502,7 @@ async function main() {
|
|
|
1489
1502
|
if (hook.reason) console.log(` ${hook.reason}`);
|
|
1490
1503
|
}
|
|
1491
1504
|
try {
|
|
1492
|
-
|
|
1505
|
+
boundedChildOutput(
|
|
1493
1506
|
"node",
|
|
1494
1507
|
dryRun ? [materialize, "--dry-run"] : [materialize],
|
|
1495
1508
|
{ stdio: "inherit" }
|
|
@@ -21,7 +21,6 @@
|
|
|
21
21
|
* @module verify-remote-env
|
|
22
22
|
*/
|
|
23
23
|
|
|
24
|
-
import { execFileSync } from "node:child_process";
|
|
25
24
|
import { fileURLToPath } from "node:url";
|
|
26
25
|
import { existsSync, readFileSync, statSync, realpathSync } from "node:fs";
|
|
27
26
|
import { join } from "node:path";
|
|
@@ -29,6 +28,8 @@ import { join } from "node:path";
|
|
|
29
28
|
import { probe, readRemoteEnvConfig } from "./setup-remote-env.mjs";
|
|
30
29
|
import { planToolchain } from "./toolchain.mjs";
|
|
31
30
|
|
|
31
|
+
import { boundedChildOutput } from "../../lisa-setup-workstation/scripts/bounded-child.mjs";
|
|
32
|
+
|
|
32
33
|
/** Collected results, so every check runs before anything reports failure. */
|
|
33
34
|
const results = [];
|
|
34
35
|
|
|
@@ -73,12 +74,15 @@ function checkMode(path, mode, label) {
|
|
|
73
74
|
*/
|
|
74
75
|
function node(args) {
|
|
75
76
|
try {
|
|
76
|
-
const out =
|
|
77
|
+
const out = boundedChildOutput("node", args, {
|
|
77
78
|
encoding: "utf8",
|
|
78
79
|
stdio: ["ignore", "pipe", "pipe"],
|
|
79
80
|
});
|
|
80
81
|
return { ok: true, out: out.trim() };
|
|
81
82
|
} catch (err) {
|
|
83
|
+
// KEPT: `ok: false` fails the check either way, and a killed child's
|
|
84
|
+
// `err.message` already reads "... ETIMEDOUT", so the timeout names
|
|
85
|
+
// itself. This is the shape the others are being moved toward.
|
|
82
86
|
return { ok: false, out: String(err.stderr ?? err.message).trim() };
|
|
83
87
|
}
|
|
84
88
|
}
|
|
@@ -149,7 +153,7 @@ export function verifyNotProxied(required, env, report) {
|
|
|
149
153
|
*/
|
|
150
154
|
function verifyCleanCheckout() {
|
|
151
155
|
try {
|
|
152
|
-
const out =
|
|
156
|
+
const out = boundedChildOutput("git", ["status", "--porcelain"], {
|
|
153
157
|
encoding: "utf8",
|
|
154
158
|
stdio: ["ignore", "pipe", "ignore"],
|
|
155
159
|
});
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* bounded-child — the plugin payload's child-start deadline.
|
|
3
|
+
*
|
|
4
|
+
* @remarks
|
|
5
|
+
* ## Why this exists separately from `scripts/lib/bounded-spawn.mjs`
|
|
6
|
+
*
|
|
7
|
+
* Same property, different reach. A plugin payload cannot import from
|
|
8
|
+
* `scripts/lib/`: the built plugin is a self-contained copy of
|
|
9
|
+
* `plugins/src/<name>/`, and nothing outside that tree exists at runtime. So the
|
|
10
|
+
* rule is written out here rather than pointed at — the accommodation
|
|
11
|
+
* `threshold-ratchet.mjs` and `preflight-secrets.mjs` already make for their
|
|
12
|
+
* entry guards.
|
|
13
|
+
*
|
|
14
|
+
* It is a MODULE rather than 28 inline copies because the fanout preserves
|
|
15
|
+
* relative imports. Measured in the built output, not assumed:
|
|
16
|
+
* `plugins/lisa/skills/lisa-setup-workstation/scripts/catalogue.mjs` exists and
|
|
17
|
+
* is imported as `../../lisa-setup-workstation/scripts/catalogue.mjs` from a
|
|
18
|
+
* sibling skill. The build copies the plugin tree wholesale, so a path that
|
|
19
|
+
* resolves in `plugins/src/base/` resolves in all five copies.
|
|
20
|
+
*
|
|
21
|
+
* That distinction matters because the accommodation above is narrower than its
|
|
22
|
+
* wording suggests. `threshold-ratchet.mjs` cannot import a sibling because it
|
|
23
|
+
* is **materialized into other lanes** — `typescript/`, `rails/` — where the
|
|
24
|
+
* sibling does not exist at the destination. A file that lives only in this
|
|
25
|
+
* plugin has no such problem.
|
|
26
|
+
*
|
|
27
|
+
* ## The one thing a deadline alone does not fix
|
|
28
|
+
*
|
|
29
|
+
* `spawnSync` does not throw when it kills a child. It returns:
|
|
30
|
+
*
|
|
31
|
+
* | outcome | `.code` | `.status` | `.signal` | streams |
|
|
32
|
+
* | --- | --- | --- | --- | --- |
|
|
33
|
+
* | killed at deadline | `"ETIMEDOUT"` | `null` | `"SIGKILL"` | **empty** |
|
|
34
|
+
* | ran, exited non-zero | *(absent)* | `3` | `null` | real |
|
|
35
|
+
* | binary not found | `"ENOENT"` | `null` | `null` | empty |
|
|
36
|
+
*
|
|
37
|
+
* So the shape that appears all over this tree —
|
|
38
|
+
*
|
|
39
|
+
* ```js
|
|
40
|
+
* return result.status === 0 ? result.stdout : null;
|
|
41
|
+
* ```
|
|
42
|
+
*
|
|
43
|
+
* — takes the benign branch for a killed child exactly as it does for a program
|
|
44
|
+
* that ran and said no. **Adding `timeout:` gives such a call site a deadline
|
|
45
|
+
* and changes nothing about its verdict.** There is no `catch` to fix, because
|
|
46
|
+
* the fail-open is in the conditional.
|
|
47
|
+
*
|
|
48
|
+
* That is why `boundedChild` THROWS rather than returning a flag: a caller that
|
|
49
|
+
* does nothing inherits fail-closed behaviour, and only one that deliberately
|
|
50
|
+
* catches can proceed.
|
|
51
|
+
* ## Why it lives in THIS skill's directory
|
|
52
|
+
*
|
|
53
|
+
* It is not about workstation setup, and it is here anyway, for a measured
|
|
54
|
+
* reason. The built Codex artifact — `plugins/lisa/.codex-plugin/` — copies
|
|
55
|
+
* `skills/` and nothing else: no `scripts/`, `agents/`, `commands/` or
|
|
56
|
+
* `rules/`. So a skill script importing `../../../scripts/lib/…` resolves to a
|
|
57
|
+
* path that does not exist in that copy, while `../../<skill>/scripts/…`
|
|
58
|
+
* resolves everywhere, which is exactly why `catalogue.mjs` beside this file is
|
|
59
|
+
* already imported that way from a sibling skill.
|
|
60
|
+
*
|
|
61
|
+
* A directory of its own under `skills/` was the tidier home and was rejected
|
|
62
|
+
* on evidence: every `skills/` subdirectory in this tree is a real skill with a
|
|
63
|
+
* `SKILL.md`, the generators parse that file to build their artifacts, and a
|
|
64
|
+
* directory without one is a shape nothing here has ever produced. Choosing an
|
|
65
|
+
* unproven layout is what put the import in the wrong place to begin with.
|
|
66
|
+
*
|
|
67
|
+
* **Do not delete this as unused from its host skill.** Its importers are in
|
|
68
|
+
* other skills and in `../../../scripts/`; `lisa-setup-workstation` hosts it,
|
|
69
|
+
* it does not own it.
|
|
70
|
+
* @module bounded-child
|
|
71
|
+
*/
|
|
72
|
+
|
|
73
|
+
import { execFileSync, spawnSync } from "node:child_process";
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Deadline a child gets when the caller states none, in milliseconds.
|
|
77
|
+
*
|
|
78
|
+
* A hang detector, not a performance budget. `git` resolved through `PATH` on
|
|
79
|
+
* macOS goes via Apple's `xcrun` shim, measured at over 20 seconds under load
|
|
80
|
+
* against 11ms for a real binary (CodySwannGT/lisa#2887) — so a tighter
|
|
81
|
+
* deadline would make this module's own timeout the dominant failure mode, and
|
|
82
|
+
* the tool would start manufacturing the failures it exists to detect.
|
|
83
|
+
*/
|
|
84
|
+
export const CHILD_BUDGET_MS = 30_000;
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Whether this value is a child that was killed at its deadline.
|
|
88
|
+
*
|
|
89
|
+
* Reads `code` rather than an error class, because `ETIMEDOUT` is set by Node
|
|
90
|
+
* itself on all three call forms — it is a platform fact, not a convention this
|
|
91
|
+
* module invented, so the same test is correct anywhere including inline.
|
|
92
|
+
*
|
|
93
|
+
* Defensive about the shape: a `catch` binding is `unknown` and a thrown
|
|
94
|
+
* non-object is legal JavaScript, so this must answer `false` rather than crash
|
|
95
|
+
* the guard that was being careful.
|
|
96
|
+
* @param {unknown} error A caught value, or `result.error` from `spawnSync`.
|
|
97
|
+
* @returns {boolean} True when the child was killed at its deadline.
|
|
98
|
+
*/
|
|
99
|
+
export function isChildTimeout(error) {
|
|
100
|
+
return (
|
|
101
|
+
typeof error === "object" &&
|
|
102
|
+
error !== null &&
|
|
103
|
+
/** @type {{ code?: unknown }} */ (error).code === "ETIMEDOUT"
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Re-raise a caught value when it is a killed child, otherwise do nothing.
|
|
109
|
+
*
|
|
110
|
+
* The correct spelling is `throw error` — not wrapping — because a wrapped
|
|
111
|
+
* error loses the `code` the next frame up needs to make the same decision.
|
|
112
|
+
* @param {unknown} error The caught value.
|
|
113
|
+
* @returns {void}
|
|
114
|
+
* @throws {unknown} The original value, when it is a killed child.
|
|
115
|
+
*/
|
|
116
|
+
export function rethrowIfChildTimeout(error) {
|
|
117
|
+
if (isChildTimeout(error)) throw error;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* `spawnSync` with a deadline, refusing to return a killed child's result.
|
|
122
|
+
* @param {string} command The executable.
|
|
123
|
+
* @param {readonly string[]} args Arguments.
|
|
124
|
+
* @param {object} [options] `spawnSync` options; `timeout` overrides the default.
|
|
125
|
+
* @returns {import("node:child_process").SpawnSyncReturns<string>} The result.
|
|
126
|
+
* @throws {Error} When the child was killed at its deadline.
|
|
127
|
+
*/
|
|
128
|
+
export function boundedChild(command, args = [], options = {}) {
|
|
129
|
+
const result = spawnSync(command, [...args], {
|
|
130
|
+
...options,
|
|
131
|
+
// `SIGKILL` rather than the default `SIGTERM`: the hang this exists for is
|
|
132
|
+
// a process not servicing signals, and a `SIGTERM` a child ignores turns
|
|
133
|
+
// the deadline into a suggestion.
|
|
134
|
+
killSignal: "SIGKILL",
|
|
135
|
+
timeout: options.timeout ?? CHILD_BUDGET_MS,
|
|
136
|
+
});
|
|
137
|
+
if (isChildTimeout(result.error)) throw result.error;
|
|
138
|
+
return result;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* `execFileSync` with a deadline.
|
|
143
|
+
*
|
|
144
|
+
* `execFileSync` already throws on a timeout, so this changes the FAILURE MODE
|
|
145
|
+
* rather than adding one: without a `timeout` there is no deadline to reach and
|
|
146
|
+
* the call blocks for as long as the child lives.
|
|
147
|
+
* @param {string} command The executable.
|
|
148
|
+
* @param {readonly string[]} args Arguments.
|
|
149
|
+
* @param {object} [options] `execFileSync` options; `timeout` overrides the default.
|
|
150
|
+
* @returns {string|Buffer} Whatever the child wrote to stdout.
|
|
151
|
+
* @throws {Error} When the child was killed, or exited non-zero.
|
|
152
|
+
*/
|
|
153
|
+
export function boundedChildOutput(command, args = [], options = {}) {
|
|
154
|
+
return execFileSync(command, [...args], {
|
|
155
|
+
...options,
|
|
156
|
+
killSignal: "SIGKILL",
|
|
157
|
+
timeout: options.timeout ?? CHILD_BUDGET_MS,
|
|
158
|
+
});
|
|
159
|
+
}
|
|
@@ -33,10 +33,11 @@
|
|
|
33
33
|
* @module workstation
|
|
34
34
|
*/
|
|
35
35
|
|
|
36
|
-
import { execFileSync } from "node:child_process";
|
|
37
36
|
import { existsSync, lstatSync, mkdirSync, rmSync, symlinkSync } from "node:fs";
|
|
38
37
|
import { join } from "node:path";
|
|
39
38
|
|
|
39
|
+
import { boundedChildOutput } from "./bounded-child.mjs";
|
|
40
|
+
|
|
40
41
|
import {
|
|
41
42
|
AGENTS,
|
|
42
43
|
CHECKSUMMED,
|
|
@@ -56,7 +57,7 @@ import {
|
|
|
56
57
|
* @param {Function} [exec] Runner, injectable for tests.
|
|
57
58
|
* @returns {string|null} Absolute path, or null when absent.
|
|
58
59
|
*/
|
|
59
|
-
export function locate(name, exec =
|
|
60
|
+
export function locate(name, exec = boundedChildOutput) {
|
|
60
61
|
try {
|
|
61
62
|
return String(
|
|
62
63
|
exec("command", ["-v", name], { encoding: "utf8", shell: true })
|
|
@@ -76,7 +77,7 @@ export function locate(name, exec = execFileSync) {
|
|
|
76
77
|
* @param {Function} [exec] Runner, injectable for tests.
|
|
77
78
|
* @returns {string|null} A dotted version, or null.
|
|
78
79
|
*/
|
|
79
|
-
export function version(name, exec =
|
|
80
|
+
export function version(name, exec = boundedChildOutput) {
|
|
80
81
|
const read = out => {
|
|
81
82
|
const match = /(\d+(?:\.\d+)+)/.exec(String(out ?? ""));
|
|
82
83
|
return match ? match[1] : null;
|
|
@@ -381,7 +382,7 @@ export function installEntry(entry, row, deps = {}) {
|
|
|
381
382
|
const run =
|
|
382
383
|
deps.run ??
|
|
383
384
|
((cmd, args, opts) =>
|
|
384
|
-
|
|
385
|
+
boundedChildOutput(cmd, args, { stdio: "inherit", ...opts }));
|
|
385
386
|
const find = deps.locate ?? locate;
|
|
386
387
|
|
|
387
388
|
try {
|