@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
@@ -0,0 +1,366 @@
1
+ import { createHash } from "node:crypto";
2
+ import { AwsClient } from "aws4fetch";
3
+ import { BlobRangeError, BlobStoreError, BlobTooLargeError, DEFAULT_CONTENT_TYPE, assertBlobKey, assertBlobPrefix, assertMaxBytes, assertRange, chunksOf, } from "./types.js";
4
+ const MIN_PART_SIZE = 5 * 1024 * 1024;
5
+ const DEFAULT_PART_SIZE = 8 * 1024 * 1024;
6
+ function sha256Hex(bytes) {
7
+ return createHash("sha256").update(bytes).digest("hex");
8
+ }
9
+ function concat(chunks, length) {
10
+ if (chunks.length === 1)
11
+ return chunks[0];
12
+ const out = new Uint8Array(length);
13
+ let offset = 0;
14
+ for (const chunk of chunks) {
15
+ out.set(chunk, offset);
16
+ offset += chunk.byteLength;
17
+ }
18
+ return out;
19
+ }
20
+ function decodeXml(text) {
21
+ return text
22
+ .replace(/&lt;/g, "<")
23
+ .replace(/&gt;/g, ">")
24
+ .replace(/&quot;/g, '"')
25
+ .replace(/&apos;/g, "'")
26
+ .replace(/&#(\d+);/g, (_, code) => String.fromCodePoint(Number(code)))
27
+ .replace(/&amp;/g, "&");
28
+ }
29
+ function escapeXml(text) {
30
+ return text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
31
+ }
32
+ /** The text of the first `<name>` element in `xml`, decoded. */
33
+ function tag(xml, name) {
34
+ const match = new RegExp(`<${name}>([\\s\\S]*?)</${name}>`).exec(xml);
35
+ return match ? decodeXml(match[1]) : undefined;
36
+ }
37
+ function blocks(xml, name) {
38
+ return [...xml.matchAll(new RegExp(`<${name}>([\\s\\S]*?)</${name}>`, "g"))].map((m) => m[1]);
39
+ }
40
+ /**
41
+ * The `s3` adapter over the plain S3 API: PutObject, multipart upload, GetObject with Range,
42
+ * HeadObject, DeleteObject and ListObjectsV2, signed with SigV4 by aws4fetch (MIT, no
43
+ * dependencies, built on `fetch` and web streams). It uses no server's admin, KMS or lifecycle
44
+ * API (D35), so RustFS, S3, R2 or any S3 server sits behind it.
45
+ *
46
+ * Uploads stream: the body is counted and hashed as it arrives and sent in parts of
47
+ * `partSize`, so memory stays at about one part whatever the size, and a body that passes
48
+ * `maxBytes` aborts its multipart upload. Each request carries the SHA-256 of its payload,
49
+ * which the server checks. Downloads hand back the response's stream.
50
+ */
51
+ export function createS3BlobStore(options) {
52
+ const endpoint = new URL(options.endpoint);
53
+ if (endpoint.protocol !== "http:" && endpoint.protocol !== "https:")
54
+ throw new TypeError("The S3 endpoint must be an http or https URL");
55
+ const base = endpoint.toString().replace(/\/+$/, "");
56
+ const pathStyle = options.forcePathStyle !== false;
57
+ const bucketUrl = pathStyle
58
+ ? `${base}/${options.bucket}`
59
+ : `${endpoint.protocol}//${options.bucket}.${endpoint.host}${endpoint.pathname.replace(/\/+$/, "")}`;
60
+ const partSize = Math.max(options.partSize ?? DEFAULT_PART_SIZE, MIN_PART_SIZE);
61
+ const attempts = Math.max(1, options.attempts ?? 3);
62
+ const fetchImpl = options.fetch ?? globalThis.fetch;
63
+ const client = new AwsClient({
64
+ accessKeyId: options.accessKeyId,
65
+ secretAccessKey: options.secretAccessKey,
66
+ service: "s3",
67
+ region: options.region ?? "us-east-1",
68
+ retries: 0,
69
+ });
70
+ const objectUrl = (key, query) => {
71
+ const url = new URL(`${bucketUrl}/${key}`);
72
+ for (const [name, value] of Object.entries(query ?? {}))
73
+ url.searchParams.set(name, value);
74
+ return url.toString();
75
+ };
76
+ /** One signed request; retried on a network error, 429 or 5xx unless `once`. */
77
+ async function send(url, init, once = false) {
78
+ let lastError;
79
+ for (let attempt = 1; attempt <= (once ? 1 : attempts); attempt += 1) {
80
+ try {
81
+ const response = await fetchImpl(await client.sign(url, init));
82
+ if ((response.status >= 500 || response.status === 429) && attempt < attempts && !once) {
83
+ await response.body?.cancel();
84
+ await new Promise((resolve) => setTimeout(resolve, 50 * 2 ** attempt));
85
+ continue;
86
+ }
87
+ return response;
88
+ }
89
+ catch (error) {
90
+ if (init.signal?.aborted)
91
+ throw error;
92
+ lastError = error;
93
+ if (attempt < (once ? 1 : attempts))
94
+ await new Promise((resolve) => setTimeout(resolve, 50 * 2 ** attempt));
95
+ }
96
+ }
97
+ throw new BlobStoreError(`The Object store is unreachable: ${lastError instanceof Error ? lastError.message : String(lastError)}`);
98
+ }
99
+ async function failure(response, what) {
100
+ const text = await response.text().catch(() => "");
101
+ const code = tag(text, "Code");
102
+ const message = tag(text, "Message");
103
+ return new BlobStoreError(`${what} failed: ${response.status}${code ? ` ${code}` : ""}${message ? `: ${message}` : ""}`, response.status, code);
104
+ }
105
+ /** PUT with the payload's SHA-256, which the server verifies. */
106
+ const putBytes = (url, bytes, headers, signal) => send(url, {
107
+ method: "PUT",
108
+ // fetch sends any ArrayBufferView; the DOM types want one over an ArrayBuffer.
109
+ body: bytes,
110
+ headers: { ...headers, "x-amz-content-sha256": sha256Hex(bytes) },
111
+ ...(signal ? { signal } : {}),
112
+ });
113
+ async function head(key) {
114
+ assertBlobKey(key);
115
+ const response = await send(objectUrl(key), { method: "HEAD" });
116
+ if (response.status === 404)
117
+ return undefined;
118
+ if (!response.ok)
119
+ throw await failure(response, `HEAD ${key}`);
120
+ return metaOf(key, response.headers, Number(response.headers.get("content-length") ?? 0));
121
+ }
122
+ function metaOf(key, headers, size) {
123
+ const modified = headers.get("last-modified");
124
+ return {
125
+ key,
126
+ size,
127
+ contentType: headers.get("content-type") ?? DEFAULT_CONTENT_TYPE,
128
+ lastModified: modified ? new Date(modified) : new Date(0),
129
+ };
130
+ }
131
+ async function putMultipart(key, first, rest, state, put) {
132
+ const signal = put.signal;
133
+ const created = await send(objectUrl(key, { uploads: "" }), {
134
+ method: "POST",
135
+ headers: { "content-type": put.contentType ?? DEFAULT_CONTENT_TYPE },
136
+ ...(signal ? { signal } : {}),
137
+ }, true);
138
+ if (!created.ok)
139
+ throw await failure(created, `CreateMultipartUpload ${key}`);
140
+ const uploadId = tag(await created.text(), "UploadId");
141
+ if (!uploadId)
142
+ throw new BlobStoreError(`CreateMultipartUpload ${key} returned no UploadId`);
143
+ const parts = [];
144
+ const uploadPart = async (bytes) => {
145
+ const number = parts.length + 1;
146
+ const response = await putBytes(objectUrl(key, { partNumber: String(number), uploadId }), bytes, {}, signal);
147
+ if (!response.ok)
148
+ throw await failure(response, `UploadPart ${number} of ${key}`);
149
+ await response.body?.cancel();
150
+ const etag = response.headers.get("etag");
151
+ if (!etag)
152
+ throw new BlobStoreError(`UploadPart ${number} of ${key} returned no ETag`);
153
+ parts.push({ number, etag });
154
+ };
155
+ try {
156
+ await uploadPart(first);
157
+ let buffered = [];
158
+ let length = 0;
159
+ for (;;) {
160
+ const next = await rest.next();
161
+ if (next.done)
162
+ break;
163
+ const chunk = next.value;
164
+ signal?.throwIfAborted();
165
+ state.size += chunk.byteLength;
166
+ if (put.maxBytes !== undefined && state.size > put.maxBytes)
167
+ throw new BlobTooLargeError(put.maxBytes);
168
+ state.hash.update(chunk);
169
+ buffered.push(chunk);
170
+ length += chunk.byteLength;
171
+ if (length >= partSize) {
172
+ await uploadPart(concat(buffered, length));
173
+ buffered = [];
174
+ length = 0;
175
+ }
176
+ }
177
+ if (length > 0)
178
+ await uploadPart(concat(buffered, length));
179
+ const body = `<CompleteMultipartUpload>${parts
180
+ .map((part) => `<Part><PartNumber>${part.number}</PartNumber><ETag>${escapeXml(part.etag)}</ETag></Part>`)
181
+ .join("")}</CompleteMultipartUpload>`;
182
+ const completed = await send(objectUrl(key, { uploadId }), {
183
+ method: "POST",
184
+ body,
185
+ headers: { "content-type": "application/xml", "x-amz-content-sha256": sha256Hex(new TextEncoder().encode(body)) },
186
+ ...(signal ? { signal } : {}),
187
+ }, true);
188
+ // CompleteMultipartUpload may answer 200 with an error in the body.
189
+ const text = completed.ok ? await completed.text() : "";
190
+ if (!completed.ok)
191
+ throw await failure(completed, `CompleteMultipartUpload ${key}`);
192
+ if (/<Error>/.test(text))
193
+ throw new BlobStoreError(`CompleteMultipartUpload ${key} failed: ${tag(text, "Code") ?? "error"}: ${tag(text, "Message") ?? ""}`, completed.status, tag(text, "Code"));
194
+ }
195
+ catch (error) {
196
+ // Best effort: an upload left behind holds only parts, never an object.
197
+ await send(objectUrl(key, { uploadId }), { method: "DELETE" })
198
+ .then((response) => response.body?.cancel())
199
+ .catch(() => undefined);
200
+ throw error;
201
+ }
202
+ }
203
+ return {
204
+ kind: "s3",
205
+ async ensureBucket() {
206
+ const found = await send(bucketUrl, { method: "HEAD" });
207
+ if (found.ok)
208
+ return;
209
+ if (found.status !== 404)
210
+ throw await failure(found, `HEAD bucket ${options.bucket}`);
211
+ const region = options.region ?? "us-east-1";
212
+ const body = region === "us-east-1"
213
+ ? undefined
214
+ : `<CreateBucketConfiguration><LocationConstraint>${escapeXml(region)}</LocationConstraint></CreateBucketConfiguration>`;
215
+ const created = await send(bucketUrl, {
216
+ method: "PUT",
217
+ ...(body
218
+ ? {
219
+ body,
220
+ headers: {
221
+ "content-type": "application/xml",
222
+ "x-amz-content-sha256": sha256Hex(new TextEncoder().encode(body)),
223
+ },
224
+ }
225
+ : {}),
226
+ });
227
+ if (created.ok) {
228
+ await created.body?.cancel();
229
+ return;
230
+ }
231
+ const error = await failure(created, `CreateBucket ${options.bucket}`);
232
+ if (error.code === "BucketAlreadyOwnedByYou" || error.code === "BucketAlreadyExists")
233
+ return;
234
+ throw error;
235
+ },
236
+ async put(key, body, put = {}) {
237
+ assertBlobKey(key);
238
+ assertMaxBytes(put.maxBytes);
239
+ put.signal?.throwIfAborted();
240
+ const state = { size: 0, hash: createHash("sha256") };
241
+ const chunks = chunksOf(body)[Symbol.asyncIterator]();
242
+ // Buffer up to one part: a body that ends first goes in one PutObject.
243
+ const buffered = [];
244
+ let length = 0;
245
+ let ended = false;
246
+ try {
247
+ while (length < partSize) {
248
+ const next = await chunks.next();
249
+ if (next.done) {
250
+ ended = true;
251
+ break;
252
+ }
253
+ put.signal?.throwIfAborted();
254
+ state.size += next.value.byteLength;
255
+ if (put.maxBytes !== undefined && state.size > put.maxBytes)
256
+ throw new BlobTooLargeError(put.maxBytes);
257
+ state.hash.update(next.value);
258
+ buffered.push(next.value);
259
+ length += next.value.byteLength;
260
+ }
261
+ const first = concat(buffered, length);
262
+ if (ended) {
263
+ const response = await putBytes(objectUrl(key), first, { "content-type": put.contentType ?? DEFAULT_CONTENT_TYPE }, put.signal);
264
+ if (!response.ok)
265
+ throw await failure(response, `PUT ${key}`);
266
+ await response.body?.cancel();
267
+ }
268
+ else {
269
+ await putMultipart(key, first, chunks, state, put);
270
+ }
271
+ }
272
+ catch (error) {
273
+ await chunks.return?.(undefined).catch(() => undefined);
274
+ throw error;
275
+ }
276
+ return { key, size: state.size, sha256: state.hash.digest("hex") };
277
+ },
278
+ async get(key, get = {}) {
279
+ assertBlobKey(key);
280
+ const range = get.range;
281
+ if (range)
282
+ assertRange(range);
283
+ const response = await send(objectUrl(key), {
284
+ method: "GET",
285
+ ...(range ? { headers: { range: `bytes=${range.start}-${range.end ?? ""}` } } : {}),
286
+ ...(get.signal ? { signal: get.signal } : {}),
287
+ });
288
+ if (response.status === 404) {
289
+ await response.body?.cancel();
290
+ return undefined;
291
+ }
292
+ if (response.status === 416 && range) {
293
+ await response.body?.cancel();
294
+ const total = /\/(\d+)$/.exec(response.headers.get("content-range") ?? "")?.[1];
295
+ throw new BlobRangeError(range, total !== undefined ? Number(total) : ((await head(key))?.size ?? 0));
296
+ }
297
+ if (!response.ok || !response.body)
298
+ throw await failure(response, `GET ${key}`);
299
+ const length = Number(response.headers.get("content-length") ?? 0);
300
+ if (response.status === 206) {
301
+ const match = /^bytes (\d+)-(\d+)\/(\d+|\*)$/.exec(response.headers.get("content-range") ?? "");
302
+ if (!match) {
303
+ await response.body.cancel();
304
+ throw new BlobStoreError(`GET ${key} answered 206 without a Content-Range`);
305
+ }
306
+ const size = match[3] === "*" ? ((await head(key))?.size ?? 0) : Number(match[3]);
307
+ return {
308
+ ...metaOf(key, response.headers, size),
309
+ body: response.body,
310
+ range: { start: Number(match[1]), end: Number(match[2]) },
311
+ };
312
+ }
313
+ // 200: the whole object. A server that ignores Range sends it all too.
314
+ const meta = metaOf(key, response.headers, length);
315
+ if (!range)
316
+ return { ...meta, body: response.body };
317
+ if (range.start >= length) {
318
+ await response.body.cancel();
319
+ throw new BlobRangeError(range, length);
320
+ }
321
+ if (range.start !== 0) {
322
+ await response.body.cancel();
323
+ throw new BlobStoreError(`GET ${key} ignored the Range header`);
324
+ }
325
+ return { ...meta, body: response.body, range: { start: 0, end: length - 1 } };
326
+ },
327
+ head,
328
+ async delete(key) {
329
+ assertBlobKey(key);
330
+ const response = await send(objectUrl(key), { method: "DELETE" });
331
+ if (response.ok || response.status === 404) {
332
+ await response.body?.cancel();
333
+ return;
334
+ }
335
+ throw await failure(response, `DELETE ${key}`);
336
+ },
337
+ async *list(prefix) {
338
+ assertBlobPrefix(prefix);
339
+ let token;
340
+ do {
341
+ const url = new URL(bucketUrl);
342
+ url.searchParams.set("list-type", "2");
343
+ if (prefix !== "")
344
+ url.searchParams.set("prefix", prefix);
345
+ if (token)
346
+ url.searchParams.set("continuation-token", token);
347
+ const response = await send(url.toString(), { method: "GET" });
348
+ if (!response.ok)
349
+ throw await failure(response, `ListObjectsV2 ${prefix}`);
350
+ const xml = await response.text();
351
+ for (const entry of blocks(xml, "Contents")) {
352
+ const key = tag(entry, "Key");
353
+ if (key === undefined)
354
+ continue;
355
+ const modified = tag(entry, "LastModified");
356
+ yield {
357
+ key,
358
+ size: Number(tag(entry, "Size") ?? 0),
359
+ lastModified: modified ? new Date(modified) : new Date(0),
360
+ };
361
+ }
362
+ token = tag(xml, "IsTruncated") === "true" ? tag(xml, "NextContinuationToken") : undefined;
363
+ } while (token);
364
+ },
365
+ };
366
+ }
@@ -0,0 +1,108 @@
1
+ /**
2
+ * The `BlobStore` seam (blueprint D35, F8.1): the only way the Runtime reaches the Object
3
+ * store. Bytes live here; what they mean lives in Postgres, which is the commit point: an
4
+ * object counts only once a committed row references its key, so an orphaned upload is
5
+ * garbage, never data. Keys are chosen by the Runtime (fresh random ids), so the seam needs no
6
+ * conditional writes.
7
+ *
8
+ * Two adapters, held to one conformance suite (`test/blob/conformance.ts`):
9
+ * - `s3` (`./s3.ts`): the plain S3 API, against RustFS in the local stack and S3 or any S3
10
+ * server elsewhere;
11
+ * - `fs` (`./fs.ts`): files under a directory, for embedding and tests.
12
+ */
13
+ /** A body to store: bytes, a web stream, or any async iterable of chunks (a Node stream). */
14
+ export type BlobBody = Uint8Array | ReadableStream<Uint8Array> | AsyncIterable<Uint8Array>;
15
+ export interface BlobPutOptions {
16
+ /** Stored with the object and returned by `head` and `get`. Default `application/octet-stream`. */
17
+ contentType?: string;
18
+ /**
19
+ * Refuse a body longer than this many bytes with `BlobTooLargeError`; nothing is stored. The
20
+ * body is counted as it streams, so a caller need not know its length up front.
21
+ */
22
+ maxBytes?: number;
23
+ signal?: AbortSignal;
24
+ }
25
+ export interface BlobPutResult {
26
+ key: string;
27
+ /** Bytes stored. */
28
+ size: number;
29
+ /** SHA-256 of the bytes stored, lowercase hex. */
30
+ sha256: string;
31
+ }
32
+ export interface BlobMeta {
33
+ key: string;
34
+ /** The whole object's size in bytes. */
35
+ size: number;
36
+ contentType: string;
37
+ lastModified: Date;
38
+ }
39
+ /** Bytes `start` to `end`, both inclusive, as in an HTTP `Range` header. */
40
+ export interface BlobRange {
41
+ start: number;
42
+ /** Inclusive; past the end of the object means "to the end". Default: to the end. */
43
+ end?: number;
44
+ }
45
+ export interface BlobGetOptions {
46
+ /** Only these bytes. A range that starts at or past the end throws `BlobRangeError`. */
47
+ range?: BlobRange;
48
+ signal?: AbortSignal;
49
+ }
50
+ export interface BlobGetResult extends BlobMeta {
51
+ /** The bytes asked for: the whole object, or the range. Read it or cancel it. */
52
+ body: ReadableStream<Uint8Array>;
53
+ /** Present when a range was asked for: the bytes `body` holds, both inclusive. */
54
+ range?: {
55
+ start: number;
56
+ end: number;
57
+ };
58
+ }
59
+ /** An entry of `list`: the object's key, size and when it was last written. */
60
+ export interface BlobListEntry {
61
+ key: string;
62
+ size: number;
63
+ lastModified: Date;
64
+ }
65
+ export interface BlobStore {
66
+ /** Which adapter this is, for logs and `/ready`. */
67
+ readonly kind: "fs" | "s3";
68
+ /** Store `body` at `key`, replacing what is there. Streams; counts and hashes as it goes. */
69
+ put(key: string, body: BlobBody, options?: BlobPutOptions): Promise<BlobPutResult>;
70
+ /** The object's bytes and metadata, or `undefined` when there is none at `key`. */
71
+ get(key: string, options?: BlobGetOptions): Promise<BlobGetResult | undefined>;
72
+ /** The object's metadata, or `undefined` when there is none at `key`. */
73
+ head(key: string): Promise<BlobMeta | undefined>;
74
+ /** Remove the object; removing one that is not there succeeds. */
75
+ delete(key: string): Promise<void>;
76
+ /** Every object whose key starts with `prefix`, in key order. */
77
+ list(prefix: string): AsyncIterable<BlobListEntry>;
78
+ }
79
+ /** The Object store failed or refused a request; `status` is the HTTP status when it has one. */
80
+ export declare class BlobStoreError extends Error {
81
+ readonly status?: number | undefined;
82
+ readonly code?: string | undefined;
83
+ constructor(message: string, status?: number | undefined, code?: string | undefined);
84
+ }
85
+ /** A `put` body grew past `maxBytes`. Nothing was stored. */
86
+ export declare class BlobTooLargeError extends Error {
87
+ readonly maxBytes: number;
88
+ constructor(maxBytes: number);
89
+ }
90
+ /** A `get` range starts at or past the end of the object (HTTP 416). */
91
+ export declare class BlobRangeError extends Error {
92
+ readonly range: BlobRange;
93
+ readonly size: number;
94
+ constructor(range: BlobRange, size: number);
95
+ }
96
+ export declare const DEFAULT_CONTENT_TYPE = "application/octet-stream";
97
+ export declare const MAX_KEY_BYTES = 1024;
98
+ /**
99
+ * Keys are `/`-separated segments of `A-Z a-z 0-9 . _ -`, neither `.` nor `..`, at most 1024
100
+ * bytes: safe as S3 keys without encoding surprises and as relative paths for `fs`.
101
+ */
102
+ export declare function assertBlobKey(key: string): void;
103
+ /** A `list` prefix: empty, a key, or a key followed by `/` (or the start of a segment). */
104
+ export declare function assertBlobPrefix(prefix: string): void;
105
+ export declare function assertRange(range: BlobRange): void;
106
+ export declare function assertMaxBytes(maxBytes: number | undefined): void;
107
+ /** The chunks of a `BlobBody`, as `Uint8Array`s. */
108
+ export declare function chunksOf(body: BlobBody): AsyncGenerator<Uint8Array>;
@@ -0,0 +1,85 @@
1
+ /**
2
+ * The `BlobStore` seam (blueprint D35, F8.1): the only way the Runtime reaches the Object
3
+ * store. Bytes live here; what they mean lives in Postgres, which is the commit point: an
4
+ * object counts only once a committed row references its key, so an orphaned upload is
5
+ * garbage, never data. Keys are chosen by the Runtime (fresh random ids), so the seam needs no
6
+ * conditional writes.
7
+ *
8
+ * Two adapters, held to one conformance suite (`test/blob/conformance.ts`):
9
+ * - `s3` (`./s3.ts`): the plain S3 API, against RustFS in the local stack and S3 or any S3
10
+ * server elsewhere;
11
+ * - `fs` (`./fs.ts`): files under a directory, for embedding and tests.
12
+ */
13
+ /** The Object store failed or refused a request; `status` is the HTTP status when it has one. */
14
+ export class BlobStoreError extends Error {
15
+ status;
16
+ code;
17
+ constructor(message, status, code) {
18
+ super(message);
19
+ this.status = status;
20
+ this.code = code;
21
+ this.name = "BlobStoreError";
22
+ }
23
+ }
24
+ /** A `put` body grew past `maxBytes`. Nothing was stored. */
25
+ export class BlobTooLargeError extends Error {
26
+ maxBytes;
27
+ constructor(maxBytes) {
28
+ super(`The body is larger than ${maxBytes} bytes`);
29
+ this.maxBytes = maxBytes;
30
+ this.name = "BlobTooLargeError";
31
+ }
32
+ }
33
+ /** A `get` range starts at or past the end of the object (HTTP 416). */
34
+ export class BlobRangeError extends Error {
35
+ range;
36
+ size;
37
+ constructor(range, size) {
38
+ super(`Range ${range.start}-${range.end ?? ""} is outside an object of ${size} bytes`);
39
+ this.range = range;
40
+ this.size = size;
41
+ this.name = "BlobRangeError";
42
+ }
43
+ }
44
+ export const DEFAULT_CONTENT_TYPE = "application/octet-stream";
45
+ export const MAX_KEY_BYTES = 1024;
46
+ const SEGMENT = /^[A-Za-z0-9._-]+$/;
47
+ /**
48
+ * Keys are `/`-separated segments of `A-Z a-z 0-9 . _ -`, neither `.` nor `..`, at most 1024
49
+ * bytes: safe as S3 keys without encoding surprises and as relative paths for `fs`.
50
+ */
51
+ export function assertBlobKey(key) {
52
+ if (typeof key !== "string" || key === "" || key.length > MAX_KEY_BYTES)
53
+ throw new TypeError(`Blob key must be 1 to ${MAX_KEY_BYTES} characters`);
54
+ for (const segment of key.split("/"))
55
+ if (!SEGMENT.test(segment) || segment === "." || segment === "..")
56
+ throw new TypeError(`Blob key ${JSON.stringify(key)} must be /-separated segments of A-Z a-z 0-9 . _ -, none . or ..`);
57
+ }
58
+ /** A `list` prefix: empty, a key, or a key followed by `/` (or the start of a segment). */
59
+ export function assertBlobPrefix(prefix) {
60
+ if (prefix === "")
61
+ return;
62
+ assertBlobKey(prefix.endsWith("/") ? prefix.slice(0, -1) : prefix);
63
+ }
64
+ export function assertRange(range) {
65
+ if (!Number.isSafeInteger(range.start) || range.start < 0)
66
+ throw new TypeError("Range start must be a non-negative integer");
67
+ if (range.end !== undefined && (!Number.isSafeInteger(range.end) || range.end < range.start))
68
+ throw new TypeError("Range end must be an integer no smaller than start");
69
+ }
70
+ export function assertMaxBytes(maxBytes) {
71
+ if (maxBytes !== undefined && (!Number.isSafeInteger(maxBytes) || maxBytes < 0))
72
+ throw new TypeError("maxBytes must be a non-negative integer");
73
+ }
74
+ /** The chunks of a `BlobBody`, as `Uint8Array`s. */
75
+ export async function* chunksOf(body) {
76
+ if (body instanceof Uint8Array) {
77
+ yield body;
78
+ return;
79
+ }
80
+ for await (const chunk of body) {
81
+ if (!(chunk instanceof Uint8Array))
82
+ throw new TypeError("A blob body must yield Uint8Array chunks");
83
+ yield chunk;
84
+ }
85
+ }
@@ -1,5 +1,5 @@
1
1
  import type { Action } from "@nylorun/core/contracts";
2
- import type { SandboxManager } from "../sandbox/manager.js";
2
+ import type { WorkspacePort } from "../harness-api/workspace.js";
3
3
  import type { SandboxToolOutcome } from "../sandbox/tools.js";
4
4
  import { type SessionSandboxRef } from "../sandbox/share.js";
5
5
  export declare class SandboxRouteError extends Error {
@@ -16,7 +16,7 @@ export type SandboxRouteRead = {
16
16
  readonly lookup: (id: string) => SessionSandboxRef | undefined;
17
17
  };
18
18
  export type SandboxRouteDeps = {
19
- readonly sandbox: SandboxManager;
19
+ readonly sandbox: Pick<WorkspacePort, "run">;
20
20
  /** The session (rejects when missing) and its owner chain, read in one transaction. */
21
21
  readonly session: (id: string) => Promise<SandboxRouteRead>;
22
22
  readonly getAction: (id: string) => Promise<Action | undefined>;
@@ -3,7 +3,7 @@
3
3
  * Wired from TenantRuntime; keeps route bodies out of the hot runtime file.
4
4
  */
5
5
  import { isSandboxToolName, } from "@nylorun/core/define";
6
- import { capabilityForSandbox, owningSandboxSessionId, sandboxSpecOf, sandboxSpecsEqual, sessionSandboxSpec, } from "../sandbox/share.js";
6
+ import { capabilityForSandbox, owningSandboxSessionId, sandboxSpecOf, sandboxWorkspaceOf, sandboxSpecsEqual, sessionSandboxSpec, } from "../sandbox/share.js";
7
7
  export class SandboxRouteError extends Error {
8
8
  status;
9
9
  constructor(status, message) {
@@ -36,11 +36,12 @@ function toolInput(body) {
36
36
  }
37
37
  async function runTool(deps, { session, lookup }, tool, input, signal) {
38
38
  const spec = resolveSpec(session, lookup);
39
- const ownerId = owningSandboxSessionId(session, lookup);
39
+ const workspace = sandboxWorkspaceOf(session, lookup);
40
40
  const ref = {
41
- id: ownerId,
41
+ id: workspace.ownerId,
42
42
  activeTurnId: session.activeTurnId,
43
43
  manifest: session.manifest,
44
+ ...(workspace.sandboxId === undefined ? {} : { sandboxId: workspace.sandboxId }),
44
45
  };
45
46
  return deps.sandbox.run(ref, capabilityForSandbox(spec), tool, input, signal);
46
47
  }
@@ -1,4 +1,4 @@
1
- import { type DurableExecution, type Wake, type WorkerHandlers } from "./types.js";
1
+ import { type DurableExecution, type SandboxSignal, type Wake, type WorkerHandlers } from "./types.js";
2
2
  export interface MemoryExecutionOptions {
3
3
  /** Delay between sweep passes of one Tenant. Default 5000. */
4
4
  sweepIntervalMs?: number;
@@ -8,6 +8,11 @@ export interface MemoryExecutionOptions {
8
8
  maxAttempts?: number;
9
9
  /** How long a dedupe key merges repeated wakes. Default 24 hours. */
10
10
  dedupeRetentionMs?: number;
11
+ /**
12
+ * How long `stop` waits for running handlers after aborting them; the ones still running
13
+ * then are abandoned. Default `DEFAULT_STOP_GRACE_MS`.
14
+ */
15
+ stopGraceMs?: number;
11
16
  /** Receives handler failures that are not retried. */
12
17
  onError?: (error: unknown) => void;
13
18
  }
@@ -23,16 +28,21 @@ export declare class MemoryExecution implements DurableExecution {
23
28
  private readonly seen;
24
29
  private readonly timers;
25
30
  private readonly sweeps;
31
+ private readonly sandboxes;
26
32
  private readonly delayed;
27
33
  private readonly inflight;
28
34
  private readonly sweepIntervalMs;
29
35
  private readonly retryDelayMs;
30
36
  private readonly maxAttempts;
31
37
  private readonly dedupeRetentionMs;
38
+ private readonly stopGraceMs;
32
39
  private readonly onError;
33
40
  constructor(options?: MemoryExecutionOptions);
34
41
  wake(tenantId: string, sessionId: string, wake: Wake): Promise<void>;
35
42
  deliver(tenantId: string, actionId: string): Promise<void>;
43
+ sandbox(tenantId: string, sandboxId: string, signal: SandboxSignal): Promise<void>;
44
+ private armSandbox;
45
+ private runSandbox;
36
46
  timer(tenantId: string, key: string, at: Date): Promise<void>;
37
47
  armSweep(tenantId: string): Promise<void>;
38
48
  disarmSweep(tenantId: string): Promise<void>;