From 0d515e87b853f9332ee6c1c3ffa3cffcf699cee4 Mon Sep 17 00:00:00 2001 From: thesujai Date: Fri, 18 Sep 2026 12:19:36 +0530 Subject: [PATCH 1/5] feat: persist and expose sandbox provider name in settings --- .changeset/sandbox-provider-name.md | 5 ++ .../trueforge/src/apis/sandboxProviders.ts | 8 +- .../20260918_000001_sandbox_provider_name.ts | 26 +++++++ .../PostgresSandboxProviderStore.ts | 5 ++ packages/trueforge/src/db/postgres/types.ts | 2 + .../trueforge/src/db/sandboxProviderStore.ts | 2 + .../20260918_000001_sandbox_provider_name.ts | 73 +++++++++++++++++++ .../SqliteSandboxProviderStore.ts | 5 ++ packages/trueforge/src/db/sqlite/types.ts | 2 + .../trueforge/src/schemas/sandboxProvider.ts | 7 +- .../TrueFoundrySandboxProviderStore.ts | 34 +++++---- .../tests/unit/apis/sandboxProviders.test.ts | 4 +- .../unit/schemas/sandboxProvider.test.ts | 2 +- 13 files changed, 153 insertions(+), 22 deletions(-) create mode 100644 .changeset/sandbox-provider-name.md create mode 100644 packages/trueforge/src/db/postgres/migrations/20260918_000001_sandbox_provider_name.ts create mode 100644 packages/trueforge/src/db/sqlite/migrations/20260918_000001_sandbox_provider_name.ts diff --git a/.changeset/sandbox-provider-name.md b/.changeset/sandbox-provider-name.md new file mode 100644 index 000000000..58f137897 --- /dev/null +++ b/.changeset/sandbox-provider-name.md @@ -0,0 +1,5 @@ +--- +"@truefoundry/trueforge": patch +--- + +Persist sandbox provider `name` column (= `manifest.type` on write) and expose it on configured settings responses. diff --git a/packages/trueforge/src/apis/sandboxProviders.ts b/packages/trueforge/src/apis/sandboxProviders.ts index 3b713ca02..891da3a93 100644 --- a/packages/trueforge/src/apis/sandboxProviders.ts +++ b/packages/trueforge/src/apis/sandboxProviders.ts @@ -51,6 +51,7 @@ export function createSandboxProvidersRouter(deps: SandboxProvider return c.json( { data: { + name: record.name, manifest: redactSandboxProvider(record.manifest), status: status?.status ?? record.status, status_reason: status?.status_reason ?? record.status_reason, @@ -76,7 +77,7 @@ export function createSandboxProvidersRouter(deps: SandboxProvider }); try { // NOTE: build (Daytona network I/O) runs inside the transaction for now; the design is being revisited. - const { manifest, status } = await deps.withTransaction(async transaction => { + const { name, manifest, status } = await deps.withTransaction(async transaction => { const locked = await store.getSandboxProviderForUpdate(requestContext.tenant_id, transaction); const resolved = resolveManifest(locked); // Pass persisted build_metadata so a settings re-save does not start a new snapshot for a @@ -90,15 +91,16 @@ export function createSandboxProvidersRouter(deps: SandboxProvider const built = toSandboxStatus( await withTimeout(provider.buildImage(), BUILD_REQUEST_TIMEOUT_MS, 'sandbox buildImage'), ); - await store.upsertSandboxProvider( + const record = await store.upsertSandboxProvider( { tenant_id: requestContext.tenant_id, manifest: resolved, ...built }, transaction, ); - return { manifest: resolved, status: built }; + return { name: record.name, manifest: resolved, status: built }; }); return c.json( { data: { + name, manifest: redactSandboxProvider(manifest), status: status.status, status_reason: status.status_reason, diff --git a/packages/trueforge/src/db/postgres/migrations/20260918_000001_sandbox_provider_name.ts b/packages/trueforge/src/db/postgres/migrations/20260918_000001_sandbox_provider_name.ts new file mode 100644 index 000000000..e6a999421 --- /dev/null +++ b/packages/trueforge/src/db/postgres/migrations/20260918_000001_sandbox_provider_name.ts @@ -0,0 +1,26 @@ +import { sql, type Kysely } from 'kysely'; + +/** + * Persist sandbox provider identity `name` (always equal to `manifest.type` for now). + * Backfill existing rows from the jsonb type, then require NOT NULL. + */ +export async function up(db: Kysely): Promise { + await sql`SET LOCAL lock_timeout = '5s'`.execute(db); + await db.schema.alterTable('sandbox_provider').addColumn('name', 'text').execute(); + + await sql` + UPDATE sandbox_provider + SET name = manifest ->> 'type' + WHERE name IS NULL + `.execute(db); + + await sql` + ALTER TABLE sandbox_provider + ALTER COLUMN name SET NOT NULL + `.execute(db); +} + +export async function down(db: Kysely): Promise { + await sql`SET LOCAL lock_timeout = '5s'`.execute(db); + await db.schema.alterTable('sandbox_provider').dropColumn('name').execute(); +} diff --git a/packages/trueforge/src/db/postgres/sandbox-provider-store/PostgresSandboxProviderStore.ts b/packages/trueforge/src/db/postgres/sandbox-provider-store/PostgresSandboxProviderStore.ts index c0ca35dfb..e9b924878 100644 --- a/packages/trueforge/src/db/postgres/sandbox-provider-store/PostgresSandboxProviderStore.ts +++ b/packages/trueforge/src/db/postgres/sandbox-provider-store/PostgresSandboxProviderStore.ts @@ -11,6 +11,7 @@ import type { Database, SandboxProviderTable } from '../types'; function toRecord(row: Selectable): SandboxProviderRecord { return { tenant_id: row.tenant_id, + name: row.name, manifest: row.manifest, status: row.status, status_reason: row.status_reason, @@ -58,10 +59,13 @@ export class PostgresSandboxProviderStore implements ISandboxProviderStore, ): Promise { const db = transaction ?? this.#db; + // TEMP: name is always manifest.type until providers can have distinct identities. + const name = input.manifest.type; const row = await db .insertInto('sandbox_provider') .values({ tenant_id: input.tenant_id, + name, manifest: json(input.manifest), status: input.status, status_reason: input.status_reason, @@ -71,6 +75,7 @@ export class PostgresSandboxProviderStore implements ISandboxProviderStore oc.columns(['tenant_id']).doUpdateSet({ + name, manifest: json(input.manifest), status: input.status, status_reason: input.status_reason, diff --git a/packages/trueforge/src/db/postgres/types.ts b/packages/trueforge/src/db/postgres/types.ts index 41d78f462..dc3e4203e 100644 --- a/packages/trueforge/src/db/postgres/types.ts +++ b/packages/trueforge/src/db/postgres/types.ts @@ -363,6 +363,8 @@ export interface SkillTable { export interface SandboxProviderTable { /** key */ tenant_id: string; + /** Identity; currently always equal to `manifest.type`. */ + name: string; /** StoredSandboxProviderManifest document; replaced whole on every upsert */ manifest: JSONColumnType; /** Last persisted build status of the release sandbox image. */ diff --git a/packages/trueforge/src/db/sandboxProviderStore.ts b/packages/trueforge/src/db/sandboxProviderStore.ts index 55d25942f..5e131a240 100644 --- a/packages/trueforge/src/db/sandboxProviderStore.ts +++ b/packages/trueforge/src/db/sandboxProviderStore.ts @@ -12,6 +12,8 @@ import type { export interface SandboxProviderRecord { tenant_id: string; + /** Identity; currently always `manifest.type` (see upsert). */ + name: string; manifest: StoredSandboxProviderManifest; /** Last persisted build status of the release sandbox image. */ status: SandboxBuildStatus; diff --git a/packages/trueforge/src/db/sqlite/migrations/20260918_000001_sandbox_provider_name.ts b/packages/trueforge/src/db/sqlite/migrations/20260918_000001_sandbox_provider_name.ts new file mode 100644 index 000000000..a984b3d50 --- /dev/null +++ b/packages/trueforge/src/db/sqlite/migrations/20260918_000001_sandbox_provider_name.ts @@ -0,0 +1,73 @@ +import { sql, type Kysely } from 'kysely'; + +/** + * Persist sandbox provider identity `name` (always equal to `manifest.type` for now). + * Rebuilds the table so `name` is NOT NULL without a leftover DEFAULT (SQLite cannot + * drop a column default in place). Also drops the temporary `status` DEFAULT from + * the earlier status migration. + */ +export async function up(db: Kysely): Promise { + await db.transaction().execute(async trx => { + await sql` + CREATE TABLE sandbox_provider__with_name ( + tenant_id TEXT NOT NULL, + name TEXT NOT NULL, + manifest BLOB NOT NULL, + status TEXT NOT NULL, + status_reason TEXT, + build_metadata BLOB, + created_at TEXT NOT NULL, + updated_at TEXT NOT NULL, + PRIMARY KEY (tenant_id) + ) STRICT + `.execute(trx); + + await sql` + INSERT INTO sandbox_provider__with_name ( + tenant_id, name, manifest, status, status_reason, build_metadata, created_at, updated_at + ) + SELECT + tenant_id, + json_extract(json(manifest), '$.type'), + manifest, + status, + status_reason, + build_metadata, + created_at, + updated_at + FROM sandbox_provider + `.execute(trx); + + await sql`DROP TABLE sandbox_provider`.execute(trx); + await sql`ALTER TABLE sandbox_provider__with_name RENAME TO sandbox_provider`.execute(trx); + }); +} + +export async function down(db: Kysely): Promise { + await db.transaction().execute(async trx => { + await sql` + CREATE TABLE sandbox_provider__without_name ( + tenant_id TEXT NOT NULL, + manifest BLOB NOT NULL, + status TEXT NOT NULL DEFAULT 'pending', + status_reason TEXT, + build_metadata BLOB, + created_at TEXT NOT NULL, + updated_at TEXT NOT NULL, + PRIMARY KEY (tenant_id) + ) STRICT + `.execute(trx); + + await sql` + INSERT INTO sandbox_provider__without_name ( + tenant_id, manifest, status, status_reason, build_metadata, created_at, updated_at + ) + SELECT + tenant_id, manifest, status, status_reason, build_metadata, created_at, updated_at + FROM sandbox_provider + `.execute(trx); + + await sql`DROP TABLE sandbox_provider`.execute(trx); + await sql`ALTER TABLE sandbox_provider__without_name RENAME TO sandbox_provider`.execute(trx); + }); +} diff --git a/packages/trueforge/src/db/sqlite/sandbox-provider-store/SqliteSandboxProviderStore.ts b/packages/trueforge/src/db/sqlite/sandbox-provider-store/SqliteSandboxProviderStore.ts index a52acd5d8..57529d10e 100644 --- a/packages/trueforge/src/db/sqlite/sandbox-provider-store/SqliteSandboxProviderStore.ts +++ b/packages/trueforge/src/db/sqlite/sandbox-provider-store/SqliteSandboxProviderStore.ts @@ -13,6 +13,7 @@ import type { Database } from '../types'; function recordColumns(eb: ExpressionBuilder) { return [ 'tenant_id' as const, + 'name' as const, jsonText(eb.ref('manifest')).as('manifest'), 'status' as const, 'status_reason' as const, @@ -62,10 +63,13 @@ export class SqliteSandboxProviderStore implements ISandboxProviderStore { const db = transaction ?? this.#db; const timestamp = nowIso(); + // TEMP: name is always manifest.type until providers can have distinct identities. + const name = input.manifest.type; return await db .insertInto('sandbox_provider') .values({ tenant_id: input.tenant_id, + name, manifest: jsonbBind(input.manifest), status: input.status, status_reason: input.status_reason, @@ -75,6 +79,7 @@ export class SqliteSandboxProviderStore implements ISandboxProviderStore oc.columns(['tenant_id']).doUpdateSet({ + name, manifest: jsonbBind(input.manifest), status: input.status, status_reason: input.status_reason, diff --git a/packages/trueforge/src/db/sqlite/types.ts b/packages/trueforge/src/db/sqlite/types.ts index 9c2b62303..755009c87 100644 --- a/packages/trueforge/src/db/sqlite/types.ts +++ b/packages/trueforge/src/db/sqlite/types.ts @@ -207,6 +207,8 @@ export interface SkillTable { */ export interface SandboxProviderTable { tenant_id: string; + /** Identity; currently always equal to `manifest.type`. */ + name: string; /** StoredSandboxProviderManifest document; replaced whole on every upsert */ manifest: JsonbColumn; /** Last persisted build status of the release sandbox image. */ diff --git a/packages/trueforge/src/schemas/sandboxProvider.ts b/packages/trueforge/src/schemas/sandboxProvider.ts index 380ed8c6d..368bbc3cc 100644 --- a/packages/trueforge/src/schemas/sandboxProvider.ts +++ b/packages/trueforge/src/schemas/sandboxProvider.ts @@ -2,7 +2,9 @@ * Sandbox-provider domain + wire schemas: configured provider jsonb and OpenAPI * request/response shapes. Catalog file schemas live in sandboxCatalog.ts. * - * Singleton per tenant — no identity `name` (unlike model providers / skills). + * Singleton per tenant. Identity `name` is a DB column; writers set it from + * `manifest.type` for now (not accepted on PUT). Wire `ConfiguredSandboxProvider.name` + * mirrors the column. * * Settings OpenAPI stays Daytona-only (`SandboxProviderManifest`), matching main. * Env-synthesized truefoundry records use `StoredSandboxProviderManifest` (store/runtime only). @@ -94,9 +96,10 @@ export const SandboxStatusSchema = z }) .strict(); -/** Settings wire item: nested Daytona manifest plus build status (no build_metadata). */ +/** Settings wire item: identity name, nested Daytona manifest, build status (no build_metadata). */ export const ConfiguredSandboxProviderSchema = z .object({ + name: z.string().min(1).describe('Sandbox provider name.'), manifest: SandboxProviderManifestSchema, status: SandboxBuildStatusSchema, status_reason: z.string().nullable().describe('Human-readable detail for the current status; null when ready.'), diff --git a/packages/trueforge/src/truefoundry/TrueFoundrySandboxProviderStore.ts b/packages/trueforge/src/truefoundry/TrueFoundrySandboxProviderStore.ts index e309f0e41..6c4bd9117 100644 --- a/packages/trueforge/src/truefoundry/TrueFoundrySandboxProviderStore.ts +++ b/packages/trueforge/src/truefoundry/TrueFoundrySandboxProviderStore.ts @@ -20,16 +20,19 @@ function synthesizeDaytonaRecord({ }): SandboxProviderRecord { const { settings } = providerConfig; const now = new Date().toISOString(); + const manifest = { + type: 'daytona' as const, + auth: { api_key: providerConfig.apiKey }, + exec_timeout_ms: settings.timeoutMs, + auto_stop_interval_in_minutes: settings.autoStopIntervalInMinutes, + auto_archive_interval_in_minutes: settings.autoArchiveIntervalInMinutes, + auto_delete_interval_in_minutes: settings.autoDeleteIntervalInMinutes, + }; return { tenant_id: tenantId, - manifest: { - type: 'daytona', - auth: { api_key: providerConfig.apiKey }, - exec_timeout_ms: settings.timeoutMs, - auto_stop_interval_in_minutes: settings.autoStopIntervalInMinutes, - auto_archive_interval_in_minutes: settings.autoArchiveIntervalInMinutes, - auto_delete_interval_in_minutes: settings.autoDeleteIntervalInMinutes, - }, + // TEMP: name is always manifest.type until providers can have distinct identities. + name: manifest.type, + manifest, status: 'ready', status_reason: null, // Snapshot name only — no image_uri; TrueFoundry mode never registers a snapshot. @@ -48,14 +51,17 @@ function synthesizeTrueFoundryRecord({ providerConfig: Extract; }): SandboxProviderRecord { const now = new Date().toISOString(); + const manifest = { + type: 'truefoundry' as const, + server_url: providerConfig.serverUrl, + nats_bridge_url: providerConfig.natsBridgeUrl, + exec_timeout_ms: SANDBOX_DEFAULT_SETTINGS.timeoutMs, + }; return { tenant_id: tenantId, - manifest: { - type: 'truefoundry', - server_url: providerConfig.serverUrl, - nats_bridge_url: providerConfig.natsBridgeUrl, - exec_timeout_ms: SANDBOX_DEFAULT_SETTINGS.timeoutMs, - }, + // TEMP: name is always manifest.type until providers can have distinct identities. + name: manifest.type, + manifest, status: 'ready', status_reason: null, build_metadata: null, diff --git a/packages/trueforge/tests/unit/apis/sandboxProviders.test.ts b/packages/trueforge/tests/unit/apis/sandboxProviders.test.ts index 56140983f..d451643f0 100644 --- a/packages/trueforge/tests/unit/apis/sandboxProviders.test.ts +++ b/packages/trueforge/tests/unit/apis/sandboxProviders.test.ts @@ -48,9 +48,9 @@ const expectedStatus = { status_reason: null, }; -/** Wire GET/PUT response: the (redacted) manifest nested under `manifest`, plus the build status. */ +/** Wire GET/PUT response: identity name (= manifest.type), redacted manifest, build status. */ function wireResponse(manifest: Record) { - return { manifest, ...expectedStatus }; + return { name: manifest.type, manifest, ...expectedStatus }; } const putBodyWire = wireResponse({ diff --git a/packages/trueforge/tests/unit/schemas/sandboxProvider.test.ts b/packages/trueforge/tests/unit/schemas/sandboxProvider.test.ts index 12a3a7412..dd953f526 100644 --- a/packages/trueforge/tests/unit/schemas/sandboxProvider.test.ts +++ b/packages/trueforge/tests/unit/schemas/sandboxProvider.test.ts @@ -1,8 +1,8 @@ +import type { SandboxProviderManifest } from '../../../src/schemas/sandboxProvider'; import { StoredSandboxProviderManifestSchema, UpdateSandboxProviderRequestSchema, toDaytonaSandboxProviderInput, - type SandboxProviderManifest, } from '../../../src/schemas/sandboxProvider'; describe('toDaytonaSandboxProviderInput', () => { From 372c42dcced5d233d7646b3be588be7887f16c9b Mon Sep 17 00:00:00 2001 From: "trueforge-dev-bot[bot]" Date: Fri, 18 Sep 2026 06:53:15 +0000 Subject: [PATCH 2/5] Regenerate OpenAPI document and SDKs --- .changeset/20260918065315-regenerate-sdk-from-openapi.md | 5 +++++ .github/fern/openapi/openapi.json | 6 ++++++ docs/openapi.json | 6 ++++++ .../src/api/types/ConfiguredSandboxProvider.ts | 2 ++ .../src/serialization/types/ConfiguredSandboxProvider.ts | 2 ++ .../tests/wire/settings/sandboxProviders.test.ts | 4 ++++ .../src/trueforge_sdk/types/configured_sandbox_provider.py | 5 +++++ 7 files changed, 30 insertions(+) create mode 100644 .changeset/20260918065315-regenerate-sdk-from-openapi.md diff --git a/.changeset/20260918065315-regenerate-sdk-from-openapi.md b/.changeset/20260918065315-regenerate-sdk-from-openapi.md new file mode 100644 index 000000000..efd8ff00f --- /dev/null +++ b/.changeset/20260918065315-regenerate-sdk-from-openapi.md @@ -0,0 +1,5 @@ +--- +"@truefoundry/trueforge-sdk": patch +--- + +Regenerate SDK from updated OpenAPI spec. diff --git a/.github/fern/openapi/openapi.json b/.github/fern/openapi/openapi.json index 7a06b406e..b40934b3f 100644 --- a/.github/fern/openapi/openapi.json +++ b/.github/fern/openapi/openapi.json @@ -947,6 +947,11 @@ "manifest": { "$ref": "#/components/schemas/SandboxProviderManifest" }, + "name": { + "description": "Sandbox provider name.", + "minLength": 1, + "type": "string" + }, "status": { "$ref": "#/components/schemas/SandboxBuildStatus" }, @@ -959,6 +964,7 @@ } }, "required": [ + "name", "manifest", "status", "status_reason" diff --git a/docs/openapi.json b/docs/openapi.json index 7a06b406e..b40934b3f 100644 --- a/docs/openapi.json +++ b/docs/openapi.json @@ -947,6 +947,11 @@ "manifest": { "$ref": "#/components/schemas/SandboxProviderManifest" }, + "name": { + "description": "Sandbox provider name.", + "minLength": 1, + "type": "string" + }, "status": { "$ref": "#/components/schemas/SandboxBuildStatus" }, @@ -959,6 +964,7 @@ } }, "required": [ + "name", "manifest", "status", "status_reason" diff --git a/packages/trueforge-sdk/src/api/types/ConfiguredSandboxProvider.ts b/packages/trueforge-sdk/src/api/types/ConfiguredSandboxProvider.ts index d766d8e2d..3b726b304 100644 --- a/packages/trueforge-sdk/src/api/types/ConfiguredSandboxProvider.ts +++ b/packages/trueforge-sdk/src/api/types/ConfiguredSandboxProvider.ts @@ -4,6 +4,8 @@ import type * as TrueForge from "../index.js"; export interface ConfiguredSandboxProvider { manifest: TrueForge.SandboxProviderManifest; + /** Sandbox provider name. */ + name: string; status: TrueForge.SandboxBuildStatus; /** Human-readable detail for the current status; null when ready. */ statusReason: string | null; diff --git a/packages/trueforge-sdk/src/serialization/types/ConfiguredSandboxProvider.ts b/packages/trueforge-sdk/src/serialization/types/ConfiguredSandboxProvider.ts index 6bdb0087d..b7e31f337 100644 --- a/packages/trueforge-sdk/src/serialization/types/ConfiguredSandboxProvider.ts +++ b/packages/trueforge-sdk/src/serialization/types/ConfiguredSandboxProvider.ts @@ -11,6 +11,7 @@ export const ConfiguredSandboxProvider: core.serialization.ObjectSchema< TrueForge.ConfiguredSandboxProvider > = core.serialization.object({ manifest: SandboxProviderManifest, + name: core.serialization.string(), status: SandboxBuildStatus, statusReason: core.serialization.property("status_reason", core.serialization.string().nullable()), }); @@ -18,6 +19,7 @@ export const ConfiguredSandboxProvider: core.serialization.ObjectSchema< export declare namespace ConfiguredSandboxProvider { export interface Raw { manifest: SandboxProviderManifest.Raw; + name: string; status: SandboxBuildStatus.Raw; status_reason?: string | null; } diff --git a/packages/trueforge-sdk/tests/wire/settings/sandboxProviders.test.ts b/packages/trueforge-sdk/tests/wire/settings/sandboxProviders.test.ts index 0edb2ac60..24b468d8f 100644 --- a/packages/trueforge-sdk/tests/wire/settings/sandboxProviders.test.ts +++ b/packages/trueforge-sdk/tests/wire/settings/sandboxProviders.test.ts @@ -19,6 +19,7 @@ describe("SandboxProvidersClient", () => { exec_timeout_ms: 1, type: "daytona", }, + name: "name", status: "pending", status_reason: "status_reason", }, @@ -45,6 +46,7 @@ describe("SandboxProvidersClient", () => { execTimeoutMs: 1, type: "daytona", }, + name: "name", status: "pending", statusReason: "status_reason", }, @@ -93,6 +95,7 @@ describe("SandboxProvidersClient", () => { exec_timeout_ms: 1, type: "daytona", }, + name: "name", status: "pending", status_reason: "status_reason", }, @@ -131,6 +134,7 @@ describe("SandboxProvidersClient", () => { execTimeoutMs: 1, type: "daytona", }, + name: "name", status: "pending", statusReason: "status_reason", }, diff --git a/python/trueforge_sdk/src/trueforge_sdk/types/configured_sandbox_provider.py b/python/trueforge_sdk/src/trueforge_sdk/types/configured_sandbox_provider.py index 557c21dc9..1f60d8208 100644 --- a/python/trueforge_sdk/src/trueforge_sdk/types/configured_sandbox_provider.py +++ b/python/trueforge_sdk/src/trueforge_sdk/types/configured_sandbox_provider.py @@ -11,6 +11,11 @@ class ConfiguredSandboxProvider(UncheckedBaseModel): manifest: SandboxProviderManifest + name: str = pydantic.Field() + """ + Sandbox provider name. + """ + status: SandboxBuildStatus status_reason: typing.Optional[str] = pydantic.Field(default=None) """ From 94aabf57ec7defc7bc2d2a5d31a4ed0b02d0b744 Mon Sep 17 00:00:00 2001 From: thesujai Date: Fri, 18 Sep 2026 12:25:30 +0530 Subject: [PATCH 3/5] fix: correct property access for manifest type in wireResponse function --- packages/trueforge/tests/unit/apis/sandboxProviders.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/trueforge/tests/unit/apis/sandboxProviders.test.ts b/packages/trueforge/tests/unit/apis/sandboxProviders.test.ts index d451643f0..3cfe639b0 100644 --- a/packages/trueforge/tests/unit/apis/sandboxProviders.test.ts +++ b/packages/trueforge/tests/unit/apis/sandboxProviders.test.ts @@ -50,7 +50,7 @@ const expectedStatus = { /** Wire GET/PUT response: identity name (= manifest.type), redacted manifest, build status. */ function wireResponse(manifest: Record) { - return { name: manifest.type, manifest, ...expectedStatus }; + return { name: manifest['type'], manifest, ...expectedStatus }; } const putBodyWire = wireResponse({ From 1dc68ae2402495348d838db66a0dfed6fba591b0 Mon Sep 17 00:00:00 2001 From: thesujai Date: Fri, 18 Sep 2026 15:05:20 +0530 Subject: [PATCH 4/5] test: add sandbox provider name to response in sandboxProviderCatalog test --- .../catalogs/sandboxProviderCatalog.test.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/trueforge-ui/test/plugins/trueforge-agent-server-adapter/catalogs/sandboxProviderCatalog.test.ts b/packages/trueforge-ui/test/plugins/trueforge-agent-server-adapter/catalogs/sandboxProviderCatalog.test.ts index 59948064f..4ad0be7d4 100644 --- a/packages/trueforge-ui/test/plugins/trueforge-agent-server-adapter/catalogs/sandboxProviderCatalog.test.ts +++ b/packages/trueforge-ui/test/plugins/trueforge-agent-server-adapter/catalogs/sandboxProviderCatalog.test.ts @@ -33,6 +33,7 @@ describe('sandboxProviderCatalog mappers', () => { statusReason: string | null; }): TrueForgeApi.GetSandboxProviderResponse['data'] { return { + name: 'daytona', manifest: harnessConfigured, status, statusReason, From 8229f0afc12fb85f96129386168df6ae0eb9155d Mon Sep 17 00:00:00 2001 From: thesujai Date: Fri, 18 Sep 2026 16:35:06 +0530 Subject: [PATCH 5/5] fix: unique (tenant_id, name) on sandbox_provider; rename migration after main Co-authored-by: Cursor --- packages/trueforge/src/db/indexes.ts | 3 +++ ...me.ts => 20260918_000002_sandbox_provider_name.ts} | 11 ++++++++++- packages/trueforge/src/db/postgres/types.ts | 4 ++-- ...me.ts => 20260918_000002_sandbox_provider_name.ts} | 7 ++++--- packages/trueforge/src/db/sqlite/types.ts | 4 ++-- 5 files changed, 21 insertions(+), 8 deletions(-) rename packages/trueforge/src/db/postgres/migrations/{20260918_000001_sandbox_provider_name.ts => 20260918_000002_sandbox_provider_name.ts} (69%) rename packages/trueforge/src/db/sqlite/migrations/{20260918_000001_sandbox_provider_name.ts => 20260918_000002_sandbox_provider_name.ts} (91%) diff --git a/packages/trueforge/src/db/indexes.ts b/packages/trueforge/src/db/indexes.ts index 36eefa9ee..22ab4c7b5 100644 --- a/packages/trueforge/src/db/indexes.ts +++ b/packages/trueforge/src/db/indexes.ts @@ -27,3 +27,6 @@ export const SCHEDULE_AGENT_ID_IDX = 'schedule_agent_id_idx'; /** `(tenant_id, created_by_subject.subject_id)` on schedule_run. */ export const SCHEDULE_RUN_CREATED_BY_SUBJECT_ID_IDX = 'schedule_run_created_by_subject_id_idx'; + +/** `(tenant_id, name)` unique on sandbox_provider. */ +export const SANDBOX_PROVIDER_TENANT_NAME_UQ = 'sandbox_provider_tenant_name_uq'; diff --git a/packages/trueforge/src/db/postgres/migrations/20260918_000001_sandbox_provider_name.ts b/packages/trueforge/src/db/postgres/migrations/20260918_000002_sandbox_provider_name.ts similarity index 69% rename from packages/trueforge/src/db/postgres/migrations/20260918_000001_sandbox_provider_name.ts rename to packages/trueforge/src/db/postgres/migrations/20260918_000002_sandbox_provider_name.ts index e6a999421..ae5a24fdc 100644 --- a/packages/trueforge/src/db/postgres/migrations/20260918_000001_sandbox_provider_name.ts +++ b/packages/trueforge/src/db/postgres/migrations/20260918_000002_sandbox_provider_name.ts @@ -1,8 +1,11 @@ import { sql, type Kysely } from 'kysely'; +import { SANDBOX_PROVIDER_TENANT_NAME_UQ } from '../../indexes'; + /** * Persist sandbox provider identity `name` (always equal to `manifest.type` for now). - * Backfill existing rows from the jsonb type, then require NOT NULL. + * Backfill existing rows from the jsonb type, then require NOT NULL and + * UNIQUE (tenant_id, name). */ export async function up(db: Kysely): Promise { await sql`SET LOCAL lock_timeout = '5s'`.execute(db); @@ -18,9 +21,15 @@ export async function up(db: Kysely): Promise { ALTER TABLE sandbox_provider ALTER COLUMN name SET NOT NULL `.execute(db); + + await db.schema + .alterTable('sandbox_provider') + .addUniqueConstraint(SANDBOX_PROVIDER_TENANT_NAME_UQ, ['tenant_id', 'name']) + .execute(); } export async function down(db: Kysely): Promise { await sql`SET LOCAL lock_timeout = '5s'`.execute(db); + await db.schema.alterTable('sandbox_provider').dropConstraint(SANDBOX_PROVIDER_TENANT_NAME_UQ).execute(); await db.schema.alterTable('sandbox_provider').dropColumn('name').execute(); } diff --git a/packages/trueforge/src/db/postgres/types.ts b/packages/trueforge/src/db/postgres/types.ts index dc3e4203e..7723013e9 100644 --- a/packages/trueforge/src/db/postgres/types.ts +++ b/packages/trueforge/src/db/postgres/types.ts @@ -358,12 +358,12 @@ export interface SkillTable { /** * Configured sandbox provider — mirrors the Postgres `sandbox_provider` table. - * PRIMARY KEY (tenant_id) — at most one row per tenant. + * PRIMARY KEY (tenant_id) — at most one row per tenant; UNIQUE (tenant_id, name). */ export interface SandboxProviderTable { /** key */ tenant_id: string; - /** Identity; currently always equal to `manifest.type`. */ + /** Identity; currently always equal to `manifest.type`. UNIQUE with tenant_id. */ name: string; /** StoredSandboxProviderManifest document; replaced whole on every upsert */ manifest: JSONColumnType; diff --git a/packages/trueforge/src/db/sqlite/migrations/20260918_000001_sandbox_provider_name.ts b/packages/trueforge/src/db/sqlite/migrations/20260918_000002_sandbox_provider_name.ts similarity index 91% rename from packages/trueforge/src/db/sqlite/migrations/20260918_000001_sandbox_provider_name.ts rename to packages/trueforge/src/db/sqlite/migrations/20260918_000002_sandbox_provider_name.ts index a984b3d50..1c3f581e7 100644 --- a/packages/trueforge/src/db/sqlite/migrations/20260918_000001_sandbox_provider_name.ts +++ b/packages/trueforge/src/db/sqlite/migrations/20260918_000002_sandbox_provider_name.ts @@ -3,8 +3,8 @@ import { sql, type Kysely } from 'kysely'; /** * Persist sandbox provider identity `name` (always equal to `manifest.type` for now). * Rebuilds the table so `name` is NOT NULL without a leftover DEFAULT (SQLite cannot - * drop a column default in place). Also drops the temporary `status` DEFAULT from - * the earlier status migration. + * drop a column default in place), with UNIQUE (tenant_id, name). Also drops the + * temporary `status` DEFAULT from the earlier status migration. */ export async function up(db: Kysely): Promise { await db.transaction().execute(async trx => { @@ -18,7 +18,8 @@ export async function up(db: Kysely): Promise { build_metadata BLOB, created_at TEXT NOT NULL, updated_at TEXT NOT NULL, - PRIMARY KEY (tenant_id) + PRIMARY KEY (tenant_id), + UNIQUE (tenant_id, name) ) STRICT `.execute(trx); diff --git a/packages/trueforge/src/db/sqlite/types.ts b/packages/trueforge/src/db/sqlite/types.ts index 755009c87..e19c679d4 100644 --- a/packages/trueforge/src/db/sqlite/types.ts +++ b/packages/trueforge/src/db/sqlite/types.ts @@ -203,11 +203,11 @@ export interface SkillTable { /** * Configured sandbox provider — mirrors the Postgres `sandbox_provider` table. - * PRIMARY KEY (tenant_id) — at most one row per tenant. + * PRIMARY KEY (tenant_id) — at most one row per tenant; UNIQUE (tenant_id, name). */ export interface SandboxProviderTable { tenant_id: string; - /** Identity; currently always equal to `manifest.type`. */ + /** Identity; currently always equal to `manifest.type`. UNIQUE with tenant_id. */ name: string; /** StoredSandboxProviderManifest document; replaced whole on every upsert */ manifest: JsonbColumn;