@codyswann/lisa 4.70.4 → 4.71.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (139) hide show
  1. package/all/copy-overwrite/scripts/lib/automation-provenance-contract.mjs +174 -0
  2. package/all/copy-overwrite/scripts/lib/automation-provenance-local.mjs +279 -0
  3. package/all/copy-overwrite/scripts/lib/gate-failure-diagnosis.mjs +17 -7
  4. package/all/copy-overwrite/scripts/lib/github-attestation-provider.mjs +241 -0
  5. package/all/copy-overwrite/scripts/lib/github-attestation-recovery.mjs +283 -0
  6. package/all/copy-overwrite/scripts/lib/github-attestation-verifier.mjs +244 -0
  7. package/all/copy-overwrite/scripts/lib/npm-update-allocate.mjs +289 -0
  8. package/all/copy-overwrite/scripts/lib/npm-update-authorization.mjs +235 -0
  9. package/all/copy-overwrite/scripts/lib/npm-update-broker-client.mjs +92 -0
  10. package/all/copy-overwrite/scripts/lib/npm-update-checkpoint.mjs +241 -0
  11. package/all/copy-overwrite/scripts/lib/npm-update-contract.mjs +278 -0
  12. package/all/copy-overwrite/scripts/lib/npm-update-controller-broker.mjs +234 -0
  13. package/all/copy-overwrite/scripts/lib/npm-update-controller-factory.mjs +278 -0
  14. package/all/copy-overwrite/scripts/lib/npm-update-controller-recipe.mjs +135 -0
  15. package/all/copy-overwrite/scripts/lib/npm-update-execution-adapter.mjs +292 -0
  16. package/all/copy-overwrite/scripts/lib/npm-update-gate-hooks.mjs +214 -0
  17. package/all/copy-overwrite/scripts/lib/npm-update-gate-install.mjs +267 -0
  18. package/all/copy-overwrite/scripts/lib/npm-update-gate-proof.mjs +99 -0
  19. package/all/copy-overwrite/scripts/lib/npm-update-gate.mjs +300 -0
  20. package/all/copy-overwrite/scripts/lib/npm-update-gh-dispatch.mjs +182 -0
  21. package/all/copy-overwrite/scripts/lib/npm-update-gh-grants.mjs +156 -0
  22. package/all/copy-overwrite/scripts/lib/npm-update-gh-requests.mjs +221 -0
  23. package/all/copy-overwrite/scripts/lib/npm-update-github.mjs +214 -0
  24. package/all/copy-overwrite/scripts/lib/npm-update-helper-graph.mjs +238 -0
  25. package/all/copy-overwrite/scripts/lib/npm-update-helper-inventory.mjs +126 -0
  26. package/all/copy-overwrite/scripts/lib/npm-update-helper.mjs +292 -0
  27. package/all/copy-overwrite/scripts/lib/npm-update-hook-installation.mjs +116 -0
  28. package/all/copy-overwrite/scripts/lib/npm-update-hook-preload.mjs +153 -0
  29. package/all/copy-overwrite/scripts/lib/npm-update-hook-provider.mjs +207 -0
  30. package/all/copy-overwrite/scripts/lib/npm-update-hook-read-client.mjs +164 -0
  31. package/all/copy-overwrite/scripts/lib/npm-update-hosted-gate.mjs +300 -0
  32. package/all/copy-overwrite/scripts/lib/npm-update-hosted-hook.mjs +157 -0
  33. package/all/copy-overwrite/scripts/lib/npm-update-invariants.mjs +28 -0
  34. package/all/copy-overwrite/scripts/lib/npm-update-isolation.mjs +11 -0
  35. package/all/copy-overwrite/scripts/lib/npm-update-leaf-contract.mjs +171 -0
  36. package/all/copy-overwrite/scripts/lib/npm-update-leaf.mjs +194 -0
  37. package/all/copy-overwrite/scripts/lib/npm-update-native-process.mjs +155 -0
  38. package/all/copy-overwrite/scripts/lib/npm-update-npm.mjs +190 -0
  39. package/all/copy-overwrite/scripts/lib/npm-update-object.mjs +112 -0
  40. package/all/copy-overwrite/scripts/lib/npm-update-orchestrator.mjs +273 -0
  41. package/all/copy-overwrite/scripts/lib/npm-update-owner.mjs +292 -0
  42. package/all/copy-overwrite/scripts/lib/npm-update-prepare.mjs +263 -0
  43. package/all/copy-overwrite/scripts/lib/npm-update-process-core.mjs +179 -0
  44. package/all/copy-overwrite/scripts/lib/npm-update-process.mjs +16 -0
  45. package/all/copy-overwrite/scripts/lib/npm-update-publication.mjs +258 -0
  46. package/all/copy-overwrite/scripts/lib/npm-update-publish.mjs +254 -0
  47. package/all/copy-overwrite/scripts/lib/npm-update-quality.mjs +153 -0
  48. package/all/copy-overwrite/scripts/lib/npm-update-recovery.mjs +264 -0
  49. package/all/copy-overwrite/scripts/lib/npm-update-runtime-archive.mjs +204 -0
  50. package/all/copy-overwrite/scripts/lib/npm-update-runtime-graph.mjs +185 -0
  51. package/all/copy-overwrite/scripts/lib/npm-update-runtime-transport.mjs +221 -0
  52. package/all/copy-overwrite/scripts/lib/npm-update-runtime.mjs +284 -0
  53. package/all/copy-overwrite/scripts/lib/npm-update-tool-launcher.mjs +287 -0
  54. package/all/copy-overwrite/scripts/lib/npm-update-worker-environment.mjs +161 -0
  55. package/all/copy-overwrite/scripts/lib/npm-update-worker-inspection.mjs +142 -0
  56. package/all/copy-overwrite/scripts/lib/npm-update-worker-lifecycle.mjs +256 -0
  57. package/all/copy-overwrite/scripts/lib/npm-update-worker-policy.mjs +266 -0
  58. package/all/copy-overwrite/scripts/lib/process-tree-runner.mjs +230 -36
  59. package/all/copy-overwrite/scripts/lisa-automation-provenance.mjs +161 -0
  60. package/all/copy-overwrite/scripts/lisa-hooks/block-no-verify.sh +89 -5
  61. package/all/copy-overwrite/scripts/lisa-npm-updater.mjs +204 -0
  62. package/all/copy-overwrite/scripts/lisa-run-gates.mjs +48 -123
  63. package/all/copy-overwrite/scripts/lisa-work-item.mjs +107 -41
  64. package/all/copy-overwrite/scripts/npm-updater-gate-runtime.json +1 -0
  65. package/all/copy-overwrite/scripts/npm-updater-gate-supervisor.c +121 -0
  66. package/all/copy-overwrite/scripts/npm-updater-gate.Dockerfile +49 -0
  67. package/all/copy-overwrite/scripts/npm-updater-helper-graph.json +1085 -0
  68. package/dist/codex/scripts/block-no-verify.sh +89 -5
  69. package/dist/core/lisa-owned-hash-ledger.d.ts.map +1 -1
  70. package/dist/core/lisa-owned-hash-ledger.js +604 -0
  71. package/dist/core/lisa-owned-hash-ledger.js.map +1 -1
  72. package/dist/core/nightly-e2e-guard-behavior-certificate.js +2 -2
  73. package/dist/core/upstream-evidence-manifest.d.ts.map +1 -1
  74. package/dist/core/upstream-evidence-manifest.js +159 -11
  75. package/dist/core/upstream-evidence-manifest.js.map +1 -1
  76. package/dist/opencode/plugin-templates/block-no-verify.sh +89 -5
  77. package/package.json +4 -4
  78. package/plugins/lisa/.claude-plugin/plugin.json +1 -1
  79. package/plugins/lisa/.codex-plugin/plugin.json +1 -1
  80. package/plugins/lisa/hooks/block-no-verify.agy.sh +89 -5
  81. package/plugins/lisa/hooks/block-no-verify.sh +89 -5
  82. package/plugins/lisa-agy/hooks/block-no-verify.agy.sh +89 -5
  83. package/plugins/lisa-agy/plugin.json +1 -1
  84. package/plugins/lisa-cdk/.claude-plugin/plugin.json +1 -1
  85. package/plugins/lisa-cdk/.codex-plugin/plugin.json +1 -1
  86. package/plugins/lisa-cdk-agy/plugin.json +1 -1
  87. package/plugins/lisa-cdk-copilot/.claude-plugin/plugin.json +1 -1
  88. package/plugins/lisa-cdk-cursor/.claude-plugin/plugin.json +1 -1
  89. package/plugins/lisa-copilot/.claude-plugin/plugin.json +1 -1
  90. package/plugins/lisa-copilot/hooks/block-no-verify.sh +89 -5
  91. package/plugins/lisa-cursor/.claude-plugin/plugin.json +1 -1
  92. package/plugins/lisa-cursor/hooks/block-no-verify.sh +89 -5
  93. package/plugins/lisa-expo/.claude-plugin/plugin.json +1 -1
  94. package/plugins/lisa-expo/.codex-plugin/plugin.json +1 -1
  95. package/plugins/lisa-expo-agy/plugin.json +1 -1
  96. package/plugins/lisa-expo-copilot/.claude-plugin/plugin.json +1 -1
  97. package/plugins/lisa-expo-cursor/.claude-plugin/plugin.json +1 -1
  98. package/plugins/lisa-harper-fabric/.claude-plugin/plugin.json +1 -1
  99. package/plugins/lisa-harper-fabric/.codex-plugin/plugin.json +1 -1
  100. package/plugins/lisa-harper-fabric-agy/plugin.json +1 -1
  101. package/plugins/lisa-harper-fabric-copilot/.claude-plugin/plugin.json +1 -1
  102. package/plugins/lisa-harper-fabric-cursor/.claude-plugin/plugin.json +1 -1
  103. package/plugins/lisa-nestjs/.claude-plugin/plugin.json +1 -1
  104. package/plugins/lisa-nestjs/.codex-plugin/plugin.json +1 -1
  105. package/plugins/lisa-nestjs-agy/plugin.json +1 -1
  106. package/plugins/lisa-nestjs-copilot/.claude-plugin/plugin.json +1 -1
  107. package/plugins/lisa-nestjs-cursor/.claude-plugin/plugin.json +1 -1
  108. package/plugins/lisa-openclaw/.claude-plugin/plugin.json +1 -1
  109. package/plugins/lisa-openclaw/.codex-plugin/plugin.json +1 -1
  110. package/plugins/lisa-openclaw-agy/plugin.json +1 -1
  111. package/plugins/lisa-openclaw-copilot/.claude-plugin/plugin.json +1 -1
  112. package/plugins/lisa-openclaw-cursor/.claude-plugin/plugin.json +1 -1
  113. package/plugins/lisa-phaser/.claude-plugin/plugin.json +1 -1
  114. package/plugins/lisa-phaser/.codex-plugin/plugin.json +1 -1
  115. package/plugins/lisa-phaser-agy/plugin.json +1 -1
  116. package/plugins/lisa-phaser-copilot/.claude-plugin/plugin.json +1 -1
  117. package/plugins/lisa-phaser-cursor/.claude-plugin/plugin.json +1 -1
  118. package/plugins/lisa-rails/.claude-plugin/plugin.json +1 -1
  119. package/plugins/lisa-rails/.codex-plugin/plugin.json +1 -1
  120. package/plugins/lisa-rails-agy/plugin.json +1 -1
  121. package/plugins/lisa-rails-copilot/.claude-plugin/plugin.json +1 -1
  122. package/plugins/lisa-rails-cursor/.claude-plugin/plugin.json +1 -1
  123. package/plugins/lisa-typescript/.claude-plugin/plugin.json +1 -1
  124. package/plugins/lisa-typescript/.codex-plugin/plugin.json +1 -1
  125. package/plugins/lisa-typescript-agy/plugin.json +1 -1
  126. package/plugins/lisa-typescript-copilot/.claude-plugin/plugin.json +1 -1
  127. package/plugins/lisa-typescript-cursor/.claude-plugin/plugin.json +1 -1
  128. package/plugins/lisa-wiki/.claude-plugin/plugin.json +1 -1
  129. package/plugins/lisa-wiki/.codex-plugin/plugin.json +1 -1
  130. package/plugins/lisa-wiki-agy/plugin.json +1 -1
  131. package/plugins/lisa-wiki-copilot/.claude-plugin/plugin.json +1 -1
  132. package/plugins/lisa-wiki-cursor/.claude-plugin/plugin.json +1 -1
  133. package/plugins/src/base/hooks/block-no-verify.agy.sh +89 -5
  134. package/plugins/src/base/hooks/block-no-verify.sh +89 -5
  135. package/scripts/check-shipped-surface-removals.mjs +96 -24
  136. package/scripts/generate-npm-updater-helper-graph.mjs +184 -0
  137. package/scripts/two-channel-couplings.json +31 -7
  138. package/typescript/copy-contents/.husky/commit-msg +18 -0
  139. package/typescript/copy-overwrite/.prettierignore +2 -0
@@ -0,0 +1,256 @@
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
+ /** Parent registry and real daemon exit/removal own every worker lifecycle verdict. @module npm-updater */
5
+ import { required, UpdaterError } from "./npm-update-contract.mjs";
6
+ import { canonicalJson } from "../lisa-automation-provenance.mjs";
7
+ import { readBytes, runProcess } from "./npm-update-process.mjs";
8
+ import { sha256 } from "./github-attestation-verifier.mjs";
9
+ import {
10
+ constants,
11
+ openSync,
12
+ closeSync,
13
+ readSync,
14
+ fstatSync,
15
+ lstatSync,
16
+ realpathSync,
17
+ } from "node:fs";
18
+ import { createHash } from "node:crypto";
19
+ import {
20
+ registerNamespace,
21
+ assignNamespaceId,
22
+ namespaceRecords,
23
+ completeNamespace,
24
+ } from "./npm-update-orchestrator.mjs";
25
+ import { workerArguments } from "./npm-update-worker-policy.mjs";
26
+ import { workerEnvironment } from "./npm-update-worker-environment.mjs";
27
+ import { assertWorkerInspection } from "./npm-update-worker-inspection.mjs";
28
+
29
+ /** Only the parent-qualified Docker executable can create or observe a candidate namespace. */
30
+ export function dockerClient(boundary) {
31
+ const stat = lstatSync(boundary.docker);
32
+ required(
33
+ stat.isFile() &&
34
+ !stat.isSymbolicLink() &&
35
+ realpathSync(boundary.docker) === boundary.docker &&
36
+ /^[a-f0-9]{64}$/.test(boundary.dockerSha256) &&
37
+ binaryDigest(boundary.docker) === boundary.dockerSha256,
38
+ "Docker controller binary is unqualified"
39
+ );
40
+ const env = {
41
+ PATH: "/usr/bin:/bin",
42
+ HOME: process.env.HOME,
43
+ LANG: "C.UTF-8",
44
+ };
45
+ return (args, options = {}) =>
46
+ runProcess(boundary.docker, args, { env, maximum: 8_388_608, ...options });
47
+ }
48
+
49
+ /** Large native tools are hashed through a no-follow descriptor and a bounded streaming buffer. */
50
+ export function binaryDigest(path) {
51
+ const fd = openSync(path, constants.O_RDONLY | constants.O_NOFOLLOW);
52
+ try {
53
+ const stat = fstatSync(fd);
54
+ required(
55
+ stat.isFile() && stat.size > 0 && stat.size <= 150_000_000,
56
+ "controller binary exceeds bound"
57
+ );
58
+ const hash = createHash("sha256");
59
+ const buffer = Buffer.alloc(65_536);
60
+ let total = 0;
61
+ let count;
62
+ while ((count = readSync(fd, buffer, 0, buffer.length, null)) > 0) {
63
+ total += count;
64
+ required(total <= stat.size, "controller binary changed while hashing");
65
+ hash.update(buffer.subarray(0, count));
66
+ }
67
+ required(total === stat.size, "controller binary length changed");
68
+ return hash.digest("hex");
69
+ } finally {
70
+ closeSync(fd);
71
+ }
72
+ }
73
+
74
+ /** Readback checks ownership before every lifecycle mutation; foreign IDs are never removed. */
75
+ async function ownedInspection(docker, id, boundary) {
76
+ const result = await docker(["inspect", "--type", "container", id]);
77
+ const entries = JSON.parse(result.stdout.toString());
78
+ required(
79
+ Array.isArray(entries) &&
80
+ entries.length === 1 &&
81
+ entries[0].Id === id &&
82
+ entries[0].Image === boundary.image &&
83
+ entries[0].Config?.Labels?.["dev.lisa.npm.owner"] === boundary.nonce &&
84
+ (!boundary.name || entries[0].Name === `/${boundary.name}`),
85
+ "container ownership readback differs"
86
+ );
87
+ return entries[0];
88
+ }
89
+
90
+ /** Recovery never removes a foreign object or infers daemon absence from an inspect error. */
91
+ async function reapRegistration(docker, entry) {
92
+ const { record } = entry;
93
+ let removedId = record.id;
94
+ const observed = await docker(
95
+ ["inspect", "--type", "container", record.id ?? record.name],
96
+ { allowed: [0, 1] }
97
+ );
98
+ if (observed.code === 0) {
99
+ const values = JSON.parse(observed.stdout.toString());
100
+ required(
101
+ Array.isArray(values) &&
102
+ values.length === 1 &&
103
+ /^[a-f0-9]{64}$/.test(values[0].Id),
104
+ "namespace recovery inspection differs"
105
+ );
106
+ const id = values[0].Id;
107
+ removedId = id;
108
+ required(
109
+ record.id === null || record.id === id,
110
+ "namespace recovery ID differs"
111
+ );
112
+ await ownedInspection(docker, id, { ...record, name: record.name });
113
+ if (record.id === null) assignNamespaceId(entry.file, id);
114
+ await docker(["rm", "--force", id]);
115
+ }
116
+ const absent = await docker([
117
+ "ps",
118
+ "--all",
119
+ "--quiet",
120
+ "--no-trunc",
121
+ "--filter",
122
+ `name=^/${record.name}$`,
123
+ ]);
124
+ required(
125
+ absent.stdout.toString().trim() === "",
126
+ "registered owned namespace still exists"
127
+ );
128
+ if (removedId) {
129
+ const byId = await docker([
130
+ "ps",
131
+ "--all",
132
+ "--quiet",
133
+ "--no-trunc",
134
+ "--filter",
135
+ `id=${removedId}`,
136
+ ]);
137
+ required(
138
+ byId.stdout.toString().trim() === "",
139
+ "registered owned namespace ID still exists"
140
+ );
141
+ }
142
+ required(
143
+ record.id !== null || observed.code === 0,
144
+ "unconfirmed namespace creation remains registered"
145
+ );
146
+ completeNamespace(entry.file, true);
147
+ }
148
+
149
+ /** The guest deadline never expands the existing outer process budget. */
150
+ export function workerCaptureBudget(deadlineMs) {
151
+ required(
152
+ Number.isSafeInteger(deadlineMs) &&
153
+ deadlineMs > 0 &&
154
+ deadlineMs <= 1_800_000,
155
+ "invalid worker capture deadline"
156
+ );
157
+ return Math.min(deadlineMs + 6000, 1_800_000);
158
+ }
159
+
160
+ /** Dead-caller recovery covers created-but-unstarted objects before any new execution/sealing. */
161
+ export async function recoverWorkers(boundary) {
162
+ const docker = dockerClient(boundary);
163
+ const entries = namespaceRecords(boundary.root);
164
+ for (const entry of entries) {
165
+ let alive = true;
166
+ try {
167
+ process.kill(entry.record.callerPid, 0);
168
+ } catch (error) {
169
+ required(
170
+ error.code === "ESRCH",
171
+ "registered caller visibility is unavailable"
172
+ );
173
+ alive = false;
174
+ }
175
+ required(!alive, "registered namespace belongs to a live caller");
176
+ await reapRegistration(docker, entry);
177
+ }
178
+ required(
179
+ namespaceRecords(boundary.root).length === 0,
180
+ "owned namespace recovery remains outstanding"
181
+ );
182
+ }
183
+
184
+ /** Daemon exit and whole-namespace removal decide the result, never a writable guest receipt. */
185
+ export async function executeWorker(boundary, invocation, { input, env } = {}) {
186
+ const args = workerArguments(
187
+ boundary,
188
+ invocation,
189
+ workerEnvironment(env, boundary)
190
+ );
191
+ required(
192
+ sha256(
193
+ `${canonicalJson(JSON.parse(readBytes(boundary.seccompPath, 1_048_576).toString()))}\n`
194
+ ) === boundary.seccompSha256,
195
+ "worker seccomp input bytes differ"
196
+ );
197
+ const docker = dockerClient(boundary);
198
+ const registration = registerNamespace(boundary);
199
+ args.splice(1, 0, "--name", registration.record.name);
200
+ try {
201
+ const created = await docker(args);
202
+ const id = created.stdout.toString().trim();
203
+ required(/^[a-f0-9]{64}$/.test(id), "Docker returned an invalid worker ID");
204
+ assignNamespaceId(registration.file, id);
205
+ const ownedBoundary = { ...boundary, name: registration.record.name };
206
+ assertWorkerInspection(
207
+ await ownedInspection(docker, id, ownedBoundary),
208
+ boundary,
209
+ invocation
210
+ );
211
+ const attached = await docker(["start", "--attach", "--interactive", id], {
212
+ input,
213
+ timeout: workerCaptureBudget(boundary.deadlineMs),
214
+ allowed: Array.from({ length: 256 }, (_, code) => code),
215
+ });
216
+ const waited = (await docker(["wait", id])).stdout.toString().trim();
217
+ required(
218
+ /^(?:0|[1-9]\d{0,2})$/.test(waited) && Number(waited) <= 255,
219
+ "Docker returned an invalid actual worker exit"
220
+ );
221
+ const inspected = await ownedInspection(docker, id, ownedBoundary);
222
+ if (
223
+ !(
224
+ inspected.State?.Running === false &&
225
+ inspected.State?.ExitCode === Number(waited) &&
226
+ attached.code === Number(waited) &&
227
+ !inspected.State?.OOMKilled &&
228
+ !inspected.State?.Error
229
+ )
230
+ )
231
+ throw Object.assign(
232
+ new UpdaterError(
233
+ "worker actual status/attach differs or worker was killed"
234
+ ),
235
+ {
236
+ workerDiagnostic: {
237
+ attach: attached.code,
238
+ wait: Number(waited),
239
+ state: inspected.State,
240
+ stderr: attached.stderr.toString(),
241
+ },
242
+ }
243
+ );
244
+ return {
245
+ code: Number(waited),
246
+ stdout: attached.stdout,
247
+ stderr: attached.stderr,
248
+ };
249
+ } finally {
250
+ const active = namespaceRecords(boundary.root).find(
251
+ entry => entry.file === registration.file
252
+ );
253
+ required(active, "owned namespace registration disappeared");
254
+ await reapRegistration(docker, active);
255
+ }
256
+ }
@@ -0,0 +1,266 @@
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
+ /** Closed worker roles, scratch and literal command data grant no controller authority. @module npm-updater */
5
+ import { required, keys } from "./npm-update-contract.mjs";
6
+ import { dirname, resolve } from "node:path";
7
+
8
+ import {
9
+ inspectedEnvironment,
10
+ isInstallerRole,
11
+ assertRubyEnvironment,
12
+ } from "./npm-update-worker-environment.mjs";
13
+ export {
14
+ inspectedEnvironment,
15
+ isInstallerRole,
16
+ workerEnvironment,
17
+ } from "./npm-update-worker-environment.mjs";
18
+
19
+ export function workerScratch(boundary) {
20
+ const uid = boundary.uid ?? 2001;
21
+ const gid = boundary.gid ?? 2001;
22
+ return {
23
+ "/tmp": "rw,nosuid,nodev,size=536870912,mode=1777",
24
+ "/home/candidate": `rw,nosuid,nodev,size=536870912,uid=${uid},gid=${gid},mode=700`,
25
+ [`${boundary.workspace}/tmp`]: `rw,nosuid,nodev,size=536870912,uid=${uid},gid=${gid},mode=700`,
26
+ };
27
+ }
28
+
29
+ /** Only the fixed qualified protected parent accepts a bounded original deadline. */
30
+ function supervisorIdentity(boundary) {
31
+ keys(boundary.supervisor, ["path", "version", "sha256"]);
32
+ required(
33
+ boundary.supervisor.path === "/usr/local/bin/lisa-npm-supervisor" &&
34
+ boundary.supervisor.version === "lisa-npm-supervisor 1" &&
35
+ /^[a-f0-9]{64}$/.test(boundary.supervisor.sha256) &&
36
+ /^[a-f0-9]{64}$/.test(boundary.seccompSha256) &&
37
+ Number.isSafeInteger(boundary.deadlineMs) &&
38
+ boundary.deadlineMs >= 1 &&
39
+ boundary.deadlineMs <= 1_800_000,
40
+ "unqualified supervisor or bounded deadline"
41
+ );
42
+ }
43
+
44
+ /** Closed inputs are validated before any Docker argument is constructed. */
45
+ function validateBoundary(boundary, invocation) {
46
+ keys(boundary, [
47
+ "role",
48
+ "uid",
49
+ "gid",
50
+ "root",
51
+ "docker",
52
+ "dockerSha256",
53
+ "nonce",
54
+ "platform",
55
+ "image",
56
+ "network",
57
+ "seccompPath",
58
+ "seccompSha256",
59
+ "mounts",
60
+ "workspace",
61
+ "cwd",
62
+ "deadlineMs",
63
+ "supervisor",
64
+ ]);
65
+ supervisorIdentity(boundary);
66
+ required(
67
+ [
68
+ boundary.root,
69
+ boundary.workspace,
70
+ boundary.cwd,
71
+ boundary.seccompPath,
72
+ ].every(
73
+ path =>
74
+ typeof path === "string" &&
75
+ path.startsWith("/") &&
76
+ !/[\n\0,:]/.test(path) &&
77
+ !path.split("/").includes("..")
78
+ ) &&
79
+ (boundary.cwd === boundary.workspace ||
80
+ boundary.cwd.startsWith(`${boundary.workspace}/`)),
81
+ "invalid worker boundary path"
82
+ );
83
+ required(
84
+ (boundary.role === "gate" || isInstallerRole(boundary.role)) &&
85
+ [boundary.uid, boundary.gid].every(
86
+ value => Number.isSafeInteger(value) && value > 0 && value <= 65_535
87
+ ) &&
88
+ (isInstallerRole(boundary.role) ||
89
+ (boundary.uid === 2001 && boundary.gid === 2001)),
90
+ "invalid worker role or identity"
91
+ );
92
+ required(
93
+ typeof boundary.nonce === "string" &&
94
+ /^[a-f0-9]{48}$/.test(boundary.nonce) &&
95
+ /^sha256:[a-f0-9]{64}$/.test(boundary.image) &&
96
+ ["linux/arm64", "linux/amd64"].includes(boundary.platform),
97
+ "unqualified worker identity"
98
+ );
99
+ required(
100
+ boundary.network === "none" ||
101
+ boundary.network === `lisa-npm-${boundary.nonce}`,
102
+ "foreign worker network"
103
+ );
104
+ invocationIdentity(invocation);
105
+ }
106
+
107
+ /** A native entrypoint remains literal bounded data, never a Docker option. */
108
+ function invocationIdentity(invocation) {
109
+ required(
110
+ typeof invocation.command === "string" &&
111
+ /^\/(?:[a-zA-Z0-9_.-]+\/)*[a-zA-Z0-9_.-]+$/.test(invocation.command) &&
112
+ Array.isArray(invocation.args),
113
+ "invalid real-tool entrypoint"
114
+ );
115
+ required(
116
+ invocation.args.length <= 512 &&
117
+ invocation.args.every(
118
+ value =>
119
+ typeof value === "string" &&
120
+ !value.includes("\0") &&
121
+ Buffer.byteLength(value) <= 65_536
122
+ ) &&
123
+ Buffer.byteLength(JSON.stringify(invocation.args)) <= 262_144,
124
+ "invalid or unbounded real-tool argument"
125
+ );
126
+ }
127
+
128
+ /** Fixed kernel options cannot be supplied by a candidate as flags. */
129
+ function createArguments(boundary) {
130
+ return [
131
+ "create",
132
+ "--interactive",
133
+ "--platform",
134
+ boundary.platform,
135
+ "--read-only",
136
+ "--user",
137
+ `${boundary.uid}:${boundary.gid}`,
138
+ "--cap-drop",
139
+ "ALL",
140
+ "--security-opt",
141
+ "no-new-privileges",
142
+ "--security-opt",
143
+ `seccomp=${boundary.seccompPath}`,
144
+ "--pids-limit",
145
+ "512",
146
+ "--memory",
147
+ "4g",
148
+ "--cpus",
149
+ "4",
150
+ "--ipc",
151
+ "private",
152
+ "--network",
153
+ boundary.network,
154
+ "--label",
155
+ `dev.lisa.npm.owner=${boundary.nonce}`,
156
+ "--workdir",
157
+ boundary.cwd,
158
+ ];
159
+ }
160
+
161
+ /** Installation owns exactly one dependency-only write mount. */
162
+ function sourceProjection(boundary, mount) {
163
+ const source = mount.source;
164
+ required(
165
+ typeof source === "string" &&
166
+ resolve(source) === source &&
167
+ !/(?:^|\/)\.git(?:\/|$)|docker\.sock/.test(source),
168
+ "worker source mount is Git, socket or aliased projection"
169
+ );
170
+ const projections = [
171
+ boundary.workspace,
172
+ `${boundary.root}/source`,
173
+ `${dirname(boundary.root)}/source`,
174
+ `${dirname(boundary.root)}/workspace`,
175
+ ];
176
+ const sourceInput =
177
+ mount.readOnly &&
178
+ mount.target === boundary.workspace &&
179
+ projections.includes(source);
180
+ const dependency =
181
+ (source === `${boundary.root}/dependencies` &&
182
+ mount.target === `${boundary.workspace}/node_modules`) ||
183
+ (source === `${boundary.root}/ruby-dependencies` &&
184
+ mount.target === `${boundary.workspace}/vendor/bundle`);
185
+ required(
186
+ sourceInput || dependency,
187
+ "worker source mount exposes undeclared control or ancestor projection"
188
+ );
189
+ required(
190
+ source !== "/" &&
191
+ source !== boundary.root &&
192
+ !boundary.root.startsWith(`${source}/`),
193
+ "worker source mount exposes controller ancestor"
194
+ );
195
+ }
196
+
197
+ /** Every namespace binds only its fixed source and separate dependency cohorts. */
198
+ function mountArguments(boundary, args) {
199
+ required(
200
+ Array.isArray(boundary.mounts) &&
201
+ boundary.mounts.length > 0 &&
202
+ boundary.mounts.length <= 8,
203
+ "invalid worker mounts"
204
+ );
205
+ for (const mount of boundary.mounts) {
206
+ keys(mount, ["source", "target", "readOnly"]);
207
+ sourceProjection(boundary, mount);
208
+ const dependencyWrite =
209
+ (boundary.role === "install" &&
210
+ mount.source === `${boundary.root}/dependencies` &&
211
+ mount.target === `${boundary.workspace}/node_modules`) ||
212
+ (boundary.role === "ruby-install" &&
213
+ mount.source === `${boundary.root}/ruby-dependencies` &&
214
+ mount.target === `${boundary.workspace}/vendor/bundle`);
215
+ required(
216
+ typeof mount.readOnly === "boolean" &&
217
+ (mount.readOnly || dependencyWrite) &&
218
+ [mount.source, mount.target].every(
219
+ path =>
220
+ typeof path === "string" &&
221
+ path.startsWith("/") &&
222
+ !/[\n\0,]/.test(path)
223
+ ) &&
224
+ !/docker\.sock|\/\.git(?:\/|$)/.test(mount.target) &&
225
+ !mount.target.startsWith(boundary.root) &&
226
+ mount.source !== boundary.root,
227
+ "worker mount exposes writable/control authority"
228
+ );
229
+ args.push(
230
+ "--mount",
231
+ `type=bind,src=${mount.source},dst=${mount.target}${mount.readOnly ? ",readonly" : ""}`
232
+ );
233
+ }
234
+ }
235
+
236
+ /** Parent-qualified literal command data follows the fixed protected entrypoint. */
237
+ export function workerArguments(boundary, invocation, environment) {
238
+ validateBoundary(boundary, invocation);
239
+ const args = createArguments(boundary);
240
+ mountArguments(boundary, args);
241
+ for (const [path, policy] of Object.entries(workerScratch(boundary)))
242
+ args.push("--tmpfs", `${path}:${policy}`);
243
+ inspectedEnvironment(
244
+ Object.entries(environment).map(([name, value]) => `${name}=${value}`)
245
+ );
246
+ if (isInstallerRole(boundary.role))
247
+ required(
248
+ !Object.keys(environment).some(name =>
249
+ /^(?:GH_TOKEN|GITHUB_|DATABASE_)/.test(name)
250
+ ),
251
+ "installer contains provider or application credentials"
252
+ );
253
+ assertRubyEnvironment(environment, boundary);
254
+ for (const [name, value] of Object.entries(environment))
255
+ args.push("--env", `${name}=${value}`);
256
+ return [
257
+ ...args,
258
+ "--entrypoint",
259
+ boundary.supervisor.path,
260
+ boundary.image,
261
+ String(boundary.deadlineMs),
262
+ "--",
263
+ invocation.command,
264
+ ...invocation.args,
265
+ ];
266
+ }