@codyswann/lisa 4.71.7 → 4.71.9
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/README.md +7 -0
- package/all/copy-overwrite/scripts/check-threshold-ratchet.mjs +571 -0
- package/all/copy-overwrite/scripts/check-workflow-load-failures.mjs +31 -9
- package/all/copy-overwrite/scripts/lib/automation-provenance-contract.mjs +19 -0
- package/all/copy-overwrite/scripts/lib/automation-provenance-files.mjs +49 -0
- package/all/copy-overwrite/scripts/lib/automation-provenance-local.mjs +54 -38
- package/all/copy-overwrite/scripts/lib/npm-update-allocate.mjs +8 -2
- package/all/copy-overwrite/scripts/lib/npm-update-bun.mjs +189 -0
- package/all/copy-overwrite/scripts/lib/npm-update-cancel-origin.mjs +7 -3
- package/all/copy-overwrite/scripts/lib/npm-update-cancellation-proof.mjs +6 -1
- package/all/copy-overwrite/scripts/lib/npm-update-contract.mjs +26 -5
- package/all/copy-overwrite/scripts/lib/npm-update-gate.mjs +8 -5
- package/all/copy-overwrite/scripts/lib/npm-update-github.mjs +2 -2
- package/all/copy-overwrite/scripts/lib/npm-update-helper-inventory.mjs +3 -4
- package/all/copy-overwrite/scripts/lib/npm-update-hook-installation.mjs +68 -1
- package/all/copy-overwrite/scripts/lib/npm-update-hosted-gate.mjs +13 -80
- package/all/copy-overwrite/scripts/lib/npm-update-leaf-contract.mjs +6 -5
- package/all/copy-overwrite/scripts/lib/npm-update-prepare.mjs +35 -7
- package/all/copy-overwrite/scripts/lib/npm-update-publish.mjs +1 -1
- package/all/copy-overwrite/scripts/npm-updater-helper-graph.json +43 -16
- package/all/copy-overwrite/scripts/threshold-ratchet-compare.mjs +521 -0
- package/all/copy-overwrite/scripts/threshold-ratchet-families.mjs +546 -0
- package/dist/configs/repo-scan.js +1 -1
- package/dist/core/lisa-owned-hash-ledger.d.ts.map +1 -1
- package/dist/core/lisa-owned-hash-ledger.js +60 -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 +46 -32
- package/dist/core/upstream-evidence-manifest.js.map +1 -1
- package/dist/strategies/merge.d.ts +1 -1
- package/dist/strategies/merge.d.ts.map +1 -1
- package/dist/strategies/merge.js +22 -2
- package/dist/strategies/merge.js.map +1 -1
- package/package.json +4 -4
- package/plugins/lisa/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa/hooks/threshold-ratchet-compare.mjs +3 -3
- package/plugins/lisa/hooks/threshold-ratchet.mjs +1 -1
- package/plugins/lisa-agy/plugin.json +1 -1
- package/plugins/lisa-cdk/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-cdk/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-cdk-agy/plugin.json +1 -1
- package/plugins/lisa-cdk-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-cdk-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-copilot/hooks/threshold-ratchet-compare.mjs +3 -3
- package/plugins/lisa-copilot/hooks/threshold-ratchet.mjs +1 -1
- package/plugins/lisa-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-cursor/hooks/threshold-ratchet-compare.mjs +3 -3
- package/plugins/lisa-cursor/hooks/threshold-ratchet.mjs +1 -1
- package/plugins/lisa-expo/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-expo/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-expo-agy/plugin.json +1 -1
- package/plugins/lisa-expo-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-expo-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-harper-fabric/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-harper-fabric/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-harper-fabric-agy/plugin.json +1 -1
- package/plugins/lisa-harper-fabric-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-harper-fabric-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-nestjs/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-nestjs/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-nestjs-agy/plugin.json +1 -1
- package/plugins/lisa-nestjs-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-nestjs-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-openclaw/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-openclaw/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-openclaw-agy/plugin.json +1 -1
- package/plugins/lisa-openclaw-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-openclaw-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-phaser/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-phaser/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-phaser-agy/plugin.json +1 -1
- package/plugins/lisa-phaser-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-phaser-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-rails/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-rails/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-rails-agy/plugin.json +1 -1
- package/plugins/lisa-rails-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-rails-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-typescript/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-typescript/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-typescript-agy/plugin.json +1 -1
- package/plugins/lisa-typescript-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-typescript-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-wiki/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-wiki/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-wiki-agy/plugin.json +1 -1
- package/plugins/lisa-wiki-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-wiki-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/materialized-artifacts.json +3 -0
- package/plugins/src/base/hooks/threshold-ratchet-compare.mjs +3 -3
- package/plugins/src/base/hooks/threshold-ratchet.mjs +1 -1
- package/rails/copy-overwrite/scripts/check-threshold-ratchet.mjs +1 -1
- package/rails/copy-overwrite/scripts/threshold-ratchet-compare.mjs +3 -3
- package/scripts/build-plugins.sh +2 -1
- package/scripts/generate-scratch-supervisor-profile.mjs +1 -1
- package/scripts/install-claude-plugins.sh +8 -4
- package/scripts/two-channel-couplings.json +9 -7
- package/typescript/copy-overwrite/scripts/check-threshold-ratchet.mjs +1 -1
- package/typescript/copy-overwrite/scripts/threshold-ratchet-compare.mjs +3 -3
- /package/{rails → all}/copy-contents/scripts/lisa-mutation.sh +0 -0
- /package/{rails → all}/copy-overwrite/scripts/lisa-clean-git-env.sh +0 -0
- /package/{rails → all}/copy-overwrite/scripts/lisa-scratch-run.sh +0 -0
|
@@ -0,0 +1,49 @@
|
|
|
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
|
+
/** Descriptor-backed readers reject aliases and growth before proof bytes gain authority. */
|
|
5
|
+
import {
|
|
6
|
+
constants,
|
|
7
|
+
closeSync,
|
|
8
|
+
fstatSync,
|
|
9
|
+
lstatSync,
|
|
10
|
+
openSync,
|
|
11
|
+
readSync,
|
|
12
|
+
} from "node:fs";
|
|
13
|
+
import { requireProof } from "./github-attestation-verifier.mjs";
|
|
14
|
+
|
|
15
|
+
/** Only native ENOENT establishes absence; dangling aliases and query errors refuse. */
|
|
16
|
+
export function fileAbsent(file) {
|
|
17
|
+
try {
|
|
18
|
+
lstatSync(file);
|
|
19
|
+
return false;
|
|
20
|
+
} catch (error) {
|
|
21
|
+
if (error.code === "ENOENT") return true;
|
|
22
|
+
throw error;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** Refuse aliases and oversize files; snapshots remain private, regular files. */
|
|
27
|
+
export function privateBytes(file, maximum, privateFile = true) {
|
|
28
|
+
const fd = openSync(file, constants.O_RDONLY | constants.O_NOFOLLOW);
|
|
29
|
+
try {
|
|
30
|
+
const stat = fstatSync(fd);
|
|
31
|
+
requireProof(
|
|
32
|
+
stat.isFile() &&
|
|
33
|
+
stat.size <= maximum &&
|
|
34
|
+
(!privateFile || (stat.mode & 0o077) === 0),
|
|
35
|
+
"invalid regular bounded proof file"
|
|
36
|
+
);
|
|
37
|
+
const bytes = Buffer.alloc(maximum + 1);
|
|
38
|
+
let count = 0;
|
|
39
|
+
while (count < bytes.length) {
|
|
40
|
+
const added = readSync(fd, bytes, count, bytes.length - count, null);
|
|
41
|
+
if (added === 0) break;
|
|
42
|
+
count += added;
|
|
43
|
+
}
|
|
44
|
+
requireProof(count <= maximum, "proof file grew beyond limit");
|
|
45
|
+
return bytes.subarray(0, count);
|
|
46
|
+
} finally {
|
|
47
|
+
closeSync(fd);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
@@ -2,14 +2,9 @@
|
|
|
2
2
|
// Do not edit directly — durable changes belong upstream in Lisa.
|
|
3
3
|
|
|
4
4
|
/** Local proof snapshots and the genuine canonical resolver preserve exact final-read authority. */
|
|
5
|
-
import {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
fstatSync,
|
|
9
|
-
lstatSync,
|
|
10
|
-
openSync,
|
|
11
|
-
readSync,
|
|
12
|
-
} from "node:fs";
|
|
5
|
+
import { lstatSync } from "node:fs";
|
|
6
|
+
import { fileAbsent, privateBytes } from "./automation-provenance-files.mjs";
|
|
7
|
+
export { privateBytes } from "./automation-provenance-files.mjs";
|
|
13
8
|
import { dirname, resolve } from "node:path";
|
|
14
9
|
import { TextDecoder } from "node:util";
|
|
15
10
|
import { resolveWorkItemContext, run } from "../lisa-work-item.mjs";
|
|
@@ -18,37 +13,13 @@ import { boundedSpawnSync } from "./bounded-spawn.mjs";
|
|
|
18
13
|
import {
|
|
19
14
|
canonicalJson,
|
|
20
15
|
exactKeys,
|
|
21
|
-
|
|
16
|
+
proposalFileNames,
|
|
17
|
+
optionalLockFields,
|
|
22
18
|
DESCRIPTOR_KEYS,
|
|
23
19
|
HEX,
|
|
24
20
|
OBJECT_ID,
|
|
25
21
|
} from "./automation-provenance-contract.mjs";
|
|
26
22
|
|
|
27
|
-
/** Refuse aliases and oversize files; snapshots remain private, regular files. */
|
|
28
|
-
export function privateBytes(file, maximum, privateFile = true) {
|
|
29
|
-
const fd = openSync(file, constants.O_RDONLY | constants.O_NOFOLLOW);
|
|
30
|
-
try {
|
|
31
|
-
const stat = fstatSync(fd);
|
|
32
|
-
requireProof(
|
|
33
|
-
stat.isFile() &&
|
|
34
|
-
stat.size <= maximum &&
|
|
35
|
-
(!privateFile || (stat.mode & 0o077) === 0),
|
|
36
|
-
"invalid regular bounded proof file"
|
|
37
|
-
);
|
|
38
|
-
const bytes = Buffer.alloc(maximum + 1);
|
|
39
|
-
let count = 0;
|
|
40
|
-
while (count < bytes.length) {
|
|
41
|
-
const added = readSync(fd, bytes, count, bytes.length - count, null);
|
|
42
|
-
if (added === 0) break;
|
|
43
|
-
count += added;
|
|
44
|
-
}
|
|
45
|
-
requireProof(count <= maximum, "proof file grew beyond limit");
|
|
46
|
-
return bytes.subarray(0, count);
|
|
47
|
-
} finally {
|
|
48
|
-
closeSync(fd);
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
|
|
52
23
|
/** Git reads always use the actual checkout and bounded shared runner. */
|
|
53
24
|
export function git(args) {
|
|
54
25
|
return run("git", args, {
|
|
@@ -59,7 +30,7 @@ export function git(args) {
|
|
|
59
30
|
|
|
60
31
|
/** Alias and exact staged-blob checks run after descriptor metadata validation. */
|
|
61
32
|
function stagedFiles(descriptor) {
|
|
62
|
-
for (const file of
|
|
33
|
+
for (const file of proposalFileNames(descriptor)) {
|
|
63
34
|
const entry = git(["ls-files", "--stage", "--", file]);
|
|
64
35
|
requireProof(
|
|
65
36
|
entry.startsWith("100644 ") && entry.split("\n").length === 1,
|
|
@@ -76,6 +47,47 @@ function stagedFiles(descriptor) {
|
|
|
76
47
|
}
|
|
77
48
|
}
|
|
78
49
|
|
|
50
|
+
/** Bind optional lock presence to HEAD and refuse aliased working-tree counterparts. */
|
|
51
|
+
function originalBun(descriptor) {
|
|
52
|
+
const original = git(["ls-tree", "HEAD", "--", "bun.lock"]);
|
|
53
|
+
if (!Object.hasOwn(descriptor, "bunLockSha256")) {
|
|
54
|
+
requireProof(original === "", "original Bun lock omitted from descriptor");
|
|
55
|
+
requireProof(
|
|
56
|
+
fileAbsent(resolve("bun.lock")),
|
|
57
|
+
"unexpected working Bun lock"
|
|
58
|
+
);
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
requireProof(
|
|
62
|
+
/^100644 blob [a-f0-9]{40}\tbun\.lock$/.test(original),
|
|
63
|
+
"original Bun lock is not regular"
|
|
64
|
+
);
|
|
65
|
+
const bytes = run("git", ["show", "HEAD:bun.lock"], {
|
|
66
|
+
timeout: 30_000,
|
|
67
|
+
maxBuffer: 1_048_576,
|
|
68
|
+
}).stdout;
|
|
69
|
+
requireProof(
|
|
70
|
+
sha256(bytes) === descriptor.bunLockSha256,
|
|
71
|
+
"original Bun lock digest differs"
|
|
72
|
+
);
|
|
73
|
+
const path = resolve("bun.lock");
|
|
74
|
+
const before = lstatSync(path);
|
|
75
|
+
requireProof(
|
|
76
|
+
before.isFile() && before.nlink === 1,
|
|
77
|
+
"Bun lock path is aliased"
|
|
78
|
+
);
|
|
79
|
+
const current = privateBytes(path, 1_048_576, false);
|
|
80
|
+
const after = lstatSync(path);
|
|
81
|
+
requireProof(
|
|
82
|
+
after.isFile() &&
|
|
83
|
+
after.nlink === 1 &&
|
|
84
|
+
before.dev === after.dev &&
|
|
85
|
+
before.ino === after.ino &&
|
|
86
|
+
sha256(current) === descriptor.files["bun.lock"],
|
|
87
|
+
"working Bun lock identity differs"
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
|
|
79
91
|
/** Match complete message and Git metadata before any network/proof acceptance. */
|
|
80
92
|
export function localDescriptor(descriptor, messageBytes, reference, policy) {
|
|
81
93
|
// Parsing never supplies the signed digest. Reject malformed UTF8 only on
|
|
@@ -84,7 +96,11 @@ export function localDescriptor(descriptor, messageBytes, reference, policy) {
|
|
|
84
96
|
fatal: true,
|
|
85
97
|
ignoreBOM: true,
|
|
86
98
|
}).decode(messageBytes);
|
|
87
|
-
exactKeys(
|
|
99
|
+
exactKeys(
|
|
100
|
+
descriptor,
|
|
101
|
+
[...DESCRIPTOR_KEYS, ...optionalLockFields(descriptor)],
|
|
102
|
+
"descriptor"
|
|
103
|
+
);
|
|
88
104
|
requireProof(
|
|
89
105
|
descriptor.version === 1 &&
|
|
90
106
|
descriptor.runId === reference.runId &&
|
|
@@ -139,10 +155,10 @@ export function localDescriptor(descriptor, messageBytes, reference, policy) {
|
|
|
139
155
|
.split("\n")
|
|
140
156
|
.sort();
|
|
141
157
|
requireProof(
|
|
142
|
-
changed.join("\n") ===
|
|
158
|
+
changed.join("\n") === proposalFileNames(descriptor).join("\n"),
|
|
143
159
|
"proposal changes foreign files"
|
|
144
160
|
);
|
|
145
|
-
|
|
161
|
+
originalBun(descriptor);
|
|
146
162
|
stagedFiles(descriptor);
|
|
147
163
|
}
|
|
148
164
|
|
|
@@ -2,7 +2,12 @@
|
|
|
2
2
|
// Do not edit directly — durable changes belong upstream in Lisa.
|
|
3
3
|
|
|
4
4
|
/** Fixed-schema writer evidence and idempotent current claim. @module npm-updater */
|
|
5
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
required,
|
|
7
|
+
CLAIM,
|
|
8
|
+
validateProposal,
|
|
9
|
+
proposalFileNames,
|
|
10
|
+
} from "./npm-update-contract.mjs";
|
|
6
11
|
import { baseline } from "./npm-update-prepare.mjs";
|
|
7
12
|
import { locateCheckpoint } from "./npm-update-recovery.mjs";
|
|
8
13
|
import { runProcess } from "./npm-update-process.mjs";
|
|
@@ -45,6 +50,7 @@ export async function filingEvidence(api, proposal, config, cwd) {
|
|
|
45
50
|
"--",
|
|
46
51
|
"package.json",
|
|
47
52
|
"package-lock.json",
|
|
53
|
+
...(Object.hasOwn(proposal, "bunLockSha256") ? ["bun.lock"] : []),
|
|
48
54
|
],
|
|
49
55
|
{
|
|
50
56
|
cwd,
|
|
@@ -63,7 +69,7 @@ export async function filingEvidence(api, proposal, config, cwd) {
|
|
|
63
69
|
);
|
|
64
70
|
return {
|
|
65
71
|
history: {
|
|
66
|
-
command: `git log ${proposal.parent} --max-count=20 -- package.json package-lock.json`,
|
|
72
|
+
command: `git log ${proposal.parent} --max-count=20 -- package.json package-lock.json${proposalFileNames(proposal).includes("bun.lock") ? " bun.lock" : ""}`,
|
|
67
73
|
result: history.stdout.toString() || "No matching history.",
|
|
68
74
|
},
|
|
69
75
|
search: { query, total: search.total_count },
|
|
@@ -0,0 +1,189 @@
|
|
|
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
|
+
/** Optional Bun coherence uses a pinned native tool and the original committed lock. */
|
|
5
|
+
import { existsSync, lstatSync, realpathSync, readFileSync } from "node:fs";
|
|
6
|
+
import { isAbsolute, join, resolve } from "node:path";
|
|
7
|
+
import { required } from "./npm-update-invariants.mjs";
|
|
8
|
+
import { sha256 } from "./github-attestation-verifier.mjs";
|
|
9
|
+
import { runProcess, readBytes } from "./npm-update-process-core.mjs";
|
|
10
|
+
import { binaryDigest } from "./npm-update-worker-lifecycle.mjs";
|
|
11
|
+
import { canonicalJson } from "./automation-provenance-contract.mjs";
|
|
12
|
+
|
|
13
|
+
/** An untracked, removed, aliased or changed original lock never becomes authority. */
|
|
14
|
+
export async function committedBun(cwd, command, proposal) {
|
|
15
|
+
const entry = (
|
|
16
|
+
await command(["ls-tree", "HEAD", "--", "bun.lock"])
|
|
17
|
+
).stdout.toString();
|
|
18
|
+
const path = join(cwd, "bun.lock");
|
|
19
|
+
if (!entry) {
|
|
20
|
+
required(!existsSync(path), "unexpected Bun lock path");
|
|
21
|
+
try {
|
|
22
|
+
lstatSync(path);
|
|
23
|
+
required(false, "unexpected Bun lock alias");
|
|
24
|
+
} catch (error) {
|
|
25
|
+
if (error.code !== "ENOENT") throw error;
|
|
26
|
+
}
|
|
27
|
+
required(
|
|
28
|
+
!proposal || !Object.hasOwn(proposal, "bunLockSha256"),
|
|
29
|
+
"original Bun lock was removed"
|
|
30
|
+
);
|
|
31
|
+
return undefined;
|
|
32
|
+
}
|
|
33
|
+
required(
|
|
34
|
+
/^100644 blob [a-f0-9]{40}\tbun\.lock\n$/.test(entry),
|
|
35
|
+
"regular committed Bun lock required"
|
|
36
|
+
);
|
|
37
|
+
const stat = lstatSync(path);
|
|
38
|
+
required(
|
|
39
|
+
stat.isFile() &&
|
|
40
|
+
!stat.isSymbolicLink() &&
|
|
41
|
+
stat.nlink === 1 &&
|
|
42
|
+
realpathSync(path) === path,
|
|
43
|
+
"Bun lock path is aliased"
|
|
44
|
+
);
|
|
45
|
+
const bytes = (await command(["show", "HEAD:bun.lock"])).stdout;
|
|
46
|
+
const digest = sha256(bytes);
|
|
47
|
+
required(
|
|
48
|
+
proposal
|
|
49
|
+
? proposal.bunLockSha256 === digest
|
|
50
|
+
: readBytes(path, 1_048_576, false).equals(bytes),
|
|
51
|
+
"original Bun lock bytes differ"
|
|
52
|
+
);
|
|
53
|
+
return bytes.toString("utf8");
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** Recheck the same regular executable identity and bytes around every native call. */
|
|
57
|
+
function toolIdentity(path) {
|
|
58
|
+
const stat = lstatSync(path);
|
|
59
|
+
required(
|
|
60
|
+
isAbsolute(path) &&
|
|
61
|
+
stat.isFile() &&
|
|
62
|
+
!stat.isSymbolicLink() &&
|
|
63
|
+
stat.nlink === 1 &&
|
|
64
|
+
realpathSync(path) === path,
|
|
65
|
+
"Bun executable is aliased"
|
|
66
|
+
);
|
|
67
|
+
return `${stat.dev}:${stat.ino}:${stat.uid}:${stat.mode}:${binaryDigest(path)}`;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** Only the original trusted PATH may select a supported native Bun installation. */
|
|
71
|
+
export async function qualifiedBun(env, { executable, deadline } = {}) {
|
|
72
|
+
const path =
|
|
73
|
+
executable ??
|
|
74
|
+
(env.PATH ?? "")
|
|
75
|
+
.split(":")
|
|
76
|
+
.filter(isAbsolute)
|
|
77
|
+
.map(directory => join(directory, "bun"))
|
|
78
|
+
.find(existsSync);
|
|
79
|
+
required(typeof path === "string", "native Bun is unavailable");
|
|
80
|
+
const absolute = resolve(path);
|
|
81
|
+
const identity = toolIdentity(absolute);
|
|
82
|
+
const invoke = async (args, options = {}) => {
|
|
83
|
+
required(
|
|
84
|
+
toolIdentity(absolute) === identity,
|
|
85
|
+
"Bun executable identity changed"
|
|
86
|
+
);
|
|
87
|
+
const timeout = Math.min(
|
|
88
|
+
options.timeout ?? 120_000,
|
|
89
|
+
120_000,
|
|
90
|
+
deadline === undefined ? 120_000 : deadline - Date.now()
|
|
91
|
+
);
|
|
92
|
+
required(
|
|
93
|
+
Number.isSafeInteger(timeout) && timeout > 0,
|
|
94
|
+
"original Bun operation deadline expired"
|
|
95
|
+
);
|
|
96
|
+
const result = await runProcess(absolute, args, {
|
|
97
|
+
...options,
|
|
98
|
+
env: { ...env, BUN_FEATURE_FLAG_DISABLE_NATIVE_DEPENDENCY_LINKER: "1" },
|
|
99
|
+
timeout,
|
|
100
|
+
});
|
|
101
|
+
required(
|
|
102
|
+
toolIdentity(absolute) === identity,
|
|
103
|
+
"Bun executable changed during execution"
|
|
104
|
+
);
|
|
105
|
+
return result;
|
|
106
|
+
};
|
|
107
|
+
const version = await invoke(["--version"]);
|
|
108
|
+
required(
|
|
109
|
+
version.stdout.toString() === "1.3.8\n" && !version.stderr.length,
|
|
110
|
+
"Bun1.3.8 is required"
|
|
111
|
+
);
|
|
112
|
+
return invoke;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** Native JSONC parsing is data-only; every Bun package must match a public npm lock identity. */
|
|
116
|
+
export async function verifyBunLock(bun, app, options = {}) {
|
|
117
|
+
const text = readBytes(join(app, "bun.lock"), 1_048_576, false);
|
|
118
|
+
const parsed = await bun(
|
|
119
|
+
[
|
|
120
|
+
"--eval",
|
|
121
|
+
"process.stdout.write(JSON.stringify(Bun.JSONC.parse(await Bun.stdin.text())))",
|
|
122
|
+
],
|
|
123
|
+
{ ...options, cwd: app, input: text, maximum: 1_048_576 }
|
|
124
|
+
);
|
|
125
|
+
const value = JSON.parse(parsed.stdout.toString());
|
|
126
|
+
const manifest = JSON.parse(readFileSync(join(app, "package.json"), "utf8"));
|
|
127
|
+
const npm = JSON.parse(readFileSync(join(app, "package-lock.json"), "utf8"));
|
|
128
|
+
required(
|
|
129
|
+
value.lockfileVersion === 1 &&
|
|
130
|
+
Object.keys(value.workspaces ?? {}).join() === "" &&
|
|
131
|
+
value.workspaces?.[""]?.name === manifest.name,
|
|
132
|
+
"unsupported Bun root lock"
|
|
133
|
+
);
|
|
134
|
+
for (const section of [
|
|
135
|
+
"dependencies",
|
|
136
|
+
"devDependencies",
|
|
137
|
+
"optionalDependencies",
|
|
138
|
+
"peerDependencies",
|
|
139
|
+
])
|
|
140
|
+
required(
|
|
141
|
+
canonicalJson(value.workspaces[""][section] ?? {}) ===
|
|
142
|
+
canonicalJson(manifest[section] ?? {}),
|
|
143
|
+
"Bun root selections differ"
|
|
144
|
+
);
|
|
145
|
+
required(
|
|
146
|
+
value.packages &&
|
|
147
|
+
typeof value.packages === "object" &&
|
|
148
|
+
!Array.isArray(value.packages),
|
|
149
|
+
"Bun packages are absent"
|
|
150
|
+
);
|
|
151
|
+
const nodes = Object.entries(npm.packages).filter(([name]) => name !== "");
|
|
152
|
+
for (const row of Object.values(value.packages)) {
|
|
153
|
+
required(
|
|
154
|
+
Array.isArray(row) && row.length === 4 && typeof row[0] === "string",
|
|
155
|
+
"unsupported Bun package record"
|
|
156
|
+
);
|
|
157
|
+
const matching = nodes.some(
|
|
158
|
+
([path, node]) =>
|
|
159
|
+
row[0] === `${path.split("node_modules/").at(-1)}@${node.version}` &&
|
|
160
|
+
row[3] === node.integrity &&
|
|
161
|
+
(row[1] === "" || row[1] === node.resolved)
|
|
162
|
+
);
|
|
163
|
+
required(matching, "Bun package identity differs from public npm lock");
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/** Frozen native installation must preserve every prepared manifest and lock byte. */
|
|
168
|
+
export async function frozenBun(bun, app, files, options = {}) {
|
|
169
|
+
await verifyBunLock(bun, app, options);
|
|
170
|
+
const before = files.map(file =>
|
|
171
|
+
readBytes(join(app, file), 1_048_576, false)
|
|
172
|
+
);
|
|
173
|
+
await bun(
|
|
174
|
+
[
|
|
175
|
+
"install",
|
|
176
|
+
"--frozen-lockfile",
|
|
177
|
+
"--ignore-scripts",
|
|
178
|
+
"--registry",
|
|
179
|
+
"https://registry.npmjs.org/",
|
|
180
|
+
],
|
|
181
|
+
{ ...options, cwd: app }
|
|
182
|
+
);
|
|
183
|
+
required(
|
|
184
|
+
files.every((file, index) =>
|
|
185
|
+
readBytes(join(app, file), 1_048_576, false).equals(before[index])
|
|
186
|
+
),
|
|
187
|
+
"frozen Bun rewrote manifest or lock"
|
|
188
|
+
);
|
|
189
|
+
}
|
|
@@ -28,7 +28,10 @@ import {
|
|
|
28
28
|
import { sha256 } from "./github-attestation-verifier.mjs";
|
|
29
29
|
import { verifyStaleOriginProvider } from "./github-attestation-provider.mjs";
|
|
30
30
|
import { verifyHistoricalDescriptor } from "./github-attestation-recovery.mjs";
|
|
31
|
-
import {
|
|
31
|
+
import {
|
|
32
|
+
DESCRIPTOR_KEYS,
|
|
33
|
+
optionalLockFields,
|
|
34
|
+
} from "./automation-provenance-contract.mjs";
|
|
32
35
|
|
|
33
36
|
/** Only complete, digest-checked transport is considered; incomplete chunks grant nothing. */
|
|
34
37
|
export function completeCheckpoints(comments) {
|
|
@@ -168,11 +171,11 @@ async function inspectOrigin(api, issue, payload, oldConfig, currentConfig) {
|
|
|
168
171
|
}
|
|
169
172
|
|
|
170
173
|
/** Descriptor fields preserve the complete original immutable proposal and claim. */
|
|
171
|
-
function originalDescriptor(checkpoint, old, allocation, authority) {
|
|
174
|
+
export function originalDescriptor(checkpoint, old, allocation, authority) {
|
|
172
175
|
const preview = checkpoint.payload.preview;
|
|
173
176
|
keys(preview, ["descriptor", "message", "epoch"]);
|
|
174
177
|
const d = preview.descriptor;
|
|
175
|
-
keys(d, DESCRIPTOR_KEYS);
|
|
178
|
+
keys(d, [...DESCRIPTOR_KEYS, ...optionalLockFields(old)]);
|
|
176
179
|
required(
|
|
177
180
|
d.version === 1 &&
|
|
178
181
|
d.parent === old.parent &&
|
|
@@ -183,6 +186,7 @@ function originalDescriptor(checkpoint, old, allocation, authority) {
|
|
|
183
186
|
d.proposalKey === old.bindingKey &&
|
|
184
187
|
d.policySha256 === sha256(canonicalJson(authority)) &&
|
|
185
188
|
canonicalJson(d.files) === canonicalJson(old.hashes) &&
|
|
189
|
+
d.bunLockSha256 === old.bunLockSha256 &&
|
|
186
190
|
canonicalJson(d.updates) === canonicalJson(old.updates) &&
|
|
187
191
|
typeof preview.message === "string" &&
|
|
188
192
|
sha256(preview.message) === d.messageSha256 &&
|
|
@@ -105,7 +105,12 @@ export function assertManualCancellation(
|
|
|
105
105
|
/** The closed subject preserves one stale origin and one exact replacement, with no published destination. */
|
|
106
106
|
export function validateCancellation(value, expected) {
|
|
107
107
|
keys(value, RECORD_KEYS);
|
|
108
|
-
keys(
|
|
108
|
+
keys(
|
|
109
|
+
value.proposalHashes,
|
|
110
|
+
Object.hasOwn(expected.proposalHashes, "bun.lock")
|
|
111
|
+
? ["bun.lock", ...FILES]
|
|
112
|
+
: FILES
|
|
113
|
+
);
|
|
109
114
|
keys(value.destination, ["branch", "expectedBranchHead", "prNumber"]);
|
|
110
115
|
required(
|
|
111
116
|
value.version === 1 &&
|
|
@@ -10,6 +10,11 @@ import { required, keys } from "./npm-update-invariants.mjs";
|
|
|
10
10
|
export { UpdaterError, required, keys } from "./npm-update-invariants.mjs";
|
|
11
11
|
import { canonicalJson } from "../lisa-automation-provenance.mjs";
|
|
12
12
|
import { sha256 } from "./github-attestation-verifier.mjs";
|
|
13
|
+
import {
|
|
14
|
+
optionalLockFields,
|
|
15
|
+
proposalFileNames,
|
|
16
|
+
} from "./automation-provenance-contract.mjs";
|
|
17
|
+
export { proposalFileNames } from "./automation-provenance-contract.mjs";
|
|
13
18
|
|
|
14
19
|
export const FILES = ["package-lock.json", "package.json"];
|
|
15
20
|
export const SECTIONS = [
|
|
@@ -207,9 +212,17 @@ export function selectionKey(policy, updates) {
|
|
|
207
212
|
}
|
|
208
213
|
|
|
209
214
|
/** Recovery binds policy as well as versions; the hook's signed proposal key is separate. */
|
|
210
|
-
export function proposalFrom(
|
|
215
|
+
export function proposalFrom(
|
|
216
|
+
policy,
|
|
217
|
+
parent,
|
|
218
|
+
before,
|
|
219
|
+
files,
|
|
220
|
+
updates,
|
|
221
|
+
bunLockSha256
|
|
222
|
+
) {
|
|
211
223
|
required(OBJECT.test(parent), "unsupported Git parent or object format");
|
|
212
|
-
|
|
224
|
+
const optional = bunLockSha256 === undefined ? {} : { bunLockSha256 };
|
|
225
|
+
const names = proposalFileNames({ files, ...optional });
|
|
213
226
|
for (const value of Object.values(files))
|
|
214
227
|
required(
|
|
215
228
|
typeof value === "string" && Buffer.byteLength(value) <= 1_048_576,
|
|
@@ -228,6 +241,7 @@ export function proposalFrom(policy, parent, before, files, updates) {
|
|
|
228
241
|
parent,
|
|
229
242
|
policySha256: sha256(canonicalJson(policy)),
|
|
230
243
|
updates: ordered,
|
|
244
|
+
...optional,
|
|
231
245
|
};
|
|
232
246
|
return {
|
|
233
247
|
version: 1,
|
|
@@ -235,11 +249,16 @@ export function proposalFrom(policy, parent, before, files, updates) {
|
|
|
235
249
|
selectionKey: selectionKey(policy, ordered),
|
|
236
250
|
key: sha256(canonicalJson(identity)),
|
|
237
251
|
bindingKey: sha256(
|
|
238
|
-
canonicalJson({
|
|
252
|
+
canonicalJson({
|
|
253
|
+
repository: policy.repository,
|
|
254
|
+
parent,
|
|
255
|
+
updates: ordered,
|
|
256
|
+
...optional,
|
|
257
|
+
})
|
|
239
258
|
),
|
|
240
259
|
before,
|
|
241
260
|
files,
|
|
242
|
-
hashes: Object.fromEntries(
|
|
261
|
+
hashes: Object.fromEntries(names.map(file => [file, sha256(files[file])])),
|
|
243
262
|
};
|
|
244
263
|
}
|
|
245
264
|
|
|
@@ -260,13 +279,15 @@ export function validateProposal(value, policy) {
|
|
|
260
279
|
"before",
|
|
261
280
|
"files",
|
|
262
281
|
"hashes",
|
|
282
|
+
...optionalLockFields(value),
|
|
263
283
|
]);
|
|
264
284
|
const expected = proposalFrom(
|
|
265
285
|
policy,
|
|
266
286
|
value.parent,
|
|
267
287
|
value.before,
|
|
268
288
|
value.files,
|
|
269
|
-
value.updates
|
|
289
|
+
value.updates,
|
|
290
|
+
value.bunLockSha256
|
|
270
291
|
);
|
|
271
292
|
required(
|
|
272
293
|
canonicalJson(value) === canonicalJson(expected),
|
|
@@ -10,7 +10,7 @@ import { writeFileSync } from "node:fs";
|
|
|
10
10
|
import { join } from "node:path";
|
|
11
11
|
import { canonicalJson } from "../lisa-automation-provenance.mjs";
|
|
12
12
|
import {
|
|
13
|
-
|
|
13
|
+
proposalFileNames,
|
|
14
14
|
OBJECT,
|
|
15
15
|
required,
|
|
16
16
|
validateProposal,
|
|
@@ -95,7 +95,7 @@ export async function descriptorFor({
|
|
|
95
95
|
await baseline(cwd, env, proposal);
|
|
96
96
|
const staged = { ...env, GIT_INDEX_FILE: join(root, "index") };
|
|
97
97
|
await git(cwd, staged, ["read-tree", proposal.parent]);
|
|
98
|
-
for (const file of
|
|
98
|
+
for (const file of proposalFileNames(proposal)) {
|
|
99
99
|
const blob = (
|
|
100
100
|
await git(
|
|
101
101
|
cwd,
|
|
@@ -125,6 +125,9 @@ export async function descriptorFor({
|
|
|
125
125
|
author: identity,
|
|
126
126
|
committer: identity,
|
|
127
127
|
files: proposal.hashes,
|
|
128
|
+
...(proposal.bunLockSha256 === undefined
|
|
129
|
+
? {}
|
|
130
|
+
: { bunLockSha256: proposal.bunLockSha256 }),
|
|
128
131
|
messageSha256: sha256(message),
|
|
129
132
|
updates: proposal.updates,
|
|
130
133
|
proposalKey: proposal.bindingKey,
|
|
@@ -161,9 +164,9 @@ async function stageProposal(context, env) {
|
|
|
161
164
|
await withStage("gate-attach", () =>
|
|
162
165
|
executeCanonicalHelper(context, "stage-read", "attach-branch")
|
|
163
166
|
);
|
|
164
|
-
for (const file of
|
|
167
|
+
for (const file of proposalFileNames(context.proposal))
|
|
165
168
|
writeFileSync(join(cwd, file), proposal.files[file]);
|
|
166
|
-
await command(["add", "--", ...
|
|
169
|
+
await command(["add", "--", ...proposalFileNames(context.proposal)]);
|
|
167
170
|
return branch;
|
|
168
171
|
}
|
|
169
172
|
|
|
@@ -192,7 +195,7 @@ async function committedProposal(
|
|
|
192
195
|
.split("\n")
|
|
193
196
|
.sort();
|
|
194
197
|
required(
|
|
195
|
-
changed.join("\n") ===
|
|
198
|
+
changed.join("\n") === proposalFileNames(proposal).join("\n"),
|
|
196
199
|
"committed diff includes foreign paths"
|
|
197
200
|
);
|
|
198
201
|
const ranges = (
|
|
@@ -7,7 +7,7 @@ import { assertPinnedVerifier } from "./github-attestation-verifier.mjs";
|
|
|
7
7
|
import { required } from "./npm-update-contract.mjs";
|
|
8
8
|
import { runProcess } from "./npm-update-process.mjs";
|
|
9
9
|
import { gitObjectId } from "./npm-update-object.mjs";
|
|
10
|
-
import {
|
|
10
|
+
import { proposalFileNames } from "./npm-update-contract.mjs";
|
|
11
11
|
import { executeCanonicalHelper } from "./npm-update-controller-factory.mjs";
|
|
12
12
|
/** Explicit minimum gh identity and token; no caller/candidate executable override. */
|
|
13
13
|
const HOST_OPTION = "--hostname";
|
|
@@ -179,7 +179,7 @@ export class GitHub {
|
|
|
179
179
|
async gitCommit(proposal, descriptor, commit, authorize, cwd) {
|
|
180
180
|
const path = `repos/${this.policy.repository}/git`;
|
|
181
181
|
const tree = [];
|
|
182
|
-
for (const file of
|
|
182
|
+
for (const file of proposalFileNames(proposal)) {
|
|
183
183
|
await authorize();
|
|
184
184
|
const bytes = Buffer.from(proposal.files[file]);
|
|
185
185
|
const blob = await this.request(`${path}/blobs`, "POST", {
|
|
@@ -29,9 +29,8 @@ const COMMON = [
|
|
|
29
29
|
"lib/github-attestation-provider.mjs",
|
|
30
30
|
"lib/github-attestation-recovery.mjs",
|
|
31
31
|
"lib/automation-provenance-contract.mjs",
|
|
32
|
+
"lib/automation-provenance-files.mjs",
|
|
32
33
|
"lib/automation-provenance-local.mjs",
|
|
33
|
-
];
|
|
34
|
-
const RAILS = [
|
|
35
34
|
"lisa-clean-git-env.sh",
|
|
36
35
|
"lisa-scratch-run.sh",
|
|
37
36
|
"check-threshold-ratchet.mjs",
|
|
@@ -42,6 +41,7 @@ const PRODUCER = [
|
|
|
42
41
|
"allocate",
|
|
43
42
|
"authorization",
|
|
44
43
|
"broker-client",
|
|
44
|
+
"bun",
|
|
45
45
|
"cancel-origin",
|
|
46
46
|
"cancellation",
|
|
47
47
|
"cancellation-proof",
|
|
@@ -125,7 +125,6 @@ export function managedTemplateMembers() {
|
|
|
125
125
|
"npm-updater-gate.Dockerfile",
|
|
126
126
|
...PRODUCER.map(name => `lib/npm-update-${name}.mjs`),
|
|
127
127
|
].map(member => [member, `all/copy-overwrite/scripts/${member}`]),
|
|
128
|
-
|
|
129
|
-
["lisa-mutation.sh", "rails/copy-contents/scripts/lisa-mutation.sh"],
|
|
128
|
+
["lisa-mutation.sh", "all/copy-contents/scripts/lisa-mutation.sh"],
|
|
130
129
|
]);
|
|
131
130
|
}
|