@codyswann/lisa 4.15.1 → 4.17.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/all/copy-overwrite/scripts/lisa-hooks/parity-safety-net.sh +62 -9
- package/dist/cli/doctor-override-floor-conflicts.d.ts +15 -0
- package/dist/cli/doctor-override-floor-conflicts.d.ts.map +1 -0
- package/dist/cli/doctor-override-floor-conflicts.js +225 -0
- package/dist/cli/doctor-override-floor-conflicts.js.map +1 -0
- package/dist/cli/doctor.d.ts.map +1 -1
- package/dist/cli/doctor.js +8 -0
- package/dist/cli/doctor.js.map +1 -1
- package/dist/core/lisa-owned-hash-ledger.d.ts.map +1 -1
- package/dist/core/lisa-owned-hash-ledger.js +2 -0
- package/dist/core/lisa-owned-hash-ledger.js.map +1 -1
- package/dist/core/override-floors.d.ts +146 -0
- package/dist/core/override-floors.d.ts.map +1 -0
- package/dist/core/override-floors.js +200 -0
- package/dist/core/override-floors.js.map +1 -0
- package/dist/core/upstream-evidence-manifest.d.ts.map +1 -1
- package/dist/core/upstream-evidence-manifest.js +8 -2
- package/dist/core/upstream-evidence-manifest.js.map +1 -1
- package/dist/opencode/plugin-templates/parity-safety-net.sh +62 -9
- package/dist/strategies/package-lisa.d.ts +59 -0
- package/dist/strategies/package-lisa.d.ts.map +1 -1
- package/dist/strategies/package-lisa.js +112 -130
- package/dist/strategies/package-lisa.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/hooks/parity-safety-net.sh +62 -9
- package/plugins/lisa-agy/hooks/parity-safety-net.sh +62 -9
- 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-copilot/hooks/parity-safety-net.sh +62 -9
- package/plugins/lisa-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-cursor/hooks/parity-safety-net.sh +62 -9
- 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/hooks/parity-safety-net.sh +62 -9
|
@@ -61,12 +61,20 @@
|
|
|
61
61
|
# can match — including one whose target sits in the SAME quoted run as the
|
|
62
62
|
# prose, which the #3106 scoping deliberately does not reach. Upstream exempts
|
|
63
63
|
# those through an engine-only DISPLAY_COMMANDS list that a grep hook cannot
|
|
64
|
-
# replicate.
|
|
64
|
+
# replicate. WHAT TO DO WHEN YOU HIT IT (issue #3191): do not reword the prose.
|
|
65
|
+
# When the destructive text is a commit message, an issue body or a results
|
|
66
|
+
# table, the string IS the deliverable and there is no narrower spelling that
|
|
67
|
+
# still says what it must say. Write the text to a file with the Write tool and
|
|
68
|
+
# pass it BY PATH — `gh issue comment --body-file <file>`, `git commit -F
|
|
69
|
+
# <file>` — because these guards scan the COMMAND and never the file. block()
|
|
70
|
+
# prints this at the wall so the operator is told rather than left to find it.
|
|
71
|
+
# The same text-scan limit means the rm guard treats
|
|
65
72
|
# a substitution-wrapped catastrophic delete as verdict-neutral (issue #1982): an
|
|
66
73
|
# executable `echo "$(rm -rf /)"` is blocked, but so are inert twins the shell
|
|
67
74
|
# would never expand — a single-quoted `echo '$(rm -rf /)'` or an escaped
|
|
68
75
|
# `echo "\$(rm -rf /)"` — because the scan has no quote-context awareness. That
|
|
69
|
-
# over-block stays inside this accepted class. Workaround:
|
|
76
|
+
# over-block stays inside this accepted class. Workaround: the write-to-a-file
|
|
77
|
+
# and pass-by-path form above, or quote-break the string,
|
|
70
78
|
# or use the gh-writer heredoc form, whose payload is stripped before the guards
|
|
71
79
|
# run — spelled with a QUOTE-PAIR delimiter (`<<'EOF'`). That exact spelling is
|
|
72
80
|
# what the SAFE path recognises; `<<"EOF"` and `<<\EOF` do not reach it, and
|
|
@@ -115,15 +123,58 @@ if [ -z "$command_str" ]; then
|
|
|
115
123
|
exit 0
|
|
116
124
|
fi
|
|
117
125
|
|
|
126
|
+
# Guidance printed under an ordinary destructive-guard refusal.
|
|
127
|
+
#
|
|
128
|
+
# It teaches the remedy at the wall, the way block_heredoc() does (gardener
|
|
129
|
+
# #1789, extended by issue #3191). It used to end in "narrow the command so it
|
|
130
|
+
# no longer matches the guard", which is advice the accepted prose class CANNOT
|
|
131
|
+
# follow: when the destructive text is a commit message, an issue body or a
|
|
132
|
+
# results table, the string IS the deliverable and there is no narrower spelling
|
|
133
|
+
# that still says what it must say. An operator acts on a printed remedy, so a
|
|
134
|
+
# remedy that cannot be performed is worse than none — it spends the detour and
|
|
135
|
+
# teaches that the refusal was a formality. The remedy that does work is the
|
|
136
|
+
# sibling heredoc one, one surface over: write the text with the Write tool,
|
|
137
|
+
# then pass it BY PATH. The guards scan the COMMAND and never the file, so the
|
|
138
|
+
# path form carries the text through intact.
|
|
139
|
+
#
|
|
140
|
+
# Both branches are printed on every ordinary refusal, deliberately: this guard
|
|
141
|
+
# is a text scan and CANNOT tell which one applies, so naming both and letting
|
|
142
|
+
# the reader pick is the only honest shape. Deciding for them is what the
|
|
143
|
+
# engine-only DISPLAY_COMMANDS list does, and the header explains why a grep
|
|
144
|
+
# hook cannot have one.
|
|
145
|
+
DESTRUCTIVE_GUIDANCE="$(
|
|
146
|
+
cat <<'EOF'
|
|
147
|
+
This command matched a destructive-operation guard. The guard is a text scan,
|
|
148
|
+
not a shell engine, so it cannot tell a destructive command you are RUNNING
|
|
149
|
+
from the same text quoted as CONTENT you are writing. Take the branch that
|
|
150
|
+
describes yours:
|
|
151
|
+
|
|
152
|
+
- The destructive text is only QUOTED CONTENT — a commit message, an issue or
|
|
153
|
+
PR body, a report, a results table, a note. Do not try to reword it; the
|
|
154
|
+
string is the deliverable and no narrower spelling still says what it must.
|
|
155
|
+
Write the text to a file with the Write tool and pass it BY PATH instead:
|
|
156
|
+
`gh issue comment --body-file <file>`, `gh pr create --body-file <file>`,
|
|
157
|
+
`git commit -F <file>`, or read the file back with the Read tool. This guard
|
|
158
|
+
scans the command, never the file, so the path form goes through intact.
|
|
159
|
+
|
|
160
|
+
- You genuinely intend to run the destructive operation. Ask the user to
|
|
161
|
+
confirm, then run it manually outside the agent.
|
|
162
|
+
EOF
|
|
163
|
+
)"
|
|
164
|
+
|
|
118
165
|
# block() prints the reason to stderr (surfaced to the model) and exits 2 so the
|
|
119
166
|
# Bash tool call is denied. $1 = human-readable reason for the block.
|
|
167
|
+
#
|
|
168
|
+
# $2 = optional guidance REPLACING $DESTRUCTIVE_GUIDANCE. block_heredoc() passes
|
|
169
|
+
# its own, because the two remedies are mutually exclusive by construction
|
|
170
|
+
# (issue #1958 F6): a heredoc payload IS executable shell, so the quoted-content
|
|
171
|
+
# branch is exactly the misdirection that finding removed, and the file-based
|
|
172
|
+
# execution advice is exactly what an ordinary prose refusal must not be given.
|
|
120
173
|
block() {
|
|
121
174
|
cat >&2 <<EOF
|
|
122
175
|
Blocked by safety-net: $1
|
|
123
176
|
|
|
124
|
-
|
|
125
|
-
intentional, ask the user to confirm, then run it manually outside the agent, or
|
|
126
|
-
narrow the command so it no longer matches the guard.
|
|
177
|
+
${2:-$DESTRUCTIVE_GUIDANCE}
|
|
127
178
|
EOF
|
|
128
179
|
exit 2
|
|
129
180
|
}
|
|
@@ -199,20 +250,22 @@ EOF
|
|
|
199
250
|
# depends on the command shape (issue #1958): `git commit -m "$(cat <<EOF …)"`
|
|
200
251
|
# attempts get the commit -F text; every other heredoc denial gets the
|
|
201
252
|
# file-based execution guidance instead — the commit text is misleading there.
|
|
253
|
+
# Both are passed as block()'s SECOND argument, which REPLACES the ordinary
|
|
254
|
+
# destructive guidance rather than appending to it (issue #3191): that guidance
|
|
255
|
+
# offers a quoted-content branch, and a heredoc payload is executable shell, so
|
|
256
|
+
# printing it here would reintroduce the misdirection #1958 F6 removed.
|
|
202
257
|
# The git-commit detection inlines the GIT_GLOBAL_OPTS shape (defined later in
|
|
203
258
|
# this file, after the heredoc dispatch runs) so `git -C <path> commit` and
|
|
204
259
|
# `git -c k=v commit` spellings are still recognized.
|
|
205
260
|
block_heredoc() {
|
|
206
261
|
if scan -E "$command_str" \
|
|
207
262
|
'(^|[^[:alnum:]_-])git[[:space:]]+(-[^;&|[:space:]]+([[:space:]]+[^-;&|[:space:]][^;&|[:space:]]*)?[[:space:]]+)*commit([^[:alnum:]_-]|$)'; then
|
|
208
|
-
block "$1
|
|
209
|
-
Heredoc commit invocations are blocked (the payload is executable shell).
|
|
263
|
+
block "$1" "Heredoc commit invocations are blocked (the payload is executable shell).
|
|
210
264
|
Fix: write the commit message to a file and run \`git commit -F <file>\`.
|
|
211
265
|
Every commit must also carry a Co-authored-by trailer for a supported agent
|
|
212
266
|
(Claude/Codex/OpenCode) — the commit-msg hook enforces this."
|
|
213
267
|
fi
|
|
214
|
-
block "$1
|
|
215
|
-
Heredoc payloads are blocked here (the payload is executable shell).
|
|
268
|
+
block "$1" "Heredoc payloads are blocked here (the payload is executable shell).
|
|
216
269
|
Fix: write the payload to a file with the Write tool, then execute that file
|
|
217
270
|
directly (for example \`python3 <file>\` or \`bash <file>\`)."
|
|
218
271
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { FileOperationResult, ProjectType } from "../core/config.js";
|
|
2
2
|
import type { ICopyStrategy, StrategyContext } from "./strategy.interface.js";
|
|
3
|
+
import { type OverrideFloorAudit } from "../core/override-floors.js";
|
|
3
4
|
/**
|
|
4
5
|
* @file package-lisa.ts
|
|
5
6
|
* @description Package.lisa.json strategy for governance-driven package.json management
|
|
@@ -24,6 +25,34 @@ import type { ICopyStrategy, StrategyContext } from "./strategy.interface.js";
|
|
|
24
25
|
* Child types override parent values in each section.
|
|
25
26
|
* @module strategies
|
|
26
27
|
*/
|
|
28
|
+
/** Which of the two apply paths an override-floor audit simulated. */
|
|
29
|
+
export type ApplyPathLabel = "postinstall" | "full apply";
|
|
30
|
+
/** One apply path's answer: an audit, or the reason there is none. */
|
|
31
|
+
export interface OverrideFloorPathAudit {
|
|
32
|
+
/** Apply path simulated. */
|
|
33
|
+
readonly applyPath: ApplyPathLabel;
|
|
34
|
+
/** What the path inspected and found, or null when it could not run. */
|
|
35
|
+
readonly audit: OverrideFloorAudit | null;
|
|
36
|
+
/** Why the path could not run, or null when it did. */
|
|
37
|
+
readonly error: string | null;
|
|
38
|
+
}
|
|
39
|
+
/** Everything one override-floor audit of a project looked at. */
|
|
40
|
+
export interface OverrideFloorAuditReport {
|
|
41
|
+
/**
|
|
42
|
+
* Literal (non-`$name`) entries across the resolved template's
|
|
43
|
+
* `force.overrides` and `force.resolutions` — the floors Lisa would write.
|
|
44
|
+
*
|
|
45
|
+
* Zero is the inert case, not a clean one: it means template resolution
|
|
46
|
+
* produced nothing to compare against, so the audit below cannot have
|
|
47
|
+
* verified anything. Callers must not read an empty conflict list as a pass
|
|
48
|
+
* without reading this first.
|
|
49
|
+
*/
|
|
50
|
+
readonly lisaFloors: number;
|
|
51
|
+
/** Project types the apply itself would detect here. */
|
|
52
|
+
readonly detectedTypes: readonly ProjectType[];
|
|
53
|
+
/** One entry per apply path. */
|
|
54
|
+
readonly paths: readonly OverrideFloorPathAudit[];
|
|
55
|
+
}
|
|
27
56
|
/**
|
|
28
57
|
* Package.lisa.json strategy: Manage package.json via separate template files
|
|
29
58
|
* - Loads templates from all applicable types in inheritance chain
|
|
@@ -58,6 +87,36 @@ export declare class PackageLisaStrategy implements ICopyStrategy {
|
|
|
58
87
|
* @returns Package document that apply would persist
|
|
59
88
|
*/
|
|
60
89
|
planPackageJson(projectJson: Record<string, unknown>, detectedTypes: readonly ProjectType[], lisaDir: string, securityPinsOnly?: boolean): Promise<Record<string, unknown>>;
|
|
90
|
+
/**
|
|
91
|
+
* Report every `$name` override that would resolve BELOW a Lisa floor, before
|
|
92
|
+
* an apply refuses over it.
|
|
93
|
+
* @remarks
|
|
94
|
+
* Deliberately re-runs the apply's own resolution — the same type detection,
|
|
95
|
+
* the same template inheritance chain, the same force/preserve/adopt phases —
|
|
96
|
+
* rather than re-deriving the answer from `package.lisa.json` and the host
|
|
97
|
+
* manifest side by side. A second implementation of "what would the merged
|
|
98
|
+
* manifest look like" is a second thing to keep in step with this file, and
|
|
99
|
+
* the whole value of the report is that it agrees with the refusal.
|
|
100
|
+
*
|
|
101
|
+
* BOTH apply paths are simulated. The postinstall path applies a template
|
|
102
|
+
* restricted to security pins and pulls in the direct dependencies backing
|
|
103
|
+
* them, so it can accept a manifest the unrestricted path refuses and vice
|
|
104
|
+
* versa. Reporting only one leaves an operator refused by the other with a
|
|
105
|
+
* doctor line that said nothing.
|
|
106
|
+
* @param projectDir - Host project directory containing package.json
|
|
107
|
+
* @param lisaDir - Installed Lisa package root
|
|
108
|
+
* @returns What was inspected on each path, and every conflict found
|
|
109
|
+
*/
|
|
110
|
+
auditOverrideFloors(projectDir: string, lisaDir: string): Promise<OverrideFloorAuditReport>;
|
|
111
|
+
/**
|
|
112
|
+
* Simulate one apply path and audit the manifest it would produce.
|
|
113
|
+
* @param projectJson - Parsed host package.json
|
|
114
|
+
* @param merged - Fully resolved template for the detected types
|
|
115
|
+
* @param applyPath - Which path to simulate
|
|
116
|
+
* @returns The path's audit, or the reason it could not be produced
|
|
117
|
+
* @private
|
|
118
|
+
*/
|
|
119
|
+
private auditOneApplyPath;
|
|
61
120
|
/**
|
|
62
121
|
* Apply package-lisa strategy: Load templates from inheritance chain, apply to package.json
|
|
63
122
|
* @remarks
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"package-lisa.d.ts","sourceRoot":"","sources":["../../src/strategies/package-lisa.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,mBAAmB,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAE1E,OAAO,KAAK,EAAE,aAAa,EAAE,eAAe,EAAE,MAAM,yBAAyB,CAAC;
|
|
1
|
+
{"version":3,"file":"package-lisa.d.ts","sourceRoot":"","sources":["../../src/strategies/package-lisa.ts"],"names":[],"mappings":"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"}
|
|
@@ -8,32 +8,9 @@ import { PROJECT_TYPE_HIERARCHY, PROJECT_TYPE_ORDER } from "../core/config.js";
|
|
|
8
8
|
import { ensureParentDir } from "../utils/file-operations.js";
|
|
9
9
|
import { readJson, writeJson, deepMerge, readJsonOrNull, } from "../utils/json-utils.js";
|
|
10
10
|
import { JsonMergeError } from "../errors/index.js";
|
|
11
|
+
import { asRecord, auditSelfReferenceRewrites, collectDirectDependencyNames, describeSelfReferenceRemedy, DIRECT_DEPENDENCY_SECTIONS, OVERRIDE_SECTIONS, } from "../core/override-floors.js";
|
|
11
12
|
import { LISA_PACKAGE_NAME } from "../core/self-apply.js";
|
|
12
13
|
import { getPackageVersion } from "../cli/version.js";
|
|
13
|
-
/**
|
|
14
|
-
* @file package-lisa.ts
|
|
15
|
-
* @description Package.lisa.json strategy for governance-driven package.json management
|
|
16
|
-
*
|
|
17
|
-
* Implements a two-file approach to package.json governance:
|
|
18
|
-
* - Source: package.lisa.json files in type directories (all/, typescript/, expo/, etc.)
|
|
19
|
-
* - Destination: project's package.json
|
|
20
|
-
*
|
|
21
|
-
* Behavior is defined in package.lisa.json:
|
|
22
|
-
* - force: Lisa's values completely replace project's values
|
|
23
|
-
* - adopt: Lisa reclaims a key still holding a value Lisa itself wrote
|
|
24
|
-
* - defaults: Project's values preserved; Lisa's used only if missing
|
|
25
|
-
* - merge: Arrays concatenated and deduplicated
|
|
26
|
-
* - remove: Retired keys deleted from their section
|
|
27
|
-
*
|
|
28
|
-
* A governed script CI invokes is shipped as a pair — `lint:lisa` forced, and
|
|
29
|
-
* `lint` merely defaulted to invoke it — so the host owns the composition point
|
|
30
|
-
* and anything chained onto it survives an apply. See the reserved-base section
|
|
31
|
-
* of `wiki/documentation/specs/package-lisa-json.md`.
|
|
32
|
-
*
|
|
33
|
-
* Inheritance chain: all → typescript → specific types (expo, nestjs, cdk, npm-package)
|
|
34
|
-
* Child types override parent values in each section.
|
|
35
|
-
* @module strategies
|
|
36
|
-
*/
|
|
37
14
|
/**
|
|
38
15
|
* Package.lisa.json strategy: Manage package.json via separate template files
|
|
39
16
|
* - Loads templates from all applicable types in inheritance chain
|
|
@@ -78,6 +55,66 @@ export class PackageLisaStrategy {
|
|
|
78
55
|
assertManifestIsInstallable(result.packageJson, this.PACKAGE_JSON);
|
|
79
56
|
return result.packageJson;
|
|
80
57
|
}
|
|
58
|
+
/**
|
|
59
|
+
* Report every `$name` override that would resolve BELOW a Lisa floor, before
|
|
60
|
+
* an apply refuses over it.
|
|
61
|
+
* @remarks
|
|
62
|
+
* Deliberately re-runs the apply's own resolution — the same type detection,
|
|
63
|
+
* the same template inheritance chain, the same force/preserve/adopt phases —
|
|
64
|
+
* rather than re-deriving the answer from `package.lisa.json` and the host
|
|
65
|
+
* manifest side by side. A second implementation of "what would the merged
|
|
66
|
+
* manifest look like" is a second thing to keep in step with this file, and
|
|
67
|
+
* the whole value of the report is that it agrees with the refusal.
|
|
68
|
+
*
|
|
69
|
+
* BOTH apply paths are simulated. The postinstall path applies a template
|
|
70
|
+
* restricted to security pins and pulls in the direct dependencies backing
|
|
71
|
+
* them, so it can accept a manifest the unrestricted path refuses and vice
|
|
72
|
+
* versa. Reporting only one leaves an operator refused by the other with a
|
|
73
|
+
* doctor line that said nothing.
|
|
74
|
+
* @param projectDir - Host project directory containing package.json
|
|
75
|
+
* @param lisaDir - Installed Lisa package root
|
|
76
|
+
* @returns What was inspected on each path, and every conflict found
|
|
77
|
+
*/
|
|
78
|
+
async auditOverrideFloors(projectDir, lisaDir) {
|
|
79
|
+
const projectJson = (await readJsonOrNull(path.join(projectDir, this.PACKAGE_JSON))) ?? {};
|
|
80
|
+
const detectedTypes = await this.detectProjectTypes(projectDir);
|
|
81
|
+
const merged = await this.loadAndMergeTemplates(lisaDir, detectedTypes);
|
|
82
|
+
return {
|
|
83
|
+
lisaFloors: countTemplateFloors(merged),
|
|
84
|
+
detectedTypes,
|
|
85
|
+
paths: [
|
|
86
|
+
this.auditOneApplyPath(projectJson, merged, "postinstall"),
|
|
87
|
+
this.auditOneApplyPath(projectJson, merged, "full apply"),
|
|
88
|
+
],
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Simulate one apply path and audit the manifest it would produce.
|
|
93
|
+
* @param projectJson - Parsed host package.json
|
|
94
|
+
* @param merged - Fully resolved template for the detected types
|
|
95
|
+
* @param applyPath - Which path to simulate
|
|
96
|
+
* @returns The path's audit, or the reason it could not be produced
|
|
97
|
+
* @private
|
|
98
|
+
*/
|
|
99
|
+
auditOneApplyPath(projectJson, merged, applyPath) {
|
|
100
|
+
const restricted = applyPath === "postinstall" || projectJson.name === LISA_PACKAGE_NAME;
|
|
101
|
+
const effective = restricted ? this.restrictToSecurityPins(merged) : merged;
|
|
102
|
+
try {
|
|
103
|
+
const forced = this.applyTemplate(projectJson, effective, this.PACKAGE_JSON, restricted);
|
|
104
|
+
return {
|
|
105
|
+
applyPath,
|
|
106
|
+
audit: auditSelfReferenceRewrites(forced.packageJson),
|
|
107
|
+
error: null,
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
catch (error) {
|
|
111
|
+
return {
|
|
112
|
+
applyPath,
|
|
113
|
+
audit: null,
|
|
114
|
+
error: error instanceof Error ? error.message : String(error),
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
}
|
|
81
118
|
/**
|
|
82
119
|
* Apply package-lisa strategy: Load templates from inheritance chain, apply to package.json
|
|
83
120
|
* @remarks
|
|
@@ -928,15 +965,6 @@ function describeUnrunGates(after, template) {
|
|
|
928
965
|
];
|
|
929
966
|
});
|
|
930
967
|
}
|
|
931
|
-
/** package.json sections whose keys are treated as direct dependencies. */
|
|
932
|
-
const DIRECT_DEPENDENCY_SECTIONS = [
|
|
933
|
-
"dependencies",
|
|
934
|
-
"devDependencies",
|
|
935
|
-
"optionalDependencies",
|
|
936
|
-
"peerDependencies",
|
|
937
|
-
];
|
|
938
|
-
/** package.json sections that carry npm-style version overrides. */
|
|
939
|
-
const OVERRIDE_SECTIONS = ["overrides", "resolutions"];
|
|
940
968
|
/**
|
|
941
969
|
* Every section whose forced values are a version FLOOR rather than an
|
|
942
970
|
* assignment, and which therefore may never be merged downwards.
|
|
@@ -1206,85 +1234,55 @@ function rangeFloor(range) {
|
|
|
1206
1234
|
* @returns Planned package.json plus operator-visible notes.
|
|
1207
1235
|
*/
|
|
1208
1236
|
function planSelfReferencingOverrideNormalization(pkg, fileName) {
|
|
1209
|
-
const
|
|
1210
|
-
|
|
1237
|
+
const audit = auditSelfReferenceRewrites(pkg);
|
|
1238
|
+
const refused = audit.conflicts[0];
|
|
1239
|
+
if (refused !== undefined) {
|
|
1240
|
+
throw new JsonMergeError(fileName, formatSelfReferenceRefusal(refused));
|
|
1241
|
+
}
|
|
1242
|
+
if (audit.rewritable.length === 0) {
|
|
1211
1243
|
return { packageJson: pkg, notes: [] };
|
|
1212
1244
|
}
|
|
1213
|
-
const
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
typeof entries !== "object" ||
|
|
1218
|
-
Array.isArray(entries)) {
|
|
1219
|
-
return acc;
|
|
1220
|
-
}
|
|
1221
|
-
const pairs = Object.entries(entries);
|
|
1222
|
-
const sectionRewrites = pairs.flatMap(([name, value]) => {
|
|
1223
|
-
if (!needsSelfReferenceRewrite(directDeps, name, value)) {
|
|
1224
|
-
return [];
|
|
1225
|
-
}
|
|
1226
|
-
const directRange = directRanges.get(name);
|
|
1227
|
-
if (directRange === undefined) {
|
|
1228
|
-
return [];
|
|
1229
|
-
}
|
|
1230
|
-
const literalRange = value;
|
|
1231
|
-
assertSelfReferenceRewriteDoesNotWiden(fileName, section, name, literalRange, directRange);
|
|
1232
|
-
return [{ section, name, literalRange, directRange }];
|
|
1233
|
-
});
|
|
1234
|
-
if (sectionRewrites.length === 0) {
|
|
1235
|
-
return acc;
|
|
1245
|
+
const sections = OVERRIDE_SECTIONS.flatMap(section => {
|
|
1246
|
+
const rewrites = audit.rewritable.filter(rewrite => rewrite.section === section);
|
|
1247
|
+
if (rewrites.length === 0) {
|
|
1248
|
+
return [];
|
|
1236
1249
|
}
|
|
1237
|
-
const normalized = Object.fromEntries(
|
|
1250
|
+
const normalized = Object.fromEntries(Object.entries(asRecord(pkg[section])).map(([name, value]) => rewrites.some(rewrite => rewrite.name === name)
|
|
1238
1251
|
? [name, `$${name}`]
|
|
1239
1252
|
: [name, value]));
|
|
1240
|
-
return
|
|
1241
|
-
|
|
1242
|
-
rewrites: [...acc.rewrites, ...sectionRewrites],
|
|
1243
|
-
};
|
|
1244
|
-
}, { sections: {}, rewrites: [] });
|
|
1245
|
-
const packageJson = Object.keys(rewritePlan.sections).length > 0
|
|
1246
|
-
? { ...pkg, ...rewritePlan.sections }
|
|
1247
|
-
: pkg;
|
|
1253
|
+
return [[section, normalized]];
|
|
1254
|
+
});
|
|
1248
1255
|
return {
|
|
1249
|
-
packageJson,
|
|
1250
|
-
notes:
|
|
1256
|
+
packageJson: { ...pkg, ...Object.fromEntries(sections) },
|
|
1257
|
+
notes: audit.rewritable.map(formatSelfReferenceRewriteNote),
|
|
1251
1258
|
};
|
|
1252
1259
|
}
|
|
1253
1260
|
/**
|
|
1254
|
-
*
|
|
1255
|
-
* @
|
|
1256
|
-
*
|
|
1257
|
-
*
|
|
1258
|
-
*
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
/**
|
|
1267
|
-
* Refuse a `$name` rewrite unless the direct dependency range is no wider.
|
|
1268
|
-
* @param fileName - Basename used in error messages
|
|
1269
|
-
* @param section - Override section carrying the entry
|
|
1270
|
-
* @param name - Package name being rewritten
|
|
1271
|
-
* @param literalRange - Literal override/resolution range to replace
|
|
1272
|
-
* @param directRange - Direct dependency range `$name` would resolve to
|
|
1273
|
-
* @throws JsonMergeError when the rewrite is unprovable or widening
|
|
1261
|
+
* Explain a refused `$name` rewrite, ending with a remedy Lisa has TESTED.
|
|
1262
|
+
* @remarks
|
|
1263
|
+
* The old message ended "Pin the direct dependency to <the override> or choose
|
|
1264
|
+
* a direct range that is no wider" — advice naming no concrete range which, for
|
|
1265
|
+
* a bounded floor, is not satisfiable by the obvious reading of it. An operator
|
|
1266
|
+
* who follows a refusal's own instructions and is refused a second time
|
|
1267
|
+
* concludes the guard is broken rather than the range wrong
|
|
1268
|
+
* (CodySwannGT/lisa#3191), which is how a correct security control gets routed
|
|
1269
|
+
* around. The replacement names ONE range and has run it back through the same
|
|
1270
|
+
* predicate that produced the refusal.
|
|
1271
|
+
* @param conflict - The conflict that stopped the apply
|
|
1272
|
+
* @returns Human-readable refusal naming both ranges and the verified raise
|
|
1274
1273
|
*/
|
|
1275
|
-
function
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
`
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
throw new JsonMergeError(fileName, `${section}.${name} would widen if rewritten to "$${name}": the literal ` +
|
|
1283
|
-
`override ${JSON.stringify(literalRange)} would resolve to direct ` +
|
|
1284
|
-
`dependency range ${JSON.stringify(directRange)}. Pin the direct ` +
|
|
1285
|
-
`dependency to ${JSON.stringify(literalRange)} or choose a direct range ` +
|
|
1286
|
-
`that is no wider than the override.`);
|
|
1274
|
+
function formatSelfReferenceRefusal(conflict) {
|
|
1275
|
+
const remedy = describeSelfReferenceRemedy(conflict);
|
|
1276
|
+
if (conflict.verdict === "unprovable") {
|
|
1277
|
+
return (`${conflict.section}.${conflict.name} cannot be rewritten to ` +
|
|
1278
|
+
`"$${conflict.name}" because Lisa cannot prove the direct dependency ` +
|
|
1279
|
+
`range ${JSON.stringify(conflict.directRange)} is no wider than the ` +
|
|
1280
|
+
`literal override ${JSON.stringify(conflict.floorRange)}. ${remedy}`);
|
|
1287
1281
|
}
|
|
1282
|
+
return (`${conflict.section}.${conflict.name} would widen if rewritten to ` +
|
|
1283
|
+
`"$${conflict.name}": the literal override ` +
|
|
1284
|
+
`${JSON.stringify(conflict.floorRange)} would resolve to direct ` +
|
|
1285
|
+
`dependency range ${JSON.stringify(conflict.directRange)}. ${remedy}`);
|
|
1288
1286
|
}
|
|
1289
1287
|
/**
|
|
1290
1288
|
* Format the operator-visible explanation for a safe `$name` rewrite.
|
|
@@ -1293,21 +1291,10 @@ function assertSelfReferenceRewriteDoesNotWiden(fileName, section, name, literal
|
|
|
1293
1291
|
*/
|
|
1294
1292
|
function formatSelfReferenceRewriteNote(rewrite) {
|
|
1295
1293
|
return (`Normalized ${rewrite.section}.${rewrite.name}: replaced literal ` +
|
|
1296
|
-
`${JSON.stringify(rewrite.
|
|
1294
|
+
`${JSON.stringify(rewrite.floorRange)} with "$${rewrite.name}" ` +
|
|
1297
1295
|
`resolving to direct dependency range ` +
|
|
1298
1296
|
`${JSON.stringify(rewrite.directRange)}.`);
|
|
1299
1297
|
}
|
|
1300
|
-
/**
|
|
1301
|
-
* Narrow an unknown value to a plain object record, returning {} otherwise.
|
|
1302
|
-
* @param value - Candidate value
|
|
1303
|
-
* @returns The value as a record, or an empty record when it is not a plain object
|
|
1304
|
-
*/
|
|
1305
|
-
function asRecord(value) {
|
|
1306
|
-
if (value !== null && typeof value === "object" && !Array.isArray(value)) {
|
|
1307
|
-
return value;
|
|
1308
|
-
}
|
|
1309
|
-
return {};
|
|
1310
|
-
}
|
|
1311
1298
|
/**
|
|
1312
1299
|
* Recursively collect every `$name` reference found in a single JSON value. npm
|
|
1313
1300
|
* overrides/resolutions use a `"$name"` string value to mean "resolve to the
|
|
@@ -1351,23 +1338,6 @@ function collectLiteralOverrideNames(sections) {
|
|
|
1351
1338
|
: []);
|
|
1352
1339
|
}));
|
|
1353
1340
|
}
|
|
1354
|
-
/**
|
|
1355
|
-
* Collect the names of every direct dependency declared across a package.json's
|
|
1356
|
-
* dependency sections.
|
|
1357
|
-
* @param pkg - Merged package.json object
|
|
1358
|
-
* @returns Set of direct dependency names
|
|
1359
|
-
*/
|
|
1360
|
-
function collectDirectDependencyNames(pkg) {
|
|
1361
|
-
return new Set(DIRECT_DEPENDENCY_SECTIONS.flatMap(section => Object.keys(asRecord(pkg[section]))));
|
|
1362
|
-
}
|
|
1363
|
-
/**
|
|
1364
|
-
* Collect direct dependency ranges by package name.
|
|
1365
|
-
* @param pkg - Merged package.json object
|
|
1366
|
-
* @returns Package name to string range map
|
|
1367
|
-
*/
|
|
1368
|
-
function collectDirectDependencyRanges(pkg) {
|
|
1369
|
-
return new Map(DIRECT_DEPENDENCY_SECTIONS.flatMap(section => Object.entries(asRecord(pkg[section])).flatMap(([name, value]) => typeof value === "string" ? [[name, value]] : [])));
|
|
1370
|
-
}
|
|
1371
1341
|
/**
|
|
1372
1342
|
* Fail the apply when the merged package.json carries a `$name` self-reference
|
|
1373
1343
|
* in overrides/resolutions without the backing direct dependency. Writing such
|
|
@@ -1452,4 +1422,16 @@ function assertManifestIsInstallable(pkg, fileName) {
|
|
|
1452
1422
|
assertNoDanglingDollarRefs(pkg, fileName);
|
|
1453
1423
|
}
|
|
1454
1424
|
/* eslint-enable max-lines -- Re-enable after comprehensive package merge strategy */
|
|
1425
|
+
/**
|
|
1426
|
+
* Count the literal version floors a resolved template would write.
|
|
1427
|
+
*
|
|
1428
|
+
* A `$name` entry is not counted: it carries no floor of its own, so it can
|
|
1429
|
+
* never be the thing a host range resolves below.
|
|
1430
|
+
* @param template - Fully resolved package.lisa.json template
|
|
1431
|
+
* @returns Number of literal entries across force.overrides and force.resolutions
|
|
1432
|
+
*/
|
|
1433
|
+
function countTemplateFloors(template) {
|
|
1434
|
+
return OVERRIDE_SECTIONS.reduce((total, section) => total +
|
|
1435
|
+
Object.values(asRecord(template.force[section])).filter(value => typeof value === "string" && !value.startsWith("$")).length, 0);
|
|
1436
|
+
}
|
|
1455
1437
|
//# sourceMappingURL=package-lisa.js.map
|