@codyswann/lisa 4.4.0 → 4.4.2
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/codex/mcp-installer.d.ts.map +1 -1
- package/dist/codex/mcp-installer.js +55 -10
- package/dist/codex/mcp-installer.js.map +1 -1
- package/dist/core/upstream-evidence-manifest.d.ts.map +1 -1
- package/dist/core/upstream-evidence-manifest.js +15 -13
- 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-secrets-access/scripts/materialize-secrets.mjs +130 -26
- package/plugins/lisa/skills/lisa-secrets-access/scripts/materialize-secrets.mjs +130 -26
- package/plugins/lisa-agy/plugin.json +1 -1
- package/plugins/lisa-agy/skills/lisa-secrets-access/scripts/materialize-secrets.mjs +130 -26
- 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/skills/lisa-secrets-access/scripts/materialize-secrets.mjs +130 -26
- package/plugins/lisa-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-cursor/skills/lisa-secrets-access/scripts/materialize-secrets.mjs +130 -26
- 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/plugins/src/base/skills/lisa-secrets-access/scripts/materialize-secrets.mjs +130 -26
- package/scripts/check-delivery-deletion-conflicts.mjs +2 -2
- package/scripts/check-derived-artifacts.mjs +24 -5
- package/scripts/check-orphan-test-processes.mjs +2 -2
- package/scripts/check-security-floors.mjs +20 -9
- package/scripts/check-template-workflow-refs.mjs +23 -8
- package/scripts/check-workflow-package-paths.mjs +12 -8
- package/scripts/generate-lisa-owned-hash-ledger.mjs +12 -4
- package/scripts/generate-upstream-evidence-manifest.mjs +4 -4
- package/scripts/verify-health-agentic-built.mjs +4 -3
- package/scripts/verify-health-consumer-built.mjs +3 -2
- package/scripts/verify-health-contract-built.mjs +3 -2
- package/scripts/verify-health-deterministic-built.mjs +5 -4
|
@@ -68,11 +68,114 @@ function writeAtomic(destination, contents) {
|
|
|
68
68
|
* @param {object} [cfg] Resolved configuration.
|
|
69
69
|
* @returns {{count: number, dir: string}} What was written, and where.
|
|
70
70
|
*/
|
|
71
|
+
/**
|
|
72
|
+
* Marker version this build writes. Bumping it is SAFE, which is the point.
|
|
73
|
+
*
|
|
74
|
+
* The recognisers below match the FAMILY — the frozen identifier plus any
|
|
75
|
+
* version — rather than one literal string, so a block written by an older
|
|
76
|
+
* build is still found and replaced in place. Before that, the reader looked
|
|
77
|
+
* for the exact text it was about to write, so the first rename made it
|
|
78
|
+
* conclude there was no block here and append a second one.
|
|
79
|
+
*
|
|
80
|
+
* That failure is additive and silent, and this module is the worst place in
|
|
81
|
+
* Lisa for it: it writes into `~/.aws` files and a shell profile, outside any
|
|
82
|
+
* repository, where no apply, diff, or review ever revisits the result. An
|
|
83
|
+
* orphaned block in a shell profile is STILL SOURCED, and profiles apply
|
|
84
|
+
* assignments in order — so whichever block comes last wins, and an operator
|
|
85
|
+
* can end up running under credentials from a block Lisa believes it no longer
|
|
86
|
+
* manages. That is credential selection going wrong with no review step.
|
|
87
|
+
*
|
|
88
|
+
* Everywhere else in the codebase the mitigation is "widen the recogniser and
|
|
89
|
+
* accept the old shape until an apply normalises the file". There is no such
|
|
90
|
+
* operation here: the old population never drains and cannot be counted, so
|
|
91
|
+
* the recogniser must accept every past version permanently.
|
|
92
|
+
*
|
|
93
|
+
* Following `core/apply-receipt`, which treats a `schema_version` it does not
|
|
94
|
+
* recognise as NO RECEIPT rather than one it half-understands: on an
|
|
95
|
+
* unrecognised marker, fail toward redoing the work, not toward assuming it is
|
|
96
|
+
* done. Here that means replacing every family member found, so a file already
|
|
97
|
+
* carrying orphans from a past rename is repaired on the next run rather than
|
|
98
|
+
* accumulating one more.
|
|
99
|
+
*/
|
|
100
|
+
const MARKER_VERSION = "v2";
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* The frozen half of each marker. Changing one of these DOES orphan blocks —
|
|
104
|
+
* that is the whole contract, and it is why the version lives beside it.
|
|
105
|
+
*
|
|
106
|
+
* This does not make renaming impossible, it makes it deliberate: an innocuous
|
|
107
|
+
* text edit no longer orphans a block, and the one edit that still would is the
|
|
108
|
+
* one these comments forbid.
|
|
109
|
+
*/
|
|
110
|
+
const PROFILE_FAMILY = "lisa secrets (managed";
|
|
111
|
+
|
|
112
|
+
/** The frozen family identifier for an `~/.aws` managed region. */
|
|
113
|
+
const MANAGED_FAMILY = "managed by lisa-secrets-access";
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Build the start/end recognisers for one marker family.
|
|
117
|
+
*
|
|
118
|
+
* Matches the family followed by anything up to the closing delimiter, so
|
|
119
|
+
* `(managed)` — every block in the field today — and `(managed v2)` are both
|
|
120
|
+
* found by the same reader.
|
|
121
|
+
* @param {string} family Frozen family identifier.
|
|
122
|
+
* @returns {{start: RegExp, end: RegExp}} Global recognisers for the family.
|
|
123
|
+
*/
|
|
124
|
+
function familyRecognisers(family) {
|
|
125
|
+
const quoted = family.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
126
|
+
return {
|
|
127
|
+
start: new RegExp(`# >>> ${quoted}[^\\n]*>>>`, "g"),
|
|
128
|
+
end: new RegExp(`# <<< ${quoted}[^\\n]*<<<`, "g"),
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Index of the first family match at or after `from`, and its length.
|
|
134
|
+
* @param {string} text Haystack.
|
|
135
|
+
* @param {RegExp} recogniser Global family recogniser.
|
|
136
|
+
* @param {number} from Index to search from.
|
|
137
|
+
* @returns {{index: number, length: number}} `index` is -1 when absent.
|
|
138
|
+
*/
|
|
139
|
+
function findFamily(text, recogniser, from = 0) {
|
|
140
|
+
recogniser.lastIndex = from;
|
|
141
|
+
const match = recogniser.exec(text);
|
|
142
|
+
return match === null
|
|
143
|
+
? { index: -1, length: 0 }
|
|
144
|
+
: { index: match.index, length: match[0].length };
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Remove every managed block of one family, leaving the rest of the file.
|
|
149
|
+
*
|
|
150
|
+
* Every block, not just the first: a file that already carries orphans from a
|
|
151
|
+
* rename that happened before this fix must come out with exactly one block,
|
|
152
|
+
* or the fix would leave the damage it exists to prevent.
|
|
153
|
+
* @param {string} text File contents.
|
|
154
|
+
* @param {string} family Frozen family identifier.
|
|
155
|
+
* @returns {string} The file with every family block removed.
|
|
156
|
+
*/
|
|
157
|
+
function stripFamilyBlocks(text, family) {
|
|
158
|
+
const { start, end } = familyRecognisers(family);
|
|
159
|
+
let out = text;
|
|
160
|
+
for (;;) {
|
|
161
|
+
const opened = findFamily(out, start);
|
|
162
|
+
if (opened.index === -1) return out;
|
|
163
|
+
const closed = findFamily(out, end, opened.index + opened.length);
|
|
164
|
+
// A truncated block (opened, never closed) would otherwise swallow the rest
|
|
165
|
+
// of the file on every subsequent write.
|
|
166
|
+
const after =
|
|
167
|
+
closed.index === -1
|
|
168
|
+
? ""
|
|
169
|
+
: out.slice(closed.index + closed.length).replace(/^\n/, "");
|
|
170
|
+
out = `${out.slice(0, opened.index)}${after}`;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
71
174
|
/** Marks the block this owns, so it is replaced rather than appended twice. */
|
|
72
|
-
const PROFILE_MARKER =
|
|
175
|
+
const PROFILE_MARKER = `# >>> ${PROFILE_FAMILY} ${MARKER_VERSION}) >>>`;
|
|
73
176
|
|
|
74
177
|
/** Closes the managed block. */
|
|
75
|
-
const PROFILE_END =
|
|
178
|
+
const PROFILE_END = `# <<< ${PROFILE_FAMILY} ${MARKER_VERSION}) <<<`;
|
|
76
179
|
|
|
77
180
|
/**
|
|
78
181
|
* Identifies an `~/.aws` file as one this wrote, and may therefore replace.
|
|
@@ -81,10 +184,10 @@ const PROFILE_END = "# <<< lisa secrets (managed) <<<";
|
|
|
81
184
|
* consumer while still being the thing that distinguishes "our file, refresh
|
|
82
185
|
* it" from "someone else's file, leave it alone".
|
|
83
186
|
*/
|
|
84
|
-
const MANAGED_MARKER =
|
|
187
|
+
const MANAGED_MARKER = `# >>> ${MANAGED_FAMILY} ${MARKER_VERSION} >>>`;
|
|
85
188
|
|
|
86
189
|
/** Closes the managed region of an `~/.aws` file. */
|
|
87
|
-
const MANAGED_END =
|
|
190
|
+
const MANAGED_END = `# <<< ${MANAGED_FAMILY} ${MARKER_VERSION} <<<`;
|
|
88
191
|
|
|
89
192
|
/**
|
|
90
193
|
* Replace this module's delimited region in a file, preserving everything else.
|
|
@@ -99,22 +202,21 @@ const MANAGED_END = "# <<< managed by lisa-secrets-access <<<";
|
|
|
99
202
|
*/
|
|
100
203
|
export function upsertManagedBlock(current, body) {
|
|
101
204
|
const block = `${MANAGED_MARKER}\n${body.trimEnd()}\n${MANAGED_END}`;
|
|
102
|
-
const
|
|
205
|
+
const opened = findFamily(current, familyRecognisers(MANAGED_FAMILY).start);
|
|
103
206
|
|
|
104
|
-
if (
|
|
207
|
+
if (opened.index === -1) {
|
|
105
208
|
const prefix =
|
|
106
209
|
current && !current.endsWith("\n") ? `${current}\n` : current;
|
|
107
210
|
return `${prefix}${prefix ? "\n" : ""}${block}\n`;
|
|
108
211
|
}
|
|
109
212
|
|
|
110
|
-
|
|
111
|
-
//
|
|
112
|
-
//
|
|
113
|
-
const
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
return `${current.slice(0, start)}${block}\n${after}`;
|
|
213
|
+
// Everything this module owns is stripped first, so a file already carrying
|
|
214
|
+
// orphans from a rename that predates the family recogniser comes out with
|
|
215
|
+
// exactly one block rather than one more.
|
|
216
|
+
const withoutOurs = stripFamilyBlocks(current, MANAGED_FAMILY);
|
|
217
|
+
const head = withoutOurs.slice(0, opened.index);
|
|
218
|
+
const tail = withoutOurs.slice(opened.index);
|
|
219
|
+
return `${head}${block}\n${tail}`;
|
|
118
220
|
}
|
|
119
221
|
|
|
120
222
|
/**
|
|
@@ -173,13 +275,18 @@ export function installProfileSourcing(valuesFile, options = {}) {
|
|
|
173
275
|
|
|
174
276
|
// Replace an existing managed block rather than appending another: this
|
|
175
277
|
// runs on every session, and an appended-forever profile is its own bug.
|
|
176
|
-
const
|
|
278
|
+
const opened = findFamily(current, familyRecognisers(PROFILE_FAMILY).start);
|
|
279
|
+
// Stripping EVERY family block before writing one is what makes an already
|
|
280
|
+
// orphaned profile self-heal. It matters more here than anywhere else in
|
|
281
|
+
// Lisa: an orphaned block in a shell profile is still sourced, and the last
|
|
282
|
+
// assignment wins, so a stale block silently selects the wrong credentials.
|
|
283
|
+
const withoutOurs = stripFamilyBlocks(current, PROFILE_FAMILY);
|
|
177
284
|
const next =
|
|
178
|
-
|
|
285
|
+
opened.index === -1
|
|
179
286
|
? `${current}${current.endsWith("\n") || !current ? "" : "\n"}\n${block}\n`
|
|
180
|
-
: `${
|
|
181
|
-
|
|
182
|
-
|
|
287
|
+
: `${withoutOurs.slice(0, opened.index)}${block}\n${withoutOurs
|
|
288
|
+
.slice(opened.index)
|
|
289
|
+
.replace(/^\n/, "")}`;
|
|
183
290
|
|
|
184
291
|
if (next !== current) {
|
|
185
292
|
write(file, next, { mode: 0o600 });
|
|
@@ -214,13 +321,10 @@ export function collidingProfiles(dir, names, io = {}) {
|
|
|
214
321
|
if (!exists(file)) return [];
|
|
215
322
|
|
|
216
323
|
const text = String(read(file, "utf8"));
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
? text
|
|
222
|
-
: text.slice(0, start) +
|
|
223
|
-
(endAt === -1 ? "" : text.slice(endAt + MANAGED_END.length));
|
|
324
|
+
// Every family block, not just the current version's. An orphan left by an
|
|
325
|
+
// older marker is still OUR previous output, so counting its profiles as a
|
|
326
|
+
// host collision would refuse to write the very names we wrote last time.
|
|
327
|
+
const outside = stripFamilyBlocks(text, MANAGED_FAMILY);
|
|
224
328
|
|
|
225
329
|
return names.filter(name =>
|
|
226
330
|
new RegExp(
|
|
@@ -82,12 +82,12 @@
|
|
|
82
82
|
*
|
|
83
83
|
* @module scripts/check-delivery-deletion-conflicts
|
|
84
84
|
*/
|
|
85
|
-
import { execFileSync } from "node:child_process";
|
|
86
85
|
import fs from "node:fs";
|
|
87
86
|
import path from "node:path";
|
|
88
87
|
import process from "node:process";
|
|
89
88
|
import { fileURLToPath } from "node:url";
|
|
90
89
|
|
|
90
|
+
import { boundedExecFileSync } from "./lib/bounded-spawn.mjs";
|
|
91
91
|
import { invokedAsScript } from "./lib/invoked-as-script.mjs";
|
|
92
92
|
|
|
93
93
|
const REPO_ROOT = path.resolve(
|
|
@@ -250,7 +250,7 @@ export function matchDeletion(delivered, deleted) {
|
|
|
250
250
|
function listTrackedFiles(root) {
|
|
251
251
|
let stdout;
|
|
252
252
|
try {
|
|
253
|
-
stdout =
|
|
253
|
+
stdout = boundedExecFileSync("git", ["-C", root, "ls-files", "-z"], {
|
|
254
254
|
encoding: "utf8",
|
|
255
255
|
maxBuffer: MAX_GIT_OUTPUT_BYTES,
|
|
256
256
|
stdio: ["ignore", "pipe", "ignore"],
|
|
@@ -44,10 +44,10 @@
|
|
|
44
44
|
* on a commit that owed nothing, which is the safe direction to be wrong in.
|
|
45
45
|
* @module scripts/check-derived-artifacts
|
|
46
46
|
*/
|
|
47
|
-
import { execFileSync } from "node:child_process";
|
|
48
47
|
import path from "node:path";
|
|
49
48
|
import process from "node:process";
|
|
50
49
|
|
|
50
|
+
import { boundedExecFileSync, isChildTimeout } from "./lib/bounded-spawn.mjs";
|
|
51
51
|
import { invokedAsScript } from "./lib/invoked-as-script.mjs";
|
|
52
52
|
|
|
53
53
|
const repoRoot = path.resolve(import.meta.dirname, "..");
|
|
@@ -118,18 +118,26 @@ export function requiresManifestCheck(stagedPaths) {
|
|
|
118
118
|
* Renames report their destination (`-M` plus `--diff-filter=R`), and deletions
|
|
119
119
|
* are included because removing a template is also a way to move what the
|
|
120
120
|
* artifacts describe.
|
|
121
|
-
*
|
|
121
|
+
*
|
|
122
|
+
* A KILLED child re-raises rather than returning empty. An empty list means
|
|
123
|
+
* "nothing staged touches the artifacts", which makes this guard SKIP — so a
|
|
124
|
+
* busy machine would let through the commit that moves a template without
|
|
125
|
+
* regenerating what describes it. "I could not ask" is not "there is nothing
|
|
126
|
+
* to check", and only one of those is safe to act on.
|
|
127
|
+
* @returns {string[]} Staged paths, empty when git said there were none
|
|
128
|
+
* @throws {Error} When the child was killed at its deadline
|
|
122
129
|
*/
|
|
123
130
|
export function stagedPaths() {
|
|
124
131
|
try {
|
|
125
|
-
return
|
|
132
|
+
return boundedExecFileSync(
|
|
126
133
|
"git",
|
|
127
134
|
["diff", "--cached", "--name-only", "--diff-filter=ACMRD"],
|
|
128
135
|
{ cwd: repoRoot, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }
|
|
129
136
|
)
|
|
130
137
|
.split("\n")
|
|
131
138
|
.filter(Boolean);
|
|
132
|
-
} catch {
|
|
139
|
+
} catch (error) {
|
|
140
|
+
if (isChildTimeout(error)) throw error;
|
|
133
141
|
return [];
|
|
134
142
|
}
|
|
135
143
|
}
|
|
@@ -172,13 +180,24 @@ export function readableFailure(raw) {
|
|
|
172
180
|
*/
|
|
173
181
|
function runCheck(script) {
|
|
174
182
|
try {
|
|
175
|
-
|
|
183
|
+
boundedExecFileSync(process.execPath, [script, "--check"], {
|
|
176
184
|
cwd: repoRoot,
|
|
177
185
|
encoding: "utf8",
|
|
178
186
|
stdio: ["ignore", "pipe", "pipe"],
|
|
179
187
|
});
|
|
180
188
|
return { ok: true, output: "" };
|
|
181
189
|
} catch (error) {
|
|
190
|
+
// Already fail-closed — `ok: false` fails the guard either way — so the
|
|
191
|
+
// catch stays. What it must NOT do is stay silent about a kill: a killed
|
|
192
|
+
// child hands back EMPTY streams, so a timeout would otherwise render as a
|
|
193
|
+
// generator that failed with no diagnostic, which reads as a content
|
|
194
|
+
// problem and sends the operator to the wrong place entirely.
|
|
195
|
+
if (isChildTimeout(error)) {
|
|
196
|
+
return {
|
|
197
|
+
ok: false,
|
|
198
|
+
output: `${script} was killed before it finished. This is a timeout, not a check failure — nothing about the artifact was measured.`,
|
|
199
|
+
};
|
|
200
|
+
}
|
|
182
201
|
const stderr = error?.stderr ?? "";
|
|
183
202
|
const stdout = error?.stdout ?? "";
|
|
184
203
|
return { ok: false, output: readableFailure(`${stdout}${stderr}`) };
|
|
@@ -44,8 +44,8 @@
|
|
|
44
44
|
*
|
|
45
45
|
* @module scripts/check-orphan-test-processes
|
|
46
46
|
*/
|
|
47
|
-
import { execFileSync } from "node:child_process";
|
|
48
47
|
import process from "node:process";
|
|
48
|
+
import { boundedExecFileSync } from "./lib/bounded-spawn.mjs";
|
|
49
49
|
import { invokedAsScript } from "./lib/invoked-as-script.mjs";
|
|
50
50
|
|
|
51
51
|
/**
|
|
@@ -168,7 +168,7 @@ export function selectOrphans(rows, minAgeSeconds) {
|
|
|
168
168
|
*/
|
|
169
169
|
function readProcessTable() {
|
|
170
170
|
// eslint-disable-next-line sonarjs/no-os-command-from-path -- fixed executable and argv
|
|
171
|
-
return
|
|
171
|
+
return boundedExecFileSync("ps", ["-eo", "pid,ppid,etime,command"], {
|
|
172
172
|
encoding: "utf8",
|
|
173
173
|
maxBuffer: 32 * 1024 * 1024,
|
|
174
174
|
});
|
|
@@ -23,10 +23,11 @@
|
|
|
23
23
|
* @module scripts/check-security-floors
|
|
24
24
|
*/
|
|
25
25
|
|
|
26
|
-
import { execFileSync } from "node:child_process";
|
|
27
26
|
import { existsSync, readFileSync } from "node:fs";
|
|
28
27
|
import { globSync } from "node:fs";
|
|
29
28
|
|
|
29
|
+
import { boundedExecFileSync } from "./lib/bounded-spawn.mjs";
|
|
30
|
+
|
|
30
31
|
/**
|
|
31
32
|
* Fixed absolute git locations, tried in order rather than a bare command name
|
|
32
33
|
* so a writeable directory early on `PATH` cannot decide which binary runs.
|
|
@@ -129,18 +130,28 @@ const NOT_A_MANIFEST = [
|
|
|
129
130
|
/**
|
|
130
131
|
* Every manifest git tracks, so the glob can be checked against reality.
|
|
131
132
|
*
|
|
132
|
-
* Returns null when the question cannot be asked — outside a checkout,
|
|
133
|
-
*
|
|
134
|
-
* missing": the whole defect being
|
|
133
|
+
* Returns null when the question cannot be asked — outside a checkout, with no
|
|
134
|
+
* git, or with the child killed at its deadline. A null is reported as "not
|
|
135
|
+
* reconciled", never folded into "nothing missing": the whole defect being
|
|
136
|
+
* fixed here is a scan that narrowed silently.
|
|
137
|
+
*
|
|
138
|
+
* The swallow is therefore KEPT for a timeout rather than kept by inheritance.
|
|
139
|
+
* A kill is one more way the question cannot be asked, and this function
|
|
140
|
+
* already routes "cannot ask" to the safe answer — so re-raising would trade a
|
|
141
|
+
* correct report for a crash and gain nothing.
|
|
135
142
|
* @returns {string[]|null} Tracked manifest paths, or null when unknowable.
|
|
136
143
|
*/
|
|
137
144
|
function trackedManifests() {
|
|
138
145
|
try {
|
|
139
|
-
return
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
146
|
+
return boundedExecFileSync(
|
|
147
|
+
GIT_BIN,
|
|
148
|
+
["ls-files", "-z", "*package.lisa.json"],
|
|
149
|
+
{
|
|
150
|
+
encoding: "utf8",
|
|
151
|
+
maxBuffer: 8 * 1024 * 1024,
|
|
152
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
153
|
+
}
|
|
154
|
+
)
|
|
144
155
|
.split("\0")
|
|
145
156
|
.filter(Boolean)
|
|
146
157
|
.filter(
|
|
@@ -57,12 +57,12 @@
|
|
|
57
57
|
*
|
|
58
58
|
* @module scripts/check-template-workflow-refs
|
|
59
59
|
*/
|
|
60
|
-
import { execFileSync } from "node:child_process";
|
|
61
60
|
import fs from "node:fs";
|
|
62
61
|
import path from "node:path";
|
|
63
62
|
import process from "node:process";
|
|
64
63
|
import { fileURLToPath } from "node:url";
|
|
65
64
|
|
|
65
|
+
import { boundedExecFileSync, isChildTimeout } from "./lib/bounded-spawn.mjs";
|
|
66
66
|
import { invokedAsScript } from "./lib/invoked-as-script.mjs";
|
|
67
67
|
|
|
68
68
|
const REPO_ROOT = path.resolve(
|
|
@@ -124,7 +124,7 @@ export function findWorkflowRefs(content) {
|
|
|
124
124
|
function listTrackedFiles(root) {
|
|
125
125
|
let stdout;
|
|
126
126
|
try {
|
|
127
|
-
stdout =
|
|
127
|
+
stdout = boundedExecFileSync("git", ["-C", root, "ls-files", "-z"], {
|
|
128
128
|
encoding: "utf8",
|
|
129
129
|
maxBuffer: MAX_GIT_OUTPUT_BYTES,
|
|
130
130
|
stdio: ["ignore", "pipe", "ignore"],
|
|
@@ -140,13 +140,19 @@ function listTrackedFiles(root) {
|
|
|
140
140
|
/**
|
|
141
141
|
* Whether `ref` names an object present in this clone.
|
|
142
142
|
*
|
|
143
|
+
* A KILLED child re-raises. `false` here does not mean "unknown", it means
|
|
144
|
+
* "this reference is broken" — a verdict the caller reports. So a busy machine
|
|
145
|
+
* would manufacture a failure about a reference that is perfectly fine, and a
|
|
146
|
+
* guard that invents failures is one people learn to re-run rather than read.
|
|
147
|
+
* That is the sibling harm to failing open, and it is just as corrosive.
|
|
143
148
|
* @param {string} root - the repository root.
|
|
144
149
|
* @param {string} ref - a tag, branch, or SHA.
|
|
145
150
|
* @returns {boolean} whether the ref resolves locally.
|
|
151
|
+
* @throws {Error} When the child was killed at its deadline
|
|
146
152
|
*/
|
|
147
153
|
function refExists(root, ref) {
|
|
148
154
|
try {
|
|
149
|
-
|
|
155
|
+
boundedExecFileSync(
|
|
150
156
|
"git",
|
|
151
157
|
["-C", root, "rev-parse", "--verify", `${ref}^{commit}`],
|
|
152
158
|
{
|
|
@@ -154,7 +160,8 @@ function refExists(root, ref) {
|
|
|
154
160
|
}
|
|
155
161
|
);
|
|
156
162
|
return true;
|
|
157
|
-
} catch {
|
|
163
|
+
} catch (error) {
|
|
164
|
+
if (isChildTimeout(error)) throw error;
|
|
158
165
|
return false;
|
|
159
166
|
}
|
|
160
167
|
}
|
|
@@ -162,18 +169,26 @@ function refExists(root, ref) {
|
|
|
162
169
|
/**
|
|
163
170
|
* Whether `target` exists in the tree named by `ref`.
|
|
164
171
|
*
|
|
172
|
+
* A KILLED child re-raises, for the reason given on `refExists` above: `false`
|
|
173
|
+
* is a verdict of "absent at that ref", not an admission of ignorance.
|
|
165
174
|
* @param {string} root - the repository root.
|
|
166
175
|
* @param {string} ref - a tag, branch, or SHA known to resolve.
|
|
167
176
|
* @param {string} target - a repo-relative path.
|
|
168
177
|
* @returns {boolean} whether the path exists at that ref.
|
|
178
|
+
* @throws {Error} When the child was killed at its deadline
|
|
169
179
|
*/
|
|
170
180
|
function pathExistsAtRef(root, ref, target) {
|
|
171
181
|
try {
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
182
|
+
boundedExecFileSync(
|
|
183
|
+
"git",
|
|
184
|
+
["-C", root, "cat-file", "-e", `${ref}:${target}`],
|
|
185
|
+
{
|
|
186
|
+
stdio: ["ignore", "ignore", "ignore"],
|
|
187
|
+
}
|
|
188
|
+
);
|
|
175
189
|
return true;
|
|
176
|
-
} catch {
|
|
190
|
+
} catch (error) {
|
|
191
|
+
if (isChildTimeout(error)) throw error;
|
|
177
192
|
return false;
|
|
178
193
|
}
|
|
179
194
|
}
|