From b0d19a1d73b9d147bb71ea518fa12cfc528f6b79 Mon Sep 17 00:00:00 2001 From: Govinda Vashishtha <57435703+govindavashishtha@users.noreply.github.com> Date: Wed, 23 Sep 2026 11:40:34 +0530 Subject: [PATCH] Make sandbox provider port types provider-agnostic. Move Daytona lifecycle fields off the shared sandbox base onto host DaytonaSandboxConfig so other providers are not forced to supply them. Co-authored-by: Cursor --- .../daytona-sandbox-config-extension.md | 6 +++ packages/assistant-ui-runtime/src/index.ts | 2 - .../assistant-ui-runtime/src/server/index.ts | 2 - .../assistant-ui-runtime/src/server/types.ts | 20 +++----- .../SettingsBuilder/ConfigureSandboxForm.tsx | 10 ++-- .../SettingsBuilder/SandboxSettings.tsx | 17 ++++--- packages/trueforge-ui/src/index.ts | 2 +- .../catalogs/sandboxProviderCatalog.ts | 49 ++++++++++++++++--- .../trueforge-agent-server-adapter/index.ts | 9 ++++ packages/trueforge-ui/src/server/types.ts | 1 - .../SettingsBuilder/SandboxSettings.test.tsx | 46 ++++++++--------- .../catalogs/sandboxProviderCatalog.test.ts | 8 ++- 12 files changed, 108 insertions(+), 64 deletions(-) create mode 100644 .changeset/daytona-sandbox-config-extension.md diff --git a/.changeset/daytona-sandbox-config-extension.md b/.changeset/daytona-sandbox-config-extension.md new file mode 100644 index 000000000..71f45042d --- /dev/null +++ b/.changeset/daytona-sandbox-config-extension.md @@ -0,0 +1,6 @@ +--- +'@truefoundry/trueforge-assistant-ui-runtime': minor +'@truefoundry/trueforge-ui': minor +--- + +Make sandbox provider port types identity-only; move Daytona lifecycle fields (`execTimeoutMs`, auto-stop/archive/delete intervals) onto host `DaytonaSandboxConfig`. diff --git a/packages/assistant-ui-runtime/src/index.ts b/packages/assistant-ui-runtime/src/index.ts index cfdd8e993..0d15a0d31 100644 --- a/packages/assistant-ui-runtime/src/index.ts +++ b/packages/assistant-ui-runtime/src/index.ts @@ -168,11 +168,9 @@ export type { SandboxBase, SandboxCatalogEntry, SandboxCatalogServer, - SandboxConfig, SandboxCreatedEvent, SandboxProviderBase, SandboxProviderCatalogEntry, - SandboxProviderConfig, SandboxProviderListEntry, SandboxSnapshotSyncStatus, SaveAgentRequest, diff --git a/packages/assistant-ui-runtime/src/server/index.ts b/packages/assistant-ui-runtime/src/server/index.ts index 721400328..bf30d5ce9 100644 --- a/packages/assistant-ui-runtime/src/server/index.ts +++ b/packages/assistant-ui-runtime/src/server/index.ts @@ -92,10 +92,8 @@ export type { SandboxBase, SandboxCatalogEntry, SandboxCatalogServer, - SandboxConfig, SandboxProviderBase, SandboxProviderCatalogEntry, - SandboxProviderConfig, SandboxProviderListEntry, SandboxSnapshotSyncStatus, SaveAgentRequest, diff --git a/packages/assistant-ui-runtime/src/server/types.ts b/packages/assistant-ui-runtime/src/server/types.ts index 1a09b12a7..55328d7ef 100644 --- a/packages/assistant-ui-runtime/src/server/types.ts +++ b/packages/assistant-ui-runtime/src/server/types.ts @@ -724,15 +724,8 @@ export interface SkillCatalogServer< // Sandbox providers catalog — public rows omit credentials; writes accept them // --------------------------------------------------------------------------- -/** Mutable sandbox provider settings shared by catalog rows, create, and update. */ -export interface SandboxConfig { - execTimeoutMs: number; - autoStopIntervalInMinutes: number; - autoArchiveIntervalInMinutes: number; - autoDeleteIntervalInMinutes: number; -} - -export interface SandboxCatalogEntry extends SandboxConfig { +/** Discovery catalog row. Hosts extend for provider-specific defaults. */ +export interface SandboxCatalogEntry { id: string; name: string; type: string; @@ -740,9 +733,9 @@ export interface SandboxCatalogEntry extends SandboxConfig { /** * Connected sandbox provider row (settings/sandboxes). No raw `apiKey`. - * Includes last-saved config so update forms can show previous values. + * Hosts extend for provider-specific settings shown on update forms. */ -export interface SandboxBase extends SandboxConfig { +export interface SandboxBase { id: string; name: string; catalogId: string; @@ -759,7 +752,7 @@ export interface SandboxProviderListEntry ({ +}: DaytonaSandboxConfig): DaytonaSandboxConfig => ({ execTimeoutMs, autoStopIntervalInMinutes, autoArchiveIntervalInMinutes, @@ -144,10 +147,11 @@ const SandboxSettings = () => { return catalog.filter(entry => !connectedCatalogIds.has(entry.id)); }, [catalog, providers, hasConfiguredProvider]); - const formInitialConfig = useMemo( - () => (updateProvider ? configFrom(updateProvider) : createEntry ? configFrom(createEntry) : null), - [updateProvider, createEntry], - ); + const formInitialConfig = useMemo(() => { + const source = updateProvider ?? createEntry; + if (source == null || !isDaytonaSandboxConfig(source)) return null; + return configFrom(source); + }, [updateProvider, createEntry]); if (!sandboxCatalog) { return

Sandbox provider catalog is not available.

; @@ -202,6 +206,7 @@ const SandboxSettings = () => { }; const handleRetry = (provider: SandboxProviderBase) => { + if (!isDaytonaSandboxConfig(provider)) return; void runMutation(async () => { await sandboxCatalog.updateSandboxProvider({ id: provider.id, diff --git a/packages/trueforge-ui/src/index.ts b/packages/trueforge-ui/src/index.ts index ad55ee35d..2df5d850a 100644 --- a/packages/trueforge-ui/src/index.ts +++ b/packages/trueforge-ui/src/index.ts @@ -404,7 +404,6 @@ export type { SandboxCatalogServer, SandboxProviderBase, SandboxProviderCatalogEntry, - SandboxProviderConfig, SandboxProviderListEntry, SandboxSnapshotSyncStatus, SaveAgentRequest, @@ -477,6 +476,7 @@ export type { AgentSessionsProps, } from './atoms/agent-details/types.js'; export { AgentMetricsContainer } from './containers/AgentMetricsContainer.js'; +export type { DaytonaSandboxConfig } from './plugins/trueforge-agent-server-adapter/catalogs/sandboxProviderCatalog.js'; export type { SessionEventTimelineSegment, SessionEventType } from './utils/sessionEventTimeline.js'; export type { SessionTurnView } from './utils/sessionTurnViews.js'; diff --git a/packages/trueforge-ui/src/plugins/trueforge-agent-server-adapter/catalogs/sandboxProviderCatalog.ts b/packages/trueforge-ui/src/plugins/trueforge-agent-server-adapter/catalogs/sandboxProviderCatalog.ts index 1c434208b..fb3958aaf 100644 --- a/packages/trueforge-ui/src/plugins/trueforge-agent-server-adapter/catalogs/sandboxProviderCatalog.ts +++ b/packages/trueforge-ui/src/plugins/trueforge-agent-server-adapter/catalogs/sandboxProviderCatalog.ts @@ -5,20 +5,42 @@ * UI: multi-row providers with `id` / `catalogId` / `name` / flat `apiKey`. * Harness: one Daytona provider per tenant; catalog YAML has no name — synthetic * identity uses `type` (`daytona`) as id/catalogId and display name `Daytona`. + * + * Daytona lifecycle fields live here as a host extension of the generic sandbox + * port types (identity + credentials only). */ import type { TrueForge } from '@truefoundry/trueforge-sdk'; import { TrueForgeApi } from '@truefoundry/trueforge-sdk'; import type { + CreateSandboxProviderRequest, SandboxCatalogServer, SandboxProviderBase, SandboxProviderCatalogEntry, - SandboxProviderConfig, SandboxProviderListEntry, + UpdateSandboxProviderRequest, } from '../../../server/types.js'; -export type UiSandboxProvider = SandboxProviderBase; -export type UiSandboxProviderCatalogEntry = SandboxProviderCatalogEntry; -export type UiSandboxProviderListEntry = SandboxProviderListEntry; +/** Daytona-only sandbox lifecycle settings. Not on the generic sandbox port. */ +export interface DaytonaSandboxConfig { + execTimeoutMs: number; + autoStopIntervalInMinutes: number; + autoArchiveIntervalInMinutes: number; + autoDeleteIntervalInMinutes: number; +} + +export type UiSandboxProvider = SandboxProviderBase & DaytonaSandboxConfig; +export type UiSandboxProviderCatalogEntry = SandboxProviderCatalogEntry & DaytonaSandboxConfig; +export type UiSandboxProviderListEntry = SandboxProviderListEntry; +export type UiCreateSandboxProviderRequest = CreateSandboxProviderRequest & DaytonaSandboxConfig; +export type UiUpdateSandboxProviderRequest = UpdateSandboxProviderRequest & DaytonaSandboxConfig; + +export type DaytonaSandboxCatalogServer = SandboxCatalogServer< + UiSandboxProvider, + UiSandboxProviderCatalogEntry, + UiCreateSandboxProviderRequest, + UiUpdateSandboxProviderRequest, + UiSandboxProviderListEntry +>; const DAYTONA_TYPE = 'daytona'; const DAYTONA_DISPLAY_NAME = 'Daytona'; @@ -30,9 +52,22 @@ function displayNameForType(type: string): string { return type; } +export function isDaytonaSandboxConfig(value: object): value is DaytonaSandboxConfig { + return ( + 'execTimeoutMs' in value && + typeof value.execTimeoutMs === 'number' && + 'autoStopIntervalInMinutes' in value && + typeof value.autoStopIntervalInMinutes === 'number' && + 'autoArchiveIntervalInMinutes' in value && + typeof value.autoArchiveIntervalInMinutes === 'number' && + 'autoDeleteIntervalInMinutes' in value && + typeof value.autoDeleteIntervalInMinutes === 'number' + ); +} + export function configFromHarness( provider: TrueForgeApi.CatalogSandboxProvider | TrueForgeApi.SandboxProviderManifest, -): SandboxProviderConfig { +): DaytonaSandboxConfig { return { execTimeoutMs: provider.execTimeoutMs, autoStopIntervalInMinutes: provider.autoStopIntervalInMinutes, @@ -94,7 +129,7 @@ export function toHarnessManifest( req: { type: string; apiKey: string; - } & SandboxProviderConfig, + } & DaytonaSandboxConfig, ): TrueForgeApi.SandboxProviderManifest { if (req.type !== DAYTONA_TYPE) { throw new Error(`Unsupported sandbox provider type: ${req.type}`); @@ -110,7 +145,7 @@ export function toHarnessManifest( } /** Settings sandbox-catalog port for `createTrueForgeServer`. Delete omitted (no BE route). */ -export function createSandboxProviderCatalog(client: TrueForge): SandboxCatalogServer { +export function createSandboxProviderCatalog(client: TrueForge): DaytonaSandboxCatalogServer { async function resolveApiKey(apiKey: string | undefined): Promise { const trimmed = apiKey?.trim(); if (trimmed !== undefined && trimmed !== '') { diff --git a/packages/trueforge-ui/src/plugins/trueforge-agent-server-adapter/index.ts b/packages/trueforge-ui/src/plugins/trueforge-agent-server-adapter/index.ts index bf0dfc84c..80741b256 100644 --- a/packages/trueforge-ui/src/plugins/trueforge-agent-server-adapter/index.ts +++ b/packages/trueforge-ui/src/plugins/trueforge-agent-server-adapter/index.ts @@ -45,9 +45,18 @@ export { configFromHarness, createSandboxProviderCatalog, filterUiSandboxProviders, + isDaytonaSandboxConfig, toHarnessManifest as toHarnessSandboxManifest, + toUiCatalogEntry as toUiSandboxCatalogEntry, toUiSandboxProvider, toUiSandboxProviderListEntry, + type DaytonaSandboxCatalogServer, + type DaytonaSandboxConfig, + type UiCreateSandboxProviderRequest, + type UiSandboxProvider, + type UiSandboxProviderCatalogEntry, + type UiSandboxProviderListEntry, + type UiUpdateSandboxProviderRequest, } from './catalogs/sandboxProviderCatalog.js'; export { createSkillCatalog, toHarnessManifest as toHarnessSkillManifest, toUiSkill } from './catalogs/skillCatalog.js'; export { diff --git a/packages/trueforge-ui/src/server/types.ts b/packages/trueforge-ui/src/server/types.ts index 28fa1e38a..2b3a7f3d8 100644 --- a/packages/trueforge-ui/src/server/types.ts +++ b/packages/trueforge-ui/src/server/types.ts @@ -94,7 +94,6 @@ export type { SandboxCatalogServer, SandboxProviderBase, SandboxProviderCatalogEntry, - SandboxProviderConfig, SandboxProviderListEntry, SandboxSnapshotSyncStatus, SaveAgentRequest, diff --git a/packages/trueforge-ui/test/containers/SettingsBuilder/SandboxSettings.test.tsx b/packages/trueforge-ui/test/containers/SettingsBuilder/SandboxSettings.test.tsx index 5ca026ba0..958ab757e 100644 --- a/packages/trueforge-ui/test/containers/SettingsBuilder/SandboxSettings.test.tsx +++ b/packages/trueforge-ui/test/containers/SettingsBuilder/SandboxSettings.test.tsx @@ -4,15 +4,15 @@ import type { ReactNode } from 'react'; import { afterEach, beforeAll, describe, expect, it, vi } from 'vitest'; import SandboxSettings from '@/containers/SettingsBuilder/SandboxSettings.js'; -import { ServerProvider } from '@/server/ServerContext.js'; import type { - CreateSandboxProviderRequest, - SandboxProviderBase, - SandboxProviderCatalogEntry, - SandboxProviderListEntry, - SandboxSnapshotSyncStatus, - UpdateSandboxProviderRequest, -} from '@/server/types.js'; + UiCreateSandboxProviderRequest, + UiSandboxProvider, + UiSandboxProviderCatalogEntry, + UiSandboxProviderListEntry, + UiUpdateSandboxProviderRequest, +} from '@/plugins/trueforge-agent-server-adapter/catalogs/sandboxProviderCatalog.js'; +import { ServerProvider } from '@/server/ServerContext.js'; +import type { SandboxSnapshotSyncStatus } from '@/server/types.js'; import { createMockAgentUIServer, createMockCatalog } from '../../server/mockServer.js'; beforeAll(() => { @@ -28,7 +28,7 @@ afterEach(() => { vi.useRealTimers(); }); -const catalogEntry: SandboxProviderCatalogEntry = { +const catalogEntry: UiSandboxProviderCatalogEntry = { id: 'cat-daytona', name: 'Daytona', type: 'daytona', @@ -43,10 +43,10 @@ function sandboxEntry({ status = 'ready', statusReason, }: { - provider: SandboxProviderBase; + provider: UiSandboxProvider; status?: SandboxSnapshotSyncStatus['status']; statusReason?: string; -}): SandboxProviderListEntry { +}): UiSandboxProviderListEntry { return { data: provider, snapshotSyncStatus: { @@ -56,11 +56,11 @@ function sandboxEntry({ }; } -function createFakeHost(initial: SandboxProviderListEntry[] = []) { +function createFakeHost(initial: UiSandboxProviderListEntry[] = []) { let providers = [...initial]; let listCalls = 0; - const created: CreateSandboxProviderRequest[] = []; - const updated: UpdateSandboxProviderRequest[] = []; + const created: UiCreateSandboxProviderRequest[] = []; + const updated: UiUpdateSandboxProviderRequest[] = []; const sandboxCatalog = { getSandboxProviderCatalog: async () => [catalogEntry], @@ -68,9 +68,9 @@ function createFakeHost(initial: SandboxProviderListEntry[] = []) { listCalls += 1; return providers; }, - createSandboxProvider: async (req: CreateSandboxProviderRequest) => { + createSandboxProvider: async (req: UiCreateSandboxProviderRequest) => { created.push(req); - const provider: SandboxProviderBase = { + const provider: UiSandboxProvider = { id: `sb-${req.catalogId}`, name: req.name, catalogId: req.catalogId, @@ -83,7 +83,7 @@ function createFakeHost(initial: SandboxProviderListEntry[] = []) { providers = [...providers, sandboxEntry({ provider, status: 'pending' })]; return provider; }, - updateSandboxProvider: async (req: UpdateSandboxProviderRequest) => { + updateSandboxProvider: async (req: UiUpdateSandboxProviderRequest) => { updated.push(req); providers = providers.map(entry => entry.data.id === req.id @@ -116,7 +116,7 @@ function createFakeHost(initial: SandboxProviderListEntry[] = []) { updated, getListCalls: () => listCalls, getProviders: () => providers, - setProviders: (next: SandboxProviderListEntry[]) => { + setProviders: (next: UiSandboxProviderListEntry[]) => { providers = next; }, wrapper: ({ children }: { children: ReactNode }) => {children}, @@ -170,7 +170,7 @@ describe('SandboxSettings', () => { }); it('prefills update form and allows saving without re-entering apiKey', async () => { - const existing: SandboxProviderBase = { + const existing: UiSandboxProvider = { id: 'sb-1', name: 'Daytona', catalogId: 'cat-daytona', @@ -219,7 +219,7 @@ describe('SandboxSettings', () => { }); it('hides other catalog providers once one is configured', async () => { - const existing: SandboxProviderBase = { + const existing: UiSandboxProvider = { id: 'sb-1', name: 'Daytona', catalogId: 'cat-daytona', @@ -247,7 +247,7 @@ describe('SandboxSettings', () => { }); it('renders pending and ready snapshot status badges', async () => { - const provider: SandboxProviderBase = { + const provider: UiSandboxProvider = { id: 'sb-1', name: 'Daytona', catalogId: 'cat-daytona', @@ -289,7 +289,7 @@ describe('SandboxSettings', () => { it('polls pending snapshot status every ten seconds until it changes', async () => { vi.useFakeTimers(); - const provider: SandboxProviderBase = { + const provider: UiSandboxProvider = { id: 'sb-1', name: 'Daytona', catalogId: 'cat-daytona', @@ -331,7 +331,7 @@ describe('SandboxSettings', () => { }); it('renders snapshot status badges and exposes failed status reason in a tooltip', async () => { - const provider: SandboxProviderBase = { + const provider: UiSandboxProvider = { id: 'sb-1', name: 'Daytona', catalogId: 'cat-daytona', 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..5bbdecc32 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 @@ -5,6 +5,7 @@ import { describe, it } from 'vitest'; import { configFromHarness, filterUiSandboxProviders, + isDaytonaSandboxConfig, toHarnessManifest, toUiCatalogEntry, toUiSandboxProvider, @@ -39,8 +40,6 @@ describe('sandboxProviderCatalog mappers', () => { }; } - // Snapshot/image is release-owned now; mappers emit an empty snapshotName only to satisfy - // the external SandboxProviderConfig type, and toHarnessManifest omits it entirely. it('stamps catalog identity from type and strips auth', () => { assert.deepEqual(toUiCatalogEntry(harnessCatalog), { id: 'daytona', @@ -124,4 +123,9 @@ describe('sandboxProviderCatalog mappers', () => { /Unsupported sandbox provider type/i, ); }); + + it('narrows DaytonaSandboxConfig via type guard', () => { + assert.equal(isDaytonaSandboxConfig(configFromHarness(harnessCatalog)), true); + assert.equal(isDaytonaSandboxConfig({ id: 'daytona' }), false); + }); });