@ricsam/r5d-worker 0.0.136 → 0.0.138

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 (240) hide show
  1. package/README.md +4 -94
  2. package/dist/cjs/package.json +1 -1
  3. package/dist/mjs/main.mjs +46 -7537
  4. package/dist/mjs/package.json +1 -1
  5. package/dist/mjs/personal/action-scheduler.mjs +31 -0
  6. package/dist/mjs/personal/client.mjs +215 -0
  7. package/dist/mjs/personal/runtime.mjs +328 -0
  8. package/dist/mjs/personal/tcp.mjs +217 -0
  9. package/dist/mjs/runtime/adapter.mjs +42 -0
  10. package/dist/mjs/runtime/client.mjs +95 -0
  11. package/dist/mjs/runtime/daemon.mjs +131 -0
  12. package/dist/mjs/runtime/executor.mjs +436 -0
  13. package/dist/mjs/runtime/index.mjs +15 -0
  14. package/dist/mjs/runtime/protocol.mjs +139 -0
  15. package/dist/mjs/runtime/pty-host.mjs +156 -0
  16. package/dist/mjs/runtime/pty.mjs +127 -0
  17. package/dist/mjs/runtime/releases/artifacts.mjs +201 -0
  18. package/dist/mjs/runtime/releases/grants.mjs +93 -0
  19. package/dist/mjs/runtime/releases/main.mjs +101 -0
  20. package/dist/mjs/runtime/releases/manager.mjs +541 -0
  21. package/dist/mjs/runtime/releases/process.mjs +88 -0
  22. package/dist/mjs/runtime/releases/retirement.mjs +18 -0
  23. package/dist/mjs/runtime/releases/rpc-client.mjs +184 -0
  24. package/dist/mjs/runtime/releases/rpc-main.mjs +213 -0
  25. package/dist/mjs/runtime/releases/rpc-protocol.mjs +103 -0
  26. package/dist/mjs/runtime/storage.mjs +156 -0
  27. package/dist/mjs/runtime/workspace/artifacts.mjs +121 -0
  28. package/dist/mjs/runtime/workspace/authority.mjs +1355 -0
  29. package/dist/mjs/runtime/workspace/contracts.mjs +60 -0
  30. package/dist/mjs/runtime/workspace/file-write.mjs +23 -0
  31. package/dist/mjs/runtime/workspace/files.mjs +308 -0
  32. package/dist/mjs/runtime/workspace/http-body.mjs +45 -0
  33. package/dist/mjs/runtime/workspace/storage-client.mjs +59 -0
  34. package/dist/mjs/runtime/workspace/storage-wire.mjs +53 -0
  35. package/dist/types/main.d.ts +0 -1167
  36. package/dist/types/personal/action-scheduler.d.ts +23 -0
  37. package/dist/types/personal/client.d.ts +14 -0
  38. package/dist/types/personal/runtime.d.ts +121 -0
  39. package/dist/types/personal/tcp.d.ts +14 -0
  40. package/dist/types/runtime/client.d.ts +17 -0
  41. package/dist/types/runtime/daemon.d.ts +8 -0
  42. package/dist/types/runtime/executor.d.ts +35 -0
  43. package/dist/types/runtime/index.d.ts +5 -0
  44. package/dist/types/runtime/protocol.d.ts +544 -0
  45. package/dist/types/runtime/pty.d.ts +30 -0
  46. package/dist/types/runtime/releases/artifacts.d.ts +51 -0
  47. package/dist/types/runtime/releases/grants.d.ts +16 -0
  48. package/dist/types/runtime/releases/manager.d.ts +145 -0
  49. package/dist/types/runtime/releases/process.d.ts +14 -0
  50. package/dist/types/runtime/releases/retirement.d.ts +15 -0
  51. package/dist/types/runtime/releases/rpc-client.d.ts +52 -0
  52. package/dist/types/runtime/releases/rpc-main.d.ts +10 -0
  53. package/dist/types/runtime/releases/rpc-protocol.d.ts +1139 -0
  54. package/dist/types/runtime/storage.d.ts +21 -0
  55. package/dist/types/runtime/test-harness.d.ts +53 -0
  56. package/dist/types/runtime/workspace/artifacts.d.ts +30 -0
  57. package/dist/types/runtime/workspace/authority.d.ts +338 -0
  58. package/dist/types/runtime/workspace/contracts.d.ts +86 -0
  59. package/dist/types/runtime/workspace/file-write.d.ts +14 -0
  60. package/dist/types/runtime/workspace/files.d.ts +21 -0
  61. package/dist/types/runtime/workspace/http-body.d.ts +9 -0
  62. package/dist/types/runtime/workspace/storage-client.d.ts +23 -0
  63. package/dist/types/runtime/workspace/storage-wire.d.ts +138 -0
  64. package/package.json +8 -6
  65. package/dist/cjs/atomic-rename.cjs +0 -303
  66. package/dist/cjs/browser-upload.cjs +0 -73
  67. package/dist/cjs/cli-update.cjs +0 -108
  68. package/dist/cjs/command-launcher.cjs +0 -423
  69. package/dist/cjs/control-command-policy.cjs +0 -177
  70. package/dist/cjs/git-blob-hash.cjs +0 -41
  71. package/dist/cjs/git-identity.cjs +0 -39
  72. package/dist/cjs/git-process-environment.cjs +0 -133
  73. package/dist/cjs/inline-output.cjs +0 -62
  74. package/dist/cjs/main.cjs +0 -7511
  75. package/dist/cjs/managed-paths.cjs +0 -215
  76. package/dist/cjs/plan-lint.cjs +0 -165
  77. package/dist/cjs/plan-parser.cjs +0 -127
  78. package/dist/cjs/plan-paths.cjs +0 -53
  79. package/dist/cjs/port-forward-client.cjs +0 -206
  80. package/dist/cjs/process-tree.cjs +0 -117
  81. package/dist/cjs/project-checkout-garbage.cjs +0 -219
  82. package/dist/cjs/project-mirror-fetch-policy.cjs +0 -82
  83. package/dist/cjs/project-mirror-refs-token.cjs +0 -85
  84. package/dist/cjs/project-workspace-state.cjs +0 -972
  85. package/dist/cjs/project-worktrees.cjs +0 -1713
  86. package/dist/cjs/pty-output-coalescer.cjs +0 -87
  87. package/dist/cjs/recovery-journal-protocol.cjs +0 -56
  88. package/dist/cjs/recovery-journal-runtime.cjs +0 -133
  89. package/dist/cjs/recovery-journal-thread.cjs +0 -9
  90. package/dist/cjs/recovery-journal.cjs +0 -742
  91. package/dist/cjs/recovery-output-window.cjs +0 -77
  92. package/dist/cjs/recovery-protocol.cjs +0 -31
  93. package/dist/cjs/recovery-store.cjs +0 -252
  94. package/dist/cjs/registry-auth.cjs +0 -310
  95. package/dist/cjs/repository-transition-policy.cjs +0 -49
  96. package/dist/cjs/runtime-version.cjs +0 -88
  97. package/dist/cjs/session-file-mutations.cjs +0 -61
  98. package/dist/cjs/supervisor.cjs +0 -147
  99. package/dist/cjs/three-way-merge.cjs +0 -346
  100. package/dist/cjs/working-tree-mirror.cjs +0 -1872
  101. package/dist/cjs/workspace-automatic-sync-policy.cjs +0 -79
  102. package/dist/cjs/workspace-branch-incarnation-policy.cjs +0 -37
  103. package/dist/cjs/workspace-command-sync-policy.cjs +0 -112
  104. package/dist/cjs/workspace-command-targets.cjs +0 -63
  105. package/dist/cjs/workspace-filesystem-executor-thread.cjs +0 -36
  106. package/dist/cjs/workspace-filesystem-executor.cjs +0 -327
  107. package/dist/cjs/workspace-filesystem-job-types.cjs +0 -144
  108. package/dist/cjs/workspace-filesystem-jobs.cjs +0 -59
  109. package/dist/cjs/workspace-git-sync.cjs +0 -3450
  110. package/dist/cjs/workspace-hydration-ledger.cjs +0 -66
  111. package/dist/cjs/workspace-hydration-merge.cjs +0 -433
  112. package/dist/cjs/workspace-hydration-recovery-state.cjs +0 -53
  113. package/dist/cjs/workspace-incident-state.cjs +0 -37
  114. package/dist/cjs/workspace-merge-projection.cjs +0 -467
  115. package/dist/cjs/workspace-mount-boundary.cjs +0 -71
  116. package/dist/cjs/workspace-mount-hold-fence.cjs +0 -120
  117. package/dist/cjs/workspace-mutation-gate.cjs +0 -127
  118. package/dist/cjs/workspace-path-move.cjs +0 -195
  119. package/dist/cjs/workspace-preserve-only-policy.cjs +0 -36
  120. package/dist/cjs/workspace-project-config-policy.cjs +0 -53
  121. package/dist/cjs/workspace-projection-ledger.cjs +0 -81
  122. package/dist/cjs/workspace-publication-evidence.cjs +0 -46
  123. package/dist/cjs/workspace-sync-coalescer.cjs +0 -58
  124. package/dist/mjs/atomic-rename.mjs +0 -261
  125. package/dist/mjs/browser-upload.mjs +0 -42
  126. package/dist/mjs/command-launcher.mjs +0 -393
  127. package/dist/mjs/control-command-policy.mjs +0 -146
  128. package/dist/mjs/git-blob-hash.mjs +0 -16
  129. package/dist/mjs/git-identity.mjs +0 -15
  130. package/dist/mjs/git-process-environment.mjs +0 -105
  131. package/dist/mjs/inline-output.mjs +0 -37
  132. package/dist/mjs/managed-paths.mjs +0 -174
  133. package/dist/mjs/plan-lint.mjs +0 -129
  134. package/dist/mjs/plan-parser.mjs +0 -94
  135. package/dist/mjs/plan-paths.mjs +0 -25
  136. package/dist/mjs/port-forward-client.mjs +0 -172
  137. package/dist/mjs/project-checkout-garbage.mjs +0 -178
  138. package/dist/mjs/project-mirror-fetch-policy.mjs +0 -56
  139. package/dist/mjs/project-mirror-refs-token.mjs +0 -53
  140. package/dist/mjs/project-workspace-state.mjs +0 -937
  141. package/dist/mjs/project-worktrees.mjs +0 -1657
  142. package/dist/mjs/pty-output-coalescer.mjs +0 -61
  143. package/dist/mjs/recovery-journal-protocol.mjs +0 -30
  144. package/dist/mjs/recovery-journal-runtime.mjs +0 -112
  145. package/dist/mjs/recovery-journal-thread.mjs +0 -8
  146. package/dist/mjs/recovery-journal.mjs +0 -694
  147. package/dist/mjs/recovery-output-window.mjs +0 -53
  148. package/dist/mjs/recovery-protocol.mjs +0 -6
  149. package/dist/mjs/recovery-store.mjs +0 -217
  150. package/dist/mjs/registry-auth.mjs +0 -269
  151. package/dist/mjs/repository-transition-policy.mjs +0 -22
  152. package/dist/mjs/runtime-version.mjs +0 -54
  153. package/dist/mjs/session-file-mutations.mjs +0 -37
  154. package/dist/mjs/supervisor.mjs +0 -114
  155. package/dist/mjs/three-way-merge.mjs +0 -318
  156. package/dist/mjs/working-tree-mirror.mjs +0 -1817
  157. package/dist/mjs/workspace-automatic-sync-policy.mjs +0 -48
  158. package/dist/mjs/workspace-branch-incarnation-policy.mjs +0 -13
  159. package/dist/mjs/workspace-command-sync-policy.mjs +0 -84
  160. package/dist/mjs/workspace-command-targets.mjs +0 -37
  161. package/dist/mjs/workspace-filesystem-executor-thread.mjs +0 -38
  162. package/dist/mjs/workspace-filesystem-executor.mjs +0 -287
  163. package/dist/mjs/workspace-filesystem-job-types.mjs +0 -116
  164. package/dist/mjs/workspace-filesystem-jobs.mjs +0 -55
  165. package/dist/mjs/workspace-git-sync.mjs +0 -3407
  166. package/dist/mjs/workspace-hydration-ledger.mjs +0 -42
  167. package/dist/mjs/workspace-hydration-merge.mjs +0 -399
  168. package/dist/mjs/workspace-hydration-recovery-state.mjs +0 -29
  169. package/dist/mjs/workspace-incident-state.mjs +0 -12
  170. package/dist/mjs/workspace-merge-projection.mjs +0 -433
  171. package/dist/mjs/workspace-mount-boundary.mjs +0 -37
  172. package/dist/mjs/workspace-mount-hold-fence.mjs +0 -95
  173. package/dist/mjs/workspace-mutation-gate.mjs +0 -103
  174. package/dist/mjs/workspace-path-move.mjs +0 -160
  175. package/dist/mjs/workspace-preserve-only-policy.mjs +0 -11
  176. package/dist/mjs/workspace-project-config-policy.mjs +0 -27
  177. package/dist/mjs/workspace-projection-ledger.mjs +0 -57
  178. package/dist/mjs/workspace-publication-evidence.mjs +0 -21
  179. package/dist/mjs/workspace-sync-coalescer.mjs +0 -34
  180. package/dist/types/atomic-rename.d.ts +0 -78
  181. package/dist/types/browser-upload.d.ts +0 -7
  182. package/dist/types/command-launcher.d.ts +0 -104
  183. package/dist/types/control-command-policy.d.ts +0 -53
  184. package/dist/types/git-blob-hash.d.ts +0 -10
  185. package/dist/types/git-identity.d.ts +0 -9
  186. package/dist/types/git-process-environment.d.ts +0 -9
  187. package/dist/types/inline-output.d.ts +0 -3
  188. package/dist/types/managed-paths.d.ts +0 -19
  189. package/dist/types/plan-lint.d.ts +0 -16
  190. package/dist/types/plan-parser.d.ts +0 -38
  191. package/dist/types/plan-paths.d.ts +0 -36
  192. package/dist/types/port-forward-client.d.ts +0 -8
  193. package/dist/types/project-checkout-garbage.d.ts +0 -86
  194. package/dist/types/project-mirror-fetch-policy.d.ts +0 -84
  195. package/dist/types/project-mirror-refs-token.d.ts +0 -53
  196. package/dist/types/project-workspace-state.d.ts +0 -188
  197. package/dist/types/project-worktrees.d.ts +0 -397
  198. package/dist/types/pty-output-coalescer.d.ts +0 -20
  199. package/dist/types/recovery-journal-protocol.d.ts +0 -35
  200. package/dist/types/recovery-journal-runtime.d.ts +0 -12
  201. package/dist/types/recovery-journal-thread.d.ts +0 -1
  202. package/dist/types/recovery-journal.d.ts +0 -248
  203. package/dist/types/recovery-output-window.d.ts +0 -19
  204. package/dist/types/recovery-protocol.d.ts +0 -59
  205. package/dist/types/recovery-store.d.ts +0 -52
  206. package/dist/types/registry-auth.d.ts +0 -47
  207. package/dist/types/repository-transition-policy.d.ts +0 -26
  208. package/dist/types/runtime-version.d.ts +0 -1
  209. package/dist/types/session-file-mutations.d.ts +0 -22
  210. package/dist/types/supervisor-daemonized-fixture.d.ts +0 -1
  211. package/dist/types/supervisor-launcher-cleanup-fixture.d.ts +0 -1
  212. package/dist/types/supervisor-signal-fixture.d.ts +0 -1
  213. package/dist/types/supervisor.d.ts +0 -23
  214. package/dist/types/three-way-merge.d.ts +0 -77
  215. package/dist/types/working-tree-mirror.d.ts +0 -424
  216. package/dist/types/workspace-automatic-sync-policy.d.ts +0 -57
  217. package/dist/types/workspace-branch-incarnation-policy.d.ts +0 -14
  218. package/dist/types/workspace-command-sync-policy.d.ts +0 -57
  219. package/dist/types/workspace-command-targets.d.ts +0 -37
  220. package/dist/types/workspace-filesystem-executor-thread.d.ts +0 -1
  221. package/dist/types/workspace-filesystem-executor.d.ts +0 -123
  222. package/dist/types/workspace-filesystem-job-types.d.ts +0 -288
  223. package/dist/types/workspace-filesystem-jobs.d.ts +0 -7
  224. package/dist/types/workspace-git-sync.d.ts +0 -495
  225. package/dist/types/workspace-hydration-ledger.d.ts +0 -43
  226. package/dist/types/workspace-hydration-merge.d.ts +0 -95
  227. package/dist/types/workspace-hydration-recovery-state.d.ts +0 -10
  228. package/dist/types/workspace-incident-state.d.ts +0 -19
  229. package/dist/types/workspace-merge-projection.d.ts +0 -60
  230. package/dist/types/workspace-mount-boundary.d.ts +0 -10
  231. package/dist/types/workspace-mount-hold-fence.d.ts +0 -42
  232. package/dist/types/workspace-mutation-gate.d.ts +0 -42
  233. package/dist/types/workspace-path-move.d.ts +0 -21
  234. package/dist/types/workspace-preserve-only-policy.d.ts +0 -15
  235. package/dist/types/workspace-project-config-policy.d.ts +0 -48
  236. package/dist/types/workspace-projection-ledger.d.ts +0 -59
  237. package/dist/types/workspace-publication-evidence.d.ts +0 -17
  238. package/dist/types/workspace-sync-coalescer.d.ts +0 -28
  239. /package/dist/types/{credential-authority-lock-fixture.d.ts → runtime/adapter.d.ts} +0 -0
  240. /package/dist/types/{recovery-journal-stall-fixture.d.ts → runtime/releases/main.d.ts} +0 -0
@@ -0,0 +1,541 @@
1
+ import { Database } from "bun:sqlite";
2
+ import { chmodSync, closeSync, constants, existsSync, fsyncSync, lstatSync, openSync, readFileSync, writeFileSync } from "node:fs";
3
+ import { join } from "node:path";
4
+ import { createHmac, randomBytes, randomUUID, timingSafeEqual } from "node:crypto";
5
+ import { z } from "zod";
6
+ import { canonicalJson, negotiateProtocol, OwnershipFence, RuntimeHello, RuntimeId } from "@ricsam/r5d-api/runtime-protocol";
7
+ import { HostExecutorClient } from "../client.mjs";
8
+ import { AdapterConfig, AdapterGrant, ExecutorCommand, ExecutorError, EXECUTOR_CAPABILITY } from "../protocol.mjs";
9
+ import { privateDirectory, readPrivateJson } from "../storage.mjs";
10
+ import { AdapterManifest, bytesHash, canonicalDirectory, ImmutableAdapterArtifacts, outsideWorkspaces } from "./artifacts.mjs";
11
+ import { digest } from "./grants.mjs";
12
+ import { AdapterChild } from "./process.mjs";
13
+ import { AdapterRetirementCompletion } from "./retirement.mjs";
14
+ const ReleaseManagerConfig = z.object({
15
+ installationId: RuntimeId,
16
+ workerId: RuntimeId,
17
+ stateDir: z.string().startsWith("/"),
18
+ artifactsDir: z.string().startsWith("/"),
19
+ workspaceRoots: z.array(z.string().startsWith("/")).min(1),
20
+ runtimePath: z.string().startsWith("/"),
21
+ runtimeSha256: z.string().regex(/^[a-f0-9]{64}$/),
22
+ controller: AdapterConfig
23
+ }).strict();
24
+ function callDigest(command) {
25
+ const parsed = ExecutorCommand.parse(command);
26
+ if (parsed.method === "start") return digest({ ...parsed, operation: { ...parsed.operation, fence: null } });
27
+ if ("run" in parsed) return digest({ ...parsed, run: { ...parsed.run, fence: null } });
28
+ return digest(parsed);
29
+ }
30
+ function privateSqlitePath(path) {
31
+ if (!existsSync(path)) return;
32
+ const stat = lstatSync(path);
33
+ if (!stat.isFile() || stat.isSymbolicLink() || stat.nlink !== 1 || stat.uid !== process.getuid?.() || stat.mode & 63)
34
+ throw new ExecutorError("insecure_release_state");
35
+ }
36
+ class WorkerAdapterReleases {
37
+ invocation = randomUUID();
38
+ config;
39
+ artifacts;
40
+ controller;
41
+ db;
42
+ lock;
43
+ ackKey;
44
+ children = /* @__PURE__ */ new Map();
45
+ deliveries = /* @__PURE__ */ new Map();
46
+ probes = 0;
47
+ retiring = /* @__PURE__ */ new Set();
48
+ serial = Promise.resolve();
49
+ closing = false;
50
+ closed;
51
+ constructor(input) {
52
+ this.config = ReleaseManagerConfig.parse(input);
53
+ const config = this.config;
54
+ if (config.controller.hello.installationId !== config.installationId || config.controller.hello.role !== "scheduler")
55
+ throw new ExecutorError("wrong_controller");
56
+ privateDirectory(config.stateDir);
57
+ outsideWorkspaces(config.stateDir, config.workspaceRoots);
58
+ this.verifyRuntime();
59
+ this.artifacts = new ImmutableAdapterArtifacts(config.artifactsDir, config.installationId, config.workerId, config.workspaceRoots);
60
+ this.controller = new HostExecutorClient(config.controller);
61
+ for (const file of ["manager.lock.sqlite", "releases.sqlite", "releases.sqlite-wal", "releases.sqlite-shm"])
62
+ privateSqlitePath(join(config.stateDir, file));
63
+ this.lock = new Database(join(config.stateDir, "manager.lock.sqlite"), { create: true });
64
+ chmodSync(join(config.stateDir, "manager.lock.sqlite"), 384);
65
+ try {
66
+ this.lock.exec("PRAGMA busy_timeout=0; BEGIN EXCLUSIVE; CREATE TABLE IF NOT EXISTS singleton (id INTEGER)");
67
+ } catch {
68
+ this.lock.close();
69
+ throw new ExecutorError("release_manager_already_running");
70
+ }
71
+ let opened;
72
+ try {
73
+ this.db = opened = new Database(join(config.stateDir, "releases.sqlite"), { create: true });
74
+ chmodSync(join(config.stateDir, "releases.sqlite"), 384);
75
+ this.db.exec(`PRAGMA journal_mode=WAL; PRAGMA synchronous=FULL;
76
+ CREATE TABLE IF NOT EXISTS identity (id INTEGER PRIMARY KEY CHECK(id=1), digest TEXT NOT NULL);
77
+ CREATE TABLE IF NOT EXISTS releases (id TEXT PRIMARY KEY, record TEXT NOT NULL);
78
+ CREATE TABLE IF NOT EXISTS calls (id TEXT PRIMARY KEY, principal TEXT NOT NULL, hash TEXT NOT NULL,
79
+ state TEXT NOT NULL, acknowledged INTEGER NOT NULL DEFAULT 0);
80
+ CREATE TABLE IF NOT EXISTS transitions (id INTEGER PRIMARY KEY AUTOINCREMENT, kind TEXT NOT NULL, body TEXT NOT NULL, state TEXT NOT NULL)`);
81
+ const binding = digest({
82
+ installationId: config.installationId,
83
+ workerId: config.workerId,
84
+ artifactsDir: config.artifactsDir,
85
+ socketPath: config.controller.socketPath,
86
+ controllerId: config.controller.principalId
87
+ });
88
+ const prior = this.db.query("SELECT digest FROM identity WHERE id=1").get();
89
+ if (prior && prior.digest !== binding) throw new ExecutorError("wrong_store_identity");
90
+ if (!prior) this.db.query("INSERT INTO identity VALUES (1,?)").run(binding);
91
+ this.db.exec("UPDATE calls SET state='unknown' WHERE state='pending'; UPDATE transitions SET state='unknown' WHERE state='pending'");
92
+ this.db.exec(`CREATE TABLE IF NOT EXISTS deliveries (id TEXT PRIMARY KEY, receipt TEXT NOT NULL);
93
+ CREATE TABLE IF NOT EXISTS rpc_identity (id INTEGER PRIMARY KEY CHECK(id=1), digest TEXT NOT NULL);
94
+ CREATE TABLE IF NOT EXISTS observation_sequence (id INTEGER PRIMARY KEY CHECK(id=1), seq INTEGER NOT NULL, ack_key TEXT NOT NULL)`);
95
+ this.db.query("INSERT OR IGNORE INTO observation_sequence VALUES(1,0,?)").run(randomBytes(32).toString("hex"));
96
+ this.ackKey = this.db.query("SELECT ack_key FROM observation_sequence WHERE id=1").get().ack_key;
97
+ for (const row of this.db.query("SELECT id,receipt FROM deliveries").all()) {
98
+ const receipt = JSON.parse(row.receipt);
99
+ if (receipt.state === "pending") this.saveDelivery({ ...receipt, state: "unknown", outcome: null });
100
+ }
101
+ privateDirectory(join(config.stateDir, "configs"));
102
+ } catch (error) {
103
+ opened?.close();
104
+ this.lock.close();
105
+ throw error;
106
+ }
107
+ }
108
+ verifyRuntime() {
109
+ const config = this.config;
110
+ canonicalDirectory(config.runtimePath.slice(0, config.runtimePath.lastIndexOf("/")) || "/");
111
+ const runtime = lstatSync(config.runtimePath);
112
+ if (!runtime.isFile() || runtime.isSymbolicLink() || runtime.mode & 18 || bytesHash(readFileSync(config.runtimePath)) !== config.runtimeSha256)
113
+ throw new ExecutorError("unapproved_runtime");
114
+ }
115
+ exclusive(fn) {
116
+ const result = this.serial.then(() => {
117
+ if (this.closing) throw new ExecutorError("release_manager_closed");
118
+ return fn();
119
+ });
120
+ this.serial = result.catch(() => {
121
+ });
122
+ return result;
123
+ }
124
+ list() {
125
+ return this.db.query("SELECT record FROM releases ORDER BY id").all().map((row) => JSON.parse(row.record));
126
+ }
127
+ calls() {
128
+ return this.db.query("SELECT * FROM calls ORDER BY id").all();
129
+ }
130
+ transitions() {
131
+ return this.db.query("SELECT * FROM transitions ORDER BY id").all();
132
+ }
133
+ get(id) {
134
+ const record = this.list().find((record2) => record2.principalId === id);
135
+ if (!record) throw new ExecutorError("adapter_not_found");
136
+ return record;
137
+ }
138
+ put(record) {
139
+ this.db.query("INSERT INTO releases VALUES (?,?) ON CONFLICT(id) DO UPDATE SET record=excluded.record").run(record.principalId, canonicalJson(record));
140
+ }
141
+ configPath(id) {
142
+ return join(this.config.stateDir, "configs", `${digest(id)}.json`);
143
+ }
144
+ async keeper() {
145
+ const status = await this.controller.status();
146
+ if (status.hello.installationId !== this.config.installationId || status.workerId !== this.config.workerId)
147
+ throw new ExecutorError("wrong_resource");
148
+ return status;
149
+ }
150
+ /** Approval digest must come from the authenticated release ledger, not the source directory itself. */
151
+ stage(input) {
152
+ return this.exclusive(async () => {
153
+ const manifest = AdapterManifest.parse(input.manifest), grant = AdapterGrant.parse(input.grant), adapter = AdapterConfig.parse(input.adapter);
154
+ if (digest(manifest.hello) !== digest(grant.hello) || digest(adapter.hello) !== digest(grant.hello) || grant.artifactDigest !== input.approvalDigest || grant.installationId !== manifest.installationId || grant.workerId !== manifest.workerId || adapter.principalId !== grant.principal.id || adapter.socketPath !== this.config.controller.socketPath || bytesHash(Buffer.from(adapter.token)) !== grant.principal.tokenHash)
155
+ throw new ExecutorError("artifact_grant_mismatch");
156
+ const prior = this.list().find((row) => row.principalId === grant.principal.id);
157
+ if (prior && (digest(prior.grant) !== digest(grant) || prior.artifactDigest !== input.approvalDigest))
158
+ throw new ExecutorError("grant_identity_conflict");
159
+ if (prior?.phase === "retired") throw new ExecutorError("adapter_retired");
160
+ if (!prior && this.list().filter((row) => row.phase !== "retired").length >= 2) throw new ExecutorError("adapter_retention_blocked");
161
+ const keeper = await this.keeper();
162
+ negotiateProtocol(manifest.hello, keeper.hello, manifest.hello.capabilities);
163
+ this.artifacts.stage(input.source, manifest, input.approvalDigest);
164
+ const path = this.configPath(adapter.principalId);
165
+ if (existsSync(path)) {
166
+ if (digest(AdapterConfig.parse(readPrivateJson(path))) !== digest(adapter)) throw new ExecutorError("adapter_config_conflict");
167
+ } else {
168
+ const fd = openSync(path, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | constants.O_NOFOLLOW, 384);
169
+ try {
170
+ writeFileSync(fd, JSON.stringify(adapter));
171
+ fsyncSync(fd);
172
+ } finally {
173
+ closeSync(fd);
174
+ }
175
+ const directory = openSync(join(this.config.stateDir, "configs"), constants.O_RDONLY | constants.O_DIRECTORY);
176
+ try {
177
+ fsyncSync(directory);
178
+ } finally {
179
+ closeSync(directory);
180
+ }
181
+ }
182
+ const record = prior ?? {
183
+ principalId: adapter.principalId,
184
+ manifest,
185
+ artifactDigest: input.approvalDigest,
186
+ grant,
187
+ phase: "staged"
188
+ };
189
+ this.put(record);
190
+ await this.controller.registerAdapter(grant);
191
+ return record;
192
+ });
193
+ }
194
+ launch(id) {
195
+ return this.exclusive(async () => {
196
+ const record = this.get(id);
197
+ if (record.phase === "retired") throw new ExecutorError("adapter_retired");
198
+ await this.controller.registerAdapter(record.grant);
199
+ const directory = this.artifacts.verify(record.manifest, record.artifactDigest);
200
+ let child = this.children.get(id);
201
+ if (!child?.alive) {
202
+ this.verifyRuntime();
203
+ const adapter = AdapterConfig.parse(readPrivateJson(this.configPath(id)));
204
+ if (digest(adapter.hello) !== digest(record.grant.hello) || adapter.principalId !== id || adapter.socketPath !== this.config.controller.socketPath || bytesHash(Buffer.from(adapter.token)) !== record.grant.principal.tokenHash)
205
+ throw new ExecutorError("adapter_config_conflict");
206
+ child = new AdapterChild(
207
+ this.config.runtimePath,
208
+ join(directory, record.manifest.entrypoint),
209
+ this.configPath(id),
210
+ this.config.stateDir,
211
+ adapter.timeoutMs
212
+ );
213
+ this.children.set(id, child);
214
+ }
215
+ try {
216
+ const status = await child.request({ method: "status" });
217
+ if (status.workerId !== this.config.workerId || status.degraded) throw new ExecutorError("candidate_not_ready");
218
+ negotiateProtocol(record.manifest.hello, status.hello, [EXECUTOR_CAPABILITY, ...record.manifest.hello.capabilities]);
219
+ if (record.phase === "staged") {
220
+ record.phase = "ready";
221
+ this.put(record);
222
+ }
223
+ return { pid: child.process.pid };
224
+ } catch (error) {
225
+ await child.close();
226
+ this.children.delete(id);
227
+ throw error;
228
+ }
229
+ });
230
+ }
231
+ /** Fence/lease are the committed DB grant from the separately authenticated controller, not a locally invented epoch. */
232
+ promote(id, inputFence, validUntil) {
233
+ return this.exclusive(async () => {
234
+ const record = this.get(id), fence = OwnershipFence.parse(inputFence);
235
+ if (record.phase === "staged" || record.phase === "retired" || !this.children.get(id)?.alive)
236
+ throw new ExecutorError("candidate_not_ready");
237
+ if (fence.installationId !== this.config.installationId || fence.resourceId !== this.config.workerId || fence.resourceType !== "worker" || fence.ownerId !== record.grant.principal.ownerId)
238
+ throw new ExecutorError("wrong_resource");
239
+ this.artifacts.verify(record.manifest, record.artifactDigest);
240
+ const readiness = await this.children.get(id).request({ method: "status" });
241
+ if (readiness.workerId !== this.config.workerId || readiness.degraded) throw new ExecutorError("candidate_not_ready");
242
+ negotiateProtocol(record.manifest.hello, readiness.hello, record.manifest.hello.capabilities);
243
+ const transition = this.db.query("INSERT INTO transitions(kind,body,state) VALUES ('promote',?,'pending') RETURNING id").get(canonicalJson({ principalId: id, fence, validUntil }));
244
+ try {
245
+ await this.controller.setAuthority(fence, validUntil);
246
+ this.recordAuthority(id);
247
+ this.db.query("UPDATE transitions SET state='completed' WHERE id=?").run(transition.id);
248
+ } catch (error) {
249
+ this.db.query("UPDATE transitions SET state='unknown' WHERE id=?").run(transition.id);
250
+ throw error;
251
+ }
252
+ });
253
+ }
254
+ recordAuthority(id) {
255
+ this.db.transaction(() => {
256
+ for (const record of this.list()) {
257
+ if (record.principalId === id) {
258
+ record.phase = "active";
259
+ this.put(record);
260
+ } else if (record.phase === "active") {
261
+ record.phase = "retained";
262
+ this.put(record);
263
+ }
264
+ }
265
+ })();
266
+ }
267
+ /** Recovery reads the keeper, never replays a promotion or kills a remembered PID. */
268
+ reconcile() {
269
+ return this.exclusive(async () => {
270
+ const status = await this.keeper(), owner = status.authority?.fence.ownerId;
271
+ const record = this.list().find((row) => row.grant.principal.ownerId === owner);
272
+ if (record && record.phase !== "retired") this.recordAuthority(record.principalId);
273
+ for (const row of this.list()) {
274
+ if (status.adapters.some((grant) => grant.principalId === row.principalId && grant.state === "retired")) {
275
+ row.phase = "retired";
276
+ this.put(row);
277
+ await this.children.get(row.principalId)?.close();
278
+ this.children.delete(row.principalId);
279
+ }
280
+ }
281
+ return status;
282
+ });
283
+ }
284
+ bindRpcPrincipals(principals) {
285
+ const hash = digest(principals);
286
+ const old = this.db.query("SELECT digest FROM rpc_identity WHERE id=1").get();
287
+ if (old && old.digest !== hash) throw new ExecutorError("rpc_identity_conflict");
288
+ if (!old) this.db.query("INSERT INTO rpc_identity VALUES(1,?)").run(hash);
289
+ }
290
+ /** Actual child request, independent of the delivery/admin queues; never substitutes direct executor status. */
291
+ async probe(id, hello) {
292
+ const record = this.get(id), child = this.children.get(id);
293
+ if (this.closing || this.retiring.has(id) || record.phase === "retired" || !child?.alive || digest(hello) !== digest(record.grant.hello))
294
+ throw new ExecutorError("candidate_not_ready");
295
+ if (this.probes >= 8) throw new ExecutorError("manager_probe_busy");
296
+ this.probes++;
297
+ try {
298
+ const status = await child.request({ method: "status" });
299
+ if (!child.alive || this.retiring.has(id) || this.children.get(id) !== child || status.workerId !== this.config.workerId || status.degraded)
300
+ throw new ExecutorError("candidate_not_ready");
301
+ negotiateProtocol(hello, status.hello, hello.capabilities);
302
+ return { ...status, adapterPid: child.process.pid };
303
+ } finally {
304
+ this.probes--;
305
+ }
306
+ }
307
+ delivery(callId) {
308
+ const row = this.db.query("SELECT receipt FROM deliveries WHERE id=?").get(callId);
309
+ if (!row) return null;
310
+ const receipt = JSON.parse(row.receipt);
311
+ if (receipt.responseDigest !== digest({ callId: receipt.callId, commandDigest: receipt.commandDigest, state: receipt.state, outcome: receipt.outcome }))
312
+ throw new ExecutorError("delivery_corrupt");
313
+ return receipt;
314
+ }
315
+ ackToken(callId, responseDigest, requesterId, deliveryKind) {
316
+ return createHmac("sha256", this.ackKey).update(canonicalJson({ callId, responseDigest, requesterId, deliveryKind })).digest("hex");
317
+ }
318
+ saveDelivery(receipt) {
319
+ const value = {
320
+ ...receipt,
321
+ responseDigest: digest({
322
+ callId: receipt.callId,
323
+ commandDigest: receipt.commandDigest,
324
+ state: receipt.state,
325
+ outcome: receipt.outcome
326
+ })
327
+ };
328
+ value.ackToken = this.ackToken(value.callId, value.responseDigest, value.requesterId, value.deliveryKind);
329
+ const serialized = canonicalJson(value);
330
+ if (Buffer.byteLength(serialized) > 1024 * 1024 - 4096) throw new ExecutorError("delivery_result_too_large");
331
+ this.db.query("INSERT INTO deliveries VALUES (?,?) ON CONFLICT(id) DO UPDATE SET receipt=excluded.receipt").run(value.callId, serialized);
332
+ return value;
333
+ }
334
+ /** Stable transport ID: persisted completed outcomes replay exactly; unknown never redispatches. */
335
+ deliver(callId, id, hello, requesterId, command) {
336
+ if (callId.startsWith("observation-")) return Promise.reject(new ExecutorError("reserved_delivery_id"));
337
+ if (command.method === "poll") return Promise.reject(new ExecutorError("observation_required"));
338
+ return this.dispatchDelivery(callId, id, hello, requesterId, command, "mutation");
339
+ }
340
+ observe(id, hello, requesterId, command) {
341
+ if (command.method !== "poll") return Promise.reject(new ExecutorError("forbidden"));
342
+ const seq = this.db.query("UPDATE observation_sequence SET seq=seq+1 WHERE id=1 AND seq<9007199254740990 RETURNING seq").get();
343
+ if (!seq) return Promise.reject(new ExecutorError("observation_sequence_exhausted"));
344
+ return this.dispatchDelivery(`observation-${seq.seq}`, id, hello, requesterId, command, "observation");
345
+ }
346
+ dispatchDelivery(callId, id, hello, requesterId, command, deliveryKind) {
347
+ const notAdmitted = (code) => new ExecutorError(code, code, command.method === "start");
348
+ if (this.closing) return Promise.reject(notAdmitted("release_manager_closed"));
349
+ RuntimeId.parse(callId);
350
+ RuntimeId.parse(requesterId);
351
+ const parsed = ExecutorCommand.parse(command);
352
+ if (!["start", "poll", "write", "resize", "cancel"].includes(parsed.method)) return Promise.reject(new ExecutorError("forbidden"));
353
+ const identity = parsed.method === "start" ? parsed.operation : "run" in parsed ? parsed.run : null;
354
+ if (!identity?.sessionId) return Promise.reject(new ExecutorError("forbidden"));
355
+ const hash = digest(parsed), prior = this.delivery(callId);
356
+ if (prior) {
357
+ if (prior.adapterId !== id || digest(this.get(id).grant.hello) !== digest(hello) || prior.commandDigest !== hash || prior.requesterId !== requesterId || prior.userId !== identity.userId || prior.sessionId !== identity.sessionId)
358
+ return Promise.reject(new ExecutorError("call_payload_mismatch"));
359
+ if (prior.state === "completed" && prior.outcome === null) return Promise.reject(new ExecutorError("delivery_payload_missing"));
360
+ return this.deliveries.get(callId) ?? Promise.resolve(prior);
361
+ }
362
+ const record = this.get(id), child = this.children.get(id);
363
+ if (this.retiring.has(id) || record.phase === "retired" || digest(record.grant.hello) !== digest(hello) || !child?.alive)
364
+ return Promise.reject(notAdmitted("candidate_not_ready"));
365
+ if (this.deliveries.size >= (parsed.method === "cancel" ? 32 : 28)) return Promise.reject(notAdmitted("manager_delivery_busy"));
366
+ const size = this.db.query(
367
+ "SELECT count(*) AS count, coalesce(sum(length(CAST(receipt AS BLOB))),0) AS bytes FROM deliveries WHERE json_extract(receipt,'$.deliveryKind')=?"
368
+ ).get(deliveryKind);
369
+ const countLimit = deliveryKind === "observation" ? 256 : parsed.method === "start" ? 2048 : parsed.method === "cancel" ? 4096 : 3072;
370
+ const byteLimit = (deliveryKind === "observation" ? 8 : parsed.method === "start" ? 16 : parsed.method === "cancel" ? 32 : 24) * 1024 * 1024;
371
+ if (size.count >= countLimit || size.bytes + (this.deliveries.size + 1) * 1024 * 1024 > byteLimit)
372
+ return Promise.reject(notAdmitted("delivery_retention_full"));
373
+ const receipt = this.saveDelivery({
374
+ callId,
375
+ deliveryKind,
376
+ installationId: this.config.installationId,
377
+ workerId: this.config.workerId,
378
+ adapterId: id,
379
+ requesterId,
380
+ userId: identity.userId,
381
+ sessionId: identity.sessionId,
382
+ operationId: identity.operationId,
383
+ commandDigest: hash,
384
+ state: "pending",
385
+ outcome: null,
386
+ acknowledged: false
387
+ });
388
+ const task = (async () => {
389
+ try {
390
+ const result = await child.request(parsed);
391
+ if (result && typeof result === "object" && (result.state === "unknown" || result.receipt?.state === "unknown"))
392
+ return this.saveDelivery({ ...receipt, state: "unknown", outcome: null });
393
+ return this.saveDelivery({ ...receipt, state: "completed", outcome: { ok: true, result } });
394
+ } catch (error) {
395
+ const known = error instanceof ExecutorError && !error.code.endsWith("_unknown") && !["invalid_response", "delivery_result_too_large", "process_cleanup_uncertain"].includes(error.code);
396
+ const outcome = known ? { ok: false, error: { code: error.code, rejectedBeforeAdmission: error.rejectedBeforeAdmission } } : null;
397
+ return this.saveDelivery({ ...receipt, state: known ? "completed" : "unknown", outcome });
398
+ } finally {
399
+ this.deliveries.delete(callId);
400
+ }
401
+ })();
402
+ this.deliveries.set(callId, task);
403
+ return task;
404
+ }
405
+ acknowledgeDelivery(callId, responseDigest, ackToken, requesterId) {
406
+ const receipt = this.delivery(callId);
407
+ const kind = callId.startsWith("observation-") ? "observation" : "mutation";
408
+ const expected = this.ackToken(callId, responseDigest, requesterId, kind);
409
+ if (!/^[a-f0-9]{64}$/.test(ackToken) || !timingSafeEqual(Buffer.from(expected, "hex"), Buffer.from(ackToken, "hex")))
410
+ throw new ExecutorError("delivery_ack_mismatch");
411
+ if (!receipt) {
412
+ const match = /^observation-([1-9][0-9]*)$/.exec(callId), seq = Number(match?.[1]);
413
+ const highwater = this.db.query("SELECT seq FROM observation_sequence WHERE id=1").get().seq;
414
+ if (!match || !Number.isSafeInteger(seq) || seq > highwater) throw new ExecutorError("delivery_ack_mismatch");
415
+ } else {
416
+ if (receipt.requesterId !== requesterId || receipt.responseDigest !== responseDigest)
417
+ throw new ExecutorError("delivery_ack_mismatch");
418
+ if (receipt.state !== "completed" || receipt.outcome === null) throw new ExecutorError("delivery_reconciliation_required");
419
+ if (receipt.deliveryKind === "observation") this.db.query("DELETE FROM deliveries WHERE id=?").run(callId);
420
+ else this.saveDelivery({ ...receipt, acknowledged: true });
421
+ }
422
+ return { callId, responseDigest, acknowledged: true };
423
+ }
424
+ /** Admin-only attestation to independently durable reconciliation. Never replays or changes the unknown outcome. */
425
+ settleUnknownDelivery(callId, evidenceDigest) {
426
+ z.string().regex(/^[a-f0-9]{64}$/).parse(evidenceDigest);
427
+ const receipt = this.delivery(callId);
428
+ if (!receipt || receipt.state !== "unknown") throw new ExecutorError("delivery_not_unknown");
429
+ return this.db.transaction(() => {
430
+ this.db.query("INSERT INTO transitions(kind,body,state) VALUES ('settle-unknown',?,'completed')").run(canonicalJson({ callId, responseDigest: receipt.responseDigest, evidenceDigest }));
431
+ return this.saveDelivery({ ...receipt, acknowledged: true });
432
+ })();
433
+ }
434
+ deliveryDrain(id) {
435
+ const rows = this.db.query("SELECT receipt FROM deliveries").all();
436
+ const receipts = rows.map((row) => JSON.parse(row.receipt)).filter((receipt) => receipt.adapterId === id && !receipt.acknowledged);
437
+ return { unsettled: receipts.length, unknown: receipts.filter((receipt) => receipt.state === "unknown").length };
438
+ }
439
+ /** Stable callId journals delivery; reusing it never dispatches twice. Explicit retry uses reconcileCall. */
440
+ request(callId, id, command) {
441
+ return this.exclusive(() => this.dispatch(callId, id, command, false));
442
+ }
443
+ reconcileCall(callId, id, command) {
444
+ return this.exclusive(() => this.dispatch(callId, id, command, true));
445
+ }
446
+ async dispatch(callId, id, command, explicitRetry) {
447
+ RuntimeId.parse(callId);
448
+ if (!["start", "poll", "write", "resize", "cancel"].includes(command.method)) throw new ExecutorError("forbidden");
449
+ const hash = callDigest(command), prior = this.calls().find((call) => call.id === callId);
450
+ if (prior && prior.hash !== hash) throw new ExecutorError("call_payload_mismatch");
451
+ if (prior && !explicitRetry)
452
+ throw new ExecutorError(prior.state === "completed" ? "call_already_delivered" : "adapter_outcome_unknown");
453
+ if (!prior && explicitRetry) throw new ExecutorError("call_not_found");
454
+ if (!this.children.get(id)?.alive) throw new ExecutorError("candidate_not_ready");
455
+ if (!prior) this.db.query("INSERT INTO calls VALUES (?,?,?,'pending',0)").run(callId, id, hash);
456
+ try {
457
+ const result = await this.children.get(id).request(command);
458
+ this.db.query("UPDATE calls SET state='completed' WHERE id=?").run(callId);
459
+ return result;
460
+ } catch (error) {
461
+ this.db.query("UPDATE calls SET state='unknown' WHERE id=?").run(callId);
462
+ throw error;
463
+ }
464
+ }
465
+ /** Called only after the controller's durable inbox/outbox has recorded the result OR explicit unknown reconciliation. */
466
+ acknowledge(callId) {
467
+ return this.exclusive(async () => {
468
+ const call = this.calls().find((row) => row.id === callId);
469
+ if (!call || call.state === "pending") throw new ExecutorError("call_not_settled");
470
+ this.db.query("UPDATE calls SET acknowledged=1 WHERE id=?").run(callId);
471
+ });
472
+ }
473
+ retire(id, retirementIntentDigest) {
474
+ return this.exclusive(async () => {
475
+ const record = this.get(id);
476
+ if (retirementIntentDigest !== void 0 && !/^[a-f0-9]{64}$/.test(retirementIntentDigest))
477
+ throw new ExecutorError("invalid_retirement_intent");
478
+ if (record.phase === "retired") {
479
+ const completion = record.retirementCompletion;
480
+ if (!completion || retirementIntentDigest && completion.retirementIntentDigest !== retirementIntentDigest)
481
+ throw new ExecutorError("adapter_retirement_unknown");
482
+ AdapterRetirementCompletion.parse(completion);
483
+ if (completion.installationId !== this.config.installationId || completion.workerId !== this.config.workerId || completion.adapterId !== id || completion.manifestDigest !== digest(record.manifest) || completion.grantDigest !== digest(record.grant))
484
+ throw new ExecutorError("adapter_retirement_unknown");
485
+ if (this.calls().some((call) => call.principal === id && !call.acknowledged) || this.deliveryDrain(id).unsettled)
486
+ throw new ExecutorError("adapter_drain_blocked");
487
+ return;
488
+ }
489
+ const child = this.children.get(id);
490
+ if (this.calls().some((call) => call.principal === id && !call.acknowledged) || this.deliveryDrain(id).unsettled)
491
+ throw new ExecutorError("adapter_drain_blocked");
492
+ this.retiring.add(id);
493
+ try {
494
+ await this.controller.retireAdapter(id);
495
+ } catch (error) {
496
+ this.retiring.delete(id);
497
+ throw error;
498
+ }
499
+ record.phase = "retired";
500
+ this.put(record);
501
+ await child?.close();
502
+ this.children.delete(id);
503
+ if (child && child.process.pid && retirementIntentDigest && (child.process.exitCode !== null || child.process.signalCode !== null)) {
504
+ record.retirementCompletion = AdapterRetirementCompletion.parse({
505
+ format: 1,
506
+ installationId: this.config.installationId,
507
+ workerId: this.config.workerId,
508
+ adapterId: id,
509
+ manifestDigest: digest(record.manifest),
510
+ grantDigest: digest(record.grant),
511
+ retirementIntentDigest,
512
+ managerInvocation: this.invocation,
513
+ childPid: child.process.pid,
514
+ completionId: randomUUID()
515
+ });
516
+ this.put(record);
517
+ }
518
+ });
519
+ }
520
+ stopAdapter(id) {
521
+ return this.exclusive(async () => {
522
+ await this.children.get(id)?.close();
523
+ this.children.delete(id);
524
+ });
525
+ }
526
+ close() {
527
+ return this.closed ??= (async () => {
528
+ this.closing = true;
529
+ await this.serial;
530
+ await Promise.allSettled([...this.deliveries.values()]);
531
+ for (const child of this.children.values()) await child.close();
532
+ this.children.clear();
533
+ this.db.close();
534
+ this.lock.close();
535
+ })();
536
+ }
537
+ }
538
+ export {
539
+ ReleaseManagerConfig,
540
+ WorkerAdapterReleases
541
+ };
@@ -0,0 +1,88 @@
1
+ import { spawn } from "node:child_process";
2
+ import { randomUUID } from "node:crypto";
3
+ import { ExecutorError, isStartNonAdmissionCode, MAX_FRAME_BYTES } from "../protocol.mjs";
4
+ class AdapterChild {
5
+ constructor(runtimePath, entrypoint, configPath, home, timeoutMs = 1e4) {
6
+ this.timeoutMs = timeoutMs;
7
+ this.process = spawn(runtimePath, [entrypoint, configPath], {
8
+ cwd: home,
9
+ env: { PATH: "/usr/local/bin:/usr/bin:/bin", HOME: home, LANG: "C.UTF-8" },
10
+ stdio: "pipe"
11
+ });
12
+ let buffer = Buffer.alloc(0);
13
+ this.process.stdout.on("data", (chunk) => {
14
+ buffer = Buffer.concat([buffer, chunk]);
15
+ if (buffer.length > MAX_FRAME_BYTES) {
16
+ this.fail();
17
+ this.process.kill();
18
+ return;
19
+ }
20
+ while (buffer.includes(10)) {
21
+ const end = buffer.indexOf(10), line = buffer.subarray(0, end);
22
+ buffer = buffer.subarray(end + 1);
23
+ try {
24
+ const message = JSON.parse(line.toString("utf8"));
25
+ const pending = this.pending.get(message.id);
26
+ if (!pending) continue;
27
+ this.pending.delete(message.id);
28
+ clearTimeout(pending.timer);
29
+ if (message.ok === true) pending.resolve(message.result);
30
+ else
31
+ pending.reject(
32
+ new ExecutorError(
33
+ message.error?.code ?? "adapter_outcome_unknown",
34
+ void 0,
35
+ message.ok === false && pending.method === "start" && isStartNonAdmissionCode(message.error?.code) && message.error?.rejectedBeforeAdmission === true
36
+ )
37
+ );
38
+ } catch {
39
+ this.fail();
40
+ this.process.kill();
41
+ }
42
+ }
43
+ });
44
+ this.process.stderr.resume();
45
+ this.process.stdin.on("error", () => this.fail());
46
+ this.process.once("error", () => this.fail());
47
+ this.process.once("exit", () => this.fail());
48
+ }
49
+ timeoutMs;
50
+ process;
51
+ pending = /* @__PURE__ */ new Map();
52
+ gone = false;
53
+ fail() {
54
+ this.gone = true;
55
+ for (const call of this.pending.values()) {
56
+ clearTimeout(call.timer);
57
+ call.reject(new ExecutorError("adapter_outcome_unknown"));
58
+ }
59
+ this.pending.clear();
60
+ }
61
+ get alive() {
62
+ return !this.gone && this.process.exitCode === null && this.process.signalCode === null;
63
+ }
64
+ request(command) {
65
+ if (!this.alive) return Promise.reject(new ExecutorError("adapter_outcome_unknown"));
66
+ const id = randomUUID(), frame = JSON.stringify({ id, command }) + "\n";
67
+ if (Buffer.byteLength(frame) > MAX_FRAME_BYTES) return Promise.reject(new ExecutorError("request_too_large"));
68
+ return new Promise((resolve, reject) => {
69
+ const timer = setTimeout(() => {
70
+ this.pending.delete(id);
71
+ reject(new ExecutorError("adapter_outcome_unknown"));
72
+ }, this.timeoutMs);
73
+ this.pending.set(id, { resolve: (result) => resolve(result), reject, timer, method: command.method });
74
+ this.process.stdin.write(frame);
75
+ });
76
+ }
77
+ async close() {
78
+ if (this.process.exitCode !== null || this.process.signalCode !== null) return;
79
+ const stopped = new Promise((resolve) => this.process.once("close", () => resolve()));
80
+ this.process.kill("SIGTERM");
81
+ const timer = setTimeout(() => this.process.kill("SIGKILL"), 2e3);
82
+ await stopped;
83
+ clearTimeout(timer);
84
+ }
85
+ }
86
+ export {
87
+ AdapterChild
88
+ };
@@ -0,0 +1,18 @@
1
+ import { z } from "zod";
2
+ import { RuntimeId } from "@ricsam/r5d-api/runtime-protocol";
3
+ const Digest = z.string().regex(/^[a-f0-9]{64}$/);
4
+ const AdapterRetirementCompletion = z.object({
5
+ format: z.literal(1),
6
+ installationId: RuntimeId,
7
+ workerId: RuntimeId,
8
+ adapterId: RuntimeId,
9
+ manifestDigest: Digest,
10
+ grantDigest: Digest,
11
+ retirementIntentDigest: Digest,
12
+ managerInvocation: z.string().uuid(),
13
+ childPid: z.number().int().positive(),
14
+ completionId: z.string().uuid()
15
+ }).strict();
16
+ export {
17
+ AdapterRetirementCompletion
18
+ };