@codyswann/lisa 4.4.1 → 4.4.3
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 +12 -29
- 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-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-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
- package/plugins/lisa/.codex-plugin/skills/lisa-parity-safety-net-rules/SKILL.md +0 -263
- package/plugins/lisa/.codex-plugin/skills/lisa-parity-safety-net-rules/agents/openai.yaml +0 -4
- package/plugins/lisa/commands/parity-safety-net-rules.md +0 -6
- package/plugins/lisa/skills/lisa-parity-safety-net-rules/SKILL.md +0 -263
- package/plugins/lisa/skills/lisa-parity-safety-net-rules/agents/openai.yaml +0 -4
- package/plugins/lisa-agy/commands/lisa/parity-safety-net-rules.md +0 -6
- package/plugins/lisa-agy/skills/lisa-parity-safety-net-rules/SKILL.md +0 -263
- package/plugins/lisa-copilot/commands/lisa/parity-safety-net-rules.md +0 -6
- package/plugins/lisa-copilot/skills/lisa-parity-safety-net-rules/SKILL.md +0 -263
- package/plugins/lisa-cursor/commands/lisa/parity-safety-net-rules.md +0 -6
- package/plugins/lisa-cursor/skills/lisa-parity-safety-net-rules/SKILL.md +0 -263
- package/plugins/src/base/commands/parity-safety-net-rules.md +0 -6
- package/plugins/src/base/skills/lisa-parity-safety-net-rules/SKILL.md +0 -263
|
@@ -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
|
}
|
|
@@ -77,7 +77,6 @@
|
|
|
77
77
|
*
|
|
78
78
|
* @module scripts/check-workflow-package-paths
|
|
79
79
|
*/
|
|
80
|
-
import { execFileSync } from "node:child_process";
|
|
81
80
|
import {
|
|
82
81
|
existsSync,
|
|
83
82
|
mkdirSync,
|
|
@@ -89,6 +88,7 @@ import { mkdtempSync } from "node:fs";
|
|
|
89
88
|
import { tmpdir } from "node:os";
|
|
90
89
|
import path from "node:path";
|
|
91
90
|
|
|
91
|
+
import { boundedExecFileSync } from "./lib/bounded-spawn.mjs";
|
|
92
92
|
import { invokedAsScript } from "./lib/invoked-as-script.mjs";
|
|
93
93
|
|
|
94
94
|
/** The package whose released layout a workflow may make claims about. */
|
|
@@ -179,7 +179,7 @@ function parseArgs(argv) {
|
|
|
179
179
|
* @returns {readonly string[]} Repo-relative workflow paths
|
|
180
180
|
*/
|
|
181
181
|
function trackedWorkflows(root) {
|
|
182
|
-
const output =
|
|
182
|
+
const output = boundedExecFileSync(
|
|
183
183
|
"git",
|
|
184
184
|
["-C", root, "ls-files", ".github/workflows"],
|
|
185
185
|
{ encoding: "utf8" }
|
|
@@ -285,7 +285,7 @@ export function collectClaims(root) {
|
|
|
285
285
|
*/
|
|
286
286
|
function publishedVersions() {
|
|
287
287
|
const versions = JSON.parse(
|
|
288
|
-
|
|
288
|
+
boundedExecFileSync("npm", ["view", PACKAGE_NAME, "versions", "--json"], {
|
|
289
289
|
encoding: "utf8",
|
|
290
290
|
stdio: ["ignore", "pipe", "pipe"],
|
|
291
291
|
maxBuffer: 16 * 1024 * 1024,
|
|
@@ -357,7 +357,7 @@ function fetchReleaseListings(chosen, cacheDir) {
|
|
|
357
357
|
continue;
|
|
358
358
|
}
|
|
359
359
|
const staging = mkdtempSync(path.join(tmpdir(), "lisa-release-"));
|
|
360
|
-
|
|
360
|
+
boundedExecFileSync(
|
|
361
361
|
"npm",
|
|
362
362
|
["pack", `${PACKAGE_NAME}@${version}`, "--pack-destination", staging],
|
|
363
363
|
{ stdio: ["ignore", "pipe", "pipe"] }
|
|
@@ -366,10 +366,14 @@ function fetchReleaseListings(chosen, cacheDir) {
|
|
|
366
366
|
if (tarball === undefined) {
|
|
367
367
|
throw new Error(`npm pack produced no tarball for ${version}`);
|
|
368
368
|
}
|
|
369
|
-
const entries =
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
369
|
+
const entries = boundedExecFileSync(
|
|
370
|
+
"tar",
|
|
371
|
+
["-tzf", path.join(staging, tarball)],
|
|
372
|
+
{
|
|
373
|
+
encoding: "utf8",
|
|
374
|
+
maxBuffer: 64 * 1024 * 1024,
|
|
375
|
+
}
|
|
376
|
+
)
|
|
373
377
|
.split("\n")
|
|
374
378
|
.filter(Boolean);
|
|
375
379
|
listings[version] = entries;
|
|
@@ -19,11 +19,12 @@
|
|
|
19
19
|
* legitimately stale file as host-modified and stop refreshing it — failing
|
|
20
20
|
* safe, but permanently.
|
|
21
21
|
*/
|
|
22
|
-
import { execFileSync } from "node:child_process";
|
|
23
22
|
import { createHash } from "node:crypto";
|
|
24
23
|
import { readFileSync, writeFileSync } from "node:fs";
|
|
25
24
|
import path from "node:path";
|
|
26
25
|
|
|
26
|
+
import { boundedExecFileSync, isChildTimeout } from "./lib/bounded-spawn.mjs";
|
|
27
|
+
|
|
27
28
|
const repoRoot = path.resolve(import.meta.dirname, "..");
|
|
28
29
|
const outputPath = path.join(repoRoot, "src/core/lisa-owned-hash-ledger.ts");
|
|
29
30
|
const COPY_OVERWRITE = "copy-overwrite";
|
|
@@ -35,18 +36,25 @@ const ENFORCEMENT_TREE = "scripts/";
|
|
|
35
36
|
*
|
|
36
37
|
* Blobs are read as `binary` so arbitrary bytes survive the round trip into the
|
|
37
38
|
* Buffer that gets hashed; decoding as UTF-8 would corrupt any non-text file.
|
|
39
|
+
* A KILLED child re-raises. Every caller reads this as content, and an empty
|
|
40
|
+
* string is a perfectly well-formed answer meaning "no files" — so a timeout
|
|
41
|
+
* on the `ls-files` call would build a ledger that vouches for NOTHING and
|
|
42
|
+
* report it as a complete one. A ledger's whole job is to vouch for bytes; one
|
|
43
|
+
* assembled from silence is worse than none, because it looks authoritative.
|
|
38
44
|
* @param {readonly string[]} args - Arguments passed to git
|
|
39
|
-
* @returns {string} Command stdout, or an empty string when git
|
|
45
|
+
* @returns {string} Command stdout, or an empty string when git said nothing
|
|
46
|
+
* @throws {Error} When the child was killed at its deadline
|
|
40
47
|
*/
|
|
41
48
|
function git(args) {
|
|
42
49
|
try {
|
|
43
|
-
return
|
|
50
|
+
return boundedExecFileSync("git", args, {
|
|
44
51
|
cwd: repoRoot,
|
|
45
52
|
encoding: "buffer",
|
|
46
53
|
maxBuffer: 64 * 1024 * 1024,
|
|
47
54
|
stdio: ["ignore", "pipe", "ignore"],
|
|
48
55
|
}).toString("binary");
|
|
49
|
-
} catch {
|
|
56
|
+
} catch (error) {
|
|
57
|
+
if (isChildTimeout(error)) throw error;
|
|
50
58
|
return "";
|
|
51
59
|
}
|
|
52
60
|
}
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/** Generate the exact hash-pinned allowlist for public upstream evidence. */
|
|
3
|
-
import { execFileSync } from "node:child_process";
|
|
4
3
|
import { createHash } from "node:crypto";
|
|
5
4
|
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
6
5
|
import path from "node:path";
|
|
7
6
|
|
|
7
|
+
import { boundedExecFileSync } from "./lib/bounded-spawn.mjs";
|
|
8
8
|
import {
|
|
9
9
|
describeStaleManifest,
|
|
10
10
|
diagnoseStaleManifest,
|
|
@@ -86,7 +86,7 @@ function repositoryPath(url) {
|
|
|
86
86
|
// comparison is by path: `url.<base>.insteadOf` rewrites what `git remote
|
|
87
87
|
// get-url` returns, so it answers "how do I reach this" rather than "what is
|
|
88
88
|
// this". Falls back to the resolved URL only if no value is configured.
|
|
89
|
-
const originUrl =
|
|
89
|
+
const originUrl = boundedExecFileSync(
|
|
90
90
|
"git",
|
|
91
91
|
["config", "--get", "remote.origin.url"],
|
|
92
92
|
{ cwd: repoRoot, encoding: "utf8" }
|
|
@@ -100,7 +100,7 @@ if (repositoryPath(originUrl) !== canonicalRepository.toLowerCase()) {
|
|
|
100
100
|
);
|
|
101
101
|
}
|
|
102
102
|
|
|
103
|
-
const tracked =
|
|
103
|
+
const tracked = boundedExecFileSync("git", ["ls-files", "-z"], {
|
|
104
104
|
cwd: repoRoot,
|
|
105
105
|
encoding: "utf8",
|
|
106
106
|
})
|
|
@@ -135,7 +135,7 @@ const existingPublicCommits = [
|
|
|
135
135
|
.filter(Boolean);
|
|
136
136
|
const refreshPublicCommits = process.argv.includes("--refresh-public-commits");
|
|
137
137
|
const discoveredPublicCommits = refreshPublicCommits
|
|
138
|
-
?
|
|
138
|
+
? boundedExecFileSync("git", ["rev-list", "--remotes=origin"], {
|
|
139
139
|
cwd: repoRoot,
|
|
140
140
|
encoding: "utf8",
|
|
141
141
|
})
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/** Empirical proof for the shipped optional agentic Health composition API. */
|
|
3
3
|
import assert from "node:assert/strict";
|
|
4
|
-
import { execFileSync } from "node:child_process";
|
|
5
4
|
import { createHash } from "node:crypto";
|
|
6
5
|
import {
|
|
7
6
|
lstat,
|
|
@@ -17,6 +16,8 @@ import {
|
|
|
17
16
|
import { tmpdir } from "node:os";
|
|
18
17
|
import path from "node:path";
|
|
19
18
|
|
|
19
|
+
import { boundedExecFileSync } from "./lib/bounded-spawn.mjs";
|
|
20
|
+
|
|
20
21
|
// Literals named once — each was repeated enough times that a typo in one
|
|
21
22
|
// copy would diverge silently.
|
|
22
23
|
const TEMPLATES_MANAGED = "templates.managed";
|
|
@@ -62,14 +63,14 @@ const root = process.cwd();
|
|
|
62
63
|
const workspace = await mkdtemp(path.join(tmpdir(), "lisa-health-agentic-"));
|
|
63
64
|
const project = path.join(workspace, "host");
|
|
64
65
|
const isolatedBin = path.join(workspace, "bin");
|
|
65
|
-
const gitExecutable =
|
|
66
|
+
const gitExecutable = boundedExecFileSync("which", ["git"], {
|
|
66
67
|
encoding: "utf8",
|
|
67
68
|
}).trim();
|
|
68
69
|
const healthFile = path.join(project, ".lisa", "health", "latest.json");
|
|
69
70
|
const startedAt = Date.now();
|
|
70
71
|
|
|
71
72
|
const run = (executable, argv, options = {}) =>
|
|
72
|
-
|
|
73
|
+
boundedExecFileSync(executable, argv, {
|
|
73
74
|
cwd: project,
|
|
74
75
|
encoding: "utf8",
|
|
75
76
|
stdio: ["ignore", "pipe", "pipe"],
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/** Built-package proof for the real two-phase Health CLI consumer. */
|
|
3
3
|
import assert from "node:assert/strict";
|
|
4
|
-
import { execFileSync } from "node:child_process";
|
|
5
4
|
import {
|
|
6
5
|
lstat,
|
|
7
6
|
mkdir,
|
|
@@ -14,6 +13,8 @@ import {
|
|
|
14
13
|
import { tmpdir } from "node:os";
|
|
15
14
|
import path from "node:path";
|
|
16
15
|
|
|
16
|
+
import { boundedExecFileSync } from "./lib/bounded-spawn.mjs";
|
|
17
|
+
|
|
17
18
|
// Literals named once — each was repeated enough times that a typo in one
|
|
18
19
|
// copy would diverge silently.
|
|
19
20
|
const LATEST_JSON = "latest.json";
|
|
@@ -53,7 +54,7 @@ async function createProject(name) {
|
|
|
53
54
|
|
|
54
55
|
/** Run the compiled CLI and capture its exact standard-output bytes. */
|
|
55
56
|
function runCli(project, argv, input) {
|
|
56
|
-
return
|
|
57
|
+
return boundedExecFileSync(
|
|
57
58
|
process.execPath,
|
|
58
59
|
[cli, "--no-update-check", "health", ...argv, project],
|
|
59
60
|
{
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/** Empirical proof for the shipped Health v1 contract and real built CLI. */
|
|
3
3
|
import assert from "node:assert/strict";
|
|
4
|
-
import { execFileSync } from "node:child_process";
|
|
5
4
|
import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
|
|
6
5
|
import { tmpdir } from "node:os";
|
|
7
6
|
import path from "node:path";
|
|
8
7
|
|
|
8
|
+
import { boundedExecFileSync } from "./lib/bounded-spawn.mjs";
|
|
9
|
+
|
|
9
10
|
let assertions = 0;
|
|
10
11
|
const check = (condition, message) => {
|
|
11
12
|
assertions += 1;
|
|
@@ -23,7 +24,7 @@ try {
|
|
|
23
24
|
path.join(project, ".lisa.config.json"),
|
|
24
25
|
`${JSON.stringify({ tracker: "github", github: { org: "acme", repo: "app" } }, null, 2)}\n`
|
|
25
26
|
);
|
|
26
|
-
|
|
27
|
+
boundedExecFileSync(
|
|
27
28
|
process.execPath,
|
|
28
29
|
[path.join(root, "dist/index.js"), "sync", project, "--json"],
|
|
29
30
|
{
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/** Empirical proof for the shipped deterministic Health fast path. */
|
|
3
3
|
import assert from "node:assert/strict";
|
|
4
|
-
import { execFileSync } from "node:child_process";
|
|
5
4
|
import { createHash } from "node:crypto";
|
|
6
5
|
import {
|
|
7
6
|
chmod,
|
|
@@ -18,6 +17,8 @@ import {
|
|
|
18
17
|
import { tmpdir } from "node:os";
|
|
19
18
|
import path from "node:path";
|
|
20
19
|
|
|
20
|
+
import { boundedExecFileSync } from "./lib/bounded-spawn.mjs";
|
|
21
|
+
|
|
21
22
|
// Literals named once — each was repeated enough times that a typo in one
|
|
22
23
|
// copy would diverge silently.
|
|
23
24
|
const PACKAGE_JSON = "package.json";
|
|
@@ -40,14 +41,14 @@ const root = process.cwd();
|
|
|
40
41
|
const workspace = await mkdtemp(path.join(tmpdir(), "lisa-health-fast-path-"));
|
|
41
42
|
const project = path.join(workspace, "host");
|
|
42
43
|
const isolatedBin = path.join(workspace, "bin");
|
|
43
|
-
const gitExecutable =
|
|
44
|
+
const gitExecutable = boundedExecFileSync("which", ["git"], {
|
|
44
45
|
encoding: "utf8",
|
|
45
46
|
}).trim();
|
|
46
47
|
const healthFile = path.join(project, ".lisa", "health", "latest.json");
|
|
47
48
|
const startedAt = Date.now();
|
|
48
49
|
|
|
49
50
|
const run = (executable, argv, options = {}) =>
|
|
50
|
-
|
|
51
|
+
boundedExecFileSync(executable, argv, {
|
|
51
52
|
cwd: project,
|
|
52
53
|
encoding: "utf8",
|
|
53
54
|
stdio: ["ignore", "pipe", "pipe"],
|
|
@@ -466,7 +467,7 @@ try {
|
|
|
466
467
|
path.join(unsafeProject, LISA_CONFIG_JSON),
|
|
467
468
|
'{"tracker":"github","harness":"codex"}\n'
|
|
468
469
|
);
|
|
469
|
-
|
|
470
|
+
boundedExecFileSync("mkfifo", [path.join(unsafeProject, PACKAGE_JSON)]);
|
|
470
471
|
const unsafeStarted = Date.now();
|
|
471
472
|
const unsafe = await health.runDeterministicHealth(unsafeProject, {
|
|
472
473
|
lisaRoot: root,
|
|
@@ -1,263 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: lisa-parity-safety-net-rules
|
|
3
|
-
description: "View, set, and verify the…"
|
|
4
|
-
allowed-tools: ["Read", "Edit", "Write", "Bash"]
|
|
5
|
-
synced-from: safety-net@cc-marketplace@1.0.6
|
|
6
|
-
---
|
|
7
|
-
|
|
8
|
-
# Parity Safety-Net Rules
|
|
9
|
-
|
|
10
|
-
Manage the **custom guard rules** that Lisa's safety-net hook enforces on every
|
|
11
|
-
Bash command. The hook (`hooks/parity-safety-net.sh`, registered as a
|
|
12
|
-
`PreToolUse` matcher on `Bash`) ships with built-in guards against catastrophic
|
|
13
|
-
commands; this skill lets a project **view**, **set**, and **verify** *additional*
|
|
14
|
-
project-specific rules on top of those built-ins.
|
|
15
|
-
|
|
16
|
-
> **Lisa-native reimplementation.** Upstream 0.9.0 shipped two rule-management
|
|
17
|
-
> skills (`set-custom-rules` + `verify-custom-rules`), which this skill
|
|
18
|
-
> consolidates. Upstream 1.0.6 consolidated them too (into `cc-safety-net`) and
|
|
19
|
-
> moved custom rules to a JSON rulebook system driven by the
|
|
20
|
-
> `npx cc-safety-net rule` CLI. Lisa **deliberately keeps** its simpler
|
|
21
|
-
> ERE-lines-file design: the Lisa hook must run identically on Codex, agy,
|
|
22
|
-
> Copilot, Cursor, and Claude without an npx dependency, and a flat regex file
|
|
23
|
-
> is auditable in any of those runtimes. It is reimplemented from scratch
|
|
24
|
-
> against Lisa conventions — it does **not** port or invoke upstream plugin
|
|
25
|
-
> code.
|
|
26
|
-
>
|
|
27
|
-
> **Drift tracking.** Pinned to `safety-net@cc-marketplace@1.0.6`.
|
|
28
|
-
> `scripts/plugin-parity-drift.mjs` compares this pin against the upstream
|
|
29
|
-
> version in the plugin cache and flags staleness. **Do not port or copy upstream
|
|
30
|
-
> plugin code.**
|
|
31
|
-
>
|
|
32
|
-
> **2.0.3 review.** Upstream 2.0.2–2.0.3 added Kimi Code and Amp installation
|
|
33
|
-
> support and release tooling. Those changes do not alter the upstream
|
|
34
|
-
> rule-management skill or Lisa's project-local ERE rule contract, and Lisa does
|
|
35
|
-
> not currently ship hooks for those runtimes, so no behavior is absorbed here.
|
|
36
|
-
>
|
|
37
|
-
> **2.0.4 review.** Method, so the next person can re-run it rather than trust
|
|
38
|
-
> it: hash the sorted (relative path, content) manifest of every rule-bearing
|
|
39
|
-
> directory in the plugin cache under both versions, then diff the trees
|
|
40
|
-
> excluding `dist/`, `.git/`, and `node_modules/`. Result — `src/` (230 files),
|
|
41
|
-
> `skills/` (1), and `hooks/` (1) are byte-identical across 2.0.3 and 2.0.4
|
|
42
|
-
> (`src` digest `7e346e205fbd1aa7` both sides). The only non-generated
|
|
43
|
-
> differences are the `version` string in `package.json`, `kimi.plugin.json`,
|
|
44
|
-
> and `.claude-plugin/plugin.json`, plus a test-harness fix in
|
|
45
|
-
> `tests/cli/cli-statusline.test.ts` that tolerates EPIPE when an oversized
|
|
46
|
-
> payload is flushed at the bounded reader's cap. Everything else is rebuilt
|
|
47
|
-
> `dist/` output. No rule, guard family, or skill surface moved, so nothing is
|
|
48
|
-
> absorbed here — and, critically, **2.0.4 does not close the gap recorded
|
|
49
|
-
> below**, which therefore carries forward unchanged rather than being dropped
|
|
50
|
-
> at the new pin.
|
|
51
|
-
>
|
|
52
|
-
> **Known gap at the 2.0.4 pin.** Upstream 2.0.0 rebuilt its engine and added two
|
|
53
|
-
> guard families this hook does **not** mirror: `secret.*` (blocks reading or
|
|
54
|
-
> copying SSH keys, `.env` files, cloud credentials, and coding-CLI credential
|
|
55
|
-
> stores) and `rm.git-metadata` (blocks deleting the `.git` control plane). Both
|
|
56
|
-
> were measured against this hook rather than assumed — see the probe table in
|
|
57
|
-
> `parity/plugin-routing/safety-net@cc-marketplace.md`. Absorbing `secret.*` also
|
|
58
|
-
> needs a matcher change, because this hook is registered on `Bash` only while
|
|
59
|
-
> upstream also screens file tools. That work is tracked separately in
|
|
60
|
-
> `parity/FOLLOWUPS.md` §5; **do not treat this skill's built-in guard list as
|
|
61
|
-
> covering secret access.**
|
|
62
|
-
|
|
63
|
-
## How the rules work
|
|
64
|
-
|
|
65
|
-
- The hook always enforces its **built-in guards** (see below). These cannot be
|
|
66
|
-
disabled from the rules file — they are the floor.
|
|
67
|
-
- **Custom rules** live in a project-local file, one **POSIX extended regular
|
|
68
|
-
expression (ERE)** per line. Blank lines and lines beginning with `#` are
|
|
69
|
-
ignored. Matching is case-insensitive (`grep -Ei`).
|
|
70
|
-
- If *any* built-in guard or custom rule matches the proposed command, the hook
|
|
71
|
-
exits non-zero and the Bash call is **blocked**, with the reason shown to the
|
|
72
|
-
agent.
|
|
73
|
-
|
|
74
|
-
### Rules file location
|
|
75
|
-
|
|
76
|
-
Resolved in this order:
|
|
77
|
-
|
|
78
|
-
1. `$SAFETY_NET_RULES_FILE` (explicit override), else
|
|
79
|
-
2. `${CLAUDE_PROJECT_DIR:-$PWD}/.claude/safety-net-rules.txt`
|
|
80
|
-
|
|
81
|
-
```bash
|
|
82
|
-
RULES_FILE="${SAFETY_NET_RULES_FILE:-${CLAUDE_PROJECT_DIR:-$PWD}/.claude/safety-net-rules.txt}"
|
|
83
|
-
```
|
|
84
|
-
|
|
85
|
-
### Built-in guards (always on)
|
|
86
|
-
|
|
87
|
-
1. `rm -rf` of a filesystem root, `$HOME`/`~`, or a top-level wildcard — with
|
|
88
|
-
quote-aware boundaries, so wrapper/interpreter forms like
|
|
89
|
-
`bash -c "rm -rf /"` and `python -c "… os.system('rm -rf /')"` match too.
|
|
90
|
-
Path-prefixed spellings (`/bin/rm`, `./rm`) count as `rm` for every rm
|
|
91
|
-
guard.
|
|
92
|
-
2. `rm -rf` target hardening: the cwd itself (`.`/`./`), `..`-traversal paths,
|
|
93
|
-
home-anchored `~/…` paths, absolute paths outside the project (`/tmp`,
|
|
94
|
-
`/var/tmp`, and `$TMPDIR` are allowed), `$VAR` targets other than `$TMPDIR`,
|
|
95
|
-
and **any** recursive forced delete while the working directory is `$HOME`.
|
|
96
|
-
3. Force-pushing a protected branch
|
|
97
|
-
(`main`/`master`/`production`/`prod`/`release`). `--force-with-lease` is
|
|
98
|
-
intentionally allowed, and so is force-pushing a feature branch (sanctioned
|
|
99
|
-
rebase workflow). Acceptable parity divergence: a refspec force-push
|
|
100
|
-
(`git push origin +main`) is not blocked — upstream 1.0.6 allows it too.
|
|
101
|
-
4. `git reset --hard` / `git reset --merge` while the working tree is **dirty**
|
|
102
|
-
(would discard work). Clean-tree resets are intentionally allowed.
|
|
103
|
-
5. `git rebase --abort` / `git rebase --quit` while the in-progress rebase
|
|
104
|
-
holds human-made conflict resolutions (they would be discarded). A clean or
|
|
105
|
-
untouched rebase state stays abortable — an agent-recoverable wedge; the
|
|
106
|
-
apply backend and a missing `AUTO_MERGE` ref fail closed. Deliberate
|
|
107
|
-
divergence: upstream blocks `rebase --abort` unconditionally.
|
|
108
|
-
6. `git checkout` discards: the `--` pathspec form (with or without a ref),
|
|
109
|
-
`-f`/`--force`, `--pathspec-from-file`, and bare `git checkout .`.
|
|
110
|
-
Branch switching and `-b`/`-B` creation stay allowed.
|
|
111
|
-
7. `git switch --discard-changes` / `-f`/`--force`.
|
|
112
|
-
8. `git restore` touching the worktree — only `git restore --staged <path>`
|
|
113
|
-
without `--worktree` is allowed (unstaging is safe).
|
|
114
|
-
9. `git stash drop` / `git stash clear` (push/pop/list/apply stay allowed).
|
|
115
|
-
10. `git clean` with a force flag and no dry-run — `-n`/`--dry-run` anywhere
|
|
116
|
-
makes it a safe preview.
|
|
117
|
-
11. `git branch -D` (or `-d` combined with `-f`, clustered or split);
|
|
118
|
-
safe `-d` and rename `-m` stay allowed.
|
|
119
|
-
12. `git tag -d`, `git reflog delete`, `git worktree remove --force`.
|
|
120
|
-
13. Deletion via `find … -delete`, `find … -exec rm -rf`, and `xargs … rm -rf`
|
|
121
|
-
(plain non-recursive `rm` on find/xargs output stays allowed).
|
|
122
|
-
14. Disk destroyers: `dd of=/dev/…`, `mkfs … /dev/…`, `shred`.
|
|
123
|
-
15. Destructive SQL — `DROP DATABASE/SCHEMA/TABLE`, `TRUNCATE TABLE`.
|
|
124
|
-
|
|
125
|
-
Every git guard sees through leading git **global options** (`-C <path>`,
|
|
126
|
-
`-c <k>=<v>`, `--git-dir[=…]`, `--no-pager`, …), so `git -C /path <destructive>`
|
|
127
|
-
is screened the same as `git <destructive>`.
|
|
128
|
-
|
|
129
|
-
Malformed hook input fails **closed** (exit 2 denies the command). A text-scan
|
|
130
|
-
hook cannot exempt display commands, so prose like
|
|
131
|
-
`echo "docs about rm -rf /"` can false-positive — quote-break the string or use
|
|
132
|
-
the gh-writer heredoc form (payload is stripped before the guards run). For the
|
|
133
|
-
same reason the rm guard treats substitution-wrapping as verdict-neutral (issue
|
|
134
|
-
#1982): an executable `echo "$(rm -rf /)"` is blocked, and so are inert twins the
|
|
135
|
-
shell would never expand — a single-quoted `echo '$(rm -rf /)'` or an escaped
|
|
136
|
-
`echo "\$(rm -rf /)"` — since the scan has no quote-context awareness. The same
|
|
137
|
-
quote-break or heredoc workaround applies. Spell that heredoc workaround with a
|
|
138
|
-
**quote-pair** delimiter (`gh issue create --body-file - <<'EOF'`) — only that
|
|
139
|
-
spelling reaches the payload-strip path. `<<"EOF"` and `<<\EOF` do not, and a
|
|
140
|
-
`gh … <<\EOF` writer now fails closed instead.
|
|
141
|
-
|
|
142
|
-
## What this does and does not defend against
|
|
143
|
-
|
|
144
|
-
Read this before treating the safety net as a wall.
|
|
145
|
-
|
|
146
|
-
**What it is for:** stopping *accidental* catastrophic commands — an agent (or a
|
|
147
|
-
human) that reaches for `rm -rf /`, force-pushes `main`, drops a table, or writes
|
|
148
|
-
to a raw device by mistake. Within that job the guards above are the floor and
|
|
149
|
-
they are always on.
|
|
150
|
-
|
|
151
|
-
**What it is not:** a security boundary against a deliberate adversary. Two
|
|
152
|
-
structural reasons, both accepted rather than open bugs:
|
|
153
|
-
|
|
154
|
-
- **It is a text scan, not a shell engine.** It has no quote-context awareness,
|
|
155
|
-
which produces the false positives described above and means an operation
|
|
156
|
-
spelled in a way the patterns do not anticipate is not matched.
|
|
157
|
-
- **The here-doc classifier re-implements bash's lexer in Python** to decide
|
|
158
|
-
which payload text is inert data. Every divergence from real bash is a shape it
|
|
159
|
-
mis-classifies. Eight distinct divergences were found and fixed across issues
|
|
160
|
-
#1958 and #1993; the class is open-ended, so it is now closed as an accepted
|
|
161
|
-
limitation rather than chased indefinitely.
|
|
162
|
-
|
|
163
|
-
**Why that is tolerable:** a here-doc mis-classification degrades to the content
|
|
164
|
-
guards rather than bypassing them. Only the narrow `gh issue|pr` writer form ever
|
|
165
|
-
strips payload text; every other path passes the **raw** command to the guards,
|
|
166
|
-
and since issue #1982 those guards see through substitution wrappers. So a
|
|
167
|
-
mis-read here-doc costs no guard coverage for any destructive class listed above.
|
|
168
|
-
|
|
169
|
-
**The honest bottom line:** an agent that is actively trying to run something
|
|
170
|
-
destructive has many paths that never involve a here-doc at all — the safety net
|
|
171
|
-
does not claim to close them. Do not rely on it as the only control over what an
|
|
172
|
-
agent may execute; treat it as the last line of defence against mistakes, and
|
|
173
|
-
keep the real boundary (credentials, permissions, environment isolation) outside
|
|
174
|
-
the agent.
|
|
175
|
-
|
|
176
|
-
## View the current rules
|
|
177
|
-
|
|
178
|
-
```bash
|
|
179
|
-
RULES_FILE="${SAFETY_NET_RULES_FILE:-${CLAUDE_PROJECT_DIR:-$PWD}/.claude/safety-net-rules.txt}"
|
|
180
|
-
if [ -f "$RULES_FILE" ]; then
|
|
181
|
-
echo "Custom safety-net rules ($RULES_FILE):"
|
|
182
|
-
grep -vE '^[[:space:]]*(#|$)' "$RULES_FILE" || echo "(no active rules)"
|
|
183
|
-
else
|
|
184
|
-
echo "No custom rules file yet ($RULES_FILE). Only built-in guards are active."
|
|
185
|
-
fi
|
|
186
|
-
```
|
|
187
|
-
|
|
188
|
-
`Read` the file to show comments and structure as well.
|
|
189
|
-
|
|
190
|
-
## Set (add or edit) a rule
|
|
191
|
-
|
|
192
|
-
A rule is an ERE matched against the full command string. Keep rules **specific**
|
|
193
|
-
to avoid blocking legitimate work — anchor on the dangerous verb and its target.
|
|
194
|
-
|
|
195
|
-
1. Ensure the file exists, then **append** a commented rule (use `Edit`/`Write`,
|
|
196
|
-
or append from the shell):
|
|
197
|
-
|
|
198
|
-
```bash
|
|
199
|
-
RULES_FILE="${SAFETY_NET_RULES_FILE:-${CLAUDE_PROJECT_DIR:-$PWD}/.claude/safety-net-rules.txt}"
|
|
200
|
-
mkdir -p "$(dirname "$RULES_FILE")"
|
|
201
|
-
{
|
|
202
|
-
echo "# Block deleting a Kubernetes namespace"
|
|
203
|
-
echo 'kubectl[[:space:]]+delete[[:space:]]+namespace'
|
|
204
|
-
} >> "$RULES_FILE"
|
|
205
|
-
```
|
|
206
|
-
|
|
207
|
-
2. **Always verify** the new rule (next section) before considering it set —
|
|
208
|
-
confirm it blocks what it should and allows what it shouldn't.
|
|
209
|
-
|
|
210
|
-
Editing/removing: open the file with `Edit` and change or delete the line.
|
|
211
|
-
Removing a rule never affects the built-in guards.
|
|
212
|
-
|
|
213
|
-
## Verify the rules
|
|
214
|
-
|
|
215
|
-
Two checks — both should pass before you trust a rule.
|
|
216
|
-
|
|
217
|
-
### 1. The ERE is valid
|
|
218
|
-
|
|
219
|
-
An invalid regex would make the hook error on every command. Validate it:
|
|
220
|
-
|
|
221
|
-
```bash
|
|
222
|
-
printf '%s' "$RULE" | grep -Eq -- "$RULE" 2>/dev/null && echo "valid ERE" \
|
|
223
|
-
|| echo "INVALID ERE — fix before saving"
|
|
224
|
-
```
|
|
225
|
-
|
|
226
|
-
### 2. The rule behaves as intended
|
|
227
|
-
|
|
228
|
-
Drive the **actual hook** with a fake `PreToolUse` payload and assert the exit
|
|
229
|
-
code (non-zero = blocked, 0 = allowed). Build the JSON with `jq` so the test
|
|
230
|
-
command line itself never contains the dangerous literal:
|
|
231
|
-
|
|
232
|
-
```bash
|
|
233
|
-
HOOK="${CLAUDE_PLUGIN_ROOT}/hooks/parity-safety-net.sh"
|
|
234
|
-
|
|
235
|
-
check() { # check <expect: block|allow> <command>
|
|
236
|
-
jq -nc --arg c "$2" '{tool_name:"Bash",tool_input:{command:$c}}' \
|
|
237
|
-
| bash "$HOOK" >/dev/null 2>&1
|
|
238
|
-
local code=$?
|
|
239
|
-
local got=allow; [ "$code" -ne 0 ] && got=block
|
|
240
|
-
printf '%-5s want=%-5s got=%-5s %s\n' \
|
|
241
|
-
"$([ "$got" = "$1" ] && echo OK || echo FAIL)" "$1" "$got" "$2"
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
# Should block (matches the new rule):
|
|
245
|
-
check block "kubectl delete namespace prod"
|
|
246
|
-
# Should allow (must not over-match):
|
|
247
|
-
check allow "kubectl get pods"
|
|
248
|
-
```
|
|
249
|
-
|
|
250
|
-
Report a table of cases with want/got/verdict. If any case disagrees, tighten the
|
|
251
|
-
ERE and re-verify.
|
|
252
|
-
|
|
253
|
-
## Rules
|
|
254
|
-
|
|
255
|
-
- **Built-in guards are the floor** — custom rules only *add* blocks; they cannot
|
|
256
|
-
weaken the built-ins.
|
|
257
|
-
- **Prefer specific over broad** — a rule that blocks too much trains users to
|
|
258
|
-
bypass the safety net. Anchor on verb + target.
|
|
259
|
-
- **Verify every rule against the real hook** before saving — never ship an
|
|
260
|
-
unverified or syntactically invalid ERE.
|
|
261
|
-
- **Never weaken the net to unblock yourself.** If a built-in guard fires on a
|
|
262
|
-
command that is genuinely safe, run it manually outside the agent after the
|
|
263
|
-
user confirms — do not edit the hook to remove the guard.
|