@codyswann/lisa 4.71.2 → 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/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-helper-inventory.mjs +4 -0
- package/all/copy-overwrite/scripts/lib/npm-update-invariants.mjs +4 -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-supersession.mjs +274 -0
- package/all/copy-overwrite/scripts/lisa-npm-updater.mjs +61 -5
- package/all/copy-overwrite/scripts/npm-updater-helper-graph.json +74 -8
- package/dist/core/lisa-owned-hash-ledger.d.ts.map +1 -1
- package/dist/core/lisa-owned-hash-ledger.js +58 -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 +20 -9
- 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/scripts/two-channel-couplings.json +2 -2
|
@@ -0,0 +1,248 @@
|
|
|
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
|
+
/** Explicit hosted manual cancellation preserves the old leaf and issues no publication authority. */
|
|
5
|
+
import { join } from "node:path";
|
|
6
|
+
import { writeFileSync } from "node:fs";
|
|
7
|
+
import { canonicalJson } from "../lisa-automation-provenance.mjs";
|
|
8
|
+
import { required, keys } from "./npm-update-contract.mjs";
|
|
9
|
+
import { readBytes, readJson, writeJson } from "./npm-update-process.mjs";
|
|
10
|
+
import { assertHostedGate } from "./npm-update-hosted-gate.mjs";
|
|
11
|
+
import { sha256 } from "./github-attestation-verifier.mjs";
|
|
12
|
+
import { discoverPrior } from "./npm-update-allocate.mjs";
|
|
13
|
+
import { authenticateStaleOrigin } from "./npm-update-cancel-origin.mjs";
|
|
14
|
+
import {
|
|
15
|
+
recordedCancellation,
|
|
16
|
+
cancellationDestinationAbsent,
|
|
17
|
+
} from "./npm-update-supersession.mjs";
|
|
18
|
+
import {
|
|
19
|
+
assertManualCancellation,
|
|
20
|
+
cancellationRecord,
|
|
21
|
+
validateCancellation,
|
|
22
|
+
verifyCancellationAttestation,
|
|
23
|
+
} from "./npm-update-cancellation-proof.mjs";
|
|
24
|
+
import {
|
|
25
|
+
persistCheckpoint,
|
|
26
|
+
decodeCheckpoint,
|
|
27
|
+
} from "./npm-update-checkpoint.mjs";
|
|
28
|
+
import { withStage } from "./npm-update-invariants.mjs";
|
|
29
|
+
|
|
30
|
+
/** Native dispatch bytes are checked independently of reusable workflow input forwarding. */
|
|
31
|
+
export function assertDispatchCancellation(event, run, request, authority) {
|
|
32
|
+
required(
|
|
33
|
+
event &&
|
|
34
|
+
["main", "refs/heads/main"].includes(event.ref) &&
|
|
35
|
+
event.repository?.full_name === authority.repository &&
|
|
36
|
+
String(event.repository.id) === authority.repositoryId &&
|
|
37
|
+
String(event.repository.owner?.id) === authority.ownerId &&
|
|
38
|
+
event.sender?.type === "User" &&
|
|
39
|
+
event.sender.id === run.triggering_actor?.id &&
|
|
40
|
+
event.sender.login === run.triggering_actor.login &&
|
|
41
|
+
event.inputs?.cancel_issue === String(request.number) &&
|
|
42
|
+
event.inputs.cancel_proposal_key === request.proposalKey &&
|
|
43
|
+
event.inputs.cancel_expected_main === request.parent,
|
|
44
|
+
"native manual dispatch intent differs"
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** The actual run and repository permission are read through the existing pinned default-token transport. */
|
|
49
|
+
async function manualAuthority(api, proposal, config, request) {
|
|
50
|
+
assertHostedGate({ policy: config.npmUpdater, proposal, config });
|
|
51
|
+
const invocation = {
|
|
52
|
+
runId: process.env.GITHUB_RUN_ID,
|
|
53
|
+
runAttempt: process.env.GITHUB_RUN_ATTEMPT,
|
|
54
|
+
};
|
|
55
|
+
required(
|
|
56
|
+
Object.values(invocation).every(
|
|
57
|
+
value => typeof value === "string" && /^[1-9]\d*$/.test(value)
|
|
58
|
+
),
|
|
59
|
+
"native cancellation invocation is absent"
|
|
60
|
+
);
|
|
61
|
+
await api.main(proposal.parent);
|
|
62
|
+
const run = await api.request(
|
|
63
|
+
`repos/${proposal.repository}/actions/runs/${invocation.runId}/attempts/${invocation.runAttempt}`
|
|
64
|
+
);
|
|
65
|
+
const permission = await api.request(
|
|
66
|
+
`repos/${proposal.repository}/collaborators/${config.npmUpdater.maintainer}/permission`
|
|
67
|
+
);
|
|
68
|
+
const operatorId = assertManualCancellation(
|
|
69
|
+
run,
|
|
70
|
+
permission,
|
|
71
|
+
api.policy,
|
|
72
|
+
config.npmUpdater.maintainer,
|
|
73
|
+
proposal,
|
|
74
|
+
request,
|
|
75
|
+
invocation
|
|
76
|
+
);
|
|
77
|
+
const event = JSON.parse(
|
|
78
|
+
new TextDecoder("utf8", { fatal: true }).decode(
|
|
79
|
+
readBytes(process.env.GITHUB_EVENT_PATH, 1_048_576, false)
|
|
80
|
+
)
|
|
81
|
+
);
|
|
82
|
+
assertDispatchCancellation(event, run, request, api.policy);
|
|
83
|
+
return { invocation, operatorId };
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** Named stale origins are never substituted by a closed implementation ticket or another active leaf. */
|
|
87
|
+
async function cancellationState(context) {
|
|
88
|
+
const { api, cwd, proposal, config, request } = context;
|
|
89
|
+
const manual = await withStage("cancel-intent", () =>
|
|
90
|
+
manualAuthority(api, proposal, config, request)
|
|
91
|
+
);
|
|
92
|
+
const prior = await discoverPrior(api, proposal, { cwd, config });
|
|
93
|
+
const issue = await api.issue(request.number);
|
|
94
|
+
const origin = await withStage("cancel-origin", () =>
|
|
95
|
+
authenticateStaleOrigin({ api, cwd, issue, proposal, config })
|
|
96
|
+
);
|
|
97
|
+
required(
|
|
98
|
+
origin.proposal.key === request.proposalKey,
|
|
99
|
+
"named original proposal differs"
|
|
100
|
+
);
|
|
101
|
+
await cancellationDestinationAbsent(api, request.proposalKey);
|
|
102
|
+
const recorded = await recordedCancellation({
|
|
103
|
+
api,
|
|
104
|
+
cwd,
|
|
105
|
+
issue,
|
|
106
|
+
proposal,
|
|
107
|
+
config,
|
|
108
|
+
origin,
|
|
109
|
+
});
|
|
110
|
+
if (recorded) {
|
|
111
|
+
const record = recorded.payload.preview.cancellation;
|
|
112
|
+
required(
|
|
113
|
+
record.parent === proposal.parent &&
|
|
114
|
+
record.proposalKey === proposal.key &&
|
|
115
|
+
canonicalJson(record.proposalHashes) === canonicalJson(proposal.hashes),
|
|
116
|
+
"recorded replacement no longer matches"
|
|
117
|
+
);
|
|
118
|
+
} else
|
|
119
|
+
required(
|
|
120
|
+
issue.state === "open" &&
|
|
121
|
+
prior.status === "stale-outstanding" &&
|
|
122
|
+
prior.issue.number === request.number,
|
|
123
|
+
"only the active stale origin can be cancelled"
|
|
124
|
+
);
|
|
125
|
+
return { ...manual, issue, origin, recorded };
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/** Preparation writes only a private prospective subject, never a provider lifecycle mutation. */
|
|
129
|
+
export async function prepareCancellation(context) {
|
|
130
|
+
const { output, proposal, config, request } = context;
|
|
131
|
+
const state = await cancellationState(context);
|
|
132
|
+
const record =
|
|
133
|
+
state.recorded?.payload.preview.cancellation ??
|
|
134
|
+
cancellationRecord(
|
|
135
|
+
state.origin,
|
|
136
|
+
proposal,
|
|
137
|
+
config,
|
|
138
|
+
state.invocation,
|
|
139
|
+
state.operatorId
|
|
140
|
+
);
|
|
141
|
+
validateCancellation(record, record);
|
|
142
|
+
writeJson(join(output, "cancellation.json"), record);
|
|
143
|
+
const mode = state.recorded ? "recorded" : "fresh";
|
|
144
|
+
if (state.recorded) {
|
|
145
|
+
const bytes = Buffer.from(state.recorded.payload.bundle);
|
|
146
|
+
required(bytes.length <= 1_048_576, "cancellation bundle exceeds bound");
|
|
147
|
+
writeFileSync(join(output, "cancellation-bundle.json"), bytes, {
|
|
148
|
+
mode: 0o600,
|
|
149
|
+
flag: "wx",
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
writeJson(join(output, "cancellation-state.json"), { mode, request });
|
|
153
|
+
return { mode };
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/** Each chunk and closure rechecks current operator, main, holds, destination and immutable subject. */
|
|
157
|
+
async function authorizeCancellation(context, record, paths, state) {
|
|
158
|
+
const current = await cancellationState(context);
|
|
159
|
+
if (state.mode === "fresh") {
|
|
160
|
+
validateCancellation(
|
|
161
|
+
record,
|
|
162
|
+
cancellationRecord(
|
|
163
|
+
current.origin,
|
|
164
|
+
context.proposal,
|
|
165
|
+
context.config,
|
|
166
|
+
current.invocation,
|
|
167
|
+
current.operatorId
|
|
168
|
+
)
|
|
169
|
+
);
|
|
170
|
+
verifyCancellationAttestation(
|
|
171
|
+
context.api.policy,
|
|
172
|
+
record,
|
|
173
|
+
paths,
|
|
174
|
+
sha256(readBytes(paths.cancellation, 65_536))
|
|
175
|
+
);
|
|
176
|
+
} else
|
|
177
|
+
required(
|
|
178
|
+
state.mode === "recorded" &&
|
|
179
|
+
current.recorded &&
|
|
180
|
+
canonicalJson(current.recorded.payload.preview.cancellation) ===
|
|
181
|
+
canonicalJson(record),
|
|
182
|
+
"recorded cancellation authority differs"
|
|
183
|
+
);
|
|
184
|
+
return current;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/** Durable proof precedes a not_planned close; interruptions retain all evidence for the next manual run. */
|
|
188
|
+
export async function finalizeCancellation(context) {
|
|
189
|
+
const { api, output, proposal, request } = context;
|
|
190
|
+
const state = readJson(join(output, "cancellation-state.json"), 65_536);
|
|
191
|
+
keys(state, ["mode", "request"]);
|
|
192
|
+
required(
|
|
193
|
+
canonicalJson(state.request) === canonicalJson(request),
|
|
194
|
+
"cancellation intent changed"
|
|
195
|
+
);
|
|
196
|
+
const paths = {
|
|
197
|
+
cancellation: join(output, "cancellation.json"),
|
|
198
|
+
cancellationBundle: join(output, "cancellation-bundle.json"),
|
|
199
|
+
};
|
|
200
|
+
const record = readJson(paths.cancellation, 65_536);
|
|
201
|
+
const digest = sha256(readBytes(paths.cancellation, 65_536));
|
|
202
|
+
const current = await authorizeCancellation(context, record, paths, state);
|
|
203
|
+
const payload = current.recorded?.payload ?? {
|
|
204
|
+
version: 1,
|
|
205
|
+
proposal,
|
|
206
|
+
allocation: current.origin.allocation,
|
|
207
|
+
preview: { cancellation: record },
|
|
208
|
+
bundle: readBytes(paths.cancellationBundle, 1_048_576).toString("utf8"),
|
|
209
|
+
};
|
|
210
|
+
if (!current.recorded)
|
|
211
|
+
await withStage("cancel-checkpoint", () =>
|
|
212
|
+
persistCheckpoint(api, request.number, payload, digest, () =>
|
|
213
|
+
authorizeCancellation(context, record, paths, state)
|
|
214
|
+
)
|
|
215
|
+
);
|
|
216
|
+
const issue = (await authorizeCancellation(context, record, paths, state))
|
|
217
|
+
.issue;
|
|
218
|
+
required(
|
|
219
|
+
canonicalJson(decodeCheckpoint(issue.comments, digest)) ===
|
|
220
|
+
canonicalJson(payload),
|
|
221
|
+
"durable cancellation checkpoint differs"
|
|
222
|
+
);
|
|
223
|
+
if (issue.state === "open")
|
|
224
|
+
await withStage("cancel-close", () =>
|
|
225
|
+
api.request(
|
|
226
|
+
`repos/${proposal.repository}/issues/${request.number}`,
|
|
227
|
+
"PATCH",
|
|
228
|
+
{ state: "closed", state_reason: "not_planned" }
|
|
229
|
+
)
|
|
230
|
+
);
|
|
231
|
+
const closed = await api.issue(request.number);
|
|
232
|
+
required(
|
|
233
|
+
closed.state === "closed" &&
|
|
234
|
+
closed.state_reason === "not_planned" &&
|
|
235
|
+
closed.closed_by?.type === "Bot" &&
|
|
236
|
+
String(closed.closed_by.id) === api.policy.claimActorId &&
|
|
237
|
+
closed.body === issue.body &&
|
|
238
|
+
closed.title === issue.title,
|
|
239
|
+
"cancellation closure readback differs"
|
|
240
|
+
);
|
|
241
|
+
await api.main(proposal.parent);
|
|
242
|
+
await cancellationDestinationAbsent(api, request.proposalKey);
|
|
243
|
+
return {
|
|
244
|
+
status: "cancelled",
|
|
245
|
+
number: request.number,
|
|
246
|
+
cancellationSha256: digest,
|
|
247
|
+
};
|
|
248
|
+
}
|
|
@@ -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",
|
|
@@ -4,6 +4,10 @@
|
|
|
4
4
|
/** Pure shared diagnostics retain one error identity without importing producer orchestration. */
|
|
5
5
|
const stages = new Set([
|
|
6
6
|
"configuration",
|
|
7
|
+
"cancel-intent",
|
|
8
|
+
"cancel-origin",
|
|
9
|
+
"cancel-checkpoint",
|
|
10
|
+
"cancel-close",
|
|
7
11
|
"gate-input",
|
|
8
12
|
"gate-validate",
|
|
9
13
|
"gate-scratch",
|
|
@@ -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"
|
|
@@ -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
|
+
}
|