@codyswann/lisa 4.70.3 → 4.71.0
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/automation-provenance-contract.mjs +174 -0
- package/all/copy-overwrite/scripts/lib/automation-provenance-local.mjs +279 -0
- package/all/copy-overwrite/scripts/lib/gate-failure-diagnosis.mjs +17 -7
- package/all/copy-overwrite/scripts/lib/github-attestation-provider.mjs +241 -0
- package/all/copy-overwrite/scripts/lib/github-attestation-recovery.mjs +283 -0
- package/all/copy-overwrite/scripts/lib/github-attestation-verifier.mjs +244 -0
- package/all/copy-overwrite/scripts/lib/history-secret-evidence-shape.mjs +188 -0
- package/all/copy-overwrite/scripts/lib/history-secret-evidence.mjs +271 -0
- package/all/copy-overwrite/scripts/lib/history-secret-scanner.mjs +6 -296
- package/all/copy-overwrite/scripts/lib/npm-update-allocate.mjs +289 -0
- package/all/copy-overwrite/scripts/lib/npm-update-authorization.mjs +235 -0
- package/all/copy-overwrite/scripts/lib/npm-update-broker-client.mjs +92 -0
- package/all/copy-overwrite/scripts/lib/npm-update-checkpoint.mjs +241 -0
- package/all/copy-overwrite/scripts/lib/npm-update-contract.mjs +278 -0
- package/all/copy-overwrite/scripts/lib/npm-update-controller-broker.mjs +234 -0
- package/all/copy-overwrite/scripts/lib/npm-update-controller-factory.mjs +278 -0
- package/all/copy-overwrite/scripts/lib/npm-update-controller-recipe.mjs +135 -0
- package/all/copy-overwrite/scripts/lib/npm-update-execution-adapter.mjs +292 -0
- package/all/copy-overwrite/scripts/lib/npm-update-gate-hooks.mjs +214 -0
- package/all/copy-overwrite/scripts/lib/npm-update-gate-install.mjs +267 -0
- package/all/copy-overwrite/scripts/lib/npm-update-gate-proof.mjs +99 -0
- package/all/copy-overwrite/scripts/lib/npm-update-gate.mjs +300 -0
- package/all/copy-overwrite/scripts/lib/npm-update-gh-dispatch.mjs +182 -0
- package/all/copy-overwrite/scripts/lib/npm-update-gh-grants.mjs +156 -0
- package/all/copy-overwrite/scripts/lib/npm-update-gh-requests.mjs +221 -0
- package/all/copy-overwrite/scripts/lib/npm-update-github.mjs +214 -0
- package/all/copy-overwrite/scripts/lib/npm-update-helper-graph.mjs +238 -0
- package/all/copy-overwrite/scripts/lib/npm-update-helper-inventory.mjs +126 -0
- package/all/copy-overwrite/scripts/lib/npm-update-helper.mjs +292 -0
- package/all/copy-overwrite/scripts/lib/npm-update-hook-installation.mjs +116 -0
- package/all/copy-overwrite/scripts/lib/npm-update-hook-preload.mjs +153 -0
- package/all/copy-overwrite/scripts/lib/npm-update-hook-provider.mjs +207 -0
- package/all/copy-overwrite/scripts/lib/npm-update-hook-read-client.mjs +164 -0
- package/all/copy-overwrite/scripts/lib/npm-update-hosted-gate.mjs +272 -0
- package/all/copy-overwrite/scripts/lib/npm-update-hosted-hook.mjs +157 -0
- package/all/copy-overwrite/scripts/lib/npm-update-invariants.mjs +28 -0
- package/all/copy-overwrite/scripts/lib/npm-update-isolation.mjs +11 -0
- package/all/copy-overwrite/scripts/lib/npm-update-leaf-contract.mjs +171 -0
- package/all/copy-overwrite/scripts/lib/npm-update-leaf.mjs +194 -0
- package/all/copy-overwrite/scripts/lib/npm-update-native-process.mjs +155 -0
- package/all/copy-overwrite/scripts/lib/npm-update-npm.mjs +190 -0
- package/all/copy-overwrite/scripts/lib/npm-update-object.mjs +112 -0
- package/all/copy-overwrite/scripts/lib/npm-update-orchestrator.mjs +273 -0
- package/all/copy-overwrite/scripts/lib/npm-update-owner.mjs +292 -0
- package/all/copy-overwrite/scripts/lib/npm-update-prepare.mjs +263 -0
- package/all/copy-overwrite/scripts/lib/npm-update-process-core.mjs +179 -0
- package/all/copy-overwrite/scripts/lib/npm-update-process.mjs +16 -0
- package/all/copy-overwrite/scripts/lib/npm-update-publication.mjs +258 -0
- package/all/copy-overwrite/scripts/lib/npm-update-publish.mjs +258 -0
- package/all/copy-overwrite/scripts/lib/npm-update-quality.mjs +153 -0
- package/all/copy-overwrite/scripts/lib/npm-update-recovery.mjs +264 -0
- package/all/copy-overwrite/scripts/lib/npm-update-runtime-archive.mjs +204 -0
- package/all/copy-overwrite/scripts/lib/npm-update-runtime-graph.mjs +185 -0
- package/all/copy-overwrite/scripts/lib/npm-update-runtime-transport.mjs +221 -0
- package/all/copy-overwrite/scripts/lib/npm-update-runtime.mjs +284 -0
- package/all/copy-overwrite/scripts/lib/npm-update-tool-launcher.mjs +287 -0
- package/all/copy-overwrite/scripts/lib/npm-update-worker-environment.mjs +161 -0
- package/all/copy-overwrite/scripts/lib/npm-update-worker-inspection.mjs +142 -0
- package/all/copy-overwrite/scripts/lib/npm-update-worker-lifecycle.mjs +256 -0
- package/all/copy-overwrite/scripts/lib/npm-update-worker-policy.mjs +266 -0
- package/all/copy-overwrite/scripts/lib/process-tree-runner.mjs +230 -36
- package/all/copy-overwrite/scripts/lisa-automation-provenance.mjs +161 -0
- package/all/copy-overwrite/scripts/lisa-history-secrets.mjs +8 -1
- package/all/copy-overwrite/scripts/lisa-hooks/block-no-verify.sh +89 -5
- package/all/copy-overwrite/scripts/lisa-npm-updater.mjs +204 -0
- package/all/copy-overwrite/scripts/lisa-run-gates.mjs +48 -123
- package/all/copy-overwrite/scripts/lisa-work-item.mjs +107 -41
- package/all/copy-overwrite/scripts/npm-updater-gate-runtime.json +1 -0
- package/all/copy-overwrite/scripts/npm-updater-gate-supervisor.c +121 -0
- package/all/copy-overwrite/scripts/npm-updater-gate.Dockerfile +49 -0
- package/all/copy-overwrite/scripts/npm-updater-helper-graph.json +1085 -0
- package/dist/codex/scripts/block-no-verify.sh +89 -5
- package/dist/core/lisa-owned-hash-ledger.d.ts.map +1 -1
- package/dist/core/lisa-owned-hash-ledger.js +620 -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 +165 -13
- package/dist/core/upstream-evidence-manifest.js.map +1 -1
- package/dist/opencode/plugin-templates/block-no-verify.sh +89 -5
- 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/block-no-verify.agy.sh +89 -5
- package/plugins/lisa/hooks/block-no-verify.sh +89 -5
- package/plugins/lisa-agy/hooks/block-no-verify.agy.sh +89 -5
- 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/block-no-verify.sh +89 -5
- package/plugins/lisa-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-cursor/hooks/block-no-verify.sh +89 -5
- 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/block-no-verify.agy.sh +89 -5
- package/plugins/src/base/hooks/block-no-verify.sh +89 -5
- package/scripts/check-shipped-surface-removals.mjs +96 -24
- package/scripts/generate-npm-updater-helper-graph.mjs +184 -0
- package/scripts/two-channel-couplings.json +31 -7
- package/typescript/copy-contents/.husky/commit-msg +18 -0
- package/typescript/copy-overwrite/.prettierignore +2 -0
|
@@ -0,0 +1,214 @@
|
|
|
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
|
+
/** Original hook transport distinguishes artifact audit from actual remote state. */
|
|
5
|
+
import { canonicalJson } from "../lisa-automation-provenance.mjs";
|
|
6
|
+
import { required, OBJECT, keys } from "./npm-update-contract.mjs";
|
|
7
|
+
import { sha256 } from "./github-attestation-verifier.mjs";
|
|
8
|
+
export { managedTemplateMembers } from "./npm-update-helper-graph.mjs";
|
|
9
|
+
export {
|
|
10
|
+
originalPrePush,
|
|
11
|
+
originalHookInstallation,
|
|
12
|
+
} from "./npm-update-hook-installation.mjs";
|
|
13
|
+
|
|
14
|
+
/** Private parent-created tuples remain data; their configuration and helper authority is qualified separately. */
|
|
15
|
+
function configuredTuple(route, graph) {
|
|
16
|
+
keys(route, ["tool", "args", "cwd", "entry", "configuration", "environment"]);
|
|
17
|
+
required(
|
|
18
|
+
["node", "shell", "bash"].includes(route.tool) &&
|
|
19
|
+
typeof route.cwd === "string" &&
|
|
20
|
+
route.cwd.startsWith("/") &&
|
|
21
|
+
!route.cwd.split("/").includes(".."),
|
|
22
|
+
"invalid configured controller tuple"
|
|
23
|
+
);
|
|
24
|
+
required(
|
|
25
|
+
Array.isArray(route.args) &&
|
|
26
|
+
route.args.length <= 512 &&
|
|
27
|
+
route.args.every(
|
|
28
|
+
value =>
|
|
29
|
+
typeof value === "string" &&
|
|
30
|
+
!value.includes("\0") &&
|
|
31
|
+
Buffer.byteLength(value) <= 65_536
|
|
32
|
+
) &&
|
|
33
|
+
Buffer.byteLength(canonicalJson(route.args)) <= 262_144,
|
|
34
|
+
"unbounded configured command tuple"
|
|
35
|
+
);
|
|
36
|
+
required(
|
|
37
|
+
route.entry === null ||
|
|
38
|
+
(typeof route.entry === "string" && Object.hasOwn(graph, route.entry)),
|
|
39
|
+
"configured helper entry is unqualified"
|
|
40
|
+
);
|
|
41
|
+
if (route.tool === "node")
|
|
42
|
+
required(
|
|
43
|
+
route.entry !== null &&
|
|
44
|
+
route.args[0] === "--input-type=module" &&
|
|
45
|
+
route.args[1] === "-e" &&
|
|
46
|
+
typeof route.args[2] === "string",
|
|
47
|
+
"unknown configured inline Node route"
|
|
48
|
+
);
|
|
49
|
+
else
|
|
50
|
+
required(
|
|
51
|
+
route.args.length === 2 && route.args[0] === "-c",
|
|
52
|
+
"unknown configured native shell route"
|
|
53
|
+
);
|
|
54
|
+
required(
|
|
55
|
+
route.configuration &&
|
|
56
|
+
typeof route.configuration === "object" &&
|
|
57
|
+
!Array.isArray(route.configuration) &&
|
|
58
|
+
Object.keys(route.configuration).length > 0 &&
|
|
59
|
+
Object.keys(route.configuration).length <= 16,
|
|
60
|
+
"configured command authority is missing or unbounded"
|
|
61
|
+
);
|
|
62
|
+
required(
|
|
63
|
+
route.environment &&
|
|
64
|
+
typeof route.environment === "object" &&
|
|
65
|
+
!Array.isArray(route.environment) &&
|
|
66
|
+
Object.keys(route.environment).length <= 64 &&
|
|
67
|
+
typeof route.environment.PATH === "string",
|
|
68
|
+
"configured environment is unresolved"
|
|
69
|
+
);
|
|
70
|
+
required(
|
|
71
|
+
Object.entries(route.environment).every(
|
|
72
|
+
([name, value]) =>
|
|
73
|
+
/^[A-Z][A-Z0-9_]*$/.test(name) &&
|
|
74
|
+
typeof value === "string" &&
|
|
75
|
+
!value.includes("\0") &&
|
|
76
|
+
Buffer.byteLength(value) <= 4096
|
|
77
|
+
),
|
|
78
|
+
"invalid configured environment tuple"
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** Helper imports and every included configuration byte form one bounded current closure. */
|
|
83
|
+
export function controllerGraph(graph, routes = []) {
|
|
84
|
+
required(
|
|
85
|
+
Array.isArray(routes) && routes.length <= 64,
|
|
86
|
+
"configured command inventory exceeds bound"
|
|
87
|
+
);
|
|
88
|
+
const expected = { ...graph };
|
|
89
|
+
for (const route of routes) {
|
|
90
|
+
configuredTuple(route, graph);
|
|
91
|
+
for (const [path, hash] of Object.entries(route.configuration)) {
|
|
92
|
+
required(
|
|
93
|
+
path.startsWith("/") &&
|
|
94
|
+
/^[a-f0-9]{64}$/.test(hash) &&
|
|
95
|
+
(!Object.hasOwn(expected, path) || expected[path] === hash),
|
|
96
|
+
"configured configuration graph differs"
|
|
97
|
+
);
|
|
98
|
+
expected[path] = hash;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
required(
|
|
102
|
+
Object.keys(expected).length <= 128,
|
|
103
|
+
"configured helper/configuration closure exceeds bound"
|
|
104
|
+
);
|
|
105
|
+
return expected;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** Exact tuples cannot grant general eval or native shell authority to an unmatched invocation. */
|
|
109
|
+
export function configuredRoute(tool, args, routes, cwd, environment) {
|
|
110
|
+
const matches = routes.filter(
|
|
111
|
+
route =>
|
|
112
|
+
route.tool === tool &&
|
|
113
|
+
route.cwd === cwd &&
|
|
114
|
+
canonicalJson(route.args) === canonicalJson(args)
|
|
115
|
+
);
|
|
116
|
+
required(matches.length <= 1, "ambiguous configured controller command");
|
|
117
|
+
if (!matches.length) return false;
|
|
118
|
+
required(
|
|
119
|
+
![
|
|
120
|
+
"NODE_OPTIONS",
|
|
121
|
+
"NODE_PATH",
|
|
122
|
+
"ENV",
|
|
123
|
+
"BASH_ENV",
|
|
124
|
+
"LD_PRELOAD",
|
|
125
|
+
"LD_LIBRARY_PATH",
|
|
126
|
+
"RUBYOPT",
|
|
127
|
+
"GIT_EXEC_PATH",
|
|
128
|
+
].some(name => environment[name]),
|
|
129
|
+
"unqualified configured environment loader"
|
|
130
|
+
);
|
|
131
|
+
required(
|
|
132
|
+
Object.entries(matches[0].environment).every(
|
|
133
|
+
([name, value]) => environment[name] === value
|
|
134
|
+
),
|
|
135
|
+
"configured controller environment changed"
|
|
136
|
+
);
|
|
137
|
+
return true;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/** A publication has one full audit and one accurately observed destination stream. */
|
|
141
|
+
export function gateStreams(branch, commit, parent, destination) {
|
|
142
|
+
required(
|
|
143
|
+
/^lisa\/npm-[a-f0-9]{64}$/.test(branch) &&
|
|
144
|
+
OBJECT.test(commit) &&
|
|
145
|
+
OBJECT.test(parent),
|
|
146
|
+
"invalid gated ref identity"
|
|
147
|
+
);
|
|
148
|
+
required(
|
|
149
|
+
destination === null || destination === commit,
|
|
150
|
+
"foreign publication destination"
|
|
151
|
+
);
|
|
152
|
+
const line = oid =>
|
|
153
|
+
`refs/heads/${branch} ${commit} refs/heads/${branch} ${oid}\n`;
|
|
154
|
+
return {
|
|
155
|
+
audit: { kind: "full-proposal-audit", refs: line(parent), range: [commit] },
|
|
156
|
+
destination: {
|
|
157
|
+
kind:
|
|
158
|
+
destination === null
|
|
159
|
+
? "absent-destination"
|
|
160
|
+
: "existing-destination-no-op",
|
|
161
|
+
refs: line(destination ?? "0".repeat(40)),
|
|
162
|
+
range: destination === null ? [commit] : [],
|
|
163
|
+
},
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/** Empty transport never supplies a full proposal audit or excuses a failed original gate. */
|
|
168
|
+
export function assertGateTransport(
|
|
169
|
+
receipt,
|
|
170
|
+
branch,
|
|
171
|
+
commit,
|
|
172
|
+
parent,
|
|
173
|
+
destination
|
|
174
|
+
) {
|
|
175
|
+
const streams = gateStreams(branch, commit, parent, destination);
|
|
176
|
+
for (const role of ["audit", "destination"]) {
|
|
177
|
+
const actual = receipt[role];
|
|
178
|
+
required(
|
|
179
|
+
actual?.exit === 0 &&
|
|
180
|
+
actual.kind === streams[role].kind &&
|
|
181
|
+
actual.refs === streams[role].refs &&
|
|
182
|
+
canonicalJson(actual.range) === canonicalJson(streams[role].range),
|
|
183
|
+
"original full audit or destination gate differs"
|
|
184
|
+
);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/** A no-op destination cannot substitute for an independently successful complete-range audit. */
|
|
189
|
+
export async function runGateStreams(
|
|
190
|
+
branch,
|
|
191
|
+
commit,
|
|
192
|
+
parent,
|
|
193
|
+
destination,
|
|
194
|
+
execute
|
|
195
|
+
) {
|
|
196
|
+
const streams = gateStreams(branch, commit, parent, destination);
|
|
197
|
+
const receipt = {};
|
|
198
|
+
for (const role of ["audit", "destination"]) {
|
|
199
|
+
const result = await execute(streams[role].refs);
|
|
200
|
+
required(
|
|
201
|
+
result?.code === 0 &&
|
|
202
|
+
Buffer.isBuffer(result.stdout) &&
|
|
203
|
+
Buffer.isBuffer(result.stderr),
|
|
204
|
+
`ordinary ${role} gate failed or native result is invalid`
|
|
205
|
+
);
|
|
206
|
+
receipt[role] = {
|
|
207
|
+
...streams[role],
|
|
208
|
+
exit: result.code,
|
|
209
|
+
logSha256: sha256(Buffer.concat([result.stdout, result.stderr])),
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
assertGateTransport(receipt, branch, commit, parent, destination);
|
|
213
|
+
return receipt;
|
|
214
|
+
}
|
|
@@ -0,0 +1,267 @@
|
|
|
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
|
+
/** Exact proposed installation is sealed only after its isolated namespace is absent. @module npm-updater */
|
|
5
|
+
import { required, keys } from "./npm-update-contract.mjs";
|
|
6
|
+
import {
|
|
7
|
+
chmodSync,
|
|
8
|
+
lstatSync,
|
|
9
|
+
readdirSync,
|
|
10
|
+
realpathSync,
|
|
11
|
+
existsSync,
|
|
12
|
+
} from "node:fs";
|
|
13
|
+
import { join, sep } from "node:path";
|
|
14
|
+
import { executeWorker } from "./npm-update-isolation.mjs";
|
|
15
|
+
import { namespaceRecords } from "./npm-update-orchestrator.mjs";
|
|
16
|
+
import { toolIdentity, validateRuntime } from "./npm-update-runtime.mjs";
|
|
17
|
+
import { sha256 } from "./github-attestation-verifier.mjs";
|
|
18
|
+
import { readBytes } from "./npm-update-process.mjs";
|
|
19
|
+
import { fileURLToPath } from "node:url";
|
|
20
|
+
const HEX = /^[a-f0-9]{64}$/;
|
|
21
|
+
export { validateRuntime } from "./npm-update-runtime.mjs";
|
|
22
|
+
|
|
23
|
+
/** Ordinary npm installs the exact proposal with no lifecycle code or deployment authority. */
|
|
24
|
+
export async function installDependencies(boundary, tools, engineStrict) {
|
|
25
|
+
required(
|
|
26
|
+
boundary.role === "install" && typeof engineStrict === "boolean",
|
|
27
|
+
"installation requires its isolated role and resolved engine policy"
|
|
28
|
+
);
|
|
29
|
+
toolIdentity(tools);
|
|
30
|
+
required(
|
|
31
|
+
namespaceRecords(boundary.root).length === 0,
|
|
32
|
+
"installation requires completed owned namespace recovery"
|
|
33
|
+
);
|
|
34
|
+
const args = [
|
|
35
|
+
tools.npm.path,
|
|
36
|
+
"ci",
|
|
37
|
+
"--ignore-scripts",
|
|
38
|
+
"--no-audit",
|
|
39
|
+
"--no-fund",
|
|
40
|
+
];
|
|
41
|
+
if (engineStrict) args.push("--engine-strict");
|
|
42
|
+
const result = await executeWorker(boundary, {
|
|
43
|
+
command: tools.node.path,
|
|
44
|
+
args,
|
|
45
|
+
});
|
|
46
|
+
required(result.code === 0, "actual proposed npm installation failed");
|
|
47
|
+
required(
|
|
48
|
+
namespaceRecords(boundary.root).length === 0,
|
|
49
|
+
"installer namespace cleanup remains outstanding"
|
|
50
|
+
);
|
|
51
|
+
const sealed = sealDependencies(join(boundary.root, "dependencies"), true);
|
|
52
|
+
return { code: result.code, files: sealed.files, bytes: sealed.bytes };
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Frozen Bundler supports only the committed public-source and exact runtime contract. */
|
|
56
|
+
export function rubyInstallationInputs(inputs, tools) {
|
|
57
|
+
keys(inputs, ["lock", "rubyVersion", "configurationPresent"]);
|
|
58
|
+
required(
|
|
59
|
+
typeof inputs.lock === "string" &&
|
|
60
|
+
Buffer.byteLength(inputs.lock) <= 3_145_728 &&
|
|
61
|
+
!inputs.lock.includes("\0") &&
|
|
62
|
+
typeof inputs.rubyVersion === "string" &&
|
|
63
|
+
inputs.rubyVersion.trim() === "3.4.11",
|
|
64
|
+
"unsupported Ruby installation inputs"
|
|
65
|
+
);
|
|
66
|
+
required(
|
|
67
|
+
inputs.configurationPresent === false,
|
|
68
|
+
"configured alternate Bundler policy is unsupported"
|
|
69
|
+
);
|
|
70
|
+
required(
|
|
71
|
+
tools.ruby?.path === "/usr/local/bin/ruby" &&
|
|
72
|
+
/^ruby 3\.4\.11(?:p\d+)?\s/.test(tools.ruby.version) &&
|
|
73
|
+
tools.bundle?.path === "/usr/local/bin/bundle" &&
|
|
74
|
+
tools.bundle.version === "Bundler version 2.4.10",
|
|
75
|
+
"unsupported genuine Ruby/Bundler identity"
|
|
76
|
+
);
|
|
77
|
+
const lock = inputs.lock.replaceAll("\r\n", "\n");
|
|
78
|
+
const headers = [...lock.matchAll(/^([A-Z][A-Z ]+)$/gm)].map(
|
|
79
|
+
match => match[1]
|
|
80
|
+
);
|
|
81
|
+
required(
|
|
82
|
+
lock.startsWith("GEM\n") &&
|
|
83
|
+
headers.every(header =>
|
|
84
|
+
[
|
|
85
|
+
"GEM",
|
|
86
|
+
"PLATFORMS",
|
|
87
|
+
"DEPENDENCIES",
|
|
88
|
+
"RUBY VERSION",
|
|
89
|
+
"BUNDLED WITH",
|
|
90
|
+
"CHECKSUMS",
|
|
91
|
+
].includes(header)
|
|
92
|
+
),
|
|
93
|
+
"private/git/path Ruby sources are unsupported"
|
|
94
|
+
);
|
|
95
|
+
const remotes = [...lock.matchAll(/^ {2}remote: (.+)$/gm)].map(
|
|
96
|
+
match => match[1]
|
|
97
|
+
);
|
|
98
|
+
required(
|
|
99
|
+
remotes.length === 1 && /^https:\/\/rubygems\.org\/?$/.test(remotes[0]),
|
|
100
|
+
"private or alternate Ruby source is unsupported"
|
|
101
|
+
);
|
|
102
|
+
required(
|
|
103
|
+
/\nRUBY VERSION\n {3}ruby 3\.4\.11p\d*(?:\n|$)/.test(lock) &&
|
|
104
|
+
/\nBUNDLED WITH\n {3}2\.4\.10(?:\n|$)/.test(lock),
|
|
105
|
+
"frozen Ruby/Bundler lock identity differs"
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** Read-only source bytes are checked before and after the entire isolated installer. */
|
|
110
|
+
function rubySource(boundary, tools) {
|
|
111
|
+
const mount = boundary.mounts.find(
|
|
112
|
+
value => value.target === boundary.workspace && value.readOnly === true
|
|
113
|
+
);
|
|
114
|
+
required(
|
|
115
|
+
mount && realpathSync(mount.source) === mount.source,
|
|
116
|
+
"Ruby installer source projection is missing or aliased"
|
|
117
|
+
);
|
|
118
|
+
const source = mount.source;
|
|
119
|
+
const lock = readBytes(join(source, "Gemfile.lock"), 3_145_728, false);
|
|
120
|
+
const manifest = readBytes(join(source, "Gemfile"), 1_048_576, false);
|
|
121
|
+
const decoder = new TextDecoder("utf8", { fatal: true });
|
|
122
|
+
rubyInstallationInputs(
|
|
123
|
+
{
|
|
124
|
+
lock: decoder.decode(lock),
|
|
125
|
+
rubyVersion: decoder.decode(
|
|
126
|
+
readBytes(join(source, ".ruby-version"), 128, false)
|
|
127
|
+
),
|
|
128
|
+
configurationPresent: existsSync(join(source, ".bundle")),
|
|
129
|
+
},
|
|
130
|
+
tools
|
|
131
|
+
);
|
|
132
|
+
return { source, lock, manifest };
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/** Native extensions load only in the separate dependency-only installer namespace. */
|
|
136
|
+
export async function installRubyDependencies(boundary, tools) {
|
|
137
|
+
required(
|
|
138
|
+
boundary.role === "ruby-install",
|
|
139
|
+
"Ruby installation requires its separate isolated role"
|
|
140
|
+
);
|
|
141
|
+
toolIdentity(tools);
|
|
142
|
+
required(
|
|
143
|
+
namespaceRecords(boundary.root).length === 0,
|
|
144
|
+
"Ruby installation requires completed namespace recovery"
|
|
145
|
+
);
|
|
146
|
+
const input = rubySource(boundary, tools);
|
|
147
|
+
const result = await executeWorker(boundary, {
|
|
148
|
+
command: tools.bundle.path,
|
|
149
|
+
args: ["_2.4.10_", "install", "--jobs", "2", "--retry", "0"],
|
|
150
|
+
});
|
|
151
|
+
required(result.code === 0, "actual frozen Ruby installation failed");
|
|
152
|
+
required(
|
|
153
|
+
namespaceRecords(boundary.root).length === 0,
|
|
154
|
+
"Ruby installer namespace cleanup remains outstanding"
|
|
155
|
+
);
|
|
156
|
+
required(
|
|
157
|
+
readBytes(join(input.source, "Gemfile"), 1_048_576, false).equals(
|
|
158
|
+
input.manifest
|
|
159
|
+
) &&
|
|
160
|
+
readBytes(join(input.source, "Gemfile.lock"), 3_145_728, false).equals(
|
|
161
|
+
input.lock
|
|
162
|
+
),
|
|
163
|
+
"Ruby installer changed committed baseline bytes"
|
|
164
|
+
);
|
|
165
|
+
const sealed = sealDependencies(
|
|
166
|
+
join(boundary.root, "ruby-dependencies"),
|
|
167
|
+
true
|
|
168
|
+
);
|
|
169
|
+
return { code: result.code, files: sealed.files, bytes: sealed.bytes };
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/** Only installer-owned dependency files may be sealed after its entire namespace is absent. */
|
|
173
|
+
export function sealDependencies(directory, installerAbsent) {
|
|
174
|
+
required(
|
|
175
|
+
installerAbsent === true && realpathSync(directory) === directory,
|
|
176
|
+
"dependency sealing requires reaped installer and unaliased root"
|
|
177
|
+
);
|
|
178
|
+
const root = lstatSync(directory);
|
|
179
|
+
required(
|
|
180
|
+
root.isDirectory() &&
|
|
181
|
+
!root.isSymbolicLink() &&
|
|
182
|
+
root.uid === process.getuid(),
|
|
183
|
+
"installed dependency root is not controller owned"
|
|
184
|
+
);
|
|
185
|
+
const pending = [directory];
|
|
186
|
+
let count = 0;
|
|
187
|
+
let bytes = 0;
|
|
188
|
+
const files = [];
|
|
189
|
+
while (pending.length) {
|
|
190
|
+
const parent = pending.pop();
|
|
191
|
+
for (const entry of readdirSync(parent, { withFileTypes: true })) {
|
|
192
|
+
const path = join(parent, entry.name);
|
|
193
|
+
const stat = lstatSync(path);
|
|
194
|
+
required(
|
|
195
|
+
++count <= 300_000 && stat.uid === process.getuid(),
|
|
196
|
+
"installed dependency ownership/inventory differs"
|
|
197
|
+
);
|
|
198
|
+
if (stat.isSymbolicLink()) {
|
|
199
|
+
required(
|
|
200
|
+
realpathSync(path).startsWith(`${directory}${sep}`),
|
|
201
|
+
"installed dependency alias escapes its sealed tree"
|
|
202
|
+
);
|
|
203
|
+
continue;
|
|
204
|
+
}
|
|
205
|
+
required(
|
|
206
|
+
stat.isFile() || stat.isDirectory(),
|
|
207
|
+
"installed dependency contains a special file"
|
|
208
|
+
);
|
|
209
|
+
if (stat.isDirectory()) pending.push(path);
|
|
210
|
+
else {
|
|
211
|
+
bytes += stat.size;
|
|
212
|
+
required(
|
|
213
|
+
bytes <= 10_737_418_240 && stat.nlink === 1,
|
|
214
|
+
"installed dependency size or link ownership differs"
|
|
215
|
+
);
|
|
216
|
+
}
|
|
217
|
+
files.push({
|
|
218
|
+
path,
|
|
219
|
+
mode: stat.isDirectory() || stat.mode & 0o111 ? 0o555 : 0o444,
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
for (const file of files) chmodSync(file.path, file.mode);
|
|
224
|
+
chmodSync(directory, 0o555);
|
|
225
|
+
required(
|
|
226
|
+
files.every(file => (lstatSync(file.path).mode & 0o222) === 0) &&
|
|
227
|
+
(lstatSync(directory).mode & 0o222) === 0,
|
|
228
|
+
"installed dependency sealing readback differs"
|
|
229
|
+
);
|
|
230
|
+
return { files: count, bytes };
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/** The committed caller pins the actual managed catalog bytes independently of the parsed policy. */
|
|
234
|
+
export function loadRuntime(config, platform) {
|
|
235
|
+
const file = fileURLToPath(
|
|
236
|
+
new URL("../npm-updater-gate-runtime.json", import.meta.url)
|
|
237
|
+
);
|
|
238
|
+
const digest = config.automationProvenance?.gateRuntimeSha256;
|
|
239
|
+
required(
|
|
240
|
+
typeof digest === "string" && HEX.test(digest),
|
|
241
|
+
"committed qualified gate runtime digest is missing"
|
|
242
|
+
);
|
|
243
|
+
const bytes = readBytes(file, 1_048_576, false);
|
|
244
|
+
required(
|
|
245
|
+
sha256(bytes) === digest,
|
|
246
|
+
"qualified gate runtime catalog bytes differ"
|
|
247
|
+
);
|
|
248
|
+
const value = JSON.parse(
|
|
249
|
+
new TextDecoder("utf8", { fatal: true }).decode(bytes)
|
|
250
|
+
);
|
|
251
|
+
const selected = validateRuntime(value, platform);
|
|
252
|
+
const recipe = fileURLToPath(
|
|
253
|
+
new URL("../npm-updater-gate.Dockerfile", import.meta.url)
|
|
254
|
+
);
|
|
255
|
+
required(
|
|
256
|
+
sha256(readBytes(recipe, 65_536, false)) === value.recipeSha256,
|
|
257
|
+
"qualified runtime recipe bytes differ"
|
|
258
|
+
);
|
|
259
|
+
const supervisor = fileURLToPath(
|
|
260
|
+
new URL("../npm-updater-gate-supervisor.c", import.meta.url)
|
|
261
|
+
);
|
|
262
|
+
required(
|
|
263
|
+
sha256(readBytes(supervisor, 65_536, false)) === value.supervisorSha256,
|
|
264
|
+
"qualified supervisor source bytes differ"
|
|
265
|
+
);
|
|
266
|
+
return { value, selected };
|
|
267
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
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
|
+
/** Complete private proof transport; the ordinary verifier still authenticates every byte. */
|
|
5
|
+
import { lstatSync, mkdirSync, realpathSync, writeFileSync } from "node:fs";
|
|
6
|
+
import { join, resolve } from "node:path";
|
|
7
|
+
import { required } from "./npm-update-contract.mjs";
|
|
8
|
+
import { readBytes, writeJson } from "./npm-update-process.mjs";
|
|
9
|
+
|
|
10
|
+
/** Optional recovery is always a pair, never silently downgraded to origin authority. */
|
|
11
|
+
export function validateGateProof(proof) {
|
|
12
|
+
required(
|
|
13
|
+
(proof.recovery !== undefined) === (proof.recoveryBundle !== undefined),
|
|
14
|
+
"partial gate recovery proof"
|
|
15
|
+
);
|
|
16
|
+
for (const [name, maximum] of [
|
|
17
|
+
["bundle", 1_048_576],
|
|
18
|
+
["recovery", 65_536],
|
|
19
|
+
["recoveryBundle", 1_048_576],
|
|
20
|
+
]) {
|
|
21
|
+
const bytes = proof[name];
|
|
22
|
+
required(
|
|
23
|
+
(name !== "bundle" && bytes === undefined) ||
|
|
24
|
+
(Buffer.isBuffer(bytes) && bytes.length > 0 && bytes.length <= maximum),
|
|
25
|
+
"invalid bounded gate proof bytes"
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Dangling aliases are present invalid inputs, never an absent recovery slot. */
|
|
31
|
+
function present(path) {
|
|
32
|
+
try {
|
|
33
|
+
lstatSync(path);
|
|
34
|
+
return true;
|
|
35
|
+
} catch (error) {
|
|
36
|
+
if (error.code === "ENOENT") return false;
|
|
37
|
+
throw error;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Read all fixed slots before any attributed checkout operation. */
|
|
42
|
+
export function readGateProof(directory) {
|
|
43
|
+
const recovery = join(directory, "recovery.json");
|
|
44
|
+
const recoveryBundle = join(directory, "recovery-bundle.json");
|
|
45
|
+
required(
|
|
46
|
+
present(recovery) === present(recoveryBundle),
|
|
47
|
+
"partial gate recovery proof"
|
|
48
|
+
);
|
|
49
|
+
const proof = {
|
|
50
|
+
bundle: readBytes(join(directory, "bundle.json"), 1_048_576),
|
|
51
|
+
recovery: present(recovery) ? readBytes(recovery, 65_536) : undefined,
|
|
52
|
+
recoveryBundle: present(recoveryBundle)
|
|
53
|
+
? readBytes(recoveryBundle, 1_048_576)
|
|
54
|
+
: undefined,
|
|
55
|
+
};
|
|
56
|
+
validateGateProof(proof);
|
|
57
|
+
return proof;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** Exclusive writes preserve original and recovery proof bytes; no transport grants verification. */
|
|
61
|
+
export function writeGateProof(directory, descriptor, proof) {
|
|
62
|
+
validateGateProof(proof);
|
|
63
|
+
mkdirSync(directory, { recursive: true, mode: 0o700 });
|
|
64
|
+
const stat = lstatSync(directory);
|
|
65
|
+
required(
|
|
66
|
+
stat.isDirectory() &&
|
|
67
|
+
!stat.isSymbolicLink() &&
|
|
68
|
+
realpathSync(directory) === resolve(directory) &&
|
|
69
|
+
stat.uid === process.getuid() &&
|
|
70
|
+
(stat.mode & 0o077) === 0,
|
|
71
|
+
"gate proof directory is aliased or public"
|
|
72
|
+
);
|
|
73
|
+
writeJson(join(directory, "descriptor.json"), descriptor);
|
|
74
|
+
for (const [key, file] of [
|
|
75
|
+
["bundle", "bundle.json"],
|
|
76
|
+
["recovery", "recovery.json"],
|
|
77
|
+
["recoveryBundle", "recovery-bundle.json"],
|
|
78
|
+
])
|
|
79
|
+
if (proof[key] !== undefined)
|
|
80
|
+
writeFileSync(join(directory, file), proof[key], {
|
|
81
|
+
mode: 0o600,
|
|
82
|
+
flag: "wx",
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** Resolve the real Git control path and preserve the complete private proof and message. */
|
|
87
|
+
export async function installGateProof(context, root, git) {
|
|
88
|
+
const control = (await git(["rev-parse", "--git-common-dir"])).stdout
|
|
89
|
+
.toString()
|
|
90
|
+
.trim();
|
|
91
|
+
writeGateProof(
|
|
92
|
+
resolve(context.cwd, control, "lisa/automation-provenance"),
|
|
93
|
+
context.preview.descriptor,
|
|
94
|
+
context
|
|
95
|
+
);
|
|
96
|
+
const message = join(root, "message");
|
|
97
|
+
writeFileSync(message, context.preview.message, { mode: 0o600, flag: "wx" });
|
|
98
|
+
return message;
|
|
99
|
+
}
|