@nylorun/runtime 0.15.1-beta → 0.16.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 (220) hide show
  1. package/CHANGELOG.md +82 -0
  2. package/README.md +33 -6
  3. package/dist/adapters/execution/restate.d.ts +9 -1
  4. package/dist/adapters/execution/restate.js +64 -5
  5. package/dist/adapters/replication/pgoutput.js +3 -1
  6. package/dist/adapters/sandbox/local.d.ts +10 -0
  7. package/dist/adapters/sandbox/local.js +195 -0
  8. package/dist/adapters/sandbox/virtual.js +36 -1
  9. package/dist/admin-openapi.json +37 -3
  10. package/dist/api/a2a/tasks.js +6 -1
  11. package/dist/api/ag-ui/history.js +6 -1
  12. package/dist/api/ag-ui/routes.js +5 -1
  13. package/dist/api/components.d.ts +2673 -655
  14. package/dist/api/components.js +17 -1
  15. package/dist/api/gate/keys.d.ts +4 -3
  16. package/dist/api/gate/routes.d.ts +19 -3
  17. package/dist/api/gate/routes.js +162 -53
  18. package/dist/api/http/app.js +4 -0
  19. package/dist/api/http/define.d.ts +10 -0
  20. package/dist/api/http/define.js +2 -1
  21. package/dist/api/http/routes/artifacts.d.ts +11 -0
  22. package/dist/api/http/routes/artifacts.js +518 -0
  23. package/dist/api/http/routes/sandboxes.d.ts +9 -0
  24. package/dist/api/http/routes/sandboxes.js +152 -0
  25. package/dist/api/http/routes/sessions.js +3 -1
  26. package/dist/api/http/routes/tenant.js +36 -2
  27. package/dist/artifacts/config.d.ts +23 -0
  28. package/dist/artifacts/config.js +27 -0
  29. package/dist/artifacts/export.d.ts +8 -0
  30. package/dist/artifacts/export.js +205 -0
  31. package/dist/artifacts/files.d.ts +36 -0
  32. package/dist/artifacts/files.js +45 -0
  33. package/dist/artifacts/folders.d.ts +60 -0
  34. package/dist/artifacts/folders.js +209 -0
  35. package/dist/artifacts/links.d.ts +28 -0
  36. package/dist/artifacts/links.js +92 -0
  37. package/dist/artifacts/media-types.d.ts +13 -0
  38. package/dist/artifacts/media-types.js +80 -0
  39. package/dist/artifacts/parts.d.ts +34 -0
  40. package/dist/artifacts/parts.js +38 -0
  41. package/dist/artifacts/service.d.ts +74 -0
  42. package/dist/artifacts/service.js +301 -0
  43. package/dist/artifacts/workspace.d.ts +60 -0
  44. package/dist/artifacts/workspace.js +57 -0
  45. package/dist/artifacts/zip.d.ts +10 -0
  46. package/dist/artifacts/zip.js +126 -0
  47. package/dist/blob/fs.d.ts +13 -0
  48. package/dist/blob/fs.js +194 -0
  49. package/dist/blob/index.d.ts +9 -0
  50. package/dist/blob/index.js +9 -0
  51. package/dist/blob/s3.d.ts +42 -0
  52. package/dist/blob/s3.js +366 -0
  53. package/dist/blob/types.d.ts +108 -0
  54. package/dist/blob/types.js +85 -0
  55. package/dist/core/sandbox-routes.d.ts +2 -2
  56. package/dist/core/sandbox-routes.js +4 -3
  57. package/dist/execution/memory.d.ts +11 -1
  58. package/dist/execution/memory.js +77 -3
  59. package/dist/execution/types.d.ts +54 -1
  60. package/dist/execution/types.js +31 -0
  61. package/dist/gates/contract.d.ts +14 -13
  62. package/dist/gates/contract.js +13 -12
  63. package/dist/gates/egress.d.ts +72 -0
  64. package/dist/gates/egress.js +319 -0
  65. package/dist/gates/handler.d.ts +3 -0
  66. package/dist/gates/handler.js +3 -2
  67. package/dist/gates/http-client.d.ts +3 -2
  68. package/dist/gates/http-client.js +17 -6
  69. package/dist/gates/in-process.d.ts +13 -1
  70. package/dist/gates/in-process.js +11 -0
  71. package/dist/gates/inflight.d.ts +23 -4
  72. package/dist/gates/inflight.js +18 -4
  73. package/dist/gates/model-gate.d.ts +6 -2
  74. package/dist/gates/tool-calls.d.ts +9 -5
  75. package/dist/gates/tool-calls.js +6 -6
  76. package/dist/gates/tool-client.d.ts +9 -2
  77. package/dist/gates/tool-client.js +56 -22
  78. package/dist/gates/tool-contract.d.ts +12 -7
  79. package/dist/gates/tool-contract.js +10 -4
  80. package/dist/gates/tool-gate.d.ts +1 -0
  81. package/dist/harness/calls.d.ts +52 -0
  82. package/dist/harness/calls.js +113 -0
  83. package/dist/harness/executors.d.ts +22 -0
  84. package/dist/harness/executors.js +96 -0
  85. package/dist/harness/main.d.ts +6 -0
  86. package/dist/harness/main.js +103 -0
  87. package/dist/harness/pod.d.ts +21 -0
  88. package/dist/harness/pod.js +119 -0
  89. package/dist/harness/run-tokens.d.ts +18 -0
  90. package/dist/harness/run-tokens.js +15 -0
  91. package/dist/harness/service.d.ts +46 -0
  92. package/dist/harness/service.js +179 -0
  93. package/dist/harness/ws-client.d.ts +43 -0
  94. package/dist/harness/ws-client.js +122 -0
  95. package/dist/harness/ws-port.d.ts +22 -0
  96. package/dist/harness/ws-port.js +78 -0
  97. package/dist/harness-api/in-process.d.ts +24 -0
  98. package/dist/harness-api/in-process.js +119 -0
  99. package/dist/harness-api/record.d.ts +44 -0
  100. package/dist/harness-api/record.js +250 -0
  101. package/dist/harness-api/renew.d.ts +37 -0
  102. package/dist/harness-api/renew.js +77 -0
  103. package/dist/harness-api/server.d.ts +104 -0
  104. package/dist/harness-api/server.js +408 -0
  105. package/dist/harness-api/start.d.ts +36 -0
  106. package/dist/harness-api/start.js +118 -0
  107. package/dist/harness-api/workspace.d.ts +53 -0
  108. package/dist/harness-api/workspace.js +201 -0
  109. package/dist/harness-api/ws-server.d.ts +34 -0
  110. package/dist/harness-api/ws-server.js +227 -0
  111. package/dist/host/app.js +23 -2
  112. package/dist/host/gates.d.ts +27 -2
  113. package/dist/host/gates.js +46 -1
  114. package/dist/host/http.d.ts +4 -1
  115. package/dist/host/http.js +6 -1
  116. package/dist/host/main.js +135 -27
  117. package/dist/host/stack-config.d.ts +101 -1
  118. package/dist/host/stack-config.js +199 -8
  119. package/dist/mcp/snapshot.d.ts +8 -1
  120. package/dist/mcp/snapshot.js +28 -0
  121. package/dist/media.d.ts +5 -24
  122. package/dist/media.js +5 -0
  123. package/dist/model/pi-model.d.ts +6 -2
  124. package/dist/model/pi-model.js +39 -22
  125. package/dist/node/index.d.ts +1 -1
  126. package/dist/node/index.js +1 -1
  127. package/dist/openapi.json +15245 -8850
  128. package/dist/record/index.d.ts +1 -0
  129. package/dist/record/index.js +1 -0
  130. package/dist/record/sandbox.d.ts +25 -0
  131. package/dist/record/sandbox.js +43 -0
  132. package/dist/sandbox/capability.d.ts +3 -0
  133. package/dist/sandbox/capability.js +10 -0
  134. package/dist/sandbox/egress-token.d.ts +26 -0
  135. package/dist/sandbox/egress-token.js +8 -0
  136. package/dist/sandbox/join.d.ts +37 -0
  137. package/dist/sandbox/join.js +165 -0
  138. package/dist/sandbox/manager.d.ts +99 -34
  139. package/dist/sandbox/manager.js +216 -84
  140. package/dist/sandbox/placement.d.ts +27 -0
  141. package/dist/sandbox/placement.js +44 -0
  142. package/dist/sandbox/pods/client.d.ts +90 -0
  143. package/dist/sandbox/pods/client.js +79 -0
  144. package/dist/sandbox/pods/lifecycle.d.ts +91 -0
  145. package/dist/sandbox/pods/lifecycle.js +178 -0
  146. package/dist/sandbox/pods/name.d.ts +1 -0
  147. package/dist/sandbox/pods/name.js +28 -0
  148. package/dist/sandbox/pods/network-gate.d.ts +25 -0
  149. package/dist/sandbox/pods/network-gate.js +71 -0
  150. package/dist/sandbox/pods/reconcile.d.ts +11 -0
  151. package/dist/sandbox/pods/reconcile.js +152 -0
  152. package/dist/sandbox/pods/spec.d.ts +45 -0
  153. package/dist/sandbox/pods/spec.js +62 -0
  154. package/dist/sandbox/records.d.ts +29 -0
  155. package/dist/sandbox/records.js +79 -0
  156. package/dist/sandbox/resolve.d.ts +5 -0
  157. package/dist/sandbox/resolve.js +3 -2
  158. package/dist/sandbox/session-sandbox.js +7 -5
  159. package/dist/sandbox/share.d.ts +11 -0
  160. package/dist/sandbox/share.js +10 -0
  161. package/dist/sandbox/tenant-config.d.ts +16 -1
  162. package/dist/sandbox/tenant-config.js +9 -0
  163. package/dist/sandbox/types.d.ts +29 -1
  164. package/dist/sandbox/types.js +13 -1
  165. package/dist/store/postgres/drizzle/0004_sandbox_resources.sql +20 -0
  166. package/dist/store/postgres/drizzle/0005_artifacts.sql +27 -0
  167. package/dist/store/postgres/drizzle/0006_folder_artifacts.sql +12 -0
  168. package/dist/store/postgres/drizzle/0007_sandbox_pods.sql +14 -0
  169. package/dist/store/postgres/drizzle/meta/_journal.json +28 -0
  170. package/dist/store/postgres/record-writer.d.ts +6 -1
  171. package/dist/store/postgres/record-writer.js +20 -2
  172. package/dist/store/postgres/schema.d.ts +894 -0
  173. package/dist/store/postgres/schema.js +130 -2
  174. package/dist/store/postgres/store.js +289 -4
  175. package/dist/store/types.d.ts +143 -6
  176. package/dist/streams/control.d.ts +2 -0
  177. package/dist/streams/control.js +5 -0
  178. package/dist/streams/types.d.ts +10 -1
  179. package/dist/tenant/advance.js +218 -160
  180. package/dist/tenant/artifact-tool.d.ts +5 -0
  181. package/dist/tenant/artifact-tool.js +119 -0
  182. package/dist/tenant/commands.js +30 -5
  183. package/dist/tenant/context.d.ts +64 -5
  184. package/dist/tenant/effects.d.ts +19 -13
  185. package/dist/tenant/effects.js +5 -382
  186. package/dist/tenant/history.d.ts +13 -23
  187. package/dist/tenant/history.js +14 -62
  188. package/dist/tenant/host-token.d.ts +92 -0
  189. package/dist/tenant/host-token.js +179 -0
  190. package/dist/tenant/module.js +1 -0
  191. package/dist/tenant/paths.d.ts +2 -1
  192. package/dist/tenant/paths.js +3 -1
  193. package/dist/tenant/reset.d.ts +5 -2
  194. package/dist/tenant/reset.js +22 -2
  195. package/dist/tenant/run-grants.d.ts +49 -0
  196. package/dist/tenant/run-grants.js +62 -0
  197. package/dist/tenant/run-token.d.ts +70 -0
  198. package/dist/tenant/run-token.js +138 -0
  199. package/dist/tenant/runtime.d.ts +38 -0
  200. package/dist/tenant/runtime.js +120 -32
  201. package/dist/tenant/sandboxes.d.ts +119 -0
  202. package/dist/tenant/sandboxes.js +551 -0
  203. package/dist/tenant/session.d.ts +2 -13
  204. package/dist/tenant/session.js +4 -28
  205. package/dist/tenant/sessions.d.ts +5 -0
  206. package/dist/tenant/sessions.js +28 -1
  207. package/dist/tenant/status.d.ts +8 -3
  208. package/dist/tenant/status.js +5 -0
  209. package/dist/tenant/streams.d.ts +7 -0
  210. package/dist/tenant/streams.js +24 -1
  211. package/dist/tenant/sweep.js +37 -4
  212. package/dist/tenant/tokens.js +9 -2
  213. package/dist/tenant/types.d.ts +18 -1
  214. package/dist/tenant/worker.d.ts +11 -5
  215. package/dist/tenant/worker.js +12 -6
  216. package/dist/version.d.ts +1 -1
  217. package/dist/version.js +1 -1
  218. package/package.json +13 -10
  219. package/dist/adapters/media.d.ts +0 -24
  220. package/dist/adapters/media.js +0 -111
@@ -6,8 +6,8 @@
6
6
  *
7
7
  * - `nylorun` holds the Tenant's state: the one `tenant` row, the document tables, Action
8
8
  * endpoints, principals, vaults and credentials, signing keys, publishable keys, subject
9
- * epochs and usage, settings, the model usage ledger, the model budgets and the Tool Gate's
10
- * crossings.
9
+ * epochs and usage, settings, the model usage ledger, the model budgets, the Tool Gate's
10
+ * crossings and file artifacts with their versions.
11
11
  * - `nylorun_streams` holds the record (Durable Streams §6): `session_events`,
12
12
  * `session_log_heads` and the relay's `relay_slots`. The relay's publication is custom SQL
13
13
  * (`drizzle/0002_stream_relay.sql`).
@@ -103,10 +103,13 @@ export const sessions = nylorun.table("sessions", {
103
103
  ownerExpiresAt: timestamp({ withTimezone: true, mode: "date" }),
104
104
  /** The session owner (`ownerUserId`), not the lease column `owner`. */
105
105
  ownerUserId: field("->>'ownerUserId'"),
106
+ /** The sandbox resource the session is attached to (`sandbox: { id }`). */
107
+ sandboxId: field("->>'sandboxId'"),
106
108
  }, (t) => [
107
109
  index("sessions_status").on(t.status, t.ownerExpiresAt),
108
110
  index("sessions_agent").on(t.agentId),
109
111
  index("sessions_owner_user").on(t.ownerUserId),
112
+ index("sessions_sandbox").on(t.sandboxId),
110
113
  ]);
111
114
  export const commands = documents("commands");
112
115
  export const effects = nylorun.table("effects", {
@@ -335,8 +338,133 @@ export const toolCrossings = nylorun.table("tool_crossings", {
335
338
  /** The gate's answer (`McpAnswer`), once the call ended; null while it runs or once lost. */
336
339
  answer: jsonText(),
337
340
  }, (t) => [index("tool_crossings_settled").on(t.settledAt)]);
341
+ /**
342
+ * A sandbox resource (blueprint D39, F7.1): its id, kind, the spec resolved against the
343
+ * Tenant's limits when it was created, and the developer's labels. Sessions attach to it by id
344
+ * (the session body's `sandboxId`). Not the `sandboxes` document table, which holds the virtual
345
+ * backend's compute records, keyed by workspace key.
346
+ */
347
+ export const sandboxResources = nylorun.table("sandbox_resources", {
348
+ id: textC().primaryKey(),
349
+ kind: text({ enum: ["virtual", "pod"] }).notNull(),
350
+ /** The resolved `SandboxManifest`. */
351
+ spec: jsonText().notNull(),
352
+ /** `{ key: value }`, filtered through `nylorun.doc(labels)`. */
353
+ labels: jsonText().notNull(),
354
+ createdAt: textC().notNull(),
355
+ updatedAt: textC().notNull(),
356
+ // Kind `pod` (F7.2, D34): null for a virtual sandbox. `tenant/sandboxes.ts` and the
357
+ // `Sandbox` object's reconcile (`sandbox/pods/reconcile.ts`) are the only writers.
358
+ /** The agent-sandbox Sandbox's name, `sbx-<16 base32>-g<volume_gen>` (`podName`). */
359
+ k8sName: textC(),
360
+ /** Bumped by every reset: a new Sandbox on a new volume. */
361
+ volumeGen: integer().notNull().default(0),
362
+ /** `running`, `suspended` or `deleted`. */
363
+ desired: text({ enum: ["running", "suspended", "deleted"] }),
364
+ /** `creating`, `running`, `suspended`, `expired`, `lost`, `failed` or `deleting`. */
365
+ observed: text({
366
+ enum: ["creating", "running", "suspended", "expired", "lost", "failed", "deleting"],
367
+ }),
368
+ /** The UID of the pod that last joined. */
369
+ podUid: text(),
370
+ /**
371
+ * Bumped by every join (`host.join`), relaunch, loss and reset: host and egress tokens carry
372
+ * it (`epc`), and one of an older epoch is refused (egress-gate reads it too).
373
+ */
374
+ hostEpoch: integer().notNull().default(0),
375
+ /** sha256 (hex) of the incarnation's join token; the token itself is never stored. */
376
+ joinTokenHash: text(),
377
+ /** Bumped by every change the Runtime asks of the pod: the sandboxes service's `opId`. */
378
+ rev: integer().notNull().default(0),
379
+ /** When a turn last ended on it, for the idle timer. */
380
+ lastActiveAt: timestamp({ withTimezone: true }),
381
+ /** When its TTL passes (`createdAt + lifecycle.ttl`). */
382
+ expiresAt: timestamp({ withTimezone: true }),
383
+ /** When the current incarnation was asked to run, for the ready deadline. */
384
+ startedAt: timestamp({ withTimezone: true }),
385
+ /** Why it failed or was lost. */
386
+ reason: text(),
387
+ /** An earlier incarnation's Sandbox still to delete (after a reset or a loss). */
388
+ retiring: textC(),
389
+ /**
390
+ * The id the engine keeps on the volume (`/harness/volume-id`), recorded at the first join:
391
+ * a join with another one means the volume was replaced (its files are gone), so `lost`.
392
+ */
393
+ volumeId: text(),
394
+ });
395
+ /**
396
+ * Artifacts (blueprint D35, F8.1 and F8.2): an id, a name and numbered versions. The bytes are in
397
+ * the Object store under each version's `blobKey` (a folder's: its manifest); a blob counts only
398
+ * once its version row commits. `sessionId` is the session the artifact belongs to, or null for
399
+ * a Tenant-wide one an application made. `latestVersion` is the newest version's number.
400
+ */
401
+ export const artifacts = nylorun.table("artifacts", {
402
+ id: textC().primaryKey(),
403
+ kind: text({ enum: ["file", "folder"] }).notNull(),
404
+ name: text().notNull(),
405
+ contentType: text().notNull(),
406
+ sessionId: textC(),
407
+ latestVersion: integer().notNull(),
408
+ /** Labels as JSON text, or null. */
409
+ labelsJson: text(),
410
+ createdAt: textC().notNull(),
411
+ updatedAt: text().notNull(),
412
+ }, (t) => [
413
+ index("artifacts_session").on(t.sessionId, t.createdAt, t.id),
414
+ check("artifacts_kind_check", sql `kind IN ('file', 'folder')`),
415
+ ]);
416
+ /** The versions of an artifact, immutable once written. */
417
+ export const artifactVersions = nylorun.table("artifact_versions", {
418
+ artifactId: textC().notNull(),
419
+ version: integer().notNull(),
420
+ /** Where the bytes are in the Object store (`BlobStore`). */
421
+ blobKey: text().notNull(),
422
+ size: bigint({ mode: "number" }).notNull(),
423
+ sha256: text().notNull(),
424
+ contentType: text().notNull(),
425
+ source: text({ enum: ["upload", "engine", "export"] }).notNull(),
426
+ createdAt: textC().notNull(),
427
+ }, (t) => [
428
+ primaryKey({ name: "artifact_versions_pkey", columns: [t.artifactId, t.version] }),
429
+ foreignKey({
430
+ name: "artifact_versions_artifact_id_fkey",
431
+ columns: [t.artifactId],
432
+ foreignColumns: [artifacts.id],
433
+ }).onDelete("cascade"),
434
+ ]);
435
+ /**
436
+ * The content-addressed files each folder version references (F8.2), one row per distinct
437
+ * SHA-256: the index that says which `blobs/sha256/<hex>` objects are still in use, so deleting
438
+ * a folder removes only the files no other version names, and the Tenant total counts each file
439
+ * once. The manifest itself stays the source of truth for paths.
440
+ */
441
+ export const artifactContent = nylorun.table("artifact_content", {
442
+ artifactId: textC().notNull(),
443
+ version: integer().notNull(),
444
+ sha256: textC().notNull(),
445
+ size: bigint({ mode: "number" }).notNull(),
446
+ }, (t) => [
447
+ primaryKey({ name: "artifact_content_pkey", columns: [t.artifactId, t.version, t.sha256] }),
448
+ foreignKey({
449
+ name: "artifact_content_version_fkey",
450
+ columns: [t.artifactId, t.version],
451
+ foreignColumns: [artifactVersions.artifactId, artifactVersions.version],
452
+ }).onDelete("cascade"),
453
+ index("artifact_content_sha256").on(t.sha256),
454
+ ]);
338
455
  // ---------------------------------------------------------------------------
339
456
  // nylorun_streams: the record
457
+ /**
458
+ * Each sandbox's lifecycle stream (`nylorun.sandbox-event/1`), numbered from 0 per sandbox id.
459
+ * Written only by the record module; not published to the stream relay.
460
+ */
461
+ export const sandboxEvents = nylorunStreams.table("sandbox_events", {
462
+ sandboxId: textC().notNull(),
463
+ seq: bigint({ mode: "number" }).notNull(),
464
+ type: text().notNull(),
465
+ body: jsonText().notNull(),
466
+ committedAt: timestamp({ withTimezone: true, mode: "string" }).notNull().defaultNow(),
467
+ }, (t) => [primaryKey({ name: "sandbox_events_pkey", columns: [t.sandboxId, t.seq] })]);
340
468
  /** Each session's log head: the next seq and the basin generation its events go to. */
341
469
  export const sessionLogHeads = nylorunStreams.table("session_log_heads", {
342
470
  sessionId: textC().primaryKey(),
@@ -32,6 +32,9 @@
32
32
  * head).
33
33
  * 2. A transaction that touches several sessions locks them in ascending id
34
34
  * order, with `lockSessions` (`./locking.ts`), before writing any of them.
35
+ * 3. A sandbox resource's row (`sandboxResource(id, { lock: true })`, which orders its
36
+ * lifecycle stream) is locked after every session row the transaction locks, never
37
+ * before one.
35
38
  *
36
39
  * Serialization failures and deadlocks are not retried here: with the order
37
40
  * above, READ COMMITTED transactions do not raise them. A `40P01` means a
@@ -47,17 +50,19 @@
47
50
  */
48
51
  import { AsyncLocalStorage } from "node:async_hooks";
49
52
  import { and, count, eq, getTableColumns, gte, inArray, isNotNull, isNull, lt, lte, ne, or, sql, } from "drizzle-orm";
50
- import { appendEvent } from "../../record/index.js";
53
+ import { appendEvent, appendSandboxEvent } from "../../record/index.js";
51
54
  import { OwnershipLostError } from "../ownership.js";
52
55
  import { database, driverError } from "./db.js";
53
56
  import { expectedSchemaVersion, readSchemaVersion } from "./migrate.js";
54
57
  import { createPostgresRecordReader } from "./record.js";
55
- import { postgresRecordWriter } from "./record-writer.js";
56
- import { actions, commands, definitions, effects, endpoints, links, modelBudgets, modelUsage, principals, publishableKeys, sandboxes, sessionEvents, sessionLogHeads, sessions, toolCrossings, signingKeys, subjectEpochs, subjectUsage, tenant, tenantSettings, vaultAudit, vaultCredentials, vaultIdempotency, vaults, } from "./schema.js";
58
+ import { postgresRecordWriter, postgresSandboxRecordWriter } from "./record-writer.js";
59
+ import { actions, artifacts, artifactVersions, artifactContent, commands, definitions, effects, endpoints, links, modelBudgets, modelUsage, principals, publishableKeys, sandboxes, sandboxEvents, sandboxResources, sessionEvents, sessionLogHeads, sessions, toJson, toolCrossings, signingKeys, subjectEpochs, subjectUsage, tenant, tenantSettings, vaultAudit, vaultCredentials, vaultIdempotency, vaults, } from "./schema.js";
57
60
  export function createPostgresSessionStore(options) {
58
61
  return new PostgresSessionStore(options);
59
62
  }
60
63
  const OPEN_SESSION = ["running", "runnable"];
64
+ /** Rows per statement when a folder version names many files. */
65
+ const CONTENT_BATCH = 1000;
61
66
  const DOCUMENTS = {
62
67
  definitions,
63
68
  sessions: sessions,
@@ -187,6 +192,57 @@ function ownership(row) {
187
192
  ownerExpiresAt: row.ownerExpiresAt?.toISOString() ?? null,
188
193
  };
189
194
  }
195
+ function sandboxResourceOf(row) {
196
+ return {
197
+ id: row.id,
198
+ kind: row.kind,
199
+ spec: row.spec,
200
+ labels: row.labels,
201
+ createdAt: row.createdAt,
202
+ updatedAt: row.updatedAt,
203
+ ...(row.k8sName !== null && row.desired !== null && row.observed !== null
204
+ ? {
205
+ pod: {
206
+ k8sName: row.k8sName,
207
+ volumeGen: row.volumeGen,
208
+ desired: row.desired,
209
+ observed: row.observed,
210
+ ...(row.podUid !== null ? { podUid: row.podUid } : {}),
211
+ hostEpoch: row.hostEpoch,
212
+ ...(row.joinTokenHash !== null ? { joinTokenHash: row.joinTokenHash } : {}),
213
+ rev: row.rev,
214
+ ...(row.lastActiveAt ? { lastActiveAt: row.lastActiveAt.toISOString() } : {}),
215
+ ...(row.expiresAt ? { expiresAt: row.expiresAt.toISOString() } : {}),
216
+ ...(row.startedAt ? { startedAt: row.startedAt.toISOString() } : {}),
217
+ ...(row.reason !== null ? { reason: row.reason } : {}),
218
+ ...(row.retiring !== null ? { retiring: row.retiring } : {}),
219
+ ...(row.volumeId !== null ? { volumeId: row.volumeId } : {}),
220
+ },
221
+ }
222
+ : {}),
223
+ };
224
+ }
225
+ /** A pod patch as column values: ISO times become dates, `null` clears. */
226
+ function podColumns(patch) {
227
+ const date = (value) => value === undefined ? undefined : value === null ? null : new Date(value);
228
+ const columns = {
229
+ k8sName: patch.k8sName,
230
+ volumeGen: patch.volumeGen,
231
+ desired: patch.desired,
232
+ observed: patch.observed,
233
+ podUid: patch.podUid,
234
+ hostEpoch: patch.hostEpoch,
235
+ joinTokenHash: patch.joinTokenHash,
236
+ rev: patch.rev,
237
+ lastActiveAt: date(patch.lastActiveAt),
238
+ expiresAt: date(patch.expiresAt),
239
+ startedAt: date(patch.startedAt),
240
+ reason: patch.reason,
241
+ retiring: patch.retiring,
242
+ volumeId: patch.volumeId,
243
+ };
244
+ return Object.fromEntries(Object.entries(columns).filter(([, value]) => value !== undefined));
245
+ }
190
246
  /** An endpoint row with its unset (null) columns left out, as `EndpointRow` has them. */
191
247
  function endpointRow(row) {
192
248
  return Object.fromEntries(Object.entries(row).filter(([, value]) => value !== null));
@@ -345,6 +401,18 @@ class PostgresTx {
345
401
  throw new OwnershipLostError(sessionId, epoch, session.epoch);
346
402
  return session;
347
403
  }
404
+ async runState(sessionId) {
405
+ this.check();
406
+ const [row] = await this.db
407
+ .select({
408
+ epoch: sessions.epoch,
409
+ status: sessions.status,
410
+ activeTurnId: sql `nylorun.doc(${sessions.body})->>'activeTurnId'`,
411
+ })
412
+ .from(sessions)
413
+ .where(eq(sessions.id, sessionId));
414
+ return row && { epoch: row.epoch, status: row.status ?? "", activeTurnId: row.activeTurnId ?? null };
415
+ }
348
416
  // --- typed queries -------------------------------------------------------
349
417
  async sessionsWithStatus(statuses) {
350
418
  this.check();
@@ -383,6 +451,91 @@ class PostgresTx {
383
451
  this.check();
384
452
  return this.bodies(sandboxes);
385
453
  }
454
+ // --- sandbox resources ---------------------------------------------------
455
+ async sandboxResource(id, options = {}) {
456
+ this.check();
457
+ const query = this.db.select().from(sandboxResources).where(eq(sandboxResources.id, id));
458
+ const [row] = options.lock ? await query.for("update") : await query;
459
+ return row && sandboxResourceOf(row);
460
+ }
461
+ async createSandboxResource(row, limit) {
462
+ this.check();
463
+ // Every create takes this lock, so two never both see room for one more.
464
+ await this.db.execute(sql `SELECT pg_advisory_xact_lock(hashtext('nylorun.sandbox_resources'))`);
465
+ const [found] = await this.db
466
+ .select({ id: sandboxResources.id })
467
+ .from(sandboxResources)
468
+ .where(eq(sandboxResources.id, row.id));
469
+ if (found)
470
+ return "exists";
471
+ const [counted] = await this.db.select({ n: count() }).from(sandboxResources);
472
+ if ((counted?.n ?? 0) >= limit)
473
+ return "limit";
474
+ const { pod, ...resource } = row;
475
+ await this.db.insert(sandboxResources).values({ ...resource, ...(pod ? podColumns(pod) : {}) });
476
+ return "created";
477
+ }
478
+ async updateSandboxSpec(id, spec, updatedAt) {
479
+ this.check();
480
+ await this.db.update(sandboxResources).set({ spec, updatedAt }).where(eq(sandboxResources.id, id));
481
+ }
482
+ async updateSandboxPod(id, patch, updatedAt) {
483
+ this.check();
484
+ await this.db
485
+ .update(sandboxResources)
486
+ .set({ ...podColumns(patch), updatedAt })
487
+ .where(eq(sandboxResources.id, id));
488
+ }
489
+ async updateSandboxLabels(id, labels, updatedAt) {
490
+ this.check();
491
+ await this.db
492
+ .update(sandboxResources)
493
+ .set({ labels, updatedAt })
494
+ .where(eq(sandboxResources.id, id));
495
+ }
496
+ async deleteSandboxResource(id) {
497
+ this.check();
498
+ await this.db.delete(sandboxResources).where(eq(sandboxResources.id, id));
499
+ }
500
+ async listSandboxResources(filter = {}) {
501
+ this.check();
502
+ const labels = filter.labels;
503
+ const rows = await this.db
504
+ .select()
505
+ .from(sandboxResources)
506
+ .where(labels === undefined || Object.keys(labels).length === 0
507
+ ? undefined
508
+ : sql `nylorun.doc(${sandboxResources.labels}) @> ${toJson(labels)}::jsonb`)
509
+ .orderBy(sandboxResources.id);
510
+ return rows.map(sandboxResourceOf);
511
+ }
512
+ async sessionsOnSandbox(sandboxId) {
513
+ this.check();
514
+ const rows = await this.sessions()
515
+ .where(eq(sessions.sandboxId, sandboxId))
516
+ .orderBy(sessions.id);
517
+ return rows.map((row) => storedSession(row));
518
+ }
519
+ async sandboxEvent(sandboxId, type, payload) {
520
+ this.check();
521
+ return appendSandboxEvent(postgresSandboxRecordWriter(this.db), {
522
+ tenantId: this.tenantId,
523
+ sandboxId,
524
+ time: this.now(),
525
+ type,
526
+ payload,
527
+ });
528
+ }
529
+ async sandboxEvents(sandboxId, options = {}) {
530
+ this.check();
531
+ const rows = await this.db
532
+ .select({ body: sandboxEvents.body })
533
+ .from(sandboxEvents)
534
+ .where(and(eq(sandboxEvents.sandboxId, sandboxId), options.fromSeq === undefined ? undefined : gte(sandboxEvents.seq, options.fromSeq)))
535
+ .orderBy(sandboxEvents.seq)
536
+ .limit(options.limit ?? 1000);
537
+ return rows.map((row) => row.body);
538
+ }
386
539
  async actionBodies(where) {
387
540
  const rows = await this.db
388
541
  .select({ body: actions.body })
@@ -906,6 +1059,132 @@ class PostgresTx {
906
1059
  .returning({ key: toolCrossings.key });
907
1060
  return deleted.length;
908
1061
  }
1062
+ // --- file artifacts --------------------------------------------------------
1063
+ async artifact(id, options = {}) {
1064
+ this.check();
1065
+ const query = this.db.select().from(artifacts).where(eq(artifacts.id, id));
1066
+ const [row] = options.lock ? await query.for("update") : await query;
1067
+ return row;
1068
+ }
1069
+ async listArtifacts(filter = {}) {
1070
+ this.check();
1071
+ return this.db
1072
+ .select()
1073
+ .from(artifacts)
1074
+ .where(filter.sessionId === undefined ? undefined : eq(artifacts.sessionId, filter.sessionId))
1075
+ .orderBy(artifacts.createdAt, artifacts.id);
1076
+ }
1077
+ async insertArtifact(row, version) {
1078
+ this.check();
1079
+ await this.db.insert(artifacts).values(row);
1080
+ await this.db.insert(artifactVersions).values(version);
1081
+ }
1082
+ async insertArtifactVersion(version) {
1083
+ this.check();
1084
+ await this.db.insert(artifactVersions).values(version);
1085
+ await this.db
1086
+ .update(artifacts)
1087
+ .set({
1088
+ latestVersion: version.version,
1089
+ contentType: version.contentType,
1090
+ updatedAt: version.createdAt,
1091
+ })
1092
+ .where(eq(artifacts.id, version.artifactId));
1093
+ }
1094
+ async artifactVersion(artifactId, version) {
1095
+ this.check();
1096
+ const [row] = await this.db
1097
+ .select()
1098
+ .from(artifactVersions)
1099
+ .where(and(eq(artifactVersions.artifactId, artifactId), eq(artifactVersions.version, version)));
1100
+ return row;
1101
+ }
1102
+ async artifactVersions(artifactId) {
1103
+ this.check();
1104
+ return this.db
1105
+ .select()
1106
+ .from(artifactVersions)
1107
+ .where(eq(artifactVersions.artifactId, artifactId))
1108
+ .orderBy(artifactVersions.version);
1109
+ }
1110
+ async deleteArtifact(id) {
1111
+ this.check();
1112
+ const deleted = await this.db
1113
+ .delete(artifactVersions)
1114
+ .where(eq(artifactVersions.artifactId, id))
1115
+ .returning({ blobKey: artifactVersions.blobKey });
1116
+ await this.db.delete(artifacts).where(eq(artifacts.id, id));
1117
+ return deleted.map((row) => row.blobKey);
1118
+ }
1119
+ async lockArtifactQuota() {
1120
+ this.check();
1121
+ await this.db.execute(sql `SELECT pg_advisory_xact_lock(hashtext('nylorun.artifacts'))`);
1122
+ }
1123
+ async artifactBytes() {
1124
+ this.check();
1125
+ const [files] = await this.db
1126
+ .select({
1127
+ bytes: sql `coalesce(sum(${artifactVersions.size}), 0)::bigint`.mapWith(Number),
1128
+ })
1129
+ .from(artifactVersions)
1130
+ .innerJoin(artifacts, eq(artifacts.id, artifactVersions.artifactId))
1131
+ .where(eq(artifacts.kind, "file"));
1132
+ // A folder's files are stored once per distinct hash, however many versions name them.
1133
+ const distinct = this.db
1134
+ .select({ size: sql `max(${artifactContent.size})`.as("size") })
1135
+ .from(artifactContent)
1136
+ .groupBy(artifactContent.sha256)
1137
+ .as("distinct_content");
1138
+ const [content] = await this.db
1139
+ .select({ bytes: sql `coalesce(sum(${distinct.size}), 0)::bigint`.mapWith(Number) })
1140
+ .from(distinct);
1141
+ return files.bytes + content.bytes;
1142
+ }
1143
+ async insertArtifactContent(artifactId, version, files) {
1144
+ this.check();
1145
+ for (let at = 0; at < files.length; at += CONTENT_BATCH)
1146
+ await this.db
1147
+ .insert(artifactContent)
1148
+ .values(files
1149
+ .slice(at, at + CONTENT_BATCH)
1150
+ .map((file) => ({ artifactId, version, sha256: file.sha256, size: file.size })))
1151
+ .onConflictDoNothing();
1152
+ }
1153
+ async referencedArtifactContent(shas) {
1154
+ this.check();
1155
+ const found = new Set();
1156
+ for (let at = 0; at < shas.length; at += CONTENT_BATCH) {
1157
+ const rows = await this.db
1158
+ .selectDistinct({ sha256: artifactContent.sha256 })
1159
+ .from(artifactContent)
1160
+ .where(inArray(artifactContent.sha256, shas.slice(at, at + CONTENT_BATCH)));
1161
+ for (const row of rows)
1162
+ found.add(row.sha256);
1163
+ }
1164
+ return found;
1165
+ }
1166
+ async artifactContentShas(scope) {
1167
+ this.check();
1168
+ const rows = await this.db
1169
+ .selectDistinct({ sha256: artifactContent.sha256 })
1170
+ .from(artifactContent)
1171
+ .innerJoin(artifacts, eq(artifacts.id, artifactContent.artifactId))
1172
+ .where(scope === "all"
1173
+ ? undefined
1174
+ : scope === "sessions"
1175
+ ? isNotNull(artifacts.sessionId)
1176
+ : eq(artifacts.id, scope.artifactId));
1177
+ return rows.map((row) => row.sha256);
1178
+ }
1179
+ async artifactBlobKeys(scope) {
1180
+ this.check();
1181
+ const rows = await this.db
1182
+ .select({ blobKey: artifactVersions.blobKey })
1183
+ .from(artifactVersions)
1184
+ .innerJoin(artifacts, eq(artifacts.id, artifactVersions.artifactId))
1185
+ .where(scope === "sessions" ? isNotNull(artifacts.sessionId) : undefined);
1186
+ return rows.map((row) => row.blobKey);
1187
+ }
909
1188
  // --- settings ------------------------------------------------------------
910
1189
  async getSetting(key) {
911
1190
  this.check();
@@ -931,6 +1210,8 @@ class PostgresTx {
931
1210
  await db.delete(table);
932
1211
  await db.delete(subjectUsage);
933
1212
  await db.delete(toolCrossings);
1213
+ // A session's artifacts go with it (their versions by cascade).
1214
+ await db.delete(artifacts).where(isNotNull(artifacts.sessionId));
934
1215
  // The record goes with the sessions, and the Tenant moves to a new basin: the ids it
935
1216
  // frees start again in an empty one (Durable Streams §8.1).
936
1217
  await db.delete(sessionEvents);
@@ -940,13 +1221,17 @@ class PostgresTx {
940
1221
  basinGeneration: sql `${tenant.basinGeneration} + 1`,
941
1222
  });
942
1223
  }
943
- if (scope === "sandboxes" || scope === "all")
1224
+ if (scope === "sandboxes" || scope === "all") {
944
1225
  await db.delete(sandboxes);
1226
+ await db.delete(sandboxResources);
1227
+ await db.delete(sandboxEvents);
1228
+ }
945
1229
  if (scope === "all") {
946
1230
  await db.delete(definitions);
947
1231
  await db.delete(endpoints);
948
1232
  await db.delete(modelUsage);
949
1233
  await db.delete(modelBudgets);
1234
+ await db.delete(artifacts);
950
1235
  await db.delete(vaults).where(ne(vaults.scope, "host"));
951
1236
  }
952
1237
  }