@nylorun/runtime 0.15.2-beta → 0.17.0-beta

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 (291) hide show
  1. package/CHANGELOG.md +134 -0
  2. package/README.md +68 -29
  3. package/dist/adapters/execution/restate.d.ts +9 -1
  4. package/dist/adapters/execution/restate.js +64 -5
  5. package/dist/adapters/sandbox/local.d.ts +10 -0
  6. package/dist/adapters/sandbox/local.js +195 -0
  7. package/dist/adapters/sandbox/virtual.js +36 -1
  8. package/dist/admin-openapi.json +341 -7
  9. package/dist/api/a2a/tasks.js +6 -1
  10. package/dist/api/ag-ui/endpoint.js +1 -2
  11. package/dist/api/ag-ui/history.js +6 -1
  12. package/dist/api/ag-ui/routes.d.ts +1 -1
  13. package/dist/api/ag-ui/routes.js +15 -13
  14. package/dist/api/components.d.ts +2667 -711
  15. package/dist/api/components.js +24 -11
  16. package/dist/api/gate/keys.d.ts +4 -3
  17. package/dist/api/gate/routes.d.ts +19 -3
  18. package/dist/api/gate/routes.js +162 -53
  19. package/dist/api/http/app.js +6 -0
  20. package/dist/api/http/define.d.ts +19 -12
  21. package/dist/api/http/define.js +13 -27
  22. package/dist/api/http/routes/access.d.ts +3 -19
  23. package/dist/api/http/routes/access.js +12 -172
  24. package/dist/api/http/routes/actions.js +0 -1
  25. package/dist/api/http/routes/artifacts.d.ts +11 -0
  26. package/dist/api/http/routes/artifacts.js +516 -0
  27. package/dist/api/http/routes/me.d.ts +13 -0
  28. package/dist/api/http/routes/me.js +45 -0
  29. package/dist/api/http/routes/sandboxes.d.ts +9 -0
  30. package/dist/api/http/routes/sandboxes.js +150 -0
  31. package/dist/api/http/routes/sessions.js +9 -13
  32. package/dist/api/http/routes/tenant.js +36 -2
  33. package/dist/api/http/routes/vaults.d.ts +12 -3
  34. package/dist/api/http/routes/vaults.js +122 -54
  35. package/dist/api/openapi.js +4 -10
  36. package/dist/artifacts/config.d.ts +23 -0
  37. package/dist/artifacts/config.js +27 -0
  38. package/dist/artifacts/export.d.ts +8 -0
  39. package/dist/artifacts/export.js +205 -0
  40. package/dist/artifacts/files.d.ts +36 -0
  41. package/dist/artifacts/files.js +45 -0
  42. package/dist/artifacts/folders.d.ts +60 -0
  43. package/dist/artifacts/folders.js +209 -0
  44. package/dist/artifacts/links.d.ts +28 -0
  45. package/dist/artifacts/links.js +92 -0
  46. package/dist/artifacts/media-types.d.ts +13 -0
  47. package/dist/artifacts/media-types.js +80 -0
  48. package/dist/artifacts/parts.d.ts +34 -0
  49. package/dist/artifacts/parts.js +38 -0
  50. package/dist/artifacts/service.d.ts +74 -0
  51. package/dist/artifacts/service.js +301 -0
  52. package/dist/artifacts/workspace.d.ts +60 -0
  53. package/dist/artifacts/workspace.js +57 -0
  54. package/dist/artifacts/zip.d.ts +10 -0
  55. package/dist/artifacts/zip.js +126 -0
  56. package/dist/blob/fs.d.ts +13 -0
  57. package/dist/blob/fs.js +194 -0
  58. package/dist/blob/index.d.ts +9 -0
  59. package/dist/blob/index.js +9 -0
  60. package/dist/blob/s3.d.ts +42 -0
  61. package/dist/blob/s3.js +366 -0
  62. package/dist/blob/types.d.ts +108 -0
  63. package/dist/blob/types.js +85 -0
  64. package/dist/core/sandbox-routes.d.ts +2 -2
  65. package/dist/core/sandbox-routes.js +4 -3
  66. package/dist/execution/memory.d.ts +11 -1
  67. package/dist/execution/memory.js +77 -3
  68. package/dist/execution/types.d.ts +54 -1
  69. package/dist/execution/types.js +31 -0
  70. package/dist/gates/contract.d.ts +14 -13
  71. package/dist/gates/contract.js +13 -12
  72. package/dist/gates/egress.d.ts +72 -0
  73. package/dist/gates/egress.js +319 -0
  74. package/dist/gates/handler.d.ts +3 -0
  75. package/dist/gates/handler.js +3 -2
  76. package/dist/gates/http-client.d.ts +3 -2
  77. package/dist/gates/http-client.js +17 -6
  78. package/dist/gates/in-process.d.ts +13 -1
  79. package/dist/gates/in-process.js +11 -0
  80. package/dist/gates/inflight.d.ts +23 -4
  81. package/dist/gates/inflight.js +18 -4
  82. package/dist/gates/mcp-handler.js +5 -1
  83. package/dist/gates/model-gate.d.ts +6 -2
  84. package/dist/gates/tenant-vaults.d.ts +18 -9
  85. package/dist/gates/tenant-vaults.js +17 -12
  86. package/dist/gates/tool-calls.d.ts +9 -5
  87. package/dist/gates/tool-calls.js +6 -6
  88. package/dist/gates/tool-client.d.ts +9 -2
  89. package/dist/gates/tool-client.js +56 -22
  90. package/dist/gates/tool-contract.d.ts +12 -7
  91. package/dist/gates/tool-contract.js +10 -4
  92. package/dist/gates/tool-gate.d.ts +1 -0
  93. package/dist/harness/calls.d.ts +52 -0
  94. package/dist/harness/calls.js +113 -0
  95. package/dist/harness/executors.d.ts +22 -0
  96. package/dist/harness/executors.js +96 -0
  97. package/dist/harness/main.d.ts +6 -0
  98. package/dist/harness/main.js +103 -0
  99. package/dist/harness/pod.d.ts +21 -0
  100. package/dist/harness/pod.js +119 -0
  101. package/dist/harness/run-tokens.d.ts +18 -0
  102. package/dist/harness/run-tokens.js +15 -0
  103. package/dist/harness/service.d.ts +46 -0
  104. package/dist/harness/service.js +179 -0
  105. package/dist/harness/ws-client.d.ts +43 -0
  106. package/dist/harness/ws-client.js +122 -0
  107. package/dist/harness/ws-port.d.ts +22 -0
  108. package/dist/harness/ws-port.js +78 -0
  109. package/dist/harness-api/in-process.d.ts +24 -0
  110. package/dist/harness-api/in-process.js +119 -0
  111. package/dist/harness-api/record.d.ts +44 -0
  112. package/dist/harness-api/record.js +250 -0
  113. package/dist/harness-api/renew.d.ts +37 -0
  114. package/dist/harness-api/renew.js +77 -0
  115. package/dist/harness-api/server.d.ts +104 -0
  116. package/dist/harness-api/server.js +408 -0
  117. package/dist/harness-api/start.d.ts +36 -0
  118. package/dist/harness-api/start.js +118 -0
  119. package/dist/harness-api/workspace.d.ts +53 -0
  120. package/dist/harness-api/workspace.js +201 -0
  121. package/dist/harness-api/ws-server.d.ts +34 -0
  122. package/dist/harness-api/ws-server.js +227 -0
  123. package/dist/host/admin-api.d.ts +12 -5
  124. package/dist/host/admin-api.js +72 -6
  125. package/dist/host/app.d.ts +0 -1
  126. package/dist/host/app.js +35 -24
  127. package/dist/host/config-for.d.ts +4 -0
  128. package/dist/host/config-for.js +2 -0
  129. package/dist/host/config.d.ts +0 -6
  130. package/dist/host/create-host.d.ts +3 -10
  131. package/dist/host/create-host.js +4 -1
  132. package/dist/host/gates.d.ts +27 -2
  133. package/dist/host/gates.js +52 -2
  134. package/dist/host/http.d.ts +4 -1
  135. package/dist/host/http.js +6 -1
  136. package/dist/host/main.js +167 -37
  137. package/dist/host/stack-config.d.ts +112 -9
  138. package/dist/host/stack-config.js +222 -20
  139. package/dist/index.d.ts +3 -0
  140. package/dist/index.js +2 -0
  141. package/dist/keys/client.js +9 -1
  142. package/dist/keys/keys.d.ts +12 -2
  143. package/dist/keys/keys.js +8 -0
  144. package/dist/mcp/connect.js +2 -1
  145. package/dist/mcp/pool.d.ts +1 -0
  146. package/dist/mcp/pool.js +1 -0
  147. package/dist/mcp/snapshot.d.ts +8 -1
  148. package/dist/mcp/snapshot.js +28 -0
  149. package/dist/media.d.ts +5 -24
  150. package/dist/media.js +5 -0
  151. package/dist/model/pi-model.d.ts +6 -2
  152. package/dist/model/pi-model.js +39 -22
  153. package/dist/node/index.d.ts +1 -1
  154. package/dist/node/index.js +1 -1
  155. package/dist/openapi.json +14084 -9426
  156. package/dist/record/index.d.ts +1 -0
  157. package/dist/record/index.js +1 -0
  158. package/dist/record/sandbox.d.ts +25 -0
  159. package/dist/record/sandbox.js +43 -0
  160. package/dist/sandbox/capability.d.ts +3 -0
  161. package/dist/sandbox/capability.js +10 -0
  162. package/dist/sandbox/egress-token.d.ts +26 -0
  163. package/dist/sandbox/egress-token.js +8 -0
  164. package/dist/sandbox/join.d.ts +37 -0
  165. package/dist/sandbox/join.js +165 -0
  166. package/dist/sandbox/manager.d.ts +99 -34
  167. package/dist/sandbox/manager.js +216 -84
  168. package/dist/sandbox/placement.d.ts +27 -0
  169. package/dist/sandbox/placement.js +44 -0
  170. package/dist/sandbox/pods/client.d.ts +90 -0
  171. package/dist/sandbox/pods/client.js +79 -0
  172. package/dist/sandbox/pods/lifecycle.d.ts +91 -0
  173. package/dist/sandbox/pods/lifecycle.js +178 -0
  174. package/dist/sandbox/pods/name.d.ts +1 -0
  175. package/dist/sandbox/pods/name.js +28 -0
  176. package/dist/sandbox/pods/network-gate.d.ts +25 -0
  177. package/dist/sandbox/pods/network-gate.js +71 -0
  178. package/dist/sandbox/pods/reconcile.d.ts +11 -0
  179. package/dist/sandbox/pods/reconcile.js +152 -0
  180. package/dist/sandbox/pods/spec.d.ts +45 -0
  181. package/dist/sandbox/pods/spec.js +62 -0
  182. package/dist/sandbox/records.d.ts +29 -0
  183. package/dist/sandbox/records.js +79 -0
  184. package/dist/sandbox/resolve.d.ts +5 -0
  185. package/dist/sandbox/resolve.js +3 -2
  186. package/dist/sandbox/session-sandbox.js +7 -5
  187. package/dist/sandbox/share.d.ts +11 -0
  188. package/dist/sandbox/share.js +10 -0
  189. package/dist/sandbox/tenant-config.d.ts +16 -1
  190. package/dist/sandbox/tenant-config.js +9 -0
  191. package/dist/sandbox/types.d.ts +29 -1
  192. package/dist/sandbox/types.js +13 -1
  193. package/dist/store/postgres/drizzle/0004_sandbox_resources.sql +20 -0
  194. package/dist/store/postgres/drizzle/0005_artifacts.sql +27 -0
  195. package/dist/store/postgres/drizzle/0006_folder_artifacts.sql +12 -0
  196. package/dist/store/postgres/drizzle/0007_sandbox_pods.sql +14 -0
  197. package/dist/store/postgres/drizzle/0008_installation_vaults.sql +2 -0
  198. package/dist/store/postgres/drizzle/0009_oauth_pending.sql +19 -0
  199. package/dist/store/postgres/drizzle/0010_oss_auth_removals.sql +3 -0
  200. package/dist/store/postgres/drizzle/meta/_journal.json +49 -0
  201. package/dist/store/postgres/record-writer.d.ts +6 -1
  202. package/dist/store/postgres/record-writer.js +20 -2
  203. package/dist/store/postgres/schema.d.ts +1168 -250
  204. package/dist/store/postgres/schema.js +174 -28
  205. package/dist/store/postgres/store.js +339 -103
  206. package/dist/store/postgres/tenant.d.ts +1 -1
  207. package/dist/store/postgres/tenant.js +2 -2
  208. package/dist/store/types.d.ts +163 -32
  209. package/dist/streams/control.d.ts +2 -5
  210. package/dist/streams/control.js +5 -8
  211. package/dist/streams/types.d.ts +8 -8
  212. package/dist/streams/types.js +1 -1
  213. package/dist/tenant/advance.js +218 -160
  214. package/dist/tenant/artifact-tool.d.ts +5 -0
  215. package/dist/tenant/artifact-tool.js +119 -0
  216. package/dist/tenant/auth.d.ts +13 -9
  217. package/dist/tenant/auth.js +25 -32
  218. package/dist/tenant/commands.js +31 -10
  219. package/dist/tenant/context.d.ts +83 -26
  220. package/dist/tenant/delivery-token.js +4 -4
  221. package/dist/tenant/effects.d.ts +24 -14
  222. package/dist/tenant/effects.js +10 -398
  223. package/dist/tenant/ephemeral.d.ts +14 -2
  224. package/dist/tenant/ephemeral.js +8 -1
  225. package/dist/tenant/history.d.ts +13 -23
  226. package/dist/tenant/history.js +14 -62
  227. package/dist/tenant/host-token.d.ts +92 -0
  228. package/dist/tenant/host-token.js +179 -0
  229. package/dist/tenant/identity-file.d.ts +75 -0
  230. package/dist/tenant/identity-file.js +202 -0
  231. package/dist/tenant/issuers.d.ts +48 -0
  232. package/dist/tenant/issuers.js +311 -0
  233. package/dist/tenant/jwt.js +3 -3
  234. package/dist/tenant/module.js +1 -0
  235. package/dist/tenant/operator-keys.d.ts +18 -0
  236. package/dist/tenant/operator-keys.js +55 -0
  237. package/dist/tenant/outbound.d.ts +30 -0
  238. package/dist/tenant/outbound.js +142 -10
  239. package/dist/tenant/paths.d.ts +2 -1
  240. package/dist/tenant/paths.js +3 -1
  241. package/dist/tenant/principals.d.ts +2 -11
  242. package/dist/tenant/principals.js +10 -30
  243. package/dist/tenant/reset.d.ts +5 -2
  244. package/dist/tenant/reset.js +22 -2
  245. package/dist/tenant/run-grants.d.ts +49 -0
  246. package/dist/tenant/run-grants.js +62 -0
  247. package/dist/tenant/run-token.d.ts +70 -0
  248. package/dist/tenant/run-token.js +138 -0
  249. package/dist/tenant/runtime.d.ts +42 -0
  250. package/dist/tenant/runtime.js +134 -33
  251. package/dist/tenant/sandboxes.d.ts +119 -0
  252. package/dist/tenant/sandboxes.js +551 -0
  253. package/dist/tenant/session-streams.d.ts +6 -10
  254. package/dist/tenant/session-streams.js +9 -27
  255. package/dist/tenant/session.d.ts +2 -13
  256. package/dist/tenant/session.js +4 -28
  257. package/dist/tenant/sessions.d.ts +6 -1
  258. package/dist/tenant/sessions.js +29 -2
  259. package/dist/tenant/signing-keys.d.ts +2 -1
  260. package/dist/tenant/signing-keys.js +2 -2
  261. package/dist/tenant/status.d.ts +8 -3
  262. package/dist/tenant/status.js +5 -0
  263. package/dist/tenant/streams.d.ts +8 -2
  264. package/dist/tenant/streams.js +25 -6
  265. package/dist/tenant/sweep.js +37 -4
  266. package/dist/tenant/types.d.ts +39 -1
  267. package/dist/tenant/worker.d.ts +11 -5
  268. package/dist/tenant/worker.js +12 -6
  269. package/dist/vault/crypto.d.ts +7 -0
  270. package/dist/vault/crypto.js +16 -0
  271. package/dist/vault/oauth.d.ts +55 -0
  272. package/dist/vault/oauth.js +186 -0
  273. package/dist/vault/service.d.ts +54 -7
  274. package/dist/vault/service.js +261 -17
  275. package/dist/vault/sources.d.ts +54 -0
  276. package/dist/vault/sources.js +185 -0
  277. package/dist/version.d.ts +1 -1
  278. package/dist/version.js +1 -1
  279. package/package.json +14 -10
  280. package/dist/adapters/media.d.ts +0 -24
  281. package/dist/adapters/media.js +0 -111
  282. package/dist/host/cors.d.ts +0 -27
  283. package/dist/host/cors.js +0 -64
  284. package/dist/tenant/access-policy.d.ts +0 -30
  285. package/dist/tenant/access-policy.js +0 -52
  286. package/dist/tenant/browser.d.ts +0 -15
  287. package/dist/tenant/browser.js +0 -39
  288. package/dist/tenant/subject-limits.d.ts +0 -16
  289. package/dist/tenant/subject-limits.js +0 -45
  290. package/dist/tenant/tokens.d.ts +0 -14
  291. package/dist/tenant/tokens.js +0 -180
@@ -1,4 +1,4 @@
1
- import { WAKE_REASONS, sessionKey, } from "./types.js";
1
+ import { DEFAULT_STOP_GRACE_MS, WAKE_REASONS, sessionKey, settleWithin, } from "./types.js";
2
2
  /**
3
3
  * In-process `DurableExecution`. Not durable: wakes, timers and sweeps live in
4
4
  * memory. It keeps the seam's ordering guarantees within one process, which is
@@ -11,18 +11,21 @@ export class MemoryExecution {
11
11
  seen = new Map();
12
12
  timers = new Map();
13
13
  sweeps = new Map();
14
+ sandboxes = new Map();
14
15
  delayed = new Set();
15
16
  inflight = new Set();
16
17
  sweepIntervalMs;
17
18
  retryDelayMs;
18
19
  maxAttempts;
19
20
  dedupeRetentionMs;
21
+ stopGraceMs;
20
22
  onError;
21
23
  constructor(options = {}) {
22
24
  this.sweepIntervalMs = options.sweepIntervalMs ?? 5000;
23
25
  this.retryDelayMs = options.retryDelayMs ?? 50;
24
26
  this.maxAttempts = options.maxAttempts ?? 5;
25
27
  this.dedupeRetentionMs = options.dedupeRetentionMs ?? 24 * 60 * 60_000;
28
+ this.stopGraceMs = options.stopGraceMs ?? DEFAULT_STOP_GRACE_MS;
26
29
  this.onError =
27
30
  options.onError ??
28
31
  ((error) => queueMicrotask(() => {
@@ -46,6 +49,66 @@ export class MemoryExecution {
46
49
  async deliver(tenantId, actionId) {
47
50
  this.enqueue("deliver", tenantId, actionId);
48
51
  }
52
+ async sandbox(tenantId, sandboxId, signal) {
53
+ const key = `${tenantId}\u0000${sandboxId}`;
54
+ let state = this.sandboxes.get(key);
55
+ if (!state)
56
+ this.sandboxes.set(key, (state = { tenantId, sandboxId, tail: Promise.resolve(), pending: [], attempts: 0, timers: new Map() }));
57
+ if (signal.kind === "arm")
58
+ this.armSandbox(state, signal.timer, signal.at);
59
+ else
60
+ this.runSandbox(state, "reconcile");
61
+ }
62
+ armSandbox(state, timer, at) {
63
+ const existing = state.timers.get(timer);
64
+ if (existing?.handle)
65
+ clearTimeout(existing.handle);
66
+ const entry = { at };
67
+ state.timers.set(timer, entry);
68
+ if (!this.handlers)
69
+ return;
70
+ entry.handle = setTimeout(() => {
71
+ if (state.timers.get(timer) !== entry)
72
+ return;
73
+ state.timers.delete(timer);
74
+ this.runSandbox(state, timer === "retry" ? "reconcile" : timer);
75
+ }, Math.max(0, at - Date.now()));
76
+ }
77
+ runSandbox(state, trigger) {
78
+ const handlers = this.handlers;
79
+ if (!handlers) {
80
+ state.pending.push(trigger);
81
+ return;
82
+ }
83
+ const run = state.tail.then(async () => {
84
+ if (!this.handlers)
85
+ return;
86
+ if (!handlers.sandbox) {
87
+ this.onError(new Error("WorkerHandlers.sandbox is required for sandbox reconciles"));
88
+ return;
89
+ }
90
+ const controller = new AbortController();
91
+ try {
92
+ const result = await handlers.sandbox(state.tenantId, state.sandboxId, trigger, controller.signal);
93
+ state.attempts = 0;
94
+ for (const item of result.arm ?? [])
95
+ this.armSandbox(state, item.timer, item.at);
96
+ if (result.retryAfterMs !== undefined)
97
+ this.armSandbox(state, "retry", Date.now() + Math.max(0, result.retryAfterMs));
98
+ }
99
+ catch (error) {
100
+ state.attempts += 1;
101
+ if (state.attempts >= this.maxAttempts) {
102
+ state.attempts = 0;
103
+ this.onError(error);
104
+ }
105
+ else
106
+ this.armSandbox(state, "retry", Date.now() + this.retryDelayMs * 2 ** (state.attempts - 1));
107
+ }
108
+ });
109
+ state.tail = run.catch(() => undefined);
110
+ this.track(state.tail);
111
+ }
49
112
  async timer(tenantId, key, at) {
50
113
  const id = `${tenantId}\u0000${key}`;
51
114
  const existing = this.timers.get(id);
@@ -81,6 +144,12 @@ export class MemoryExecution {
81
144
  this.armTimer(id, timer);
82
145
  for (const [tenantId, sweep] of this.sweeps)
83
146
  this.scheduleSweep(tenantId, sweep, 0);
147
+ for (const state of this.sandboxes.values()) {
148
+ for (const [timer, entry] of state.timers)
149
+ this.armSandbox(state, timer, entry.at);
150
+ for (const trigger of state.pending.splice(0))
151
+ this.runSandbox(state, trigger);
152
+ }
84
153
  }
85
154
  async stop() {
86
155
  this.handlers = undefined;
@@ -93,10 +162,15 @@ export class MemoryExecution {
93
162
  for (const sweep of this.sweeps.values())
94
163
  if (sweep.handle)
95
164
  clearTimeout(sweep.handle);
165
+ for (const state of this.sandboxes.values())
166
+ for (const entry of state.timers.values())
167
+ if (entry.handle)
168
+ clearTimeout(entry.handle);
96
169
  for (const state of this.keys.values())
97
170
  state.controller?.abort();
98
- while (this.inflight.size > 0)
99
- await Promise.allSettled(this.inflight);
171
+ // Bounded: a handler that ignores its abort is abandoned and runs on unwaited. An
172
+ // abandoned advance's lease lapses and the next advance takes the session over (§11.4).
173
+ await settleWithin(this.inflight, this.stopGraceMs);
100
174
  }
101
175
  /**
102
176
  * Resolves once no advance or delivery is running or queued and no busy or
@@ -37,6 +37,13 @@
37
37
  * - **Retry after `retry`.** A delivery that returns `retry` is run again for
38
38
  * the same key after `retryAfterMs`. This is how an Action endpoint that is
39
39
  * down or busy is retried; the handler throws only on infrastructure errors.
40
+ * - **One reconcile per sandbox at a time** (F7.2). `sandbox` runs
41
+ * `WorkerHandlers.sandbox` for the key `<tenantId>:<sandboxId>` at least once
42
+ * after a `reconcile` resolves, never overlapping another run for the key. A
43
+ * run that answers `retryAfterMs` runs again after it (one pending retry per
44
+ * key: a later one replaces it), and each timer it answers is armed. `arm`
45
+ * sets one of the sandbox's timers (`idle`, `ttl`): setting it again replaces
46
+ * the earlier time, and the run it causes is told which timer fired.
40
47
  */
41
48
  /** Why a session is woken (§12.3, "Where wakes come from"). */
42
49
  export type WakeReason =
@@ -69,6 +76,28 @@ export type AdvanceResult = {
69
76
  status: "busy";
70
77
  retryAfterMs: number;
71
78
  };
79
+ /** A pod sandbox's timers. */
80
+ export type SandboxTimer = "idle" | "ttl";
81
+ /** What `DurableExecution.sandbox` asks for: a reconcile, or a timer. */
82
+ export type SandboxSignal = {
83
+ kind: "reconcile";
84
+ } | {
85
+ kind: "arm";
86
+ timer: SandboxTimer;
87
+ at: number;
88
+ };
89
+ /** Why `WorkerHandlers.sandbox` runs. */
90
+ export type SandboxTrigger = "reconcile" | SandboxTimer;
91
+ /** What a sandbox reconcile asks for next. */
92
+ export interface SandboxResult {
93
+ /** Run again this soon. */
94
+ retryAfterMs?: number;
95
+ /** Timers to set, at ms since the epoch. */
96
+ arm?: readonly {
97
+ timer: SandboxTimer;
98
+ at: number;
99
+ }[];
100
+ }
72
101
  export type DeliverResult = {
73
102
  status: "done";
74
103
  }
@@ -94,6 +123,11 @@ export interface WorkerHandlers {
94
123
  * stops. Throw only on infrastructure errors. Required when `deliver` is used.
95
124
  */
96
125
  deliver?(tenantId: string, actionId: string, signal: AbortSignal): Promise<DeliverResult>;
126
+ /**
127
+ * Reconciles one pod sandbox (F7.2), serialized per sandbox. Throw only on infrastructure
128
+ * errors. Required when `sandbox` is used.
129
+ */
130
+ sandbox?(tenantId: string, sandboxId: string, trigger: SandboxTrigger, signal: AbortSignal): Promise<SandboxResult>;
97
131
  }
98
132
  export interface DurableExecution {
99
133
  /** At-least-once. `dedupeKey` merges repeated wakes for the same cause. */
@@ -110,13 +144,22 @@ export interface DurableExecution {
110
144
  * after the transaction that made the Action pending commits.
111
145
  */
112
146
  deliver(tenantId: string, actionId: string): Promise<void>;
147
+ /**
148
+ * A pod sandbox's reconcile, or one of its timers (F7.2): see "One reconcile per sandbox
149
+ * at a time". Absent where pods are not supported.
150
+ */
151
+ sandbox?(tenantId: string, sandboxId: string, signal: SandboxSignal): Promise<void>;
113
152
  /** Arms the Tenant's self-re-arming sweep. Idempotent. */
114
153
  armSweep(tenantId: string): Promise<void>;
115
154
  /** Stops re-arming the Tenant's sweep (Tenant deleted). A pass already running finishes. */
116
155
  disarmSweep(tenantId: string): Promise<void>;
117
156
  /** Starts delivering to `handlers` (serving the Worker endpoint, for Restate). */
118
157
  start(handlers: WorkerHandlers): Promise<void>;
119
- /** Stops delivering, aborts running advances' signals and waits for them. */
158
+ /**
159
+ * Stops delivering, aborts running advances' signals and waits for them, up to the
160
+ * implementation's stop grace (`DEFAULT_STOP_GRACE_MS`). Handlers still running then are
161
+ * abandoned: their sessions' leases lapse and the next advance takes over (§11.4).
162
+ */
120
163
  stop(): Promise<void>;
121
164
  /**
122
165
  * Resolves when the backing service answers, rejects otherwise (readiness,
@@ -155,3 +198,13 @@ export declare function parseSessionKey(key: string): {
155
198
  tenantId: string;
156
199
  sessionId: string;
157
200
  };
201
+ /**
202
+ * Default time `DurableExecution.stop` waits for running handlers after aborting them: the
203
+ * advance grace period (`DEFAULT_ADVANCE_GRACE_MS` in `tenant/worker.ts`).
204
+ */
205
+ export declare const DEFAULT_STOP_GRACE_MS = 30000;
206
+ /**
207
+ * Waits until `inflight` is empty (it shrinks as its promises settle and may grow meanwhile),
208
+ * for at most `graceMs`. Resolves to how many were still running when the grace ran out.
209
+ */
210
+ export declare function settleWithin(inflight: ReadonlySet<Promise<unknown>>, graceMs: number): Promise<number>;
@@ -37,6 +37,13 @@
37
37
  * - **Retry after `retry`.** A delivery that returns `retry` is run again for
38
38
  * the same key after `retryAfterMs`. This is how an Action endpoint that is
39
39
  * down or busy is retried; the handler throws only on infrastructure errors.
40
+ * - **One reconcile per sandbox at a time** (F7.2). `sandbox` runs
41
+ * `WorkerHandlers.sandbox` for the key `<tenantId>:<sandboxId>` at least once
42
+ * after a `reconcile` resolves, never overlapping another run for the key. A
43
+ * run that answers `retryAfterMs` runs again after it (one pending retry per
44
+ * key: a later one replaces it), and each timer it answers is armed. `arm`
45
+ * sets one of the sandbox's timers (`idle`, `ttl`): setting it again replaces
46
+ * the earlier time, and the run it causes is told which timer fired.
40
47
  */
41
48
  export const WAKE_REASONS = [
42
49
  "message",
@@ -59,3 +66,27 @@ export function parseSessionKey(key) {
59
66
  throw new Error(`Invalid session key: ${key}`);
60
67
  return { tenantId: key.slice(0, at), sessionId: key.slice(at + 1) };
61
68
  }
69
+ /**
70
+ * Default time `DurableExecution.stop` waits for running handlers after aborting them: the
71
+ * advance grace period (`DEFAULT_ADVANCE_GRACE_MS` in `tenant/worker.ts`).
72
+ */
73
+ export const DEFAULT_STOP_GRACE_MS = 30_000;
74
+ /**
75
+ * Waits until `inflight` is empty (it shrinks as its promises settle and may grow meanwhile),
76
+ * for at most `graceMs`. Resolves to how many were still running when the grace ran out.
77
+ */
78
+ export async function settleWithin(inflight, graceMs) {
79
+ let timer;
80
+ const expired = new Promise((resolve) => {
81
+ timer = setTimeout(() => resolve("expired"), Math.max(0, graceMs));
82
+ });
83
+ try {
84
+ while (inflight.size > 0)
85
+ if ((await Promise.race([Promise.allSettled(inflight), expired])) === "expired")
86
+ break;
87
+ }
88
+ finally {
89
+ clearTimeout(timer);
90
+ }
91
+ return inflight.size;
92
+ }
@@ -3,20 +3,24 @@
3
3
  * it is not in the published OpenAPI documents.
4
4
  *
5
5
  * `POST /nylorun/v1/model-calls`
6
- * - Headers: `Authorization: Bearer <NYLORUN_GATES_TOKEN>`, `Idempotency-Key: <effect id>`,
7
- * and optionally `Nylorun-Tenant: <tenant id>`, which must then be the database's one
8
- * Tenant (a call naming another is refused).
9
- * - Body: `ModelCallBody`, at most `MAX_MODEL_CALL_BYTES`.
6
+ * - Headers: `Authorization: Bearer <run token>` (F5, `tenant/run-token.ts`), `Idempotency-Key:
7
+ * <effect id>`, and optionally `Nylorun-Tenant: <tenant id>`, which must then be the token's
8
+ * Tenant. Core's credential (`NYLORUN_GATES_TOKEN`) is refused: only a run calls the model.
9
+ * - Body: `ModelCallBody`, at most `MAX_MODEL_CALL_BYTES`. It names no session, turn or agent:
10
+ * the gate takes them from the token, and refuses a body that names them.
10
11
  * - `200 {outcome}` once the call has finished, whether the outcome is a candidate or a
11
- * failure; nothing is sent while it runs. `401 gate_unauthorized` for a missing or wrong
12
- * token, `400 invalid_request` for a malformed Tenant header or a bad body, `409
13
- * gate_conflict` when a different request already runs under the same key.
12
+ * failure; nothing is sent while it runs. `401 gate_unauthorized` for a missing, malformed,
13
+ * expired or foreign token, `409 run_stale` when the token's lease is no longer the
14
+ * session's (a cancel, a new turn, a takeover), `400 invalid_request` for a malformed Tenant
15
+ * header or a bad body, `409 gate_conflict` when a different request already runs under the
16
+ * same key.
14
17
  * - With an `Idempotency-Key` the call outlives its client (P1.2): a re-send with the same key
15
18
  * and body joins it, or gets its outcome for 30 minutes. Without one, a client that goes
16
19
  * away aborts the call.
17
20
  *
18
21
  * `POST /nylorun/v1/model-calls/{key}/cancel` (same headers): aborts the keyed call; `204`
19
- * whether or not it exists.
22
+ * whether or not it exists, `403 gate_forbidden` when it is another session's call. The token
23
+ * need not be live: a user cancel makes it stale, and still stops its call.
20
24
  */
21
25
  import { z } from "zod";
22
26
  import type { ModelGateOutcome } from "./model-gate.js";
@@ -26,9 +30,6 @@ export declare const TENANT_HEADER = "nylorun-tenant";
26
30
  /** Largest request body the gate reads: one `ModelCall` with its whole projected history. */
27
31
  export declare const MAX_MODEL_CALL_BYTES: number;
28
32
  export declare const ModelCallBodySchema: z.ZodObject<{
29
- sessionId: z.ZodString;
30
- turnId: z.ZodString;
31
- agentId: z.ZodString;
32
33
  effectId: z.ZodString;
33
34
  invocationId: z.ZodString;
34
35
  call: z.ZodObject<{
@@ -37,7 +38,7 @@ export declare const ModelCallBodySchema: z.ZodObject<{
37
38
  name: z.ZodString;
38
39
  }, z.core.$loose>>;
39
40
  }, z.core.$loose>;
40
- }, z.core.$strip>;
41
+ }, z.core.$strict>;
41
42
  export type ModelCallBody = z.infer<typeof ModelCallBodySchema>;
42
43
  export interface ModelCallResponse {
43
44
  readonly outcome: ModelGateOutcome;
@@ -45,7 +46,7 @@ export interface ModelCallResponse {
45
46
  /** An error answer: `{error: {code, message}}`. */
46
47
  export interface GateErrorBody {
47
48
  readonly error: {
48
- readonly code: "gate_unauthorized" | "invalid_request" | "gate_conflict";
49
+ readonly code: "gate_unauthorized" | "gate_forbidden" | "run_stale" | "invalid_request" | "gate_conflict";
49
50
  readonly message: string;
50
51
  };
51
52
  }
@@ -3,20 +3,24 @@
3
3
  * it is not in the published OpenAPI documents.
4
4
  *
5
5
  * `POST /nylorun/v1/model-calls`
6
- * - Headers: `Authorization: Bearer <NYLORUN_GATES_TOKEN>`, `Idempotency-Key: <effect id>`,
7
- * and optionally `Nylorun-Tenant: <tenant id>`, which must then be the database's one
8
- * Tenant (a call naming another is refused).
9
- * - Body: `ModelCallBody`, at most `MAX_MODEL_CALL_BYTES`.
6
+ * - Headers: `Authorization: Bearer <run token>` (F5, `tenant/run-token.ts`), `Idempotency-Key:
7
+ * <effect id>`, and optionally `Nylorun-Tenant: <tenant id>`, which must then be the token's
8
+ * Tenant. Core's credential (`NYLORUN_GATES_TOKEN`) is refused: only a run calls the model.
9
+ * - Body: `ModelCallBody`, at most `MAX_MODEL_CALL_BYTES`. It names no session, turn or agent:
10
+ * the gate takes them from the token, and refuses a body that names them.
10
11
  * - `200 {outcome}` once the call has finished, whether the outcome is a candidate or a
11
- * failure; nothing is sent while it runs. `401 gate_unauthorized` for a missing or wrong
12
- * token, `400 invalid_request` for a malformed Tenant header or a bad body, `409
13
- * gate_conflict` when a different request already runs under the same key.
12
+ * failure; nothing is sent while it runs. `401 gate_unauthorized` for a missing, malformed,
13
+ * expired or foreign token, `409 run_stale` when the token's lease is no longer the
14
+ * session's (a cancel, a new turn, a takeover), `400 invalid_request` for a malformed Tenant
15
+ * header or a bad body, `409 gate_conflict` when a different request already runs under the
16
+ * same key.
14
17
  * - With an `Idempotency-Key` the call outlives its client (P1.2): a re-send with the same key
15
18
  * and body joins it, or gets its outcome for 30 minutes. Without one, a client that goes
16
19
  * away aborts the call.
17
20
  *
18
21
  * `POST /nylorun/v1/model-calls/{key}/cancel` (same headers): aborts the keyed call; `204`
19
- * whether or not it exists.
22
+ * whether or not it exists, `403 gate_forbidden` when it is another session's call. The token
23
+ * need not be live: a user cancel makes it stale, and still stops its call.
20
24
  */
21
25
  import { z } from "zod";
22
26
  export const MODEL_CALLS_PATH = "/nylorun/v1/model-calls";
@@ -24,10 +28,7 @@ export const MODEL_CALLS_PATH = "/nylorun/v1/model-calls";
24
28
  export const TENANT_HEADER = "nylorun-tenant";
25
29
  /** Largest request body the gate reads: one `ModelCall` with its whole projected history. */
26
30
  export const MAX_MODEL_CALL_BYTES = 32 * 1024 * 1024;
27
- export const ModelCallBodySchema = z.object({
28
- sessionId: z.string().min(1),
29
- turnId: z.string().min(1),
30
- agentId: z.string().min(1),
31
+ export const ModelCallBodySchema = z.strictObject({
31
32
  effectId: z.string().min(1),
32
33
  invocationId: z.string().min(1),
33
34
  call: z.looseObject({
@@ -0,0 +1,72 @@
1
+ import { type Server } from "node:http";
2
+ import { type Socket } from "node:net";
3
+ import type { EgressTokenVerdict } from "../sandbox/egress-token.js";
4
+ import type { SessionStore } from "../store/types.js";
5
+ import type { Logger } from "../tenant/types.js";
6
+ /**
7
+ * What egress-gate reads of a sandbox: its current host epoch, the pod that joined at it, and
8
+ * its spec's `network.allow`; `undefined` when it is gone, deleted, lost, expired or not a pod.
9
+ */
10
+ export interface EgressSandboxes {
11
+ live(sandboxId: string): Promise<{
12
+ readonly epoch: number;
13
+ readonly podUid?: string;
14
+ readonly allow: readonly string[];
15
+ } | undefined>;
16
+ }
17
+ /** Ports a tunnel may open. */
18
+ export declare const EGRESS_PORTS: readonly number[];
19
+ /** Tunnels one sandbox may hold open at once. */
20
+ export declare const EGRESS_MAX_PER_SANDBOX = 64;
21
+ /** A tunnel with no traffic either way for this long is closed. */
22
+ export declare const EGRESS_IDLE_MS: number;
23
+ /** How long a sandbox's epoch and allowlist are reused before they are read again. */
24
+ export declare const EGRESS_CACHE_MS = 5000;
25
+ /** How long the gate waits for the upstream connection. */
26
+ export declare const EGRESS_CONNECT_TIMEOUT_MS = 10000;
27
+ export interface StartEgressGateOptions {
28
+ readonly listen: {
29
+ readonly host: string;
30
+ readonly port: number;
31
+ };
32
+ readonly logger: Logger;
33
+ /** Verifies a presented egress token (`verifyEgressToken` over the Tenant's keys). */
34
+ verify(raw: string): Promise<EgressTokenVerdict>;
35
+ readonly sandboxes: EgressSandboxes;
36
+ readonly idleMs?: number;
37
+ readonly maxPerSandbox?: number;
38
+ readonly cacheMs?: number;
39
+ readonly connectTimeoutMs?: number;
40
+ /** Tests replace name resolution. */
41
+ readonly lookup?: (host: string) => Promise<readonly {
42
+ address: string;
43
+ family: number;
44
+ }[]>;
45
+ /** Tests replace which resolved addresses are refused. Default `isEgressBlocked`. */
46
+ readonly blocked?: (address: string) => boolean;
47
+ /** Tests replace how the checked address is dialled. */
48
+ readonly dial?: (address: string, port: number) => Socket;
49
+ }
50
+ export interface EgressGate {
51
+ /** Where the gate listens, e.g. `http://0.0.0.0:4200`. */
52
+ readonly url: string;
53
+ readonly server: Server;
54
+ close(): Promise<void>;
55
+ }
56
+ /**
57
+ * True when a tunnel must not reach `address`: private, loopback, link-local (169.254.169.254, the
58
+ * cloud metadata address, included), unspecified, multicast, reserved, or an IPv6 form that
59
+ * embeds an IPv4 address.
60
+ */
61
+ export declare function isEgressBlocked(address: string): boolean;
62
+ /** Whether `host` (a lowercase name) matches one of `allow`'s names or `*.suffix` patterns. */
63
+ export declare function egressAllows(host: string, allow: readonly string[]): boolean;
64
+ /** The egress token in a `Proxy-Authorization` header, or `undefined`. */
65
+ export declare function proxyToken(header: string | undefined): string | undefined;
66
+ export declare function startEgressGate(options: StartEgressGateOptions): Promise<EgressGate>;
67
+ /**
68
+ * `EgressSandboxes` over the Tenant's `sandbox_resources` (one indexed read): a pod sandbox's
69
+ * host epoch, the pod that joined at it and its spec's allowlist; nothing for a virtual
70
+ * sandbox, or one deleted, lost or expired, as `staleHost` judges a host token.
71
+ */
72
+ export declare function storeEgressSandboxes(store: SessionStore): EgressSandboxes;