@codyswann/lisa 4.71.2 → 4.71.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (95) hide show
  1. package/all/copy-overwrite/scripts/lib/github-attestation-provider.mjs +22 -0
  2. package/all/copy-overwrite/scripts/lib/npm-update-allocate.mjs +13 -3
  3. package/all/copy-overwrite/scripts/lib/npm-update-cancel-origin.mjs +291 -0
  4. package/all/copy-overwrite/scripts/lib/npm-update-cancellation-proof.mjs +327 -0
  5. package/all/copy-overwrite/scripts/lib/npm-update-cancellation.mjs +248 -0
  6. package/all/copy-overwrite/scripts/lib/npm-update-classifier-cache.mjs +94 -0
  7. package/all/copy-overwrite/scripts/lib/npm-update-helper-inventory.mjs +5 -0
  8. package/all/copy-overwrite/scripts/lib/npm-update-helper.mjs +9 -2
  9. package/all/copy-overwrite/scripts/lib/npm-update-invariants.mjs +4 -0
  10. package/all/copy-overwrite/scripts/lib/npm-update-leaf-contract.mjs +1 -1
  11. package/all/copy-overwrite/scripts/lib/npm-update-leaf.mjs +29 -3
  12. package/all/copy-overwrite/scripts/lib/npm-update-supersession.mjs +274 -0
  13. package/all/copy-overwrite/scripts/lisa-hooks/parity-safety-net.sh +122 -8
  14. package/all/copy-overwrite/scripts/lisa-npm-updater.mjs +61 -5
  15. package/all/copy-overwrite/scripts/npm-updater-helper-graph.json +85 -10
  16. package/dist/cli/hook-refresh-files.d.ts +86 -0
  17. package/dist/cli/hook-refresh-files.d.ts.map +1 -0
  18. package/dist/cli/hook-refresh-files.js +209 -0
  19. package/dist/cli/hook-refresh-files.js.map +1 -0
  20. package/dist/cli/index.d.ts +3 -0
  21. package/dist/cli/index.d.ts.map +1 -1
  22. package/dist/cli/index.js +3 -0
  23. package/dist/cli/index.js.map +1 -1
  24. package/dist/cli/refresh-hooks-cmd.d.ts +27 -0
  25. package/dist/cli/refresh-hooks-cmd.d.ts.map +1 -0
  26. package/dist/cli/refresh-hooks-cmd.js +156 -0
  27. package/dist/cli/refresh-hooks-cmd.js.map +1 -0
  28. package/dist/core/lisa-owned-hash-ledger.d.ts.map +1 -1
  29. package/dist/core/lisa-owned-hash-ledger.js +78 -0
  30. package/dist/core/lisa-owned-hash-ledger.js.map +1 -1
  31. package/dist/core/nightly-e2e-guard-behavior-certificate.js +2 -2
  32. package/dist/core/upstream-evidence-manifest.d.ts.map +1 -1
  33. package/dist/core/upstream-evidence-manifest.js +37 -14
  34. package/dist/core/upstream-evidence-manifest.js.map +1 -1
  35. package/dist/opencode/plugin-templates/parity-safety-net.sh +122 -8
  36. package/package.json +4 -4
  37. package/plugins/lisa/.claude-plugin/plugin.json +1 -1
  38. package/plugins/lisa/.codex-plugin/plugin.json +1 -1
  39. package/plugins/lisa/hooks/parity-safety-net.sh +122 -8
  40. package/plugins/lisa-agy/hooks/parity-safety-net.sh +122 -8
  41. package/plugins/lisa-agy/plugin.json +1 -1
  42. package/plugins/lisa-cdk/.claude-plugin/plugin.json +1 -1
  43. package/plugins/lisa-cdk/.codex-plugin/plugin.json +1 -1
  44. package/plugins/lisa-cdk-agy/plugin.json +1 -1
  45. package/plugins/lisa-cdk-copilot/.claude-plugin/plugin.json +1 -1
  46. package/plugins/lisa-cdk-cursor/.claude-plugin/plugin.json +1 -1
  47. package/plugins/lisa-copilot/.claude-plugin/plugin.json +1 -1
  48. package/plugins/lisa-copilot/hooks/parity-safety-net.sh +122 -8
  49. package/plugins/lisa-cursor/.claude-plugin/plugin.json +1 -1
  50. package/plugins/lisa-cursor/hooks/parity-safety-net.sh +122 -8
  51. package/plugins/lisa-expo/.claude-plugin/plugin.json +1 -1
  52. package/plugins/lisa-expo/.codex-plugin/plugin.json +1 -1
  53. package/plugins/lisa-expo-agy/plugin.json +1 -1
  54. package/plugins/lisa-expo-copilot/.claude-plugin/plugin.json +1 -1
  55. package/plugins/lisa-expo-cursor/.claude-plugin/plugin.json +1 -1
  56. package/plugins/lisa-harper-fabric/.claude-plugin/plugin.json +1 -1
  57. package/plugins/lisa-harper-fabric/.codex-plugin/plugin.json +1 -1
  58. package/plugins/lisa-harper-fabric-agy/plugin.json +1 -1
  59. package/plugins/lisa-harper-fabric-copilot/.claude-plugin/plugin.json +1 -1
  60. package/plugins/lisa-harper-fabric-cursor/.claude-plugin/plugin.json +1 -1
  61. package/plugins/lisa-nestjs/.claude-plugin/plugin.json +1 -1
  62. package/plugins/lisa-nestjs/.codex-plugin/plugin.json +1 -1
  63. package/plugins/lisa-nestjs-agy/plugin.json +1 -1
  64. package/plugins/lisa-nestjs-copilot/.claude-plugin/plugin.json +1 -1
  65. package/plugins/lisa-nestjs-cursor/.claude-plugin/plugin.json +1 -1
  66. package/plugins/lisa-openclaw/.claude-plugin/plugin.json +1 -1
  67. package/plugins/lisa-openclaw/.codex-plugin/plugin.json +1 -1
  68. package/plugins/lisa-openclaw-agy/plugin.json +1 -1
  69. package/plugins/lisa-openclaw-copilot/.claude-plugin/plugin.json +1 -1
  70. package/plugins/lisa-openclaw-cursor/.claude-plugin/plugin.json +1 -1
  71. package/plugins/lisa-phaser/.claude-plugin/plugin.json +1 -1
  72. package/plugins/lisa-phaser/.codex-plugin/plugin.json +1 -1
  73. package/plugins/lisa-phaser-agy/plugin.json +1 -1
  74. package/plugins/lisa-phaser-copilot/.claude-plugin/plugin.json +1 -1
  75. package/plugins/lisa-phaser-cursor/.claude-plugin/plugin.json +1 -1
  76. package/plugins/lisa-rails/.claude-plugin/plugin.json +1 -1
  77. package/plugins/lisa-rails/.codex-plugin/plugin.json +1 -1
  78. package/plugins/lisa-rails-agy/plugin.json +1 -1
  79. package/plugins/lisa-rails-copilot/.claude-plugin/plugin.json +1 -1
  80. package/plugins/lisa-rails-cursor/.claude-plugin/plugin.json +1 -1
  81. package/plugins/lisa-typescript/.claude-plugin/plugin.json +1 -1
  82. package/plugins/lisa-typescript/.codex-plugin/plugin.json +1 -1
  83. package/plugins/lisa-typescript-agy/plugin.json +1 -1
  84. package/plugins/lisa-typescript-copilot/.claude-plugin/plugin.json +1 -1
  85. package/plugins/lisa-typescript-cursor/.claude-plugin/plugin.json +1 -1
  86. package/plugins/lisa-wiki/.claude-plugin/plugin.json +1 -1
  87. package/plugins/lisa-wiki/.codex-plugin/plugin.json +1 -1
  88. package/plugins/lisa-wiki-agy/plugin.json +1 -1
  89. package/plugins/lisa-wiki-copilot/.claude-plugin/plugin.json +1 -1
  90. package/plugins/lisa-wiki-cursor/.claude-plugin/plugin.json +1 -1
  91. package/plugins/src/base/hooks/parity-safety-net.sh +122 -8
  92. package/rails/copy-overwrite/scripts/lisa-scratch-run.sh +54 -9
  93. package/scripts/build-plugins.sh +3 -0
  94. package/scripts/generate-scratch-supervisor-profile.mjs +75 -0
  95. package/scripts/two-channel-couplings.json +2 -2
@@ -163,6 +163,28 @@ export function verifyHistoricalProvider(
163
163
  return providerOrigin(policy, descriptor, true, execute);
164
164
  }
165
165
 
166
+ /** Cancellation authenticates an old origin against a separately observed current main. */
167
+ export function verifyStaleOriginProvider(
168
+ policy,
169
+ descriptor,
170
+ currentParent,
171
+ execute = boundedSpawnSync
172
+ ) {
173
+ requireProof(
174
+ typeof currentParent === "string" &&
175
+ /^[a-f0-9]{40}$/.test(currentParent) &&
176
+ currentParent !== descriptor.parent,
177
+ "cancellation requires a distinct current parent"
178
+ );
179
+ const read = endpoint =>
180
+ ghJson(policy, [API, HOST_FLAG, PUBLIC_HOST, endpoint], execute);
181
+ repositoryState(read, policy, currentParent);
182
+ return {
183
+ run: runState(read, policy, descriptor, true),
184
+ claim: claimState(read, policy, descriptor),
185
+ };
186
+ }
187
+
166
188
  /** Fresh permission checks its own invocation and the exact observed destination. */
167
189
  export function verifyRecoveryProvider(
168
190
  policy,
@@ -8,6 +8,8 @@ import { locateCheckpoint } from "./npm-update-recovery.mjs";
8
8
  import { runProcess } from "./npm-update-process.mjs";
9
9
  import { sha256 } from "./github-attestation-verifier.mjs";
10
10
  import { registryVersion } from "./npm-update-npm.mjs";
11
+ import { discoverSupersession } from "./npm-update-supersession.mjs";
12
+ import { canonicalJson } from "../lisa-automation-provenance.mjs";
11
13
  import {
12
14
  buildDraft,
13
15
  qualityGates,
@@ -72,7 +74,7 @@ export async function filingEvidence(api, proposal, config, cwd) {
72
74
  }
73
75
 
74
76
  /** Same selected versions are discovered before deriving a new parent-bound key. */
75
- export async function discoverPrior(api, proposal) {
77
+ export async function discoverPrior(api, proposal, context) {
76
78
  const query = `repo:${proposal.repository} "${proposal.selectionKey}" is:issue in:body`;
77
79
  const found = await api.request(
78
80
  `search/issues?q=${encodeURIComponent(query)}&per_page=100`
@@ -80,12 +82,14 @@ export async function discoverPrior(api, proposal) {
80
82
  required(
81
83
  found.incomplete_results === false &&
82
84
  Number.isSafeInteger(found.total_count) &&
83
- found.total_count <= 1 &&
85
+ found.total_count <= (context ? 100 : 1) &&
84
86
  Array.isArray(found.items) &&
85
87
  found.items.length === found.total_count,
86
88
  "prior selection search is incomplete or ambiguous"
87
89
  );
88
90
  if (!found.total_count) return { status: "new" };
91
+ if (context && (found.total_count > 1 || found.items[0].state === "closed"))
92
+ return discoverSupersession(api, proposal, found.items, context);
89
93
  const issue = await api.issue(found.items[0].number);
90
94
  required(
91
95
  issue.state === "open" &&
@@ -254,7 +258,7 @@ export async function allocateLeaf({
254
258
  let proposal = suppliedProposal;
255
259
  validateProposal(proposal, policy);
256
260
  await api.main(proposal.parent);
257
- const prior = await discoverPrior(api, proposal);
261
+ const prior = await discoverPrior(api, proposal, { cwd, config });
258
262
  if (prior.status === "stale-outstanding")
259
263
  return {
260
264
  status: prior.status,
@@ -263,6 +267,12 @@ export async function allocateLeaf({
263
267
  };
264
268
  await api.assignable(policy.maintainer);
265
269
  const currentEvidence = await filingEvidence(api, proposal, config, cwd);
270
+ if (prior.supersession)
271
+ currentEvidence.supersession = {
272
+ workItem: prior.supersession.workItem,
273
+ cancellationSha256: sha256(`${canonicalJson(prior.supersession)}\n`),
274
+ proposalKey: prior.supersession.oldProposalKey,
275
+ };
266
276
  const checkpoint = prior.issue
267
277
  ? locateCheckpoint(prior.issue.comments, proposal, policy)
268
278
  : undefined;
@@ -0,0 +1,291 @@
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
+ /** Cancellation reads immutable old authority without granting that authority to new bytes. */
5
+ import { join } from "node:path";
6
+ import { writeFileSync } from "node:fs";
7
+ import { canonicalJson } from "../lisa-automation-provenance.mjs";
8
+ import {
9
+ required,
10
+ keys,
11
+ OBJECT,
12
+ validatePolicy,
13
+ validateProposal,
14
+ } from "./npm-update-contract.mjs";
15
+ import {
16
+ runProcess,
17
+ withPrivateRoot,
18
+ writeJson,
19
+ } from "./npm-update-process.mjs";
20
+ import { decodeCheckpoint } from "./npm-update-checkpoint.mjs";
21
+ import {
22
+ buildDraft,
23
+ historicalFiling,
24
+ inspectLeaf,
25
+ inspectCancelledLeaf,
26
+ qualityGates,
27
+ } from "./npm-update-leaf.mjs";
28
+ import { sha256 } from "./github-attestation-verifier.mjs";
29
+ import { verifyStaleOriginProvider } from "./github-attestation-provider.mjs";
30
+ import { verifyHistoricalDescriptor } from "./github-attestation-recovery.mjs";
31
+ import { DESCRIPTOR_KEYS } from "./automation-provenance-contract.mjs";
32
+
33
+ /** Only complete, digest-checked transport is considered; incomplete chunks grant nothing. */
34
+ export function completeCheckpoints(comments) {
35
+ required(
36
+ Array.isArray(comments) && comments.length <= 2000,
37
+ "checkpoint inventory exceeds bound"
38
+ );
39
+ const digests = new Set();
40
+ for (const comment of comments) {
41
+ const match = /^\[lisa-npm-checkpoint\] v1 ([a-f0-9]{64}) complete\n/.exec(
42
+ comment.body ?? ""
43
+ );
44
+ if (match) digests.add(match[1]);
45
+ }
46
+ required(digests.size <= 100, "completed checkpoint inventory exceeds bound");
47
+ return [...digests].map(digest => ({
48
+ digest,
49
+ payload: decodeCheckpoint(comments, digest),
50
+ }));
51
+ }
52
+
53
+ /** Named committed objects, never ignored config or candidate paths, select historical authority. */
54
+ export async function committedCancellationConfig(cwd, parent) {
55
+ required(
56
+ typeof parent === "string" && OBJECT.test(parent),
57
+ "invalid committed cancellation parent"
58
+ );
59
+ const result = await runProcess(
60
+ "git",
61
+ ["show", `${parent}:.lisa.config.json`],
62
+ {
63
+ cwd,
64
+ env: {
65
+ PATH: process.env.PATH,
66
+ HOME: "/nonexistent",
67
+ GIT_TERMINAL_PROMPT: "0",
68
+ },
69
+ maximum: 1_048_576,
70
+ }
71
+ );
72
+ const config = JSON.parse(result.stdout.toString());
73
+ required(
74
+ config.automationProvenance?.enabled === true,
75
+ "original committed authority is disabled"
76
+ );
77
+ validatePolicy(config.npmUpdater, config);
78
+ required(
79
+ config.automationProvenance.repository === config.npmUpdater.repository,
80
+ "original committed authority repository differs"
81
+ );
82
+ return config;
83
+ }
84
+
85
+ /** One original signed checkpoint is required, even if an unsigned filing or cancellation is present. */
86
+ function originCheckpoint(issue) {
87
+ const origins = completeCheckpoints(issue.comments).filter(
88
+ ({ payload }) => payload.preview?.descriptor
89
+ );
90
+ required(
91
+ origins.length === 1 && origins[0].payload.bundle.length > 0,
92
+ "complete original signed checkpoint is missing or ambiguous"
93
+ );
94
+ return origins[0];
95
+ }
96
+
97
+ /** Original chunk bytes must be the unedited Bot's posts during its witnessed run, not later copied markers. */
98
+ export function assertOriginTransport(comments, digest, policy, run) {
99
+ const start = Date.parse(run.run_started_at),
100
+ end = Date.parse(run.updated_at);
101
+ required(
102
+ Number.isFinite(start) && Number.isFinite(end) && start <= end,
103
+ "invalid original transport chronology"
104
+ );
105
+ const selected = comments.filter(comment =>
106
+ comment.body?.startsWith(`[lisa-npm-checkpoint] v1 ${digest} `)
107
+ );
108
+ required(selected.length > 0, "original transport is missing");
109
+ for (const comment of selected) {
110
+ const posted = Date.parse(comment.created_at);
111
+ required(
112
+ comment.user?.type === "Bot" &&
113
+ String(comment.user.id) === policy.claimActorId &&
114
+ comment.updated_at === comment.created_at &&
115
+ Number.isFinite(posted) &&
116
+ posted >= start - 60_000 &&
117
+ posted <= end + 60_000,
118
+ "original checkpoint was edited, copied or posted by a foreign actor"
119
+ );
120
+ }
121
+ }
122
+
123
+ /** Original claim, body, policy and proposal are checked separately from current authority. */
124
+ async function inspectOrigin(api, issue, payload, oldConfig, currentConfig) {
125
+ const policy = validatePolicy(oldConfig.npmUpdater, oldConfig);
126
+ const proposal = validateProposal(payload.proposal, policy);
127
+ const allocation = payload.allocation;
128
+ keys(allocation, [
129
+ "number",
130
+ "workItem",
131
+ "claimCommentId",
132
+ "claimSha256",
133
+ "draft",
134
+ "evidence",
135
+ "quality",
136
+ ]);
137
+ const evidence = historicalFiling(issue);
138
+ const draft = buildDraft(proposal, policy, oldConfig, evidence);
139
+ required(
140
+ canonicalJson(allocation.evidence) === canonicalJson(evidence) &&
141
+ canonicalJson(allocation.draft) === canonicalJson(draft) &&
142
+ allocation.number === issue.number &&
143
+ allocation.workItem === `${policy.repository}#${issue.number}`,
144
+ "original allocation differs"
145
+ );
146
+ // The current qualified classifier checks present holds; historical config only supplies the original draft.
147
+ const state = await (
148
+ issue.state === "open" ? inspectLeaf : inspectCancelledLeaf
149
+ )(issue, draft, policy, currentConfig);
150
+ required(
151
+ state.claimed &&
152
+ !state.unassigned &&
153
+ state.claims.length === 1 &&
154
+ String(state.claims[0].id) === allocation.claimCommentId &&
155
+ sha256(state.claims[0].body) === allocation.claimSha256 &&
156
+ String(state.claims[0].user?.id) === api.policy.claimActorId &&
157
+ state.claims[0].user?.type === "Bot",
158
+ "original managed claim changed"
159
+ );
160
+ qualityGates(
161
+ { ...draft, body: issue.body, title: issue.title },
162
+ proposal,
163
+ policy,
164
+ oldConfig,
165
+ evidence
166
+ );
167
+ return { policy, proposal, allocation };
168
+ }
169
+
170
+ /** Descriptor fields preserve the complete original immutable proposal and claim. */
171
+ function originalDescriptor(checkpoint, old, allocation, authority) {
172
+ const preview = checkpoint.payload.preview;
173
+ keys(preview, ["descriptor", "message", "epoch"]);
174
+ const d = preview.descriptor;
175
+ keys(d, DESCRIPTOR_KEYS);
176
+ required(
177
+ d.version === 1 &&
178
+ d.parent === old.parent &&
179
+ d.queue === old.repository &&
180
+ d.workItem === allocation.workItem &&
181
+ d.claimCommentId === allocation.claimCommentId &&
182
+ d.claimSha256 === allocation.claimSha256 &&
183
+ d.proposalKey === old.bindingKey &&
184
+ d.policySha256 === sha256(canonicalJson(authority)) &&
185
+ canonicalJson(d.files) === canonicalJson(old.hashes) &&
186
+ canonicalJson(d.updates) === canonicalJson(old.updates) &&
187
+ typeof preview.message === "string" &&
188
+ sha256(preview.message) === d.messageSha256 &&
189
+ checkpoint.digest === sha256(`${canonicalJson(d)}\n`),
190
+ "original descriptor differs"
191
+ );
192
+ return d;
193
+ }
194
+
195
+ /** The original fixed signing purpose and native transport chronology remain mandatory. */
196
+ async function verifyOriginalSubject(checkpoint, authority, d, issue, parent) {
197
+ await withPrivateRoot(async root => {
198
+ const paths = {
199
+ descriptor: join(root, "descriptor.json"),
200
+ bundle: join(root, "bundle.json"),
201
+ };
202
+ writeJson(paths.descriptor, d);
203
+ required(
204
+ Buffer.byteLength(checkpoint.payload.bundle) <= 1_048_576,
205
+ "origin bundle exceeds bound"
206
+ );
207
+ writeFileSync(paths.bundle, checkpoint.payload.bundle, {
208
+ mode: 0o600,
209
+ flag: "wx",
210
+ });
211
+ const observed = verifyStaleOriginProvider(authority, d, parent);
212
+ assertOriginTransport(
213
+ issue.comments,
214
+ checkpoint.digest,
215
+ authority,
216
+ observed.run
217
+ );
218
+ verifyHistoricalDescriptor(
219
+ authority,
220
+ d,
221
+ paths,
222
+ checkpoint.digest,
223
+ observed.run,
224
+ observed.claim
225
+ );
226
+ });
227
+ }
228
+
229
+ /** Proof reads use the actual original pin and original run, while live main remains the replacement parent. */
230
+ export async function authenticateStaleOrigin({
231
+ api,
232
+ cwd,
233
+ issue,
234
+ proposal,
235
+ config,
236
+ }) {
237
+ await api.main(proposal.parent);
238
+ const checkpoint = originCheckpoint(issue);
239
+ const old = checkpoint.payload.proposal;
240
+ required(
241
+ old.parent !== proposal.parent &&
242
+ old.selectionKey === proposal.selectionKey,
243
+ "origin is not this stale selection"
244
+ );
245
+ const oldConfig = await committedCancellationConfig(cwd, old.parent);
246
+ const authority = oldConfig.automationProvenance;
247
+ required(
248
+ [
249
+ "repository",
250
+ "repositoryId",
251
+ "ownerId",
252
+ "claimActorId",
253
+ "ghExecutable",
254
+ "ghSha256",
255
+ ].every(key => authority[key] === api.policy[key]),
256
+ "original and current repository/transport identity differs"
257
+ );
258
+ const inspected = await inspectOrigin(
259
+ api,
260
+ issue,
261
+ checkpoint.payload,
262
+ oldConfig,
263
+ config
264
+ );
265
+ const manifest = await runProcess(
266
+ "git",
267
+ ["show", `${old.parent}:package.json`],
268
+ {
269
+ cwd,
270
+ env: {
271
+ PATH: process.env.PATH,
272
+ HOME: "/nonexistent",
273
+ GIT_TERMINAL_PROMPT: "0",
274
+ },
275
+ maximum: 1_048_576,
276
+ }
277
+ );
278
+ required(
279
+ canonicalJson(JSON.parse(manifest.stdout.toString())) ===
280
+ canonicalJson(old.before),
281
+ "original committed manifest differs"
282
+ );
283
+ const d = originalDescriptor(
284
+ checkpoint,
285
+ old,
286
+ inspected.allocation,
287
+ authority
288
+ );
289
+ await verifyOriginalSubject(checkpoint, authority, d, issue, proposal.parent);
290
+ return { ...inspected, checkpoint, oldConfig, descriptor: d };
291
+ }
@@ -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
+ }