@codyswann/lisa 4.71.1 → 4.71.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/all/copy-overwrite/scripts/lib/github-attestation-provider.mjs +22 -0
- package/all/copy-overwrite/scripts/lib/history-secret-scanner.mjs +46 -37
- package/all/copy-overwrite/scripts/lib/npm-update-allocate.mjs +13 -3
- package/all/copy-overwrite/scripts/lib/npm-update-cancel-origin.mjs +291 -0
- package/all/copy-overwrite/scripts/lib/npm-update-cancellation-proof.mjs +327 -0
- package/all/copy-overwrite/scripts/lib/npm-update-cancellation.mjs +248 -0
- package/all/copy-overwrite/scripts/lib/npm-update-gate.mjs +52 -35
- package/all/copy-overwrite/scripts/lib/npm-update-helper-inventory.mjs +4 -0
- package/all/copy-overwrite/scripts/lib/npm-update-hosted-gate.mjs +48 -26
- package/all/copy-overwrite/scripts/lib/npm-update-invariants.mjs +101 -0
- package/all/copy-overwrite/scripts/lib/npm-update-leaf-contract.mjs +1 -1
- package/all/copy-overwrite/scripts/lib/npm-update-leaf.mjs +29 -3
- package/all/copy-overwrite/scripts/lib/npm-update-native-process.mjs +24 -6
- package/all/copy-overwrite/scripts/lib/npm-update-supersession.mjs +274 -0
- package/all/copy-overwrite/scripts/lisa-npm-updater.mjs +98 -21
- package/all/copy-overwrite/scripts/npm-updater-helper-graph.json +82 -12
- package/dist/core/lisa-owned-hash-ledger.d.ts.map +1 -1
- package/dist/core/lisa-owned-hash-ledger.js +86 -0
- package/dist/core/lisa-owned-hash-ledger.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 +28 -14
- package/dist/core/upstream-evidence-manifest.js.map +1 -1
- package/package.json +4 -4
- package/plugins/lisa/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-agy/plugin.json +1 -1
- package/plugins/lisa-cdk/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-cdk/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-cdk-agy/plugin.json +1 -1
- package/plugins/lisa-cdk-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-cdk-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-expo/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-expo/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-expo-agy/plugin.json +1 -1
- package/plugins/lisa-expo-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-expo-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-harper-fabric/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-harper-fabric/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-harper-fabric-agy/plugin.json +1 -1
- package/plugins/lisa-harper-fabric-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-harper-fabric-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-nestjs/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-nestjs/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-nestjs-agy/plugin.json +1 -1
- package/plugins/lisa-nestjs-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-nestjs-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-openclaw/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-openclaw/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-openclaw-agy/plugin.json +1 -1
- package/plugins/lisa-openclaw-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-openclaw-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-phaser/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-phaser/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-phaser-agy/plugin.json +1 -1
- package/plugins/lisa-phaser-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-phaser-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-rails/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-rails/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-rails-agy/plugin.json +1 -1
- package/plugins/lisa-rails-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-rails-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-typescript/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-typescript/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-typescript-agy/plugin.json +1 -1
- package/plugins/lisa-typescript-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-typescript-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-wiki/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-wiki/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-wiki-agy/plugin.json +1 -1
- package/plugins/lisa-wiki-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-wiki-cursor/.claude-plugin/plugin.json +1 -1
- package/rails/copy-overwrite/ast-grep/rules/ruby/no-unsafe-send.yml +17 -0
- package/scripts/two-channel-couplings.json +2 -2
|
@@ -42,6 +42,9 @@ const PRODUCER = [
|
|
|
42
42
|
"allocate",
|
|
43
43
|
"authorization",
|
|
44
44
|
"broker-client",
|
|
45
|
+
"cancel-origin",
|
|
46
|
+
"cancellation",
|
|
47
|
+
"cancellation-proof",
|
|
45
48
|
"checkpoint",
|
|
46
49
|
"contract",
|
|
47
50
|
"controller-broker",
|
|
@@ -81,6 +84,7 @@ const PRODUCER = [
|
|
|
81
84
|
"publish",
|
|
82
85
|
"quality",
|
|
83
86
|
"recovery",
|
|
87
|
+
"supersession",
|
|
84
88
|
"runtime-archive",
|
|
85
89
|
"runtime-graph",
|
|
86
90
|
"runtime-transport",
|
|
@@ -21,6 +21,7 @@ import {
|
|
|
21
21
|
} from "./npm-update-controller-factory.mjs";
|
|
22
22
|
import { startHookReadBroker } from "./npm-update-hook-provider.mjs";
|
|
23
23
|
import { originalHookInstallation } from "./npm-update-hook-installation.mjs";
|
|
24
|
+
import { withStage } from "./npm-update-invariants.mjs";
|
|
24
25
|
import {
|
|
25
26
|
sha256,
|
|
26
27
|
PROPOSAL_PREDICATE,
|
|
@@ -234,22 +235,9 @@ function nodeGateway(root, context) {
|
|
|
234
235
|
return bin;
|
|
235
236
|
}
|
|
236
237
|
|
|
237
|
-
/**
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
const native = controllerTools(context.config.automationProvenance);
|
|
241
|
-
const graph = await qualifiedControllerGraph(context.cwd, context.config, [
|
|
242
|
-
"lisa-work-item.mjs",
|
|
243
|
-
"lisa-automation-provenance.mjs",
|
|
244
|
-
"lisa-rails-prepush.mjs",
|
|
245
|
-
"lib/npm-update-hosted-hook.mjs",
|
|
246
|
-
"lib/npm-update-hook-preload.mjs",
|
|
247
|
-
]);
|
|
248
|
-
const installed = await installHostedDependencies(context, root, env);
|
|
249
|
-
const installation = await installOriginalManager(context, installed);
|
|
250
|
-
const scope = await proofScope(context, installed);
|
|
251
|
-
mkdirSync(join(root, "gh-config"), { mode: 0o700 });
|
|
252
|
-
const profile = {
|
|
238
|
+
/** The unchanged read-only subject is constructed separately so diagnostic wrapping stays within function budgets. */
|
|
239
|
+
function hookProfile(context, root, native, scope) {
|
|
240
|
+
return {
|
|
253
241
|
version: 1,
|
|
254
242
|
deadline: context.deadline,
|
|
255
243
|
cwd: context.cwd,
|
|
@@ -267,16 +255,50 @@ export async function createHostedGate(context, root, env) {
|
|
|
267
255
|
scope.proofs
|
|
268
256
|
),
|
|
269
257
|
};
|
|
270
|
-
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/** A separate read-only capability is available during original gates; it cannot acquire publisher/issuer permissions. */
|
|
261
|
+
export async function createHostedGate(context, root, env) {
|
|
262
|
+
await withStage("gate-validate", () => assertHostedGate(context));
|
|
263
|
+
const native = await withStage("gate-tools", () =>
|
|
264
|
+
controllerTools(context.config.automationProvenance)
|
|
265
|
+
);
|
|
266
|
+
const graph = await withStage("gate-graph", () =>
|
|
267
|
+
qualifiedControllerGraph(context.cwd, context.config, [
|
|
268
|
+
"lisa-work-item.mjs",
|
|
269
|
+
"lisa-automation-provenance.mjs",
|
|
270
|
+
"lisa-rails-prepush.mjs",
|
|
271
|
+
"lib/npm-update-hosted-hook.mjs",
|
|
272
|
+
"lib/npm-update-hook-preload.mjs",
|
|
273
|
+
])
|
|
274
|
+
);
|
|
275
|
+
const installed = await withStage("gate-install", () =>
|
|
276
|
+
installHostedDependencies(context, root, env)
|
|
277
|
+
);
|
|
278
|
+
const installation = await withStage("gate-hooks", () =>
|
|
279
|
+
installOriginalManager(context, installed)
|
|
280
|
+
);
|
|
281
|
+
const scope = await withStage("gate-scope", () =>
|
|
282
|
+
proofScope(context, installed)
|
|
283
|
+
);
|
|
284
|
+
const profile = await withStage("gate-scope", () => {
|
|
285
|
+
mkdirSync(join(root, "gh-config"), { mode: 0o700 });
|
|
286
|
+
return hookProfile(context, root, native, scope);
|
|
287
|
+
});
|
|
288
|
+
const broker = await withStage("gate-broker", () =>
|
|
289
|
+
startHookReadBroker(profile, context.token)
|
|
290
|
+
);
|
|
271
291
|
try {
|
|
272
|
-
const bin =
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
292
|
+
const bin = await withStage("gate-gateway", () =>
|
|
293
|
+
nodeGateway(root, {
|
|
294
|
+
version: 1,
|
|
295
|
+
root,
|
|
296
|
+
cwd: context.cwd,
|
|
297
|
+
graph,
|
|
298
|
+
native,
|
|
299
|
+
reader: { root, path: broker.path, deadline: context.deadline },
|
|
300
|
+
})
|
|
301
|
+
);
|
|
280
302
|
return {
|
|
281
303
|
installation,
|
|
282
304
|
env: {
|
|
@@ -294,7 +316,7 @@ export async function createHostedGate(context, root, env) {
|
|
|
294
316
|
close: broker.close,
|
|
295
317
|
};
|
|
296
318
|
} catch (error) {
|
|
297
|
-
await broker.close();
|
|
319
|
+
await withStage("gate-close", () => broker.close());
|
|
298
320
|
throw error;
|
|
299
321
|
}
|
|
300
322
|
}
|
|
@@ -2,6 +2,107 @@
|
|
|
2
2
|
// Do not edit directly — durable changes belong upstream in Lisa.
|
|
3
3
|
|
|
4
4
|
/** Pure shared diagnostics retain one error identity without importing producer orchestration. */
|
|
5
|
+
const stages = new Set([
|
|
6
|
+
"configuration",
|
|
7
|
+
"cancel-intent",
|
|
8
|
+
"cancel-origin",
|
|
9
|
+
"cancel-checkpoint",
|
|
10
|
+
"cancel-close",
|
|
11
|
+
"gate-input",
|
|
12
|
+
"gate-validate",
|
|
13
|
+
"gate-scratch",
|
|
14
|
+
"gate-stage",
|
|
15
|
+
"gate-baseline",
|
|
16
|
+
"gate-link",
|
|
17
|
+
"gate-attach",
|
|
18
|
+
"gate-proof",
|
|
19
|
+
"gate-tools",
|
|
20
|
+
"gate-graph",
|
|
21
|
+
"gate-install",
|
|
22
|
+
"gate-hooks",
|
|
23
|
+
"gate-scope",
|
|
24
|
+
"gate-broker",
|
|
25
|
+
"gate-gateway",
|
|
26
|
+
"gate-commit",
|
|
27
|
+
"gate-push",
|
|
28
|
+
"gate-close",
|
|
29
|
+
"gate-output",
|
|
30
|
+
]);
|
|
31
|
+
const diagnostics = new WeakMap();
|
|
32
|
+
const signals = new Set([
|
|
33
|
+
"SIGTERM",
|
|
34
|
+
"SIGINT",
|
|
35
|
+
"SIGKILL",
|
|
36
|
+
"SIGHUP",
|
|
37
|
+
"SIGQUIT",
|
|
38
|
+
"SIGABRT",
|
|
39
|
+
"SIGPIPE",
|
|
40
|
+
"SIGSEGV",
|
|
41
|
+
"SIGBUS",
|
|
42
|
+
"SIGILL",
|
|
43
|
+
"SIGTRAP",
|
|
44
|
+
"SIGFPE",
|
|
45
|
+
]);
|
|
46
|
+
const errnos = new Set([
|
|
47
|
+
"EACCES",
|
|
48
|
+
"ENOENT",
|
|
49
|
+
"ENOEXEC",
|
|
50
|
+
"EMFILE",
|
|
51
|
+
"ENFILE",
|
|
52
|
+
"EAGAIN",
|
|
53
|
+
"EIO",
|
|
54
|
+
"EPIPE",
|
|
55
|
+
]);
|
|
56
|
+
const categories = new Set([
|
|
57
|
+
"exit",
|
|
58
|
+
"signal",
|
|
59
|
+
"cancelled",
|
|
60
|
+
"deadline",
|
|
61
|
+
"output-bound",
|
|
62
|
+
"input-failed",
|
|
63
|
+
"spawn-error",
|
|
64
|
+
]);
|
|
65
|
+
|
|
66
|
+
/** Nested stages preserve the innermost observed boundary and the exact original thrown value. */
|
|
67
|
+
export async function withStage(stage, operation) {
|
|
68
|
+
required(stages.has(stage), "invalid updater diagnostic stage");
|
|
69
|
+
try {
|
|
70
|
+
return await operation();
|
|
71
|
+
} catch (error) {
|
|
72
|
+
if (
|
|
73
|
+
error !== null &&
|
|
74
|
+
(typeof error === "object" || typeof error === "function")
|
|
75
|
+
) {
|
|
76
|
+
const prior = diagnostics.get(error) ?? {};
|
|
77
|
+
diagnostics.set(error, { ...prior, stage: prior.stage ?? stage });
|
|
78
|
+
}
|
|
79
|
+
throw error;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** Only native capture registers observed close facts; exception properties never establish provenance. */
|
|
84
|
+
export function recordNativeFailure(error, category, status, signal, errno) {
|
|
85
|
+
diagnostics.set(error, {
|
|
86
|
+
...diagnostics.get(error),
|
|
87
|
+
native: categories.has(category) ? category : "unknown",
|
|
88
|
+
status:
|
|
89
|
+
Number.isInteger(status) && status >= 0 && status <= 255 ? status : null,
|
|
90
|
+
signal: signals.has(signal) ? signal : null,
|
|
91
|
+
errno: errnos.has(errno) ? errno : null,
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** Never inspect arbitrary messages, stacks, getters, commands, paths or captured child output. */
|
|
96
|
+
export function publicFailure(error) {
|
|
97
|
+
const facts = diagnostics.get(error);
|
|
98
|
+
const parts = [`stage=${facts?.stage ?? "unknown"}`];
|
|
99
|
+
if (facts?.native) {
|
|
100
|
+
parts.push(`native=${facts.native}`, `status=${facts.status ?? "null"}`);
|
|
101
|
+
if (facts.signal) parts.push(`signal=${facts.signal}`);
|
|
102
|
+
if (facts.errno) parts.push(`errno=${facts.errno}`);
|
|
103
|
+
}
|
|
104
|
+
return `npm updater failed: required policy, authorization, installation, gate or exact publication evidence was unavailable (${parts.join(" ")})`;
|
|
105
|
+
}
|
|
5
106
|
|
|
6
107
|
/** Only trusted static boundary reasons may leave a phase as a public diagnostic. */
|
|
7
108
|
export class UpdaterError extends Error {
|
|
@@ -142,7 +142,7 @@ Git history command: ${evidence.history.command}
|
|
|
142
142
|
Git history outcome: ${evidence.history.result}
|
|
143
143
|
All-state GitHub query: ${evidence.search.query}
|
|
144
144
|
Outcome: every matching record was loaded completely; unrelated work was preserved and exact proposal recovery was checked separately.
|
|
145
|
-
Matches are read completely before allocating this deterministic proposal. No parent or PRD lineage is declared; this build-ready standalone Task is one leaf.
|
|
145
|
+
Matches are read completely before allocating this deterministic proposal. No parent or PRD lineage is declared; this build-ready standalone Task is one leaf.${evidence.supersession ? `\nSupersedes cancelled proposal ${evidence.supersession.workItem}. Original proposal key: ${evidence.supersession.proposalKey}. Verified cancellation SHA256: ${evidence.supersession.cancellationSha256}. The previous claim, specification and checkpoint are preserved; this leaf has fresh parent-bound authority.` : ""}
|
|
146
146
|
|
|
147
147
|
## Validation Journey
|
|
148
148
|
|
|
@@ -82,12 +82,29 @@ export async function claimLeaf(
|
|
|
82
82
|
|
|
83
83
|
/** Complete provider history and immutable human policy govern every mutation. */
|
|
84
84
|
export async function inspectLeaf(issue, draft, policy, config) {
|
|
85
|
-
const roles = leafRoles(config, policy.repository);
|
|
86
|
-
const labels = issue.labels.map(label => label.name);
|
|
87
85
|
required(
|
|
88
86
|
issue.state === "open" && !issue.pull_request && issue.number > 0,
|
|
89
87
|
"closed or nonissue leaf"
|
|
90
88
|
);
|
|
89
|
+
return inspectLeafFields(issue, draft, policy, config);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** Closed-state inspection never grants authority without a separately verified cancellation. */
|
|
93
|
+
export async function inspectCancelledLeaf(issue, draft, policy, config) {
|
|
94
|
+
required(
|
|
95
|
+
issue.state === "closed" &&
|
|
96
|
+
issue.state_reason === "not_planned" &&
|
|
97
|
+
!issue.pull_request &&
|
|
98
|
+
issue.number > 0,
|
|
99
|
+
"leaf is not explicitly cancelled"
|
|
100
|
+
);
|
|
101
|
+
return inspectLeafFields(issue, draft, policy, config);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** Both fixed state entry points preserve the original complete ownership and hold checks. */
|
|
105
|
+
async function inspectLeafFields(issue, draft, policy, config) {
|
|
106
|
+
const roles = leafRoles(config, policy.repository);
|
|
107
|
+
const labels = issue.labels.map(label => label.name);
|
|
91
108
|
required(
|
|
92
109
|
issue.children.length === 0 &&
|
|
93
110
|
issue.blockers.every(item => item.state === "closed"),
|
|
@@ -151,7 +168,16 @@ export function historicalFiling(issue) {
|
|
|
151
168
|
const value = JSON.parse(
|
|
152
169
|
new TextDecoder("utf-8", { fatal: true, ignoreBOM: true }).decode(bytes)
|
|
153
170
|
);
|
|
154
|
-
keys(value, [
|
|
171
|
+
keys(value, [
|
|
172
|
+
"history",
|
|
173
|
+
"search",
|
|
174
|
+
"registry",
|
|
175
|
+
"labels",
|
|
176
|
+
"main",
|
|
177
|
+
...(value.supersession ? ["supersession"] : []),
|
|
178
|
+
]);
|
|
179
|
+
if (value.supersession)
|
|
180
|
+
keys(value.supersession, ["workItem", "cancellationSha256", "proposalKey"]);
|
|
155
181
|
required(
|
|
156
182
|
canonicalJson(value) === bytes.toString("utf8"),
|
|
157
183
|
"original filing fields are noncanonical"
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
import { spawn } from "node:child_process";
|
|
6
6
|
import { fileURLToPath } from "node:url";
|
|
7
7
|
import { required } from "./npm-update-contract.mjs";
|
|
8
|
+
import { recordNativeFailure } from "./npm-update-invariants.mjs";
|
|
8
9
|
|
|
9
10
|
/** The complete supervisor vector, including its control overhead, is bounded literal data. */
|
|
10
11
|
function supervisedArguments(vector) {
|
|
@@ -104,27 +105,37 @@ function captureProcess(
|
|
|
104
105
|
const errors = [];
|
|
105
106
|
let size = 0;
|
|
106
107
|
let failure;
|
|
108
|
+
let category;
|
|
109
|
+
let errno;
|
|
107
110
|
let closed = false;
|
|
108
|
-
const stop = reason => {
|
|
109
|
-
|
|
111
|
+
const stop = (reason, kind) => {
|
|
112
|
+
if (!failure) {
|
|
113
|
+
failure = new Error(`npm updater: ${reason}`);
|
|
114
|
+
category = kind;
|
|
115
|
+
}
|
|
110
116
|
if (!closed && child.pid) child.kill("SIGTERM");
|
|
111
117
|
};
|
|
112
|
-
const abort = () => stop("child was cancelled");
|
|
118
|
+
const abort = () => stop("child was cancelled", "cancelled");
|
|
113
119
|
abortSignal?.addEventListener("abort", abort, { once: true });
|
|
114
120
|
if (abortSignal?.aborted) abort();
|
|
115
|
-
const timer = setTimeout(
|
|
121
|
+
const timer = setTimeout(
|
|
122
|
+
() => stop("child exceeded deadline", "deadline"),
|
|
123
|
+
timeout
|
|
124
|
+
);
|
|
116
125
|
const collect = target => data => {
|
|
117
126
|
size += data.length;
|
|
118
|
-
if (size > maximum) stop("child output exceeded bound");
|
|
127
|
+
if (size > maximum) stop("child output exceeded bound", "output-bound");
|
|
119
128
|
else target.push(data);
|
|
120
129
|
};
|
|
121
130
|
child.stdout.on("data", collect(chunks));
|
|
122
131
|
child.stderr.on("data", collect(errors));
|
|
123
132
|
child.on("error", error => {
|
|
124
133
|
failure = error;
|
|
134
|
+
category = "spawn-error";
|
|
135
|
+
errno = error.code;
|
|
125
136
|
});
|
|
126
137
|
child.stdin.on("error", error => {
|
|
127
|
-
if (error.code !== "EPIPE") stop("child input failed");
|
|
138
|
+
if (error.code !== "EPIPE") stop("child input failed", "input-failed");
|
|
128
139
|
});
|
|
129
140
|
child.stdin.end(input);
|
|
130
141
|
child.on("close", (code, signal) => {
|
|
@@ -134,6 +145,13 @@ function captureProcess(
|
|
|
134
145
|
if (failure || signal || !allowed.includes(code)) {
|
|
135
146
|
const error =
|
|
136
147
|
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
|
+
);
|
|
137
155
|
reject(
|
|
138
156
|
Object.assign(error, {
|
|
139
157
|
stdout: Buffer.concat(chunks),
|
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
// This file is managed by Lisa and IS replaced on each `lisa` run.
|
|
2
|
+
// Do not edit directly — durable changes belong upstream in Lisa.
|
|
3
|
+
|
|
4
|
+
/** Authenticated cancellation chains remain finite, exact and separate from ordinary recovery. */
|
|
5
|
+
import { canonicalJson } from "../lisa-automation-provenance.mjs";
|
|
6
|
+
import { required } from "./npm-update-contract.mjs";
|
|
7
|
+
import { validateProposal, keys } from "./npm-update-contract.mjs";
|
|
8
|
+
import { join } from "node:path";
|
|
9
|
+
import { writeFileSync } from "node:fs";
|
|
10
|
+
import { writeJson, withPrivateRoot } from "./npm-update-process.mjs";
|
|
11
|
+
import { sha256 } from "./github-attestation-verifier.mjs";
|
|
12
|
+
import {
|
|
13
|
+
completeCheckpoints,
|
|
14
|
+
committedCancellationConfig,
|
|
15
|
+
authenticateStaleOrigin,
|
|
16
|
+
assertOriginTransport,
|
|
17
|
+
} from "./npm-update-cancel-origin.mjs";
|
|
18
|
+
import {
|
|
19
|
+
cancellationRecord,
|
|
20
|
+
validateCancellation,
|
|
21
|
+
assertRecordedOperator,
|
|
22
|
+
verifyRecordedCancellation,
|
|
23
|
+
} from "./npm-update-cancellation-proof.mjs";
|
|
24
|
+
|
|
25
|
+
/** This pure topology function consumes already verified records; it grants no signature or provider authority. */
|
|
26
|
+
export function selectSupersession(issues, records, proposal) {
|
|
27
|
+
required(
|
|
28
|
+
issues.length > 0 && issues.length <= 100,
|
|
29
|
+
"supersession inventory exceeds bound"
|
|
30
|
+
);
|
|
31
|
+
const keys = new Map(issues.map(issue => [issue.key, issue]));
|
|
32
|
+
required(keys.size === issues.length, "duplicate proposal keys");
|
|
33
|
+
const active = issues.filter(issue => issue.state === "open");
|
|
34
|
+
required(
|
|
35
|
+
active.length <= 1 && records.size === issues.length - active.length,
|
|
36
|
+
"unverified closure or multiple active proposals"
|
|
37
|
+
);
|
|
38
|
+
const incoming = new Set();
|
|
39
|
+
for (const issue of issues.filter(item => item.state !== "open")) {
|
|
40
|
+
const record = records.get(issue.number);
|
|
41
|
+
required(
|
|
42
|
+
issue.state === "closed" &&
|
|
43
|
+
issue.state_reason === "not_planned" &&
|
|
44
|
+
record?.oldProposalKey === issue.key &&
|
|
45
|
+
record.proposalKey !== issue.key &&
|
|
46
|
+
!incoming.has(record.proposalKey),
|
|
47
|
+
"invalid or forked cancellation chain"
|
|
48
|
+
);
|
|
49
|
+
incoming.add(record.proposalKey);
|
|
50
|
+
}
|
|
51
|
+
const roots = issues.filter(issue => !incoming.has(issue.key));
|
|
52
|
+
required(roots.length === 1, "disconnected or cyclic cancellation chain");
|
|
53
|
+
let cursor = roots[0],
|
|
54
|
+
count = 0,
|
|
55
|
+
last;
|
|
56
|
+
while (cursor.state === "closed") {
|
|
57
|
+
required(++count <= issues.length, "cyclic cancellation chain");
|
|
58
|
+
last = records.get(cursor.number);
|
|
59
|
+
cursor = keys.get(last.proposalKey);
|
|
60
|
+
if (!cursor) break;
|
|
61
|
+
}
|
|
62
|
+
required(
|
|
63
|
+
count === records.size && (!active.length || cursor === active[0]),
|
|
64
|
+
"disconnected cancellation chain"
|
|
65
|
+
);
|
|
66
|
+
if (cursor)
|
|
67
|
+
return {
|
|
68
|
+
status:
|
|
69
|
+
cursor.parent === proposal.parent ? "existing" : "stale-outstanding",
|
|
70
|
+
issue: cursor.issue ?? cursor,
|
|
71
|
+
};
|
|
72
|
+
required(
|
|
73
|
+
last?.proposalKey === proposal.key &&
|
|
74
|
+
last.parent === proposal.parent &&
|
|
75
|
+
canonicalJson(last.proposalHashes) === canonicalJson(proposal.hashes),
|
|
76
|
+
"cancelled replacement no longer matches; preserve the chain"
|
|
77
|
+
);
|
|
78
|
+
return { status: "new", supersession: last };
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** Both branch and every historical PR must actually be absent before cancellation or reuse. */
|
|
82
|
+
export async function cancellationDestinationAbsent(api, proposalKey) {
|
|
83
|
+
required(
|
|
84
|
+
typeof proposalKey === "string" && /^[a-f0-9]{64}$/.test(proposalKey),
|
|
85
|
+
"invalid cancellation destination"
|
|
86
|
+
);
|
|
87
|
+
const branch = `lisa/npm-${proposalKey}`;
|
|
88
|
+
required(
|
|
89
|
+
(await api.maybe(
|
|
90
|
+
`repos/${api.policy.repository}/git/ref/heads/${branch}`
|
|
91
|
+
)) === null,
|
|
92
|
+
"published branch prevents cancellation"
|
|
93
|
+
);
|
|
94
|
+
const head = encodeURIComponent(
|
|
95
|
+
`${api.policy.repository.split("/")[0]}:${branch}`
|
|
96
|
+
);
|
|
97
|
+
const pulls = await api.list(
|
|
98
|
+
`repos/${api.policy.repository}/pulls?state=all&head=${head}`
|
|
99
|
+
);
|
|
100
|
+
required(pulls.length === 0, "published PR prevents cancellation");
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** Checkpoint discovery remains transport-only and refuses unknown complete roles. */
|
|
104
|
+
function cancellationCheckpoint(issue) {
|
|
105
|
+
const checkpoints = completeCheckpoints(issue.comments);
|
|
106
|
+
for (const { payload } of checkpoints)
|
|
107
|
+
required(
|
|
108
|
+
payload.preview === null ||
|
|
109
|
+
payload.preview?.descriptor ||
|
|
110
|
+
payload.preview?.cancellation,
|
|
111
|
+
"unknown completed checkpoint role"
|
|
112
|
+
);
|
|
113
|
+
const matches = checkpoints.filter(
|
|
114
|
+
({ payload }) => payload.preview?.cancellation
|
|
115
|
+
);
|
|
116
|
+
required(matches.length <= 1, "ambiguous completed cancellation records");
|
|
117
|
+
return matches[0];
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** Provider operator and official signature independently authenticate the complete historical subject. */
|
|
121
|
+
async function verifyRecordedBundle(api, authority, checkpoint, record, issue) {
|
|
122
|
+
const run = await api.request(
|
|
123
|
+
`repos/${record.repository}/actions/runs/${record.runId}/attempts/${record.runAttempt}`
|
|
124
|
+
);
|
|
125
|
+
const permission = await api.request(
|
|
126
|
+
`repos/${record.repository}/collaborators/${record.maintainer}/permission`
|
|
127
|
+
);
|
|
128
|
+
assertRecordedOperator(run, permission, authority, record);
|
|
129
|
+
assertOriginTransport(issue.comments, checkpoint.digest, authority, run);
|
|
130
|
+
await withPrivateRoot(async root => {
|
|
131
|
+
const paths = {
|
|
132
|
+
cancellation: join(root, "cancellation.json"),
|
|
133
|
+
cancellationBundle: join(root, "bundle.json"),
|
|
134
|
+
};
|
|
135
|
+
writeJson(paths.cancellation, record);
|
|
136
|
+
required(
|
|
137
|
+
Buffer.byteLength(checkpoint.payload.bundle) <= 1_048_576,
|
|
138
|
+
"cancellation bundle exceeds bound"
|
|
139
|
+
);
|
|
140
|
+
writeFileSync(paths.cancellationBundle, checkpoint.payload.bundle, {
|
|
141
|
+
mode: 0o600,
|
|
142
|
+
flag: "wx",
|
|
143
|
+
});
|
|
144
|
+
verifyRecordedCancellation(
|
|
145
|
+
authority,
|
|
146
|
+
record,
|
|
147
|
+
paths,
|
|
148
|
+
checkpoint.digest,
|
|
149
|
+
run
|
|
150
|
+
);
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/** A completion marker is only transport; original authority and a third fixed proof are independently required. */
|
|
155
|
+
export async function recordedCancellation({
|
|
156
|
+
api,
|
|
157
|
+
cwd,
|
|
158
|
+
issue,
|
|
159
|
+
proposal,
|
|
160
|
+
config,
|
|
161
|
+
origin,
|
|
162
|
+
}) {
|
|
163
|
+
const checkpoint = cancellationCheckpoint(issue);
|
|
164
|
+
if (!checkpoint) return undefined;
|
|
165
|
+
keys(checkpoint.payload.preview, ["cancellation"]);
|
|
166
|
+
const record = checkpoint.payload.preview.cancellation;
|
|
167
|
+
validateCancellation(record, record);
|
|
168
|
+
const historical = await committedCancellationConfig(cwd, record.parent);
|
|
169
|
+
const authority = historical.automationProvenance;
|
|
170
|
+
required(
|
|
171
|
+
[
|
|
172
|
+
"repository",
|
|
173
|
+
"repositoryId",
|
|
174
|
+
"ownerId",
|
|
175
|
+
"claimActorId",
|
|
176
|
+
"ghExecutable",
|
|
177
|
+
"ghSha256",
|
|
178
|
+
].every(key => authority[key] === api.policy[key]) &&
|
|
179
|
+
historical.npmUpdater.maintainer === config.npmUpdater.maintainer,
|
|
180
|
+
"recorded cancellation authority differs"
|
|
181
|
+
);
|
|
182
|
+
const replacement = validateProposal(
|
|
183
|
+
checkpoint.payload.proposal,
|
|
184
|
+
historical.npmUpdater
|
|
185
|
+
);
|
|
186
|
+
required(
|
|
187
|
+
replacement.selectionKey === proposal.selectionKey,
|
|
188
|
+
"recorded cancellation selection differs"
|
|
189
|
+
);
|
|
190
|
+
const original =
|
|
191
|
+
origin ??
|
|
192
|
+
(await authenticateStaleOrigin({ api, cwd, issue, proposal, config }));
|
|
193
|
+
required(
|
|
194
|
+
canonicalJson(checkpoint.payload.allocation) ===
|
|
195
|
+
canonicalJson(original.allocation),
|
|
196
|
+
"recorded cancellation allocation differs"
|
|
197
|
+
);
|
|
198
|
+
const expected = cancellationRecord(
|
|
199
|
+
original,
|
|
200
|
+
replacement,
|
|
201
|
+
historical,
|
|
202
|
+
record,
|
|
203
|
+
record.operatorId
|
|
204
|
+
);
|
|
205
|
+
validateCancellation(record, expected);
|
|
206
|
+
required(
|
|
207
|
+
checkpoint.digest === sha256(`${canonicalJson(record)}\n`),
|
|
208
|
+
"cancellation subject digest differs"
|
|
209
|
+
);
|
|
210
|
+
await verifyRecordedBundle(api, authority, checkpoint, record, issue);
|
|
211
|
+
await api.main(proposal.parent);
|
|
212
|
+
await cancellationDestinationAbsent(api, record.oldProposalKey);
|
|
213
|
+
if (issue.state === "closed")
|
|
214
|
+
required(
|
|
215
|
+
issue.state_reason === "not_planned" &&
|
|
216
|
+
issue.closed_by?.type === "Bot" &&
|
|
217
|
+
String(issue.closed_by.id) === api.policy.claimActorId,
|
|
218
|
+
"closure is not the authenticated managed cancellation"
|
|
219
|
+
);
|
|
220
|
+
return checkpoint;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/** Every closed match must independently authenticate before a finite connected chain is accepted. */
|
|
224
|
+
export async function discoverSupersession(
|
|
225
|
+
api,
|
|
226
|
+
proposal,
|
|
227
|
+
found,
|
|
228
|
+
{ cwd, config }
|
|
229
|
+
) {
|
|
230
|
+
required(
|
|
231
|
+
found.length > 0 && found.length <= 100,
|
|
232
|
+
"supersession search exceeds bound"
|
|
233
|
+
);
|
|
234
|
+
const issues = [],
|
|
235
|
+
records = new Map();
|
|
236
|
+
for (const item of found) {
|
|
237
|
+
const issue = await api.issue(item.number);
|
|
238
|
+
required(
|
|
239
|
+
!issue.pull_request && typeof issue.body === "string",
|
|
240
|
+
"foreign supersession issue"
|
|
241
|
+
);
|
|
242
|
+
const one = (name, pattern) => {
|
|
243
|
+
const values = [
|
|
244
|
+
...issue.body.matchAll(new RegExp(`^${name}: (${pattern})$`, "gm")),
|
|
245
|
+
];
|
|
246
|
+
required(values.length === 1, "ambiguous supersession identity");
|
|
247
|
+
return values[0][1];
|
|
248
|
+
};
|
|
249
|
+
required(
|
|
250
|
+
one("Selection key", "[a-f0-9]{64}") === proposal.selectionKey,
|
|
251
|
+
"supersession selection differs"
|
|
252
|
+
);
|
|
253
|
+
issues.push({
|
|
254
|
+
number: issue.number,
|
|
255
|
+
state: issue.state,
|
|
256
|
+
state_reason: issue.state_reason,
|
|
257
|
+
key: one("Proposal key", "[a-f0-9]{64}"),
|
|
258
|
+
parent: one("Base parent", "[a-f0-9]{40}"),
|
|
259
|
+
issue,
|
|
260
|
+
});
|
|
261
|
+
if (issue.state !== "open") {
|
|
262
|
+
const checkpoint = await recordedCancellation({
|
|
263
|
+
api,
|
|
264
|
+
cwd,
|
|
265
|
+
issue,
|
|
266
|
+
proposal,
|
|
267
|
+
config,
|
|
268
|
+
});
|
|
269
|
+
required(checkpoint, "closed leaf lacks authenticated cancellation");
|
|
270
|
+
records.set(issue.number, checkpoint.payload.preview.cancellation);
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
return selectSupersession(issues, records, proposal);
|
|
274
|
+
}
|