@nylorun/runtime 0.26.0-beta → 0.28.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 (379) hide show
  1. package/CHANGELOG.md +171 -0
  2. package/README.md +18 -14
  3. package/dist/adapters/execution/restate.d.ts +15 -18
  4. package/dist/adapters/execution/restate.js +31 -27
  5. package/dist/adapters/replication/pgoutput.d.ts +1 -1
  6. package/dist/adapters/replication/pgoutput.js +3 -2
  7. package/dist/adapters/sandbox/local.js +8 -5
  8. package/dist/adapters/sandbox/virtual.js +2 -2
  9. package/dist/adapters/streams/s2.d.ts +1 -1
  10. package/dist/adapters/streams/s2.js +18 -14
  11. package/dist/api/a2a/card.d.ts +4 -3
  12. package/dist/api/a2a/card.js +4 -4
  13. package/dist/api/a2a/endpoint.js +3 -4
  14. package/dist/api/a2a/protocol.js +4 -0
  15. package/dist/api/a2a/routes.d.ts +2 -6
  16. package/dist/api/a2a/routes.js +30 -25
  17. package/dist/api/ag-ui/endpoint.js +2 -3
  18. package/dist/api/ag-ui/routes.js +8 -11
  19. package/dist/api/components.d.ts +6789 -3405
  20. package/dist/api/components.js +43 -30
  21. package/dist/api/gate/keys.js +7 -4
  22. package/dist/api/gate/routes.d.ts +6 -2
  23. package/dist/api/gate/routes.js +15 -11
  24. package/dist/api/http/app.js +9 -4
  25. package/dist/api/http/body.d.ts +17 -3
  26. package/dist/api/http/body.js +35 -7
  27. package/dist/api/http/respond.js +6 -2
  28. package/dist/api/http/routes/access.js +5 -4
  29. package/dist/api/http/routes/agent-manifests.d.ts +10 -0
  30. package/dist/api/http/routes/agent-manifests.js +135 -0
  31. package/dist/api/http/routes/agents.d.ts +11 -0
  32. package/dist/api/http/routes/agents.js +418 -0
  33. package/dist/api/http/routes/artifacts.js +21 -9
  34. package/dist/api/http/routes/coverage.d.ts +4 -4
  35. package/dist/api/http/routes/coverage.js +13 -7
  36. package/dist/api/http/routes/files.d.ts +2 -1
  37. package/dist/api/http/routes/files.js +31 -5
  38. package/dist/api/http/routes/mcp.js +3 -3
  39. package/dist/api/http/routes/me.d.ts +1 -1
  40. package/dist/api/http/routes/me.js +1 -2
  41. package/dist/api/http/routes/memories.d.ts +9 -0
  42. package/dist/api/http/routes/memories.js +152 -0
  43. package/dist/api/http/routes/reads.d.ts +10 -111
  44. package/dist/api/http/routes/reads.js +51 -29
  45. package/dist/api/http/routes/sessions.js +26 -54
  46. package/dist/api/http/routes/tenant.d.ts +2 -2
  47. package/dist/api/http/routes/tenant.js +77 -24
  48. package/dist/api/http/routes/triggers.d.ts +10 -0
  49. package/dist/api/http/routes/triggers.js +180 -0
  50. package/dist/api/inbound-files.d.ts +3 -9
  51. package/dist/api/inbound-files.js +18 -24
  52. package/dist/api/openapi.js +73 -47
  53. package/dist/artifacts/disk.d.ts +10 -14
  54. package/dist/artifacts/disk.js +99 -95
  55. package/dist/artifacts/files.d.ts +13 -4
  56. package/dist/artifacts/files.js +27 -9
  57. package/dist/artifacts/folder-upload.d.ts +75 -0
  58. package/dist/artifacts/folder-upload.js +263 -0
  59. package/dist/artifacts/folders.d.ts +14 -8
  60. package/dist/artifacts/folders.js +19 -11
  61. package/dist/artifacts/links.js +7 -6
  62. package/dist/artifacts/parts.d.ts +9 -14
  63. package/dist/artifacts/parts.js +11 -9
  64. package/dist/artifacts/pipe.d.ts +15 -0
  65. package/dist/artifacts/pipe.js +71 -0
  66. package/dist/artifacts/quota.d.ts +36 -0
  67. package/dist/artifacts/quota.js +44 -0
  68. package/dist/artifacts/service.d.ts +25 -1
  69. package/dist/artifacts/service.js +72 -27
  70. package/dist/blob/fs.js +2 -1
  71. package/dist/blob/s3.d.ts +6 -2
  72. package/dist/blob/s3.js +9 -7
  73. package/dist/blob/types.d.ts +1 -1
  74. package/dist/blob/types.js +1 -1
  75. package/dist/core/flow-host.d.ts +0 -1
  76. package/dist/core/limits.d.ts +11 -35
  77. package/dist/core/limits.js +5 -73
  78. package/dist/core/sandbox-routes.js +0 -1
  79. package/dist/core/turn-manifest.d.ts +1 -2
  80. package/dist/core/turn-manifest.js +4 -28
  81. package/dist/execution/memory.d.ts +8 -5
  82. package/dist/execution/memory.js +8 -6
  83. package/dist/execution/types.d.ts +1 -6
  84. package/dist/execution/types.js +0 -5
  85. package/dist/gates/contract.d.ts +3 -2
  86. package/dist/gates/contract.js +1 -1
  87. package/dist/gates/egress-credentials.js +5 -4
  88. package/dist/gates/egress.d.ts +13 -13
  89. package/dist/gates/egress.js +19 -21
  90. package/dist/gates/fixture-model.js +13 -2
  91. package/dist/gates/handler.d.ts +1 -3
  92. package/dist/gates/handler.js +4 -3
  93. package/dist/gates/http-client.d.ts +4 -3
  94. package/dist/gates/http-client.js +5 -7
  95. package/dist/gates/http-tool.d.ts +24 -4
  96. package/dist/gates/http-tool.js +41 -13
  97. package/dist/gates/in-process.d.ts +2 -1
  98. package/dist/gates/in-process.js +17 -9
  99. package/dist/gates/inflight.d.ts +2 -2
  100. package/dist/gates/inflight.js +3 -2
  101. package/dist/gates/journal.js +5 -7
  102. package/dist/gates/mcp-handler.d.ts +4 -3
  103. package/dist/gates/mcp-handler.js +15 -5
  104. package/dist/gates/meter.js +6 -5
  105. package/dist/gates/model-gate.d.ts +11 -2
  106. package/dist/gates/policy.d.ts +34 -6
  107. package/dist/gates/policy.js +66 -18
  108. package/dist/gates/provider-gate.d.ts +22 -0
  109. package/dist/gates/provider-gate.js +30 -0
  110. package/dist/gates/tool-calls.d.ts +1 -3
  111. package/dist/gates/tool-calls.js +2 -3
  112. package/dist/gates/tool-client.d.ts +1 -1
  113. package/dist/gates/tool-client.js +5 -6
  114. package/dist/gates/tool-contract.d.ts +8 -5
  115. package/dist/gates/tool-contract.js +1 -1
  116. package/dist/gates/tool-gate.d.ts +18 -7
  117. package/dist/gates/tool-gate.js +49 -7
  118. package/dist/harness/calls.d.ts +27 -25
  119. package/dist/harness/calls.js +40 -46
  120. package/dist/harness/executors.d.ts +10 -3
  121. package/dist/harness/executors.js +53 -23
  122. package/dist/harness/main.js +2 -2
  123. package/dist/harness/pod.d.ts +2 -2
  124. package/dist/harness/pod.js +7 -6
  125. package/dist/harness/run-tokens.d.ts +4 -1
  126. package/dist/harness/run-tokens.js +6 -2
  127. package/dist/harness/service.d.ts +17 -14
  128. package/dist/harness/service.js +21 -35
  129. package/dist/harness/setup.d.ts +0 -2
  130. package/dist/harness/setup.js +21 -15
  131. package/dist/harness/ws-client.d.ts +2 -2
  132. package/dist/harness/ws-client.js +7 -5
  133. package/dist/harness/ws-port.d.ts +3 -4
  134. package/dist/harness/ws-port.js +3 -4
  135. package/dist/harness-api/artifacts.d.ts +20 -0
  136. package/dist/harness-api/artifacts.js +478 -0
  137. package/dist/harness-api/in-process.d.ts +2 -2
  138. package/dist/harness-api/in-process.js +18 -16
  139. package/dist/harness-api/record.d.ts +10 -9
  140. package/dist/harness-api/record.js +113 -157
  141. package/dist/harness-api/renew.d.ts +1 -1
  142. package/dist/harness-api/renew.js +2 -2
  143. package/dist/harness-api/server.d.ts +1 -16
  144. package/dist/harness-api/server.js +86 -73
  145. package/dist/harness-api/start.d.ts +27 -5
  146. package/dist/harness-api/start.js +102 -19
  147. package/dist/harness-api/tool-journal.d.ts +15 -10
  148. package/dist/harness-api/tool-journal.js +26 -24
  149. package/dist/harness-api/workspace.d.ts +0 -4
  150. package/dist/harness-api/workspace.js +4 -47
  151. package/dist/harness-api/ws-server.js +10 -10
  152. package/dist/host/create-host.js +2 -3
  153. package/dist/host/execution.js +2 -3
  154. package/dist/host/gates.d.ts +2 -9
  155. package/dist/host/gates.js +15 -12
  156. package/dist/host/http.d.ts +2 -4
  157. package/dist/host/http.js +5 -29
  158. package/dist/host/main.js +10 -3
  159. package/dist/host/operate.js +12 -3
  160. package/dist/host/stack-config.d.ts +11 -0
  161. package/dist/host/stack-config.js +21 -1
  162. package/dist/index.d.ts +0 -1
  163. package/dist/index.js +0 -1
  164. package/dist/infra/index.js +2 -1
  165. package/dist/infra/readiness.d.ts +0 -2
  166. package/dist/infra/readiness.js +4 -3
  167. package/dist/keys/client.d.ts +1 -0
  168. package/dist/keys/client.js +2 -3
  169. package/dist/keys/contract.d.ts +0 -2
  170. package/dist/keys/contract.js +0 -2
  171. package/dist/keys/egress-ca.d.ts +0 -2
  172. package/dist/keys/egress-ca.js +2 -3
  173. package/dist/keys/keys.d.ts +15 -4
  174. package/dist/keys/keys.js +22 -1
  175. package/dist/keys/webhooks.d.ts +25 -0
  176. package/dist/keys/webhooks.js +62 -0
  177. package/dist/keys/x509.d.ts +8 -4
  178. package/dist/keys/x509.js +14 -5
  179. package/dist/management-openapi.json +3043 -1954
  180. package/dist/mcp/connect.d.ts +35 -26
  181. package/dist/mcp/connect.js +112 -141
  182. package/dist/mcp/pool.d.ts +27 -22
  183. package/dist/mcp/pool.js +34 -48
  184. package/dist/mcp/preview.d.ts +12 -5
  185. package/dist/mcp/preview.js +43 -26
  186. package/dist/mcp/snapshot.d.ts +35 -86
  187. package/dist/mcp/snapshot.js +93 -135
  188. package/dist/memory/service.d.ts +97 -0
  189. package/dist/memory/service.js +335 -0
  190. package/dist/memory/tool.d.ts +36 -0
  191. package/dist/memory/tool.js +84 -0
  192. package/dist/model/auth-store.js +5 -3
  193. package/dist/model/capabilities.d.ts +1 -1
  194. package/dist/model/catalog.d.ts +9 -36
  195. package/dist/model/catalog.js +6 -33
  196. package/dist/model/descriptor.d.ts +51 -0
  197. package/dist/model/descriptor.js +83 -0
  198. package/dist/model/models.d.ts +0 -3
  199. package/dist/model/models.js +3 -5
  200. package/dist/model/native-blocks.js +1 -1
  201. package/dist/model/pi-model.d.ts +16 -5
  202. package/dist/model/pi-model.js +86 -105
  203. package/dist/openapi.json +13653 -9986
  204. package/dist/reads/cursor.js +3 -1
  205. package/dist/reads/types.d.ts +3 -3
  206. package/dist/record/agent.d.ts +27 -0
  207. package/dist/record/agent.js +46 -0
  208. package/dist/record/append.d.ts +34 -2
  209. package/dist/record/append.js +50 -1
  210. package/dist/record/index.d.ts +2 -2
  211. package/dist/record/index.js +2 -2
  212. package/dist/redact.d.ts +1 -1
  213. package/dist/redact.js +3 -2
  214. package/dist/sandbox/egress-token.d.ts +3 -3
  215. package/dist/sandbox/egress-token.js +3 -3
  216. package/dist/sandbox/join.js +8 -7
  217. package/dist/sandbox/manager.d.ts +41 -41
  218. package/dist/sandbox/manager.js +112 -44
  219. package/dist/sandbox/placement.d.ts +2 -2
  220. package/dist/sandbox/placement.js +7 -10
  221. package/dist/sandbox/pods/client.d.ts +1 -10
  222. package/dist/sandbox/pods/client.js +8 -4
  223. package/dist/sandbox/pods/lifecycle.d.ts +25 -17
  224. package/dist/sandbox/pods/lifecycle.js +51 -29
  225. package/dist/sandbox/pods/network-gate.d.ts +4 -5
  226. package/dist/sandbox/pods/network-gate.js +7 -7
  227. package/dist/sandbox/pods/reconcile.js +14 -3
  228. package/dist/sandbox/pods/spec.js +8 -5
  229. package/dist/sandbox/policy.d.ts +7 -1
  230. package/dist/sandbox/policy.js +10 -3
  231. package/dist/sandbox/resolve.d.ts +11 -6
  232. package/dist/sandbox/resolve.js +19 -9
  233. package/dist/sandbox/setup.d.ts +6 -2
  234. package/dist/sandbox/setup.js +47 -14
  235. package/dist/sandbox/share.d.ts +5 -14
  236. package/dist/sandbox/share.js +5 -34
  237. package/dist/sandbox/skills.d.ts +7 -13
  238. package/dist/sandbox/skills.js +16 -31
  239. package/dist/sandbox/tenant-config.d.ts +2 -2
  240. package/dist/sandbox/tenant-config.js +4 -3
  241. package/dist/sandbox/tools.d.ts +7 -1
  242. package/dist/sandbox/tools.js +26 -26
  243. package/dist/sandbox/types.d.ts +10 -3
  244. package/dist/sandbox/types.js +9 -0
  245. package/dist/store/postgres/connect.d.ts +6 -5
  246. package/dist/store/postgres/connect.js +14 -11
  247. package/dist/store/postgres/control.js +13 -13
  248. package/dist/store/postgres/drizzle/0028_artifact_write_keys.sql +2 -0
  249. package/dist/store/postgres/drizzle/0029_protocol_15.sql +11 -0
  250. package/dist/store/postgres/drizzle/0030_protocol_15_runtime_block.sql +11 -0
  251. package/dist/store/postgres/drizzle/0031_protocol_15_mcp_snapshot.sql +8 -0
  252. package/dist/store/postgres/drizzle/0032_agents.sql +54 -0
  253. package/dist/store/postgres/drizzle/0033_agent_versions.sql +7 -0
  254. package/dist/store/postgres/drizzle/0034_protocol_17_session_queue.sql +13 -0
  255. package/dist/store/postgres/drizzle/0035_t4_triggers.sql +41 -0
  256. package/dist/store/postgres/drizzle/0036_t5_memories.sql +30 -0
  257. package/dist/store/postgres/drizzle/meta/_journal.json +63 -0
  258. package/dist/store/postgres/reads.js +17 -82
  259. package/dist/store/postgres/record-writer.d.ts +4 -4
  260. package/dist/store/postgres/record-writer.js +9 -9
  261. package/dist/store/postgres/schema.d.ts +1382 -151
  262. package/dist/store/postgres/schema.js +191 -37
  263. package/dist/store/postgres/store.js +384 -105
  264. package/dist/store/postgres/tenant.js +1 -2
  265. package/dist/store/types.d.ts +172 -57
  266. package/dist/store/types.js +0 -1
  267. package/dist/streams/relay/core.d.ts +3 -3
  268. package/dist/streams/relay/core.js +9 -10
  269. package/dist/tenant/advance.js +41 -39
  270. package/dist/tenant/agent-manifests.d.ts +72 -0
  271. package/dist/tenant/agent-manifests.js +359 -0
  272. package/dist/tenant/agent-versions.d.ts +126 -0
  273. package/dist/tenant/agent-versions.js +389 -0
  274. package/dist/tenant/agents.d.ts +230 -0
  275. package/dist/tenant/{environments.js → agents.js} +493 -328
  276. package/dist/tenant/auth.d.ts +1 -1
  277. package/dist/tenant/auth.js +3 -3
  278. package/dist/tenant/commands.d.ts +63 -2
  279. package/dist/tenant/commands.js +240 -90
  280. package/dist/tenant/context.d.ts +57 -33
  281. package/dist/tenant/context.js +6 -2
  282. package/dist/tenant/definition-files.d.ts +25 -4
  283. package/dist/tenant/definition-files.js +85 -47
  284. package/dist/tenant/effects.js +15 -51
  285. package/dist/tenant/ephemeral.js +2 -1
  286. package/dist/tenant/history.d.ts +1 -1
  287. package/dist/tenant/history.js +1 -1
  288. package/dist/tenant/host-token.d.ts +3 -3
  289. package/dist/tenant/host-token.js +11 -10
  290. package/dist/tenant/http.d.ts +5 -8
  291. package/dist/tenant/http.js +9 -0
  292. package/dist/tenant/identity-file.d.ts +29 -6
  293. package/dist/tenant/identity-file.js +45 -15
  294. package/dist/tenant/issuers.d.ts +5 -5
  295. package/dist/tenant/issuers.js +34 -26
  296. package/dist/tenant/jwt.d.ts +8 -2
  297. package/dist/tenant/jwt.js +10 -2
  298. package/dist/tenant/limits.d.ts +87 -0
  299. package/dist/tenant/limits.js +172 -0
  300. package/dist/tenant/memories.d.ts +38 -0
  301. package/dist/tenant/memories.js +119 -0
  302. package/dist/tenant/module.d.ts +4 -1
  303. package/dist/tenant/module.js +9 -8
  304. package/dist/tenant/outbound.d.ts +11 -14
  305. package/dist/tenant/outbound.js +10 -15
  306. package/dist/tenant/reset.js +2 -2
  307. package/dist/tenant/run-grants.d.ts +6 -0
  308. package/dist/tenant/run-grants.js +7 -2
  309. package/dist/tenant/run-token.d.ts +0 -4
  310. package/dist/tenant/run-token.js +6 -11
  311. package/dist/tenant/runtime-limits.d.ts +24 -0
  312. package/dist/tenant/runtime-limits.js +27 -0
  313. package/dist/tenant/runtime.d.ts +1 -1
  314. package/dist/tenant/runtime.js +38 -43
  315. package/dist/tenant/session-streams.js +7 -6
  316. package/dist/tenant/session.d.ts +1 -1
  317. package/dist/tenant/session.js +1 -1
  318. package/dist/tenant/sessions.d.ts +28 -45
  319. package/dist/tenant/sessions.js +89 -209
  320. package/dist/tenant/signing-keys.d.ts +1 -1
  321. package/dist/tenant/signing-keys.js +5 -6
  322. package/dist/tenant/status.js +10 -10
  323. package/dist/tenant/streams.js +18 -20
  324. package/dist/tenant/sweep.d.ts +1 -21
  325. package/dist/tenant/sweep.js +51 -51
  326. package/dist/tenant/transcript.d.ts +70 -23
  327. package/dist/tenant/transcript.js +28 -22
  328. package/dist/tenant/triggers.d.ts +143 -0
  329. package/dist/tenant/triggers.js +715 -0
  330. package/dist/tenant/turn-timing.d.ts +11 -1
  331. package/dist/tenant/turn-timing.js +3 -1
  332. package/dist/tenant/types.d.ts +9 -14
  333. package/dist/tenant/worker.d.ts +4 -10
  334. package/dist/tenant/worker.js +10 -14
  335. package/dist/traces/ids.js +5 -1
  336. package/dist/traces/map.d.ts +10 -7
  337. package/dist/traces/map.js +16 -12
  338. package/dist/traces/otlp.d.ts +3 -3
  339. package/dist/traces/otlp.js +5 -3
  340. package/dist/traces/projector.d.ts +9 -4
  341. package/dist/traces/projector.js +13 -7
  342. package/dist/traces/read.js +6 -1
  343. package/dist/tuning.d.ts +390 -0
  344. package/dist/tuning.js +677 -0
  345. package/dist/vault/headers.js +4 -2
  346. package/dist/vault/service.d.ts +13 -8
  347. package/dist/vault/service.js +61 -17
  348. package/dist/version.d.ts +1 -1
  349. package/dist/version.js +1 -1
  350. package/package.json +13 -12
  351. package/dist/api/http/routes/environment-manifests.d.ts +0 -9
  352. package/dist/api/http/routes/environment-manifests.js +0 -118
  353. package/dist/api/http/routes/environments.d.ts +0 -11
  354. package/dist/api/http/routes/environments.js +0 -332
  355. package/dist/artifacts/export.d.ts +0 -8
  356. package/dist/artifacts/export.js +0 -204
  357. package/dist/artifacts/media-types.d.ts +0 -30
  358. package/dist/artifacts/media-types.js +0 -137
  359. package/dist/artifacts/workspace.d.ts +0 -60
  360. package/dist/artifacts/workspace.js +0 -45
  361. package/dist/media.d.ts +0 -10
  362. package/dist/media.js +0 -58
  363. package/dist/record/environment.d.ts +0 -25
  364. package/dist/record/environment.js +0 -44
  365. package/dist/sandbox/runtime-tools.d.ts +0 -9
  366. package/dist/sandbox/runtime-tools.js +0 -16
  367. package/dist/sandbox/session-sandbox.d.ts +0 -16
  368. package/dist/sandbox/session-sandbox.js +0 -77
  369. package/dist/tenant/artifact-tool.d.ts +0 -13
  370. package/dist/tenant/artifact-tool.js +0 -243
  371. package/dist/tenant/environment-manifests.d.ts +0 -57
  372. package/dist/tenant/environment-manifests.js +0 -200
  373. package/dist/tenant/environments.d.ts +0 -174
  374. package/dist/tenant/skill-tool.d.ts +0 -20
  375. package/dist/tenant/skill-tool.js +0 -67
  376. package/dist/tenant/tool-results.d.ts +0 -57
  377. package/dist/tenant/tool-results.js +0 -298
  378. package/dist/tenant/tool-search.d.ts +0 -39
  379. package/dist/tenant/tool-search.js +0 -103
@@ -0,0 +1,263 @@
1
+ /**
2
+ * A folder version a harness writes over the artifact seam (`artifact.write` with
3
+ * `kind: "folder"`, protocol 15): our engine's outputs folder, or any harness's. The harness
4
+ * sends each file (`file`, then `chunk`s naming it by hash), and `commit` stores the version
5
+ * (`commitFolderVersion`) with its content epoch, quotas and dedupe.
6
+ *
7
+ * - **No existence oracle.** Core takes every byte of every file. A file whose content the store
8
+ * already holds (or this upload took already) is hashed and its bytes dropped; any other is
9
+ * streamed to a key of its own, and copied to its content-addressed key (`contentKey`) only
10
+ * once its hash is checked. Answering "already stored" would let a session name content it
11
+ * does not hold; writing to the content key before the check would let a false hash replace
12
+ * another session's file.
13
+ * - **A hash that does not match** ends the upload: what it stored is collected, and the file's
14
+ * request is `invalid`.
15
+ * - **Unchanged.** A version whose files are the folder's latest is not added: `commit` answers
16
+ * that version, `unchanged`.
17
+ * - **Bounded.** At most `limits.folderEntries` files, each within the Tenant's per-file limit,
18
+ * together within its totals' limits (`commit` checks the room after dedupe, and answers
19
+ * `refused` past one).
20
+ */
21
+ import { createHash, randomUUID } from "node:crypto";
22
+ import { posix } from "node:path";
23
+ import { ARTIFACT_PATH_MAX, contentTypeFor, limitExceededMessage, normalizeContentType, } from "@nylorun/core/contracts";
24
+ import { HarnessApiError } from "@nylorun/core/harness-api";
25
+ import { collectContent, commitFolderVersion, contentKey, encodeManifest, FolderCommitRefused, sha256Hex, } from "./folders.js";
26
+ import { Pipe } from "./pipe.js";
27
+ const invalid = (message) => new HarnessApiError("invalid", message);
28
+ /** A folder file's path: relative, `/`-separated, without empty, `.` or `..` segments. */
29
+ export function checkFolderPath(path) {
30
+ const segments = path.split("/");
31
+ if (path.length > ARTIFACT_PATH_MAX ||
32
+ path.startsWith("/") ||
33
+ /[\\\u0000-\u001f\u007f]/u.test(path) ||
34
+ segments.some((segment) => segment === "" || segment === "." || segment === ".."))
35
+ throw invalid(`${JSON.stringify(path)} is not a path in a folder: relative, /-separated, no . or .. segments`);
36
+ }
37
+ export class FolderUpload {
38
+ ctx;
39
+ target;
40
+ id = randomUUID();
41
+ /** The most bytes its files may hold together, and one file. */
42
+ maxBytes;
43
+ maxFileBytes;
44
+ maxEntries;
45
+ entries = [];
46
+ paths = new Set();
47
+ /** Content taken whole and checked in this upload. */
48
+ taken = new Set();
49
+ /** Content this upload stored at its content key: collected again if nothing commits. */
50
+ stored = [];
51
+ bytes = 0;
52
+ files = 0;
53
+ current;
54
+ ended = false;
55
+ constructor(ctx, target) {
56
+ this.ctx = ctx;
57
+ this.target = target;
58
+ this.maxBytes = Math.min(...target.quota.totals.map((total) => total.limitBytes));
59
+ this.maxFileBytes = target.quota.fileBytes;
60
+ this.maxEntries = target.limits.folderEntries;
61
+ }
62
+ /** Starts a file; its bytes come with it (`base64`) or in `chunk`s. Answers the bytes received. */
63
+ async file(params, signal) {
64
+ this.live();
65
+ if (this.current)
66
+ throw invalid(`${this.current.path} has ${this.current.received} of its ${this.current.size} bytes; send the rest first`);
67
+ checkFolderPath(params.path);
68
+ if (this.paths.has(params.path))
69
+ throw invalid(`The folder has ${params.path} already`);
70
+ if (this.entries.length >= this.maxEntries)
71
+ throw new HarnessApiError("limit_exceeded", limitExceededMessage("The folder", this.entries.length + 1, "folderEntries", this.maxEntries));
72
+ if (params.size > this.maxFileBytes)
73
+ throw new HarnessApiError("limit_exceeded", `${params.path} is ${params.size} bytes; a file may hold at most ${this.maxFileBytes} (limitBytes ${this.maxFileBytes})`);
74
+ if (this.bytes + params.size > this.maxBytes)
75
+ throw new HarnessApiError("limit_exceeded", `The folder would hold ${this.bytes + params.size} bytes; it may hold at most ${this.maxBytes} (limitBytes ${this.maxBytes})`);
76
+ const contentType = normalizeContentType(params.contentType) ?? contentTypeFor(posix.basename(params.path));
77
+ const known = this.taken.has(params.sha256) || (await this.ctx.blobs.head(contentKey(params.sha256))) !== undefined;
78
+ let sink;
79
+ if (!known) {
80
+ const pipe = new Pipe();
81
+ const key = `artifacts/uploads/${this.id}/${(this.files += 1)}`;
82
+ const done = this.ctx.blobs.put(key, pipe, { contentType, maxBytes: params.size, signal });
83
+ // A failed put fails the chunk waiting on it; no rejection is lost.
84
+ done.catch((error) => pipe.fail(error));
85
+ sink = { key, pipe, done };
86
+ }
87
+ this.paths.add(params.path);
88
+ this.bytes += params.size;
89
+ this.current = {
90
+ path: params.path,
91
+ size: params.size,
92
+ sha256: params.sha256,
93
+ contentType,
94
+ hash: createHash("sha256"),
95
+ received: 0,
96
+ ...(sink ? { sink } : {}),
97
+ };
98
+ if (params.base64 !== undefined && params.base64 !== "")
99
+ return this.take(0, params.base64);
100
+ if (params.size === 0)
101
+ await this.finish();
102
+ return this.current?.received ?? 0;
103
+ }
104
+ /** Bytes of the file in progress from `offset`. Answers the bytes received of it. */
105
+ async chunk(params) {
106
+ this.live();
107
+ const current = this.current;
108
+ if (!current || params.sha256 !== current.sha256) {
109
+ // A repeat of a file's last chunk whose answer was lost: that file is complete.
110
+ if (params.sha256 !== undefined && this.taken.has(params.sha256))
111
+ return 0;
112
+ throw invalid(`No file ${params.sha256 ?? "(unnamed)"} is in progress in this upload`);
113
+ }
114
+ return this.take(params.offset, params.base64);
115
+ }
116
+ /** Stores the version, unless it is the folder's latest; collects what it stored when not. */
117
+ async commit(signal) {
118
+ this.live();
119
+ if (this.current)
120
+ throw invalid(`${this.current.path} has ${this.current.received} of its ${this.current.size} bytes`);
121
+ if (this.entries.length === 0)
122
+ throw invalid("A folder version holds at least one file");
123
+ this.ended = true;
124
+ const { target } = this;
125
+ const latest = target.prior?.latest;
126
+ if (latest !== undefined && sha256Hex(encodeManifest(this.entries)) === latest.sha256)
127
+ return { kind: "written", row: target.prior.row, version: latest, created: false, unchanged: true };
128
+ try {
129
+ signal.throwIfAborted();
130
+ const { artifact, version } = await commitFolderVersion(this.ctx, {
131
+ sessionId: target.sessionId,
132
+ turnId: target.turnId,
133
+ ...(target.prior ? { artifactId: target.prior.row.id } : {}),
134
+ name: target.name,
135
+ entries: this.entries,
136
+ epoch: target.epoch,
137
+ quota: target.quota,
138
+ source: "engine",
139
+ writeKey: target.writeKey,
140
+ });
141
+ return { kind: "written", row: artifact, version, created: true };
142
+ }
143
+ catch (error) {
144
+ await this.collect();
145
+ if (error instanceof FolderCommitRefused) {
146
+ if ((error.reason === "tenant_total" || error.reason === "environment_total") && error.limit !== undefined)
147
+ return { kind: "refused", reason: error.reason, message: error.message, limit: error.limit };
148
+ throw invalid(error.message);
149
+ }
150
+ throw error;
151
+ }
152
+ }
153
+ /** Ends the upload: nothing commits, and what it stored is collected. Never throws. */
154
+ async abort(reason) {
155
+ if (this.ended)
156
+ return;
157
+ this.ended = true;
158
+ await this.dropCurrent(reason);
159
+ await this.collect().catch(() => undefined);
160
+ }
161
+ live() {
162
+ if (this.ended)
163
+ throw invalid("The upload ended");
164
+ }
165
+ async take(offset, base64) {
166
+ const current = this.current;
167
+ const bytes = chunkOf(base64, this.target.limits);
168
+ // A repeat of the last chunk (its answer was lost) is acknowledged, not taken again.
169
+ if (current.last &&
170
+ offset === current.last.offset &&
171
+ bytes.byteLength === current.last.length &&
172
+ offset + bytes.byteLength === current.received)
173
+ return current.received;
174
+ if (offset !== current.received)
175
+ throw invalid(`The chunk starts at ${offset}; ${current.path} has ${current.received} bytes, so the next chunk starts there`);
176
+ if (current.received + bytes.byteLength > current.size)
177
+ throw invalid(`${current.path} is ${current.size} bytes; this chunk passes its end`);
178
+ current.received += bytes.byteLength;
179
+ current.last = { offset, length: bytes.byteLength };
180
+ current.hash.update(bytes);
181
+ if (current.sink) {
182
+ try {
183
+ await current.sink.pipe.push(bytes);
184
+ }
185
+ catch (error) {
186
+ await this.abort("the store refused the file");
187
+ throw error;
188
+ }
189
+ }
190
+ if (current.received === current.size)
191
+ await this.finish();
192
+ return current.received;
193
+ }
194
+ /** The file in progress is whole: check its hash, then keep its content once. */
195
+ async finish() {
196
+ const current = this.current;
197
+ this.current = undefined;
198
+ const { sink } = current;
199
+ try {
200
+ if (sink) {
201
+ sink.pipe.close();
202
+ await sink.done;
203
+ }
204
+ if (current.hash.digest("hex") !== current.sha256) {
205
+ await this.abort("a file's bytes did not match its hash");
206
+ throw invalid(`The bytes of ${current.path} do not match its sha256 ${current.sha256}`);
207
+ }
208
+ if (sink && !this.taken.has(current.sha256)) {
209
+ const key = contentKey(current.sha256);
210
+ if (!(await this.ctx.blobs.head(key))) {
211
+ const got = await this.ctx.blobs.get(sink.key);
212
+ if (!got)
213
+ throw new Error(`The upload of ${current.path} is missing from the Object store`);
214
+ this.stored.push(current.sha256);
215
+ await this.ctx.blobs.put(key, got.body, { contentType: current.contentType });
216
+ }
217
+ }
218
+ }
219
+ catch (error) {
220
+ if (!this.ended)
221
+ await this.abort("a file could not be stored");
222
+ throw error;
223
+ }
224
+ finally {
225
+ if (sink)
226
+ await this.ctx.blobs.delete(sink.key).catch(() => undefined);
227
+ }
228
+ this.taken.add(current.sha256);
229
+ this.entries.push({
230
+ path: current.path,
231
+ size: current.size,
232
+ sha256: current.sha256,
233
+ contentType: current.contentType,
234
+ });
235
+ }
236
+ async dropCurrent(reason) {
237
+ const current = this.current;
238
+ this.current = undefined;
239
+ const sink = current?.sink;
240
+ if (!sink)
241
+ return;
242
+ sink.pipe.fail(invalid(`The upload ended: ${reason}`));
243
+ await sink.done.catch(() => undefined);
244
+ await this.ctx.blobs.delete(sink.key).catch(() => undefined);
245
+ }
246
+ /** Content this upload stored that no committed version names is removed again. */
247
+ async collect() {
248
+ if (this.stored.length === 0)
249
+ return;
250
+ const shas = this.stored.splice(0);
251
+ await this.ctx.store.tx(async (t) => {
252
+ await t.lockArtifactQuota();
253
+ await collectContent(this.ctx.blobs, t, shas);
254
+ });
255
+ }
256
+ }
257
+ /** One chunk's bytes, at most the run's `chunkBytes`. */
258
+ function chunkOf(base64, limits) {
259
+ const bytes = Buffer.from(base64, "base64");
260
+ if (bytes.byteLength > limits.chunkBytes)
261
+ throw new HarnessApiError("limit_exceeded", limitExceededMessage("The chunk", bytes.byteLength, "chunkBytes", limits.chunkBytes));
262
+ return new Uint8Array(bytes.buffer, bytes.byteOffset, bytes.byteLength);
263
+ }
@@ -1,7 +1,8 @@
1
- import type { ArtifactDiff, FolderEntry, FolderManifest } from "@nylorun/core/contracts";
1
+ import type { ArtifactDiff, ArtifactSource, FolderEntry, FolderManifest } from "@nylorun/core/contracts";
2
2
  import type { BlobStore } from "../blob/index.js";
3
3
  import type { ArtifactRow, ArtifactVersionRow, Tx } from "../store/types.js";
4
4
  import { type TenantContext } from "../tenant/context.js";
5
+ import { type ArtifactQuota } from "./quota.js";
5
6
  /** Where a folder file's bytes are: content-addressed by their SHA-256. */
6
7
  export declare function contentKey(sha256: string): string;
7
8
  export declare function sha256Hex(bytes: Uint8Array): string;
@@ -32,11 +33,11 @@ export declare function contentEpoch(t: Tx): Promise<number>;
32
33
  * named them are gone: the deletes happen before the transaction commits, under the lock.
33
34
  */
34
35
  export declare function collectContent(blobs: BlobStore, t: Tx, shas: readonly string[]): Promise<number>;
35
- /** The commit could not go ahead: a limit, or content removed while the export ran. */
36
+ /** The commit could not go ahead: a limit, or content removed while the folder was written. */
36
37
  export declare class FolderCommitRefused extends Error {
37
- readonly reason: "tenant_total" | "content_gone" | "artifact_gone";
38
+ readonly reason: "tenant_total" | "environment_total" | "content_gone" | "artifact_gone";
38
39
  readonly limit?: number | undefined;
39
- constructor(message: string, reason: "tenant_total" | "content_gone" | "artifact_gone", limit?: number | undefined);
40
+ constructor(message: string, reason: "tenant_total" | "environment_total" | "content_gone" | "artifact_gone", limit?: number | undefined);
40
41
  }
41
42
  export interface FolderVersionInput {
42
43
  readonly sessionId: string;
@@ -48,14 +49,19 @@ export interface FolderVersionInput {
48
49
  readonly entries: readonly FolderEntry[];
49
50
  /** The content epoch read before the first `head` of those bytes. */
50
51
  readonly epoch: number;
51
- /** The Tenant total in force. */
52
- readonly totalBytes: number;
52
+ /** The totals the version counts towards (`./quota.ts`). */
53
+ readonly quota: ArtifactQuota;
54
+ /** Who wrote the files: a harness over the artifact seam (`engine`). */
55
+ readonly source: ArtifactSource;
56
+ /** The write's key in the session (`writeKeyOf`): a repeat of the write answers this version. */
57
+ readonly writeKey?: string;
53
58
  }
54
59
  /**
55
60
  * Commits a folder version whose files are already stored: its manifest to the Object store,
56
61
  * then, in one transaction under the session and quota locks, the version row, its content rows
57
- * and `artifact.created` or `artifact.version.created` (`claimed: true`, source `export`).
58
- * Throws `FolderCommitRefused` when the Tenant total would be passed or a file is gone.
62
+ * and `artifact.created` or `artifact.version.created` (`claimed: true`, with its source).
63
+ * Throws `FolderCommitRefused` when a total (the Tenant's, or the session's Environment's) would
64
+ * be passed or a file is gone.
59
65
  */
60
66
  export declare function commitFolderVersion(ctx: TenantContext, input: FolderVersionInput): Promise<{
61
67
  artifact: ArtifactRow;
@@ -22,6 +22,7 @@ import { FOLDER_CONTENT_TYPE, FOLDER_MANIFEST_FORMAT, FolderManifestSchema, } fr
22
22
  import { newArtifactId } from "@nylorun/core/compatibility";
23
23
  import { lockedSession } from "../tenant/context.js";
24
24
  import { fail } from "../tenant/http.js";
25
+ import { totalSubject } from "./quota.js";
25
26
  const CONTENT_EPOCH_SETTING = "artifacts.content_epoch";
26
27
  /** Where a folder file's bytes are: content-addressed by their SHA-256. */
27
28
  export function contentKey(sha256) {
@@ -111,7 +112,7 @@ export async function collectContent(blobs, t, shas) {
111
112
  await blobs.delete(contentKey(sha));
112
113
  return orphans.length;
113
114
  }
114
- /** The commit could not go ahead: a limit, or content removed while the export ran. */
115
+ /** The commit could not go ahead: a limit, or content removed while the folder was written. */
115
116
  export class FolderCommitRefused extends Error {
116
117
  reason;
117
118
  limit;
@@ -125,8 +126,9 @@ export class FolderCommitRefused extends Error {
125
126
  /**
126
127
  * Commits a folder version whose files are already stored: its manifest to the Object store,
127
128
  * then, in one transaction under the session and quota locks, the version row, its content rows
128
- * and `artifact.created` or `artifact.version.created` (`claimed: true`, source `export`).
129
- * Throws `FolderCommitRefused` when the Tenant total would be passed or a file is gone.
129
+ * and `artifact.created` or `artifact.version.created` (`claimed: true`, with its source).
130
+ * Throws `FolderCommitRefused` when a total (the Tenant's, or the session's Environment's) would
131
+ * be passed or a file is gone.
130
132
  */
131
133
  export async function commitFolderVersion(ctx, input) {
132
134
  const entries = sortEntries(input.entries);
@@ -151,11 +153,16 @@ export async function commitFolderVersion(ctx, input) {
151
153
  if ((await contentEpoch(t)) !== input.epoch)
152
154
  for (const sha of fresh)
153
155
  if (!(await ctx.blobs.head(contentKey(sha))))
154
- throw new FolderCommitRefused("A file's bytes were removed while the export ran", "content_gone");
155
- const added = fresh.reduce((sum, sha) => sum + files.get(sha), 0);
156
- const total = await t.artifactBytes();
157
- if (total + added > input.totalBytes)
158
- throw new FolderCommitRefused(`The Tenant's artifacts may hold at most ${input.totalBytes} bytes; these outputs add ${added} to ${total}`, "tenant_total", input.totalBytes);
156
+ throw new FolderCommitRefused("A file's bytes were removed while the folder was written", "content_gone");
157
+ for (const total of input.quota.totals) {
158
+ // A file adds bytes to a total unless an artifact it counts already holds the file.
159
+ const scope = total.environmentId === undefined ? undefined : { environmentId: total.environmentId };
160
+ const held = scope === undefined ? referenced : await t.referencedArtifactContent(shas, scope);
161
+ const added = shas.filter((sha) => !held.has(sha)).reduce((sum, sha) => sum + files.get(sha), 0);
162
+ const used = await t.artifactBytes(scope);
163
+ if (used + added > total.limitBytes)
164
+ throw new FolderCommitRefused(`${totalSubject(total)} may hold at most ${total.limitBytes} bytes; this folder version adds ${added} to ${used}`, scope === undefined ? "tenant_total" : "environment_total", total.limitBytes);
165
+ }
159
166
  let row;
160
167
  const version = {
161
168
  artifactId,
@@ -164,13 +171,14 @@ export async function commitFolderVersion(ctx, input) {
164
171
  size,
165
172
  sha256,
166
173
  contentType: FOLDER_CONTENT_TYPE,
167
- source: "export",
174
+ source: input.source,
168
175
  createdAt,
176
+ writeKey: input.writeKey ?? null,
169
177
  };
170
178
  if (input.artifactId !== undefined) {
171
179
  const prior = await t.artifact(input.artifactId, { lock: true });
172
180
  if (!prior || prior.kind !== "folder" || prior.sessionId !== input.sessionId)
173
- throw new FolderCommitRefused("The folder was deleted while the export ran", "artifact_gone");
181
+ throw new FolderCommitRefused("The folder was deleted while it was written", "artifact_gone");
174
182
  version.version = prior.latestVersion + 1;
175
183
  await t.insertArtifactVersion(version);
176
184
  row = { ...prior, latestVersion: version.version, contentType: FOLDER_CONTENT_TYPE, updatedAt: createdAt };
@@ -199,7 +207,7 @@ export async function commitFolderVersion(ctx, input) {
199
207
  version: version.version,
200
208
  size,
201
209
  sha256,
202
- source: "export",
210
+ source: input.source,
203
211
  fileCount: entries.length,
204
212
  claimed: true,
205
213
  });
@@ -3,22 +3,23 @@
3
3
  * other credential, for UIs, `<img>` tags and sharing. The token in the path is an ES256 JWT
4
4
  * (`typ: nylorun-artifact+jwt`) signed with the Tenant's current signing key by the keys
5
5
  * service, like run tokens: `sub` is the artifact, `ver` the version, `aud`
6
- * `nylorun-artifact`, and it lives at most `ARTIFACT_LINK_MAX_TTL_SECONDS` (`TOKEN_TTL_MAX_SECONDS`,
6
+ * `nylorun-artifact`, and it lives at most `keys.tokenTtlMaxSeconds` (Host tuning,
7
7
  * so a key rotation never revokes a live link). A link opens nothing once its artifact
8
8
  * is deleted. A folder's link (F8.2) opens its zip, or with a `path` claim one of its files.
9
9
  */
10
10
  import { randomUUID } from "node:crypto";
11
11
  import { errors, jwtVerify } from "jose";
12
- import { ARTIFACT_LINK_DEFAULT_TTL_SECONDS, ARTIFACT_LINK_MAX_TTL_SECONDS, ARTIFACT_LINK_TOKEN_TYPE, tenantTokenIssuer, } from "@nylorun/core/contracts";
12
+ import { ARTIFACT_LINK_DEFAULT_TTL_SECONDS, ARTIFACT_LINK_TOKEN_TYPE, tenantTokenIssuer, } from "@nylorun/core/contracts";
13
13
  import { isArtifactId } from "@nylorun/core/compatibility";
14
14
  import { failOpaque, HttpError } from "../tenant/http.js";
15
- import { CLOCK_TOLERANCE_SECONDS, tokenKeyId } from "../tenant/jwt.js";
15
+ import { clockToleranceSeconds, tokenKeyId } from "../tenant/jwt.js";
16
+ import { hostTuning } from "../tuning.js";
16
17
  /** The audience of every capability link. */
17
18
  export const ARTIFACT_LINK_AUDIENCE = "nylorun-artifact";
18
19
  /** Where a capability link is served: `GET /v1/artifact-links/<token>`. */
19
20
  export const ARTIFACT_LINK_PATH = "/v1/artifact-links";
20
21
  export async function mintArtifactLink(ctx, link) {
21
- const ttl = Math.max(1, Math.min(Math.floor(link.expiresIn ?? ARTIFACT_LINK_DEFAULT_TTL_SECONDS), ARTIFACT_LINK_MAX_TTL_SECONDS));
22
+ const ttl = Math.max(1, Math.min(Math.floor(link.expiresIn ?? ARTIFACT_LINK_DEFAULT_TTL_SECONDS), hostTuning().keys.tokenTtlMaxSeconds));
22
23
  const iat = Math.floor(Date.now() / 1000);
23
24
  // Signed by the keys service (F4.2): this process never holds the private key.
24
25
  const { token } = await ctx.keys.sign({
@@ -60,7 +61,7 @@ export async function verifyArtifactLink(ctx, raw) {
60
61
  issuer: tenantTokenIssuer(ctx.config.tenantId),
61
62
  audience: ARTIFACT_LINK_AUDIENCE,
62
63
  typ: ARTIFACT_LINK_TOKEN_TYPE,
63
- clockTolerance: CLOCK_TOLERANCE_SECONDS,
64
+ clockTolerance: clockToleranceSeconds(),
64
65
  requiredClaims: ["sub", "aud", "iat", "exp", "jti"],
65
66
  });
66
67
  claims = verified.payload;
@@ -78,7 +79,7 @@ export async function verifyArtifactLink(ctx, raw) {
78
79
  !Number.isInteger(ver) ||
79
80
  ver < 1 ||
80
81
  (path !== undefined && (typeof path !== "string" || path === "")) ||
81
- exp - iat > ARTIFACT_LINK_MAX_TTL_SECONDS)
82
+ exp - iat > hostTuning().keys.tokenTtlMaxSeconds)
82
83
  return refused(ctx, "artifact_link_claims");
83
84
  return { artifactId: sub, version: ver, ...(typeof path === "string" ? { file: path } : {}) };
84
85
  }
@@ -2,10 +2,12 @@
2
2
  * Message `parts` (protocol 6): what a user message's text and file parts become for the
3
3
  * engine. A file part names an artifact the caller may read, of this session or Tenant-wide;
4
4
  * its version is pinned when the message is accepted (the latest when the part names none).
5
- * The engine gets an opaque media part whose `reference` is `{ artifactId, version }`: model-gate
6
- * resolves it to bytes (`./files.ts`), and the transcript and the record never hold them.
5
+ * The engine gets an opaque media part whose `reference` is `{ artifactId, version, name, size }`:
6
+ * the harness decides how the model sees it, the Model Gate resolves it to bytes (`./files.ts`),
7
+ * and the transcript and the record never hold them. Any file is accepted, whatever the model
8
+ * reads (protocol 15): its size was held to the artifact limits at upload.
7
9
  */
8
- import type { MessagePart, ModelInput } from "@nylorun/core/contracts";
10
+ import type { MessagePart } from "@nylorun/core/contracts";
9
11
  import type { JsonObject, JsonValue } from "@nylorun/core/define";
10
12
  import type { Tx } from "../store/types.js";
11
13
  import { type ArtifactAccess } from "./service.js";
@@ -24,20 +26,13 @@ export interface FileReference {
24
26
  readonly version: number;
25
27
  }
26
28
  export declare function fileReferenceOf(value: JsonValue | undefined): FileReference | undefined;
27
- /** A file part the Tenant's model does not read (protocol 12, `attachment_unsupported`). */
28
- export interface RefusedPart {
29
- readonly artifactId: string;
30
- readonly name: string;
31
- readonly mediaType: string;
32
- readonly reason: string;
33
- }
34
29
  /**
35
30
  * The parts with each file pinned to a version: for the engine (`engine`) and for a workflow's
36
- * JSON input (`pinned`). Reads in the caller's transaction. With `inputs`, what the Tenant's model
37
- * reads, each file of a type it does not read is in `refused` (its type only: no size is checked).
31
+ * JSON input (`pinned`). Reads in the caller's transaction. A file part must name a file the
32
+ * caller may read, of this session or Tenant-wide, and a version that exists. A message carries at
33
+ * most the Tenant's Runtime limit `messageParts` of them.
38
34
  */
39
- export declare function resolveMessageParts(t: Tx, parts: readonly MessagePart[], sessionId: string, access: ArtifactAccess, inputs?: readonly ModelInput[]): Promise<{
35
+ export declare function resolveMessageParts(t: Tx, parts: readonly MessagePart[], sessionId: string, access: ArtifactAccess): Promise<{
40
36
  engine: EnginePart[];
41
37
  pinned: MessagePart[];
42
- refused: RefusedPart[];
43
38
  }>;
@@ -1,5 +1,5 @@
1
1
  import { fail } from "../tenant/http.js";
2
- import { unreadableReason } from "./media-types.js";
2
+ import { readRuntimeLimits } from "../tenant/runtime-limits.js";
3
3
  import { readableArtifact, versionOf } from "./service.js";
4
4
  export function fileReferenceOf(value) {
5
5
  if (!value || typeof value !== "object" || Array.isArray(value))
@@ -11,13 +11,18 @@ export function fileReferenceOf(value) {
11
11
  }
12
12
  /**
13
13
  * The parts with each file pinned to a version: for the engine (`engine`) and for a workflow's
14
- * JSON input (`pinned`). Reads in the caller's transaction. With `inputs`, what the Tenant's model
15
- * reads, each file of a type it does not read is in `refused` (its type only: no size is checked).
14
+ * JSON input (`pinned`). Reads in the caller's transaction. A file part must name a file the
15
+ * caller may read, of this session or Tenant-wide, and a version that exists. A message carries at
16
+ * most the Tenant's Runtime limit `messageParts` of them.
16
17
  */
17
- export async function resolveMessageParts(t, parts, sessionId, access, inputs) {
18
+ export async function resolveMessageParts(t, parts, sessionId, access) {
19
+ const { messageParts } = await readRuntimeLimits(t);
20
+ if (parts.length > messageParts)
21
+ fail(400, `A message may carry at most ${messageParts} parts (the Runtime limit messageParts)`, {
22
+ code: "invalid_request",
23
+ });
18
24
  const engine = [];
19
25
  const pinned = [];
20
- const refused = [];
21
26
  for (const part of parts) {
22
27
  if (part.type === "text") {
23
28
  engine.push({ type: "text", text: part.text });
@@ -30,9 +35,6 @@ export async function resolveMessageParts(t, parts, sessionId, access, inputs) {
30
35
  if (artifact.kind !== "file")
31
36
  fail(400, `Artifact ${artifact.id} is a folder; a file part names a file artifact`);
32
37
  const version = await versionOf(t, artifact, part.version);
33
- const reason = inputs && unreadableReason(version.contentType, inputs);
34
- if (reason)
35
- refused.push({ artifactId: artifact.id, name: artifact.name, mediaType: version.contentType, reason });
36
38
  engine.push({
37
39
  type: "media",
38
40
  mediaType: version.contentType,
@@ -45,5 +47,5 @@ export async function resolveMessageParts(t, parts, sessionId, access, inputs) {
45
47
  });
46
48
  pinned.push({ type: "file", artifactId: artifact.id, version: version.version });
47
49
  }
48
- return { engine, pinned, refused };
50
+ return { engine, pinned };
49
51
  }
@@ -0,0 +1,15 @@
1
+ /**
2
+ * The body of an upload: chunks handed over one at a time. `push` resolves once the store took
3
+ * the chunk, so a harness's next chunk waits for the store (backpressure across frames, nothing
4
+ * held whole); `close` ends the body, `fail` ends it with an error on both sides.
5
+ */
6
+ export declare class Pipe implements AsyncIterable<Uint8Array> {
7
+ private taker?;
8
+ private offered?;
9
+ private ended;
10
+ private error?;
11
+ push(bytes: Uint8Array): Promise<void>;
12
+ close(): void;
13
+ fail(error: unknown): void;
14
+ [Symbol.asyncIterator](): AsyncIterator<Uint8Array>;
15
+ }
@@ -0,0 +1,71 @@
1
+ /**
2
+ * An upload's body as it crosses the artifact seam (protocol 15): chunks handed over one at a
3
+ * time, so nothing is held whole.
4
+ */
5
+ import { HarnessApiError } from "@nylorun/core/harness-api";
6
+ /**
7
+ * The body of an upload: chunks handed over one at a time. `push` resolves once the store took
8
+ * the chunk, so a harness's next chunk waits for the store (backpressure across frames, nothing
9
+ * held whole); `close` ends the body, `fail` ends it with an error on both sides.
10
+ */
11
+ export class Pipe {
12
+ taker;
13
+ offered;
14
+ ended = false;
15
+ error;
16
+ push(bytes) {
17
+ if (this.error)
18
+ return Promise.reject(this.error.value);
19
+ if (this.ended)
20
+ return Promise.reject(new HarnessApiError("invalid", "The upload is committing"));
21
+ if (this.taker) {
22
+ const taker = this.taker;
23
+ this.taker = undefined;
24
+ taker.resolve({ value: bytes, done: false });
25
+ return Promise.resolve();
26
+ }
27
+ return new Promise((taken, reject) => {
28
+ this.offered = { bytes, taken, reject };
29
+ });
30
+ }
31
+ close() {
32
+ this.ended = true;
33
+ if (this.taker && !this.offered) {
34
+ const taker = this.taker;
35
+ this.taker = undefined;
36
+ taker.resolve({ value: undefined, done: true });
37
+ }
38
+ }
39
+ fail(error) {
40
+ if (this.error)
41
+ return;
42
+ this.error = { value: error };
43
+ this.offered?.reject(error);
44
+ this.offered = undefined;
45
+ this.taker?.reject(error);
46
+ this.taker = undefined;
47
+ }
48
+ [Symbol.asyncIterator]() {
49
+ return {
50
+ next: () => {
51
+ if (this.error)
52
+ return Promise.reject(this.error.value);
53
+ if (this.offered) {
54
+ const { bytes, taken } = this.offered;
55
+ this.offered = undefined;
56
+ taken();
57
+ return Promise.resolve({ value: bytes, done: false });
58
+ }
59
+ if (this.ended)
60
+ return Promise.resolve({ value: undefined, done: true });
61
+ return new Promise((resolve, reject) => {
62
+ this.taker = { resolve, reject };
63
+ });
64
+ },
65
+ return: () => {
66
+ this.fail(new HarnessApiError("invalid", "The store stopped reading the upload"));
67
+ return Promise.resolve({ value: undefined, done: true });
68
+ },
69
+ };
70
+ }
71
+ }
@@ -0,0 +1,36 @@
1
+ /**
2
+ * What one artifact write must fit (`tenant/limits.ts`): the largest file, and each total its
3
+ * bytes count towards. The Tenant's total always applies; an artifact of an Environment (one of
4
+ * its sessions' artifacts, or one of its disk versions) also counts towards that Environment's
5
+ * total when its manifest sets `limits.artifacts.totalBytes`. Without one, the Environment's
6
+ * total is the Tenant's, which the Tenant's own check already enforces.
7
+ */
8
+ import type { Tx } from "../store/types.js";
9
+ /** One total a write counts towards: the Tenant's (no `environmentId`), or an Environment's. */
10
+ export interface ArtifactTotal {
11
+ readonly environmentId?: string;
12
+ readonly limitBytes: number;
13
+ }
14
+ export interface ArtifactQuota {
15
+ /** The largest file the write may store. */
16
+ readonly fileBytes: number;
17
+ /** The Tenant's total first, then the Environment's when it sets one. */
18
+ readonly totals: readonly ArtifactTotal[];
19
+ }
20
+ /** A total, with the bytes it holds now. */
21
+ export interface ArtifactUsage {
22
+ readonly total: ArtifactTotal;
23
+ readonly usedBytes: number;
24
+ }
25
+ /** The quota for an artifact of `environmentId`, or of the Tenant alone when undefined. */
26
+ export declare function artifactQuota(t: Tx, environmentId: string | undefined): Promise<ArtifactQuota>;
27
+ /** The bytes each of the quota's totals holds now. */
28
+ export declare function artifactUsage(t: Tx, quota: ArtifactQuota): Promise<ArtifactUsage[]>;
29
+ /** The total with the least room left, and that room (none left: zero or less). */
30
+ export declare function tightest(usage: readonly ArtifactUsage[]): ArtifactUsage & {
31
+ readonly roomBytes: number;
32
+ };
33
+ /** Whose artifacts a total counts: `The Tenant's artifacts`, or `Agent <id>'s artifacts`. */
34
+ export declare function totalSubject(total: ArtifactTotal): string;
35
+ /** The Environment a session's artifacts belong to, if it names one. */
36
+ export declare function environmentOfSession(t: Tx, sessionId: string | null): Promise<string | undefined>;