@zq-silk/yui 0.6.0 → 0.6.2

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 (150) hide show
  1. package/README.md +5 -5
  2. package/dist/agent/managedRuntimeEnvironment.js +2 -1
  3. package/dist/cli/commandCatalog.js +251 -13
  4. package/dist/cli/updateOrchestrator.js +8 -0
  5. package/dist/cli/updatePorts.js +76 -22
  6. package/dist/cli.js +264 -20
  7. package/dist/commands/configCommands.js +83 -9
  8. package/dist/commands/controllerCommands.js +103 -0
  9. package/dist/commands/deliveryGuardPreflight.js +30 -0
  10. package/dist/commands/durableJobCommands.js +231 -0
  11. package/dist/commands/executionAuditCommands.js +193 -0
  12. package/dist/commands/grantCommands.js +374 -0
  13. package/dist/commands/projectCommands.js +119 -81
  14. package/dist/commands/releaseCommands.js +444 -0
  15. package/dist/commands/resourcesCommands.js +274 -0
  16. package/dist/commands/sessionCommands.js +104 -0
  17. package/dist/commands/taskActor.js +117 -0
  18. package/dist/commands/taskChangeSetCommands.js +60 -0
  19. package/dist/commands/taskCommands.js +618 -202
  20. package/dist/commands/taskCompletionGate.js +78 -1
  21. package/dist/commands/taskContextCommand.js +33 -6
  22. package/dist/commands/taskInputCommands.js +1 -1
  23. package/dist/commands/taskIntegrationCommands.js +136 -33
  24. package/dist/commands/taskIntegrationQueueCommands.js +228 -0
  25. package/dist/commands/taskNextActionCommand.js +100 -0
  26. package/dist/commands/taskOverlapCommands.js +120 -0
  27. package/dist/commands/taskOverviewCommand.js +36 -8
  28. package/dist/commands/telemetryCommands.js +330 -0
  29. package/dist/commands/workflowCommands.js +415 -0
  30. package/dist/config/yuiConfig.js +62 -0
  31. package/dist/controller/clientRuntime.js +42 -1
  32. package/dist/controller/controller.js +402 -56
  33. package/dist/controller/controllerMain.js +25 -2
  34. package/dist/controller/domainIdentity.js +16 -8
  35. package/dist/controller/fileSchedulerStoreAdapter.js +423 -31
  36. package/dist/controller/handoverCandidate.js +168 -0
  37. package/dist/controller/jobClient.js +102 -0
  38. package/dist/controller/jobControl.js +613 -0
  39. package/dist/controller/jobSupervisor.js +498 -0
  40. package/dist/controller/providerHookRunFence.js +34 -5
  41. package/dist/controller/resourceCleanupLinux.js +18 -9
  42. package/dist/controller/resourceInventoryLinux.js +90 -39
  43. package/dist/controller/runtime.js +165 -15
  44. package/dist/controller/runtimeEventInbox.js +234 -57
  45. package/dist/controller/runtimeEventProcessor.js +297 -58
  46. package/dist/controller/sessionOwnerReconciliation.js +321 -0
  47. package/dist/core/controllerServer.js +416 -27
  48. package/dist/core/controllerTelemetry.js +167 -0
  49. package/dist/doctor/doctor.js +113 -16
  50. package/dist/domain/validation.js +9 -0
  51. package/dist/execution/executionGroup.js +40 -3
  52. package/dist/executor/agentExecutor.js +6 -3
  53. package/dist/executor/effectiveLaunch.js +52 -0
  54. package/dist/executor/executorRegistry.js +50 -0
  55. package/dist/executor/fileRoleLaunchPlanner.js +61 -6
  56. package/dist/grant/capabilityGrant.js +282 -0
  57. package/dist/integration/changeSet.js +16 -3
  58. package/dist/integration/changeSetManifest.js +46 -0
  59. package/dist/integration/gitIntegrationService.js +528 -147
  60. package/dist/integration/integrationAttempt.js +54 -5
  61. package/dist/integration/integrationQueueEntry.js +221 -0
  62. package/dist/integration/integrationQueueService.js +955 -0
  63. package/dist/integration/manifestTags.js +99 -0
  64. package/dist/integration/overlapDiagnostics.js +211 -0
  65. package/dist/job/durableJob.js +449 -0
  66. package/dist/job/jobRunner.js +350 -0
  67. package/dist/lifecycle/exactRunTerminalization.js +24 -2
  68. package/dist/lifecycle/providerErrorClass.js +126 -0
  69. package/dist/message/message.js +16 -3
  70. package/dist/observability/executionAudit.js +545 -0
  71. package/dist/observability/faultClassification.js +160 -0
  72. package/dist/observability/runtimeIdentity.js +367 -0
  73. package/dist/release/fakeReleasePorts.js +55 -0
  74. package/dist/release/releaseHandover.js +475 -0
  75. package/dist/release/releaseIdempotencyStore.js +165 -0
  76. package/dist/release/releaseWorkflow.js +459 -0
  77. package/dist/release/releaseWorkflowEngine.js +688 -0
  78. package/dist/release/releaseWorkflowPorts.js +1720 -0
  79. package/dist/release/runtimeRelease.js +495 -0
  80. package/dist/release/workflowFileLock.js +218 -0
  81. package/dist/repository/gitWorkspace.js +177 -1
  82. package/dist/repository/projectMaintenanceLock.js +315 -0
  83. package/dist/repository/taskWorkspaceCoordinator.js +87 -17
  84. package/dist/repository/taskWorkspacePreparer.js +1091 -517
  85. package/dist/resources/autoResourceGc.js +116 -0
  86. package/dist/resources/liveReferences.js +574 -0
  87. package/dist/resources/resourceDiscovery.js +477 -0
  88. package/dist/resources/resourceGc.js +645 -0
  89. package/dist/resources/resourceRegistrar.js +256 -0
  90. package/dist/resources/resourceRegistry.js +150 -0
  91. package/dist/resources/resourceRegistryStore.js +41 -0
  92. package/dist/resources/resourceTypes.js +42 -0
  93. package/dist/resources/sqliteResourceRegistry.js +111 -0
  94. package/dist/review/reviewConfig.js +10 -0
  95. package/dist/review/reviewFinding.js +240 -0
  96. package/dist/review/reviewFindingLedger.js +545 -0
  97. package/dist/review/reviewOutcomeClassifier.js +61 -0
  98. package/dist/review/reviewRound.js +56 -4
  99. package/dist/run/agentRun.js +80 -4
  100. package/dist/run/providerRetry.js +84 -0
  101. package/dist/run/providerRetryConfig.js +63 -0
  102. package/dist/run/yieldReceipt.js +65 -0
  103. package/dist/runtime/exactControlPlane.js +79 -2
  104. package/dist/runtime/index.js +4 -0
  105. package/dist/runtime/sessionOwnerIdentity.js +269 -0
  106. package/dist/runtime/sessionOwnerRegistry.js +132 -0
  107. package/dist/runtime/sessionReconciliation.js +93 -0
  108. package/dist/runtime/sessionTerminationGuard.js +211 -0
  109. package/dist/runtime/taskRuntimeIsolation.js +13 -0
  110. package/dist/runtime/tmuxAdapters.js +34 -1
  111. package/dist/scheduler/actionability.js +155 -0
  112. package/dist/scheduler/activeRoleRunDelivery.js +14 -5
  113. package/dist/scheduler/activeTaskProgress.js +60 -0
  114. package/dist/scheduler/leaderWakeupProcessor.js +22 -11
  115. package/dist/scheduler/roleRunStall.js +135 -29
  116. package/dist/scheduler/taskExecutionProjection.js +11 -0
  117. package/dist/storage/compatibleTaskStore.js +112 -5
  118. package/dist/storage/migration/productionRegistry.js +736 -1
  119. package/dist/storage/sqliteSchema.js +264 -3
  120. package/dist/storage/sqliteStore.js +487 -13
  121. package/dist/storage/storeRpc.js +21 -0
  122. package/dist/storage/taskStore.js +974 -21
  123. package/dist/storage/upgrade/homeClassification.js +120 -2
  124. package/dist/storage/upgrade/migrationReceipt.js +67 -0
  125. package/dist/storage/upgrade/pseudoLayoutRepair.js +241 -0
  126. package/dist/storage/upgrade/recordVersions.js +10 -1
  127. package/dist/storage/upgrade/sqliteMigrationTarget.js +58 -6
  128. package/dist/storage/upgrade/sqliteRecordMigrationTarget.js +290 -0
  129. package/dist/storage/upgrade/sqliteStateMigration.js +258 -2
  130. package/dist/storage/upgrade/upgradeOrchestrator.js +482 -16
  131. package/dist/task/deliveryGuard.js +226 -0
  132. package/dist/task/nextAction.js +738 -0
  133. package/dist/task/repairWave.js +137 -0
  134. package/dist/task/taskRecordReference.js +6 -1
  135. package/dist/telemetry/sqliteTelemetryStore.js +387 -0
  136. package/dist/telemetry/telemetryCompaction.js +251 -0
  137. package/dist/telemetry/telemetryConfig.js +64 -0
  138. package/dist/telemetry/telemetryRouter.js +32 -0
  139. package/dist/telemetry/telemetryStore.js +19 -0
  140. package/dist/telemetry/telemetryWiring.js +33 -0
  141. package/dist/tmux/tmuxManager.js +20 -1
  142. package/dist/tmux/tmuxSocketEndpoint.js +20 -0
  143. package/dist/verification/gateArtifact.js +216 -0
  144. package/dist/verification/gateArtifactStore.js +87 -0
  145. package/dist/verification/verificationGateService.js +414 -0
  146. package/dist/verification/verificationPlan.js +308 -0
  147. package/dist/workspace/gitChangeSetCapture.js +12 -2
  148. package/dist/workspace/workItemChangeSetManager.js +60 -3
  149. package/package.json +1 -1
  150. package/skills/yui-leader/SKILL.md +8 -0
@@ -0,0 +1,495 @@
1
+ /**
2
+ * Immutable local runtime releases and Controller handover provenance
3
+ * (Issue 02).
4
+ *
5
+ * A release is a self-contained, content-addressed runtime package unpacked
6
+ * below `runtime/releases/<version>-<package-sha256>/`. The Home's active
7
+ * release pointer (`runtime/active-release.json`) names exactly one release;
8
+ * the stable launcher shim resolves the CLI through that pointer instead of
9
+ * symlinking a development checkout.
10
+ *
11
+ * The Controller writes its provenance to `runtime/runtime-identity.json` on
12
+ * every start, and a versioned handover fence (`runtime/handover-fence.json`)
13
+ * plus receipt (`runtime/handover.json`) make a Controller replacement
14
+ * deterministic and recoverable.
15
+ */
16
+ import { createHash, randomBytes, randomUUID } from "node:crypto";
17
+ import { existsSync, lstatSync, mkdirSync, readdirSync, readFileSync, readlinkSync, rmSync, writeFileSync } from "node:fs";
18
+ import { dirname, join, resolve } from "node:path";
19
+ import { readLinuxProcessStartIdentity } from "../controller/domainIdentity.js";
20
+ import { writeTextFileAtomically } from "../storage/durableFile.js";
21
+ export const RELEASES_DIRECTORY = "runtime/releases";
22
+ export const ACTIVE_RELEASE_POINTER_PATH = "runtime/active-release.json";
23
+ export const RUNTIME_IDENTITY_PATH = "runtime/runtime-identity.json";
24
+ export const HANDOVER_FENCE_PATH = "runtime/handover-fence.json";
25
+ export const HANDOVER_RECEIPT_PATH = "runtime/handover.json";
26
+ export const CANDIDATE_DISCOVERY_PATH = "runtime/controller-candidate.json";
27
+ export const RELEASE_MANIFEST_FILE = "release-manifest.json";
28
+ export const SMOKE_RECEIPT_FILE = "smoke-receipt.json";
29
+ /** `<version>-<package-sha256>` — the immutable release directory name. */
30
+ export function releaseDirectoryName(manifest) {
31
+ return `${manifest.version}-${manifest.packageDigest}`;
32
+ }
33
+ export function releasesDirectory(home) {
34
+ return join(resolve(home), RELEASES_DIRECTORY);
35
+ }
36
+ export function releaseDirectoryFor(home, manifest) {
37
+ return join(releasesDirectory(home), releaseDirectoryName(manifest));
38
+ }
39
+ /** Reads and validates a release manifest. Fails closed on any drift. */
40
+ export function readReleaseManifest(releaseDir) {
41
+ const manifestPath = join(releaseDir, RELEASE_MANIFEST_FILE);
42
+ let parsed;
43
+ try {
44
+ parsed = JSON.parse(readFileSync(manifestPath, "utf8"));
45
+ }
46
+ catch (error) {
47
+ throw new Error(`Runtime release manifest is unreadable: ${manifestPath}.`, { cause: error });
48
+ }
49
+ return validateManifest(parsed);
50
+ }
51
+ /**
52
+ * Verifies an installed release against its manifest: every listed file must
53
+ * exist with the exact SHA-256 and byte length, the tree must contain no
54
+ * unlisted files, and the package digest must match. Any drift fails closed.
55
+ */
56
+ export function verifyReleaseIntegrity(releaseDir) {
57
+ const manifest = readReleaseManifest(releaseDir);
58
+ const actual = listReleaseFiles(releaseDir)
59
+ .map((name) => inventoryFile(releaseDir, name));
60
+ const expected = [...manifest.files].sort(compareFiles);
61
+ if (actual.length !== expected.length) {
62
+ throw new Error(`Runtime release file count drifted from its manifest `
63
+ + `(expected ${expected.length}, found ${actual.length}).`);
64
+ }
65
+ for (let index = 0; index < expected.length; index += 1) {
66
+ const expectedFile = expected[index];
67
+ const actualFile = actual[index];
68
+ if (expectedFile.path !== actualFile.path
69
+ || expectedFile.sha256 !== actualFile.sha256
70
+ || expectedFile.bytes !== actualFile.bytes) {
71
+ throw new Error(`Runtime release content drifted from its manifest: ${expectedFile.path}.`);
72
+ }
73
+ }
74
+ if (computePackageDigest(actual) !== manifest.packageDigest) {
75
+ throw new Error("Runtime release package digest does not match its manifest.");
76
+ }
77
+ return manifest;
78
+ }
79
+ /**
80
+ * A release directory must be immutable and self-contained: no Git worktree
81
+ * metadata at its root and no symbolic links anywhere below it.
82
+ */
83
+ export function assertReleaseIsNotWorktreeOrLinked(releaseDir) {
84
+ const root = resolve(releaseDir);
85
+ for (const marker of [".git", ".gitfile"]) {
86
+ if (existsSync(join(root, marker))) {
87
+ throw new Error(`Runtime release directory must not be a Git worktree: ${root} `
88
+ + `(found ${marker}).`);
89
+ }
90
+ }
91
+ assertNoSymlinks(root, root);
92
+ }
93
+ export function readSmokeReceipt(releaseDir) {
94
+ const receiptPath = join(releaseDir, SMOKE_RECEIPT_FILE);
95
+ if (!existsSync(receiptPath))
96
+ return null;
97
+ try {
98
+ const parsed = JSON.parse(readFileSync(receiptPath, "utf8"));
99
+ if (parsed === null
100
+ || typeof parsed !== "object"
101
+ || parsed.schemaVersion !== 1
102
+ || typeof parsed.buildId !== "string"
103
+ || typeof parsed.packageDigest !== "string") {
104
+ return null;
105
+ }
106
+ return parsed;
107
+ }
108
+ catch {
109
+ return null;
110
+ }
111
+ }
112
+ export function writeSmokeReceipt(releaseDir, receipt) {
113
+ writeTextFileAtomically(join(releaseDir, SMOKE_RECEIPT_FILE), `${JSON.stringify(receipt, null, 2)}\n`);
114
+ }
115
+ export function readActiveReleasePointer(home) {
116
+ const pointerPath = join(resolve(home), ACTIVE_RELEASE_POINTER_PATH);
117
+ if (!existsSync(pointerPath))
118
+ return null;
119
+ let parsed;
120
+ try {
121
+ parsed = JSON.parse(readFileSync(pointerPath, "utf8"));
122
+ }
123
+ catch (error) {
124
+ throw new Error(`Active release pointer is unreadable: ${pointerPath}.`, { cause: error });
125
+ }
126
+ return validatePointer(parsed);
127
+ }
128
+ export function writeActiveReleasePointer(home, pointer) {
129
+ validatePointer(pointer);
130
+ writeTextFileAtomically(join(resolve(home), ACTIVE_RELEASE_POINTER_PATH), `${JSON.stringify(pointer, null, 2)}\n`);
131
+ }
132
+ /**
133
+ * Resolves the Home's active release. Returns null when no pointer exists
134
+ * (legacy/dev Home). Fails closed when the pointer names a release that is
135
+ * missing or drifted.
136
+ */
137
+ export function resolveActiveRelease(home) {
138
+ const pointer = readActiveReleasePointer(home);
139
+ if (pointer === null)
140
+ return null;
141
+ const releaseDir = join(releasesDirectory(home), pointer.releaseId);
142
+ if (!existsSync(releaseDir)) {
143
+ throw new Error(`Active release ${pointer.releaseId} is not installed; reinstall it or `
144
+ + "point the active release pointer at an installed release.");
145
+ }
146
+ const manifest = verifyReleaseIntegrity(releaseDir);
147
+ if (manifest.packageDigest !== pointer.packageDigest || manifest.buildId !== pointer.buildId) {
148
+ throw new Error(`Active release ${pointer.releaseId} does not match its pointer; reinstall the release.`);
149
+ }
150
+ return { releaseDir, manifest, pointer };
151
+ }
152
+ /**
153
+ * Detects the release a running script belongs to by walking up from the
154
+ * script's directory until a release manifest sits next to a `dist/cli.js`.
155
+ * Returns null for a development checkout.
156
+ */
157
+ export function detectRunningRelease(scriptPath) {
158
+ let directory = dirname(resolve(scriptPath));
159
+ for (;;) {
160
+ const manifestPath = join(directory, RELEASE_MANIFEST_FILE);
161
+ const cliEntry = join(directory, "dist", "cli.js");
162
+ if (existsSync(manifestPath) && existsSync(cliEntry)) {
163
+ return { releaseDir: directory, manifest: readReleaseManifest(directory) };
164
+ }
165
+ const parent = dirname(directory);
166
+ if (parent === directory)
167
+ return null;
168
+ directory = parent;
169
+ }
170
+ }
171
+ export function readRuntimeIdentity(home) {
172
+ const identityPath = join(resolve(home), RUNTIME_IDENTITY_PATH);
173
+ if (!existsSync(identityPath))
174
+ return null;
175
+ try {
176
+ return validateRuntimeIdentity(JSON.parse(readFileSync(identityPath, "utf8")));
177
+ }
178
+ catch (error) {
179
+ throw new Error(`Runtime identity receipt is unreadable: ${identityPath}.`, { cause: error });
180
+ }
181
+ }
182
+ export function writeRuntimeIdentity(home, receipt) {
183
+ validateRuntimeIdentity(receipt);
184
+ writeTextFileAtomically(join(resolve(home), RUNTIME_IDENTITY_PATH), `${JSON.stringify(receipt, null, 2)}\n`);
185
+ }
186
+ export function readHandoverFence(home) {
187
+ const fencePath = join(resolve(home), HANDOVER_FENCE_PATH);
188
+ if (!existsSync(fencePath))
189
+ return null;
190
+ try {
191
+ return validateHandoverFence(JSON.parse(readFileSync(fencePath, "utf8")));
192
+ }
193
+ catch (error) {
194
+ throw new Error(`Handover fence is unreadable: ${fencePath}.`, { cause: error });
195
+ }
196
+ }
197
+ export function writeHandoverFence(home, fence) {
198
+ validateHandoverFence(fence);
199
+ writeTextFileAtomically(join(resolve(home), HANDOVER_FENCE_PATH), `${JSON.stringify(fence, null, 2)}\n`);
200
+ }
201
+ export function removeHandoverFence(home) {
202
+ rmSync(join(resolve(home), HANDOVER_FENCE_PATH), { force: true });
203
+ }
204
+ export function readHandoverReceipt(home) {
205
+ const receiptPath = join(resolve(home), HANDOVER_RECEIPT_PATH);
206
+ if (!existsSync(receiptPath))
207
+ return null;
208
+ try {
209
+ const parsed = JSON.parse(readFileSync(receiptPath, "utf8"));
210
+ if (parsed === null
211
+ || typeof parsed !== "object"
212
+ || parsed.schemaVersion !== 1
213
+ || typeof parsed.handoverId !== "string") {
214
+ return null;
215
+ }
216
+ return parsed;
217
+ }
218
+ catch {
219
+ return null;
220
+ }
221
+ }
222
+ export function writeHandoverReceipt(home, receipt) {
223
+ writeTextFileAtomically(join(resolve(home), HANDOVER_RECEIPT_PATH), `${JSON.stringify(receipt, null, 2)}\n`);
224
+ }
225
+ /** True when the exact process (PID + start identity) is still alive. */
226
+ export function isOwnerLive(owner) {
227
+ return readLinuxProcessStartIdentity(owner.pid) === owner.processStartIdentity;
228
+ }
229
+ export function readCandidateDiscovery(home) {
230
+ const candidatePath = join(resolve(home), CANDIDATE_DISCOVERY_PATH);
231
+ if (!existsSync(candidatePath))
232
+ return null;
233
+ try {
234
+ const parsed = JSON.parse(readFileSync(candidatePath, "utf8"));
235
+ if (typeof parsed.pid !== "number"
236
+ || !Number.isSafeInteger(parsed.pid)
237
+ || parsed.pid <= 0
238
+ || typeof parsed.processStartIdentity !== "string"
239
+ || !/^[0-9]{1,32}$/u.test(parsed.processStartIdentity)) {
240
+ return null;
241
+ }
242
+ return { pid: parsed.pid, processStartIdentity: parsed.processStartIdentity };
243
+ }
244
+ catch {
245
+ return null;
246
+ }
247
+ }
248
+ export function writeCandidateDiscovery(home, candidate) {
249
+ writeTextFileAtomically(join(resolve(home), CANDIDATE_DISCOVERY_PATH), `${JSON.stringify(candidate, null, 2)}\n`);
250
+ }
251
+ export function removeCandidateDiscovery(home) {
252
+ rmSync(join(resolve(home), CANDIDATE_DISCOVERY_PATH), { force: true });
253
+ }
254
+ export function acquireHandoverLock(home) {
255
+ const lockPath = join(resolve(home), "runtime", "handover.lock");
256
+ mkdirSync(dirname(lockPath), { recursive: true, mode: 0o700 });
257
+ const startIdentity = readLinuxProcessStartIdentity(process.pid);
258
+ if (startIdentity === undefined) {
259
+ throw new Error(`Cannot read Linux process start identity for activator PID ${process.pid}.`);
260
+ }
261
+ const owner = Object.freeze({
262
+ pid: process.pid,
263
+ processStartIdentity: startIdentity,
264
+ token: randomBytes(16).toString("hex"),
265
+ createdAt: new Date().toISOString()
266
+ });
267
+ for (let attempt = 0; attempt < 3; attempt += 1) {
268
+ try {
269
+ writeFileSync(lockPath, `${JSON.stringify(owner)}\n`, { flag: "wx", mode: 0o600 });
270
+ let released = false;
271
+ return {
272
+ release: () => {
273
+ if (released)
274
+ return;
275
+ released = true;
276
+ try {
277
+ const current = JSON.parse(readFileSync(lockPath, "utf8"));
278
+ if (current.token === owner.token)
279
+ rmSync(lockPath, { force: true });
280
+ }
281
+ catch {
282
+ // A damaged lock must not block a future activate; leave it for
283
+ // the stale-lock diagnosis below.
284
+ }
285
+ }
286
+ };
287
+ }
288
+ catch (error) {
289
+ if (!isEexist(error))
290
+ throw error;
291
+ }
292
+ let existing;
293
+ try {
294
+ existing = JSON.parse(readFileSync(lockPath, "utf8"));
295
+ }
296
+ catch (error) {
297
+ if (isEnoent(error))
298
+ continue;
299
+ throw error;
300
+ }
301
+ if (typeof existing.pid === "number"
302
+ && Number.isSafeInteger(existing.pid)
303
+ && existing.pid > 0
304
+ && isHandoverLockLive(existing)) {
305
+ throw new Error(`Another Controller handover is already running (owner PID ${existing.pid}, `
306
+ + `createdAt ${String(existing.createdAt)}): ${lockPath}`);
307
+ }
308
+ rmSync(lockPath, { force: true });
309
+ }
310
+ throw new Error(`Cannot safely acquire the Controller handover lock because its owner changed repeatedly: `
311
+ + lockPath);
312
+ }
313
+ /**
314
+ * A handover lock is live only when its owner process is still the same
315
+ * generation that wrote it. A PID that exists but with a different start
316
+ * identity was reused by an unrelated process, so the lock is stale.
317
+ * Locks written before the start-identity field existed fall back to the
318
+ * PID-exists check for backward compatibility.
319
+ */
320
+ function isHandoverLockLive(existing) {
321
+ if (typeof existing.pid !== "number"
322
+ || !Number.isSafeInteger(existing.pid)
323
+ || existing.pid <= 0) {
324
+ return false;
325
+ }
326
+ const currentIdentity = readLinuxProcessStartIdentity(existing.pid);
327
+ if (currentIdentity === undefined)
328
+ return false;
329
+ if (typeof existing.processStartIdentity === "string") {
330
+ return currentIdentity === existing.processStartIdentity;
331
+ }
332
+ return true;
333
+ }
334
+ export function newHandoverId() {
335
+ return `handover-${randomUUID()}`;
336
+ }
337
+ // ---------------------------------------------------------------------------
338
+ // Internal helpers
339
+ function listReleaseFiles(directory, prefix = "") {
340
+ const files = [];
341
+ for (const entry of readdirSync(directory, { withFileTypes: true })) {
342
+ const relativeName = prefix.length === 0 ? entry.name : `${prefix}/${entry.name}`;
343
+ if (entry.isDirectory()) {
344
+ files.push(...listReleaseFiles(join(directory, entry.name), relativeName));
345
+ continue;
346
+ }
347
+ if (!entry.isFile() || entry.isSymbolicLink()) {
348
+ throw new Error(`Runtime release tree contains an unsupported entry: ${relativeName}.`);
349
+ }
350
+ if (relativeName === RELEASE_MANIFEST_FILE || relativeName === SMOKE_RECEIPT_FILE)
351
+ continue;
352
+ files.push(relativeName);
353
+ }
354
+ return files.sort();
355
+ }
356
+ function inventoryFile(releaseDir, relativePath) {
357
+ const absolute = join(releaseDir, relativePath);
358
+ const metadata = lstatSync(absolute);
359
+ if (!metadata.isFile() || metadata.isSymbolicLink()) {
360
+ throw new Error(`Runtime release entry must be one regular file: ${relativePath}.`);
361
+ }
362
+ return {
363
+ path: relativePath,
364
+ sha256: createHash("sha256").update(readFileSync(absolute)).digest("hex"),
365
+ bytes: metadata.size
366
+ };
367
+ }
368
+ export function computePackageDigest(files) {
369
+ const hash = createHash("sha256");
370
+ for (const file of [...files].sort(compareFiles)) {
371
+ hash.update(`${file.path}\0${file.sha256}\0${file.bytes}\n`);
372
+ }
373
+ return hash.digest("hex");
374
+ }
375
+ function assertNoSymlinks(directory, root) {
376
+ for (const entry of readdirSync(directory, { withFileTypes: true })) {
377
+ const child = join(directory, entry.name);
378
+ if (entry.isSymbolicLink()) {
379
+ const target = (() => {
380
+ try {
381
+ return readlinkSync(child);
382
+ }
383
+ catch {
384
+ return "?";
385
+ }
386
+ })();
387
+ throw new Error(`Runtime release directory must not contain symbolic links: ${child} -> ${target}.`);
388
+ }
389
+ if (entry.isDirectory())
390
+ assertNoSymlinks(child, root);
391
+ }
392
+ }
393
+ function compareFiles(left, right) {
394
+ return left.path < right.path ? -1 : left.path > right.path ? 1 : 0;
395
+ }
396
+ function validateManifest(value) {
397
+ if (value === null
398
+ || typeof value !== "object"
399
+ || value.schemaVersion !== 1
400
+ || typeof value.version !== "string"
401
+ || value.version.length === 0
402
+ || typeof value.buildId !== "string"
403
+ || value.buildId.length === 0
404
+ || typeof value.packageDigest !== "string"
405
+ || !/^[a-f0-9]{64}$/u.test(value.packageDigest)
406
+ || !Array.isArray(value.files)) {
407
+ throw new Error("Runtime release manifest is invalid.");
408
+ }
409
+ const manifest = value;
410
+ for (const file of manifest.files) {
411
+ if (file === null
412
+ || typeof file !== "object"
413
+ || typeof file.path !== "string"
414
+ || file.path.length === 0
415
+ || typeof file.sha256 !== "string"
416
+ || !/^[a-f0-9]{64}$/u.test(file.sha256)
417
+ || !Number.isSafeInteger(file.bytes)
418
+ || file.bytes < 0) {
419
+ throw new Error(`Runtime release manifest file entry is invalid: ${JSON.stringify(file)}.`);
420
+ }
421
+ }
422
+ return Object.freeze({
423
+ schemaVersion: 1,
424
+ version: manifest.version,
425
+ buildId: manifest.buildId,
426
+ packageDigest: manifest.packageDigest,
427
+ ...(manifest.sourceCommit === undefined ? {} : { sourceCommit: manifest.sourceCommit }),
428
+ files: Object.freeze(manifest.files.map((file) => Object.freeze({ ...file }))),
429
+ assembledAt: manifest.assembledAt
430
+ });
431
+ }
432
+ function validatePointer(value) {
433
+ if (value === null
434
+ || typeof value !== "object"
435
+ || value.schemaVersion !== 1
436
+ || typeof value.releaseId !== "string"
437
+ || value.releaseId.length === 0
438
+ || typeof value.version !== "string"
439
+ || typeof value.buildId !== "string"
440
+ || typeof value.packageDigest !== "string"
441
+ || !/^[a-f0-9]{64}$/u.test(value.packageDigest)
442
+ || typeof value.activatedAt !== "string") {
443
+ throw new Error("Active release pointer is invalid.");
444
+ }
445
+ const pointer = value;
446
+ return Object.freeze({ ...pointer, schemaVersion: 1 });
447
+ }
448
+ function validateRuntimeIdentity(value) {
449
+ if (value === null
450
+ || typeof value !== "object"
451
+ || value.schemaVersion !== 1
452
+ || typeof value.version !== "string"
453
+ || typeof value.executablePath !== "string"
454
+ || value.executablePath === ""
455
+ || !isStringArray(value.args)
456
+ || typeof value.buildId !== "string"
457
+ || typeof value.cliRealpath !== "string"
458
+ || typeof value.controllerRealpath !== "string"
459
+ || typeof value.pid !== "number"
460
+ || typeof value.processStartIdentity !== "string"
461
+ || (value.mode !== "primary"
462
+ && value.mode !== "candidate")
463
+ || typeof value.dualOwner !== "boolean") {
464
+ throw new Error("Runtime identity receipt is invalid.");
465
+ }
466
+ return value;
467
+ }
468
+ function validateHandoverFence(value) {
469
+ if (value === null
470
+ || typeof value !== "object"
471
+ || value.schemaVersion !== 1
472
+ || typeof value.handoverId !== "string"
473
+ || !isHandoverPhase(value.phase)) {
474
+ throw new Error("Handover fence is invalid.");
475
+ }
476
+ return value;
477
+ }
478
+ function isHandoverPhase(value) {
479
+ return value === "fenced"
480
+ || value === "candidate-ready"
481
+ || value === "committed"
482
+ || value === "rolled-back";
483
+ }
484
+ function isStringArray(value) {
485
+ return Array.isArray(value) && value.every((entry) => typeof entry === "string");
486
+ }
487
+ function isEexist(error) {
488
+ return isNodeError(error) && error.code === "EEXIST";
489
+ }
490
+ function isEnoent(error) {
491
+ return isNodeError(error) && error.code === "ENOENT";
492
+ }
493
+ function isNodeError(error) {
494
+ return error instanceof Error && "code" in error;
495
+ }