@elaraai/e3-rack 0.0.0-bootstrap.0 → 1.0.94

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 (306) hide show
  1. package/README.md +407 -2
  2. package/dist/src/client/client.spec.d.ts +6 -0
  3. package/dist/src/client/client.spec.d.ts.map +1 -0
  4. package/dist/src/client/client.spec.js +150 -0
  5. package/dist/src/client/client.spec.js.map +1 -0
  6. package/dist/src/client/connect.d.ts +52 -0
  7. package/dist/src/client/connect.d.ts.map +1 -0
  8. package/dist/src/client/connect.js +112 -0
  9. package/dist/src/client/connect.js.map +1 -0
  10. package/dist/src/client/hub-client.d.ts +57 -0
  11. package/dist/src/client/hub-client.d.ts.map +1 -0
  12. package/dist/src/client/hub-client.js +87 -0
  13. package/dist/src/client/hub-client.js.map +1 -0
  14. package/dist/src/client/local-http.d.ts +78 -0
  15. package/dist/src/client/local-http.d.ts.map +1 -0
  16. package/dist/src/client/local-http.js +149 -0
  17. package/dist/src/client/local-http.js.map +1 -0
  18. package/dist/src/client/session-attempts.d.ts +20 -0
  19. package/dist/src/client/session-attempts.d.ts.map +1 -0
  20. package/dist/src/client/session-attempts.js +88 -0
  21. package/dist/src/client/session-attempts.js.map +1 -0
  22. package/dist/src/client/session-data-server.d.ts +37 -0
  23. package/dist/src/client/session-data-server.d.ts.map +1 -0
  24. package/dist/src/client/session-data-server.js +280 -0
  25. package/dist/src/client/session-data-server.js.map +1 -0
  26. package/dist/src/client/session-data-server.spec.d.ts +6 -0
  27. package/dist/src/client/session-data-server.spec.d.ts.map +1 -0
  28. package/dist/src/client/session-data-server.spec.js +192 -0
  29. package/dist/src/client/session-data-server.spec.js.map +1 -0
  30. package/dist/src/client/session.d.ts +119 -0
  31. package/dist/src/client/session.d.ts.map +1 -0
  32. package/dist/src/client/session.js +290 -0
  33. package/dist/src/client/session.js.map +1 -0
  34. package/dist/src/hub/config.d.ts +74 -0
  35. package/dist/src/hub/config.d.ts.map +1 -0
  36. package/dist/src/hub/config.js +159 -0
  37. package/dist/src/hub/config.js.map +1 -0
  38. package/dist/src/hub/fair-lease-store.d.ts +36 -0
  39. package/dist/src/hub/fair-lease-store.d.ts.map +1 -0
  40. package/dist/src/hub/fair-lease-store.js +73 -0
  41. package/dist/src/hub/fair-lease-store.js.map +1 -0
  42. package/dist/src/hub/hub.d.ts +91 -0
  43. package/dist/src/hub/hub.d.ts.map +1 -0
  44. package/dist/src/hub/hub.js +641 -0
  45. package/dist/src/hub/hub.js.map +1 -0
  46. package/dist/src/hub/hub.spec.d.ts +6 -0
  47. package/dist/src/hub/hub.spec.d.ts.map +1 -0
  48. package/dist/src/hub/hub.spec.js +363 -0
  49. package/dist/src/hub/hub.spec.js.map +1 -0
  50. package/dist/src/hub/infrastructure.spec.d.ts +6 -0
  51. package/dist/src/hub/infrastructure.spec.d.ts.map +1 -0
  52. package/dist/src/hub/infrastructure.spec.js +211 -0
  53. package/dist/src/hub/infrastructure.spec.js.map +1 -0
  54. package/dist/src/hub/lock.d.ts +62 -0
  55. package/dist/src/hub/lock.d.ts.map +1 -0
  56. package/dist/src/hub/lock.js +165 -0
  57. package/dist/src/hub/lock.js.map +1 -0
  58. package/dist/src/hub/main.d.ts +16 -0
  59. package/dist/src/hub/main.d.ts.map +1 -0
  60. package/dist/src/hub/main.js +56 -0
  61. package/dist/src/hub/main.js.map +1 -0
  62. package/dist/src/hub/session-bridge.d.ts +116 -0
  63. package/dist/src/hub/session-bridge.d.ts.map +1 -0
  64. package/dist/src/hub/session-bridge.js +336 -0
  65. package/dist/src/hub/session-bridge.js.map +1 -0
  66. package/dist/src/identity/file-identity-store.d.ts +18 -0
  67. package/dist/src/identity/file-identity-store.d.ts.map +1 -0
  68. package/dist/src/identity/file-identity-store.js +34 -0
  69. package/dist/src/identity/file-identity-store.js.map +1 -0
  70. package/dist/src/identity/file-identity-store.spec.d.ts +6 -0
  71. package/dist/src/identity/file-identity-store.spec.d.ts.map +1 -0
  72. package/dist/src/identity/file-identity-store.spec.js +59 -0
  73. package/dist/src/identity/file-identity-store.spec.js.map +1 -0
  74. package/dist/src/identity/in-memory-identity-store.d.ts +48 -0
  75. package/dist/src/identity/in-memory-identity-store.d.ts.map +1 -0
  76. package/dist/src/identity/in-memory-identity-store.js +102 -0
  77. package/dist/src/identity/in-memory-identity-store.js.map +1 -0
  78. package/dist/src/identity/machine-identity.d.ts +134 -0
  79. package/dist/src/identity/machine-identity.d.ts.map +1 -0
  80. package/dist/src/identity/machine-identity.js +65 -0
  81. package/dist/src/identity/machine-identity.js.map +1 -0
  82. package/dist/src/index.d.ts +34 -0
  83. package/dist/src/index.d.ts.map +1 -0
  84. package/dist/src/index.js +34 -0
  85. package/dist/src/index.js.map +1 -0
  86. package/dist/src/lease/closure.d.ts +96 -0
  87. package/dist/src/lease/closure.d.ts.map +1 -0
  88. package/dist/src/lease/closure.js +192 -0
  89. package/dist/src/lease/closure.js.map +1 -0
  90. package/dist/src/lease/closure.spec.d.ts +6 -0
  91. package/dist/src/lease/closure.spec.d.ts.map +1 -0
  92. package/dist/src/lease/closure.spec.js +35 -0
  93. package/dist/src/lease/closure.spec.js.map +1 -0
  94. package/dist/src/lease/coordinator.d.ts +34 -0
  95. package/dist/src/lease/coordinator.d.ts.map +1 -0
  96. package/dist/src/lease/coordinator.js +42 -0
  97. package/dist/src/lease/coordinator.js.map +1 -0
  98. package/dist/src/lease/in-memory-lease-store.d.ts +58 -0
  99. package/dist/src/lease/in-memory-lease-store.d.ts.map +1 -0
  100. package/dist/src/lease/in-memory-lease-store.js +132 -0
  101. package/dist/src/lease/in-memory-lease-store.js.map +1 -0
  102. package/dist/src/lease/lease-store.d.ts +169 -0
  103. package/dist/src/lease/lease-store.d.ts.map +1 -0
  104. package/dist/src/lease/lease-store.js +94 -0
  105. package/dist/src/lease/lease-store.js.map +1 -0
  106. package/dist/src/lease/lease-store.spec.d.ts +6 -0
  107. package/dist/src/lease/lease-store.spec.d.ts.map +1 -0
  108. package/dist/src/lease/lease-store.spec.js +8 -0
  109. package/dist/src/lease/lease-store.spec.js.map +1 -0
  110. package/dist/src/lease/rack-dispatch.d.ts +105 -0
  111. package/dist/src/lease/rack-dispatch.d.ts.map +1 -0
  112. package/dist/src/lease/rack-dispatch.js +6 -0
  113. package/dist/src/lease/rack-dispatch.js.map +1 -0
  114. package/dist/src/mutation-queue.d.ts +13 -0
  115. package/dist/src/mutation-queue.d.ts.map +1 -0
  116. package/dist/src/mutation-queue.js +17 -0
  117. package/dist/src/mutation-queue.js.map +1 -0
  118. package/dist/src/paths.d.ts +56 -0
  119. package/dist/src/paths.d.ts.map +1 -0
  120. package/dist/src/paths.js +82 -0
  121. package/dist/src/paths.js.map +1 -0
  122. package/dist/src/protocol/control.d.ts +295 -0
  123. package/dist/src/protocol/control.d.ts.map +1 -0
  124. package/dist/src/protocol/control.js +74 -0
  125. package/dist/src/protocol/control.js.map +1 -0
  126. package/dist/src/protocol/index.d.ts +9 -0
  127. package/dist/src/protocol/index.d.ts.map +1 -0
  128. package/dist/src/protocol/index.js +9 -0
  129. package/dist/src/protocol/index.js.map +1 -0
  130. package/dist/src/protocol/rack-wire.d.ts +357 -0
  131. package/dist/src/protocol/rack-wire.d.ts.map +1 -0
  132. package/dist/src/protocol/rack-wire.js +262 -0
  133. package/dist/src/protocol/rack-wire.js.map +1 -0
  134. package/dist/src/protocol/rack-wire.spec.d.ts +6 -0
  135. package/dist/src/protocol/rack-wire.spec.d.ts.map +1 -0
  136. package/dist/src/protocol/rack-wire.spec.js +14 -0
  137. package/dist/src/protocol/rack-wire.spec.js.map +1 -0
  138. package/dist/src/protocol/registration.d.ts +70 -0
  139. package/dist/src/protocol/registration.d.ts.map +1 -0
  140. package/dist/src/protocol/registration.js +67 -0
  141. package/dist/src/protocol/registration.js.map +1 -0
  142. package/dist/src/protocol/session-data.d.ts +69 -0
  143. package/dist/src/protocol/session-data.d.ts.map +1 -0
  144. package/dist/src/protocol/session-data.js +26 -0
  145. package/dist/src/protocol/session-data.js.map +1 -0
  146. package/dist/src/protocol/task-envelope.d.ts +198 -0
  147. package/dist/src/protocol/task-envelope.d.ts.map +1 -0
  148. package/dist/src/protocol/task-envelope.js +93 -0
  149. package/dist/src/protocol/task-envelope.js.map +1 -0
  150. package/dist/src/protocol/wire-golden.d.ts +8 -0
  151. package/dist/src/protocol/wire-golden.d.ts.map +1 -0
  152. package/dist/src/protocol/wire-golden.js +34 -0
  153. package/dist/src/protocol/wire-golden.js.map +1 -0
  154. package/dist/src/protocol/wire-samples.d.ts +11 -0
  155. package/dist/src/protocol/wire-samples.d.ts.map +1 -0
  156. package/dist/src/protocol/wire-samples.js +52 -0
  157. package/dist/src/protocol/wire-samples.js.map +1 -0
  158. package/dist/src/registry/file-rack-registry.d.ts +68 -0
  159. package/dist/src/registry/file-rack-registry.d.ts.map +1 -0
  160. package/dist/src/registry/file-rack-registry.js +138 -0
  161. package/dist/src/registry/file-rack-registry.js.map +1 -0
  162. package/dist/src/registry/file-rack-registry.spec.d.ts +6 -0
  163. package/dist/src/registry/file-rack-registry.spec.d.ts.map +1 -0
  164. package/dist/src/registry/file-rack-registry.spec.js +50 -0
  165. package/dist/src/registry/file-rack-registry.spec.js.map +1 -0
  166. package/dist/src/registry/rack-registry.d.ts +64 -0
  167. package/dist/src/registry/rack-registry.d.ts.map +1 -0
  168. package/dist/src/registry/rack-registry.js +65 -0
  169. package/dist/src/registry/rack-registry.js.map +1 -0
  170. package/dist/src/registry/rack-registry.spec.d.ts +6 -0
  171. package/dist/src/registry/rack-registry.spec.d.ts.map +1 -0
  172. package/dist/src/registry/rack-registry.spec.js +8 -0
  173. package/dist/src/registry/rack-registry.spec.js.map +1 -0
  174. package/dist/src/routing/eligibility.d.ts +38 -0
  175. package/dist/src/routing/eligibility.d.ts.map +1 -0
  176. package/dist/src/routing/eligibility.js +78 -0
  177. package/dist/src/routing/eligibility.js.map +1 -0
  178. package/dist/src/routing/host-coupling.d.ts +26 -0
  179. package/dist/src/routing/host-coupling.d.ts.map +1 -0
  180. package/dist/src/routing/host-coupling.js +62 -0
  181. package/dist/src/routing/host-coupling.js.map +1 -0
  182. package/dist/src/routing/policy.d.ts +188 -0
  183. package/dist/src/routing/policy.d.ts.map +1 -0
  184. package/dist/src/routing/policy.js +212 -0
  185. package/dist/src/routing/policy.js.map +1 -0
  186. package/dist/src/routing/routing.spec.d.ts +6 -0
  187. package/dist/src/routing/routing.spec.d.ts.map +1 -0
  188. package/dist/src/routing/routing.spec.js +100 -0
  189. package/dist/src/routing/routing.spec.js.map +1 -0
  190. package/dist/src/routing/tiers.d.ts +22 -0
  191. package/dist/src/routing/tiers.d.ts.map +1 -0
  192. package/dist/src/routing/tiers.js +26 -0
  193. package/dist/src/routing/tiers.js.map +1 -0
  194. package/dist/src/runner/api-server.d.ts +45 -0
  195. package/dist/src/runner/api-server.d.ts.map +1 -0
  196. package/dist/src/runner/api-server.js +21 -0
  197. package/dist/src/runner/api-server.js.map +1 -0
  198. package/dist/src/runner/capacity.d.ts +13 -0
  199. package/dist/src/runner/capacity.d.ts.map +1 -0
  200. package/dist/src/runner/capacity.js +18 -0
  201. package/dist/src/runner/capacity.js.map +1 -0
  202. package/dist/src/runner/rack-body.d.ts +43 -0
  203. package/dist/src/runner/rack-body.d.ts.map +1 -0
  204. package/dist/src/runner/rack-body.js +208 -0
  205. package/dist/src/runner/rack-body.js.map +1 -0
  206. package/dist/src/runner/rack-runner.d.ts +62 -0
  207. package/dist/src/runner/rack-runner.d.ts.map +1 -0
  208. package/dist/src/runner/rack-runner.js +75 -0
  209. package/dist/src/runner/rack-runner.js.map +1 -0
  210. package/dist/src/runner/rack-runner.spec.d.ts +6 -0
  211. package/dist/src/runner/rack-runner.spec.d.ts.map +1 -0
  212. package/dist/src/runner/rack-runner.spec.js +302 -0
  213. package/dist/src/runner/rack-runner.spec.js.map +1 -0
  214. package/dist/src/runner/split.spec.d.ts +6 -0
  215. package/dist/src/runner/split.spec.d.ts.map +1 -0
  216. package/dist/src/runner/split.spec.js +188 -0
  217. package/dist/src/runner/split.spec.js.map +1 -0
  218. package/dist/src/runner-event.d.ts +261 -0
  219. package/dist/src/runner-event.d.ts.map +1 -0
  220. package/dist/src/runner-event.js +332 -0
  221. package/dist/src/runner-event.js.map +1 -0
  222. package/dist/src/server/beast2.d.ts +19 -0
  223. package/dist/src/server/beast2.d.ts.map +1 -0
  224. package/dist/src/server/beast2.js +18 -0
  225. package/dist/src/server/beast2.js.map +1 -0
  226. package/dist/src/server/body-limit.d.ts +16 -0
  227. package/dist/src/server/body-limit.d.ts.map +1 -0
  228. package/dist/src/server/body-limit.js +53 -0
  229. package/dist/src/server/body-limit.js.map +1 -0
  230. package/dist/src/server/body-limit.spec.d.ts +2 -0
  231. package/dist/src/server/body-limit.spec.d.ts.map +1 -0
  232. package/dist/src/server/body-limit.spec.js +32 -0
  233. package/dist/src/server/body-limit.spec.js.map +1 -0
  234. package/dist/src/server/bounded.d.ts +16 -0
  235. package/dist/src/server/bounded.d.ts.map +1 -0
  236. package/dist/src/server/bounded.js +39 -0
  237. package/dist/src/server/bounded.js.map +1 -0
  238. package/dist/src/server/dispatch.d.ts +137 -0
  239. package/dist/src/server/dispatch.d.ts.map +1 -0
  240. package/dist/src/server/dispatch.js +267 -0
  241. package/dist/src/server/dispatch.js.map +1 -0
  242. package/dist/src/server/env-publication.d.ts +45 -0
  243. package/dist/src/server/env-publication.d.ts.map +1 -0
  244. package/dist/src/server/env-publication.js +10 -0
  245. package/dist/src/server/env-publication.js.map +1 -0
  246. package/dist/src/server/index.d.ts +16 -0
  247. package/dist/src/server/index.d.ts.map +1 -0
  248. package/dist/src/server/index.js +17 -0
  249. package/dist/src/server/index.js.map +1 -0
  250. package/dist/src/server/protocol.spec.d.ts +6 -0
  251. package/dist/src/server/protocol.spec.d.ts.map +1 -0
  252. package/dist/src/server/protocol.spec.js +78 -0
  253. package/dist/src/server/protocol.spec.js.map +1 -0
  254. package/dist/src/server/rack-routes.d.ts +175 -0
  255. package/dist/src/server/rack-routes.d.ts.map +1 -0
  256. package/dist/src/server/rack-routes.js +755 -0
  257. package/dist/src/server/rack-routes.js.map +1 -0
  258. package/dist/src/server/rack-routes.spec.d.ts +6 -0
  259. package/dist/src/server/rack-routes.spec.d.ts.map +1 -0
  260. package/dist/src/server/rack-routes.spec.js +477 -0
  261. package/dist/src/server/rack-routes.spec.js.map +1 -0
  262. package/dist/src/server/storage-bridge.d.ts +140 -0
  263. package/dist/src/server/storage-bridge.d.ts.map +1 -0
  264. package/dist/src/server/storage-bridge.js +6 -0
  265. package/dist/src/server/storage-bridge.js.map +1 -0
  266. package/dist/src/state-file.d.ts +10 -0
  267. package/dist/src/state-file.d.ts.map +1 -0
  268. package/dist/src/state-file.js +38 -0
  269. package/dist/src/state-file.js.map +1 -0
  270. package/dist/src/testing/in-memory.d.ts +116 -0
  271. package/dist/src/testing/in-memory.d.ts.map +1 -0
  272. package/dist/src/testing/in-memory.js +193 -0
  273. package/dist/src/testing/in-memory.js.map +1 -0
  274. package/dist/src/testing/index.d.ts +14 -0
  275. package/dist/src/testing/index.d.ts.map +1 -0
  276. package/dist/src/testing/index.js +14 -0
  277. package/dist/src/testing/index.js.map +1 -0
  278. package/dist/src/testing/lease-store-contract.d.ts +14 -0
  279. package/dist/src/testing/lease-store-contract.d.ts.map +1 -0
  280. package/dist/src/testing/lease-store-contract.js +78 -0
  281. package/dist/src/testing/lease-store-contract.js.map +1 -0
  282. package/dist/src/testing/protocol-suite.d.ts +38 -0
  283. package/dist/src/testing/protocol-suite.d.ts.map +1 -0
  284. package/dist/src/testing/protocol-suite.js +207 -0
  285. package/dist/src/testing/protocol-suite.js.map +1 -0
  286. package/dist/src/testing/rack-registry-contract.d.ts +15 -0
  287. package/dist/src/testing/rack-registry-contract.d.ts.map +1 -0
  288. package/dist/src/testing/rack-registry-contract.js +106 -0
  289. package/dist/src/testing/rack-registry-contract.js.map +1 -0
  290. package/dist/src/testing/test-rack-agent.d.ts +121 -0
  291. package/dist/src/testing/test-rack-agent.d.ts.map +1 -0
  292. package/dist/src/testing/test-rack-agent.js +316 -0
  293. package/dist/src/testing/test-rack-agent.js.map +1 -0
  294. package/dist/src/tls/tls.d.ts +67 -0
  295. package/dist/src/tls/tls.d.ts.map +1 -0
  296. package/dist/src/tls/tls.js +200 -0
  297. package/dist/src/tls/tls.js.map +1 -0
  298. package/dist/src/tls/tls.spec.d.ts +6 -0
  299. package/dist/src/tls/tls.spec.d.ts.map +1 -0
  300. package/dist/src/tls/tls.spec.js +101 -0
  301. package/dist/src/tls/tls.spec.js.map +1 -0
  302. package/dist/src/version.d.ts +9 -0
  303. package/dist/src/version.d.ts.map +1 -0
  304. package/dist/src/version.js +10 -0
  305. package/dist/src/version.js.map +1 -0
  306. package/package.json +67 -7
@@ -0,0 +1,755 @@
1
+ /**
2
+ * Copyright (c) 2025 Elara AI Pty Ltd
3
+ * Licensed under BSL 1.1. See LICENSE for details.
4
+ */
5
+ /**
6
+ * Machine-authenticated /api/rack routes (epic #67 design §9).
7
+ *
8
+ * #72 ships the identity half: the enrollment exchange (`POST /rack/enroll`,
9
+ * authenticated by the single-use enrollment token itself) and the
10
+ * **machine-auth middleware** every other /rack/* route sits behind.
11
+ * #73 adds the dispatch protocol behind that middleware:
12
+ * - `POST /rack/lease` — long-poll claim of the next matching task
13
+ * - `POST /rack/heartbeat` — liveness + stop signals for dead leases
14
+ * - `POST /rack/lease/extend` — visibility refresh while running
15
+ * - `POST /rack/complete` — terminal result (accepted-and-discarded)
16
+ * #74 adds the data plane:
17
+ * - `POST /rack/storage` — inline bytes / presigned URLs for a claimed
18
+ * lease's objects (scope-validated, review F5) + the output PUT targets
19
+ * - `POST /rack/logs` — batched stdout/stderr → LogStore
20
+ *
21
+ * #214 moves the guest onto e3-core's own execution:
22
+ * - a lease is refused to an agent older than `LEASE_MIN_AGENT`;
23
+ * - the claim of a task or unit lease mints its attempt and records it
24
+ * `running`, stamped with the rack launch; the completion records its
25
+ * outcome, discards what it wrote and never committed, and wakes a run
26
+ * parked on it (#208);
27
+ * - `/rack/storage` admits exactly the lease's closure, and takes the objects
28
+ * its guest wrote in by checksum-bound PUTs and their commits.
29
+ *
30
+ * A rack token authenticates ONLY these routes — it is invisible to the
31
+ * human `jwtAuthorizer`, so it can never reach the human/admin API.
32
+ */
33
+ import { Hono } from 'hono';
34
+ import { setTimeout as delay } from 'node:timers/promises';
35
+ import { rackBodyLimit } from './body-limit.js';
36
+ import { mapBounded } from './bounded.js';
37
+ import { BooleanType, IntegerType, StringType, compareFor, equalFor, none, some, variant } from '@elaraai/east';
38
+ import { isObjectHash, uuidv7 } from '@elaraai/e3-core';
39
+ import { RackEnrollRequestType, RackEnrollResponseType, RackLeaseRequestType, RackLeaseGrantType, RackHeartbeatRequestType, RackHeartbeatResponseType, RackExtendRequestType, RackWatchRequestType, RackWatchResponseType, RACK_WATCH_MAX_LEASES, RackCompleteRequestType, RackCompleteResponseType, RackStorageRequestType, RackStorageResponseType, RackLogsRequestType, } from '../protocol/index.js';
40
+ import { sendSuccess, sendError, decodeBody } from './beast2.js';
41
+ import { updateRack } from '../registry/rack-registry.js';
42
+ import { MAX_LEASE_TARGETS } from '../lease/lease-store.js';
43
+ import { envLayerBlobKey } from './env-publication.js';
44
+ import { isFunctionLease, isLeaseEventV2, leaseRun } from '../protocol/task-envelope.js';
45
+ import { DEFAULT_HEALTHY_WINDOW_MS, DEFAULT_VISIBILITY_MS, heartbeatCapacity, leaseRefusal } from './dispatch.js';
46
+ import { backendLeaseCoordinator } from '../lease/coordinator.js';
47
+ import { hashToken, generateRackToken, RACK_TOKEN_PREFIX, ROTATION_AGE_MS, ROTATION_REOFFER_MS, ROTATION_GRACE_MS, } from '../identity/machine-identity.js';
48
+ /** The most objects one `/rack/storage` request declares or commits. */
49
+ export const MAX_STORAGE_OBJECTS = 256;
50
+ /** The largest object a write target is issued for: one checksum-bound PUT. */
51
+ export const MAX_RACK_OBJECT_BYTES = 5 * 1024 ** 3;
52
+ /** The leases whose closures a route keeps read, the least recently used forgotten first. */
53
+ const CLOSURE_CACHE_LIMIT = 64;
54
+ /** Bound backend reads and signing work without serializing each object. */
55
+ const STORAGE_READS_AT_ONCE = 8;
56
+ const DEFAULT_LEASE_WINDOW_MS = 20_000;
57
+ /** Claim re-check cadence inside a held window. 250 ms keeps interactive
58
+ * function calls (#83) snappy — pickup is bounded by this, not the window —
59
+ * at the cost of ~4 small listClaimable queries/s per idle agent (cents/day
60
+ * on DynamoDB on-demand; the held API-Lambda invocation dominates cost
61
+ * either way). */
62
+ const DEFAULT_POLL_INTERVAL_MS = 250;
63
+ const equalString = equalFor(StringType);
64
+ const compareInteger = compareFor(IntegerType);
65
+ function unauthorized(message) {
66
+ return new Response(JSON.stringify({ success: false, error: { type: 'unauthorized', message } }), { status: 401, headers: { 'Content-Type': 'application/json' } });
67
+ }
68
+ /**
69
+ * Machine-auth middleware for /rack/* routes: validates the bearer rack
70
+ * token (by at-rest hash) and re-resolves the rack's registration so a
71
+ * deregistered rack is rejected immediately — revocation needs no token
72
+ * bookkeeping.
73
+ *
74
+ * Rotation activation (D12, #90) lives here: the FIRST authenticated use
75
+ * of a `pending` successor promotes it to `current` and puts the token it
76
+ * replaces on the ROTATION_GRACE fuse — on any rack route, so activation
77
+ * is observed wherever the agent switches.
78
+ *
79
+ * @param identityStore - Machine-credential persistence
80
+ * @param settingsStore - Rack registry (registration existence = not revoked)
81
+ * @param now - Clock for token rotation and grace expiry
82
+ * @returns Machine authentication middleware
83
+ * @example
84
+ * app.use('/api/rack/*', createRackAuth(identities, registry));
85
+ */
86
+ export function createRackAuth(identityStore, settingsStore, now = Date.now) {
87
+ return async (c, next) => {
88
+ const header = c.req.header('authorization') ?? '';
89
+ const token = header.startsWith('Bearer ') ? header.slice('Bearer '.length) : '';
90
+ if (!token.startsWith(RACK_TOKEN_PREFIX)) {
91
+ return unauthorized('Rack token required');
92
+ }
93
+ const tokenHash = hashToken(token);
94
+ const record = await identityStore.getRackToken(tokenHash);
95
+ if (record === null) {
96
+ return unauthorized('Unknown rack token');
97
+ }
98
+ if (record.graceUntil !== undefined && record.graceUntil.getTime() <= now()) {
99
+ return unauthorized('Superseded rack token (rotation grace elapsed)');
100
+ }
101
+ const registration = await settingsStore.getRack(record.rackId);
102
+ if (registration === null) {
103
+ return unauthorized('Rack is deregistered');
104
+ }
105
+ if (record.status === 'pending') {
106
+ await identityStore.promoteRackToken(record.rackId, tokenHash, new Date(now() + ROTATION_GRACE_MS));
107
+ }
108
+ c.set('rackId', record.rackId);
109
+ c.set('registration', registration);
110
+ c.set('tokenIssuedAt', record.issuedAt);
111
+ c.set('tokenStatus', record.status);
112
+ await next();
113
+ };
114
+ }
115
+ /**
116
+ * Create the /api/rack routes: the enrollment exchange (#72), the dispatch
117
+ * protocol (#73), the data plane (#74), and a lease's attempt (#214).
118
+ *
119
+ * @param identityStore - Machine-credential persistence
120
+ * @param settingsStore - Rack registry
121
+ * @param leaseStore - Lease persistence (the pull queue)
122
+ * @param bridge - Object data-plane (presign/inline/commit)
123
+ * @param attempts - Where a lease's attempt is kept: its records, its log
124
+ * lines, and the store its output is checked whole in
125
+ * @param options - Long-poll/visibility tuning, and the waker
126
+ * @returns The rack machine API
127
+ * @example
128
+ * const app = createRackRoutes(identities, registry, leases, bridge, attempts);
129
+ */
130
+ export function createRackRoutes(identityStore, settingsStore, leaseStore, bridge, attempts, options) {
131
+ const now = options?.now ?? Date.now;
132
+ const coordinator = options?.coordinator ?? backendLeaseCoordinator;
133
+ const leaseWindowMs = options?.leaseWindowMs ?? DEFAULT_LEASE_WINDOW_MS;
134
+ const pollIntervalMs = options?.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS;
135
+ const pollMaxMs = Math.max(pollIntervalMs, options?.pollMaxMs ?? pollIntervalMs);
136
+ const policyRecheckMs = Math.max(0, options?.policyRecheckMs ?? 0);
137
+ const visibilityMs = options?.visibilityMs ?? DEFAULT_VISIBILITY_MS;
138
+ const watchWindowMs = Math.max(1, Math.min(20_000, options?.watchWindowMs ?? 20_000));
139
+ const watchPollIntervalMs = Math.max(1, options?.watchPollIntervalMs ?? 250);
140
+ const envIndex = options?.envIndex;
141
+ const e3Version = options?.e3Version;
142
+ const { logs } = attempts;
143
+ const app = new Hono();
144
+ const auth = createRackAuth(identityStore, settingsStore, now);
145
+ app.use('/api/rack/lease', auth);
146
+ app.use('/api/rack/heartbeat', auth);
147
+ app.use('/api/rack/lease/extend', auth);
148
+ app.use('/api/rack/lease/watch', auth);
149
+ app.use('/api/rack/lease/watch', rackBodyLimit(256 * 1024));
150
+ app.use('/api/rack/complete', auth);
151
+ app.use('/api/rack/storage', auth);
152
+ app.use('/api/rack/logs', auth);
153
+ for (const route of ['enroll', 'lease', 'heartbeat', 'lease/extend', 'complete', 'storage', 'logs']) {
154
+ app.use(`/api/rack/${route}`, rackBodyLimit(route === 'enroll' ? 64 * 1024 : 2 * 1024 * 1024));
155
+ }
156
+ /**
157
+ * Resolve a lease the caller actively holds — the scope gate for the
158
+ * data-plane routes (review F5): storage/logs are per-lease capabilities,
159
+ * not rack-wide ones.
160
+ */
161
+ const heldLease = async (repo, leaseId, rackId) => {
162
+ const lease = await leaseStore.get(repo, leaseId);
163
+ if (lease === null || lease.status !== 'claimed' || lease.claimedBy !== rackId)
164
+ return null;
165
+ return lease;
166
+ };
167
+ const forbidden = (message) => new Response(JSON.stringify({ success: false, error: { type: 'permission_denied', message } }), { status: 403, headers: { 'Content-Type': 'application/json' } });
168
+ /**
169
+ * Every object a held lease may read: its closure (#214), and, for one
170
+ * recorded as an object, that object. A closure never changes, so the
171
+ * hashes of the leases asked after most recently are kept.
172
+ */
173
+ const closures = new Map();
174
+ const admitted = (lease) => {
175
+ const key = `${lease.repo} ${lease.leaseId}`;
176
+ let hashes = closures.get(key);
177
+ if (hashes === undefined) {
178
+ hashes = attempts.closure(lease).then((list) => new Set(list));
179
+ hashes.catch(() => closures.delete(key));
180
+ }
181
+ else {
182
+ closures.delete(key);
183
+ }
184
+ closures.set(key, hashes);
185
+ if (closures.size > CLOSURE_CACHE_LIMIT)
186
+ closures.delete(closures.keys().next().value);
187
+ return hashes;
188
+ };
189
+ /** Serialize registration patches with revocation. Only heartbeats refresh health. */
190
+ const touchRack = (rackId, patch = () => ({})) => coordinator.run('@rack', rackId, () => updateRack(settingsStore, rackId, patch));
191
+ // POST /api/rack/enroll — exchange a single-use enrollment token for a
192
+ // long-lived rack token. The enrollment token is the sole authentication.
193
+ app.post('/api/rack/enroll', async (c) => {
194
+ let request;
195
+ try {
196
+ request = (await decodeBody(c, RackEnrollRequestType));
197
+ }
198
+ catch {
199
+ return unauthorized('Invalid enrollment request body');
200
+ }
201
+ const consumed = await identityStore.consumeEnrollmentToken(hashToken(request.enrollmentToken), new Date(now()));
202
+ if (!consumed) {
203
+ return unauthorized('Invalid, expired, or already-used enrollment token');
204
+ }
205
+ const rackId = uuidv7();
206
+ const { token: rackToken, tokenHash } = generateRackToken();
207
+ const enrolledAt = new Date(now());
208
+ await identityStore.putRackToken(tokenHash, rackId, enrolledAt, 'current');
209
+ // Boot identity starts empty: the first heartbeat's bootId is ADOPTED
210
+ // as approved (the admin authorized this start by minting the token
211
+ // moments ago); only later restarts can differ and quarantine.
212
+ const registration = {
213
+ rackId,
214
+ label: request.hostInfo.label,
215
+ enrolledAt,
216
+ tiers: request.hostInfo.tiers,
217
+ capacity: request.hostInfo.capacity,
218
+ lastSeenAt: enrolledAt,
219
+ agentVersion: request.hostInfo.agentVersion,
220
+ healthy: false,
221
+ lastBootId: none,
222
+ approvedBootId: none,
223
+ };
224
+ await settingsStore.putRack(registration);
225
+ return sendSuccess(RackEnrollResponseType, { rackId, rackToken });
226
+ });
227
+ // POST /api/rack/lease — long-poll claim of the next task matching the
228
+ // agent's tiers. 204 when the window closes with nothing claimable.
229
+ app.post('/api/rack/lease', async (c) => {
230
+ const rackId = c.get('rackId');
231
+ let tiers;
232
+ let bootId;
233
+ try {
234
+ const request = (await decodeBody(c, RackLeaseRequestType));
235
+ tiers = request.tiers;
236
+ bootId = request.bootId;
237
+ }
238
+ catch {
239
+ return sendError(RackLeaseGrantType, variant('internal', { message: 'Invalid lease request body' }));
240
+ }
241
+ // Converge the ADVERTISED tiers into the registration (found live: a
242
+ // re-install with widened --tiers left the registry on the enrollment-
243
+ // time tier set forever — heartbeats never carry tiers, so routing and
244
+ // the fleet UI kept refusing c/full work the agent could run). The
245
+ // tier list the agent POLLS with is exactly what it can execute.
246
+ if (await touchRack(rackId, () => ({ tiers })) === null)
247
+ return unauthorized('Rack is deregistered');
248
+ // #214: an agent older than LEASE_MIN_AGENT cannot run a v2 lease: it is
249
+ // refused, and told to upgrade. The registration's version is refreshed
250
+ // by every heartbeat; the dispatch routes no work at such a rack either
251
+ const refusal = leaseRefusal(c.get('registration').agentVersion);
252
+ if (refusal !== null) {
253
+ return sendError(RackLeaseGrantType, variant('internal', { message: refusal }));
254
+ }
255
+ const deadline = Date.now() + leaseWindowMs;
256
+ let waitMs = pollIntervalMs;
257
+ let policyCheckedAt = Number.NEGATIVE_INFINITY;
258
+ let requireStartApproval = false;
259
+ const signal = options?.signal ? AbortSignal.any([c.req.raw.signal, options.signal]) : c.req.raw.signal;
260
+ for (;;) {
261
+ if (signal.aborted)
262
+ return c.body(null, 204);
263
+ const currentRack = await settingsStore.getRack(rackId);
264
+ if (currentRack === null)
265
+ return unauthorized('Rack is deregistered');
266
+ // Start-approval quarantine (D12): only the APPROVED boot receives
267
+ // work, judged against the request's own bootId — no heartbeat
268
+ // ordering can leak a grant to an unapproved restart. The check
269
+ // lives INSIDE the poll loop: a quarantined agent holds its window
270
+ // open like any idle agent (same pacing, no hot 204 loop) and an
271
+ // approval landing mid-window grants within the current poll interval.
272
+ // Only idle checks may reuse a policy; every actual claim revalidates it.
273
+ if (Date.now() - policyCheckedAt >= policyRecheckMs) {
274
+ requireStartApproval = (await settingsStore.getRackSecurityPolicy())?.requireStartApproval === true;
275
+ policyCheckedAt = Date.now();
276
+ }
277
+ const quarantined = requireStartApproval
278
+ && !(currentRack.approvedBootId.type === 'some' && equalString(currentRack.approvedBootId.value, bootId));
279
+ if (!quarantined && currentRack.healthy && compareInteger(currentRack.capacity, 0n) > 0
280
+ && currentRack.lastSeenAt.getTime() >= now() - DEFAULT_HEALTHY_WINDOW_MS) {
281
+ const candidates = await leaseStore.listClaimable(tiers, now(), 10);
282
+ for (const candidate of candidates) {
283
+ const grant = await coordinator.run(candidate.repo, candidate.leaseId, async () => {
284
+ const current = await leaseStore.get(candidate.repo, candidate.leaseId);
285
+ if (current === null)
286
+ return null;
287
+ // A lease written before #214 is no work this build runs
288
+ if (!isLeaseEventV2(current.event))
289
+ return null;
290
+ // A drain, revocation or policy change can land during the queue read
291
+ // or while waiting for this lease's fence. Never grant from cached permission.
292
+ const [claimRack, security] = await Promise.all([
293
+ settingsStore.getRack(rackId), settingsStore.getRackSecurityPolicy(),
294
+ ]);
295
+ if (claimRack === null)
296
+ return unauthorized('Rack is deregistered');
297
+ if (signal.aborted || !claimRack.healthy || compareInteger(claimRack.capacity, 0n) <= 0
298
+ || claimRack.lastSeenAt.getTime() < now() - DEFAULT_HEALTHY_WINDOW_MS
299
+ || leaseRefusal(claimRack.agentVersion) !== null
300
+ || (security?.requireStartApproval === true
301
+ && !(claimRack.approvedBootId.type === 'some' && equalString(claimRack.approvedBootId.value, bootId))))
302
+ return null;
303
+ if (options?.canClaim !== undefined && !(await options.canClaim(claimRack, current)))
304
+ return null;
305
+ // A task or unit lease's claim runs an attempt, which the cloud
306
+ // records: the rack cannot (#214)
307
+ const attempt = isFunctionLease(candidate.event) ? undefined : { executionId: uuidv7(), startedAtMs: now() };
308
+ const claimKind = current.status === 'pending' ? 'initial' : 'reclaimed';
309
+ const claimed = await leaseStore.claim(candidate.repo, candidate.leaseId, rackId, now() + visibilityMs, attempt);
310
+ if (claimed !== null) {
311
+ if (attempt !== undefined) {
312
+ // A rack that went quiet past the lease's visibility runs its
313
+ // attempt no more: this claim's takes over
314
+ if (current.attempt !== undefined) {
315
+ await attempts.stopped(current, 'interrupted', `its rack ${current.claimedBy ?? ''} went quiet past its lease's visibility, and rack ${rackId} took the lease up`)
316
+ .catch((err) => console.warn(`rack lease ${candidate.leaseId}: its earlier attempt was not recorded interrupted: ${messageOf(err)}`));
317
+ }
318
+ await attempts.running(claimed, bootId);
319
+ }
320
+ await options?.onLeaseSignal?.({ kind: 'claimed', repo: claimed.repo, leaseId: claimed.leaseId, rackId });
321
+ const response = sendSuccess(RackLeaseGrantType, {
322
+ repo: claimed.repo,
323
+ workspace: claimed.workspace,
324
+ leaseId: claimed.leaseId,
325
+ tier: claimed.tier,
326
+ computeSize: claimed.computeSize ?? 'serverless',
327
+ eventJson: JSON.stringify(claimed.event),
328
+ visibilityDeadline: new Date(claimed.visibilityDeadlineMs),
329
+ });
330
+ // Source wall-clock queue time only; never mix clocks across the network.
331
+ response.headers.set('x-e3-rack-claim-kind', claimKind);
332
+ const queueMs = (claimed.claimedAtMs ?? NaN) - claimed.createdAtMs;
333
+ if (claimKind === 'initial' && Number.isSafeInteger(queueMs) && queueMs >= 0) {
334
+ response.headers.set('x-e3-rack-queue-ms', String(queueMs));
335
+ }
336
+ return response;
337
+ }
338
+ return null;
339
+ });
340
+ if (grant !== null)
341
+ return grant;
342
+ }
343
+ }
344
+ if (Date.now() + waitMs >= deadline) {
345
+ return c.body(null, 204);
346
+ }
347
+ await delay(waitMs, undefined, { signal }).catch(() => { });
348
+ waitMs = Math.min(pollMaxMs, Math.round(waitMs * 1.5));
349
+ }
350
+ });
351
+ // POST /api/rack/heartbeat — liveness + which running leases to abandon,
352
+ // plus the D12 lifecycle: boot-identity reporting (first heartbeat after
353
+ // enrollment ADOPTS its bootId as approved — the mint authorized that
354
+ // start), start-approval status, and token-rotation offers.
355
+ app.post('/api/rack/heartbeat', async (c) => {
356
+ const rackId = c.get('rackId');
357
+ let heartbeat;
358
+ let capacity;
359
+ try {
360
+ heartbeat = (await decodeBody(c, RackHeartbeatRequestType));
361
+ capacity = heartbeatCapacity(heartbeat);
362
+ }
363
+ catch {
364
+ return sendError(RackHeartbeatResponseType, variant('internal', { message: 'Invalid heartbeat body' }));
365
+ }
366
+ const registration = await touchRack(rackId, (current) => ({
367
+ healthy: heartbeat.healthy,
368
+ capacity,
369
+ lastSeenAt: new Date(now()),
370
+ agentVersion: heartbeat.agentVersion,
371
+ lastBootId: some(heartbeat.bootId),
372
+ approvedBootId: current.approvedBootId.type === 'none' && current.lastBootId.type === 'none'
373
+ ? some(heartbeat.bootId) : current.approvedBootId,
374
+ }));
375
+ if (registration === null)
376
+ return unauthorized('Rack is deregistered');
377
+ const { approvedBootId } = registration;
378
+ // A lease is dead to this agent when it is terminal-for-someone-else:
379
+ // cancelled, superseded, gone, or reclaimed by another rack after expiry.
380
+ const running = await mapBounded(heartbeat.runningLeases, STORAGE_READS_AT_ONCE, ref => leaseStore.get(ref.repo, ref.leaseId));
381
+ const stop = heartbeat.runningLeases.filter((_ref, i) => {
382
+ const lease = running[i];
383
+ return lease === null || lease.status === 'cancelled' || lease.status === 'superseded'
384
+ || (lease.status === 'claimed' && lease.claimedBy !== rackId);
385
+ });
386
+ const security = await settingsStore.getRackSecurityPolicy();
387
+ const pendingApproval = security?.requireStartApproval === true &&
388
+ (approvedBootId.type !== 'some' || !equalString(approvedBootId.value, heartbeat.bootId));
389
+ // Rotation offer (D12): only the rack's CURRENT token ages into an
390
+ // offer — a pending successor was just activated by the middleware
391
+ // (fresh clock), and a grace token proves the agent already holds a
392
+ // newer one. Offers repeat with a FRESH successor at most every
393
+ // ROTATION_REOFFER until first use activates one.
394
+ let rotatedToken = none;
395
+ if (c.get('tokenStatus') === 'current' &&
396
+ now() - c.get('tokenIssuedAt').getTime() > ROTATION_AGE_MS) {
397
+ const tokens = await identityStore.listRackTokens(rackId);
398
+ const pending = tokens.find((t) => t.status === 'pending');
399
+ const staleOffer = pending !== undefined && now() - pending.issuedAt.getTime() > ROTATION_REOFFER_MS;
400
+ if (pending === undefined || staleOffer) {
401
+ if (pending !== undefined)
402
+ await identityStore.deleteRackToken(pending.tokenHash);
403
+ const successor = generateRackToken();
404
+ await identityStore.putRackToken(successor.tokenHash, rackId, new Date(now()), 'pending');
405
+ rotatedToken = some(successor.token);
406
+ }
407
+ }
408
+ return sendSuccess(RackHeartbeatResponseType, { stop, rotatedToken, pendingApproval });
409
+ });
410
+ // Separate cancellation from health/rotation writes. This request stays
411
+ // open while work is held, and returns promptly after a terminal transition.
412
+ // It never claims work, extends visibility, or exposes another lease's data.
413
+ app.post('/api/rack/lease/watch', async (c) => {
414
+ const rackId = c.get('rackId');
415
+ let refs;
416
+ try {
417
+ refs = (await decodeBody(c, RackWatchRequestType)).runningLeases;
418
+ if (refs.length === 0 || refs.length > RACK_WATCH_MAX_LEASES
419
+ || refs.some(ref => [ref.repo, ref.leaseId].some(value => !value || Buffer.byteLength(value) > 1024 || /[\x00-\x1f\x7f]/.test(value)))
420
+ || new Set(refs.map(ref => JSON.stringify([ref.repo, ref.leaseId]))).size !== refs.length)
421
+ throw new Error();
422
+ }
423
+ catch {
424
+ return c.text('Invalid cancellation watch', 400);
425
+ }
426
+ const signal = options?.signal ? AbortSignal.any([c.req.raw.signal, options.signal]) : c.req.raw.signal;
427
+ const deadline = Date.now() + watchWindowMs;
428
+ for (;;) {
429
+ if (signal.aborted)
430
+ return c.body(null, 204);
431
+ if (await settingsStore.getRack(rackId) === null)
432
+ return unauthorized('Rack is deregistered');
433
+ const states = await mapBounded(refs, STORAGE_READS_AT_ONCE, ref => leaseStore.get(ref.repo, ref.leaseId));
434
+ const stop = refs.filter((_ref, i) => {
435
+ const lease = states[i];
436
+ return lease == null || lease.claimedBy !== rackId || lease.status !== 'claimed';
437
+ });
438
+ if (stop.length)
439
+ return sendSuccess(RackWatchResponseType, { stop });
440
+ const remaining = deadline - Date.now();
441
+ if (remaining <= 0)
442
+ return c.body(null, 204);
443
+ await delay(Math.min(watchPollIntervalMs, remaining), undefined, { signal }).catch(() => { });
444
+ }
445
+ });
446
+ // POST /api/rack/lease/extend — visibility refresh (the running lease's
447
+ // own heartbeat). False when the lease is no longer this rack's.
448
+ app.post('/api/rack/lease/extend', async (c) => {
449
+ const rackId = c.get('rackId');
450
+ try {
451
+ const request = (await decodeBody(c, RackExtendRequestType));
452
+ const extended = await coordinator.run(request.repo, request.leaseId, () => leaseStore.extend(request.repo, request.leaseId, rackId, now() + visibilityMs));
453
+ if (extended)
454
+ await options?.onLeaseSignal?.({ kind: 'extended', repo: request.repo, leaseId: request.leaseId });
455
+ return sendSuccess(BooleanType, extended);
456
+ }
457
+ catch {
458
+ return sendError(BooleanType, variant('internal', { message: 'Invalid extend body' }));
459
+ }
460
+ });
461
+ // POST /api/rack/complete — terminal result. Accepted-and-discarded: a
462
+ // late completion after supersede/cancel/reclaim is a 200 telling the
463
+ // agent to drop local state, never a retryable error. A success's output
464
+ // is in the store FIRST, every object it names (#214), so a completion only
465
+ // ever records an output hash that resolves. The attempt's outcome is
466
+ // recorded before the lease ends, so no reader finds the lease ended and
467
+ // the attempt still running; then what the lease wrote and never committed
468
+ // is discarded, and a run parked on it is woken (#208).
469
+ app.post('/api/rack/complete', async (c) => {
470
+ const rackId = c.get('rackId');
471
+ let request;
472
+ let result;
473
+ try {
474
+ request = (await decodeBody(c, RackCompleteRequestType));
475
+ result = JSON.parse(request.resultJson);
476
+ if (typeof result.taskName !== 'string' || (result.status !== 'success' && result.status !== 'failed')) {
477
+ throw new Error('malformed result');
478
+ }
479
+ if (result.state !== undefined && !['success', 'failed', 'error'].includes(result.state))
480
+ throw new Error('invalid state');
481
+ if (result.state !== undefined && (result.state === 'success') !== (result.status === 'success'))
482
+ throw new Error('inconsistent outcome');
483
+ if (result.exitCode !== undefined && !Number.isSafeInteger(result.exitCode))
484
+ throw new Error('invalid exit code');
485
+ if (result.peakBytes !== undefined && (!Number.isSafeInteger(result.peakBytes) || result.peakBytes < 0))
486
+ throw new Error('invalid peak');
487
+ if (result.duration !== undefined && (!Number.isFinite(result.duration) || result.duration < 0))
488
+ throw new Error('invalid duration');
489
+ // Inline bytes determine their own hash below; an agent's claimed hash
490
+ // has no authority, even when it is malformed.
491
+ const inlineSuccess = result.status === 'success' && request.inlineOutput.type === 'some';
492
+ if (!inlineSuccess && result.outputHash !== undefined && (typeof result.outputHash !== 'string' || !isObjectHash(result.outputHash)))
493
+ throw new Error('invalid output hash');
494
+ if (result.cancelled !== undefined && typeof result.cancelled !== 'boolean')
495
+ throw new Error('invalid cancellation');
496
+ if (result.error !== undefined && typeof result.error !== 'string')
497
+ throw new Error('invalid error');
498
+ }
499
+ catch {
500
+ return sendError(RackCompleteResponseType, variant('internal', { message: 'Invalid completion body' }));
501
+ }
502
+ return coordinator.run(request.repo, request.leaseId, async () => {
503
+ // Land the output before any lease transition. Only the lease holder
504
+ // may commit; a fenced-out agent's completion falls through to the
505
+ // state machine below, which discards it without touching storage.
506
+ const held = await heldLease(request.repo, request.leaseId, rackId);
507
+ // A task or unit lease's attempt, which the cloud records (#214)
508
+ const attempt = held !== null && !isFunctionLease(held.event) ? held.attempt : undefined;
509
+ if (held !== null && result.status === 'success') {
510
+ if (request.inlineOutput.type === 'some') {
511
+ // Small output: server computes the hash — the agent's is ignored.
512
+ result.outputHash = await bridge.writeInline(request.repo, request.inlineOutput.value);
513
+ }
514
+ else if (request.outputUploadId.type === 'some') {
515
+ if (result.outputHash === undefined) {
516
+ return sendError(RackCompleteResponseType, variant('internal', { message: 'outputUploadId without outputHash' }));
517
+ }
518
+ // A whole output, staged: taken in once the store's copy of it hashes
519
+ // to its hash (#210)
520
+ const verified = await bridge.commitStaged(request.repo, result.outputHash, request.outputUploadId.value);
521
+ if (!verified) {
522
+ // Tampered/corrupt upload: reject — the lease stays claimed, so
523
+ // the agent may retry; otherwise visibility expiry → cloud.
524
+ return sendError(RackCompleteResponseType, variant('internal', { message: 'Output verification failed' }));
525
+ }
526
+ }
527
+ if (attempt !== undefined) {
528
+ const outputHash = result.outputHash;
529
+ if (outputHash === undefined || !isObjectHash(outputHash)) {
530
+ return sendError(RackCompleteResponseType, variant('internal', { message: 'A success names its output\'s hash' }));
531
+ }
532
+ // Every object the output names is in the store, committed by the
533
+ // lease or held already, and re-referenced until the loop roots it
534
+ let whole;
535
+ try {
536
+ whole = await attempts.verifyOutput(held, outputHash);
537
+ }
538
+ catch (err) {
539
+ return sendError(RackCompleteResponseType, variant('internal', { message: `The output could not be checked: ${messageOf(err)}` }));
540
+ }
541
+ if (!whole) {
542
+ return sendError(RackCompleteResponseType, variant('internal', {
543
+ message: `Output ${outputHash} is not whole in the store: commit every object it names before completing`,
544
+ }));
545
+ }
546
+ }
547
+ }
548
+ if (held !== null && attempt !== undefined) {
549
+ try {
550
+ await logs.flush?.(held.repo, held.taskHash, held.inputsHash, attempt.executionId);
551
+ await attempts.outcome(held, result);
552
+ }
553
+ catch (err) {
554
+ // The lease stays claimed: the agent completes it again
555
+ return sendError(RackCompleteResponseType, variant('internal', { message: `The attempt's outcome was not recorded: ${messageOf(err)}` }));
556
+ }
557
+ }
558
+ const disposition = await leaseStore.complete(request.repo, request.leaseId, rackId, result);
559
+ if (held !== null) {
560
+ // What the lease wrote and nothing committed leaves no version at a
561
+ // content address (#214)
562
+ if ((held.targets?.length ?? 0) > 0) {
563
+ await bridge.discardWrites(request.repo, request.leaseId, held.targets).catch((err) => {
564
+ console.warn(`rack lease ${request.leaseId}: the versions it wrote and never committed were not discarded: ${messageOf(err)}`);
565
+ });
566
+ }
567
+ // A run parked on the attempt runs its next cycle at once (#208): the
568
+ // waker finds its outcome recorded
569
+ const run = attempt !== undefined ? leaseRun(held.event) : undefined;
570
+ if (run !== undefined && options?.wake !== undefined) {
571
+ await options.wake(request.repo, run).catch((err) => {
572
+ console.warn(`rack lease ${request.leaseId}: could not wake run ${run.id}, which its park's backstop wakes: ${messageOf(err)}`);
573
+ });
574
+ }
575
+ }
576
+ await options?.onLeaseSignal?.({ kind: 'completed', repo: request.repo, leaseId: request.leaseId, disposition });
577
+ return sendSuccess(RackCompleteResponseType, { disposition: variant(disposition, null) });
578
+ });
579
+ });
580
+ // POST /api/rack/storage — data-plane descriptors for a HELD lease. The
581
+ // request's `need` set must be within the lease's closure (F5, #214): the
582
+ // objects its guest reads, as the cloud computed them when it granted the
583
+ // lease. Anything else is a hard 403 — a compromised agent must not walk
584
+ // the repo's object store. The objects its guest wrote go in by PUTs at
585
+ // their content addresses, bound to their SHA-256 and the lease, which
586
+ // their commits take in.
587
+ app.post('/api/rack/storage', async (c) => {
588
+ const rackId = c.get('rackId');
589
+ let request;
590
+ try {
591
+ request = (await decodeBody(c, RackStorageRequestType));
592
+ }
593
+ catch {
594
+ return sendError(RackStorageResponseType, variant('internal', { message: 'Invalid storage request body' }));
595
+ }
596
+ return coordinator.run(request.repo, request.leaseId, async () => {
597
+ const lease = await heldLease(request.repo, request.leaseId, rackId);
598
+ if (lease === null) {
599
+ return forbidden('No such lease held by this rack');
600
+ }
601
+ if (!isLeaseEventV2(lease.event)) {
602
+ return forbidden(`Lease ${lease.leaseId} is no work this deployment runs`);
603
+ }
604
+ if (request.need.length > 0) {
605
+ let allowed;
606
+ try {
607
+ allowed = await admitted(lease);
608
+ }
609
+ catch (err) {
610
+ return sendError(RackStorageResponseType, variant('internal', { message: `The lease's closure could not be read: ${messageOf(err)}` }));
611
+ }
612
+ for (const hash of request.need) {
613
+ if (!allowed.has(hash)) {
614
+ return forbidden(`Hash ${hash} is not part of lease ${lease.leaseId}`);
615
+ }
616
+ }
617
+ }
618
+ const objects = await mapBounded(request.need, STORAGE_READS_AT_ONCE, async (hash) => {
619
+ const descriptor = await bridge.readDescriptor(request.repo, hash);
620
+ return descriptor.kind === 'inline'
621
+ ? variant('inline', { hash, data: descriptor.data })
622
+ : variant('url', { hash, url: descriptor.url });
623
+ });
624
+ // A whole output, staged (#210)
625
+ let output = none;
626
+ if (request.output.type === 'some') {
627
+ output = some(await bridge.stagedTarget(request.repo, lease.workspace, request.output.value.hash, request.output.value.size));
628
+ }
629
+ // #94 Slice B: published env layers for the lease's OWN environment.
630
+ // Scope (F5): the requested hash must equal the lease's declared env —
631
+ // the index/manifest are deployment-global, so this check is the only
632
+ // thing between a rack credential and every published environment.
633
+ let env = none;
634
+ if (request.envManifest.type === 'some') {
635
+ const envHash = request.envManifest.value;
636
+ if (lease.event.environment !== envHash) {
637
+ return forbidden(`Environment ${envHash} is not part of lease ${lease.leaseId}`);
638
+ }
639
+ const record = envIndex !== undefined && e3Version !== undefined
640
+ ? await envIndex.get(envHash, e3Version)
641
+ : null;
642
+ const manifestJson = record === null ? null : await bridge.readEnvManifest(record.manifestKey);
643
+ if (manifestJson === null) {
644
+ return sendError(RackStorageResponseType, variant('internal', {
645
+ message: `Environment ${envHash} has no published layers (build it, or the deployment predates publication)`,
646
+ }));
647
+ }
648
+ const manifest = JSON.parse(manifestJson);
649
+ const layers = await mapBounded(manifest.layers, STORAGE_READS_AT_ONCE, async (layer) => ({
650
+ digest: layer.digest,
651
+ url: await bridge.presignEnvLayer(envLayerBlobKey(layer.digest)),
652
+ sizeBytes: BigInt(layer.sizeBytes),
653
+ }));
654
+ env = some({ manifestJson, layers });
655
+ }
656
+ // #214: a PUT target for each object the guest wrote, at its content
657
+ // address; the keys issued are recorded on the lease before the answer,
658
+ // so its end finds every version it may have made
659
+ const issued = new Set(lease.targets ?? []);
660
+ if (request.outputs.length > 0 || request.commits.length > 0) {
661
+ if (isFunctionLease(lease.event)) {
662
+ return forbidden(`Lease ${lease.leaseId} is a function call's, which writes nothing to the store`);
663
+ }
664
+ if (request.outputs.length > MAX_STORAGE_OBJECTS || request.commits.length > MAX_STORAGE_OBJECTS) {
665
+ return sendError(RackStorageResponseType, variant('internal', { message: `A request declares or commits at most ${MAX_STORAGE_OBJECTS} objects` }));
666
+ }
667
+ }
668
+ // Validate and reserve the complete batch before issuing any capability.
669
+ // A rejected batch must leave no untracked upload for a later cleanup.
670
+ for (const { hash, size } of [...request.outputs, ...request.commits]) {
671
+ if (!isObjectHash(hash) || size < 0n || size > BigInt(MAX_RACK_OBJECT_BYTES)) {
672
+ return sendError(RackStorageResponseType, variant('internal', {
673
+ message: `Output ${hash} is not an object this lease can write: a SHA-256, of at most ${MAX_RACK_OBJECT_BYTES} bytes`,
674
+ }));
675
+ }
676
+ }
677
+ const fresh = [...new Set(request.outputs.map(({ hash }) => hash))].filter((hash) => !issued.has(hash));
678
+ if (issued.size + fresh.length > MAX_LEASE_TARGETS) {
679
+ return sendError(RackStorageResponseType, variant('internal', { message: `The lease writes more than ${MAX_LEASE_TARGETS} objects` }));
680
+ }
681
+ if (fresh.length > 0 && !await leaseStore.recordTargets(request.repo, lease.leaseId, rackId, fresh)) {
682
+ return forbidden('No such lease held by this rack');
683
+ }
684
+ for (const hash of fresh)
685
+ issued.add(hash);
686
+ const targets = await mapBounded(request.outputs, STORAGE_READS_AT_ONCE, async ({ hash, size }) => {
687
+ const target = await bridge.writeTarget(request.repo, lease.leaseId, hash, Number(size));
688
+ return {
689
+ hash,
690
+ target: target === null
691
+ ? variant('held', null)
692
+ : variant('put', { url: target.url, headers: new Map(Object.entries(target.headers)) }),
693
+ };
694
+ });
695
+ // #214: an object PUT through a target is taken in once the version it
696
+ // made is checked: never a body read
697
+ const committed = [];
698
+ for (const { hash, size, version } of request.commits) {
699
+ const ok = issued.has(hash) && await bridge.commitWrite(request.repo, lease.leaseId, rackId, hash, Number(size), version);
700
+ committed.push({ hash, committed: ok });
701
+ }
702
+ return sendSuccess(RackStorageResponseType, { objects, output, env, targets, committed });
703
+ });
704
+ });
705
+ // POST /api/rack/logs — batched stdout/stderr for a HELD lease → LogStore
706
+ // under its attempt's execution id (visible in the task UI live, #214).
707
+ app.post('/api/rack/logs', async (c) => {
708
+ const rackId = c.get('rackId');
709
+ let request;
710
+ try {
711
+ request = (await decodeBody(c, RackLogsRequestType));
712
+ }
713
+ catch {
714
+ return sendError(BooleanType, variant('internal', { message: 'Invalid logs body' }));
715
+ }
716
+ return coordinator.run(request.repo, request.leaseId, async () => {
717
+ const lease = await heldLease(request.repo, request.leaseId, rackId);
718
+ if (lease === null) {
719
+ return forbidden('No such lease held by this rack');
720
+ }
721
+ const executionId = lease.attempt?.executionId;
722
+ if (executionId === undefined) {
723
+ // Refused for good, so the rack posts it no more
724
+ return forbidden(`Lease ${lease.leaseId} runs no attempt whose log is kept: a function call's output rides its completion`);
725
+ }
726
+ try {
727
+ try {
728
+ for (const chunk of request.chunks) {
729
+ await logs.append(request.repo, lease.taskHash, lease.inputsHash, executionId, chunk.stream.type, chunk.data);
730
+ }
731
+ }
732
+ finally {
733
+ // A sink that buffers stores the batch before the rack is answered: a
734
+ // serverless host is frozen once it answers, and the rack's next post
735
+ // may reach another host
736
+ await logs.flush?.(request.repo, lease.taskHash, lease.inputsHash, executionId);
737
+ }
738
+ }
739
+ catch (err) {
740
+ // Told its lines were not stored, a rack posts them again while it
741
+ // holds the lease. Its batches carry one stream each, so a re-post
742
+ // repeats no line of another stream.
743
+ return sendError(BooleanType, variant('internal', {
744
+ message: `Logs not stored: ${err instanceof Error ? err.message : String(err)}`,
745
+ }));
746
+ }
747
+ return sendSuccess(BooleanType, true);
748
+ });
749
+ });
750
+ return app;
751
+ }
752
+ function messageOf(err) {
753
+ return err instanceof Error ? err.message : String(err);
754
+ }
755
+ //# sourceMappingURL=rack-routes.js.map