@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.
- 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-classifier-cache.mjs +94 -0
- package/all/copy-overwrite/scripts/lib/npm-update-helper-inventory.mjs +5 -0
- package/all/copy-overwrite/scripts/lib/npm-update-helper.mjs +9 -2
- 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-hooks/parity-safety-net.sh +122 -8
- package/all/copy-overwrite/scripts/lisa-npm-updater.mjs +61 -5
- package/all/copy-overwrite/scripts/npm-updater-helper-graph.json +85 -10
- package/dist/cli/hook-refresh-files.d.ts +86 -0
- package/dist/cli/hook-refresh-files.d.ts.map +1 -0
- package/dist/cli/hook-refresh-files.js +209 -0
- package/dist/cli/hook-refresh-files.js.map +1 -0
- package/dist/cli/index.d.ts +3 -0
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +3 -0
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/refresh-hooks-cmd.d.ts +27 -0
- package/dist/cli/refresh-hooks-cmd.d.ts.map +1 -0
- package/dist/cli/refresh-hooks-cmd.js +156 -0
- package/dist/cli/refresh-hooks-cmd.js.map +1 -0
- package/dist/core/lisa-owned-hash-ledger.d.ts.map +1 -1
- package/dist/core/lisa-owned-hash-ledger.js +78 -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 +37 -14
- package/dist/core/upstream-evidence-manifest.js.map +1 -1
- package/dist/opencode/plugin-templates/parity-safety-net.sh +122 -8
- 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/hooks/parity-safety-net.sh +122 -8
- package/plugins/lisa-agy/hooks/parity-safety-net.sh +122 -8
- 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-copilot/hooks/parity-safety-net.sh +122 -8
- package/plugins/lisa-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-cursor/hooks/parity-safety-net.sh +122 -8
- 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/plugins/src/base/hooks/parity-safety-net.sh +122 -8
- package/rails/copy-overwrite/scripts/lisa-scratch-run.sh +54 -9
- package/scripts/build-plugins.sh +3 -0
- package/scripts/generate-scratch-supervisor-profile.mjs +75 -0
- 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
|
+
}
|