@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.
Files changed (113) hide show
  1. package/README.md +52 -3
  2. package/all/copy-overwrite/scripts/lib/github-attestation-recovery.mjs +48 -35
  3. package/all/copy-overwrite/scripts/lib/npm-update-hosted-gate.mjs +15 -1
  4. package/all/copy-overwrite/scripts/lib/npm-update-hosted-hook.mjs +56 -27
  5. package/all/copy-overwrite/scripts/lib/npm-update-invariants.mjs +78 -1
  6. package/all/copy-overwrite/scripts/lib/npm-update-native-process.mjs +29 -10
  7. package/all/copy-overwrite/scripts/lisa-automation-provenance.mjs +86 -37
  8. package/all/copy-overwrite/scripts/npm-updater-helper-graph.json +11 -8
  9. package/all/create-only/.github/workflows/continuous-gates.yml +2 -2
  10. package/all/create-only/.github/workflows/workflow-load-failure-sweep.yml +1 -1
  11. package/cdk/create-only/.github/workflows/ci.yml +2 -2
  12. package/cdk/create-only/.github/workflows/deploy.yml +3 -3
  13. package/cdk/package-lisa/package.lisa.json +1 -1
  14. package/dist/core/lisa-owned-hash-ledger.d.ts.map +1 -1
  15. package/dist/core/lisa-owned-hash-ledger.js +26 -0
  16. package/dist/core/lisa-owned-hash-ledger.js.map +1 -1
  17. package/dist/core/lisa-owned-templates.d.ts +9 -2
  18. package/dist/core/lisa-owned-templates.d.ts.map +1 -1
  19. package/dist/core/lisa-owned-templates.js +13 -3
  20. package/dist/core/lisa-owned-templates.js.map +1 -1
  21. package/dist/core/nightly-e2e-guard-behavior-certificate.js +2 -2
  22. package/dist/core/upstream-evidence-manifest.d.ts.map +1 -1
  23. package/dist/core/upstream-evidence-manifest.js +59 -37
  24. package/dist/core/upstream-evidence-manifest.js.map +1 -1
  25. package/expo/copy-overwrite/jest.setup.pre.js +15 -1
  26. package/expo/create-only/.github/workflows/ci.yml +3 -3
  27. package/expo/create-only/.github/workflows/deploy.yml +4 -4
  28. package/expo/create-only/.github/workflows/nightly-e2e-bypass-reaper.yml +1 -1
  29. package/expo/create-only/.github/workflows/playwright-e2e.yml +1 -1
  30. package/expo/create-only/eas.json +1 -1
  31. package/harper-fabric/copy-overwrite/.github/workflows/ci.yml +1 -1
  32. package/harper-fabric/create-only/.github/workflows/deploy.yml +3 -3
  33. package/harper-fabric/create-only/.github/workflows/zap-baseline.yml +1 -1
  34. package/harper-fabric/package-lisa/package.lisa.json +2 -2
  35. package/nestjs/create-only/.github/workflows/ci.yml +2 -2
  36. package/nestjs/create-only/.github/workflows/deploy.yml +5 -5
  37. package/package.json +6 -6
  38. package/phaser/copy-overwrite/.github/workflows/ci.yml +1 -1
  39. package/phaser/package-lisa/package.lisa.json +2 -2
  40. package/plugins/lisa/.claude-plugin/plugin.json +1 -1
  41. package/plugins/lisa/.codex-plugin/plugin.json +1 -1
  42. package/plugins/lisa/.codex-plugin/skills/lisa-parity-sentry-sdk-setup/SKILL.md +104 -0
  43. package/plugins/lisa/.codex-plugin/skills/lisa-setup-workstation/scripts/workstation.mjs +3 -1
  44. package/plugins/lisa/skills/lisa-parity-sentry-sdk-setup/SKILL.md +104 -0
  45. package/plugins/lisa/skills/lisa-setup-workstation/scripts/workstation.mjs +3 -1
  46. package/plugins/lisa-agy/plugin.json +1 -1
  47. package/plugins/lisa-agy/skills/lisa-parity-sentry-sdk-setup/SKILL.md +104 -0
  48. package/plugins/lisa-agy/skills/lisa-setup-workstation/scripts/workstation.mjs +3 -1
  49. package/plugins/lisa-cdk/.claude-plugin/plugin.json +1 -1
  50. package/plugins/lisa-cdk/.codex-plugin/plugin.json +1 -1
  51. package/plugins/lisa-cdk-agy/plugin.json +1 -1
  52. package/plugins/lisa-cdk-copilot/.claude-plugin/plugin.json +1 -1
  53. package/plugins/lisa-cdk-cursor/.claude-plugin/plugin.json +1 -1
  54. package/plugins/lisa-copilot/.claude-plugin/plugin.json +1 -1
  55. package/plugins/lisa-copilot/skills/lisa-parity-sentry-sdk-setup/SKILL.md +104 -0
  56. package/plugins/lisa-copilot/skills/lisa-setup-workstation/scripts/workstation.mjs +3 -1
  57. package/plugins/lisa-cursor/.claude-plugin/plugin.json +1 -1
  58. package/plugins/lisa-cursor/skills/lisa-parity-sentry-sdk-setup/SKILL.md +104 -0
  59. package/plugins/lisa-cursor/skills/lisa-setup-workstation/scripts/workstation.mjs +3 -1
  60. package/plugins/lisa-expo/.claude-plugin/plugin.json +1 -1
  61. package/plugins/lisa-expo/.codex-plugin/plugin.json +1 -1
  62. package/plugins/lisa-expo-agy/plugin.json +1 -1
  63. package/plugins/lisa-expo-copilot/.claude-plugin/plugin.json +1 -1
  64. package/plugins/lisa-expo-cursor/.claude-plugin/plugin.json +1 -1
  65. package/plugins/lisa-harper-fabric/.claude-plugin/plugin.json +1 -1
  66. package/plugins/lisa-harper-fabric/.codex-plugin/plugin.json +1 -1
  67. package/plugins/lisa-harper-fabric-agy/plugin.json +1 -1
  68. package/plugins/lisa-harper-fabric-copilot/.claude-plugin/plugin.json +1 -1
  69. package/plugins/lisa-harper-fabric-cursor/.claude-plugin/plugin.json +1 -1
  70. package/plugins/lisa-nestjs/.claude-plugin/plugin.json +1 -1
  71. package/plugins/lisa-nestjs/.codex-plugin/plugin.json +1 -1
  72. package/plugins/lisa-nestjs-agy/plugin.json +1 -1
  73. package/plugins/lisa-nestjs-copilot/.claude-plugin/plugin.json +1 -1
  74. package/plugins/lisa-nestjs-cursor/.claude-plugin/plugin.json +1 -1
  75. package/plugins/lisa-openclaw/.claude-plugin/plugin.json +1 -1
  76. package/plugins/lisa-openclaw/.codex-plugin/plugin.json +1 -1
  77. package/plugins/lisa-openclaw-agy/plugin.json +1 -1
  78. package/plugins/lisa-openclaw-copilot/.claude-plugin/plugin.json +1 -1
  79. package/plugins/lisa-openclaw-cursor/.claude-plugin/plugin.json +1 -1
  80. package/plugins/lisa-phaser/.claude-plugin/plugin.json +1 -1
  81. package/plugins/lisa-phaser/.codex-plugin/plugin.json +1 -1
  82. package/plugins/lisa-phaser-agy/plugin.json +1 -1
  83. package/plugins/lisa-phaser-copilot/.claude-plugin/plugin.json +1 -1
  84. package/plugins/lisa-phaser-cursor/.claude-plugin/plugin.json +1 -1
  85. package/plugins/lisa-rails/.claude-plugin/plugin.json +1 -1
  86. package/plugins/lisa-rails/.codex-plugin/plugin.json +1 -1
  87. package/plugins/lisa-rails-agy/plugin.json +1 -1
  88. package/plugins/lisa-rails-copilot/.claude-plugin/plugin.json +1 -1
  89. package/plugins/lisa-rails-cursor/.claude-plugin/plugin.json +1 -1
  90. package/plugins/lisa-typescript/.claude-plugin/plugin.json +1 -1
  91. package/plugins/lisa-typescript/.codex-plugin/plugin.json +1 -1
  92. package/plugins/lisa-typescript-agy/plugin.json +1 -1
  93. package/plugins/lisa-typescript-copilot/.claude-plugin/plugin.json +1 -1
  94. package/plugins/lisa-typescript-cursor/.claude-plugin/plugin.json +1 -1
  95. package/plugins/lisa-wiki/.claude-plugin/plugin.json +1 -1
  96. package/plugins/lisa-wiki/.codex-plugin/plugin.json +1 -1
  97. package/plugins/lisa-wiki-agy/plugin.json +1 -1
  98. package/plugins/lisa-wiki-copilot/.claude-plugin/plugin.json +1 -1
  99. package/plugins/lisa-wiki-cursor/.claude-plugin/plugin.json +1 -1
  100. package/plugins/src/base/skills/lisa-parity-sentry-sdk-setup/SKILL.md +104 -0
  101. package/plugins/src/base/skills/lisa-setup-workstation/scripts/workstation.mjs +3 -1
  102. package/scripts/check-shell-guard-refusal-coverage.mjs +65 -7
  103. package/scripts/generate-lisa-owned-hash-ledger.mjs +3 -0
  104. package/scripts/lib/shell-guard-shard-runner.mjs +381 -0
  105. package/scripts/lib/shell-guard-shards.mjs +383 -0
  106. package/scripts/lib/shell-guard-terminal-reporter.mjs +89 -0
  107. package/scripts/two-channel-couplings.json +1 -1
  108. package/scripts/update-node-version.ts +52 -6
  109. package/typescript/copy-overwrite/.nvmrc +1 -1
  110. package/typescript/create-only/.github/workflows/ci.yml +1 -1
  111. package/typescript/create-only/.github/workflows/review-evidence.yml +1 -1
  112. package/typescript/create-only/.github/workflows/third-party-review-evidence.yml +1 -1
  113. 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 22.23.3 baseline,
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 overwritten `.nvmrc` supplies the shared Node runtime. Review older explicit
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
- validateRecovery(recovery, descriptor, digest, policy);
255
- for (const key of [
256
- "npmPolicySha256",
257
- "proposalKey",
258
- "leafBodySha256",
259
- "commit",
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
- recovery[key] === scope[key],
263
- `current recovery ${key} differs`
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
- requireProof(
266
- process.env.GITHUB_RUN_ID === recovery.runId &&
267
- process.env.GITHUB_RUN_ATTEMPT === recovery.runAttempt,
268
- "recovery replayed into different current invocation"
272
+ });
273
+ const origin = withProvenancePhase("historical-provider", () =>
274
+ verifyHistoricalProvider(policy, descriptor, execute)
269
275
  );
270
- const origin = verifyHistoricalProvider(policy, descriptor, execute);
271
- verifyHistoricalDescriptor(
272
- policy,
273
- descriptor,
274
- paths,
275
- digest,
276
- origin.run,
277
- origin.claim,
278
- execute
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
- verifyRecoveryAttestation(
281
- policy,
282
- recovery,
283
- paths,
284
- scope.recoverySha256,
285
- execute
287
+ withProvenancePhase("recovery-signature", () =>
288
+ verifyRecoveryAttestation(
289
+ policy,
290
+ recovery,
291
+ paths,
292
+ scope.recoverySha256,
293
+ execute
294
+ )
286
295
  );
287
- verifyRecoveryProvider(
288
- policy,
289
- descriptor,
290
- recovery,
291
- scope.maintainer,
292
- execute
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 { withStage } from "./npm-update-invariants.mjs";
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
- required(
117
- args.length >= 3 && args[0] === "--context" && args[2] === "--",
118
- "invalid hosted Node gateway"
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 = hookEntry(context, original, realpathSync(process.cwd()));
123
- if (entry)
124
- required(
125
- !original.some(arg =>
126
- /^(?:-r|--require|--import|--(?:experimental-)?loader)(?:=|$)/.test(arg)
127
- ) &&
128
- !process.env.NODE_OPTIONS &&
129
- !process.env.NODE_PATH,
130
- "unqualified original canonical hook loader"
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 chunks = [];
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: Buffer.concat(chunks),
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("original hosted hook command failed\n");
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(error, category, status, signal, errno) {
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
- Object.assign(error, {
157
- stdout: Buffer.concat(chunks),
158
- stderr: Buffer.concat(errors),
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
- /** Verify fixed private proof against the final message, tree, binding and provider. */
92
- export function verifyAutomationProvenance(messageFile) {
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
- assertPinnedVerifier(policy);
114
- const context = canonicalContext(message, config, policy, descriptor);
115
- const recoveryBytes = optional[0]
116
- ? privateBytes(paths.recovery, 65_536)
117
- : null;
118
- const recoveryBundle = optional[0]
119
- ? privateBytes(paths.recoveryBundle, 1_048_576)
120
- : null;
121
- if (recoveryBytes) {
122
- recoveryPermission(
123
- policy,
124
- descriptor,
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
- return true;
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
  }