@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
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
// This file is managed by Lisa and IS replaced on each `lisa` run.
|
|
2
|
+
// Do not edit directly — durable changes belong upstream in Lisa.
|
|
3
|
+
|
|
4
|
+
/** Authenticated cancellation chains remain finite, exact and separate from ordinary recovery. */
|
|
5
|
+
import { canonicalJson } from "../lisa-automation-provenance.mjs";
|
|
6
|
+
import { required } from "./npm-update-contract.mjs";
|
|
7
|
+
import { validateProposal, keys } from "./npm-update-contract.mjs";
|
|
8
|
+
import { join } from "node:path";
|
|
9
|
+
import { writeFileSync } from "node:fs";
|
|
10
|
+
import { writeJson, withPrivateRoot } from "./npm-update-process.mjs";
|
|
11
|
+
import { sha256 } from "./github-attestation-verifier.mjs";
|
|
12
|
+
import {
|
|
13
|
+
completeCheckpoints,
|
|
14
|
+
committedCancellationConfig,
|
|
15
|
+
authenticateStaleOrigin,
|
|
16
|
+
assertOriginTransport,
|
|
17
|
+
} from "./npm-update-cancel-origin.mjs";
|
|
18
|
+
import {
|
|
19
|
+
cancellationRecord,
|
|
20
|
+
validateCancellation,
|
|
21
|
+
assertRecordedOperator,
|
|
22
|
+
verifyRecordedCancellation,
|
|
23
|
+
} from "./npm-update-cancellation-proof.mjs";
|
|
24
|
+
|
|
25
|
+
/** This pure topology function consumes already verified records; it grants no signature or provider authority. */
|
|
26
|
+
export function selectSupersession(issues, records, proposal) {
|
|
27
|
+
required(
|
|
28
|
+
issues.length > 0 && issues.length <= 100,
|
|
29
|
+
"supersession inventory exceeds bound"
|
|
30
|
+
);
|
|
31
|
+
const keys = new Map(issues.map(issue => [issue.key, issue]));
|
|
32
|
+
required(keys.size === issues.length, "duplicate proposal keys");
|
|
33
|
+
const active = issues.filter(issue => issue.state === "open");
|
|
34
|
+
required(
|
|
35
|
+
active.length <= 1 && records.size === issues.length - active.length,
|
|
36
|
+
"unverified closure or multiple active proposals"
|
|
37
|
+
);
|
|
38
|
+
const incoming = new Set();
|
|
39
|
+
for (const issue of issues.filter(item => item.state !== "open")) {
|
|
40
|
+
const record = records.get(issue.number);
|
|
41
|
+
required(
|
|
42
|
+
issue.state === "closed" &&
|
|
43
|
+
issue.state_reason === "not_planned" &&
|
|
44
|
+
record?.oldProposalKey === issue.key &&
|
|
45
|
+
record.proposalKey !== issue.key &&
|
|
46
|
+
!incoming.has(record.proposalKey),
|
|
47
|
+
"invalid or forked cancellation chain"
|
|
48
|
+
);
|
|
49
|
+
incoming.add(record.proposalKey);
|
|
50
|
+
}
|
|
51
|
+
const roots = issues.filter(issue => !incoming.has(issue.key));
|
|
52
|
+
required(roots.length === 1, "disconnected or cyclic cancellation chain");
|
|
53
|
+
let cursor = roots[0],
|
|
54
|
+
count = 0,
|
|
55
|
+
last;
|
|
56
|
+
while (cursor.state === "closed") {
|
|
57
|
+
required(++count <= issues.length, "cyclic cancellation chain");
|
|
58
|
+
last = records.get(cursor.number);
|
|
59
|
+
cursor = keys.get(last.proposalKey);
|
|
60
|
+
if (!cursor) break;
|
|
61
|
+
}
|
|
62
|
+
required(
|
|
63
|
+
count === records.size && (!active.length || cursor === active[0]),
|
|
64
|
+
"disconnected cancellation chain"
|
|
65
|
+
);
|
|
66
|
+
if (cursor)
|
|
67
|
+
return {
|
|
68
|
+
status:
|
|
69
|
+
cursor.parent === proposal.parent ? "existing" : "stale-outstanding",
|
|
70
|
+
issue: cursor.issue ?? cursor,
|
|
71
|
+
};
|
|
72
|
+
required(
|
|
73
|
+
last?.proposalKey === proposal.key &&
|
|
74
|
+
last.parent === proposal.parent &&
|
|
75
|
+
canonicalJson(last.proposalHashes) === canonicalJson(proposal.hashes),
|
|
76
|
+
"cancelled replacement no longer matches; preserve the chain"
|
|
77
|
+
);
|
|
78
|
+
return { status: "new", supersession: last };
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** Both branch and every historical PR must actually be absent before cancellation or reuse. */
|
|
82
|
+
export async function cancellationDestinationAbsent(api, proposalKey) {
|
|
83
|
+
required(
|
|
84
|
+
typeof proposalKey === "string" && /^[a-f0-9]{64}$/.test(proposalKey),
|
|
85
|
+
"invalid cancellation destination"
|
|
86
|
+
);
|
|
87
|
+
const branch = `lisa/npm-${proposalKey}`;
|
|
88
|
+
required(
|
|
89
|
+
(await api.maybe(
|
|
90
|
+
`repos/${api.policy.repository}/git/ref/heads/${branch}`
|
|
91
|
+
)) === null,
|
|
92
|
+
"published branch prevents cancellation"
|
|
93
|
+
);
|
|
94
|
+
const head = encodeURIComponent(
|
|
95
|
+
`${api.policy.repository.split("/")[0]}:${branch}`
|
|
96
|
+
);
|
|
97
|
+
const pulls = await api.list(
|
|
98
|
+
`repos/${api.policy.repository}/pulls?state=all&head=${head}`
|
|
99
|
+
);
|
|
100
|
+
required(pulls.length === 0, "published PR prevents cancellation");
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** Checkpoint discovery remains transport-only and refuses unknown complete roles. */
|
|
104
|
+
function cancellationCheckpoint(issue) {
|
|
105
|
+
const checkpoints = completeCheckpoints(issue.comments);
|
|
106
|
+
for (const { payload } of checkpoints)
|
|
107
|
+
required(
|
|
108
|
+
payload.preview === null ||
|
|
109
|
+
payload.preview?.descriptor ||
|
|
110
|
+
payload.preview?.cancellation,
|
|
111
|
+
"unknown completed checkpoint role"
|
|
112
|
+
);
|
|
113
|
+
const matches = checkpoints.filter(
|
|
114
|
+
({ payload }) => payload.preview?.cancellation
|
|
115
|
+
);
|
|
116
|
+
required(matches.length <= 1, "ambiguous completed cancellation records");
|
|
117
|
+
return matches[0];
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** Provider operator and official signature independently authenticate the complete historical subject. */
|
|
121
|
+
async function verifyRecordedBundle(api, authority, checkpoint, record, issue) {
|
|
122
|
+
const run = await api.request(
|
|
123
|
+
`repos/${record.repository}/actions/runs/${record.runId}/attempts/${record.runAttempt}`
|
|
124
|
+
);
|
|
125
|
+
const permission = await api.request(
|
|
126
|
+
`repos/${record.repository}/collaborators/${record.maintainer}/permission`
|
|
127
|
+
);
|
|
128
|
+
assertRecordedOperator(run, permission, authority, record);
|
|
129
|
+
assertOriginTransport(issue.comments, checkpoint.digest, authority, run);
|
|
130
|
+
await withPrivateRoot(async root => {
|
|
131
|
+
const paths = {
|
|
132
|
+
cancellation: join(root, "cancellation.json"),
|
|
133
|
+
cancellationBundle: join(root, "bundle.json"),
|
|
134
|
+
};
|
|
135
|
+
writeJson(paths.cancellation, record);
|
|
136
|
+
required(
|
|
137
|
+
Buffer.byteLength(checkpoint.payload.bundle) <= 1_048_576,
|
|
138
|
+
"cancellation bundle exceeds bound"
|
|
139
|
+
);
|
|
140
|
+
writeFileSync(paths.cancellationBundle, checkpoint.payload.bundle, {
|
|
141
|
+
mode: 0o600,
|
|
142
|
+
flag: "wx",
|
|
143
|
+
});
|
|
144
|
+
verifyRecordedCancellation(
|
|
145
|
+
authority,
|
|
146
|
+
record,
|
|
147
|
+
paths,
|
|
148
|
+
checkpoint.digest,
|
|
149
|
+
run
|
|
150
|
+
);
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/** A completion marker is only transport; original authority and a third fixed proof are independently required. */
|
|
155
|
+
export async function recordedCancellation({
|
|
156
|
+
api,
|
|
157
|
+
cwd,
|
|
158
|
+
issue,
|
|
159
|
+
proposal,
|
|
160
|
+
config,
|
|
161
|
+
origin,
|
|
162
|
+
}) {
|
|
163
|
+
const checkpoint = cancellationCheckpoint(issue);
|
|
164
|
+
if (!checkpoint) return undefined;
|
|
165
|
+
keys(checkpoint.payload.preview, ["cancellation"]);
|
|
166
|
+
const record = checkpoint.payload.preview.cancellation;
|
|
167
|
+
validateCancellation(record, record);
|
|
168
|
+
const historical = await committedCancellationConfig(cwd, record.parent);
|
|
169
|
+
const authority = historical.automationProvenance;
|
|
170
|
+
required(
|
|
171
|
+
[
|
|
172
|
+
"repository",
|
|
173
|
+
"repositoryId",
|
|
174
|
+
"ownerId",
|
|
175
|
+
"claimActorId",
|
|
176
|
+
"ghExecutable",
|
|
177
|
+
"ghSha256",
|
|
178
|
+
].every(key => authority[key] === api.policy[key]) &&
|
|
179
|
+
historical.npmUpdater.maintainer === config.npmUpdater.maintainer,
|
|
180
|
+
"recorded cancellation authority differs"
|
|
181
|
+
);
|
|
182
|
+
const replacement = validateProposal(
|
|
183
|
+
checkpoint.payload.proposal,
|
|
184
|
+
historical.npmUpdater
|
|
185
|
+
);
|
|
186
|
+
required(
|
|
187
|
+
replacement.selectionKey === proposal.selectionKey,
|
|
188
|
+
"recorded cancellation selection differs"
|
|
189
|
+
);
|
|
190
|
+
const original =
|
|
191
|
+
origin ??
|
|
192
|
+
(await authenticateStaleOrigin({ api, cwd, issue, proposal, config }));
|
|
193
|
+
required(
|
|
194
|
+
canonicalJson(checkpoint.payload.allocation) ===
|
|
195
|
+
canonicalJson(original.allocation),
|
|
196
|
+
"recorded cancellation allocation differs"
|
|
197
|
+
);
|
|
198
|
+
const expected = cancellationRecord(
|
|
199
|
+
original,
|
|
200
|
+
replacement,
|
|
201
|
+
historical,
|
|
202
|
+
record,
|
|
203
|
+
record.operatorId
|
|
204
|
+
);
|
|
205
|
+
validateCancellation(record, expected);
|
|
206
|
+
required(
|
|
207
|
+
checkpoint.digest === sha256(`${canonicalJson(record)}\n`),
|
|
208
|
+
"cancellation subject digest differs"
|
|
209
|
+
);
|
|
210
|
+
await verifyRecordedBundle(api, authority, checkpoint, record, issue);
|
|
211
|
+
await api.main(proposal.parent);
|
|
212
|
+
await cancellationDestinationAbsent(api, record.oldProposalKey);
|
|
213
|
+
if (issue.state === "closed")
|
|
214
|
+
required(
|
|
215
|
+
issue.state_reason === "not_planned" &&
|
|
216
|
+
issue.closed_by?.type === "Bot" &&
|
|
217
|
+
String(issue.closed_by.id) === api.policy.claimActorId,
|
|
218
|
+
"closure is not the authenticated managed cancellation"
|
|
219
|
+
);
|
|
220
|
+
return checkpoint;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/** Every closed match must independently authenticate before a finite connected chain is accepted. */
|
|
224
|
+
export async function discoverSupersession(
|
|
225
|
+
api,
|
|
226
|
+
proposal,
|
|
227
|
+
found,
|
|
228
|
+
{ cwd, config }
|
|
229
|
+
) {
|
|
230
|
+
required(
|
|
231
|
+
found.length > 0 && found.length <= 100,
|
|
232
|
+
"supersession search exceeds bound"
|
|
233
|
+
);
|
|
234
|
+
const issues = [],
|
|
235
|
+
records = new Map();
|
|
236
|
+
for (const item of found) {
|
|
237
|
+
const issue = await api.issue(item.number);
|
|
238
|
+
required(
|
|
239
|
+
!issue.pull_request && typeof issue.body === "string",
|
|
240
|
+
"foreign supersession issue"
|
|
241
|
+
);
|
|
242
|
+
const one = (name, pattern) => {
|
|
243
|
+
const values = [
|
|
244
|
+
...issue.body.matchAll(new RegExp(`^${name}: (${pattern})$`, "gm")),
|
|
245
|
+
];
|
|
246
|
+
required(values.length === 1, "ambiguous supersession identity");
|
|
247
|
+
return values[0][1];
|
|
248
|
+
};
|
|
249
|
+
required(
|
|
250
|
+
one("Selection key", "[a-f0-9]{64}") === proposal.selectionKey,
|
|
251
|
+
"supersession selection differs"
|
|
252
|
+
);
|
|
253
|
+
issues.push({
|
|
254
|
+
number: issue.number,
|
|
255
|
+
state: issue.state,
|
|
256
|
+
state_reason: issue.state_reason,
|
|
257
|
+
key: one("Proposal key", "[a-f0-9]{64}"),
|
|
258
|
+
parent: one("Base parent", "[a-f0-9]{40}"),
|
|
259
|
+
issue,
|
|
260
|
+
});
|
|
261
|
+
if (issue.state !== "open") {
|
|
262
|
+
const checkpoint = await recordedCancellation({
|
|
263
|
+
api,
|
|
264
|
+
cwd,
|
|
265
|
+
issue,
|
|
266
|
+
proposal,
|
|
267
|
+
config,
|
|
268
|
+
});
|
|
269
|
+
required(checkpoint, "closed leaf lacks authenticated cancellation");
|
|
270
|
+
records.set(issue.number, checkpoint.payload.preview.cancellation);
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
return selectSupersession(issues, records, proposal);
|
|
274
|
+
}
|
|
@@ -527,6 +527,94 @@ follow_names=""
|
|
|
527
527
|
follow_count=0
|
|
528
528
|
follow_bytes=0
|
|
529
529
|
followed_text=""
|
|
530
|
+
followed_rm_text=""
|
|
531
|
+
# Generated from the canonical reviewed supervisor, never from a host manifest.
|
|
532
|
+
# Regenerate through scripts/generate-scratch-supervisor-profile.mjs.
|
|
533
|
+
readonly SCRATCH_SUPERVISOR_SHA256='6fdec1ad0441d7636dc6749c44a23167baf7b75004f156b8a7d4ce1c18c790b7'
|
|
534
|
+
readonly SCRATCH_SUPERVISOR_CLEANUP_COUNT='7'
|
|
535
|
+
|
|
536
|
+
# An authenticated public supervisor owns its fixed cleanup sites. Its original
|
|
537
|
+
# source remains visible to every other policy; its actual payload is followed.
|
|
538
|
+
follow_supervisor() {
|
|
539
|
+
local token="$1" depth="$2" statement="$3" path="" profile="" payload="" rm_source="" original=""
|
|
540
|
+
case "$token" in *lisa-scratch-run.sh) ;; *) return 1 ;; esac
|
|
541
|
+
follow_locate "$token" || follow_refuse "$token" "supervisor source could not be resolved"
|
|
542
|
+
path="$follow_located"
|
|
543
|
+
profile="$(python3 - "$path" "$SCRATCH_SUPERVISOR_SHA256" "$statement" "$token" "$SCRATCH_SUPERVISOR_CLEANUP_COUNT" <<'PY'
|
|
544
|
+
import hashlib
|
|
545
|
+
import json
|
|
546
|
+
import os
|
|
547
|
+
import re
|
|
548
|
+
import shlex
|
|
549
|
+
import stat
|
|
550
|
+
import sys
|
|
551
|
+
|
|
552
|
+
path, digest, statement, operand, expected_count = sys.argv[1:]
|
|
553
|
+
def refuse():
|
|
554
|
+
sys.exit(2)
|
|
555
|
+
|
|
556
|
+
try:
|
|
557
|
+
info = os.lstat(path)
|
|
558
|
+
if not stat.S_ISREG(info.st_mode) or info.st_size > 262144:
|
|
559
|
+
refuse()
|
|
560
|
+
raw = open(path, 'rb').read(262145)
|
|
561
|
+
if len(raw) != info.st_size or not re.fullmatch(r'[0-9a-f]{64}', digest):
|
|
562
|
+
refuse()
|
|
563
|
+
if hashlib.sha256(raw).hexdigest() != digest:
|
|
564
|
+
refuse()
|
|
565
|
+
tokens = shlex.split(statement)
|
|
566
|
+
indices = [index for index, token in enumerate(tokens) if token == operand]
|
|
567
|
+
if len(indices) != 1 or '$' in operand or '`' in operand:
|
|
568
|
+
refuse()
|
|
569
|
+
index = indices[0]
|
|
570
|
+
if index < 1 or tokens[index - 1] not in ('sh', 'bash', '/bin/sh', '/bin/bash'):
|
|
571
|
+
refuse()
|
|
572
|
+
prefix = tokens[:index - 1]
|
|
573
|
+
if prefix[:1] == ['env']:
|
|
574
|
+
prefix = prefix[1:]
|
|
575
|
+
assignments = {}
|
|
576
|
+
for token in prefix:
|
|
577
|
+
if '=' not in token or re.search(r'[$`\r\n]', token):
|
|
578
|
+
refuse()
|
|
579
|
+
key, value = token.split('=', 1)
|
|
580
|
+
if key not in ('LISA_SCRATCH_BASE', 'LISA_SCRATCH_TRACE', 'TMPDIR') or key in assignments:
|
|
581
|
+
refuse()
|
|
582
|
+
assignments[key] = value
|
|
583
|
+
# Match actual shell :- precedence, including inherited and
|
|
584
|
+
# explicitly empty values. TMPDIR cannot override a nonempty inherited base.
|
|
585
|
+
effective = dict(os.environ, **assignments)
|
|
586
|
+
base = effective.get('LISA_SCRATCH_BASE') or effective.get('TMPDIR') or '/tmp'
|
|
587
|
+
if not base or not os.path.isabs(base) or re.search(r'[$`\r\n]', base):
|
|
588
|
+
refuse()
|
|
589
|
+
real_base = os.path.realpath(base)
|
|
590
|
+
roots = ['/tmp', '/var/tmp', os.environ.get('TMPDIR', '/tmp')]
|
|
591
|
+
if not any(os.path.commonpath([real_base, os.path.realpath(root)]) == os.path.realpath(root) for root in roots if root and os.path.isabs(root)):
|
|
592
|
+
refuse()
|
|
593
|
+
args = tokens[index + 1:]
|
|
594
|
+
if len(args) < 4 or args[0] != '--suite' or args[2] != '--':
|
|
595
|
+
refuse()
|
|
596
|
+
if not re.fullmatch(r'[A-Za-z0-9._-]{1,64}', args[1]):
|
|
597
|
+
refuse()
|
|
598
|
+
payload = args[3:]
|
|
599
|
+
if not payload[0] or payload[0].startswith('-') or any(re.search(r'[$`\x00\r\n]', value) for value in payload):
|
|
600
|
+
refuse()
|
|
601
|
+
source = raw.decode('utf8')
|
|
602
|
+
cleaned, count = re.subn(r'(?m)^(\s*)rm -rf "(\$_quarantine|\$_root|\$_entry|\$LISA_SCRATCH_ROOT)"', r'\1:', source)
|
|
603
|
+
if not re.fullmatch(r'[1-9][0-9]*', expected_count) or count != int(expected_count):
|
|
604
|
+
refuse()
|
|
605
|
+
print(json.dumps({'source': cleaned, 'original': source, 'payload': shlex.join(payload)}))
|
|
606
|
+
except (OSError, UnicodeError, ValueError):
|
|
607
|
+
refuse()
|
|
608
|
+
PY
|
|
609
|
+
)" || follow_refuse "$path" "supervisor source or public entry authority could not be authenticated"
|
|
610
|
+
rm_source="$(printf '%s' "$profile" | jq -er '.source')" || follow_refuse "$path" "invalid supervisor profile"
|
|
611
|
+
original="$(printf '%s' "$profile" | jq -er '.original')" || follow_refuse "$path" "invalid supervisor source"
|
|
612
|
+
payload="$(printf '%s' "$profile" | jq -er '.payload')" || follow_refuse "$path" "invalid supervisor payload"
|
|
613
|
+
follow_take "$path" "$depth" "$rm_source" "$original"
|
|
614
|
+
[ "$depth" -lt "$FOLLOW_MAX_DEPTH" ] || follow_refuse "$path" "supervisor payload exceeds inspection depth"
|
|
615
|
+
follow_scan "$payload" "$((depth + 1))"
|
|
616
|
+
return 0
|
|
617
|
+
}
|
|
530
618
|
|
|
531
619
|
FOLLOW_GUIDANCE="This command EXECUTES a file. These guards classify shell text, so the script an
|
|
532
620
|
invocation runs is read and scanned as part of the command — a destructive line
|
|
@@ -813,7 +901,7 @@ follow_locate() {
|
|
|
813
901
|
# executes, to a bounded depth. Full-line comments are dropped: a commented line
|
|
814
902
|
# is never executed, so scanning it can only manufacture a false refusal.
|
|
815
903
|
follow_take() {
|
|
816
|
-
local path="$1" depth="$2" size="" content=""
|
|
904
|
+
local path="$1" depth="$2" size="" content="" rm_content="${3-}"
|
|
817
905
|
case "$follow_paths" in
|
|
818
906
|
*$'\n'"$path"$'\n'*) return 0 ;;
|
|
819
907
|
esac
|
|
@@ -833,10 +921,22 @@ follow_take() {
|
|
|
833
921
|
if [ "$follow_bytes" -gt "$FOLLOW_MAX_TOTAL_BYTES" ]; then
|
|
834
922
|
follow_refuse "$path" "the files this command executes total more than the ${FOLLOW_MAX_TOTAL_BYTES}-byte inspection budget"
|
|
835
923
|
fi
|
|
836
|
-
|
|
837
|
-
|
|
924
|
+
if [ "$#" -eq 4 ]; then
|
|
925
|
+
# Scan the same complete bytes whose digest was authenticated, not a
|
|
926
|
+
# second file read that could pair changed source with the cleanup view.
|
|
927
|
+
content="$(printf '%s' "$4" | sed 's/^[[:space:]]*#.*$//')" || follow_refuse "$path" "authenticated source could not be read"
|
|
928
|
+
else
|
|
929
|
+
content="$(LC_ALL=C tr -d '\000' <"$path" | sed 's/^[[:space:]]*#.*$//')" \
|
|
930
|
+
|| follow_refuse "$path" "it could not be read"
|
|
931
|
+
fi
|
|
838
932
|
follow_names="$follow_names $path"
|
|
839
933
|
followed_text="$followed_text"$'\n'"$content"
|
|
934
|
+
if [ "$#" -lt 3 ]; then
|
|
935
|
+
rm_content="$content"
|
|
936
|
+
else
|
|
937
|
+
rm_content="$(printf '%s' "$rm_content" | sed 's/^[[:space:]]*#.*$//')" || follow_refuse "$path" "supervisor cleanup view could not be read"
|
|
938
|
+
fi
|
|
939
|
+
followed_rm_text="$followed_rm_text"$'\n'"$rm_content"
|
|
840
940
|
if [ "$depth" -lt "$FOLLOW_MAX_DEPTH" ]; then
|
|
841
941
|
# A `cd` inside the followed script governs what THAT script goes on to
|
|
842
942
|
# execute, and nothing after it in the command that ran the script — the
|
|
@@ -855,6 +955,11 @@ follow_take() {
|
|
|
855
955
|
follow_target() {
|
|
856
956
|
local token="$1" depth="$2" path=""
|
|
857
957
|
[ -n "$token" ] || return 0
|
|
958
|
+
# Entry authority belongs to each invocation, not the file-read cache. Only
|
|
959
|
+
# follow_supervisor may admit the exact public interpreter/argv profile.
|
|
960
|
+
case "$token" in
|
|
961
|
+
*lisa-scratch-run.sh) follow_refuse "$token" "supervisor requires its authenticated public interpreter entry" ;;
|
|
962
|
+
esac
|
|
858
963
|
if ! follow_locate "$token"; then
|
|
859
964
|
if [ "$depth" -eq 0 ]; then
|
|
860
965
|
follow_refuse "$token" "no readable file could be resolved for it"
|
|
@@ -888,6 +993,9 @@ follow_direct() {
|
|
|
888
993
|
return 0
|
|
889
994
|
fi
|
|
890
995
|
path="$follow_located"
|
|
996
|
+
case "$path" in
|
|
997
|
+
*lisa-scratch-run.sh) follow_refuse "$path" "supervisor requires its authenticated public interpreter entry" ;;
|
|
998
|
+
esac
|
|
891
999
|
IFS= read -r -n 256 raw <"$path" 2>/dev/null || true
|
|
892
1000
|
while [ "$k" -lt "${#raw}" ]; do
|
|
893
1001
|
char="${raw:k:1}"
|
|
@@ -1219,7 +1327,9 @@ follow_scan() {
|
|
|
1219
1327
|
fi
|
|
1220
1328
|
follow_unwrap "${toks[j]}"
|
|
1221
1329
|
tj="$follow_unwrapped"
|
|
1222
|
-
|
|
1330
|
+
if ! follow_supervisor "$tj" "$depth" "$stmt"; then
|
|
1331
|
+
follow_target "$tj" "$depth"
|
|
1332
|
+
fi
|
|
1223
1333
|
cmd_pos=0
|
|
1224
1334
|
i=$((j + 1))
|
|
1225
1335
|
continue
|
|
@@ -1248,6 +1358,7 @@ follow_scan() {
|
|
|
1248
1358
|
}
|
|
1249
1359
|
|
|
1250
1360
|
follow_scan "$command_for_guards" 0
|
|
1361
|
+
rm_command_for_guards="$command_for_guards"$'\n'"$followed_rm_text"
|
|
1251
1362
|
if [ -n "$follow_names" ]; then
|
|
1252
1363
|
command_for_guards="$command_for_guards"$'\n'"$followed_text"
|
|
1253
1364
|
# A refusal must say WHERE the match is. Without this the operator reads
|
|
@@ -1266,6 +1377,8 @@ fi
|
|
|
1266
1377
|
# `sed ':a;N;$!ba;…'`, which errors on BSD sed (macOS) and there silently no-ops.
|
|
1267
1378
|
normalized_command_str="$(printf '%s' "$command_for_guards" \
|
|
1268
1379
|
| awk '{ if (sub(/\\$/, "")) printf "%s ", $0; else print }')"
|
|
1380
|
+
rm_normalized_command_str="$(printf '%s' "$rm_command_for_guards" \
|
|
1381
|
+
| awk '{ if (sub(/\\$/, "")) printf "%s ", $0; else print }')"
|
|
1269
1382
|
|
|
1270
1383
|
# matches / matches_cs run an ERE against the guarded command text. matches is
|
|
1271
1384
|
# case-insensitive (the default for these guards); matches_cs is case-SENSITIVE
|
|
@@ -1570,7 +1683,7 @@ rm_assignment_value() {
|
|
|
1570
1683
|
# small lexer below preserves quoted right-hand sides such as V="/tmp/x"
|
|
1571
1684
|
# while excluding an argument such as echo "V=/tmp/x". Any ambiguity fails
|
|
1572
1685
|
# closed.
|
|
1573
|
-
RM_ASSIGNMENT_NAME="$name" RM_ASSIGNMENT_COMMAND="$
|
|
1686
|
+
RM_ASSIGNMENT_NAME="$name" RM_ASSIGNMENT_COMMAND="$rm_normalized_command_str" \
|
|
1574
1687
|
RM_ASSIGNMENT_STATEMENT="$rm_assignment_stmt" \
|
|
1575
1688
|
python3 - <<'PY'
|
|
1576
1689
|
import os
|
|
@@ -1769,7 +1882,7 @@ classify_rm_target() {
|
|
|
1769
1882
|
# process substitution cannot deny anything either — it ends that subshell, not
|
|
1770
1883
|
# this hook.
|
|
1771
1884
|
rm_segments_status=0
|
|
1772
|
-
rm_segments="$(printf '%s' "$
|
|
1885
|
+
rm_segments="$(printf '%s' "$rm_normalized_command_str" | tr '&|;' '\n' \
|
|
1773
1886
|
| grep -Ei -- "$RM_CMD"'([[:space:]]|$)')" || rm_segments_status=$?
|
|
1774
1887
|
case "$rm_segments_status" in
|
|
1775
1888
|
0 | 1) ;;
|
|
@@ -2281,9 +2394,10 @@ git_segments="$(printf '%s' "$normalized_command_str" \
|
|
|
2281
2394
|
-e 's/[0-9]*>&[0-9-]*/>/g' \
|
|
2282
2395
|
-e 's/[0-9]*<&[0-9-]*/</g' \
|
|
2283
2396
|
-e 's/||/;/g' \
|
|
2284
|
-
| tr '&;' '\n'
|
|
2397
|
+
| tr '&;' '\n' \
|
|
2398
|
+
| grep -E -- "$RM_RF_CLUSTER|$RM_RF_SPLIT")" || git_segments_status=$?
|
|
2285
2399
|
case "$git_segments_status" in
|
|
2286
|
-
0) ;;
|
|
2400
|
+
0 | 1) ;;
|
|
2287
2401
|
*) scan_failed "$git_segments_status" ;;
|
|
2288
2402
|
esac
|
|
2289
2403
|
while IFS= read -r git_stmt; do
|
|
@@ -33,6 +33,62 @@ import {
|
|
|
33
33
|
import { gateProposal } from "./lib/npm-update-gate.mjs";
|
|
34
34
|
import { readGateProof } from "./lib/npm-update-gate-proof.mjs";
|
|
35
35
|
import { sha256 } from "./lib/github-attestation-verifier.mjs";
|
|
36
|
+
import {
|
|
37
|
+
prepareCancellation,
|
|
38
|
+
finalizeCancellation,
|
|
39
|
+
} from "./lib/npm-update-cancellation.mjs";
|
|
40
|
+
const CANCEL_PREPARE = "cancel-prepare";
|
|
41
|
+
const PHASES = [
|
|
42
|
+
"prepare",
|
|
43
|
+
"allocate",
|
|
44
|
+
"checkpoint",
|
|
45
|
+
"gate",
|
|
46
|
+
"publish",
|
|
47
|
+
CANCEL_PREPARE,
|
|
48
|
+
"cancel-checkpoint",
|
|
49
|
+
];
|
|
50
|
+
|
|
51
|
+
/** Three complete literal fields identify one old leaf and the exact expected current main. */
|
|
52
|
+
export function cancellationRequest(env = process.env) {
|
|
53
|
+
required(
|
|
54
|
+
typeof env.CANCEL_ISSUE === "string" && /^[1-9]\d*$/.test(env.CANCEL_ISSUE),
|
|
55
|
+
"exact cancellation issue number required"
|
|
56
|
+
);
|
|
57
|
+
const number = Number(env.CANCEL_ISSUE);
|
|
58
|
+
required(
|
|
59
|
+
Number.isSafeInteger(number) &&
|
|
60
|
+
String(number) === env.CANCEL_ISSUE &&
|
|
61
|
+
typeof env.CANCEL_PROPOSAL_KEY === "string" &&
|
|
62
|
+
/^[a-f0-9]{64}$/.test(env.CANCEL_PROPOSAL_KEY) &&
|
|
63
|
+
typeof env.CANCEL_EXPECTED_MAIN === "string" &&
|
|
64
|
+
/^[a-f0-9]{40}$/.test(env.CANCEL_EXPECTED_MAIN),
|
|
65
|
+
"complete exact cancellation identity required"
|
|
66
|
+
);
|
|
67
|
+
return {
|
|
68
|
+
number,
|
|
69
|
+
proposalKey: env.CANCEL_PROPOSAL_KEY,
|
|
70
|
+
parent: env.CANCEL_EXPECTED_MAIN,
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** Cancellation stays inside the same trusted issuer job and never consumes ordinary publication slots. */
|
|
75
|
+
async function cancellationPhase(cwd, output, proposal, config, phase) {
|
|
76
|
+
const api = new GitHub(config.automationProvenance, process.env.GH_TOKEN);
|
|
77
|
+
const context = {
|
|
78
|
+
api,
|
|
79
|
+
cwd,
|
|
80
|
+
output,
|
|
81
|
+
proposal,
|
|
82
|
+
config,
|
|
83
|
+
request: cancellationRequest(),
|
|
84
|
+
};
|
|
85
|
+
const result = await (
|
|
86
|
+
phase === CANCEL_PREPARE ? prepareCancellation : finalizeCancellation
|
|
87
|
+
)(context);
|
|
88
|
+
process.stdout.write(
|
|
89
|
+
`${phase === CANCEL_PREPARE ? result.mode : result.status}\n`
|
|
90
|
+
);
|
|
91
|
+
}
|
|
36
92
|
|
|
37
93
|
/** Only committed policy grants authority; ignored/local config cannot activate Actions. */
|
|
38
94
|
async function configuration(cwd) {
|
|
@@ -148,11 +204,8 @@ async function gatePhase(
|
|
|
148
204
|
/** Strict phase and fixed directory arguments are data, never a caller recipe. */
|
|
149
205
|
export async function main(argv = process.argv.slice(2)) {
|
|
150
206
|
required(
|
|
151
|
-
argv.length === 3 &&
|
|
152
|
-
|
|
153
|
-
argv[0]
|
|
154
|
-
),
|
|
155
|
-
"usage: lisa-npm-updater.mjs <prepare|allocate|checkpoint|gate|publish> <owned-checkout> <private-phase-directory>"
|
|
207
|
+
argv.length === 3 && PHASES.includes(argv[0]),
|
|
208
|
+
"usage: lisa-npm-updater.mjs <prepare|allocate|checkpoint|gate|publish|cancel-prepare|cancel-checkpoint> <owned-checkout> <private-phase-directory>"
|
|
156
209
|
);
|
|
157
210
|
const [phase, checkout, directory] = argv;
|
|
158
211
|
const cwd = resolve(checkout);
|
|
@@ -173,6 +226,9 @@ export async function main(argv = process.argv.slice(2)) {
|
|
|
173
226
|
readJson(join(output, "proposal.json")),
|
|
174
227
|
policy
|
|
175
228
|
);
|
|
229
|
+
if (phase === CANCEL_PREPARE || phase === "cancel-checkpoint") {
|
|
230
|
+
return cancellationPhase(cwd, output, proposal, config, phase);
|
|
231
|
+
}
|
|
176
232
|
if (phase === "allocate")
|
|
177
233
|
return allocatePhase(cwd, output, proposal, policy, config);
|
|
178
234
|
const allocation = readJson(join(output, "allocation.json"));
|