@nylorun/runtime 0.26.0-beta → 0.27.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 (321) hide show
  1. package/CHANGELOG.md +130 -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.js +1 -1
  12. package/dist/api/a2a/endpoint.js +3 -4
  13. package/dist/api/a2a/protocol.js +4 -0
  14. package/dist/api/a2a/routes.d.ts +2 -6
  15. package/dist/api/a2a/routes.js +12 -18
  16. package/dist/api/ag-ui/endpoint.js +2 -3
  17. package/dist/api/ag-ui/routes.js +4 -4
  18. package/dist/api/components.d.ts +326 -45
  19. package/dist/api/components.js +5 -1
  20. package/dist/api/gate/keys.js +7 -4
  21. package/dist/api/gate/routes.d.ts +6 -2
  22. package/dist/api/gate/routes.js +15 -11
  23. package/dist/api/http/body.d.ts +15 -3
  24. package/dist/api/http/body.js +15 -7
  25. package/dist/api/http/respond.js +6 -2
  26. package/dist/api/http/routes/access.js +5 -4
  27. package/dist/api/http/routes/artifacts.js +19 -7
  28. package/dist/api/http/routes/coverage.js +1 -1
  29. package/dist/api/http/routes/environment-manifests.js +1 -1
  30. package/dist/api/http/routes/environments.js +6 -6
  31. package/dist/api/http/routes/files.d.ts +2 -1
  32. package/dist/api/http/routes/files.js +31 -5
  33. package/dist/api/http/routes/mcp.js +1 -1
  34. package/dist/api/http/routes/reads.d.ts +35 -0
  35. package/dist/api/http/routes/reads.js +50 -22
  36. package/dist/api/http/routes/sessions.js +11 -10
  37. package/dist/api/http/routes/tenant.d.ts +2 -2
  38. package/dist/api/http/routes/tenant.js +69 -16
  39. package/dist/api/inbound-files.d.ts +3 -9
  40. package/dist/api/inbound-files.js +18 -24
  41. package/dist/api/openapi.js +6 -2
  42. package/dist/artifacts/disk.d.ts +0 -4
  43. package/dist/artifacts/disk.js +77 -73
  44. package/dist/artifacts/files.d.ts +13 -4
  45. package/dist/artifacts/files.js +27 -9
  46. package/dist/artifacts/folder-upload.d.ts +75 -0
  47. package/dist/artifacts/folder-upload.js +263 -0
  48. package/dist/artifacts/folders.d.ts +14 -8
  49. package/dist/artifacts/folders.js +19 -11
  50. package/dist/artifacts/links.js +7 -6
  51. package/dist/artifacts/parts.d.ts +9 -14
  52. package/dist/artifacts/parts.js +11 -9
  53. package/dist/artifacts/pipe.d.ts +15 -0
  54. package/dist/artifacts/pipe.js +71 -0
  55. package/dist/artifacts/quota.d.ts +36 -0
  56. package/dist/artifacts/quota.js +44 -0
  57. package/dist/artifacts/service.d.ts +25 -1
  58. package/dist/artifacts/service.js +70 -25
  59. package/dist/blob/fs.js +2 -1
  60. package/dist/blob/s3.d.ts +6 -2
  61. package/dist/blob/s3.js +9 -7
  62. package/dist/blob/types.d.ts +1 -1
  63. package/dist/blob/types.js +1 -1
  64. package/dist/core/limits.d.ts +11 -35
  65. package/dist/core/limits.js +5 -73
  66. package/dist/core/sandbox-routes.js +0 -1
  67. package/dist/core/turn-manifest.d.ts +1 -2
  68. package/dist/core/turn-manifest.js +4 -28
  69. package/dist/execution/memory.d.ts +8 -5
  70. package/dist/execution/memory.js +8 -6
  71. package/dist/execution/types.d.ts +1 -6
  72. package/dist/execution/types.js +0 -5
  73. package/dist/gates/contract.d.ts +3 -2
  74. package/dist/gates/contract.js +1 -1
  75. package/dist/gates/egress-credentials.js +4 -3
  76. package/dist/gates/egress.d.ts +9 -9
  77. package/dist/gates/egress.js +14 -16
  78. package/dist/gates/fixture-model.js +13 -2
  79. package/dist/gates/handler.d.ts +1 -3
  80. package/dist/gates/handler.js +4 -3
  81. package/dist/gates/http-client.d.ts +4 -3
  82. package/dist/gates/http-client.js +5 -7
  83. package/dist/gates/http-tool.d.ts +24 -4
  84. package/dist/gates/http-tool.js +40 -12
  85. package/dist/gates/in-process.d.ts +2 -1
  86. package/dist/gates/in-process.js +17 -9
  87. package/dist/gates/inflight.d.ts +2 -2
  88. package/dist/gates/inflight.js +3 -2
  89. package/dist/gates/journal.js +5 -7
  90. package/dist/gates/mcp-handler.d.ts +4 -3
  91. package/dist/gates/mcp-handler.js +15 -5
  92. package/dist/gates/model-gate.d.ts +11 -2
  93. package/dist/gates/policy.d.ts +28 -5
  94. package/dist/gates/policy.js +60 -17
  95. package/dist/gates/provider-gate.d.ts +22 -0
  96. package/dist/gates/provider-gate.js +30 -0
  97. package/dist/gates/tool-calls.d.ts +1 -3
  98. package/dist/gates/tool-calls.js +2 -3
  99. package/dist/gates/tool-client.d.ts +1 -1
  100. package/dist/gates/tool-client.js +5 -6
  101. package/dist/gates/tool-contract.d.ts +8 -5
  102. package/dist/gates/tool-contract.js +1 -1
  103. package/dist/gates/tool-gate.d.ts +18 -7
  104. package/dist/gates/tool-gate.js +49 -7
  105. package/dist/harness/calls.d.ts +27 -25
  106. package/dist/harness/calls.js +40 -46
  107. package/dist/harness/executors.d.ts +10 -3
  108. package/dist/harness/executors.js +53 -23
  109. package/dist/harness/main.js +2 -2
  110. package/dist/harness/pod.d.ts +0 -2
  111. package/dist/harness/pod.js +7 -6
  112. package/dist/harness/run-tokens.d.ts +4 -1
  113. package/dist/harness/run-tokens.js +6 -2
  114. package/dist/harness/service.d.ts +17 -14
  115. package/dist/harness/service.js +21 -35
  116. package/dist/harness/setup.d.ts +0 -2
  117. package/dist/harness/setup.js +4 -9
  118. package/dist/harness/ws-client.d.ts +2 -2
  119. package/dist/harness/ws-client.js +7 -5
  120. package/dist/harness/ws-port.d.ts +3 -4
  121. package/dist/harness/ws-port.js +3 -4
  122. package/dist/harness-api/artifacts.d.ts +20 -0
  123. package/dist/harness-api/artifacts.js +478 -0
  124. package/dist/harness-api/in-process.d.ts +2 -2
  125. package/dist/harness-api/in-process.js +18 -16
  126. package/dist/harness-api/record.d.ts +10 -9
  127. package/dist/harness-api/record.js +102 -157
  128. package/dist/harness-api/renew.d.ts +1 -1
  129. package/dist/harness-api/renew.js +2 -2
  130. package/dist/harness-api/server.d.ts +1 -16
  131. package/dist/harness-api/server.js +82 -73
  132. package/dist/harness-api/start.d.ts +20 -2
  133. package/dist/harness-api/start.js +57 -18
  134. package/dist/harness-api/tool-journal.d.ts +15 -10
  135. package/dist/harness-api/tool-journal.js +26 -24
  136. package/dist/harness-api/workspace.d.ts +0 -4
  137. package/dist/harness-api/workspace.js +2 -45
  138. package/dist/harness-api/ws-server.js +10 -10
  139. package/dist/host/create-host.js +2 -3
  140. package/dist/host/execution.js +2 -3
  141. package/dist/host/gates.d.ts +2 -9
  142. package/dist/host/gates.js +15 -12
  143. package/dist/host/http.d.ts +0 -2
  144. package/dist/host/http.js +0 -27
  145. package/dist/host/main.js +10 -3
  146. package/dist/host/operate.js +12 -3
  147. package/dist/host/stack-config.d.ts +11 -0
  148. package/dist/host/stack-config.js +21 -1
  149. package/dist/index.d.ts +0 -1
  150. package/dist/index.js +0 -1
  151. package/dist/infra/index.js +2 -1
  152. package/dist/infra/readiness.d.ts +0 -2
  153. package/dist/infra/readiness.js +4 -3
  154. package/dist/keys/client.d.ts +1 -0
  155. package/dist/keys/client.js +2 -3
  156. package/dist/keys/contract.d.ts +0 -2
  157. package/dist/keys/contract.js +0 -2
  158. package/dist/keys/egress-ca.d.ts +0 -2
  159. package/dist/keys/egress-ca.js +2 -3
  160. package/dist/keys/keys.js +6 -1
  161. package/dist/keys/x509.d.ts +8 -4
  162. package/dist/keys/x509.js +14 -5
  163. package/dist/management-openapi.json +1961 -994
  164. package/dist/mcp/connect.d.ts +35 -26
  165. package/dist/mcp/connect.js +112 -141
  166. package/dist/mcp/pool.d.ts +27 -22
  167. package/dist/mcp/pool.js +34 -48
  168. package/dist/mcp/preview.d.ts +12 -5
  169. package/dist/mcp/preview.js +36 -19
  170. package/dist/mcp/snapshot.d.ts +35 -86
  171. package/dist/mcp/snapshot.js +93 -135
  172. package/dist/model/auth-store.js +5 -3
  173. package/dist/model/capabilities.d.ts +1 -1
  174. package/dist/model/catalog.d.ts +9 -36
  175. package/dist/model/catalog.js +6 -33
  176. package/dist/model/descriptor.d.ts +51 -0
  177. package/dist/model/descriptor.js +83 -0
  178. package/dist/model/models.d.ts +0 -3
  179. package/dist/model/models.js +3 -5
  180. package/dist/model/native-blocks.js +1 -1
  181. package/dist/model/pi-model.d.ts +16 -5
  182. package/dist/model/pi-model.js +86 -105
  183. package/dist/openapi.json +838 -1218
  184. package/dist/reads/cursor.js +3 -1
  185. package/dist/record/append.d.ts +34 -2
  186. package/dist/record/append.js +50 -1
  187. package/dist/record/environment.d.ts +2 -0
  188. package/dist/record/environment.js +2 -0
  189. package/dist/record/index.d.ts +1 -1
  190. package/dist/record/index.js +1 -1
  191. package/dist/redact.d.ts +1 -1
  192. package/dist/redact.js +3 -2
  193. package/dist/sandbox/egress-token.d.ts +3 -3
  194. package/dist/sandbox/egress-token.js +3 -3
  195. package/dist/sandbox/join.js +3 -2
  196. package/dist/sandbox/manager.d.ts +41 -41
  197. package/dist/sandbox/manager.js +112 -44
  198. package/dist/sandbox/placement.d.ts +1 -1
  199. package/dist/sandbox/placement.js +5 -8
  200. package/dist/sandbox/pods/client.d.ts +1 -10
  201. package/dist/sandbox/pods/client.js +8 -4
  202. package/dist/sandbox/pods/lifecycle.d.ts +19 -11
  203. package/dist/sandbox/pods/lifecycle.js +45 -23
  204. package/dist/sandbox/pods/network-gate.d.ts +4 -5
  205. package/dist/sandbox/pods/network-gate.js +7 -7
  206. package/dist/sandbox/pods/reconcile.js +4 -1
  207. package/dist/sandbox/pods/spec.js +8 -5
  208. package/dist/sandbox/policy.d.ts +7 -1
  209. package/dist/sandbox/policy.js +10 -3
  210. package/dist/sandbox/resolve.d.ts +8 -3
  211. package/dist/sandbox/resolve.js +12 -2
  212. package/dist/sandbox/setup.d.ts +3 -1
  213. package/dist/sandbox/setup.js +34 -11
  214. package/dist/sandbox/share.d.ts +5 -14
  215. package/dist/sandbox/share.js +5 -34
  216. package/dist/sandbox/skills.d.ts +7 -13
  217. package/dist/sandbox/skills.js +16 -31
  218. package/dist/sandbox/tenant-config.d.ts +2 -2
  219. package/dist/sandbox/tenant-config.js +4 -3
  220. package/dist/sandbox/tools.d.ts +7 -1
  221. package/dist/sandbox/tools.js +26 -26
  222. package/dist/sandbox/types.d.ts +10 -3
  223. package/dist/sandbox/types.js +9 -0
  224. package/dist/store/postgres/connect.d.ts +6 -5
  225. package/dist/store/postgres/connect.js +14 -11
  226. package/dist/store/postgres/control.js +13 -13
  227. package/dist/store/postgres/drizzle/0028_artifact_write_keys.sql +2 -0
  228. package/dist/store/postgres/drizzle/0029_protocol_15.sql +11 -0
  229. package/dist/store/postgres/drizzle/0030_protocol_15_runtime_block.sql +11 -0
  230. package/dist/store/postgres/drizzle/0031_protocol_15_mcp_snapshot.sql +8 -0
  231. package/dist/store/postgres/drizzle/meta/_journal.json +28 -0
  232. package/dist/store/postgres/reads.js +10 -2
  233. package/dist/store/postgres/schema.d.ts +19 -0
  234. package/dist/store/postgres/schema.js +7 -0
  235. package/dist/store/postgres/store.js +55 -26
  236. package/dist/store/types.d.ts +17 -7
  237. package/dist/streams/relay/core.d.ts +3 -3
  238. package/dist/streams/relay/core.js +9 -10
  239. package/dist/tenant/advance.js +21 -31
  240. package/dist/tenant/commands.js +9 -16
  241. package/dist/tenant/context.d.ts +14 -17
  242. package/dist/tenant/definition-files.d.ts +25 -4
  243. package/dist/tenant/definition-files.js +85 -47
  244. package/dist/tenant/effects.js +9 -36
  245. package/dist/tenant/environment-manifests.js +9 -1
  246. package/dist/tenant/environments.d.ts +0 -2
  247. package/dist/tenant/environments.js +49 -28
  248. package/dist/tenant/ephemeral.js +2 -1
  249. package/dist/tenant/history.d.ts +1 -1
  250. package/dist/tenant/history.js +1 -1
  251. package/dist/tenant/host-token.d.ts +3 -3
  252. package/dist/tenant/host-token.js +11 -10
  253. package/dist/tenant/http.d.ts +5 -8
  254. package/dist/tenant/http.js +9 -0
  255. package/dist/tenant/identity-file.d.ts +23 -0
  256. package/dist/tenant/identity-file.js +34 -6
  257. package/dist/tenant/issuers.d.ts +5 -5
  258. package/dist/tenant/issuers.js +31 -23
  259. package/dist/tenant/jwt.d.ts +8 -2
  260. package/dist/tenant/jwt.js +10 -2
  261. package/dist/tenant/limits.d.ts +90 -0
  262. package/dist/tenant/limits.js +175 -0
  263. package/dist/tenant/module.d.ts +4 -1
  264. package/dist/tenant/module.js +9 -8
  265. package/dist/tenant/outbound.d.ts +11 -14
  266. package/dist/tenant/outbound.js +10 -15
  267. package/dist/tenant/run-grants.js +3 -2
  268. package/dist/tenant/run-token.d.ts +0 -4
  269. package/dist/tenant/run-token.js +6 -11
  270. package/dist/tenant/runtime-limits.d.ts +24 -0
  271. package/dist/tenant/runtime-limits.js +27 -0
  272. package/dist/tenant/runtime.js +30 -43
  273. package/dist/tenant/session-streams.js +7 -6
  274. package/dist/tenant/session.d.ts +1 -1
  275. package/dist/tenant/session.js +1 -1
  276. package/dist/tenant/sessions.js +32 -29
  277. package/dist/tenant/signing-keys.d.ts +1 -1
  278. package/dist/tenant/signing-keys.js +5 -6
  279. package/dist/tenant/status.js +4 -3
  280. package/dist/tenant/streams.js +18 -20
  281. package/dist/tenant/sweep.d.ts +1 -21
  282. package/dist/tenant/sweep.js +39 -48
  283. package/dist/tenant/transcript.d.ts +70 -23
  284. package/dist/tenant/transcript.js +28 -22
  285. package/dist/tenant/types.d.ts +9 -14
  286. package/dist/tenant/worker.d.ts +2 -10
  287. package/dist/tenant/worker.js +5 -14
  288. package/dist/traces/ids.js +5 -1
  289. package/dist/traces/map.d.ts +9 -6
  290. package/dist/traces/map.js +9 -11
  291. package/dist/traces/otlp.d.ts +3 -3
  292. package/dist/traces/otlp.js +5 -3
  293. package/dist/traces/projector.d.ts +9 -4
  294. package/dist/traces/projector.js +13 -7
  295. package/dist/traces/read.js +6 -1
  296. package/dist/tuning.d.ts +383 -0
  297. package/dist/tuning.js +658 -0
  298. package/dist/vault/headers.js +4 -2
  299. package/dist/version.d.ts +1 -1
  300. package/dist/version.js +1 -1
  301. package/package.json +3 -3
  302. package/dist/artifacts/export.d.ts +0 -8
  303. package/dist/artifacts/export.js +0 -204
  304. package/dist/artifacts/media-types.d.ts +0 -30
  305. package/dist/artifacts/media-types.js +0 -137
  306. package/dist/artifacts/workspace.d.ts +0 -60
  307. package/dist/artifacts/workspace.js +0 -45
  308. package/dist/media.d.ts +0 -10
  309. package/dist/media.js +0 -58
  310. package/dist/sandbox/runtime-tools.d.ts +0 -9
  311. package/dist/sandbox/runtime-tools.js +0 -16
  312. package/dist/sandbox/session-sandbox.d.ts +0 -16
  313. package/dist/sandbox/session-sandbox.js +0 -77
  314. package/dist/tenant/artifact-tool.d.ts +0 -13
  315. package/dist/tenant/artifact-tool.js +0 -243
  316. package/dist/tenant/skill-tool.d.ts +0 -20
  317. package/dist/tenant/skill-tool.js +0 -67
  318. package/dist/tenant/tool-results.d.ts +0 -57
  319. package/dist/tenant/tool-results.js +0 -298
  320. package/dist/tenant/tool-search.d.ts +0 -39
  321. package/dist/tenant/tool-search.js +0 -103
@@ -25,22 +25,27 @@ import { newArtifactId } from "@nylorun/core/compatibility";
25
25
  import { FOLDER_CONTENT_TYPE, FOLDER_MANIFEST_FORMAT_V2, } from "@nylorun/core/contracts";
26
26
  import { sandboxSpecOf } from "../sandbox/resolve.js";
27
27
  import { effectiveSandboxConfig, readSandboxConfig } from "../sandbox/tenant-config.js";
28
- import { assertReaches, diskHeld, liveEnvironment, newVolume, SEED_LEASE_MS, wakePod, } from "../tenant/environments.js";
29
- import { fail } from "../tenant/http.js";
30
- import { readArtifactLimits } from "./config.js";
28
+ import { assertReaches, diskHeld, liveEnvironment, newVolume, wakePod, } from "../tenant/environments.js";
29
+ import { fail, failRetryLater } from "../tenant/http.js";
30
+ import { hostTuning } from "../tuning.js";
31
+ import { artifactQuota, totalSubject } from "./quota.js";
32
+ import { limitsInForce, readTenantLimits } from "../tenant/limits.js";
33
+ import { readRuntimeLimits } from "../tenant/runtime-limits.js";
31
34
  import { collectContent, contentEpoch, contentKey, encodeManifest, readManifest, sha256Hex } from "./folders.js";
32
- import { contentTypeFor } from "./media-types.js";
33
- /** The most entries one version holds (R6 Q11). */
34
- export const DISK_MAX_ENTRIES = 200_000;
35
- /** The most bytes one version's files hold. */
36
- export const DISK_MAX_BYTES = 10 * 1024 ** 3;
37
- /** One range of a file moved at a time, well inside a Harness API frame after base64. */
38
- const RANGE_BYTES = 8 * 1024 * 1024;
39
- /** A lease lasts this long unless renewed; a copy renews it every `HEARTBEAT_MS`. */
40
- const LEASE_MS = 2 * 60_000;
41
- const HEARTBEAT_MS = 30_000;
42
- /** How long a snapshot waits for a pod's engine to connect. */
43
- const ENGINE_WAIT_MS = 60_000;
35
+ import { contentTypeFor } from "@nylorun/core/contracts";
36
+ async function copyLimits(ctx, environment) {
37
+ return ctx.store.tx(async (t) => {
38
+ const runtime = await readRuntimeLimits(t);
39
+ const disk = limitsInForce(await readTenantLimits(t), environment?.applied.limits).disk;
40
+ return {
41
+ versionEntries: disk.versionEntries,
42
+ versionBytes: disk.versionBytes,
43
+ chunkBytes: runtime.chunkBytes,
44
+ leaseMs: runtime.diskLeaseMs,
45
+ podWaitMs: runtime.diskPodWaitMs,
46
+ };
47
+ });
48
+ }
44
49
  /** The name of an Environment's disk artifact. */
45
50
  export const DISK_ARTIFACT_NAME = "disk";
46
51
  /** What the disk operations of Environment `environment` act through: a ref and its spec. */
@@ -51,7 +56,6 @@ async function diskTarget(ctx, environment) {
51
56
  ref: {
52
57
  id: `disk:${environment.id}`,
53
58
  activeTurnId: null,
54
- manifest: { manifestSchemaVersion: 7, id: "disk" },
55
59
  environmentId: environment.id,
56
60
  // Not through a host a restore or a lost volume retired: its connection outlives the commit.
57
61
  ...(environment.pod === undefined ? {} : { hostEpoch: environment.pod.hostEpoch }),
@@ -70,30 +74,30 @@ async function diskOp(ctx, target, op) {
70
74
  * Takes the disk lease of Environment `id` for `kind`: refused (`409 environment_busy`) while a
71
75
  * turn runs or another copy holds it; `400` for kind `none`, which has no disk.
72
76
  */
73
- async function lease(ctx, id, kind, version) {
77
+ async function lease(ctx, id, kind, leaseMs, version) {
74
78
  return ctx.store.tx(async (t) => {
75
79
  const environment = (await liveEnvironment(t, id, { lock: true })) ?? fail(404, "Environment not found");
76
80
  if (environment.kind === "none")
77
81
  fail(400, `Environment ${id} is of kind none: it has no disk.`);
78
82
  const busy = (await t.sessionsOnEnvironment(id)).some((session) => session.activeTurnId);
79
83
  if (busy || diskHeld(environment))
80
- fail(409, `Environment ${id} has a turn running or its disk is being copied. Try again shortly.`, {
81
- code: "environment_busy",
82
- details: { retryAfterSeconds: 5 },
83
- }, { "retry-after": "5" });
84
+ failRetryLater(`Environment ${id} has a turn running or its disk is being copied. Try again shortly.`, "environment_busy");
84
85
  const now = new Date();
85
86
  const operation = {
86
87
  kind,
87
88
  ...(version === undefined ? {} : { version }),
88
89
  startedAt: now.toISOString(),
89
- until: new Date(now.getTime() + LEASE_MS).toISOString(),
90
+ until: new Date(now.getTime() + leaseMs).toISOString(),
90
91
  };
91
92
  await t.updateEnvironment(id, { diskOp: operation }, now.toISOString());
92
93
  return { ...environment, diskOp: operation };
93
94
  });
94
95
  }
95
- /** Renews the lease every `HEARTBEAT_MS` until `stop()`, which ends it (and, with `seed`, the seed). */
96
- function heartbeat(ctx, id, ms = LEASE_MS) {
96
+ /**
97
+ * Renews the lease for `ms` more every quarter of `limits.leaseMs` until `stop()`, which ends it
98
+ * (and, with `seed`, the seed).
99
+ */
100
+ function heartbeat(ctx, id, limits, ms = limits.leaseMs) {
97
101
  const timer = setInterval(() => {
98
102
  void ctx.store
99
103
  .tx(async (t) => {
@@ -104,7 +108,7 @@ function heartbeat(ctx, id, ms = LEASE_MS) {
104
108
  await t.updateEnvironment(id, { diskOp: { ...environment.diskOp, until: new Date(now.getTime() + ms).toISOString() } }, now.toISOString());
105
109
  })
106
110
  .catch(() => undefined);
107
- }, HEARTBEAT_MS);
111
+ }, limits.leaseMs / 4);
108
112
  timer.unref();
109
113
  return {
110
114
  async stop(options = {}) {
@@ -125,25 +129,22 @@ function heartbeat(ctx, id, ms = LEASE_MS) {
125
129
  };
126
130
  }
127
131
  /** Waits until the engine of pod Environment `id` is connected, starting the pod when it is stopped. */
128
- async function awaitEngine(ctx, id) {
132
+ async function awaitEngine(ctx, id, waitMs) {
129
133
  await wakePod(ctx, id);
130
- const deadline = Date.now() + ENGINE_WAIT_MS;
134
+ const deadline = Date.now() + waitMs;
131
135
  while (!ctx.harness.hosting(id)) {
132
136
  if (Date.now() > deadline)
133
- fail(409, `Environment ${id}'s pod did not start within ${ENGINE_WAIT_MS / 1000} s.`, {
134
- code: "environment_unavailable",
135
- details: { retryAfterSeconds: 5 },
136
- }, { "retry-after": "5" });
137
- await new Promise((resolve) => setTimeout(resolve, 250));
137
+ failRetryLater(`Environment ${id}'s pod did not start within ${waitMs / 1000} s (the Runtime limit diskPodWaitMs).`, "environment_unavailable");
138
+ await new Promise((resolve) => setTimeout(resolve, hostTuning().pods.enginePollMs));
138
139
  }
139
140
  }
140
141
  /** A file's bytes and their hash, read in ranges. */
141
- async function readFile(ctx, target, path) {
142
+ async function readFile(ctx, target, path, rangeBytes) {
142
143
  const hash = createHash("sha256");
143
144
  const chunks = [];
144
145
  let offset = 0;
145
146
  for (;;) {
146
- const answer = await diskOp(ctx, target, { read: { path, offset, length: RANGE_BYTES } });
147
+ const answer = await diskOp(ctx, target, { read: { path, offset, length: rangeBytes } });
147
148
  if (answer.kind === "missing")
148
149
  return undefined;
149
150
  if (answer.kind !== "read")
@@ -163,25 +164,26 @@ async function readFile(ctx, target, path) {
163
164
  export async function snapshotEnvironment(ctx, id, scope) {
164
165
  assertReaches(scope, id);
165
166
  const started = Date.now();
166
- const environment = await lease(ctx, id, "snapshot");
167
- const beat = heartbeat(ctx, id);
167
+ const limits = await copyLimits(ctx, await ctx.store.tx((t) => liveEnvironment(t, id)));
168
+ const environment = await lease(ctx, id, "snapshot", limits.leaseMs);
169
+ const beat = heartbeat(ctx, id, limits);
168
170
  try {
169
171
  if (environment.kind === "pod")
170
- await awaitEngine(ctx, id);
172
+ await awaitEngine(ctx, id, limits.podWaitMs);
171
173
  const target = await diskTarget(ctx, environment);
172
- const listed = await diskOp(ctx, target, { list: { maxEntries: DISK_MAX_ENTRIES } });
174
+ const listed = await diskOp(ctx, target, { list: { maxEntries: limits.versionEntries } });
173
175
  if (listed.kind !== "listed")
174
176
  throw new Error("The disk was not listed");
175
177
  if (listed.truncated)
176
- fail(413, `The disk holds more than ${DISK_MAX_ENTRIES} entries.`, {
178
+ fail(413, `The disk holds more than ${limits.versionEntries} entries (the limit diskVersionEntries).`, {
177
179
  code: "limit_exceeded",
178
- details: { limit: "files", max: DISK_MAX_ENTRIES },
180
+ details: { limit: "files", max: limits.versionEntries },
179
181
  });
180
182
  const bytes = listed.entries.reduce((sum, entry) => sum + (entry.type === "file" ? entry.size : 0), 0);
181
- if (bytes > DISK_MAX_BYTES)
182
- fail(413, `The disk holds ${bytes} bytes; a version holds at most ${DISK_MAX_BYTES}.`, {
183
+ if (bytes > limits.versionBytes)
184
+ fail(413, `The disk holds ${bytes} bytes; a version holds at most ${limits.versionBytes} (the limit diskVersionBytes).`, {
183
185
  code: "limit_exceeded",
184
- details: { limit: "bytes", max: DISK_MAX_BYTES },
186
+ details: { limit: "bytes", max: limits.versionBytes },
185
187
  });
186
188
  const epoch = await ctx.store.tx((t) => contentEpoch(t));
187
189
  const entries = [];
@@ -198,7 +200,7 @@ export async function snapshotEnvironment(ctx, id, scope) {
198
200
  continue;
199
201
  }
200
202
  const base = { path: entry.path, mode: entry.mode, mtimeMs: entry.mtimeMs };
201
- const read = await readFile(ctx, target, entry.path);
203
+ const read = await readFile(ctx, target, entry.path, limits.chunkBytes);
202
204
  if (!read)
203
205
  continue; // removed while it was copied (a background process)
204
206
  const key = contentKey(read.sha256);
@@ -262,9 +264,9 @@ async function commitDiskVersion(ctx, id, entries, epoch) {
262
264
  const artifactId = prior?.id ?? newArtifactId();
263
265
  const key = `artifacts/${artifactId}/${randomUUID()}`;
264
266
  await ctx.blobs.put(key, manifest, { contentType: FOLDER_CONTENT_TYPE });
265
- const totalBytes = (await ctx.store.tx((t) => readArtifactLimits(t))).totalBytes;
266
267
  return ctx.store.tx(async (t) => {
267
268
  (await liveEnvironment(t, id, { lock: true })) ?? fail(404, "Environment not found");
269
+ const quota = await artifactQuota(t, id);
268
270
  await t.lockArtifactQuota();
269
271
  const shas = [...files.keys()];
270
272
  const referenced = await t.referencedArtifactContent(shas);
@@ -273,13 +275,18 @@ async function commitDiskVersion(ctx, id, entries, epoch) {
273
275
  for (const sha of fresh)
274
276
  if (!(await ctx.blobs.head(contentKey(sha))))
275
277
  fail(409, "A file's bytes were removed while the snapshot ran; try again.", { code: "environment_busy" });
276
- const added = fresh.reduce((sum, sha) => sum + files.get(sha), 0);
277
- const total = await t.artifactBytes();
278
- if (total + added > totalBytes)
279
- fail(413, `The Tenant's artifacts may hold at most ${totalBytes} bytes; this version adds ${added} to ${total}.`, {
280
- code: "limit_exceeded",
281
- details: { limit: "tenant_total", max: totalBytes },
282
- });
278
+ for (const total of quota.totals) {
279
+ // A file adds bytes to a total unless an artifact it counts already holds the file.
280
+ const scope = total.environmentId === undefined ? undefined : { environmentId: total.environmentId };
281
+ const held = scope === undefined ? referenced : await t.referencedArtifactContent(shas, scope);
282
+ const added = shas.filter((sha) => !held.has(sha)).reduce((sum, sha) => sum + files.get(sha), 0);
283
+ const used = await t.artifactBytes(scope);
284
+ if (used + added > total.limitBytes)
285
+ fail(413, `${totalSubject(total)} may hold at most ${total.limitBytes} bytes; this version adds ${added} to ${used}.`, {
286
+ code: "limit_exceeded",
287
+ details: { limit: scope === undefined ? "tenant_total" : "environment_total", max: total.limitBytes },
288
+ });
289
+ }
283
290
  const createdAt = new Date().toISOString();
284
291
  const current = await t.diskArtifact(id, { lock: true });
285
292
  const version = {
@@ -291,6 +298,7 @@ async function commitDiskVersion(ctx, id, entries, epoch) {
291
298
  contentType: FOLDER_CONTENT_TYPE,
292
299
  source: "snapshot",
293
300
  createdAt,
301
+ writeKey: null,
294
302
  };
295
303
  let artifact;
296
304
  if (current) {
@@ -320,7 +328,7 @@ async function commitDiskVersion(ctx, id, entries, epoch) {
320
328
  * Writes disk version `seed` into Environment `id`'s workspace: empties it (the engine's files
321
329
  * stay), then directories, files (large ones in ranges) and links. Its caller holds the lease.
322
330
  */
323
- async function writeVersion(ctx, environment, seed) {
331
+ async function writeVersion(ctx, environment, seed, rangeBytes) {
324
332
  const version = await ctx.store.tx((t) => t.artifactVersion(seed.artifactId, seed.version));
325
333
  if (!version)
326
334
  throw new Error(`Disk version ${seed.version} of ${seed.artifactId} is gone`);
@@ -354,22 +362,22 @@ async function writeVersion(ctx, environment, seed) {
354
362
  ...(entry.mode === undefined ? {} : { mode: entry.mode }),
355
363
  ...(entry.mtimeMs === undefined ? {} : { mtimeMs: entry.mtimeMs }),
356
364
  };
357
- if (content.length <= RANGE_BYTES) {
358
- if (batchBytes + content.length > RANGE_BYTES)
365
+ if (content.length <= rangeBytes) {
366
+ if (batchBytes + content.length > rangeBytes)
359
367
  await flush();
360
368
  batch.push({ path: entry.path, type: "file", bytes: content, ...meta });
361
369
  batchBytes += content.length;
362
370
  continue;
363
371
  }
364
372
  await flush();
365
- for (let offset = 0; offset < content.length; offset += RANGE_BYTES) {
366
- const last = offset + RANGE_BYTES >= content.length;
373
+ for (let offset = 0; offset < content.length; offset += rangeBytes) {
374
+ const last = offset + rangeBytes >= content.length;
367
375
  await diskOp(ctx, target, {
368
376
  put: [
369
377
  {
370
378
  path: entry.path,
371
379
  type: "file",
372
- bytes: content.subarray(offset, offset + RANGE_BYTES),
380
+ bytes: content.subarray(offset, offset + rangeBytes),
373
381
  ...(offset > 0 ? { append: true } : {}),
374
382
  ...(last ? meta : {}),
375
383
  },
@@ -408,18 +416,16 @@ export async function restoreEnvironment(ctx, id, scope, version) {
408
416
  const environment = (await liveEnvironment(t, id, { lock: true })) ?? fail(404, "Environment not found");
409
417
  const busy = (await t.sessionsOnEnvironment(id)).some((session) => session.activeTurnId);
410
418
  if (busy || diskHeld(environment) || environment.pod.retiring !== undefined)
411
- fail(409, `Environment ${id} has a turn running, its disk is being copied or its last volume is still being deleted.`, {
412
- code: "environment_busy",
413
- details: { retryAfterSeconds: 5 },
414
- }, { "retry-after": "5" });
419
+ failRetryLater(`Environment ${id} has a turn running, its disk is being copied or its last volume is still being deleted.`, "environment_busy");
415
420
  return newVolume(ctx, t, environment, { seed, reason: "restore" });
416
421
  });
417
422
  return { environmentId: id, restored: seed.version, status: "pending", volumeGeneration };
418
423
  }
419
- const environment = await lease(ctx, id, "restore", seed.version);
420
- const beat = heartbeat(ctx, id);
424
+ const limits = await copyLimits(ctx, found);
425
+ const environment = await lease(ctx, id, "restore", limits.leaseMs, seed.version);
426
+ const beat = heartbeat(ctx, id, limits);
421
427
  try {
422
- await writeVersion(ctx, environment, seed);
428
+ await writeVersion(ctx, environment, seed, limits.chunkBytes);
423
429
  }
424
430
  finally {
425
431
  await beat.stop();
@@ -444,20 +450,21 @@ export async function runPendingSeed(ctx, id) {
444
450
  const seed = environment?.diskSeed;
445
451
  if (!environment || !seed || seed.written)
446
452
  return;
453
+ const limits = await copyLimits(ctx, environment);
447
454
  if (environment.kind === "pod") {
448
- const deadline = Date.now() + ENGINE_WAIT_MS;
455
+ const deadline = Date.now() + limits.podWaitMs;
449
456
  // The engine on the new volume: the old one's connection may still be open.
450
457
  while (!ctx.harness.hosting(id, environment.pod?.hostEpoch)) {
451
458
  if (Date.now() > deadline)
452
459
  return;
453
- await new Promise((resolve) => setTimeout(resolve, 250));
460
+ await new Promise((resolve) => setTimeout(resolve, hostTuning().pods.enginePollMs));
454
461
  }
455
462
  }
456
- const beat = heartbeat(ctx, id, SEED_LEASE_MS);
463
+ const beat = heartbeat(ctx, id, limits, hostTuning().tenant.seedLeaseMs);
457
464
  let done = false;
458
465
  try {
459
466
  const started = Date.now();
460
- const written = await writeVersion(ctx, environment, seed);
467
+ const written = await writeVersion(ctx, environment, seed, limits.chunkBytes);
461
468
  done = true;
462
469
  ctx.config.logger.info("disk_seed", { environmentId: id, version: seed.version, ...written, durationMs: Date.now() - started });
463
470
  }
@@ -538,10 +545,7 @@ export async function deleteEnvironmentVersion(ctx, id, scope, version) {
538
545
  if (!artifact)
539
546
  return undefined;
540
547
  if (environment.diskSeed?.artifactId === artifact.id && environment.diskSeed.version === version)
541
- fail(409, `A new volume of Environment ${id} is being seeded from version ${version}.`, {
542
- code: "environment_busy",
543
- details: { retryAfterSeconds: 5 },
544
- }, { "retry-after": "5" });
548
+ failRetryLater(`A new volume of Environment ${id} is being seeded from version ${version}.`, "environment_busy");
545
549
  await t.lockArtifactQuota();
546
550
  const content = await t.artifactContentShas({ artifactId: artifact.id });
547
551
  const key = await t.deleteArtifactVersion(artifact.id, version);
@@ -8,7 +8,8 @@
8
8
  */
9
9
  import type { JsonValue } from "@nylorun/core/define";
10
10
  import type { BlobStore } from "../blob/index.js";
11
- import type { SessionStore } from "../store/types.js";
11
+ import type { ArtifactRow, SessionStore } from "../store/types.js";
12
+ import type { Session } from "../tenant/context.js";
12
13
  /** A file a prompt names, with its bytes. */
13
14
  export interface ResolvedFile {
14
15
  readonly name: string;
@@ -22,15 +23,23 @@ export declare class FileUnavailableError extends Error {
22
23
  }
23
24
  /** Resolves a media part's reference to the file's bytes, or throws `FileUnavailableError`. */
24
25
  export type FileResolver = (reference: JsonValue) => Promise<ResolvedFile>;
26
+ /**
27
+ * What a session may read of the Tenant's artifacts (the read rule): its own, and the Tenant-wide
28
+ * ones its messages named (`Session.fileArtifacts`); never another session's, and never an
29
+ * Environment's disk versions, which belong to no session. The Model Gate and the artifact seam
30
+ * (`artifact.read`) both read by it.
31
+ */
32
+ export declare function sessionMayRead(session: Pick<Session, "id" | "fileArtifacts"> | undefined, artifact: Pick<ArtifactRow, "id" | "sessionId" | "environmentId">): boolean;
25
33
  /**
26
34
  * Files from the Tenant's artifacts: `store` for the version's row, `blobs` for its bytes, for
27
- * the call of session `sessionId`. An artifact of another session is refused (a Tenant-wide one
28
- * is not), and without a session every file is: the check is never skipped.
35
+ * the call of session `sessionId`. It reads by the read rule (`sessionMayRead`): another
36
+ * session's artifact, or a Tenant-wide one the session never named, is refused, and without a
37
+ * session every file is: the check is never skipped. A file larger than the Tenant's Runtime
38
+ * limit `modelFileBytes`, read with the row, is refused too.
29
39
  */
30
40
  export declare function artifactFiles(options: {
31
41
  readonly store: SessionStore;
32
42
  readonly blobs: BlobStore;
33
43
  /** The session the call is for: the run token's, over HTTP. */
34
44
  readonly sessionId: string | undefined;
35
- readonly maxBytes?: number;
36
45
  }): FileResolver;
@@ -1,4 +1,4 @@
1
- import { MODEL_FILE_MAX_BYTES } from "./media-types.js";
1
+ import { readRuntimeLimits } from "../tenant/runtime-limits.js";
2
2
  import { fileReferenceOf } from "./parts.js";
3
3
  /** Why a file cannot go to the model: the message is the outcome's, so it names no secret. */
4
4
  export class FileUnavailableError extends Error {
@@ -7,13 +7,27 @@ export class FileUnavailableError extends Error {
7
7
  this.name = "FileUnavailableError";
8
8
  }
9
9
  }
10
+ /**
11
+ * What a session may read of the Tenant's artifacts (the read rule): its own, and the Tenant-wide
12
+ * ones its messages named (`Session.fileArtifacts`); never another session's, and never an
13
+ * Environment's disk versions, which belong to no session. The Model Gate and the artifact seam
14
+ * (`artifact.read`) both read by it.
15
+ */
16
+ export function sessionMayRead(session, artifact) {
17
+ if (!session)
18
+ return false;
19
+ if (artifact.sessionId !== null)
20
+ return artifact.sessionId === session.id;
21
+ return artifact.environmentId === null && session.fileArtifacts?.includes(artifact.id) === true;
22
+ }
10
23
  /**
11
24
  * Files from the Tenant's artifacts: `store` for the version's row, `blobs` for its bytes, for
12
- * the call of session `sessionId`. An artifact of another session is refused (a Tenant-wide one
13
- * is not), and without a session every file is: the check is never skipped.
25
+ * the call of session `sessionId`. It reads by the read rule (`sessionMayRead`): another
26
+ * session's artifact, or a Tenant-wide one the session never named, is refused, and without a
27
+ * session every file is: the check is never skipped. A file larger than the Tenant's Runtime
28
+ * limit `modelFileBytes`, read with the row, is refused too.
14
29
  */
15
30
  export function artifactFiles(options) {
16
- const maxBytes = options.maxBytes ?? MODEL_FILE_MAX_BYTES;
17
31
  return async (value) => {
18
32
  const reference = fileReferenceOf(value);
19
33
  if (!reference)
@@ -25,17 +39,21 @@ export function artifactFiles(options) {
25
39
  const artifact = await t.artifact(reference.artifactId);
26
40
  if (!artifact)
27
41
  return undefined;
42
+ const session = artifact.sessionId === null
43
+ ? await t.get("sessions", sessionId)
44
+ : { id: sessionId };
45
+ if (!sessionMayRead(session, artifact))
46
+ return undefined;
28
47
  const version = await t.artifactVersion(artifact.id, reference.version);
29
- return version ? { artifact, version } : undefined;
48
+ return version ? { artifact, version, maxBytes: (await readRuntimeLimits(t)).modelFileBytes } : undefined;
30
49
  });
31
- if (!found ||
32
- (found.artifact.sessionId !== null && found.artifact.sessionId !== sessionId))
50
+ if (!found)
33
51
  throw new FileUnavailableError(`Artifact ${reference.artifactId} version ${reference.version} is not available.`);
34
- const { artifact, version } = found;
52
+ const { artifact, version, maxBytes } = found;
35
53
  if (artifact.kind !== "file")
36
54
  throw new FileUnavailableError(`${artifact.name} is a folder; a model reads files only.`);
37
55
  if (version.size > maxBytes)
38
- throw new FileUnavailableError(`${artifact.name} is ${version.size} bytes; a model reads files of at most ${maxBytes} bytes.`);
56
+ throw new FileUnavailableError(`${artifact.name} is ${version.size} bytes; a model reads files of at most ${maxBytes} bytes (the Runtime limit modelFileBytes).`);
39
57
  const got = await options.blobs.get(version.blobKey);
40
58
  if (!got)
41
59
  throw new FileUnavailableError(`The bytes of ${artifact.name} are missing from the Object store.`);
@@ -0,0 +1,75 @@
1
+ import { type HarnessLimits } from "@nylorun/core/contracts";
2
+ import { type ParamsOf } from "@nylorun/core/harness-api";
3
+ import type { ArtifactRow, ArtifactVersionRow } from "../store/types.js";
4
+ import type { TenantContext } from "../tenant/context.js";
5
+ import type { ArtifactQuota } from "./quota.js";
6
+ /** What a folder upload writes: the session's folder, and what its version must fit. */
7
+ export interface FolderTarget {
8
+ readonly sessionId: string;
9
+ readonly turnId: string;
10
+ readonly name: string;
11
+ /** The folder this upload adds a version to; absent: a new folder. */
12
+ readonly prior?: {
13
+ readonly row: ArtifactRow;
14
+ readonly latest?: ArtifactVersionRow;
15
+ };
16
+ readonly quota: ArtifactQuota;
17
+ /** The content epoch read at `begin`, before any `head` of this upload. */
18
+ readonly epoch: number;
19
+ readonly writeKey: string;
20
+ readonly limits: HarnessLimits;
21
+ }
22
+ /** How a folder upload's `commit` ended. */
23
+ export type FolderCommit = {
24
+ readonly kind: "written";
25
+ readonly row: ArtifactRow;
26
+ readonly version: ArtifactVersionRow;
27
+ readonly created: boolean;
28
+ readonly unchanged?: true;
29
+ } | {
30
+ readonly kind: "refused";
31
+ readonly reason: "tenant_total" | "environment_total";
32
+ readonly message: string;
33
+ readonly limit: number;
34
+ };
35
+ /** A folder file's path: relative, `/`-separated, without empty, `.` or `..` segments. */
36
+ export declare function checkFolderPath(path: string): void;
37
+ export declare class FolderUpload {
38
+ private readonly ctx;
39
+ readonly target: FolderTarget;
40
+ readonly id: `${string}-${string}-${string}-${string}-${string}`;
41
+ /** The most bytes its files may hold together, and one file. */
42
+ readonly maxBytes: number;
43
+ readonly maxFileBytes: number;
44
+ readonly maxEntries: number;
45
+ private readonly entries;
46
+ private readonly paths;
47
+ /** Content taken whole and checked in this upload. */
48
+ private readonly taken;
49
+ /** Content this upload stored at its content key: collected again if nothing commits. */
50
+ private readonly stored;
51
+ private bytes;
52
+ private files;
53
+ private current?;
54
+ private ended;
55
+ constructor(ctx: TenantContext, target: FolderTarget);
56
+ /** Starts a file; its bytes come with it (`base64`) or in `chunk`s. Answers the bytes received. */
57
+ file(params: Extract<ParamsOf<"artifact.write">, {
58
+ op: "file";
59
+ }>, signal: AbortSignal): Promise<number>;
60
+ /** Bytes of the file in progress from `offset`. Answers the bytes received of it. */
61
+ chunk(params: Extract<ParamsOf<"artifact.write">, {
62
+ op: "chunk";
63
+ }>): Promise<number>;
64
+ /** Stores the version, unless it is the folder's latest; collects what it stored when not. */
65
+ commit(signal: AbortSignal): Promise<FolderCommit>;
66
+ /** Ends the upload: nothing commits, and what it stored is collected. Never throws. */
67
+ abort(reason: string): Promise<void>;
68
+ private live;
69
+ private take;
70
+ /** The file in progress is whole: check its hash, then keep its content once. */
71
+ private finish;
72
+ private dropCurrent;
73
+ /** Content this upload stored that no committed version names is removed again. */
74
+ private collect;
75
+ }