Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/20260918065315-regenerate-sdk-from-openapi.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@truefoundry/trueforge-sdk": patch
---

Regenerate SDK from updated OpenAPI spec.
5 changes: 5 additions & 0 deletions .changeset/sandbox-provider-name.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@truefoundry/trueforge": patch
---

Persist sandbox provider `name` column (= `manifest.type` on write) and expose it on configured settings responses.
6 changes: 6 additions & 0 deletions .github/fern/openapi/openapi.json
Original file line number Diff line number Diff line change
Expand Up @@ -947,6 +947,11 @@
"manifest": {
"$ref": "#/components/schemas/SandboxProviderManifest"
},
"name": {
"description": "Sandbox provider name.",
"minLength": 1,
"type": "string"
},
"status": {
"$ref": "#/components/schemas/SandboxBuildStatus"
},
Expand All @@ -959,6 +964,7 @@
}
},
"required": [
"name",
"manifest",
"status",
"status_reason"
Expand Down
6 changes: 6 additions & 0 deletions docs/openapi.json
Original file line number Diff line number Diff line change
Expand Up @@ -947,6 +947,11 @@
"manifest": {
"$ref": "#/components/schemas/SandboxProviderManifest"
},
"name": {
"description": "Sandbox provider name.",
"minLength": 1,
"type": "string"
},
"status": {
"$ref": "#/components/schemas/SandboxBuildStatus"
},
Expand All @@ -959,6 +964,7 @@
}
},
"required": [
"name",
"manifest",
"status",
"status_reason"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,15 @@ 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()),
});

export declare namespace ConfiguredSandboxProvider {
export interface Raw {
manifest: SandboxProviderManifest.Raw;
name: string;
status: SandboxBuildStatus.Raw;
status_reason?: string | null;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ describe("SandboxProvidersClient", () => {
exec_timeout_ms: 1,
type: "daytona",
},
name: "name",
status: "pending",
status_reason: "status_reason",
},
Expand All @@ -45,6 +46,7 @@ describe("SandboxProvidersClient", () => {
execTimeoutMs: 1,
type: "daytona",
},
name: "name",
status: "pending",
statusReason: "status_reason",
},
Expand Down Expand Up @@ -93,6 +95,7 @@ describe("SandboxProvidersClient", () => {
exec_timeout_ms: 1,
type: "daytona",
},
name: "name",
status: "pending",
status_reason: "status_reason",
},
Expand Down Expand Up @@ -131,6 +134,7 @@ describe("SandboxProvidersClient", () => {
execTimeoutMs: 1,
type: "daytona",
},
name: "name",
status: "pending",
statusReason: "status_reason",
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ describe('sandboxProviderCatalog mappers', () => {
statusReason: string | null;
}): TrueForgeApi.GetSandboxProviderResponse['data'] {
return {
name: 'daytona',
manifest: harnessConfigured,
status,
statusReason,
Expand Down
8 changes: 5 additions & 3 deletions packages/trueforge/src/apis/sandboxProviders.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ export function createSandboxProvidersRouter<TTransaction>(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,
Expand All @@ -76,7 +77,7 @@ export function createSandboxProvidersRouter<TTransaction>(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
Expand All @@ -90,15 +91,16 @@ export function createSandboxProvidersRouter<TTransaction>(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,
Expand Down
3 changes: 3 additions & 0 deletions packages/trueforge/src/db/indexes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
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 and
* UNIQUE (tenant_id, name).
*/
export async function up(db: Kysely<unknown>): Promise<void> {
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);

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

tenant_id and name will be a unique constraint

await sql`
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<unknown>): Promise<void> {
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();
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import type { Database, SandboxProviderTable } from '../types';
function toRecord(row: Selectable<SandboxProviderTable>): SandboxProviderRecord {
return {
tenant_id: row.tenant_id,
name: row.name,
manifest: row.manifest,
status: row.status,
status_reason: row.status_reason,
Expand Down Expand Up @@ -58,10 +59,13 @@ export class PostgresSandboxProviderStore implements ISandboxProviderStore<Trans
transaction?: Transaction<Database>,
): Promise<SandboxProviderRecord> {
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,
Expand All @@ -71,6 +75,7 @@ export class PostgresSandboxProviderStore implements ISandboxProviderStore<Trans
})
.onConflict(oc =>
oc.columns(['tenant_id']).doUpdateSet({
name,
manifest: json(input.manifest),
status: input.status,
status_reason: input.status_reason,
Expand Down
4 changes: 3 additions & 1 deletion packages/trueforge/src/db/postgres/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -358,11 +358,13 @@ 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`. UNIQUE with tenant_id. */
name: string;
/** StoredSandboxProviderManifest document; replaced whole on every upsert */
manifest: JSONColumnType<StoredSandboxProviderManifest, StoredSandboxProviderManifest, StoredSandboxProviderManifest>;
/** Last persisted build status of the release sandbox image. */
Expand Down
2 changes: 2 additions & 0 deletions packages/trueforge/src/db/sandboxProviderStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
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), with UNIQUE (tenant_id, name). Also drops the
* temporary `status` DEFAULT from the earlier status migration.
*/
export async function up(db: Kysely<unknown>): Promise<void> {
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),
UNIQUE (tenant_id, name)
) 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<unknown>): Promise<void> {
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);
});
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import type { Database } from '../types';
function recordColumns(eb: ExpressionBuilder<Database, 'sandbox_provider'>) {
return [
'tenant_id' as const,
'name' as const,
jsonText<StoredSandboxProviderManifest>(eb.ref('manifest')).as('manifest'),
'status' as const,
'status_reason' as const,
Expand Down Expand Up @@ -62,10 +63,13 @@ export class SqliteSandboxProviderStore implements ISandboxProviderStore<Transac
): Promise<SandboxProviderRecord> {
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,
Expand All @@ -75,6 +79,7 @@ export class SqliteSandboxProviderStore implements ISandboxProviderStore<Transac
})
.onConflict(oc =>
oc.columns(['tenant_id']).doUpdateSet({
name,
manifest: jsonbBind(input.manifest),
status: input.status,
status_reason: input.status_reason,
Expand Down
4 changes: 3 additions & 1 deletion packages/trueforge/src/db/sqlite/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -203,10 +203,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 {
tenant_id: string;
/** Identity; currently always equal to `manifest.type`. UNIQUE with tenant_id. */
name: string;
/** StoredSandboxProviderManifest document; replaced whole on every upsert */
manifest: JsonbColumn<StoredSandboxProviderManifest>;
/** Last persisted build status of the release sandbox image. */
Expand Down
7 changes: 5 additions & 2 deletions packages/trueforge/src/schemas/sandboxProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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).
Expand Down Expand Up @@ -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.'),
Expand Down
Loading
Loading