@ricsam/r5d-worker 0.0.137 → 0.0.139

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 (236) hide show
  1. package/README.md +4 -94
  2. package/dist/cjs/package.json +1 -1
  3. package/dist/mjs/main.mjs +46 -7543
  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 +359 -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 +3 -1
  41. package/dist/types/runtime/executor.d.ts +6 -1
  42. package/dist/types/runtime/protocol.d.ts +176 -4
  43. package/dist/types/runtime/pty.d.ts +1 -0
  44. package/dist/types/runtime/releases/artifacts.d.ts +51 -0
  45. package/dist/types/runtime/releases/grants.d.ts +16 -0
  46. package/dist/types/runtime/releases/manager.d.ts +145 -0
  47. package/dist/types/runtime/releases/process.d.ts +14 -0
  48. package/dist/types/runtime/releases/retirement.d.ts +15 -0
  49. package/dist/types/runtime/releases/rpc-client.d.ts +52 -0
  50. package/dist/types/runtime/releases/rpc-main.d.ts +10 -0
  51. package/dist/types/runtime/releases/rpc-protocol.d.ts +1139 -0
  52. package/dist/types/runtime/workspace/artifacts.d.ts +30 -0
  53. package/dist/types/runtime/workspace/authority.d.ts +338 -0
  54. package/dist/types/runtime/workspace/contracts.d.ts +86 -0
  55. package/dist/types/runtime/workspace/file-write.d.ts +14 -0
  56. package/dist/types/runtime/workspace/files.d.ts +21 -0
  57. package/dist/types/runtime/workspace/http-body.d.ts +9 -0
  58. package/dist/types/runtime/workspace/storage-client.d.ts +23 -0
  59. package/dist/types/runtime/workspace/storage-wire.d.ts +138 -0
  60. package/package.json +8 -6
  61. package/dist/cjs/atomic-rename.cjs +0 -303
  62. package/dist/cjs/browser-upload.cjs +0 -73
  63. package/dist/cjs/cli-update.cjs +0 -108
  64. package/dist/cjs/command-launcher.cjs +0 -423
  65. package/dist/cjs/control-command-policy.cjs +0 -177
  66. package/dist/cjs/git-blob-hash.cjs +0 -41
  67. package/dist/cjs/git-identity.cjs +0 -39
  68. package/dist/cjs/git-process-environment.cjs +0 -133
  69. package/dist/cjs/inline-output.cjs +0 -62
  70. package/dist/cjs/main.cjs +0 -7517
  71. package/dist/cjs/managed-paths.cjs +0 -215
  72. package/dist/cjs/plan-lint.cjs +0 -165
  73. package/dist/cjs/plan-parser.cjs +0 -127
  74. package/dist/cjs/plan-paths.cjs +0 -53
  75. package/dist/cjs/port-forward-client.cjs +0 -206
  76. package/dist/cjs/process-tree.cjs +0 -117
  77. package/dist/cjs/project-checkout-garbage.cjs +0 -219
  78. package/dist/cjs/project-mirror-fetch-policy.cjs +0 -82
  79. package/dist/cjs/project-mirror-refs-token.cjs +0 -85
  80. package/dist/cjs/project-workspace-state.cjs +0 -972
  81. package/dist/cjs/project-worktrees.cjs +0 -1713
  82. package/dist/cjs/pty-output-coalescer.cjs +0 -87
  83. package/dist/cjs/recovery-journal-protocol.cjs +0 -56
  84. package/dist/cjs/recovery-journal-runtime.cjs +0 -133
  85. package/dist/cjs/recovery-journal-thread.cjs +0 -9
  86. package/dist/cjs/recovery-journal.cjs +0 -742
  87. package/dist/cjs/recovery-output-window.cjs +0 -77
  88. package/dist/cjs/recovery-protocol.cjs +0 -31
  89. package/dist/cjs/recovery-store.cjs +0 -252
  90. package/dist/cjs/registry-auth.cjs +0 -310
  91. package/dist/cjs/repository-transition-policy.cjs +0 -49
  92. package/dist/cjs/runtime-version.cjs +0 -88
  93. package/dist/cjs/session-file-mutations.cjs +0 -61
  94. package/dist/cjs/supervisor.cjs +0 -147
  95. package/dist/cjs/three-way-merge.cjs +0 -346
  96. package/dist/cjs/working-tree-mirror.cjs +0 -1872
  97. package/dist/cjs/workspace-automatic-sync-policy.cjs +0 -79
  98. package/dist/cjs/workspace-branch-incarnation-policy.cjs +0 -37
  99. package/dist/cjs/workspace-command-sync-policy.cjs +0 -112
  100. package/dist/cjs/workspace-command-targets.cjs +0 -63
  101. package/dist/cjs/workspace-filesystem-executor-thread.cjs +0 -36
  102. package/dist/cjs/workspace-filesystem-executor.cjs +0 -327
  103. package/dist/cjs/workspace-filesystem-job-types.cjs +0 -144
  104. package/dist/cjs/workspace-filesystem-jobs.cjs +0 -59
  105. package/dist/cjs/workspace-git-sync.cjs +0 -3595
  106. package/dist/cjs/workspace-hydration-ledger.cjs +0 -83
  107. package/dist/cjs/workspace-hydration-merge.cjs +0 -448
  108. package/dist/cjs/workspace-hydration-recovery-state.cjs +0 -53
  109. package/dist/cjs/workspace-incident-state.cjs +0 -37
  110. package/dist/cjs/workspace-merge-projection.cjs +0 -628
  111. package/dist/cjs/workspace-mount-boundary.cjs +0 -71
  112. package/dist/cjs/workspace-mount-hold-fence.cjs +0 -120
  113. package/dist/cjs/workspace-mutation-gate.cjs +0 -127
  114. package/dist/cjs/workspace-path-move.cjs +0 -195
  115. package/dist/cjs/workspace-preserve-only-policy.cjs +0 -36
  116. package/dist/cjs/workspace-project-config-policy.cjs +0 -53
  117. package/dist/cjs/workspace-projection-ledger.cjs +0 -81
  118. package/dist/cjs/workspace-publication-evidence.cjs +0 -46
  119. package/dist/cjs/workspace-sync-coalescer.cjs +0 -58
  120. package/dist/mjs/atomic-rename.mjs +0 -261
  121. package/dist/mjs/browser-upload.mjs +0 -42
  122. package/dist/mjs/command-launcher.mjs +0 -393
  123. package/dist/mjs/control-command-policy.mjs +0 -146
  124. package/dist/mjs/git-blob-hash.mjs +0 -16
  125. package/dist/mjs/git-identity.mjs +0 -15
  126. package/dist/mjs/git-process-environment.mjs +0 -105
  127. package/dist/mjs/inline-output.mjs +0 -37
  128. package/dist/mjs/managed-paths.mjs +0 -174
  129. package/dist/mjs/plan-lint.mjs +0 -129
  130. package/dist/mjs/plan-parser.mjs +0 -94
  131. package/dist/mjs/plan-paths.mjs +0 -25
  132. package/dist/mjs/port-forward-client.mjs +0 -172
  133. package/dist/mjs/project-checkout-garbage.mjs +0 -178
  134. package/dist/mjs/project-mirror-fetch-policy.mjs +0 -56
  135. package/dist/mjs/project-mirror-refs-token.mjs +0 -53
  136. package/dist/mjs/project-workspace-state.mjs +0 -937
  137. package/dist/mjs/project-worktrees.mjs +0 -1657
  138. package/dist/mjs/pty-output-coalescer.mjs +0 -61
  139. package/dist/mjs/recovery-journal-protocol.mjs +0 -30
  140. package/dist/mjs/recovery-journal-runtime.mjs +0 -112
  141. package/dist/mjs/recovery-journal-thread.mjs +0 -8
  142. package/dist/mjs/recovery-journal.mjs +0 -694
  143. package/dist/mjs/recovery-output-window.mjs +0 -53
  144. package/dist/mjs/recovery-protocol.mjs +0 -6
  145. package/dist/mjs/recovery-store.mjs +0 -217
  146. package/dist/mjs/registry-auth.mjs +0 -269
  147. package/dist/mjs/repository-transition-policy.mjs +0 -22
  148. package/dist/mjs/runtime-version.mjs +0 -54
  149. package/dist/mjs/session-file-mutations.mjs +0 -37
  150. package/dist/mjs/supervisor.mjs +0 -114
  151. package/dist/mjs/three-way-merge.mjs +0 -318
  152. package/dist/mjs/working-tree-mirror.mjs +0 -1817
  153. package/dist/mjs/workspace-automatic-sync-policy.mjs +0 -48
  154. package/dist/mjs/workspace-branch-incarnation-policy.mjs +0 -13
  155. package/dist/mjs/workspace-command-sync-policy.mjs +0 -84
  156. package/dist/mjs/workspace-command-targets.mjs +0 -37
  157. package/dist/mjs/workspace-filesystem-executor-thread.mjs +0 -38
  158. package/dist/mjs/workspace-filesystem-executor.mjs +0 -287
  159. package/dist/mjs/workspace-filesystem-job-types.mjs +0 -116
  160. package/dist/mjs/workspace-filesystem-jobs.mjs +0 -55
  161. package/dist/mjs/workspace-git-sync.mjs +0 -3553
  162. package/dist/mjs/workspace-hydration-ledger.mjs +0 -58
  163. package/dist/mjs/workspace-hydration-merge.mjs +0 -414
  164. package/dist/mjs/workspace-hydration-recovery-state.mjs +0 -29
  165. package/dist/mjs/workspace-incident-state.mjs +0 -12
  166. package/dist/mjs/workspace-merge-projection.mjs +0 -594
  167. package/dist/mjs/workspace-mount-boundary.mjs +0 -37
  168. package/dist/mjs/workspace-mount-hold-fence.mjs +0 -95
  169. package/dist/mjs/workspace-mutation-gate.mjs +0 -103
  170. package/dist/mjs/workspace-path-move.mjs +0 -160
  171. package/dist/mjs/workspace-preserve-only-policy.mjs +0 -11
  172. package/dist/mjs/workspace-project-config-policy.mjs +0 -27
  173. package/dist/mjs/workspace-projection-ledger.mjs +0 -57
  174. package/dist/mjs/workspace-publication-evidence.mjs +0 -21
  175. package/dist/mjs/workspace-sync-coalescer.mjs +0 -34
  176. package/dist/types/atomic-rename.d.ts +0 -78
  177. package/dist/types/browser-upload.d.ts +0 -7
  178. package/dist/types/command-launcher.d.ts +0 -104
  179. package/dist/types/control-command-policy.d.ts +0 -53
  180. package/dist/types/git-blob-hash.d.ts +0 -10
  181. package/dist/types/git-identity.d.ts +0 -9
  182. package/dist/types/git-process-environment.d.ts +0 -9
  183. package/dist/types/inline-output.d.ts +0 -3
  184. package/dist/types/managed-paths.d.ts +0 -19
  185. package/dist/types/plan-lint.d.ts +0 -16
  186. package/dist/types/plan-parser.d.ts +0 -38
  187. package/dist/types/plan-paths.d.ts +0 -36
  188. package/dist/types/port-forward-client.d.ts +0 -8
  189. package/dist/types/project-checkout-garbage.d.ts +0 -86
  190. package/dist/types/project-mirror-fetch-policy.d.ts +0 -84
  191. package/dist/types/project-mirror-refs-token.d.ts +0 -53
  192. package/dist/types/project-workspace-state.d.ts +0 -188
  193. package/dist/types/project-worktrees.d.ts +0 -397
  194. package/dist/types/pty-output-coalescer.d.ts +0 -20
  195. package/dist/types/recovery-journal-protocol.d.ts +0 -35
  196. package/dist/types/recovery-journal-runtime.d.ts +0 -12
  197. package/dist/types/recovery-journal-stall-fixture.d.ts +0 -1
  198. package/dist/types/recovery-journal-thread.d.ts +0 -1
  199. package/dist/types/recovery-journal.d.ts +0 -248
  200. package/dist/types/recovery-output-window.d.ts +0 -19
  201. package/dist/types/recovery-protocol.d.ts +0 -59
  202. package/dist/types/recovery-store.d.ts +0 -52
  203. package/dist/types/registry-auth.d.ts +0 -47
  204. package/dist/types/repository-transition-policy.d.ts +0 -26
  205. package/dist/types/runtime-version.d.ts +0 -1
  206. package/dist/types/session-file-mutations.d.ts +0 -22
  207. package/dist/types/supervisor-daemonized-fixture.d.ts +0 -1
  208. package/dist/types/supervisor-launcher-cleanup-fixture.d.ts +0 -1
  209. package/dist/types/supervisor-signal-fixture.d.ts +0 -1
  210. package/dist/types/supervisor.d.ts +0 -23
  211. package/dist/types/three-way-merge.d.ts +0 -77
  212. package/dist/types/working-tree-mirror.d.ts +0 -424
  213. package/dist/types/workspace-automatic-sync-policy.d.ts +0 -57
  214. package/dist/types/workspace-branch-incarnation-policy.d.ts +0 -14
  215. package/dist/types/workspace-command-sync-policy.d.ts +0 -57
  216. package/dist/types/workspace-command-targets.d.ts +0 -37
  217. package/dist/types/workspace-filesystem-executor-thread.d.ts +0 -1
  218. package/dist/types/workspace-filesystem-executor.d.ts +0 -123
  219. package/dist/types/workspace-filesystem-job-types.d.ts +0 -302
  220. package/dist/types/workspace-filesystem-jobs.d.ts +0 -7
  221. package/dist/types/workspace-git-sync.d.ts +0 -514
  222. package/dist/types/workspace-hydration-ledger.d.ts +0 -76
  223. package/dist/types/workspace-hydration-merge.d.ts +0 -103
  224. package/dist/types/workspace-hydration-recovery-state.d.ts +0 -10
  225. package/dist/types/workspace-incident-state.d.ts +0 -19
  226. package/dist/types/workspace-merge-projection.d.ts +0 -69
  227. package/dist/types/workspace-mount-boundary.d.ts +0 -10
  228. package/dist/types/workspace-mount-hold-fence.d.ts +0 -42
  229. package/dist/types/workspace-mutation-gate.d.ts +0 -42
  230. package/dist/types/workspace-path-move.d.ts +0 -21
  231. package/dist/types/workspace-preserve-only-policy.d.ts +0 -15
  232. package/dist/types/workspace-project-config-policy.d.ts +0 -48
  233. package/dist/types/workspace-projection-ledger.d.ts +0 -59
  234. package/dist/types/workspace-publication-evidence.d.ts +0 -17
  235. package/dist/types/workspace-sync-coalescer.d.ts +0 -28
  236. /package/dist/types/{credential-authority-lock-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
+ };