@nylorun/runtime 0.15.2-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.
- package/CHANGELOG.md +76 -0
- package/README.md +33 -6
- package/dist/adapters/execution/restate.d.ts +9 -1
- package/dist/adapters/execution/restate.js +64 -5
- package/dist/adapters/sandbox/local.d.ts +10 -0
- package/dist/adapters/sandbox/local.js +195 -0
- package/dist/adapters/sandbox/virtual.js +36 -1
- package/dist/admin-openapi.json +37 -3
- package/dist/api/a2a/tasks.js +6 -1
- package/dist/api/ag-ui/history.js +6 -1
- package/dist/api/ag-ui/routes.js +5 -1
- package/dist/api/components.d.ts +2673 -655
- package/dist/api/components.js +17 -1
- package/dist/api/gate/keys.d.ts +4 -3
- package/dist/api/gate/routes.d.ts +19 -3
- package/dist/api/gate/routes.js +162 -53
- package/dist/api/http/app.js +4 -0
- package/dist/api/http/define.d.ts +10 -0
- package/dist/api/http/define.js +2 -1
- package/dist/api/http/routes/artifacts.d.ts +11 -0
- package/dist/api/http/routes/artifacts.js +518 -0
- package/dist/api/http/routes/sandboxes.d.ts +9 -0
- package/dist/api/http/routes/sandboxes.js +152 -0
- package/dist/api/http/routes/sessions.js +3 -1
- package/dist/api/http/routes/tenant.js +36 -2
- package/dist/artifacts/config.d.ts +23 -0
- package/dist/artifacts/config.js +27 -0
- package/dist/artifacts/export.d.ts +8 -0
- package/dist/artifacts/export.js +205 -0
- package/dist/artifacts/files.d.ts +36 -0
- package/dist/artifacts/files.js +45 -0
- package/dist/artifacts/folders.d.ts +60 -0
- package/dist/artifacts/folders.js +209 -0
- package/dist/artifacts/links.d.ts +28 -0
- package/dist/artifacts/links.js +92 -0
- package/dist/artifacts/media-types.d.ts +13 -0
- package/dist/artifacts/media-types.js +80 -0
- package/dist/artifacts/parts.d.ts +34 -0
- package/dist/artifacts/parts.js +38 -0
- package/dist/artifacts/service.d.ts +74 -0
- package/dist/artifacts/service.js +301 -0
- package/dist/artifacts/workspace.d.ts +60 -0
- package/dist/artifacts/workspace.js +57 -0
- package/dist/artifacts/zip.d.ts +10 -0
- package/dist/artifacts/zip.js +126 -0
- package/dist/blob/fs.d.ts +13 -0
- package/dist/blob/fs.js +194 -0
- package/dist/blob/index.d.ts +9 -0
- package/dist/blob/index.js +9 -0
- package/dist/blob/s3.d.ts +42 -0
- package/dist/blob/s3.js +366 -0
- package/dist/blob/types.d.ts +108 -0
- package/dist/blob/types.js +85 -0
- package/dist/core/sandbox-routes.d.ts +2 -2
- package/dist/core/sandbox-routes.js +4 -3
- package/dist/execution/memory.d.ts +11 -1
- package/dist/execution/memory.js +77 -3
- package/dist/execution/types.d.ts +54 -1
- package/dist/execution/types.js +31 -0
- package/dist/gates/contract.d.ts +14 -13
- package/dist/gates/contract.js +13 -12
- package/dist/gates/egress.d.ts +72 -0
- package/dist/gates/egress.js +319 -0
- package/dist/gates/handler.d.ts +3 -0
- package/dist/gates/handler.js +3 -2
- package/dist/gates/http-client.d.ts +3 -2
- package/dist/gates/http-client.js +17 -6
- package/dist/gates/in-process.d.ts +13 -1
- package/dist/gates/in-process.js +11 -0
- package/dist/gates/inflight.d.ts +23 -4
- package/dist/gates/inflight.js +18 -4
- package/dist/gates/model-gate.d.ts +6 -2
- package/dist/gates/tool-calls.d.ts +9 -5
- package/dist/gates/tool-calls.js +6 -6
- package/dist/gates/tool-client.d.ts +9 -2
- package/dist/gates/tool-client.js +56 -22
- package/dist/gates/tool-contract.d.ts +12 -7
- package/dist/gates/tool-contract.js +10 -4
- package/dist/gates/tool-gate.d.ts +1 -0
- package/dist/harness/calls.d.ts +52 -0
- package/dist/harness/calls.js +113 -0
- package/dist/harness/executors.d.ts +22 -0
- package/dist/harness/executors.js +96 -0
- package/dist/harness/main.d.ts +6 -0
- package/dist/harness/main.js +103 -0
- package/dist/harness/pod.d.ts +21 -0
- package/dist/harness/pod.js +119 -0
- package/dist/harness/run-tokens.d.ts +18 -0
- package/dist/harness/run-tokens.js +15 -0
- package/dist/harness/service.d.ts +46 -0
- package/dist/harness/service.js +179 -0
- package/dist/harness/ws-client.d.ts +43 -0
- package/dist/harness/ws-client.js +122 -0
- package/dist/harness/ws-port.d.ts +22 -0
- package/dist/harness/ws-port.js +78 -0
- package/dist/harness-api/in-process.d.ts +24 -0
- package/dist/harness-api/in-process.js +119 -0
- package/dist/harness-api/record.d.ts +44 -0
- package/dist/harness-api/record.js +250 -0
- package/dist/harness-api/renew.d.ts +37 -0
- package/dist/harness-api/renew.js +77 -0
- package/dist/harness-api/server.d.ts +104 -0
- package/dist/harness-api/server.js +408 -0
- package/dist/harness-api/start.d.ts +36 -0
- package/dist/harness-api/start.js +118 -0
- package/dist/harness-api/workspace.d.ts +53 -0
- package/dist/harness-api/workspace.js +201 -0
- package/dist/harness-api/ws-server.d.ts +34 -0
- package/dist/harness-api/ws-server.js +227 -0
- package/dist/host/app.js +23 -2
- package/dist/host/gates.d.ts +27 -2
- package/dist/host/gates.js +46 -1
- package/dist/host/http.d.ts +4 -1
- package/dist/host/http.js +6 -1
- package/dist/host/main.js +135 -27
- package/dist/host/stack-config.d.ts +101 -1
- package/dist/host/stack-config.js +199 -8
- package/dist/mcp/snapshot.d.ts +8 -1
- package/dist/mcp/snapshot.js +28 -0
- package/dist/media.d.ts +5 -24
- package/dist/media.js +5 -0
- package/dist/model/pi-model.d.ts +6 -2
- package/dist/model/pi-model.js +39 -22
- package/dist/node/index.d.ts +1 -1
- package/dist/node/index.js +1 -1
- package/dist/openapi.json +15245 -8850
- package/dist/record/index.d.ts +1 -0
- package/dist/record/index.js +1 -0
- package/dist/record/sandbox.d.ts +25 -0
- package/dist/record/sandbox.js +43 -0
- package/dist/sandbox/capability.d.ts +3 -0
- package/dist/sandbox/capability.js +10 -0
- package/dist/sandbox/egress-token.d.ts +26 -0
- package/dist/sandbox/egress-token.js +8 -0
- package/dist/sandbox/join.d.ts +37 -0
- package/dist/sandbox/join.js +165 -0
- package/dist/sandbox/manager.d.ts +99 -34
- package/dist/sandbox/manager.js +216 -84
- package/dist/sandbox/placement.d.ts +27 -0
- package/dist/sandbox/placement.js +44 -0
- package/dist/sandbox/pods/client.d.ts +90 -0
- package/dist/sandbox/pods/client.js +79 -0
- package/dist/sandbox/pods/lifecycle.d.ts +91 -0
- package/dist/sandbox/pods/lifecycle.js +178 -0
- package/dist/sandbox/pods/name.d.ts +1 -0
- package/dist/sandbox/pods/name.js +28 -0
- package/dist/sandbox/pods/network-gate.d.ts +25 -0
- package/dist/sandbox/pods/network-gate.js +71 -0
- package/dist/sandbox/pods/reconcile.d.ts +11 -0
- package/dist/sandbox/pods/reconcile.js +152 -0
- package/dist/sandbox/pods/spec.d.ts +45 -0
- package/dist/sandbox/pods/spec.js +62 -0
- package/dist/sandbox/records.d.ts +29 -0
- package/dist/sandbox/records.js +79 -0
- package/dist/sandbox/resolve.d.ts +5 -0
- package/dist/sandbox/resolve.js +3 -2
- package/dist/sandbox/session-sandbox.js +7 -5
- package/dist/sandbox/share.d.ts +11 -0
- package/dist/sandbox/share.js +10 -0
- package/dist/sandbox/tenant-config.d.ts +16 -1
- package/dist/sandbox/tenant-config.js +9 -0
- package/dist/sandbox/types.d.ts +29 -1
- package/dist/sandbox/types.js +13 -1
- package/dist/store/postgres/drizzle/0004_sandbox_resources.sql +20 -0
- package/dist/store/postgres/drizzle/0005_artifacts.sql +27 -0
- package/dist/store/postgres/drizzle/0006_folder_artifacts.sql +12 -0
- package/dist/store/postgres/drizzle/0007_sandbox_pods.sql +14 -0
- package/dist/store/postgres/drizzle/meta/_journal.json +28 -0
- package/dist/store/postgres/record-writer.d.ts +6 -1
- package/dist/store/postgres/record-writer.js +20 -2
- package/dist/store/postgres/schema.d.ts +894 -0
- package/dist/store/postgres/schema.js +130 -2
- package/dist/store/postgres/store.js +289 -4
- package/dist/store/types.d.ts +143 -6
- package/dist/streams/control.d.ts +2 -0
- package/dist/streams/control.js +5 -0
- package/dist/streams/types.d.ts +10 -1
- package/dist/tenant/advance.js +218 -160
- package/dist/tenant/artifact-tool.d.ts +5 -0
- package/dist/tenant/artifact-tool.js +119 -0
- package/dist/tenant/commands.js +30 -5
- package/dist/tenant/context.d.ts +64 -5
- package/dist/tenant/effects.d.ts +19 -13
- package/dist/tenant/effects.js +5 -382
- package/dist/tenant/history.d.ts +13 -23
- package/dist/tenant/history.js +14 -62
- package/dist/tenant/host-token.d.ts +92 -0
- package/dist/tenant/host-token.js +179 -0
- package/dist/tenant/module.js +1 -0
- package/dist/tenant/paths.d.ts +2 -1
- package/dist/tenant/paths.js +3 -1
- package/dist/tenant/reset.d.ts +5 -2
- package/dist/tenant/reset.js +22 -2
- package/dist/tenant/run-grants.d.ts +49 -0
- package/dist/tenant/run-grants.js +62 -0
- package/dist/tenant/run-token.d.ts +70 -0
- package/dist/tenant/run-token.js +138 -0
- package/dist/tenant/runtime.d.ts +38 -0
- package/dist/tenant/runtime.js +120 -32
- package/dist/tenant/sandboxes.d.ts +119 -0
- package/dist/tenant/sandboxes.js +551 -0
- package/dist/tenant/session.d.ts +2 -13
- package/dist/tenant/session.js +4 -28
- package/dist/tenant/sessions.d.ts +5 -0
- package/dist/tenant/sessions.js +28 -1
- package/dist/tenant/status.d.ts +8 -3
- package/dist/tenant/status.js +5 -0
- package/dist/tenant/streams.d.ts +7 -0
- package/dist/tenant/streams.js +24 -1
- package/dist/tenant/sweep.js +37 -4
- package/dist/tenant/tokens.js +9 -2
- package/dist/tenant/types.d.ts +18 -1
- package/dist/tenant/worker.d.ts +11 -5
- package/dist/tenant/worker.js +12 -6
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +13 -10
- package/dist/adapters/media.d.ts +0 -24
- 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
|
|
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
|
}
|