@codyswann/lisa 4.73.8 → 4.74.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/README.md +52 -3
- package/all/copy-overwrite/scripts/lib/github-attestation-recovery.mjs +48 -35
- package/all/copy-overwrite/scripts/lib/npm-update-hosted-gate.mjs +15 -1
- package/all/copy-overwrite/scripts/lib/npm-update-hosted-hook.mjs +56 -27
- package/all/copy-overwrite/scripts/lib/npm-update-invariants.mjs +78 -1
- package/all/copy-overwrite/scripts/lib/npm-update-native-process.mjs +29 -10
- package/all/copy-overwrite/scripts/lisa-automation-provenance.mjs +86 -37
- package/all/copy-overwrite/scripts/npm-updater-helper-graph.json +11 -8
- package/all/create-only/.github/workflows/continuous-gates.yml +2 -2
- package/all/create-only/.github/workflows/workflow-load-failure-sweep.yml +1 -1
- package/cdk/create-only/.github/workflows/ci.yml +2 -2
- package/cdk/create-only/.github/workflows/deploy.yml +3 -3
- package/cdk/package-lisa/package.lisa.json +1 -1
- package/dist/core/lisa-owned-hash-ledger.d.ts.map +1 -1
- package/dist/core/lisa-owned-hash-ledger.js +26 -0
- package/dist/core/lisa-owned-hash-ledger.js.map +1 -1
- package/dist/core/lisa-owned-templates.d.ts +9 -2
- package/dist/core/lisa-owned-templates.d.ts.map +1 -1
- package/dist/core/lisa-owned-templates.js +13 -3
- package/dist/core/lisa-owned-templates.js.map +1 -1
- package/dist/core/nightly-e2e-guard-behavior-certificate.js +2 -2
- package/dist/core/upstream-evidence-manifest.d.ts.map +1 -1
- package/dist/core/upstream-evidence-manifest.js +59 -37
- package/dist/core/upstream-evidence-manifest.js.map +1 -1
- package/expo/copy-overwrite/jest.setup.pre.js +15 -1
- package/expo/create-only/.github/workflows/ci.yml +3 -3
- package/expo/create-only/.github/workflows/deploy.yml +4 -4
- package/expo/create-only/.github/workflows/nightly-e2e-bypass-reaper.yml +1 -1
- package/expo/create-only/.github/workflows/playwright-e2e.yml +1 -1
- package/expo/create-only/eas.json +1 -1
- package/harper-fabric/copy-overwrite/.github/workflows/ci.yml +1 -1
- package/harper-fabric/create-only/.github/workflows/deploy.yml +3 -3
- package/harper-fabric/create-only/.github/workflows/zap-baseline.yml +1 -1
- package/harper-fabric/package-lisa/package.lisa.json +2 -2
- package/nestjs/create-only/.github/workflows/ci.yml +2 -2
- package/nestjs/create-only/.github/workflows/deploy.yml +5 -5
- package/package.json +6 -6
- package/phaser/copy-overwrite/.github/workflows/ci.yml +1 -1
- package/phaser/package-lisa/package.lisa.json +2 -2
- 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-parity-sentry-sdk-setup/SKILL.md +104 -0
- package/plugins/lisa/.codex-plugin/skills/lisa-setup-workstation/scripts/workstation.mjs +3 -1
- package/plugins/lisa/skills/lisa-parity-sentry-sdk-setup/SKILL.md +104 -0
- package/plugins/lisa/skills/lisa-setup-workstation/scripts/workstation.mjs +3 -1
- package/plugins/lisa-agy/plugin.json +1 -1
- package/plugins/lisa-agy/skills/lisa-parity-sentry-sdk-setup/SKILL.md +104 -0
- package/plugins/lisa-agy/skills/lisa-setup-workstation/scripts/workstation.mjs +3 -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/skills/lisa-parity-sentry-sdk-setup/SKILL.md +104 -0
- package/plugins/lisa-copilot/skills/lisa-setup-workstation/scripts/workstation.mjs +3 -1
- package/plugins/lisa-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-cursor/skills/lisa-parity-sentry-sdk-setup/SKILL.md +104 -0
- package/plugins/lisa-cursor/skills/lisa-setup-workstation/scripts/workstation.mjs +3 -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/plugins/src/base/skills/lisa-parity-sentry-sdk-setup/SKILL.md +104 -0
- package/plugins/src/base/skills/lisa-setup-workstation/scripts/workstation.mjs +3 -1
- package/scripts/check-shell-guard-refusal-coverage.mjs +65 -7
- package/scripts/generate-lisa-owned-hash-ledger.mjs +3 -0
- package/scripts/lib/shell-guard-shard-runner.mjs +381 -0
- package/scripts/lib/shell-guard-shards.mjs +383 -0
- package/scripts/lib/shell-guard-terminal-reporter.mjs +89 -0
- package/scripts/two-channel-couplings.json +1 -1
- package/scripts/update-node-version.ts +52 -6
- package/typescript/copy-overwrite/.nvmrc +1 -1
- package/typescript/create-only/.github/workflows/ci.yml +1 -1
- package/typescript/create-only/.github/workflows/review-evidence.yml +1 -1
- package/typescript/create-only/.github/workflows/third-party-review-evidence.yml +1 -1
- package/typescript/package-lisa/package.lisa.json +1 -1
package/README.md
CHANGED
|
@@ -186,7 +186,7 @@ any direct override in the current directory's `package.json`. It preserves the
|
|
|
186
186
|
original error and does not change dependencies. Nested selectors and unreadable
|
|
187
187
|
manifests remain unverified; this diagnostic is not a full lockfile audit.
|
|
188
188
|
|
|
189
|
-
Clean TypeScript and CDK hosts receive the shared Node
|
|
189
|
+
Clean TypeScript and CDK hosts receive the shared Node 24.21.0 LTS baseline,
|
|
190
190
|
Vitest and coverage-v8 4.1.11 minimums, and a Vite 8.3.2 minimum within Vite 8.
|
|
191
191
|
Lisa retains the TypeScript 6, ESLint 9, Knip 5, Husky 8 and Vitest 4 families.
|
|
192
192
|
New CDK hosts default to `aws-cdk-github-oidc ^5.2.0` with `constructs ^10.7.2`
|
|
@@ -204,11 +204,42 @@ lockfile alongside the host's test, typecheck and offline synth commands.
|
|
|
204
204
|
Package template ownership still applies. Explicit TypeScript Node and test-tool
|
|
205
205
|
`defaults` survive an apply, while CDK's `force` test tools advance to the shared
|
|
206
206
|
patch floor. Existing create-only workflow and Expo EAS files stay host-owned.
|
|
207
|
-
The
|
|
207
|
+
The managed `.nvmrc` supplies the shared Node runtime. Known older Lisa copies
|
|
208
|
+
advance with historical hash protection; unknown or newer custom bytes remain
|
|
209
|
+
protected. `.lisaignore` keeps an intentional runtime override outside Lisa
|
|
210
|
+
management. Review older explicit
|
|
208
211
|
pins and create-only workflow inputs before adopting this baseline in an
|
|
209
212
|
existing host. Maintainers can run `bun run update-node-version` after changing
|
|
210
213
|
the root `.nvmrc` to update current tracked runtime surfaces without rewriting
|
|
211
|
-
historical plans or generic test fixtures.
|
|
214
|
+
historical plans or generic test fixtures. The npm updater has a separately
|
|
215
|
+
qualified Node22.23.3 runtime, which this shared-default updater leaves alone.
|
|
216
|
+
|
|
217
|
+
### Moving an existing host to Node24
|
|
218
|
+
|
|
219
|
+
Review explicit `engines.node` and existing create-only workflow/EAS inputs;
|
|
220
|
+
Lisa seeds missing values and preserves those existing host-owned choices.
|
|
221
|
+
Remove `.nvmrc` from `.lisaignore` only when adopting Lisa's managed baseline.
|
|
222
|
+
If an unknown custom `.nvmrc` is preserved, deliberately select that exact file
|
|
223
|
+
with `lisa apply . --refresh-templates=.nvmrc`, then review the result. Run the
|
|
224
|
+
host's install, typecheck, build and tests on the selected runtime before deploy.
|
|
225
|
+
|
|
226
|
+
AWS Lambda `nodejs24.x` removes callback handlers and legacy context completion
|
|
227
|
+
APIs, including `callbackWaitsForEmptyEventLoop`. Use supported async/await,
|
|
228
|
+
synchronous-return or streaming forms; do not assign removed context properties.
|
|
229
|
+
Earlier non-streaming async handlers already completed without waiting for
|
|
230
|
+
unresolved promises. Node24 aligns streaming completion with that behavior:
|
|
231
|
+
await required work before returning or ending the stream. Local Node execution
|
|
232
|
+
does not prove deployed Lambda completion. See [AWS's runtime changes](https://aws.amazon.com/blogs/compute/node-js-24-runtime-now-available-in-aws-lambda/).
|
|
233
|
+
|
|
234
|
+
For unwrapped scheduled, queue or Cognito handlers using Sentry, await a finite
|
|
235
|
+
`Sentry.flush(timeoutMs)` on success and error paths. Handle false/rejected flush
|
|
236
|
+
without replacing the original return value or thrown error; do not call
|
|
237
|
+
`close()` per invocation, which disables the warm client. A compatible Sentry
|
|
238
|
+
Lambda wrapper already flushes, by default for up to2000ms via `flushTimeout`.
|
|
239
|
+
SDK compatibility matters: `@sentry/aws-serverless`11.6.0 still assigns the removed
|
|
240
|
+
context property and cannot wrap a non-extensible context lacking it. The
|
|
241
|
+
[SDK setup guidance](https://github.com/CodySwannGT/lisa/blob/main/plugins/src/base/skills/lisa-parity-sentry-sdk-setup/SKILL.md#node24-lambda-completion)
|
|
242
|
+
includes the safe unwrapped pattern, wrapper limits and migration checks.
|
|
212
243
|
|
|
213
244
|
When Lisa is invoked during installation it prints this next step. Package
|
|
214
245
|
managers that do not run its lifecycle scripts cannot display that notice.
|
|
@@ -460,6 +491,24 @@ Rails consumers can follow the [test helper isolation and existing-consumer migr
|
|
|
460
491
|
|
|
461
492
|
If you're changing Lisa itself: author agent content and templates at their source, never in generated output, and rebuild so the distributed artifacts regenerate. Lisa applies its own standards to itself, so the same gates that guard downstream projects guard this one — including the requirement to prove your change works.
|
|
462
493
|
|
|
494
|
+
Lisa's main test suite runs on Node24.21.0. Its npm updater remains separately
|
|
495
|
+
qualified on Node22.23.3, so unit coverage, push gates and the shell-guard tracer
|
|
496
|
+
also need that real executable. On a Unix checkout using mise:
|
|
497
|
+
|
|
498
|
+
```sh
|
|
499
|
+
mise install node@22.23.3 node@24.21.0 bun@1.3.8 bun@1.3.11
|
|
500
|
+
export LISA_TEST_UPDATER_NODE="$(mise where node@22.23.3)/bin/node"
|
|
501
|
+
export LISA_TEST_HOST_BUN="$(mise where bun@1.3.11)/bin/bun"
|
|
502
|
+
"$LISA_TEST_UPDATER_NODE" -p process.versions.node
|
|
503
|
+
mise exec node@24.21.0 bun@1.3.8 -- bun run test:unit
|
|
504
|
+
```
|
|
505
|
+
|
|
506
|
+
The probe must print22.23.3; the fixtures verify its actual version and bytes.
|
|
507
|
+
On macOS, select the short, non-symlink temp base `/private/tmp` before the
|
|
508
|
+
native wrapper when running Unix-socket fixtures. Inherited Harper and Phaser
|
|
509
|
+
native host checks also require their unchanged Bun1.3.11 floor; keep that
|
|
510
|
+
host runtime distinct from Lisa's Bun1.3.8 tooling.
|
|
511
|
+
|
|
463
512
|
> **Prompt for your coding agent**
|
|
464
513
|
> "I want to add or change a skill, rule, hook, or agent in Lisa. Show me the source location, the build step, what I must commit alongside it, and the CI check that fails if I edit a generated artifact directly. Then check my change against those rules before I commit."
|
|
465
514
|
|
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
verifyAssignable,
|
|
9
9
|
} from "./github-attestation-provider.mjs";
|
|
10
10
|
import { boundedSpawnSync } from "./bounded-spawn.mjs";
|
|
11
|
+
import { withProvenancePhase } from "./npm-update-invariants.mjs";
|
|
11
12
|
import {
|
|
12
13
|
requireProof,
|
|
13
14
|
verifiedRole,
|
|
@@ -251,45 +252,57 @@ export function verifyRecoveryOrigin(
|
|
|
251
252
|
scope,
|
|
252
253
|
execute = boundedSpawnSync
|
|
253
254
|
) {
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
255
|
+
withProvenancePhase("recovery-local", () => {
|
|
256
|
+
validateRecovery(recovery, descriptor, digest, policy);
|
|
257
|
+
for (const key of [
|
|
258
|
+
"npmPolicySha256",
|
|
259
|
+
"proposalKey",
|
|
260
|
+
"leafBodySha256",
|
|
261
|
+
"commit",
|
|
262
|
+
])
|
|
263
|
+
requireProof(
|
|
264
|
+
recovery[key] === scope[key],
|
|
265
|
+
`current recovery ${key} differs`
|
|
266
|
+
);
|
|
261
267
|
requireProof(
|
|
262
|
-
|
|
263
|
-
|
|
268
|
+
process.env.GITHUB_RUN_ID === recovery.runId &&
|
|
269
|
+
process.env.GITHUB_RUN_ATTEMPT === recovery.runAttempt,
|
|
270
|
+
"recovery replayed into different current invocation"
|
|
264
271
|
);
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
"recovery replayed into different current invocation"
|
|
272
|
+
});
|
|
273
|
+
const origin = withProvenancePhase("historical-provider", () =>
|
|
274
|
+
verifyHistoricalProvider(policy, descriptor, execute)
|
|
269
275
|
);
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
276
|
+
withProvenancePhase("historical-signature", () =>
|
|
277
|
+
verifyHistoricalDescriptor(
|
|
278
|
+
policy,
|
|
279
|
+
descriptor,
|
|
280
|
+
paths,
|
|
281
|
+
digest,
|
|
282
|
+
origin.run,
|
|
283
|
+
origin.claim,
|
|
284
|
+
execute
|
|
285
|
+
)
|
|
279
286
|
);
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
287
|
+
withProvenancePhase("recovery-signature", () =>
|
|
288
|
+
verifyRecoveryAttestation(
|
|
289
|
+
policy,
|
|
290
|
+
recovery,
|
|
291
|
+
paths,
|
|
292
|
+
scope.recoverySha256,
|
|
293
|
+
execute
|
|
294
|
+
)
|
|
286
295
|
);
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
296
|
+
withProvenancePhase("recovery-provider", () =>
|
|
297
|
+
verifyRecoveryProvider(
|
|
298
|
+
policy,
|
|
299
|
+
descriptor,
|
|
300
|
+
recovery,
|
|
301
|
+
scope.maintainer,
|
|
302
|
+
execute
|
|
303
|
+
)
|
|
304
|
+
);
|
|
305
|
+
withProvenancePhase("assignable", () =>
|
|
306
|
+
verifyAssignable(policy, scope.maintainer, execute)
|
|
293
307
|
);
|
|
294
|
-
verifyAssignable(policy, scope.maintainer, execute);
|
|
295
308
|
}
|
|
@@ -17,7 +17,10 @@ import { affirmRuntimeProfile } from "./npm-update-rails-runtime-contract.mjs";
|
|
|
17
17
|
import { prepareRailsTools } from "./npm-update-rails-tools.mjs";
|
|
18
18
|
import { openRailsMysqlRuntime } from "./npm-update-rails-mysql.mjs";
|
|
19
19
|
import { installOriginalManager } from "./npm-update-hook-installation.mjs";
|
|
20
|
-
import {
|
|
20
|
+
import {
|
|
21
|
+
withStage,
|
|
22
|
+
inheritProvenanceFailure,
|
|
23
|
+
} from "./npm-update-invariants.mjs";
|
|
21
24
|
|
|
22
25
|
const INSTALL_STAGE = "gate-install";
|
|
23
26
|
const VALIDATE_STAGE = "gate-validate";
|
|
@@ -66,6 +69,17 @@ function nativeStep(context, env, command, args, maximum = 3_145_728) {
|
|
|
66
69
|
env,
|
|
67
70
|
timeout: remainingGateTime(context.deadline),
|
|
68
71
|
maximum,
|
|
72
|
+
}).catch(error => {
|
|
73
|
+
if (
|
|
74
|
+
command === "node" &&
|
|
75
|
+
env.LISA_NPM_HOOK_ROLE === "commit" &&
|
|
76
|
+
typeof env.HOME === "string" &&
|
|
77
|
+
args.length === 2 &&
|
|
78
|
+
args[0] === "scripts/lisa-automation-provenance.mjs" &&
|
|
79
|
+
args[1] === join(env.HOME, "message")
|
|
80
|
+
)
|
|
81
|
+
inheritProvenanceFailure(error);
|
|
82
|
+
throw error;
|
|
69
83
|
});
|
|
70
84
|
}
|
|
71
85
|
|
|
@@ -9,6 +9,11 @@ import { realpathSync, lstatSync } from "node:fs";
|
|
|
9
9
|
import { resolve, join, basename } from "node:path";
|
|
10
10
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
11
11
|
import { binaryDigest } from "./npm-update-isolation.mjs";
|
|
12
|
+
import {
|
|
13
|
+
withProvenancePhase,
|
|
14
|
+
recordProvenancePhase,
|
|
15
|
+
provenanceFailure,
|
|
16
|
+
} from "./npm-update-invariants.mjs";
|
|
12
17
|
|
|
13
18
|
const ENTRY = fileURLToPath(import.meta.url);
|
|
14
19
|
const PRELOAD = fileURLToPath(
|
|
@@ -111,55 +116,79 @@ export function tokenFreeHookEnvironment(source) {
|
|
|
111
116
|
return env;
|
|
112
117
|
}
|
|
113
118
|
|
|
119
|
+
/** Input consumes the same fixed bound and one original deadline observation before native execution. */
|
|
120
|
+
async function originalHookInput(context) {
|
|
121
|
+
const chunks = [];
|
|
122
|
+
let size = 0;
|
|
123
|
+
try {
|
|
124
|
+
for await (const chunk of process.stdin) {
|
|
125
|
+
size += chunk.length;
|
|
126
|
+
required(size <= 4_194_304, "original hook stdin exceeded");
|
|
127
|
+
chunks.push(chunk);
|
|
128
|
+
}
|
|
129
|
+
const timeout = Math.min(1_800_000, context.deadline - Date.now());
|
|
130
|
+
required(timeout > 0, "original hook phase expired");
|
|
131
|
+
return { input: Buffer.concat(chunks), timeout };
|
|
132
|
+
} catch (error) {
|
|
133
|
+
recordProvenancePhase(error, "gateway-input");
|
|
134
|
+
throw error;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
114
138
|
async function main() {
|
|
115
139
|
const args = process.argv.slice(2);
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
140
|
+
withProvenancePhase("gateway-context", () =>
|
|
141
|
+
required(
|
|
142
|
+
args.length >= 3 && args[0] === "--context" && args[2] === "--",
|
|
143
|
+
"invalid hosted Node gateway"
|
|
144
|
+
)
|
|
145
|
+
);
|
|
146
|
+
const context = withProvenancePhase("gateway-context", () =>
|
|
147
|
+
hostedHookContext(args[1])
|
|
119
148
|
);
|
|
120
|
-
const context = hostedHookContext(args[1]);
|
|
121
149
|
const original = args.slice(3);
|
|
122
|
-
const entry =
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
!
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
150
|
+
const entry = withProvenancePhase("gateway-entry", () =>
|
|
151
|
+
hookEntry(context, original, realpathSync(process.cwd()))
|
|
152
|
+
);
|
|
153
|
+
withProvenancePhase("gateway-entry", () => {
|
|
154
|
+
if (entry)
|
|
155
|
+
required(
|
|
156
|
+
!original.some(arg =>
|
|
157
|
+
/^(?:-r|--require|--import|--(?:experimental-)?loader)(?:=|$)/.test(
|
|
158
|
+
arg
|
|
159
|
+
)
|
|
160
|
+
) &&
|
|
161
|
+
!process.env.NODE_OPTIONS &&
|
|
162
|
+
!process.env.NODE_PATH,
|
|
163
|
+
"unqualified original canonical hook loader"
|
|
164
|
+
);
|
|
165
|
+
});
|
|
132
166
|
const env = tokenFreeHookEnvironment(process.env);
|
|
133
167
|
if (entry) env.LISA_NPM_HOSTED_HOOK_CONTEXT = context.file;
|
|
134
168
|
else delete env.LISA_NPM_HOSTED_HOOK_CONTEXT;
|
|
135
|
-
const
|
|
136
|
-
let size = 0;
|
|
137
|
-
for await (const chunk of process.stdin) {
|
|
138
|
-
size += chunk.length;
|
|
139
|
-
required(size <= 4_194_304, "original hook stdin exceeded");
|
|
140
|
-
chunks.push(chunk);
|
|
141
|
-
}
|
|
142
|
-
const timeout = Math.min(1_800_000, context.deadline - Date.now());
|
|
143
|
-
required(timeout > 0, "original hook phase expired");
|
|
169
|
+
const { input, timeout } = await originalHookInput(context);
|
|
144
170
|
const result = await runProcess(
|
|
145
171
|
context.native.node.path,
|
|
146
172
|
entry ? ["--import", pathToFileURL(PRELOAD).href, ...original] : original,
|
|
147
173
|
{
|
|
148
174
|
cwd: process.cwd(),
|
|
149
175
|
env,
|
|
150
|
-
input
|
|
176
|
+
input,
|
|
151
177
|
timeout,
|
|
152
178
|
maximum: 8_388_608,
|
|
153
179
|
allowed: Array.from({ length: 256 }, (_, code) => code),
|
|
154
180
|
}
|
|
155
|
-
)
|
|
181
|
+
).catch(error => {
|
|
182
|
+
recordProvenancePhase(error, "gateway-child");
|
|
183
|
+
throw error;
|
|
184
|
+
});
|
|
156
185
|
process.stdout.write(result.stdout);
|
|
157
186
|
process.stderr.write(result.stderr);
|
|
158
187
|
process.exitCode = result.code;
|
|
159
188
|
}
|
|
160
189
|
|
|
161
190
|
if (process.argv[1] && resolve(process.argv[1]) === ENTRY)
|
|
162
|
-
main().catch(
|
|
163
|
-
process.stderr.write(
|
|
191
|
+
main().catch(error => {
|
|
192
|
+
process.stderr.write(`${provenanceFailure(error)}\n`);
|
|
164
193
|
process.exitCode = 1;
|
|
165
194
|
});
|
|
@@ -29,6 +29,73 @@ const stages = new Set([
|
|
|
29
29
|
"gate-output",
|
|
30
30
|
]);
|
|
31
31
|
const diagnostics = new WeakMap();
|
|
32
|
+
const capturedErrors = new WeakMap();
|
|
33
|
+
const provenancePhases = new Set([
|
|
34
|
+
"local-proof",
|
|
35
|
+
"configuration",
|
|
36
|
+
"verifier",
|
|
37
|
+
"canonical-context",
|
|
38
|
+
"recovery-local",
|
|
39
|
+
"historical-provider",
|
|
40
|
+
"historical-signature",
|
|
41
|
+
"recovery-signature",
|
|
42
|
+
"recovery-provider",
|
|
43
|
+
"assignable",
|
|
44
|
+
"ordinary-signature",
|
|
45
|
+
"ordinary-provider",
|
|
46
|
+
"final-proof",
|
|
47
|
+
"gateway-context",
|
|
48
|
+
"gateway-entry",
|
|
49
|
+
"gateway-input",
|
|
50
|
+
"gateway-child",
|
|
51
|
+
]);
|
|
52
|
+
const provenancePrefix =
|
|
53
|
+
"Invalid automation provenance: required proof, policy or provider evidence failed";
|
|
54
|
+
|
|
55
|
+
/** A synchronous verifier keeps its original exception and innermost source-selected phase. */
|
|
56
|
+
export function withProvenancePhase(phase, operation) {
|
|
57
|
+
required(provenancePhases.has(phase), "invalid provenance diagnostic phase");
|
|
58
|
+
try {
|
|
59
|
+
return operation();
|
|
60
|
+
} catch (error) {
|
|
61
|
+
recordProvenancePhase(error, phase);
|
|
62
|
+
throw error;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** Shared sync/async catches never inspect exception properties, including hostile getters. */
|
|
67
|
+
export function recordProvenancePhase(error, phase) {
|
|
68
|
+
required(provenancePhases.has(phase), "invalid provenance diagnostic phase");
|
|
69
|
+
if (
|
|
70
|
+
error !== null &&
|
|
71
|
+
(typeof error === "object" || typeof error === "function")
|
|
72
|
+
) {
|
|
73
|
+
const prior = diagnostics.get(error) ?? {};
|
|
74
|
+
diagnostics.set(error, { ...prior, provenance: prior.provenance ?? phase });
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** Canonical CLI and gateway failures expose one closed label, never provider or exception text. */
|
|
79
|
+
export function provenanceFailure(error) {
|
|
80
|
+
return `${provenancePrefix} (phase=${diagnostics.get(error)?.provenance ?? "unknown"})`;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** Only a complete fixed diagnostic from an observed native exit-one can add a phase, never authority. */
|
|
84
|
+
export function inheritProvenanceFailure(error) {
|
|
85
|
+
const facts = diagnostics.get(error);
|
|
86
|
+
const bytes = capturedErrors.get(error);
|
|
87
|
+
if (
|
|
88
|
+
facts?.native !== "exit" ||
|
|
89
|
+
facts.status !== 1 ||
|
|
90
|
+
!bytes ||
|
|
91
|
+
bytes.length > 256
|
|
92
|
+
)
|
|
93
|
+
return;
|
|
94
|
+
const text = bytes.toString("utf8");
|
|
95
|
+
for (const phase of provenancePhases)
|
|
96
|
+
if (text === `${provenancePrefix} (phase=${phase})\n`)
|
|
97
|
+
recordProvenancePhase(error, phase);
|
|
98
|
+
}
|
|
32
99
|
const signals = new Set([
|
|
33
100
|
"SIGTERM",
|
|
34
101
|
"SIGINT",
|
|
@@ -81,7 +148,16 @@ export async function withStage(stage, operation) {
|
|
|
81
148
|
}
|
|
82
149
|
|
|
83
150
|
/** Only native capture registers observed close facts; exception properties never establish provenance. */
|
|
84
|
-
export function recordNativeFailure(
|
|
151
|
+
export function recordNativeFailure(
|
|
152
|
+
error,
|
|
153
|
+
category,
|
|
154
|
+
status,
|
|
155
|
+
signal,
|
|
156
|
+
errno,
|
|
157
|
+
stderr
|
|
158
|
+
) {
|
|
159
|
+
if (Buffer.isBuffer(stderr) && stderr.length <= 256)
|
|
160
|
+
capturedErrors.set(error, Buffer.from(stderr));
|
|
85
161
|
diagnostics.set(error, {
|
|
86
162
|
...diagnostics.get(error),
|
|
87
163
|
native: categories.has(category) ? category : "unknown",
|
|
@@ -101,6 +177,7 @@ export function publicFailure(error) {
|
|
|
101
177
|
if (facts.signal) parts.push(`signal=${facts.signal}`);
|
|
102
178
|
if (facts.errno) parts.push(`errno=${facts.errno}`);
|
|
103
179
|
}
|
|
180
|
+
if (facts?.provenance) parts.push(`provenance=${facts.provenance}`);
|
|
104
181
|
return `npm updater failed: required policy, authorization, installation, gate or exact publication evidence was unavailable (${parts.join(" ")})`;
|
|
105
182
|
}
|
|
106
183
|
|
|
@@ -95,6 +95,29 @@ export function runProcess(
|
|
|
95
95
|
});
|
|
96
96
|
}
|
|
97
97
|
|
|
98
|
+
/** Retain the original native error and captured bytes while registering only observed close facts. */
|
|
99
|
+
function observedFailure(error, observation) {
|
|
100
|
+
const {
|
|
101
|
+
chunks,
|
|
102
|
+
errors,
|
|
103
|
+
command,
|
|
104
|
+
code,
|
|
105
|
+
nativeCompleted,
|
|
106
|
+
category,
|
|
107
|
+
signal,
|
|
108
|
+
errno,
|
|
109
|
+
} = observation;
|
|
110
|
+
const stderr = Buffer.concat(errors);
|
|
111
|
+
recordNativeFailure(error, category, code, signal, errno, stderr);
|
|
112
|
+
return Object.assign(error, {
|
|
113
|
+
stdout: Buffer.concat(chunks),
|
|
114
|
+
stderr,
|
|
115
|
+
command,
|
|
116
|
+
code,
|
|
117
|
+
nativeCompleted,
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
|
|
98
121
|
/** Native close, not cancellation delivery, completes an owned child. */
|
|
99
122
|
function captureProcess(
|
|
100
123
|
child,
|
|
@@ -145,20 +168,16 @@ function captureProcess(
|
|
|
145
168
|
if (failure || signal || !allowed.includes(code)) {
|
|
146
169
|
const error =
|
|
147
170
|
failure ?? new Error(`npm updater: child failed (${code ?? signal})`);
|
|
148
|
-
recordNativeFailure(
|
|
149
|
-
error,
|
|
150
|
-
category ?? (signal ? "signal" : "exit"),
|
|
151
|
-
code,
|
|
152
|
-
signal,
|
|
153
|
-
errno
|
|
154
|
-
);
|
|
155
171
|
reject(
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
172
|
+
observedFailure(error, {
|
|
173
|
+
chunks,
|
|
174
|
+
errors,
|
|
159
175
|
command,
|
|
160
176
|
code,
|
|
161
177
|
nativeCompleted: !failure && !signal,
|
|
178
|
+
category: category ?? (signal ? "signal" : "exit"),
|
|
179
|
+
signal,
|
|
180
|
+
errno,
|
|
162
181
|
})
|
|
163
182
|
);
|
|
164
183
|
return;
|
|
@@ -7,6 +7,10 @@ import {
|
|
|
7
7
|
verifyRecoveryOrigin,
|
|
8
8
|
} from "./lib/github-attestation-recovery.mjs";
|
|
9
9
|
import { invokedAsScript } from "./lib/invoked-as-script.mjs";
|
|
10
|
+
import {
|
|
11
|
+
withProvenancePhase,
|
|
12
|
+
provenanceFailure,
|
|
13
|
+
} from "./lib/npm-update-invariants.mjs";
|
|
10
14
|
import {
|
|
11
15
|
assertPinnedVerifier,
|
|
12
16
|
requireProof,
|
|
@@ -29,6 +33,8 @@ import {
|
|
|
29
33
|
} from "./lib/automation-provenance-local.mjs";
|
|
30
34
|
export { canonicalJson } from "./lib/automation-provenance-contract.mjs";
|
|
31
35
|
|
|
36
|
+
const LOCAL_PROOF = "local-proof";
|
|
37
|
+
|
|
32
38
|
/** A present but malformed reference is a failure, even with ordinary AI trailers. */
|
|
33
39
|
export function automationReference(message) {
|
|
34
40
|
const lines = message
|
|
@@ -88,14 +94,8 @@ function recoveryPermission(
|
|
|
88
94
|
);
|
|
89
95
|
}
|
|
90
96
|
|
|
91
|
-
/**
|
|
92
|
-
|
|
93
|
-
const messageBytes = privateBytes(messageFile, 65_536, false);
|
|
94
|
-
const message = messageBytes.toString("utf8");
|
|
95
|
-
const reference = automationReference(message);
|
|
96
|
-
if (!reference) return false;
|
|
97
|
-
const { config, policy } = trustedConfiguration(git);
|
|
98
|
-
const { paths, optional, bytes, bundleBytes, descriptor } = proofSnapshot();
|
|
97
|
+
/** Keep unchanged local authorization together without obscuring the canonical phase. */
|
|
98
|
+
function verifyLocalProposal(descriptor, messageBytes, reference, policy) {
|
|
99
99
|
localDescriptor(descriptor, messageBytes, reference, policy);
|
|
100
100
|
npmProposal(descriptor, git);
|
|
101
101
|
requireProof(
|
|
@@ -110,30 +110,18 @@ export function verifyAutomationProvenance(messageFile) {
|
|
|
110
110
|
),
|
|
111
111
|
"deterministic proposal key differs"
|
|
112
112
|
);
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
context,
|
|
126
|
-
config,
|
|
127
|
-
bytes,
|
|
128
|
-
paths,
|
|
129
|
-
recoveryBytes,
|
|
130
|
-
recoveryBundle,
|
|
131
|
-
messageBytes
|
|
132
|
-
);
|
|
133
|
-
} else {
|
|
134
|
-
verifyDescriptorAttestation(policy, descriptor, paths, sha256(bytes));
|
|
135
|
-
verifyCurrentProvider(policy, descriptor);
|
|
136
|
-
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** Final rereads preserve the original mutation checks after all provider verification. */
|
|
116
|
+
function verifyFinalSnapshot(
|
|
117
|
+
paths,
|
|
118
|
+
bundleBytes,
|
|
119
|
+
bytes,
|
|
120
|
+
descriptor,
|
|
121
|
+
messageFile,
|
|
122
|
+
reference,
|
|
123
|
+
policy
|
|
124
|
+
) {
|
|
137
125
|
requireProof(
|
|
138
126
|
privateBytes(paths.bundle, 1_048_576).equals(bundleBytes),
|
|
139
127
|
"origin bundle changed during verification"
|
|
@@ -148,17 +136,78 @@ export function verifyAutomationProvenance(messageFile) {
|
|
|
148
136
|
reference,
|
|
149
137
|
policy
|
|
150
138
|
);
|
|
151
|
-
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/** Verify fixed private proof against the final message, tree, binding and provider. */
|
|
142
|
+
export function verifyAutomationProvenance(messageFile) {
|
|
143
|
+
const messageBytes = withProvenancePhase(LOCAL_PROOF, () =>
|
|
144
|
+
privateBytes(messageFile, 65_536, false)
|
|
145
|
+
);
|
|
146
|
+
const message = messageBytes.toString("utf8");
|
|
147
|
+
const reference = withProvenancePhase(LOCAL_PROOF, () =>
|
|
148
|
+
automationReference(message)
|
|
149
|
+
);
|
|
150
|
+
if (!reference) return false;
|
|
151
|
+
const { config, policy } = withProvenancePhase("configuration", () =>
|
|
152
|
+
trustedConfiguration(git)
|
|
153
|
+
);
|
|
154
|
+
const { paths, optional, bytes, bundleBytes, descriptor } =
|
|
155
|
+
withProvenancePhase(LOCAL_PROOF, proofSnapshot);
|
|
156
|
+
withProvenancePhase(LOCAL_PROOF, () =>
|
|
157
|
+
verifyLocalProposal(descriptor, messageBytes, reference, policy)
|
|
158
|
+
);
|
|
159
|
+
withProvenancePhase("verifier", () => assertPinnedVerifier(policy));
|
|
160
|
+
const context = withProvenancePhase("canonical-context", () =>
|
|
161
|
+
canonicalContext(message, config, policy, descriptor)
|
|
162
|
+
);
|
|
163
|
+
return withProvenancePhase("final-proof", () => {
|
|
164
|
+
const recoveryBytes = optional[0]
|
|
165
|
+
? privateBytes(paths.recovery, 65_536)
|
|
166
|
+
: null;
|
|
167
|
+
const recoveryBundle = optional[0]
|
|
168
|
+
? privateBytes(paths.recoveryBundle, 1_048_576)
|
|
169
|
+
: null;
|
|
170
|
+
if (recoveryBytes) {
|
|
171
|
+
withProvenancePhase("recovery-local", () =>
|
|
172
|
+
recoveryPermission(
|
|
173
|
+
policy,
|
|
174
|
+
descriptor,
|
|
175
|
+
context,
|
|
176
|
+
config,
|
|
177
|
+
bytes,
|
|
178
|
+
paths,
|
|
179
|
+
recoveryBytes,
|
|
180
|
+
recoveryBundle,
|
|
181
|
+
messageBytes
|
|
182
|
+
)
|
|
183
|
+
);
|
|
184
|
+
} else {
|
|
185
|
+
withProvenancePhase("ordinary-signature", () =>
|
|
186
|
+
verifyDescriptorAttestation(policy, descriptor, paths, sha256(bytes))
|
|
187
|
+
);
|
|
188
|
+
withProvenancePhase("ordinary-provider", () =>
|
|
189
|
+
verifyCurrentProvider(policy, descriptor)
|
|
190
|
+
);
|
|
191
|
+
}
|
|
192
|
+
verifyFinalSnapshot(
|
|
193
|
+
paths,
|
|
194
|
+
bundleBytes,
|
|
195
|
+
bytes,
|
|
196
|
+
descriptor,
|
|
197
|
+
messageFile,
|
|
198
|
+
reference,
|
|
199
|
+
policy
|
|
200
|
+
);
|
|
201
|
+
return true;
|
|
202
|
+
});
|
|
152
203
|
}
|
|
153
204
|
|
|
154
205
|
if (invokedAsScript(import.meta.url)) {
|
|
155
206
|
try {
|
|
156
207
|
const verified = verifyAutomationProvenance(process.argv[2]);
|
|
157
208
|
process.exitCode = verified ? 0 : 10;
|
|
158
|
-
} catch {
|
|
159
|
-
console.error(
|
|
160
|
-
"Invalid automation provenance: required proof, policy or provider evidence failed"
|
|
161
|
-
);
|
|
209
|
+
} catch (error) {
|
|
210
|
+
console.error(provenanceFailure(error));
|
|
162
211
|
process.exitCode = 1;
|
|
163
212
|
}
|
|
164
213
|
}
|