@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,436 @@
1
+ import { createHash, timingSafeEqual } from "node:crypto";
2
+ import { spawn } from "node:child_process";
3
+ import { realpathSync, rmSync, writeFileSync, lstatSync, openSync, fstatSync, readFileSync, closeSync, constants } from "node:fs";
4
+ import { isAbsolute, join, relative } from "node:path";
5
+ import { PtySession } from "./pty.mjs";
6
+ import {
7
+ canonicalJson,
8
+ negotiateProtocol,
9
+ operationPayloadHash,
10
+ OwnershipFence
11
+ } from "@ricsam/r5d-api/runtime-protocol";
12
+ import { terminateProcessTree } from "../process-tree.mjs";
13
+ import {
14
+ ExecutorConfig,
15
+ ExecutorCredential,
16
+ ExecutorError,
17
+ ExecutorRequest,
18
+ EXECUTOR_CAPABILITY,
19
+ ShellPayload
20
+ } from "./protocol.mjs";
21
+ import { ExecutorStore, privateDirectory } from "./storage.mjs";
22
+ import { AdapterGrantRegistry, digest } from "./releases/grants.mjs";
23
+ const tokenHash = (token) => createHash("sha256").update(token).digest("hex");
24
+ class HostExecutor {
25
+ constructor(config, nativePty) {
26
+ this.nativePty = nativePty;
27
+ this.config = ExecutorConfig.parse(config);
28
+ this.store = new ExecutorStore(this.config.stateDir, this.config.installationId, this.config.workerId);
29
+ try {
30
+ this.adapters = new AdapterGrantRegistry(this.store.db, this.config);
31
+ } catch (error) {
32
+ this.store.close();
33
+ throw error;
34
+ }
35
+ this.hello = {
36
+ installationId: this.config.installationId,
37
+ instanceId: this.config.instanceId,
38
+ releaseId: this.config.releaseId,
39
+ role: "executor",
40
+ protocol: { min: 1, max: 1 },
41
+ capabilities: [EXECUTOR_CAPABILITY, ...nativePty ? ["host-pty-v1"] : []]
42
+ };
43
+ this.leaseTimer = setInterval(() => {
44
+ const authority = this.authority();
45
+ if (!authority || authority.validUntil <= Date.now()) {
46
+ for (const run of this.active.values())
47
+ void this.finish(run, true, "authority_expired").catch(() => {
48
+ this.poisoned = true;
49
+ });
50
+ }
51
+ }, 100);
52
+ this.leaseTimer.unref();
53
+ }
54
+ nativePty;
55
+ config;
56
+ store;
57
+ adapters;
58
+ hello;
59
+ active = /* @__PURE__ */ new Map();
60
+ leaseTimer;
61
+ closing = false;
62
+ poisoned = false;
63
+ serial = Promise.resolve();
64
+ /** Authority is synchronous and authenticated; never queue lease renewal behind a blocked OS write.
65
+ * Resource commands retain one ordered admission/action boundary. Commands already admitted may
66
+ * complete after promotion; queued commands and yielded admission hashes must recheck the fence.
67
+ */
68
+ handle(input) {
69
+ try {
70
+ const request = ExecutorRequest.parse(input);
71
+ if (request.command.method === "authority") return this.dispatch(request);
72
+ } catch (error) {
73
+ return Promise.reject(error);
74
+ }
75
+ const task = this.serial.then(() => this.dispatch(input));
76
+ this.serial = task.catch(() => {
77
+ });
78
+ return task;
79
+ }
80
+ authenticate(request) {
81
+ const grant = this.adapters.find(request.auth.principalId);
82
+ const principal = this.config.principals.find((p) => p.id === request.auth.principalId) ?? grant?.principal;
83
+ const actual = Buffer.from(tokenHash(request.auth.token), "hex");
84
+ const expected = Buffer.from(principal?.tokenHash ?? "0".repeat(64), "hex");
85
+ if (!timingSafeEqual(actual, expected) || !principal) throw new ExecutorError("unauthenticated");
86
+ if (grant && digest(grant.hello) !== digest(request.hello)) throw new ExecutorError("grant_hello_mismatch");
87
+ negotiateProtocol(this.hello, request.hello, [EXECUTOR_CAPABILITY]);
88
+ if (request.hello.role !== (principal.role === "adapter" ? "worker-adapter" : "scheduler") || request.hello.instanceId !== principal.ownerId)
89
+ throw new ExecutorError("wrong_principal");
90
+ return principal;
91
+ }
92
+ authority() {
93
+ const row = this.store.db.query("SELECT fence, valid_until FROM authority WHERE id=1").get();
94
+ return row ? { fence: OwnershipFence.parse(JSON.parse(row.fence)), validUntil: row.valid_until } : null;
95
+ }
96
+ workerFence(fence) {
97
+ if (fence.installationId !== this.config.installationId || fence.resourceType !== "worker" || fence.resourceId !== this.config.workerId)
98
+ throw new ExecutorError("wrong_resource");
99
+ }
100
+ authorize(principal, run) {
101
+ this.workerFence(run.fence);
102
+ if (principal.role !== "adapter" || !principal.userIds.includes(run.userId) || principal.ownerId !== run.fence.ownerId)
103
+ throw new ExecutorError("forbidden");
104
+ const authority = this.authority();
105
+ if (!authority || authority.validUntil <= Date.now() || canonicalJson(authority.fence) !== canonicalJson(run.fence))
106
+ throw new ExecutorError("stale_fence");
107
+ }
108
+ authorizeAdmission(principal, run) {
109
+ try {
110
+ this.authorize(principal, run);
111
+ } catch (error) {
112
+ if (error instanceof ExecutorError && error.code === "stale_fence") throw new ExecutorError("stale_fence", "stale_fence", true);
113
+ throw error;
114
+ }
115
+ }
116
+ receipt(principal, identity) {
117
+ this.authorize(principal, identity);
118
+ const run = this.store.get(identity.operationId);
119
+ if (!run || run.userId !== identity.userId || run.sessionId !== (identity.sessionId ?? null)) throw new ExecutorError("run_not_found");
120
+ return run;
121
+ }
122
+ async dispatch(input) {
123
+ if (this.closing) throw new ExecutorError("executor_unavailable");
124
+ const request = ExecutorRequest.parse(input);
125
+ const principal = this.authenticate(request);
126
+ const command = request.command;
127
+ if (command.method === "adapter.register") return this.adapters.register(principal, command.grant, this.hello);
128
+ if (command.method === "adapter.retire") return this.adapters.retire(principal, command.principalId, this.authority()?.fence.ownerId);
129
+ if (command.method === "status")
130
+ return {
131
+ hello: this.hello,
132
+ workerId: this.config.workerId,
133
+ authority: this.authority(),
134
+ capacity: this.config.capacity,
135
+ active: Object.fromEntries(
136
+ Object.keys(this.config.capacity).map((lane) => [lane, [...this.active.values()].filter((r) => r.receipt.lane === lane).length])
137
+ ),
138
+ unknown: this.store.all().filter((run2) => run2.state === "unknown").length,
139
+ degraded: this.poisoned,
140
+ ...principal.role === "controller" ? { adapters: this.adapters.list() } : {}
141
+ };
142
+ if (command.method === "authority") {
143
+ if (principal.role !== "controller") throw new ExecutorError("forbidden");
144
+ this.workerFence(command.fence);
145
+ if (!this.config.principals.some((p) => p.role === "adapter" && p.ownerId === command.fence.ownerId) && !this.adapters.hasOwner(command.fence.ownerId))
146
+ throw new ExecutorError("unknown_owner");
147
+ const previous = this.authority();
148
+ if (previous && (command.fence.epoch < previous.fence.epoch || command.fence.epoch === previous.fence.epoch && (canonicalJson(command.fence) !== canonicalJson(previous.fence) || previous.validUntil <= Date.now() || command.validUntil < previous.validUntil)))
149
+ throw new ExecutorError("stale_fence");
150
+ if (command.validUntil <= Date.now() || command.validUntil > Date.now() + this.config.maxLeaseMs)
151
+ throw new ExecutorError("invalid_lease");
152
+ this.store.db.query("INSERT INTO authority VALUES (1,?,?) ON CONFLICT(id) DO UPDATE SET fence=excluded.fence,valid_until=excluded.valid_until").run(canonicalJson(command.fence), command.validUntil);
153
+ return this.authority();
154
+ }
155
+ if (command.method === "start") {
156
+ const operation = command.operation;
157
+ this.authorizeAdmission(principal, operation);
158
+ if (operation.installationId !== this.config.installationId || operation.kind !== "host.shell")
159
+ throw new ExecutorError("unsupported_operation");
160
+ const payload = ShellPayload.parse(operation.payload);
161
+ const hash = await operationPayloadHash(operation);
162
+ this.authorizeAdmission(principal, operation);
163
+ const previous = this.store.get(operation.operationId);
164
+ if (previous) {
165
+ if (previous.payloadHash !== hash) throw new ExecutorError("operation_payload_mismatch");
166
+ return previous;
167
+ }
168
+ if (this.poisoned || this.store.all().some((run2) => run2.state === "unknown")) throw new ExecutorError("reconciliation_required");
169
+ if (!principal.lanes.includes(payload.lane)) throw new ExecutorError("forbidden_lane");
170
+ if ([...this.active.values()].filter((run2) => run2.receipt.lane === payload.lane).length >= this.config.capacity[payload.lane])
171
+ throw new ExecutorError("capacity_busy", "capacity_busy", true);
172
+ if (payload.pty && !this.nativePty) throw new ExecutorError("pty_unavailable");
173
+ if (payload.pty && !payload.interactive) throw new ExecutorError("pty_requires_interactive");
174
+ const cwd = realpathSync(payload.cwd);
175
+ if (!principal.cwdRoots.some((root) => {
176
+ const rel = relative(realpathSync(root), cwd);
177
+ return rel === "" || !rel.startsWith(`..${process.platform === "win32" ? "\\" : "/"}`) && rel !== ".." && !isAbsolute(rel);
178
+ }))
179
+ throw new ExecutorError("forbidden_cwd");
180
+ const credentials = payload.credentials.map((reference) => {
181
+ let credential = this.config.credentials.find(
182
+ (c) => c.id === reference.id && c.generation === reference.generation && c.userId === operation.userId
183
+ );
184
+ let product = false;
185
+ if (!credential && this.config.productCredentialDirectory && /^product-[a-f0-9]{40}$/.test(reference.id)) {
186
+ const root = this.config.productCredentialDirectory, stat = lstatSync(root);
187
+ if (!stat.isDirectory() || stat.isSymbolicLink() || realpathSync(root) !== root || stat.uid !== process.getuid?.() || stat.mode & 63) throw new ExecutorError("forbidden_credential");
188
+ const fd = openSync(join(root, `${reference.id}-${reference.generation}.json`), constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK);
189
+ try {
190
+ const file = fstatSync(fd);
191
+ if (!file.isFile() || file.uid !== process.getuid?.() || file.mode & 63 || file.nlink !== 1 || file.size > 1024 * 1024) throw new ExecutorError("forbidden_credential");
192
+ credential = ExecutorCredential.parse(JSON.parse(readFileSync(fd, "utf8")));
193
+ product = credential.id === reference.id && credential.generation === reference.generation && credential.userId === operation.userId && credential.installationId === this.config.installationId && principal.userIds.includes(operation.userId);
194
+ if (!product) throw new ExecutorError("forbidden_credential");
195
+ } finally {
196
+ closeSync(fd);
197
+ }
198
+ }
199
+ if (!credential || !product && !principal.credentialIds.includes(reference.id)) throw new ExecutorError("forbidden_credential");
200
+ return credential;
201
+ });
202
+ const receipt2 = {
203
+ operationId: operation.operationId,
204
+ userId: operation.userId,
205
+ sessionId: operation.sessionId ?? null,
206
+ payloadHash: hash,
207
+ state: "accepted",
208
+ pid: null,
209
+ pty: !!payload.pty,
210
+ lane: payload.lane,
211
+ stdoutBytes: 0,
212
+ stderrBytes: 0,
213
+ stdinClosed: !payload.interactive,
214
+ exitCode: null,
215
+ signal: null,
216
+ reason: null
217
+ };
218
+ this.store.put(receipt2);
219
+ const secrets = join(this.config.stateDir, "secrets", tokenHash(operation.operationId));
220
+ try {
221
+ privateDirectory(secrets);
222
+ const env = { PATH: "/usr/local/bin:/usr/bin:/bin", LANG: "C.UTF-8", HOME: cwd, ...payload.env };
223
+ for (const credential of credentials) {
224
+ Object.assign(env, credential.env);
225
+ for (const [name, content] of Object.entries(credential.files)) {
226
+ const path = join(secrets, `${tokenHash(credential.id)}-${name}`);
227
+ writeFileSync(path, content, { flag: "wx", mode: 384 });
228
+ env[name] = path;
229
+ }
230
+ }
231
+ let child;
232
+ let pty;
233
+ let resolveExit;
234
+ const exited = new Promise((resolve) => {
235
+ resolveExit = resolve;
236
+ });
237
+ if (payload.pty) {
238
+ pty = new PtySession({ argv: payload.argv, cwd, env, cols: payload.pty.cols, rows: payload.pty.rows });
239
+ await pty.ready;
240
+ } else {
241
+ child = spawn(payload.argv[0], payload.argv.slice(1), { cwd, env, detached: true, stdio: "pipe" });
242
+ }
243
+ receipt2.pid = pty?.pid ?? child?.pid ?? null;
244
+ receipt2.state = "running";
245
+ const run2 = {
246
+ receipt: receipt2,
247
+ child,
248
+ pty,
249
+ secrets,
250
+ cancelled: false,
251
+ process: {
252
+ pid: receipt2.pid ?? 0,
253
+ exited,
254
+ kill: (signal) => {
255
+ if (pty) pty.kill(typeof signal === "string" ? signal : void 0);
256
+ else child.kill(signal);
257
+ }
258
+ },
259
+ timer: setTimeout(
260
+ () => {
261
+ void this.finish(run2, true, "runtime_limit").catch(() => {
262
+ this.poisoned = true;
263
+ });
264
+ },
265
+ Math.min(payload.timeoutMs ?? this.config.maxRunMs, this.config.maxRunMs, payload.lane === "control" ? 12e4 : Infinity)
266
+ )
267
+ };
268
+ this.active.set(receipt2.operationId, run2);
269
+ this.store.put(receipt2);
270
+ const onExit = (code, signal) => {
271
+ resolveExit(code ?? -1);
272
+ receipt2.exitCode = code;
273
+ receipt2.signal = signal;
274
+ void this.finish(run2, false).catch(() => {
275
+ this.poisoned = true;
276
+ });
277
+ };
278
+ if (pty) {
279
+ pty.onData((data) => this.output(run2, "stdout", Buffer.from(data)));
280
+ pty.onExit(({ exitCode, signal }) => {
281
+ if (exitCode === -1) receipt2.reason = "pty_helper_lost";
282
+ onExit(exitCode, signal ? String(signal) : null);
283
+ });
284
+ } else {
285
+ child.stdout.on("data", (data) => this.output(run2, "stdout", data));
286
+ child.stderr.on("data", (data) => this.output(run2, "stderr", data));
287
+ child.stdin.on("error", () => {
288
+ receipt2.stdinClosed = true;
289
+ });
290
+ child.once("error", () => {
291
+ receipt2.reason = "spawn_failed";
292
+ });
293
+ child.once("exit", () => {
294
+ void this.finish(run2, false).catch(() => {
295
+ this.poisoned = true;
296
+ });
297
+ });
298
+ child.once("close", onExit);
299
+ if (!payload.interactive) child.stdin.end();
300
+ }
301
+ return { ...receipt2 };
302
+ } catch {
303
+ receipt2.state = "unknown";
304
+ receipt2.reason = "admission_outcome_uncertain";
305
+ this.store.put(receipt2);
306
+ rmSync(secrets, { force: true, recursive: true });
307
+ return receipt2;
308
+ }
309
+ }
310
+ let receipt = this.receipt(principal, command.run);
311
+ if (command.method === "poll")
312
+ return {
313
+ receipt,
314
+ stdout: this.store.read(receipt.operationId, "stdout", command.stdoutOffset, receipt.stdoutBytes, command.maxBytes),
315
+ stderr: this.store.read(receipt.operationId, "stderr", command.stderrOffset, receipt.stderrBytes, command.maxBytes)
316
+ };
317
+ const actionHash = tokenHash(
318
+ canonicalJson({
319
+ method: command.method,
320
+ ...command.method === "write" ? { data: command.data, eof: command.eof } : command.method === "resize" ? { cols: command.cols, rows: command.rows } : {}
321
+ })
322
+ );
323
+ const prior = this.store.db.query("SELECT hash,state FROM actions WHERE run_id=? AND id=?").get(receipt.operationId, command.actionId);
324
+ if (prior) {
325
+ if (prior.hash !== actionHash) throw new ExecutorError("action_payload_mismatch");
326
+ return { actionId: command.actionId, state: prior.state };
327
+ }
328
+ const run = this.active.get(receipt.operationId);
329
+ if (command.method === "cancel") {
330
+ if (run?.finishing) {
331
+ await run.finishing.catch(() => {
332
+ });
333
+ receipt = this.receipt(principal, command.run);
334
+ }
335
+ if (receipt.state === "completed" || receipt.state === "cancelled") {
336
+ this.store.db.query("INSERT INTO actions VALUES (?,?,?,'completed')").run(receipt.operationId, command.actionId, actionHash);
337
+ return { actionId: command.actionId, state: "completed" };
338
+ }
339
+ }
340
+ if (!run || run.finishing) throw new ExecutorError(receipt.state === "unknown" ? "outcome_unknown" : "run_not_running");
341
+ if (command.method === "write" && receipt.stdinClosed) throw new ExecutorError("stdin_closed");
342
+ if (command.method === "resize" && !run.pty) throw new ExecutorError("not_a_pty");
343
+ this.store.db.query("INSERT INTO actions VALUES (?,?,?,'accepted')").run(receipt.operationId, command.actionId, actionHash);
344
+ try {
345
+ if (command.method === "cancel") await this.finish(run, true, "requested");
346
+ else if (command.method === "resize") await run.pty.resize(command.cols, command.rows);
347
+ else {
348
+ if (run.pty) {
349
+ await run.pty.write(command.data);
350
+ if (command.eof) await run.pty.write("");
351
+ } else {
352
+ await new Promise((resolve, reject) => {
353
+ const timer = setTimeout(() => reject(new ExecutorError("input_outcome_unknown")), 2e3);
354
+ run.child.stdin.write(command.data, (error) => {
355
+ clearTimeout(timer);
356
+ if (error) reject(error);
357
+ else resolve();
358
+ });
359
+ });
360
+ if (command.eof) run.child.stdin.end();
361
+ }
362
+ if (command.eof) {
363
+ receipt.stdinClosed = true;
364
+ run.receipt.stdinClosed = true;
365
+ this.store.put(run.receipt);
366
+ }
367
+ }
368
+ this.store.db.query("UPDATE actions SET state='completed' WHERE run_id=? AND id=?").run(receipt.operationId, command.actionId);
369
+ return { actionId: command.actionId, state: "completed" };
370
+ } catch {
371
+ if (command.method === "write") {
372
+ run.receipt.stdinClosed = true;
373
+ this.store.put(run.receipt);
374
+ }
375
+ this.store.db.query("UPDATE actions SET state='unknown' WHERE run_id=? AND id=?").run(receipt.operationId, command.actionId);
376
+ return { actionId: command.actionId, state: "unknown" };
377
+ }
378
+ }
379
+ output(run, stream, data) {
380
+ try {
381
+ if (run.receipt.stdoutBytes + run.receipt.stderrBytes + data.length > this.config.maxLogBytes) {
382
+ void this.finish(run, true, "output_limit").catch(() => {
383
+ this.poisoned = true;
384
+ });
385
+ return;
386
+ }
387
+ this.store.append(run.receipt.operationId, stream, data);
388
+ run.receipt[stream === "stdout" ? "stdoutBytes" : "stderrBytes"] += data.length;
389
+ this.store.put(run.receipt);
390
+ } catch {
391
+ this.poisoned = true;
392
+ void this.finish(run, true, "output_persistence_failed").catch(() => {
393
+ });
394
+ }
395
+ }
396
+ finish(run, cancelled, reason) {
397
+ if (cancelled) run.cancelled = true;
398
+ if (reason) run.receipt.reason = reason;
399
+ if (run.finishing) return run.finishing;
400
+ run.finishing = (async () => {
401
+ clearTimeout(run.timer);
402
+ let cleanupFailed = run.receipt.reason === "pty_helper_lost";
403
+ try {
404
+ if (run.process.pid) await terminateProcessTree(run.process, { graceMs: 200, forceKillWaitMs: 1e3 });
405
+ await Promise.race([
406
+ run.process.exited,
407
+ new Promise((_, reject) => {
408
+ const timer = setTimeout(() => reject(new ExecutorError("process_exit_unknown")), 2e3);
409
+ void run.process.exited.then(() => clearTimeout(timer));
410
+ })
411
+ ]);
412
+ } catch {
413
+ cleanupFailed = true;
414
+ }
415
+ rmSync(run.secrets, { recursive: true, force: true });
416
+ run.receipt.state = cleanupFailed ? "unknown" : run.cancelled ? "cancelled" : "completed";
417
+ run.receipt.stdinClosed = true;
418
+ if (cleanupFailed) run.receipt.reason = "process_cleanup_uncertain";
419
+ this.store.put(run.receipt);
420
+ this.active.delete(run.receipt.operationId);
421
+ if (cleanupFailed) throw new ExecutorError("process_cleanup_uncertain");
422
+ })();
423
+ return run.finishing;
424
+ }
425
+ async close() {
426
+ this.closing = true;
427
+ clearInterval(this.leaseTimer);
428
+ await this.serial;
429
+ await Promise.allSettled([...this.active.values()].map((run) => this.finish(run, true, "executor_shutdown")));
430
+ this.store.close();
431
+ }
432
+ }
433
+ export {
434
+ HostExecutor,
435
+ tokenHash
436
+ };
@@ -0,0 +1,15 @@
1
+ import { startHostExecutor } from "./daemon.mjs";
2
+ import { HostExecutorClient } from "./client.mjs";
3
+ import { tokenHash } from "./executor.mjs";
4
+ import { AdapterConfig, ExecutorConfig, ExecutorCommand, ExecutorError, EXECUTOR_CAPABILITY, ShellPayload } from "./protocol.mjs";
5
+ export {
6
+ AdapterConfig,
7
+ EXECUTOR_CAPABILITY,
8
+ ExecutorCommand,
9
+ ExecutorConfig,
10
+ ExecutorError,
11
+ HostExecutorClient,
12
+ ShellPayload,
13
+ startHostExecutor,
14
+ tokenHash
15
+ };
@@ -0,0 +1,139 @@
1
+ import { z } from "zod";
2
+ import { OperationEnvelope, OwnershipFence, RUNTIME_PROTOCOL, RuntimeHello, RuntimeId } from "@ricsam/r5d-api/runtime-protocol";
3
+ const EXECUTOR_CAPABILITY = "host-executor-v1";
4
+ const MAX_FRAME_BYTES = 1024 * 1024;
5
+ const text = z.string().max(256 * 1024).refine((s) => !s.includes("\0"), "NUL is not allowed");
6
+ const absolutePath = z.string().min(1).max(4096).refine((s) => s.startsWith("/") && !s.includes("\0"), "Absolute POSIX path required");
7
+ const Lane = z.enum(["general", "utility", "control"]);
8
+ const ShellPayload = z.object({
9
+ argv: z.array(text).min(1).max(256).refine((argv) => !!argv[0]),
10
+ cwd: absolutePath,
11
+ env: z.record(z.string().regex(/^[A-Za-z_][A-Za-z0-9_]*$/), text).default({}),
12
+ lane: Lane.default("general"),
13
+ interactive: z.boolean().default(false),
14
+ pty: z.object({ cols: z.number().int().min(1).max(1e3), rows: z.number().int().min(1).max(1e3) }).strict().optional(),
15
+ credentials: z.array(z.object({ id: RuntimeId, generation: z.number().int().positive() }).strict()).max(32).default([]),
16
+ timeoutMs: z.number().int().min(100).max(864e5).optional()
17
+ }).strict();
18
+ const RunIdentity = z.object({ operationId: RuntimeId, userId: RuntimeId, sessionId: RuntimeId.optional(), fence: OwnershipFence }).strict();
19
+ const Principal = z.object({
20
+ id: RuntimeId,
21
+ tokenHash: z.string().regex(/^[a-f0-9]{64}$/),
22
+ role: z.enum(["controller", "adapter"]),
23
+ ownerId: RuntimeId,
24
+ userIds: z.array(RuntimeId).min(1),
25
+ cwdRoots: z.array(absolutePath),
26
+ lanes: z.array(Lane),
27
+ credentialIds: z.array(RuntimeId)
28
+ }).strict();
29
+ const AdapterGrant = z.object({
30
+ installationId: RuntimeId,
31
+ workerId: RuntimeId,
32
+ principal: Principal.extend({ role: z.literal("adapter") }),
33
+ hello: RuntimeHello,
34
+ artifactDigest: z.string().regex(/^[a-f0-9]{64}$/)
35
+ }).strict();
36
+ const ExecutorCommand = z.discriminatedUnion("method", [
37
+ z.object({ method: z.literal("adapter.register"), grant: AdapterGrant }).strict(),
38
+ z.object({ method: z.literal("adapter.retire"), principalId: RuntimeId }).strict(),
39
+ z.object({ method: z.literal("status") }).strict(),
40
+ z.object({ method: z.literal("authority"), fence: OwnershipFence, validUntil: z.number().int().positive() }).strict(),
41
+ z.object({ method: z.literal("start"), operation: OperationEnvelope }).strict(),
42
+ z.object({
43
+ method: z.literal("poll"),
44
+ run: RunIdentity,
45
+ stdoutOffset: z.number().int().nonnegative().default(0),
46
+ stderrOffset: z.number().int().nonnegative().default(0),
47
+ maxBytes: z.number().int().min(1).max(65536).default(32768)
48
+ }).strict(),
49
+ z.object({ method: z.literal("write"), run: RunIdentity, actionId: RuntimeId, data: text, eof: z.boolean().default(false) }).strict(),
50
+ z.object({
51
+ method: z.literal("resize"),
52
+ run: RunIdentity,
53
+ actionId: RuntimeId,
54
+ cols: z.number().int().min(1).max(1e3),
55
+ rows: z.number().int().min(1).max(1e3)
56
+ }).strict(),
57
+ z.object({ method: z.literal("cancel"), run: RunIdentity, actionId: RuntimeId }).strict()
58
+ ]);
59
+ const ExecutorRequest = z.object({
60
+ protocol: z.literal(RUNTIME_PROTOCOL),
61
+ requestId: RuntimeId,
62
+ auth: z.object({ principalId: RuntimeId, token: z.string().min(32).max(512) }).strict(),
63
+ hello: RuntimeHello,
64
+ command: ExecutorCommand
65
+ }).strict();
66
+ const ExecutorCredential = z.object({
67
+ id: RuntimeId,
68
+ installationId: RuntimeId,
69
+ userId: RuntimeId,
70
+ generation: z.number().int().positive(),
71
+ env: z.record(z.string().regex(/^[A-Za-z_][A-Za-z0-9_]*$/), text),
72
+ files: z.record(z.string().regex(/^[A-Za-z_][A-Za-z0-9_]*$/), text)
73
+ }).strict().refine((value) => !["R5D_ROOT", "R5D_SESSION_ID"].some((name) => name in value.env || name in value.files), "Workspace identity environment is authority controlled");
74
+ const ExecutorConfig = z.object({
75
+ installationId: RuntimeId,
76
+ workerId: RuntimeId,
77
+ instanceId: RuntimeId,
78
+ releaseId: RuntimeId,
79
+ stateDir: absolutePath,
80
+ /** Private immutable snapshots materialized by the independent workspace service. */
81
+ productCredentialDirectory: absolutePath.optional(),
82
+ capacity: z.object({
83
+ general: z.number().int().min(1).max(100),
84
+ utility: z.number().int().min(0).max(100),
85
+ control: z.number().int().min(0).max(100)
86
+ }).strict(),
87
+ maxLeaseMs: z.number().int().min(1e3).max(3e5).default(3e5),
88
+ maxRunMs: z.number().int().min(100).max(864e5).default(36e5),
89
+ maxLogBytes: z.number().int().min(1024).max(1024 * 1024 * 1024).default(64 * 1024 * 1024),
90
+ principals: z.array(Principal).min(1),
91
+ credentials: z.array(ExecutorCredential).default([])
92
+ }).strict().superRefine((value, ctx) => {
93
+ for (const field of ["principals", "credentials"]) {
94
+ if (new Set(value[field].map((item) => item.id)).size !== value[field].length)
95
+ ctx.addIssue({ code: "custom", message: `Duplicate ${field}` });
96
+ }
97
+ if (value.credentials.some((credential) => credential.installationId !== value.installationId))
98
+ ctx.addIssue({ code: "custom", message: "Cross-installation credential" });
99
+ });
100
+ const AdapterConfig = z.object({
101
+ socketPath: absolutePath,
102
+ principalId: RuntimeId,
103
+ token: z.string().min(32).max(512),
104
+ hello: RuntimeHello,
105
+ timeoutMs: z.number().int().min(100).max(6e4).default(1e4)
106
+ }).strict();
107
+ class ExecutorError extends Error {
108
+ constructor(code, message = code, rejectedBeforeAdmission = false) {
109
+ super(message);
110
+ this.code = code;
111
+ this.rejectedBeforeAdmission = rejectedBeforeAdmission;
112
+ this.name = "ExecutorError";
113
+ }
114
+ code;
115
+ rejectedBeforeAdmission;
116
+ }
117
+ function isStartNonAdmissionCode(code) {
118
+ return code === "stale_fence" || code === "capacity_busy";
119
+ }
120
+ function isDefiniteStartRejection(error) {
121
+ return error instanceof ExecutorError && error.rejectedBeforeAdmission === true && isStartNonAdmissionCode(error.code);
122
+ }
123
+ export {
124
+ AdapterConfig,
125
+ AdapterGrant,
126
+ EXECUTOR_CAPABILITY,
127
+ ExecutorCommand,
128
+ ExecutorConfig,
129
+ ExecutorCredential,
130
+ ExecutorError,
131
+ ExecutorRequest,
132
+ Lane,
133
+ MAX_FRAME_BYTES,
134
+ Principal,
135
+ RunIdentity,
136
+ ShellPayload,
137
+ isDefiniteStartRejection,
138
+ isStartNonAdmissionCode
139
+ };