@nylorun/runtime 0.23.0-beta → 0.24.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 (252) hide show
  1. package/CHANGELOG.md +148 -0
  2. package/README.md +79 -28
  3. package/dist/adapters/execution/restate.js +1 -1
  4. package/dist/adapters/replication/pgoutput.d.ts +2 -0
  5. package/dist/adapters/replication/pgoutput.js +6 -3
  6. package/dist/adapters/sandbox/local.d.ts +9 -1
  7. package/dist/adapters/sandbox/local.js +16 -4
  8. package/dist/adapters/sandbox/virtual.js +4 -1
  9. package/dist/api/a2a/endpoint.js +5 -1
  10. package/dist/api/a2a/routes.d.ts +2 -0
  11. package/dist/api/a2a/routes.js +2 -2
  12. package/dist/api/a2a/tasks.js +2 -1
  13. package/dist/api/ag-ui/endpoint.js +4 -2
  14. package/dist/api/ag-ui/history.js +4 -1
  15. package/dist/api/ag-ui/routes.d.ts +4 -1
  16. package/dist/api/ag-ui/routes.js +11 -10
  17. package/dist/api/ag-ui/translate.js +3 -2
  18. package/dist/api/components.d.ts +4953 -1204
  19. package/dist/api/components.js +25 -16
  20. package/dist/api/gate/keys.js +7 -1
  21. package/dist/api/gate/routes.d.ts +6 -0
  22. package/dist/api/gate/routes.js +39 -3
  23. package/dist/api/http/app.js +4 -4
  24. package/dist/api/http/define.d.ts +6 -0
  25. package/dist/api/http/define.js +13 -1
  26. package/dist/api/http/respond.js +1 -1
  27. package/dist/api/http/routes/artifacts.js +35 -8
  28. package/dist/api/http/routes/coverage.d.ts +9 -0
  29. package/dist/api/http/routes/coverage.js +34 -0
  30. package/dist/api/http/routes/environments.d.ts +11 -0
  31. package/dist/api/http/routes/environments.js +283 -0
  32. package/dist/api/http/routes/mcp.d.ts +1 -1
  33. package/dist/api/http/routes/mcp.js +2 -2
  34. package/dist/api/http/routes/me.js +1 -1
  35. package/dist/api/http/routes/reads.d.ts +45 -9
  36. package/dist/api/http/routes/reads.js +54 -5
  37. package/dist/api/http/routes/sessions.js +11 -6
  38. package/dist/api/http/routes/tenant.js +32 -10
  39. package/dist/api/openapi.js +31 -26
  40. package/dist/artifacts/disk.d.ts +59 -0
  41. package/dist/artifacts/disk.js +563 -0
  42. package/dist/artifacts/export.d.ts +1 -1
  43. package/dist/artifacts/export.js +2 -3
  44. package/dist/artifacts/folders.d.ts +6 -3
  45. package/dist/artifacts/folders.js +17 -12
  46. package/dist/artifacts/service.d.ts +2 -1
  47. package/dist/artifacts/service.js +6 -1
  48. package/dist/artifacts/workspace.d.ts +1 -1
  49. package/dist/artifacts/workspace.js +1 -1
  50. package/dist/core/flow-host.d.ts +2 -3
  51. package/dist/core/flow-host.js +3 -0
  52. package/dist/core/provider.d.ts +3 -2
  53. package/dist/core/provider.js +5 -37
  54. package/dist/core/sandbox-routes.d.ts +2 -21
  55. package/dist/core/sandbox-routes.js +10 -43
  56. package/dist/execution/types.d.ts +1 -1
  57. package/dist/gates/contract.d.ts +3 -24
  58. package/dist/gates/contract.js +1 -36
  59. package/dist/gates/egress-credentials.d.ts +9 -0
  60. package/dist/gates/egress-credentials.js +11 -4
  61. package/dist/gates/egress.d.ts +28 -1
  62. package/dist/gates/egress.js +115 -10
  63. package/dist/gates/fixture-model.d.ts +28 -0
  64. package/dist/gates/fixture-model.js +92 -0
  65. package/dist/gates/handler.js +4 -4
  66. package/dist/gates/http-tool.d.ts +6 -2
  67. package/dist/gates/http-tool.js +3 -5
  68. package/dist/gates/in-process.d.ts +5 -0
  69. package/dist/gates/in-process.js +12 -1
  70. package/dist/gates/journal.d.ts +30 -0
  71. package/dist/gates/journal.js +52 -0
  72. package/dist/gates/mcp-handler.d.ts +4 -0
  73. package/dist/gates/mcp-handler.js +5 -1
  74. package/dist/gates/meter.d.ts +10 -2
  75. package/dist/gates/meter.js +20 -5
  76. package/dist/gates/policy.d.ts +49 -0
  77. package/dist/gates/policy.js +134 -0
  78. package/dist/gates/tenant-vaults.d.ts +7 -7
  79. package/dist/gates/tenant-vaults.js +5 -5
  80. package/dist/gates/tool-calls.d.ts +10 -5
  81. package/dist/gates/tool-calls.js +28 -10
  82. package/dist/gates/tool-client.d.ts +8 -0
  83. package/dist/gates/tool-client.js +27 -11
  84. package/dist/gates/tool-contract.d.ts +10 -68
  85. package/dist/gates/tool-contract.js +1 -85
  86. package/dist/gates/tool-gate.d.ts +1 -1
  87. package/dist/harness/calls.d.ts +12 -5
  88. package/dist/harness/calls.js +21 -12
  89. package/dist/harness/executors.js +12 -9
  90. package/dist/harness/main.js +18 -5
  91. package/dist/harness/pod.d.ts +34 -0
  92. package/dist/harness/pod.js +41 -0
  93. package/dist/harness/service.d.ts +0 -1
  94. package/dist/harness/service.js +13 -5
  95. package/dist/harness/setup.d.ts +27 -0
  96. package/dist/harness/setup.js +150 -0
  97. package/dist/harness/ws-client.js +3 -2
  98. package/dist/harness/ws-port.d.ts +1 -5
  99. package/dist/harness/ws-port.js +1 -5
  100. package/dist/harness-api/in-process.js +3 -3
  101. package/dist/harness-api/record.d.ts +15 -1
  102. package/dist/harness-api/record.js +174 -44
  103. package/dist/harness-api/redact.d.ts +3 -0
  104. package/dist/harness-api/redact.js +45 -0
  105. package/dist/harness-api/server.d.ts +8 -0
  106. package/dist/harness-api/server.js +93 -13
  107. package/dist/harness-api/start.d.ts +10 -3
  108. package/dist/harness-api/start.js +40 -10
  109. package/dist/harness-api/tool-journal.d.ts +96 -0
  110. package/dist/harness-api/tool-journal.js +102 -0
  111. package/dist/harness-api/tool-status.d.ts +13 -0
  112. package/dist/harness-api/tool-status.js +26 -0
  113. package/dist/harness-api/workspace.d.ts +6 -3
  114. package/dist/harness-api/workspace.js +27 -11
  115. package/dist/harness-api/ws-server.d.ts +11 -3
  116. package/dist/harness-api/ws-server.js +94 -15
  117. package/dist/host/gates.d.ts +2 -1
  118. package/dist/host/gates.js +10 -2
  119. package/dist/host/main.js +54 -5
  120. package/dist/host/operate.js +67 -5
  121. package/dist/host/stack-config.d.ts +64 -0
  122. package/dist/host/stack-config.js +61 -1
  123. package/dist/keys/client.js +4 -1
  124. package/dist/keys/keys.d.ts +5 -5
  125. package/dist/keys/keys.js +5 -7
  126. package/dist/management-openapi.json +999 -2211
  127. package/dist/mcp/connect.d.ts +25 -20
  128. package/dist/mcp/connect.js +29 -30
  129. package/dist/mcp/pool.d.ts +2 -2
  130. package/dist/mcp/pool.js +1 -1
  131. package/dist/mcp/preview.d.ts +3 -3
  132. package/dist/mcp/preview.js +10 -20
  133. package/dist/mcp/snapshot.d.ts +5 -11
  134. package/dist/mcp/snapshot.js +6 -7
  135. package/dist/model/pi-model.js +8 -0
  136. package/dist/openapi.json +6955 -2526
  137. package/dist/reads/types.d.ts +16 -3
  138. package/dist/record/append.d.ts +3 -1
  139. package/dist/record/append.js +1 -0
  140. package/dist/record/envelope.d.ts +5 -1
  141. package/dist/record/envelope.js +7 -3
  142. package/dist/record/environment.d.ts +25 -0
  143. package/dist/record/environment.js +44 -0
  144. package/dist/record/index.d.ts +1 -1
  145. package/dist/record/index.js +1 -1
  146. package/dist/sandbox/disk-fs.d.ts +95 -0
  147. package/dist/sandbox/disk-fs.js +206 -0
  148. package/dist/sandbox/join.d.ts +17 -18
  149. package/dist/sandbox/join.js +42 -15
  150. package/dist/sandbox/manager.d.ts +16 -8
  151. package/dist/sandbox/manager.js +33 -17
  152. package/dist/sandbox/placement.d.ts +13 -12
  153. package/dist/sandbox/placement.js +29 -12
  154. package/dist/sandbox/pods/client.d.ts +6 -1
  155. package/dist/sandbox/pods/lifecycle.d.ts +28 -27
  156. package/dist/sandbox/pods/lifecycle.js +14 -26
  157. package/dist/sandbox/pods/reconcile.js +18 -12
  158. package/dist/sandbox/pods/spec.d.ts +15 -13
  159. package/dist/sandbox/pods/spec.js +16 -20
  160. package/dist/sandbox/records.d.ts +2 -2
  161. package/dist/sandbox/records.js +3 -3
  162. package/dist/sandbox/resolve.d.ts +35 -27
  163. package/dist/sandbox/resolve.js +66 -41
  164. package/dist/sandbox/session-sandbox.js +1 -1
  165. package/dist/sandbox/setup.d.ts +61 -0
  166. package/dist/sandbox/setup.js +123 -0
  167. package/dist/sandbox/share.d.ts +11 -21
  168. package/dist/sandbox/share.js +12 -41
  169. package/dist/sandbox/tenant-config.d.ts +12 -14
  170. package/dist/sandbox/tenant-config.js +12 -17
  171. package/dist/sandbox/types.d.ts +6 -0
  172. package/dist/store/postgres/drizzle/0020_environments.sql +63 -0
  173. package/dist/store/postgres/drizzle/0021_disk_versions.sql +7 -0
  174. package/dist/store/postgres/drizzle/0022_model_usage_timing.sql +5 -0
  175. package/dist/store/postgres/drizzle/0023_artifact_pages.sql +1 -0
  176. package/dist/store/postgres/drizzle/meta/_journal.json +28 -0
  177. package/dist/store/postgres/reads.js +117 -17
  178. package/dist/store/postgres/record-writer.d.ts +4 -4
  179. package/dist/store/postgres/record-writer.js +9 -9
  180. package/dist/store/postgres/record.d.ts +7 -0
  181. package/dist/store/postgres/record.js +23 -2
  182. package/dist/store/postgres/schema.d.ts +934 -761
  183. package/dist/store/postgres/schema.js +102 -73
  184. package/dist/store/postgres/store.js +119 -66
  185. package/dist/store/types.d.ts +109 -48
  186. package/dist/tenant/advance.d.ts +7 -6
  187. package/dist/tenant/advance.js +115 -42
  188. package/dist/tenant/artifact-tool.js +3 -4
  189. package/dist/tenant/commands.d.ts +15 -2
  190. package/dist/tenant/commands.js +93 -12
  191. package/dist/tenant/context.d.ts +48 -22
  192. package/dist/tenant/context.js +4 -15
  193. package/dist/tenant/effects.d.ts +1 -1
  194. package/dist/tenant/effects.js +37 -20
  195. package/dist/tenant/environments.d.ts +167 -0
  196. package/dist/tenant/environments.js +823 -0
  197. package/dist/tenant/harness-tokens.d.ts +21 -0
  198. package/dist/tenant/harness-tokens.js +61 -0
  199. package/dist/tenant/host-token.d.ts +1 -1
  200. package/dist/tenant/host-token.js +2 -4
  201. package/dist/tenant/identity-file.d.ts +4 -4
  202. package/dist/tenant/identity-file.js +17 -8
  203. package/dist/tenant/issuers.js +3 -3
  204. package/dist/tenant/principals.js +1 -1
  205. package/dist/tenant/reset.js +3 -3
  206. package/dist/tenant/runtime.d.ts +6 -1
  207. package/dist/tenant/runtime.js +7 -1
  208. package/dist/tenant/session.d.ts +3 -3
  209. package/dist/tenant/session.js +0 -14
  210. package/dist/tenant/sessions.d.ts +13 -13
  211. package/dist/tenant/sessions.js +61 -138
  212. package/dist/tenant/signing-keys.d.ts +5 -1
  213. package/dist/tenant/status.js +3 -3
  214. package/dist/tenant/sweep.js +12 -6
  215. package/dist/tenant/transcript.d.ts +210 -20
  216. package/dist/tenant/transcript.js +62 -11
  217. package/dist/tenant/turn-timing.d.ts +43 -0
  218. package/dist/tenant/turn-timing.js +39 -0
  219. package/dist/tenant/types.d.ts +2 -0
  220. package/dist/traces/ids.d.ts +42 -0
  221. package/dist/traces/ids.js +85 -0
  222. package/dist/traces/key.d.ts +6 -0
  223. package/dist/traces/key.js +37 -0
  224. package/dist/traces/map.d.ts +113 -0
  225. package/dist/traces/map.js +317 -0
  226. package/dist/traces/options.d.ts +7 -0
  227. package/dist/traces/options.js +1 -0
  228. package/dist/traces/otlp.d.ts +42 -0
  229. package/dist/traces/otlp.js +84 -0
  230. package/dist/traces/projector.d.ts +58 -0
  231. package/dist/traces/projector.js +200 -0
  232. package/dist/traces/read.d.ts +23 -0
  233. package/dist/traces/read.js +16 -0
  234. package/dist/vault/coverage.d.ts +26 -0
  235. package/dist/vault/coverage.js +64 -0
  236. package/dist/vault/error.d.ts +11 -1
  237. package/dist/vault/error.js +5 -1
  238. package/dist/vault/service.d.ts +47 -48
  239. package/dist/vault/service.js +321 -382
  240. package/dist/vault/sources.d.ts +5 -16
  241. package/dist/vault/sources.js +6 -8
  242. package/dist/version.d.ts +1 -1
  243. package/dist/version.js +2 -2
  244. package/package.json +13 -13
  245. package/dist/api/http/routes/sandboxes.d.ts +0 -9
  246. package/dist/api/http/routes/sandboxes.js +0 -153
  247. package/dist/api/http/routes/vaults.d.ts +0 -10
  248. package/dist/api/http/routes/vaults.js +0 -139
  249. package/dist/record/sandbox.d.ts +0 -25
  250. package/dist/record/sandbox.js +0 -43
  251. package/dist/tenant/sandboxes.d.ts +0 -123
  252. package/dist/tenant/sandboxes.js +0 -566
@@ -69,14 +69,14 @@ export const sessions = nylorun.table("sessions", {
69
69
  ownerExpiresAt: timestamp({ withTimezone: true, mode: "date" }),
70
70
  /** The session owner (`ownerUserId`), not the lease column `owner`. */
71
71
  ownerUserId: field("->>'ownerUserId'"),
72
- /** The sandbox resource the session is attached to (`sandbox: { id }`). */
73
- sandboxId: field("->>'sandboxId'"),
72
+ /** The Environment the session names (`environment`, protocol 11). */
73
+ environmentId: field("->>'environmentId'"),
74
74
  }, (t) => [
75
75
  index("sessions_created").on(t.createdAt.desc().nullsLast(), t.id.desc().nullsFirst()),
76
76
  index("sessions_status").on(t.status, t.ownerExpiresAt),
77
77
  index("sessions_agent").on(t.agentId),
78
78
  index("sessions_owner_user").on(t.ownerUserId),
79
- index("sessions_sandbox").on(t.sandboxId),
79
+ index("sessions_environment").on(t.environmentId),
80
80
  ]);
81
81
  export const commands = documents("commands");
82
82
  export const effects = nylorun.table("effects", {
@@ -107,22 +107,92 @@ export const principals = nylorun.table("principals", {
107
107
  idempotencyKey: text(),
108
108
  createdAt: text().notNull(),
109
109
  });
110
+ /**
111
+ * An Environment (D55, protocol 11): its id, kind, labels, the recipe the app asked for and the
112
+ * recipe its compute runs (`applied`, copied from `recipe` when a stopped pod is asked to run),
113
+ * the owner guard, and when it was last used. Kind `pod` adds the pod's lifecycle columns.
114
+ * Sessions name it by id (the session body's `environmentId`). Not the `sandboxes` document
115
+ * table, which holds the virtual backend's compute records, keyed by workspace key.
116
+ */
117
+ export const environments = nylorun.table("environments", {
118
+ id: textC().primaryKey(),
119
+ kind: text({ enum: ["virtual", "pod", "none"] }).notNull(),
120
+ /** `{ key: value }`, filtered through `nylorun.doc(labels)`. */
121
+ labels: jsonText().notNull(),
122
+ /** What the app asked for (`EnvironmentRecipe`), resolved against the Tenant's limits. */
123
+ recipe: jsonText().notNull(),
124
+ /** The recipe the current pod or workspace runs. */
125
+ applied: jsonText().notNull(),
126
+ version: integer().notNull().default(1),
127
+ appliedVersion: integer().notNull().default(1),
128
+ /** The owner guard: only this owner's sessions may name the Environment. */
129
+ ownerUserId: text(),
130
+ /** A turn's end, a recipe change, a secret write, a stop or a reset (`lifecycle.deleteAfter`). */
131
+ lastUsedAt: timestamp({ withTimezone: true, mode: "string" }).notNull().defaultNow(),
132
+ createdAt: textC().notNull(),
133
+ updatedAt: textC().notNull(),
134
+ // Kind `pod` (F7.2, D34): null otherwise. `tenant/environments.ts` and the `Sandbox`
135
+ // object's reconcile (`sandbox/pods/reconcile.ts`) are the only writers.
136
+ /** The agent-sandbox Sandbox's name, `sbx-<16 base32>-g<volume_gen>` (`podName`). */
137
+ k8sName: textC(),
138
+ /** Bumped by every reset: a new Sandbox on a new volume. */
139
+ volumeGen: integer().notNull().default(0),
140
+ /** `running`, `suspended` or `deleted`. */
141
+ desired: text({ enum: ["running", "suspended", "deleted"] }),
142
+ /** `creating`, `running`, `suspended`, `lost`, `failed` or `deleting`. */
143
+ observed: text({ enum: ["creating", "running", "suspended", "lost", "failed", "deleting"] }),
144
+ /** The UID of the pod that last joined. */
145
+ podUid: text(),
146
+ /**
147
+ * Bumped by every join (`host.join`), relaunch, loss and reset: host and egress tokens carry
148
+ * it (`epc`), and one of an older epoch is refused (egress-gate reads it too).
149
+ */
150
+ hostEpoch: integer().notNull().default(0),
151
+ /** sha256 (hex) of the incarnation's join token; the token itself is never stored. */
152
+ joinTokenHash: text(),
153
+ /** Bumped by every change the Runtime asks of the pod: the sandboxes service's `opId`. */
154
+ rev: integer().notNull().default(0),
155
+ /** When a turn last ended on it, for the idle timer. */
156
+ lastActiveAt: timestamp({ withTimezone: true }),
157
+ /** When the current incarnation was asked to run, for the ready deadline. */
158
+ startedAt: timestamp({ withTimezone: true }),
159
+ /** Why it failed or was lost. */
160
+ reason: text(),
161
+ /** An earlier incarnation's Sandbox still to delete (after a reset or a loss). */
162
+ retiring: textC(),
163
+ /**
164
+ * The id the engine keeps on the volume (`/harness/volume-id`), recorded at the first join:
165
+ * a join with another one means the volume was replaced (its files are gone), so `lost`.
166
+ */
167
+ volumeId: text(),
168
+ /**
169
+ * The disk lease (R2d step 3): `{ kind, version?, startedAt, until }` while a snapshot, a
170
+ * restore or a seed copies the disk; turns are refused until it ends or `until` passes.
171
+ */
172
+ diskOp: jsonText(),
173
+ /** The disk version the next volume starts from: `{ artifactId, version }` (R2d step 3). */
174
+ diskSeed: jsonText(),
175
+ }, (t) => [check("environments_kind_check", sql `${t.kind} IN ('virtual', 'pod', 'none')`)]);
176
+ /**
177
+ * A vault (D56): the host model's (`id = 'host'`), or one Environment's, created and deleted with
178
+ * it (`environment_id`, unique, deleted with the Environment's row). No other vault exists.
179
+ */
110
180
  export const vaults = nylorun.table("vaults", {
111
181
  id: textC().primaryKey(),
112
182
  name: text().notNull(),
113
- ownerUserId: text().notNull(),
114
183
  /** JSON text, or null. */
115
184
  metadataJson: text(),
116
185
  createdAt: textC().notNull(),
117
- /**
118
- * `user`: one person's (owner `ownerUserId`); `installation`: the installation's own,
119
- * attachable to any session (owner `installation`); `host`: the model vault, never attached.
120
- */
121
- scope: text({ enum: ["user", "installation", "host"] }).notNull().default("user"),
186
+ /** The Environment the vault belongs to; null only for the host vault. */
187
+ environmentId: textC(),
122
188
  }, (t) => [
123
- index("vaults_owner").on(t.ownerUserId, t.createdAt, t.id),
124
- uniqueIndex("vaults_one_host").on(t.scope).where(sql `scope = 'host'`),
125
- check("vaults_scope_check", sql `scope IN ('user', 'installation', 'host')`),
189
+ uniqueIndex("vaults_environment").on(t.environmentId),
190
+ foreignKey({
191
+ name: "vaults_environment_id_fkey",
192
+ columns: [t.environmentId],
193
+ foreignColumns: [environments.id],
194
+ }).onDelete("cascade"),
195
+ check("vaults_owner_check", sql `(${t.id} = 'host') = (${t.environmentId} IS NULL)`),
126
196
  ]);
127
197
  /** A credential of a vault: its binding in the clear, its secret sealed. */
128
198
  export const vaultCredentials = nylorun.table("vault_credentials", {
@@ -146,6 +216,7 @@ export const vaultCredentials = nylorun.table("vault_credentials", {
146
216
  foreignColumns: [vaults.id],
147
217
  }).onDelete("cascade"),
148
218
  index("vault_credentials_vault").on(t.vaultId, t.createdAt, t.id),
219
+ uniqueIndex("vault_credentials_name").on(t.vaultId, t.name),
149
220
  ]);
150
221
  export const vaultAudit = nylorun.table("vault_audit", {
151
222
  ord: bigint({ mode: "number" }).primaryKey().generatedAlwaysAsIdentity(),
@@ -198,7 +269,9 @@ export const signingKeys = nylorun.table("signing_keys", {
198
269
  * The model usage ledger (P1.3): one row per model call a gate served, written after the call.
199
270
  * A second row for the same effect (a call the gate ran again after a gateway restart) is
200
271
  * flagged `duplicate`; both were billed. Budgets read their spend from it. Token counts are 0
201
- * when the provider sent none.
272
+ * when the provider sent none. `startedAt`, `endedAt` and `firstTokenMs` are the gate's own
273
+ * timing of the call (R7.1, migration 0022); null on rows written before, and `firstTokenMs`
274
+ * when the provider streamed nothing.
202
275
  */
203
276
  export const modelUsage = nylorun.table("model_usage", {
204
277
  id: textC().primaryKey(),
@@ -222,6 +295,12 @@ export const modelUsage = nylorun.table("model_usage", {
222
295
  /** True when an earlier row has the same `effectKey`: the provider billed the call twice. */
223
296
  duplicate: boolean().notNull(),
224
297
  createdAt: text().notNull(),
298
+ /** When the gate sent the call to the provider. */
299
+ startedAt: text(),
300
+ /** When the provider's answer was complete. */
301
+ endedAt: text(),
302
+ /** From `startedAt` to the provider's first stream event. */
303
+ firstTokenMs: integer(),
225
304
  }, (t) => [
226
305
  index("model_usage_session").on(t.sessionId, t.createdAt, t.id),
227
306
  index("model_usage_export").on(t.txid, t.id),
@@ -260,60 +339,6 @@ export const toolCrossings = nylorun.table("tool_crossings", {
260
339
  /** The gate's answer (`McpAnswer`), once the call ended; null while it runs or once lost. */
261
340
  answer: jsonText(),
262
341
  }, (t) => [index("tool_crossings_settled").on(t.settledAt)]);
263
- /**
264
- * A sandbox resource (blueprint D39, F7.1): its id, kind, the spec resolved against the
265
- * Tenant's limits when it was created, and the developer's labels. Sessions attach to it by id
266
- * (the session body's `sandboxId`). Not the `sandboxes` document table, which holds the virtual
267
- * backend's compute records, keyed by workspace key.
268
- */
269
- export const sandboxResources = nylorun.table("sandbox_resources", {
270
- id: textC().primaryKey(),
271
- kind: text({ enum: ["virtual", "pod"] }).notNull(),
272
- /** The resolved `SandboxManifest`. */
273
- spec: jsonText().notNull(),
274
- /** `{ key: value }`, filtered through `nylorun.doc(labels)`. */
275
- labels: jsonText().notNull(),
276
- createdAt: textC().notNull(),
277
- updatedAt: textC().notNull(),
278
- // Kind `pod` (F7.2, D34): null for a virtual sandbox. `tenant/sandboxes.ts` and the
279
- // `Sandbox` object's reconcile (`sandbox/pods/reconcile.ts`) are the only writers.
280
- /** The agent-sandbox Sandbox's name, `sbx-<16 base32>-g<volume_gen>` (`podName`). */
281
- k8sName: textC(),
282
- /** Bumped by every reset: a new Sandbox on a new volume. */
283
- volumeGen: integer().notNull().default(0),
284
- /** `running`, `suspended` or `deleted`. */
285
- desired: text({ enum: ["running", "suspended", "deleted"] }),
286
- /** `creating`, `running`, `suspended`, `expired`, `lost`, `failed` or `deleting`. */
287
- observed: text({
288
- enum: ["creating", "running", "suspended", "expired", "lost", "failed", "deleting"],
289
- }),
290
- /** The UID of the pod that last joined. */
291
- podUid: text(),
292
- /**
293
- * Bumped by every join (`host.join`), relaunch, loss and reset: host and egress tokens carry
294
- * it (`epc`), and one of an older epoch is refused (egress-gate reads it too).
295
- */
296
- hostEpoch: integer().notNull().default(0),
297
- /** sha256 (hex) of the incarnation's join token; the token itself is never stored. */
298
- joinTokenHash: text(),
299
- /** Bumped by every change the Runtime asks of the pod: the sandboxes service's `opId`. */
300
- rev: integer().notNull().default(0),
301
- /** When a turn last ended on it, for the idle timer. */
302
- lastActiveAt: timestamp({ withTimezone: true }),
303
- /** When its TTL passes (`createdAt + lifecycle.ttl`). */
304
- expiresAt: timestamp({ withTimezone: true }),
305
- /** When the current incarnation was asked to run, for the ready deadline. */
306
- startedAt: timestamp({ withTimezone: true }),
307
- /** Why it failed or was lost. */
308
- reason: text(),
309
- /** An earlier incarnation's Sandbox still to delete (after a reset or a loss). */
310
- retiring: textC(),
311
- /**
312
- * The id the engine keeps on the volume (`/harness/volume-id`), recorded at the first join:
313
- * a join with another one means the volume was replaced (its files are gone), so `lost`.
314
- */
315
- volumeId: text(),
316
- });
317
342
  /**
318
343
  * Artifacts (blueprint D35, F8.1 and F8.2): an id, a name and numbered versions. The bytes are in
319
344
  * the Object store under each version's `blobKey` (a folder's: its manifest); a blob counts only
@@ -331,8 +356,12 @@ export const artifacts = nylorun.table("artifacts", {
331
356
  labelsJson: text(),
332
357
  createdAt: textC().notNull(),
333
358
  updatedAt: text().notNull(),
359
+ /** The Environment whose disk versions this folder holds (R2d step 3); one per Environment. */
360
+ environmentId: textC(),
334
361
  }, (t) => [
335
362
  index("artifacts_session").on(t.sessionId, t.createdAt, t.id),
363
+ uniqueIndex("artifacts_environment_disk").on(t.environmentId).where(sql `environment_id IS NOT NULL`),
364
+ index("artifacts_creation_order").on(t.createdAt, t.id),
336
365
  check("artifacts_kind_check", sql `kind IN ('file', 'folder')`),
337
366
  ]);
338
367
  /** The versions of an artifact, immutable once written. */
@@ -344,7 +373,7 @@ export const artifactVersions = nylorun.table("artifact_versions", {
344
373
  size: bigint({ mode: "number" }).notNull(),
345
374
  sha256: text().notNull(),
346
375
  contentType: text().notNull(),
347
- source: text({ enum: ["upload", "engine", "export"] }).notNull(),
376
+ source: text({ enum: ["upload", "engine", "export", "snapshot"] }).notNull(),
348
377
  createdAt: textC().notNull(),
349
378
  }, (t) => [
350
379
  primaryKey({ name: "artifact_versions_pkey", columns: [t.artifactId, t.version] }),
@@ -471,16 +500,16 @@ export const sandboxSignals = nylorun.table("sandbox_signals", {
471
500
  // ---------------------------------------------------------------------------
472
501
  // nylorun_streams: the record
473
502
  /**
474
- * Each sandbox's lifecycle stream (`nylorun.sandbox-event/1`), numbered from 0 per sandbox id.
475
- * Written only by the record module; not published to the stream relay.
503
+ * Each Environment's lifecycle stream (`nylorun.environment-event/1`), numbered from 0 per
504
+ * Environment id. Written only by the record module; not published to the stream relay.
476
505
  */
477
- export const sandboxEvents = nylorunStreams.table("sandbox_events", {
478
- sandboxId: textC().notNull(),
506
+ export const environmentEvents = nylorunStreams.table("environment_events", {
507
+ environmentId: textC().notNull(),
479
508
  seq: bigint({ mode: "number" }).notNull(),
480
509
  type: text().notNull(),
481
510
  body: jsonText().notNull(),
482
511
  committedAt: timestamp({ withTimezone: true, mode: "string" }).notNull().defaultNow(),
483
- }, (t) => [primaryKey({ name: "sandbox_events_pkey", columns: [t.sandboxId, t.seq] })]);
512
+ }, (t) => [primaryKey({ name: "environment_events_pkey", columns: [t.environmentId, t.seq] })]);
484
513
  /** Each session's log head: the next seq and the basin generation its events go to. */
485
514
  export const sessionLogHeads = nylorunStreams.table("session_log_heads", {
486
515
  sessionId: textC().primaryKey(),
@@ -32,7 +32,7 @@
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
35
+ * 3. An Environment's row (`environment(id, { lock: true })`, which orders its
36
36
  * lifecycle stream) is locked after every session row the transaction locks, never
37
37
  * before one.
38
38
  *
@@ -51,14 +51,14 @@
51
51
  import { AsyncLocalStorage } from "node:async_hooks";
52
52
  import { randomUUID } from "node:crypto";
53
53
  import { and, count, eq, getTableColumns, gte, gt, inArray, isNotNull, isNull, lt, lte, ne, or, sql, } from "drizzle-orm";
54
- import { appendEvent, appendSandboxEvent } from "../../record/index.js";
54
+ import { appendEnvironmentEvent, appendEvent } from "../../record/index.js";
55
55
  import { OwnershipLostError, PrincipalRoleConflict } from "../ownership.js";
56
56
  import { followControlSignals, pruneSignals, writeSignal } from "./control.js";
57
57
  import { database, driverError } from "./db.js";
58
58
  import { expectedSchemaVersion, readSchemaVersion } from "./migrate.js";
59
59
  import { createPostgresRecordReader } from "./record.js";
60
- import { postgresRecordWriter, postgresSandboxRecordWriter } from "./record-writer.js";
61
- import { artifacts, artifactVersions, artifactContent, definitionFiles, definitionFileUses, commands, definitions, effects, links, modelBudgets, modelUsage, principals, sandboxes, sandboxEvents, sandboxResources, sessionEvents, sessionLogHeads, sessions, toJson, toolCrossings, signingKeys, tenant, tenantSettings, vaultAudit, vaultCredentials, vaultIdempotency, vaults, sandboxSignals, wakes, } from "./schema.js";
60
+ import { postgresEnvironmentRecordWriter, postgresRecordWriter } from "./record-writer.js";
61
+ import { artifacts, artifactVersions, artifactContent, definitionFiles, definitionFileUses, commands, definitions, effects, links, modelBudgets, modelUsage, principals, sandboxes, environmentEvents, environments, sessionEvents, sessionLogHeads, sessions, toJson, toolCrossings, signingKeys, tenant, tenantSettings, vaultAudit, vaultCredentials, vaultIdempotency, vaults, sandboxSignals, wakes, } from "./schema.js";
62
62
  export function createPostgresSessionStore(options) {
63
63
  return new PostgresSessionStore(options);
64
64
  }
@@ -240,14 +240,21 @@ function ownership(row) {
240
240
  ownerExpiresAt: row.ownerExpiresAt?.toISOString() ?? null,
241
241
  };
242
242
  }
243
- function sandboxResourceOf(row) {
243
+ function environmentOf(row) {
244
244
  return {
245
245
  id: row.id,
246
246
  kind: row.kind,
247
- spec: row.spec,
248
247
  labels: row.labels,
248
+ recipe: row.recipe,
249
+ applied: row.applied,
250
+ version: row.version,
251
+ appliedVersion: row.appliedVersion,
252
+ ...(row.ownerUserId !== null ? { ownerUserId: row.ownerUserId } : {}),
253
+ lastUsedAt: new Date(row.lastUsedAt).toISOString(),
249
254
  createdAt: row.createdAt,
250
255
  updatedAt: row.updatedAt,
256
+ ...(row.diskOp ? { diskOp: row.diskOp } : {}),
257
+ ...(row.diskSeed ? { diskSeed: row.diskSeed } : {}),
251
258
  ...(row.k8sName !== null && row.desired !== null && row.observed !== null
252
259
  ? {
253
260
  pod: {
@@ -260,7 +267,6 @@ function sandboxResourceOf(row) {
260
267
  ...(row.joinTokenHash !== null ? { joinTokenHash: row.joinTokenHash } : {}),
261
268
  rev: row.rev,
262
269
  ...(row.lastActiveAt ? { lastActiveAt: row.lastActiveAt.toISOString() } : {}),
263
- ...(row.expiresAt ? { expiresAt: row.expiresAt.toISOString() } : {}),
264
270
  ...(row.startedAt ? { startedAt: row.startedAt.toISOString() } : {}),
265
271
  ...(row.reason !== null ? { reason: row.reason } : {}),
266
272
  ...(row.retiring !== null ? { retiring: row.retiring } : {}),
@@ -283,7 +289,6 @@ function podColumns(patch) {
283
289
  joinTokenHash: patch.joinTokenHash,
284
290
  rev: patch.rev,
285
291
  lastActiveAt: date(patch.lastActiveAt),
286
- expiresAt: date(patch.expiresAt),
287
292
  startedAt: date(patch.startedAt),
288
293
  reason: patch.reason,
289
294
  retiring: patch.retiring,
@@ -389,7 +394,7 @@ class PostgresTx {
389
394
  const [row] = await this.sessions().where(eq(sessions.id, id)).for("update");
390
395
  return row && storedSession(row);
391
396
  }
392
- async event(sessionId, turnId, type, payload) {
397
+ async event(sessionId, turnId, type, payload, writer) {
393
398
  this.check();
394
399
  const db = this.db;
395
400
  // The session row lock orders the session's events; the log head allocates under it.
@@ -408,6 +413,7 @@ class PostgresTx {
408
413
  time: this.now(),
409
414
  type,
410
415
  payload,
416
+ ...(writer ? { writer } : {}),
411
417
  });
412
418
  this.events.push(event);
413
419
  this.generations.push(generation);
@@ -612,88 +618,92 @@ class PostgresTx {
612
618
  this.check();
613
619
  return this.bodies(sandboxes);
614
620
  }
615
- // --- sandbox resources ---------------------------------------------------
616
- async sandboxResource(id, options = {}) {
621
+ // --- Environments ----------------------------------------------------------
622
+ async environment(id, options = {}) {
617
623
  this.check();
618
- const query = this.db.select().from(sandboxResources).where(eq(sandboxResources.id, id));
624
+ const query = this.db.select().from(environments).where(eq(environments.id, id));
619
625
  const [row] = options.lock ? await query.for("update") : await query;
620
- return row && sandboxResourceOf(row);
626
+ return row && environmentOf(row);
621
627
  }
622
- async createSandboxResource(row, limit) {
628
+ async createEnvironment(row, limit) {
623
629
  this.check();
624
630
  // Every create takes this lock, so two never both see room for one more.
625
- await this.db.execute(sql `SELECT pg_advisory_xact_lock(hashtext('nylorun.sandbox_resources'))`);
631
+ await this.db.execute(sql `SELECT pg_advisory_xact_lock(hashtext('nylorun.environments'))`);
626
632
  const [found] = await this.db
627
- .select({ id: sandboxResources.id })
628
- .from(sandboxResources)
629
- .where(eq(sandboxResources.id, row.id));
633
+ .select({ id: environments.id })
634
+ .from(environments)
635
+ .where(eq(environments.id, row.id));
630
636
  if (found)
631
637
  return "exists";
632
- const [counted] = await this.db.select({ n: count() }).from(sandboxResources);
638
+ const [counted] = await this.db.select({ n: count() }).from(environments);
633
639
  if ((counted?.n ?? 0) >= limit)
634
640
  return "limit";
635
- const { pod, ...resource } = row;
636
- await this.db.insert(sandboxResources).values({ ...resource, ...(pod ? podColumns(pod) : {}) });
641
+ const { pod, ownerUserId, diskOp, diskSeed, ...environment } = row;
642
+ await this.db.insert(environments).values({
643
+ ...environment,
644
+ ownerUserId: ownerUserId ?? null,
645
+ diskOp: diskOp ?? null,
646
+ diskSeed: diskSeed ?? null,
647
+ ...(pod ? podColumns(pod) : {}),
648
+ });
637
649
  return "created";
638
650
  }
639
- async updateSandboxSpec(id, spec, updatedAt) {
640
- this.check();
641
- await this.db.update(sandboxResources).set({ spec, updatedAt }).where(eq(sandboxResources.id, id));
642
- }
643
- async updateSandboxPod(id, patch, updatedAt) {
651
+ async updateEnvironment(id, patch, updatedAt) {
644
652
  this.check();
653
+ const columns = Object.fromEntries(Object.entries(patch).filter(([, value]) => value !== undefined));
645
654
  await this.db
646
- .update(sandboxResources)
647
- .set({ ...podColumns(patch), updatedAt })
648
- .where(eq(sandboxResources.id, id));
655
+ .update(environments)
656
+ .set({ ...columns, updatedAt })
657
+ .where(eq(environments.id, id));
649
658
  }
650
- async updateSandboxLabels(id, labels, updatedAt) {
659
+ async updateEnvironmentPod(id, patch, updatedAt) {
651
660
  this.check();
652
661
  await this.db
653
- .update(sandboxResources)
654
- .set({ labels, updatedAt })
655
- .where(eq(sandboxResources.id, id));
662
+ .update(environments)
663
+ .set({ ...podColumns(patch), updatedAt })
664
+ .where(eq(environments.id, id));
656
665
  }
657
- async deleteSandboxResource(id) {
666
+ async deleteEnvironment(id) {
658
667
  this.check();
659
- await this.db.delete(sandboxResources).where(eq(sandboxResources.id, id));
668
+ // Its vault and credentials go with it (ON DELETE CASCADE).
669
+ await this.db.delete(environments).where(eq(environments.id, id));
660
670
  }
661
- async listSandboxResources(filter = {}) {
671
+ async listEnvironments(filter = {}) {
662
672
  this.check();
663
673
  const labels = filter.labels;
664
674
  const rows = await this.db
665
675
  .select()
666
- .from(sandboxResources)
676
+ .from(environments)
667
677
  .where(labels === undefined || Object.keys(labels).length === 0
668
678
  ? undefined
669
- : sql `nylorun.doc(${sandboxResources.labels}) @> ${toJson(labels)}::jsonb`)
670
- .orderBy(sandboxResources.id);
671
- return rows.map(sandboxResourceOf);
679
+ : sql `nylorun.doc(${environments.labels}) @> ${toJson(labels)}::jsonb`)
680
+ .orderBy(environments.id);
681
+ return rows.map(environmentOf);
672
682
  }
673
- async sessionsOnSandbox(sandboxId) {
683
+ async sessionsOnEnvironment(environmentId) {
674
684
  this.check();
675
685
  const rows = await this.sessions()
676
- .where(eq(sessions.sandboxId, sandboxId))
686
+ .where(eq(sessions.environmentId, environmentId))
677
687
  .orderBy(sessions.id);
678
688
  return rows.map((row) => storedSession(row));
679
689
  }
680
- async sandboxEvent(sandboxId, type, payload) {
690
+ async environmentEvent(environmentId, type, payload) {
681
691
  this.check();
682
- return appendSandboxEvent(postgresSandboxRecordWriter(this.db), {
692
+ return appendEnvironmentEvent(postgresEnvironmentRecordWriter(this.db), {
683
693
  tenantId: this.tenantId,
684
- sandboxId,
694
+ environmentId,
685
695
  time: this.now(),
686
696
  type,
687
697
  payload,
688
698
  });
689
699
  }
690
- async sandboxEvents(sandboxId, options = {}) {
700
+ async environmentEvents(environmentId, options = {}) {
691
701
  this.check();
692
702
  const rows = await this.db
693
- .select({ body: sandboxEvents.body })
694
- .from(sandboxEvents)
695
- .where(and(eq(sandboxEvents.sandboxId, sandboxId), options.fromSeq === undefined ? undefined : gte(sandboxEvents.seq, options.fromSeq)))
696
- .orderBy(sandboxEvents.seq)
703
+ .select({ body: environmentEvents.body })
704
+ .from(environmentEvents)
705
+ .where(and(eq(environmentEvents.environmentId, environmentId), options.fromSeq === undefined ? undefined : gte(environmentEvents.seq, options.fromSeq)))
706
+ .orderBy(environmentEvents.seq)
697
707
  .limit(options.limit ?? 1000);
698
708
  return rows.map((row) => row.body);
699
709
  }
@@ -831,21 +841,13 @@ class PostgresTx {
831
841
  const [row] = await this.db.select().from(vaults).where(eq(vaults.id, id));
832
842
  return row;
833
843
  }
834
- async vaultsByOwner(ownerUserId) {
835
- this.check();
836
- return this.db
837
- .select()
838
- .from(vaults)
839
- .where(and(eq(vaults.ownerUserId, ownerUserId), eq(vaults.scope, "user")))
840
- .orderBy(vaults.createdAt, vaults.id);
841
- }
842
- async installationVaults() {
844
+ async vaultOfEnvironment(environmentId) {
843
845
  this.check();
844
- return this.db
846
+ const [row] = await this.db
845
847
  .select()
846
848
  .from(vaults)
847
- .where(eq(vaults.scope, "installation"))
848
- .orderBy(vaults.createdAt, vaults.id);
849
+ .where(eq(vaults.environmentId, environmentId));
850
+ return row;
849
851
  }
850
852
  async updateVaultMetadata(id, metadataJson) {
851
853
  this.check();
@@ -868,6 +870,14 @@ class PostgresTx {
868
870
  .where(eq(vaultCredentials.id, id));
869
871
  return row;
870
872
  }
873
+ async credentialByName(vaultId, name) {
874
+ this.check();
875
+ const [row] = await this.db
876
+ .select()
877
+ .from(vaultCredentials)
878
+ .where(and(eq(vaultCredentials.vaultId, vaultId), eq(vaultCredentials.name, name)));
879
+ return row;
880
+ }
871
881
  async credentialsForVault(vaultId, filter = {}) {
872
882
  this.check();
873
883
  return this.db
@@ -976,6 +986,16 @@ class PostgresTx {
976
986
  .returning();
977
987
  return inserted;
978
988
  }
989
+ async modelUsageOf(effectKey) {
990
+ this.check();
991
+ const [row] = await this.db
992
+ .select()
993
+ .from(modelUsage)
994
+ .where(and(eq(modelUsage.effectKey, effectKey), eq(modelUsage.duplicate, false)))
995
+ .orderBy(modelUsage.createdAt, modelUsage.id)
996
+ .limit(1);
997
+ return row;
998
+ }
979
999
  async modelUsageTotals(query) {
980
1000
  this.check();
981
1001
  const [row] = await this.db
@@ -1016,6 +1036,15 @@ class PostgresTx {
1016
1036
  .returning({ key: toolCrossings.key });
1017
1037
  return inserted.length === 1;
1018
1038
  }
1039
+ async restartToolCrossing(row) {
1040
+ this.check();
1041
+ const restarted = await this.db
1042
+ .update(toolCrossings)
1043
+ .set({ startedAt: row.startedAt })
1044
+ .where(and(eq(toolCrossings.key, row.key), eq(toolCrossings.hash, row.hash), isNull(toolCrossings.settledAt)))
1045
+ .returning({ key: toolCrossings.key });
1046
+ return restarted.length === 1;
1047
+ }
1019
1048
  async settleToolCrossing(key, answer, settledAt) {
1020
1049
  this.check();
1021
1050
  await this.db
@@ -1038,6 +1067,12 @@ class PostgresTx {
1038
1067
  const [row] = options.lock ? await query.for("update") : await query;
1039
1068
  return row;
1040
1069
  }
1070
+ async diskArtifact(environmentId, options = {}) {
1071
+ this.check();
1072
+ const query = this.db.select().from(artifacts).where(eq(artifacts.environmentId, environmentId));
1073
+ const [row] = options.lock ? await query.for("update") : await query;
1074
+ return row;
1075
+ }
1041
1076
  async listArtifacts(filter = {}) {
1042
1077
  this.check();
1043
1078
  return this.db
@@ -1088,6 +1123,24 @@ class PostgresTx {
1088
1123
  await this.db.delete(artifacts).where(eq(artifacts.id, id));
1089
1124
  return deleted.map((row) => row.blobKey);
1090
1125
  }
1126
+ async deleteArtifactVersion(artifactId, version) {
1127
+ this.check();
1128
+ const [deleted] = await this.db
1129
+ .delete(artifactVersions)
1130
+ .where(and(eq(artifactVersions.artifactId, artifactId), eq(artifactVersions.version, version)))
1131
+ .returning({ blobKey: artifactVersions.blobKey });
1132
+ if (!deleted)
1133
+ return undefined;
1134
+ const [left] = await this.db
1135
+ .select({ latest: sql `max(${artifactVersions.version})` })
1136
+ .from(artifactVersions)
1137
+ .where(eq(artifactVersions.artifactId, artifactId));
1138
+ await this.db
1139
+ .update(artifacts)
1140
+ .set({ latestVersion: Number(left?.latest ?? 0), updatedAt: new Date().toISOString() })
1141
+ .where(eq(artifacts.id, artifactId));
1142
+ return deleted.blobKey;
1143
+ }
1091
1144
  async lockArtifactQuota() {
1092
1145
  this.check();
1093
1146
  await this.db.execute(sql `SELECT pg_advisory_xact_lock(hashtext('nylorun.artifacts'))`);
@@ -1228,10 +1281,11 @@ class PostgresTx {
1228
1281
  basinGeneration: sql `${tenant.basinGeneration} + 1`,
1229
1282
  });
1230
1283
  }
1231
- if (scope === "sandboxes" || scope === "all") {
1284
+ if (scope === "environments" || scope === "all") {
1232
1285
  await db.delete(sandboxes);
1233
- await db.delete(sandboxResources);
1234
- await db.delete(sandboxEvents);
1286
+ // Their vaults and credentials go with them (ON DELETE CASCADE).
1287
+ await db.delete(environments);
1288
+ await db.delete(environmentEvents);
1235
1289
  await db.delete(sandboxSignals);
1236
1290
  }
1237
1291
  if (scope === "all") {
@@ -1241,7 +1295,6 @@ class PostgresTx {
1241
1295
  await db.delete(modelUsage);
1242
1296
  await db.delete(modelBudgets);
1243
1297
  await db.delete(artifacts);
1244
- await db.delete(vaults).where(ne(vaults.scope, "host"));
1245
1298
  }
1246
1299
  }
1247
1300
  }