@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
|
@@ -0,0 +1,327 @@
|
|
|
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
|
+
/** A manual cancellation is a third fixed signing purpose, never publication or review authority. */
|
|
5
|
+
import { canonicalJson } from "../lisa-automation-provenance.mjs";
|
|
6
|
+
import { required, keys, FILES, OBJECT } from "./npm-update-contract.mjs";
|
|
7
|
+
import {
|
|
8
|
+
verifiedRole,
|
|
9
|
+
matchSigningTime,
|
|
10
|
+
officialResult,
|
|
11
|
+
sha256,
|
|
12
|
+
} from "./github-attestation-verifier.mjs";
|
|
13
|
+
|
|
14
|
+
export const CANCELLATION_PREDICATE =
|
|
15
|
+
"https://lisa.dev/attestations/npm-cancellation/v1";
|
|
16
|
+
const DIGEST = /^[a-f0-9]{64}$/;
|
|
17
|
+
const DECIMAL = /^[1-9]\d*$/;
|
|
18
|
+
const RECORD_KEYS = [
|
|
19
|
+
"version",
|
|
20
|
+
"purpose",
|
|
21
|
+
"repository",
|
|
22
|
+
"repositoryId",
|
|
23
|
+
"ownerId",
|
|
24
|
+
"selectionKey",
|
|
25
|
+
"workItem",
|
|
26
|
+
"oldProposalKey",
|
|
27
|
+
"oldParent",
|
|
28
|
+
"parent",
|
|
29
|
+
"proposalKey",
|
|
30
|
+
"proposalHashes",
|
|
31
|
+
"originDescriptorSha256",
|
|
32
|
+
"originCheckpointSha256",
|
|
33
|
+
"claimCommentId",
|
|
34
|
+
"claimSha256",
|
|
35
|
+
"leafBodySha256",
|
|
36
|
+
"policySha256",
|
|
37
|
+
"npmPolicySha256",
|
|
38
|
+
"maintainer",
|
|
39
|
+
"operatorId",
|
|
40
|
+
"runId",
|
|
41
|
+
"runAttempt",
|
|
42
|
+
"destination",
|
|
43
|
+
];
|
|
44
|
+
|
|
45
|
+
/** Manual intent comes from the provider's actual run and collaborator, not a comment or environment assertion. */
|
|
46
|
+
export function assertManualCancellation(
|
|
47
|
+
run,
|
|
48
|
+
permission,
|
|
49
|
+
policy,
|
|
50
|
+
maintainer,
|
|
51
|
+
proposal,
|
|
52
|
+
request,
|
|
53
|
+
invocation
|
|
54
|
+
) {
|
|
55
|
+
keys(request, ["number", "proposalKey", "parent"]);
|
|
56
|
+
keys(invocation, ["runId", "runAttempt"]);
|
|
57
|
+
required(
|
|
58
|
+
Number.isSafeInteger(request.number) &&
|
|
59
|
+
request.number > 0 &&
|
|
60
|
+
typeof request.proposalKey === "string" &&
|
|
61
|
+
DIGEST.test(request.proposalKey) &&
|
|
62
|
+
request.parent === proposal.parent &&
|
|
63
|
+
OBJECT.test(request.parent),
|
|
64
|
+
"invalid exact cancellation intent"
|
|
65
|
+
);
|
|
66
|
+
required(
|
|
67
|
+
[invocation.runId, invocation.runAttempt].every(
|
|
68
|
+
value => typeof value === "string" && DECIMAL.test(value)
|
|
69
|
+
) &&
|
|
70
|
+
String(run.id) === invocation.runId &&
|
|
71
|
+
String(run.run_attempt) === invocation.runAttempt &&
|
|
72
|
+
run.event === "workflow_dispatch" &&
|
|
73
|
+
policy.allowedTriggers?.includes("workflow_dispatch") &&
|
|
74
|
+
run.status === "in_progress" &&
|
|
75
|
+
run.head_sha === proposal.parent &&
|
|
76
|
+
run.head_branch === "main" &&
|
|
77
|
+
run.path === policy.callerWorkflow.slice(policy.repository.length + 1) &&
|
|
78
|
+
String(run.head_repository?.id) === policy.repositoryId &&
|
|
79
|
+
Array.isArray(run.referenced_workflows) &&
|
|
80
|
+
run.referenced_workflows.some(
|
|
81
|
+
workflow =>
|
|
82
|
+
workflow.path?.split("@")[0] === policy.signerWorkflow &&
|
|
83
|
+
workflow.sha === policy.signerDigest
|
|
84
|
+
),
|
|
85
|
+
"cancellation requires the actual manual main invocation"
|
|
86
|
+
);
|
|
87
|
+
const actor = run.triggering_actor;
|
|
88
|
+
required(
|
|
89
|
+
actor?.type === "User" &&
|
|
90
|
+
actor.login === maintainer &&
|
|
91
|
+
[actor.id, permission.user?.id].every(id =>
|
|
92
|
+
typeof id === "string"
|
|
93
|
+
? DECIMAL.test(id)
|
|
94
|
+
: Number.isSafeInteger(id) && id > 0
|
|
95
|
+
) &&
|
|
96
|
+
permission.user?.type === "User" &&
|
|
97
|
+
permission.user.login === maintainer &&
|
|
98
|
+
String(permission.user.id) === String(actor.id) &&
|
|
99
|
+
["write", "admin"].includes(permission.permission),
|
|
100
|
+
"cancellation requires the configured operator's current repository authority"
|
|
101
|
+
);
|
|
102
|
+
return String(actor.id);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** The closed subject preserves one stale origin and one exact replacement, with no published destination. */
|
|
106
|
+
export function validateCancellation(value, expected) {
|
|
107
|
+
keys(value, RECORD_KEYS);
|
|
108
|
+
keys(value.proposalHashes, FILES);
|
|
109
|
+
keys(value.destination, ["branch", "expectedBranchHead", "prNumber"]);
|
|
110
|
+
required(
|
|
111
|
+
value.version === 1 &&
|
|
112
|
+
value.purpose === "cancel-stale-prepublication-npm-proposal" &&
|
|
113
|
+
typeof value.repository === "string" &&
|
|
114
|
+
/^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/.test(value.repository) &&
|
|
115
|
+
typeof value.maintainer === "string" &&
|
|
116
|
+
/^[A-Za-z0-9][A-Za-z0-9-]{0,38}$/.test(value.maintainer) &&
|
|
117
|
+
typeof value.oldParent === "string" &&
|
|
118
|
+
typeof value.parent === "string" &&
|
|
119
|
+
OBJECT.test(value.oldParent) &&
|
|
120
|
+
OBJECT.test(value.parent) &&
|
|
121
|
+
value.oldParent !== value.parent &&
|
|
122
|
+
value.oldProposalKey !== value.proposalKey &&
|
|
123
|
+
typeof value.workItem === "string" &&
|
|
124
|
+
value.workItem.startsWith(`${value.repository}#`) &&
|
|
125
|
+
DECIMAL.test(value.workItem.slice(value.repository.length + 1)),
|
|
126
|
+
"invalid stale cancellation scope"
|
|
127
|
+
);
|
|
128
|
+
required(
|
|
129
|
+
[
|
|
130
|
+
"repositoryId",
|
|
131
|
+
"ownerId",
|
|
132
|
+
"claimCommentId",
|
|
133
|
+
"operatorId",
|
|
134
|
+
"runId",
|
|
135
|
+
"runAttempt",
|
|
136
|
+
].every(
|
|
137
|
+
key => typeof value[key] === "string" && DECIMAL.test(value[key])
|
|
138
|
+
) &&
|
|
139
|
+
[
|
|
140
|
+
"selectionKey",
|
|
141
|
+
"oldProposalKey",
|
|
142
|
+
"proposalKey",
|
|
143
|
+
"originDescriptorSha256",
|
|
144
|
+
"originCheckpointSha256",
|
|
145
|
+
"claimSha256",
|
|
146
|
+
"leafBodySha256",
|
|
147
|
+
"policySha256",
|
|
148
|
+
"npmPolicySha256",
|
|
149
|
+
].every(
|
|
150
|
+
key => typeof value[key] === "string" && DIGEST.test(value[key])
|
|
151
|
+
) &&
|
|
152
|
+
Object.values(value.proposalHashes).every(
|
|
153
|
+
hash => typeof hash === "string" && DIGEST.test(hash)
|
|
154
|
+
),
|
|
155
|
+
"invalid cancellation identity or digest"
|
|
156
|
+
);
|
|
157
|
+
required(
|
|
158
|
+
value.destination.branch === `lisa/npm-${value.oldProposalKey}` &&
|
|
159
|
+
value.destination.expectedBranchHead === null &&
|
|
160
|
+
value.destination.prNumber === null &&
|
|
161
|
+
canonicalJson(value) === canonicalJson(expected),
|
|
162
|
+
"cancellation subject or prepublication destination differs"
|
|
163
|
+
);
|
|
164
|
+
return value;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/** A durable cancellation still proves its own actual manual operator; completion is not fresh publication authority. */
|
|
168
|
+
export function assertRecordedOperator(run, permission, policy, record) {
|
|
169
|
+
required(
|
|
170
|
+
["in_progress", "completed"].includes(run.status) &&
|
|
171
|
+
String(run.id) === record.runId &&
|
|
172
|
+
String(run.run_attempt) === record.runAttempt &&
|
|
173
|
+
run.event === "workflow_dispatch" &&
|
|
174
|
+
policy.allowedTriggers?.includes("workflow_dispatch") &&
|
|
175
|
+
run.head_sha === record.parent &&
|
|
176
|
+
run.head_branch === "main" &&
|
|
177
|
+
run.path === policy.callerWorkflow.slice(policy.repository.length + 1) &&
|
|
178
|
+
String(run.head_repository?.id) === policy.repositoryId &&
|
|
179
|
+
run.triggering_actor?.type === "User" &&
|
|
180
|
+
run.triggering_actor.login === record.maintainer &&
|
|
181
|
+
String(run.triggering_actor.id) === record.operatorId &&
|
|
182
|
+
permission.user?.type === "User" &&
|
|
183
|
+
permission.user.login === record.maintainer &&
|
|
184
|
+
String(permission.user.id) === record.operatorId &&
|
|
185
|
+
["write", "admin"].includes(permission.permission) &&
|
|
186
|
+
Array.isArray(run.referenced_workflows) &&
|
|
187
|
+
run.referenced_workflows.some(
|
|
188
|
+
workflow =>
|
|
189
|
+
workflow.path?.split("@")[0] === policy.signerWorkflow &&
|
|
190
|
+
workflow.sha === policy.signerDigest
|
|
191
|
+
),
|
|
192
|
+
"recorded cancellation operator or invocation differs"
|
|
193
|
+
);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/** The same fixed constructor is used before issuance and after independent provider verification. */
|
|
197
|
+
export function cancellationRecord(
|
|
198
|
+
origin,
|
|
199
|
+
proposal,
|
|
200
|
+
config,
|
|
201
|
+
invocation,
|
|
202
|
+
operatorId
|
|
203
|
+
) {
|
|
204
|
+
const authority = config.automationProvenance;
|
|
205
|
+
return {
|
|
206
|
+
version: 1,
|
|
207
|
+
purpose: "cancel-stale-prepublication-npm-proposal",
|
|
208
|
+
repository: proposal.repository,
|
|
209
|
+
repositoryId: authority.repositoryId,
|
|
210
|
+
ownerId: authority.ownerId,
|
|
211
|
+
selectionKey: proposal.selectionKey,
|
|
212
|
+
workItem: origin.allocation.workItem,
|
|
213
|
+
oldProposalKey: origin.proposal.key,
|
|
214
|
+
oldParent: origin.proposal.parent,
|
|
215
|
+
parent: proposal.parent,
|
|
216
|
+
proposalKey: proposal.key,
|
|
217
|
+
proposalHashes: proposal.hashes,
|
|
218
|
+
originDescriptorSha256: origin.checkpoint.digest,
|
|
219
|
+
originCheckpointSha256: sha256(canonicalJson(origin.checkpoint.payload)),
|
|
220
|
+
claimCommentId: origin.allocation.claimCommentId,
|
|
221
|
+
claimSha256: origin.allocation.claimSha256,
|
|
222
|
+
leafBodySha256: sha256(origin.allocation.draft.body),
|
|
223
|
+
policySha256: sha256(canonicalJson(authority)),
|
|
224
|
+
npmPolicySha256: proposal.policySha256,
|
|
225
|
+
maintainer: config.npmUpdater.maintainer,
|
|
226
|
+
operatorId,
|
|
227
|
+
runId: invocation.runId,
|
|
228
|
+
runAttempt: invocation.runAttempt,
|
|
229
|
+
destination: {
|
|
230
|
+
branch: `lisa/npm-${origin.proposal.key}`,
|
|
231
|
+
expectedBranchHead: null,
|
|
232
|
+
prNumber: null,
|
|
233
|
+
},
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/** Fresh cancellation always uses its fixed subject, predicate and ordinary expiration. */
|
|
238
|
+
export function assertCancellationAttestation(
|
|
239
|
+
results,
|
|
240
|
+
record,
|
|
241
|
+
digest,
|
|
242
|
+
policy,
|
|
243
|
+
now = Date.now()
|
|
244
|
+
) {
|
|
245
|
+
const result = verifiedRole(
|
|
246
|
+
results,
|
|
247
|
+
record,
|
|
248
|
+
digest,
|
|
249
|
+
policy,
|
|
250
|
+
CANCELLATION_PREDICATE,
|
|
251
|
+
"cancellation.json"
|
|
252
|
+
);
|
|
253
|
+
matchSigningTime(result.verifiedTimestamps, policy, now);
|
|
254
|
+
return result.signature.certificate;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/** A recorded cancellation authenticates historical bytes within the actual provider run chronology. */
|
|
258
|
+
export function assertRecordedCancellation(
|
|
259
|
+
results,
|
|
260
|
+
record,
|
|
261
|
+
digest,
|
|
262
|
+
policy,
|
|
263
|
+
run,
|
|
264
|
+
now = Date.now()
|
|
265
|
+
) {
|
|
266
|
+
const result = verifiedRole(
|
|
267
|
+
results,
|
|
268
|
+
record,
|
|
269
|
+
digest,
|
|
270
|
+
policy,
|
|
271
|
+
CANCELLATION_PREDICATE,
|
|
272
|
+
"cancellation.json"
|
|
273
|
+
);
|
|
274
|
+
const started = Date.parse(run.run_started_at);
|
|
275
|
+
const updated = Date.parse(run.updated_at);
|
|
276
|
+
required(
|
|
277
|
+
[started, updated].every(Number.isFinite) &&
|
|
278
|
+
started <= updated &&
|
|
279
|
+
updated <= now + 60_000,
|
|
280
|
+
"invalid cancellation provider chronology"
|
|
281
|
+
);
|
|
282
|
+
matchSigningTime(
|
|
283
|
+
result.verifiedTimestamps,
|
|
284
|
+
{ ...policy, maxAgeSeconds: Number.MAX_SAFE_INTEGER },
|
|
285
|
+
now
|
|
286
|
+
);
|
|
287
|
+
for (const time of result.verifiedTimestamps) {
|
|
288
|
+
const signed = Date.parse(time.timestamp);
|
|
289
|
+
required(
|
|
290
|
+
signed >= started - 60_000 && signed <= updated + 60_000,
|
|
291
|
+
"cancellation signed outside provider chronology"
|
|
292
|
+
);
|
|
293
|
+
}
|
|
294
|
+
return result.signature.certificate;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/** Both fixed roles run the same official binary and cancellation-only subject/predicate. */
|
|
298
|
+
function cancellationResult(policy, record, paths) {
|
|
299
|
+
return officialResult(
|
|
300
|
+
policy,
|
|
301
|
+
record,
|
|
302
|
+
paths.cancellation,
|
|
303
|
+
paths.cancellationBundle,
|
|
304
|
+
CANCELLATION_PREDICATE
|
|
305
|
+
);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
/** Verification never substitutes an operator's ambient GH binary for the immutable Linux policy. */
|
|
309
|
+
export function verifyCancellationAttestation(policy, record, paths, digest) {
|
|
310
|
+
return assertCancellationAttestation(
|
|
311
|
+
cancellationResult(policy, record, paths),
|
|
312
|
+
record,
|
|
313
|
+
digest,
|
|
314
|
+
policy
|
|
315
|
+
);
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/** This fixed historical entry point grants no current publication authority. */
|
|
319
|
+
export function verifyRecordedCancellation(policy, record, paths, digest, run) {
|
|
320
|
+
return assertRecordedCancellation(
|
|
321
|
+
cancellationResult(policy, record, paths),
|
|
322
|
+
record,
|
|
323
|
+
digest,
|
|
324
|
+
policy,
|
|
325
|
+
run
|
|
326
|
+
);
|
|
327
|
+
}
|
|
@@ -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,7 @@ import {
|
|
|
42
42
|
ordinaryGateReceipt,
|
|
43
43
|
} from "./npm-update-publication.mjs";
|
|
44
44
|
import { executeCanonicalHelper } from "./npm-update-controller-factory.mjs";
|
|
45
|
+
import { withStage } from "./npm-update-invariants.mjs";
|
|
45
46
|
|
|
46
47
|
const BOT = "github-actions[bot]";
|
|
47
48
|
const MAIL = "41898282+github-actions[bot]@users.noreply.github.com";
|
|
@@ -143,7 +144,7 @@ export async function descriptorFor({
|
|
|
143
144
|
async function stageProposal(context, env) {
|
|
144
145
|
const { cwd, proposal } = context;
|
|
145
146
|
const command = args => git(cwd, env, args, undefined, context.deadline);
|
|
146
|
-
await baseline(cwd, env, proposal);
|
|
147
|
+
await withStage("gate-baseline", () => baseline(cwd, env, proposal));
|
|
147
148
|
const parent = (await command(["rev-parse", "HEAD"])).stdout
|
|
148
149
|
.toString()
|
|
149
150
|
.trim();
|
|
@@ -154,8 +155,12 @@ async function stageProposal(context, env) {
|
|
|
154
155
|
);
|
|
155
156
|
const branch = `lisa/npm-${proposal.key}`;
|
|
156
157
|
await command(["checkout", "-b", branch]);
|
|
157
|
-
await
|
|
158
|
-
|
|
158
|
+
await withStage("gate-link", () =>
|
|
159
|
+
executeCanonicalHelper(context, "stage-read", "link")
|
|
160
|
+
);
|
|
161
|
+
await withStage("gate-attach", () =>
|
|
162
|
+
executeCanonicalHelper(context, "stage-read", "attach-branch")
|
|
163
|
+
);
|
|
159
164
|
for (const file of FILES)
|
|
160
165
|
writeFileSync(join(cwd, file), proposal.files[file]);
|
|
161
166
|
await command(["add", "--", ...FILES]);
|
|
@@ -263,38 +268,50 @@ async function pushProposal(context, env, branch, object) {
|
|
|
263
268
|
export async function gateProposal(context) {
|
|
264
269
|
const scoped = { ...context, deadline: Date.now() + 1_800_000 };
|
|
265
270
|
const { proposal, policy, token, preview } = context;
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
return withPrivateRoot(async root => {
|
|
274
|
-
const env = {
|
|
275
|
-
...candidateEnvironment(root),
|
|
276
|
-
GIT_AUTHOR_NAME: BOT,
|
|
277
|
-
GIT_AUTHOR_EMAIL: MAIL,
|
|
278
|
-
GIT_COMMITTER_NAME: BOT,
|
|
279
|
-
GIT_COMMITTER_EMAIL: MAIL,
|
|
280
|
-
GIT_AUTHOR_DATE: `${preview.epoch} +0000`,
|
|
281
|
-
GIT_COMMITTER_DATE: `${preview.epoch} +0000`,
|
|
282
|
-
};
|
|
283
|
-
const branch = await stageProposal(scoped, env);
|
|
284
|
-
const message = await installGateProof(context, root, args =>
|
|
285
|
-
git(context.cwd, env, args, undefined, scoped.deadline)
|
|
271
|
+
await withStage("gate-validate", () => {
|
|
272
|
+
validateProposal(proposal, policy);
|
|
273
|
+
assertHostedGate(context);
|
|
274
|
+
validateGateProof(context);
|
|
275
|
+
required(
|
|
276
|
+
typeof token === "string" && token.length > 0,
|
|
277
|
+
"readonly gate token is absent"
|
|
286
278
|
);
|
|
287
|
-
const hosted = await createHostedGate(scoped, root, env);
|
|
288
|
-
try {
|
|
289
|
-
const gated = { ...scoped, hookInstallation: hosted.installation };
|
|
290
|
-
const object = await committedProposal(
|
|
291
|
-
gated,
|
|
292
|
-
{ ...hosted.env, LISA_NPM_HOOK_ROLE: "commit" },
|
|
293
|
-
message
|
|
294
|
-
);
|
|
295
|
-
return await pushProposal(gated, hosted.env, branch, object);
|
|
296
|
-
} finally {
|
|
297
|
-
await hosted.close();
|
|
298
|
-
}
|
|
299
279
|
});
|
|
280
|
+
return withStage("gate-scratch", () =>
|
|
281
|
+
withPrivateRoot(async root => {
|
|
282
|
+
const env = {
|
|
283
|
+
...candidateEnvironment(root),
|
|
284
|
+
GIT_AUTHOR_NAME: BOT,
|
|
285
|
+
GIT_AUTHOR_EMAIL: MAIL,
|
|
286
|
+
GIT_COMMITTER_NAME: BOT,
|
|
287
|
+
GIT_COMMITTER_EMAIL: MAIL,
|
|
288
|
+
GIT_AUTHOR_DATE: `${preview.epoch} +0000`,
|
|
289
|
+
GIT_COMMITTER_DATE: `${preview.epoch} +0000`,
|
|
290
|
+
};
|
|
291
|
+
const branch = await withStage("gate-stage", () =>
|
|
292
|
+
stageProposal(scoped, env)
|
|
293
|
+
);
|
|
294
|
+
const proofGit = args =>
|
|
295
|
+
git(context.cwd, env, args, undefined, scoped.deadline);
|
|
296
|
+
const message = await withStage("gate-proof", () =>
|
|
297
|
+
installGateProof(context, root, proofGit)
|
|
298
|
+
);
|
|
299
|
+
const hosted = await createHostedGate(scoped, root, env);
|
|
300
|
+
try {
|
|
301
|
+
const gated = { ...scoped, hookInstallation: hosted.installation };
|
|
302
|
+
const object = await withStage("gate-commit", () =>
|
|
303
|
+
committedProposal(
|
|
304
|
+
gated,
|
|
305
|
+
{ ...hosted.env, LISA_NPM_HOOK_ROLE: "commit" },
|
|
306
|
+
message
|
|
307
|
+
)
|
|
308
|
+
);
|
|
309
|
+
return await withStage("gate-push", () =>
|
|
310
|
+
pushProposal(gated, hosted.env, branch, object)
|
|
311
|
+
);
|
|
312
|
+
} finally {
|
|
313
|
+
await withStage("gate-close", () => hosted.close());
|
|
314
|
+
}
|
|
315
|
+
})
|
|
316
|
+
);
|
|
300
317
|
}
|