@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,278 @@
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
+ /**
5
+ * @file npm-update-contract.mjs
6
+ * @description Closed data contracts prevent dependency artifacts selecting authority.
7
+ * @module npm-updater
8
+ */
9
+ import { required, keys } from "./npm-update-invariants.mjs";
10
+ export { UpdaterError, required, keys } from "./npm-update-invariants.mjs";
11
+ import { canonicalJson } from "../lisa-automation-provenance.mjs";
12
+ import { sha256 } from "./github-attestation-verifier.mjs";
13
+
14
+ export const FILES = ["package-lock.json", "package.json"];
15
+ export const SECTIONS = [
16
+ "dependencies",
17
+ "devDependencies",
18
+ "optionalDependencies",
19
+ ];
20
+ export const CLAIM =
21
+ "[lisa-tracker-claim] Claimed by Lisa. Starting implementation.";
22
+ export const NAME = /^(?:@[a-z0-9][a-z0-9_.-]*\/)?[a-z0-9][a-z0-9_.-]*$/;
23
+ export const VERSION =
24
+ /^(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)(?:-[A-Za-z0-9.-]+)?$/;
25
+ export const OBJECT = /^[a-f0-9]{40}$/;
26
+
27
+ /** Configured tracking and an explicit owner are prerequisites, never inferred. */
28
+ export function validatePolicy(value, config) {
29
+ keys(value, [
30
+ "version",
31
+ "repository",
32
+ "directory",
33
+ "target",
34
+ "maintainer",
35
+ "packages",
36
+ "lisaOwner",
37
+ ]);
38
+ required(
39
+ value.version === 1 && value.directory === "." && value.target === "main",
40
+ "only root npm on main is supported"
41
+ );
42
+ required(
43
+ config.tracker === "github" &&
44
+ value.repository === `${config.github?.org}/${config.github?.repo}`,
45
+ "configured repository/tracker differs"
46
+ );
47
+ required(
48
+ /^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/.test(value.repository),
49
+ "invalid repository"
50
+ );
51
+ required(
52
+ typeof value.maintainer === "string" &&
53
+ /^[A-Za-z0-9][A-Za-z0-9-]{0,38}$/.test(value.maintainer),
54
+ "explicit assignable maintainer required"
55
+ );
56
+ required(
57
+ ["absent", "verified-local-full-apply"].includes(value.lisaOwner),
58
+ "Lisa requires a verified local full-apply owner"
59
+ );
60
+ required(
61
+ Array.isArray(value.packages) &&
62
+ value.packages.length > 0 &&
63
+ value.packages.length <= 64,
64
+ "invalid package selection"
65
+ );
66
+ const names = new Set();
67
+ for (const selection of value.packages) {
68
+ keys(selection, ["name", "version"]);
69
+ required(
70
+ typeof selection.name === "string" &&
71
+ selection.name.length <= 214 &&
72
+ NAME.test(selection.name),
73
+ "invalid npm name"
74
+ );
75
+ required(
76
+ typeof selection.version === "string" && VERSION.test(selection.version),
77
+ "exact registry version required"
78
+ );
79
+ required(
80
+ !names.has(selection.name) && selection.name !== "@codyswann/lisa",
81
+ "duplicate selection or Lisa version-only update"
82
+ );
83
+ names.add(selection.name);
84
+ }
85
+ return structuredClone(value);
86
+ }
87
+
88
+ /** Host declarations are checked before npm or lifecycle execution. */
89
+ export function validateHost(before) {
90
+ required(
91
+ !before.workspaces &&
92
+ (!before.packageManager ||
93
+ /^npm@\d+\.\d+\.\d+$/.test(before.packageManager)),
94
+ "unsupported workspace or package manager"
95
+ );
96
+ required(
97
+ !["npm", "node"].some(name =>
98
+ /please-use|do-not-use/.test(before.engines?.[name] ?? "")
99
+ ),
100
+ "engines forbid npm or Node"
101
+ );
102
+ }
103
+
104
+ /** Script changes cannot hide in a dependency diff. */
105
+ export function validateManifest(before, after, updates, policy) {
106
+ validateHost(before);
107
+ const copy = structuredClone(before);
108
+ const seen = new Set();
109
+ required(
110
+ Array.isArray(updates) && updates.length > 0 && updates.length <= 64,
111
+ "empty or oversize update set"
112
+ );
113
+ for (const update of updates) {
114
+ keys(update, ["name", "section", "from", "to"]);
115
+ const selection = policy.packages.find(item => item.name === update.name);
116
+ required(
117
+ selection &&
118
+ update.to === selection.version &&
119
+ SECTIONS.includes(update.section),
120
+ "update outside trusted selection"
121
+ );
122
+ required(
123
+ typeof update.from === "string" &&
124
+ VERSION.test(update.from.replace(/^[~^]/, "")) &&
125
+ update.to !== update.from,
126
+ "invalid or unchanged version"
127
+ );
128
+ required(
129
+ !seen.has(update.name) &&
130
+ copy[update.section]?.[update.name] === update.from,
131
+ "duplicate or stale update"
132
+ );
133
+ seen.add(update.name);
134
+ copy[update.section][update.name] = update.to;
135
+ }
136
+ required(
137
+ canonicalJson(copy) === canonicalJson(after),
138
+ "undeclared manifest changes"
139
+ );
140
+ }
141
+
142
+ /** Public registry URLs cannot smuggle credentials or executable protocols. */
143
+ export function validateLock(lock, manifest, updates) {
144
+ required(
145
+ [2, 3].includes(lock.lockfileVersion) && lock.packages?.[""],
146
+ "supported npm lock required"
147
+ );
148
+ required(
149
+ lock.name === manifest.name && lock.version === manifest.version,
150
+ "lock root differs"
151
+ );
152
+ for (const section of SECTIONS)
153
+ required(
154
+ canonicalJson(lock.packages[""][section] ?? {}) ===
155
+ canonicalJson(manifest[section] ?? {}),
156
+ "lock root dependency set differs"
157
+ );
158
+ for (const [name, item] of Object.entries(lock.packages)) {
159
+ if (name === "") continue;
160
+ required(
161
+ name.startsWith("node_modules/") &&
162
+ !name.split("/").includes("..") &&
163
+ !item.link,
164
+ "unsupported lock node"
165
+ );
166
+ const url = new URL(item.resolved);
167
+ required(
168
+ url.protocol === "https:" &&
169
+ url.hostname === "registry.npmjs.org" &&
170
+ !url.username &&
171
+ !url.password &&
172
+ !url.port &&
173
+ !url.search &&
174
+ !url.hash,
175
+ "nonregistry or credential-bearing lock"
176
+ );
177
+ required(
178
+ typeof item.integrity === "string" &&
179
+ /^sha512-[A-Za-z0-9+/]+={0,2}$/.test(item.integrity) &&
180
+ Buffer.from(item.integrity.slice(7), "base64").length === 64 &&
181
+ Buffer.from(item.integrity.slice(7), "base64").toString("base64") ===
182
+ item.integrity.slice(7),
183
+ "registry integrity missing"
184
+ );
185
+ }
186
+ for (const update of updates)
187
+ required(
188
+ lock.packages[`node_modules/${update.name}`]?.version === update.to,
189
+ "lock target differs"
190
+ );
191
+ }
192
+
193
+ /** Base-independent discovery prevents a main advance creating a duplicate leaf. */
194
+ export function selectionKey(policy, updates) {
195
+ return sha256(
196
+ canonicalJson({
197
+ repository: policy.repository,
198
+ target: "main",
199
+ ecosystem: "npm",
200
+ directory: ".",
201
+ policySha256: sha256(canonicalJson(policy)),
202
+ versions: updates
203
+ .map(({ name, to }) => ({ name, version: to }))
204
+ .sort((a, b) => a.name.localeCompare(b.name)),
205
+ })
206
+ );
207
+ }
208
+
209
+ /** Recovery binds policy as well as versions; the hook's signed proposal key is separate. */
210
+ export function proposalFrom(policy, parent, before, files, updates) {
211
+ required(OBJECT.test(parent), "unsupported Git parent or object format");
212
+ keys(files, FILES);
213
+ for (const value of Object.values(files))
214
+ required(
215
+ typeof value === "string" && Buffer.byteLength(value) <= 1_048_576,
216
+ "oversize proposal file"
217
+ );
218
+ const after = JSON.parse(files["package.json"]);
219
+ const lock = JSON.parse(files["package-lock.json"]);
220
+ validateManifest(before, after, updates, policy);
221
+ validateLock(lock, after, updates);
222
+ const ordered = [...updates].sort((a, b) => a.name.localeCompare(b.name));
223
+ const identity = {
224
+ repository: policy.repository,
225
+ target: policy.target,
226
+ ecosystem: "npm",
227
+ directory: ".",
228
+ parent,
229
+ policySha256: sha256(canonicalJson(policy)),
230
+ updates: ordered,
231
+ };
232
+ return {
233
+ version: 1,
234
+ ...identity,
235
+ selectionKey: selectionKey(policy, ordered),
236
+ key: sha256(canonicalJson(identity)),
237
+ bindingKey: sha256(
238
+ canonicalJson({ repository: policy.repository, parent, updates: ordered })
239
+ ),
240
+ before,
241
+ files,
242
+ hashes: Object.fromEntries(FILES.map(file => [file, sha256(files[file])])),
243
+ };
244
+ }
245
+
246
+ /** Recompute every candidate field rather than accepting its claimed digest. */
247
+ export function validateProposal(value, policy) {
248
+ keys(value, [
249
+ "version",
250
+ "repository",
251
+ "target",
252
+ "ecosystem",
253
+ "directory",
254
+ "parent",
255
+ "policySha256",
256
+ "updates",
257
+ "key",
258
+ "selectionKey",
259
+ "bindingKey",
260
+ "before",
261
+ "files",
262
+ "hashes",
263
+ ]);
264
+ const expected = proposalFrom(
265
+ policy,
266
+ value.parent,
267
+ value.before,
268
+ value.files,
269
+ value.updates
270
+ );
271
+ required(
272
+ canonicalJson(value) === canonicalJson(expected),
273
+ "proposal identity or bytes differ"
274
+ );
275
+ return expected;
276
+ }
277
+
278
+ export { validateRawCommit } from "./npm-update-object.mjs";
@@ -0,0 +1,234 @@
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
+ /** Private native transport executes immutable controller recipes, never request-supplied authority. */
5
+ import { createServer } from "node:net";
6
+ import { chmodSync, lstatSync, realpathSync, unlinkSync } from "node:fs";
7
+ import { join } from "node:path";
8
+ import { required, keys } from "./npm-update-contract.mjs";
9
+ import { binaryDigest } from "./npm-update-isolation.mjs";
10
+ import { currentGraph } from "./npm-update-orchestrator.mjs";
11
+ import {
12
+ checkedRecipe,
13
+ executeRecipe,
14
+ } from "./npm-update-controller-recipe.mjs";
15
+
16
+ /** Only the controller supplies independently authenticated graph and immutable phase recipes. */
17
+ function recipesFor(authority) {
18
+ const { root, node, graph, deadline, recipes } = authority;
19
+ const stat = lstatSync(root);
20
+ required(
21
+ stat.isDirectory() &&
22
+ !stat.isSymbolicLink() &&
23
+ realpathSync(root) === root &&
24
+ stat.uid === process.getuid() &&
25
+ (stat.mode & 0o077) === 0,
26
+ "broker root is not private controller storage"
27
+ );
28
+ required(
29
+ Number.isSafeInteger(deadline) &&
30
+ deadline > Date.now() &&
31
+ deadline <= Date.now() + 1_800_000,
32
+ "invalid broker lifetime"
33
+ );
34
+ required(
35
+ node.path === realpathSync(node.path) &&
36
+ binaryDigest(node.path) === node.sha256,
37
+ "broker controller Node identity differs"
38
+ );
39
+ required(
40
+ graph && Object.keys(graph).length > 0 && Object.keys(graph).length <= 128,
41
+ "broker graph is absent or unbounded"
42
+ );
43
+ required(
44
+ Array.isArray(recipes) && recipes.length > 0 && recipes.length <= 64,
45
+ "broker recipes are absent or unbounded"
46
+ );
47
+ const selected = new Map();
48
+ for (const recipe of recipes) {
49
+ required(!selected.has(recipe.id), "duplicate broker recipe");
50
+ selected.set(recipe.id, checkedRecipe(recipe, node, graph));
51
+ }
52
+ return { selected, graph: { ...graph }, node: { ...node }, deadline, root };
53
+ }
54
+
55
+ function actualGraph(expected) {
56
+ const observed = currentGraph(expected);
57
+ required(
58
+ Object.entries(expected).every(([path, hash]) => observed[path] === hash),
59
+ "broker authenticated helper graph changed"
60
+ );
61
+ }
62
+
63
+ function response(result) {
64
+ return {
65
+ ok: true,
66
+ code: result.code,
67
+ stdout: result.stdout.toString("base64"),
68
+ stderr: result.stderr.toString("base64"),
69
+ };
70
+ }
71
+
72
+ /** Unix pathname permissions protect access; Node does not claim SO_PEERCRED observation. */
73
+ export async function startControllerBroker(authority) {
74
+ const { selected, graph, node, deadline, root } = recipesFor(authority);
75
+ actualGraph(graph);
76
+ const path = join(root, "controller.sock");
77
+ required(
78
+ Buffer.byteLength(path) <= 103,
79
+ "broker socket path exceeds native bound"
80
+ );
81
+ const connections = new Set();
82
+ const running = new Set();
83
+ const state = { closing: false };
84
+ const server = brokerServer({
85
+ state,
86
+ connections,
87
+ running,
88
+ selected,
89
+ graph,
90
+ node,
91
+ deadline,
92
+ });
93
+ const identity = await listenSocket(server, path);
94
+ let completion;
95
+ const finish = async () => {
96
+ state.closing = true;
97
+ clearTimeout(timer);
98
+ for (const socket of connections) socket.destroy();
99
+ await new Promise(resolve => server.close(resolve));
100
+ await Promise.allSettled(running);
101
+ try {
102
+ const current = lstatSync(path);
103
+ required(
104
+ current.dev === identity.dev &&
105
+ current.ino === identity.ino &&
106
+ current.isSocket(),
107
+ "broker cleanup ownership changed"
108
+ );
109
+ unlinkSync(path);
110
+ } catch (error) {
111
+ if (error.code !== "ENOENT") throw error;
112
+ }
113
+ };
114
+ const close = () => (completion ??= finish());
115
+ const timer = setTimeout(() => {
116
+ close().catch(() => {});
117
+ }, deadline - Date.now());
118
+ timer.unref();
119
+ return { path, close };
120
+ }
121
+
122
+ /** A failure after native bind still closes the owned listener before returning a refusal. */
123
+ async function listenSocket(server, path) {
124
+ let identity;
125
+ try {
126
+ await new Promise((resolve, reject) => {
127
+ server.once("error", reject);
128
+ server.listen(path, resolve);
129
+ });
130
+ identity = lstatSync(path);
131
+ required(
132
+ identity.isSocket() && identity.uid === process.getuid(),
133
+ "broker socket ownership differs"
134
+ );
135
+ chmodSync(path, 0o600);
136
+ required(
137
+ (lstatSync(path).mode & 0o077) === 0,
138
+ "broker socket permissions differ"
139
+ );
140
+ return identity;
141
+ } catch (error) {
142
+ if (server.listening) await new Promise(resolve => server.close(resolve));
143
+ if (identity) {
144
+ try {
145
+ const current = lstatSync(path);
146
+ required(
147
+ current.dev === identity.dev && current.ino === identity.ino,
148
+ "broker startup cleanup ownership changed"
149
+ );
150
+ unlinkSync(path);
151
+ } catch (cleanup) {
152
+ if (cleanup.code !== "ENOENT") throw cleanup;
153
+ }
154
+ }
155
+ throw error;
156
+ }
157
+ }
158
+
159
+ function brokerServer({
160
+ state,
161
+ connections,
162
+ running,
163
+ selected,
164
+ graph,
165
+ node,
166
+ deadline,
167
+ }) {
168
+ return createServer({ allowHalfOpen: true }, socket => {
169
+ if (state.closing || connections.size >= 64) return socket.destroy();
170
+ connections.add(socket);
171
+ const cancel = new AbortController();
172
+ socket.setTimeout(Math.min(10_000, deadline - Date.now()));
173
+ socket.on("timeout", () => socket.destroy());
174
+ socket.on("error", () => {});
175
+ socket.on("close", () => {
176
+ cancel.abort();
177
+ connections.delete(socket);
178
+ });
179
+ let bytes = Buffer.alloc(0);
180
+ socket.on("data", chunk => {
181
+ if (bytes.length + chunk.length > 1024) return socket.destroy();
182
+ bytes = Buffer.concat([bytes, chunk]);
183
+ });
184
+ socket.on("end", () => {
185
+ socket.setTimeout(0);
186
+ const operation = handleRequest(
187
+ socket,
188
+ bytes,
189
+ selected,
190
+ graph,
191
+ deadline,
192
+ cancel.signal,
193
+ node
194
+ );
195
+ running.add(operation);
196
+ operation.finally(() => running.delete(operation));
197
+ });
198
+ });
199
+ }
200
+
201
+ /** Requests supply only a recipe identifier; command/caller/phase/env/input never cross this boundary. */
202
+ async function handleRequest(
203
+ socket,
204
+ bytes,
205
+ recipes,
206
+ graph,
207
+ deadline,
208
+ signal,
209
+ node
210
+ ) {
211
+ let output;
212
+ try {
213
+ const text = new TextDecoder("utf8", { fatal: true }).decode(bytes);
214
+ required(
215
+ text.endsWith("\n") && text.indexOf("\n") === text.length - 1,
216
+ "invalid broker framing"
217
+ );
218
+ const request = JSON.parse(text);
219
+ keys(request, ["recipe"]);
220
+ required(
221
+ text === `${JSON.stringify(request)}\n` &&
222
+ typeof request.recipe === "string" &&
223
+ recipes.has(request.recipe),
224
+ "unknown or ambiguous broker recipe"
225
+ );
226
+ actualGraph(graph);
227
+ const recipe = recipes.get(request.recipe);
228
+ recipes.delete(request.recipe);
229
+ output = response(await executeRecipe(recipe, deadline, signal, node));
230
+ } catch {
231
+ output = { ok: false, error: "controller request refused" };
232
+ }
233
+ if (!socket.destroyed) socket.end(`${JSON.stringify(output)}\n`);
234
+ }