Commit 6173a745 by Jon Committed by GitHub

Refactor/unify sandbox client (#6735)

* chore: update sandbox-adapter to version 0.0.35

* refactor: unify sandbox creation through getSandboxClient

* refactor: Simplify sandbox instance handling logic

* feat: Add support for custom create config in sandbox
parent cd75ee16
...@@ -340,6 +340,39 @@ export function buildVolumeConfig( ...@@ -340,6 +340,39 @@ export function buildVolumeConfig(
} }
/** /**
* Poll the sandbox endpoint until the service inside the container is accepting connections.
*
* Uses HTTP HEAD to avoid triggering application logic; any HTTP response
* (including 4xx/5xx) means the port is open and the service is ready.
* Retries on network errors (ECONNREFUSED / fetch failure) until timeout.
*/
export async function waitForEndpointReady(
endpoint: SkillSandboxEndpointType,
options?: { timeoutMs?: number; intervalMs?: number }
): Promise<void> {
const timeoutMs = options?.timeoutMs ?? 30_000;
const intervalMs = options?.intervalMs ?? 500;
const deadline = Date.now() + timeoutMs;
while (Date.now() < deadline) {
try {
await fetch(endpoint.url, {
method: 'HEAD',
signal: AbortSignal.timeout(3_000)
});
return; // any response means port is open
} catch {
// ECONNREFUSED or timeout — service not ready yet
}
await new Promise((resolve) => setTimeout(resolve, intervalMs));
}
throw new Error(
`Sandbox endpoint ${endpoint.url} did not become ready within ${timeoutMs / 1000}s`
);
}
/**
* Build container env vars for the sandbox process. * Build container env vars for the sandbox process.
*/ */
export function buildBaseContainerEnv( export function buildBaseContainerEnv(
......
...@@ -5,7 +5,7 @@ ...@@ -5,7 +5,7 @@
* *
*/ */
import type { ISandbox, OpenSandboxVolume } from '@fastgpt-sdk/sandbox-adapter'; import type { ISandbox } from '@fastgpt-sdk/sandbox-adapter';
import mongoose from 'mongoose'; import mongoose from 'mongoose';
import { MongoSandboxInstance } from '../ai/sandbox/schema'; import { MongoSandboxInstance } from '../ai/sandbox/schema';
import { MongoAgentSkills } from './schema'; import { MongoAgentSkills } from './schema';
...@@ -16,14 +16,11 @@ import { ...@@ -16,14 +16,11 @@ import {
getSandboxDefaults, getSandboxDefaults,
validateSandboxConfig, validateSandboxConfig,
getSkillSizeLimits, getSkillSizeLimits,
buildSandboxAdapter,
connectToProviderSandbox, connectToProviderSandbox,
disconnectFromProviderSandbox, disconnectFromProviderSandbox,
getProviderSandboxEndpoint, getProviderSandboxEndpoint,
getVolumeManagerConfig, buildBaseContainerEnv,
ensureSessionVolume, waitForEndpointReady
buildVolumeConfig,
buildBaseContainerEnv
} from './sandboxConfig'; } from './sandboxConfig';
import type { import type {
SandboxInstanceSchemaType, SandboxInstanceSchemaType,
...@@ -32,8 +29,7 @@ import type { ...@@ -32,8 +29,7 @@ import type {
} from '@fastgpt/global/core/agentSkills/type'; } from '@fastgpt/global/core/agentSkills/type';
import { SandboxTypeEnum } from '@fastgpt/global/core/agentSkills/constants'; import { SandboxTypeEnum } from '@fastgpt/global/core/agentSkills/constants';
import { SandboxStatusEnum } from '@fastgpt/global/core/ai/sandbox/constants'; import { SandboxStatusEnum } from '@fastgpt/global/core/ai/sandbox/constants';
import { getSandboxClient } from '../ai/sandbox/controller'; import { getSandboxClient, type SandboxClient } from '../ai/sandbox/controller';
import { mongoSessionRun } from '../../common/mongo/sessionRun';
import { getLogger, LogCategories } from '../../common/logger'; import { getLogger, LogCategories } from '../../common/logger';
import { env } from '../../env'; import { env } from '../../env';
import type { SandboxStatusItemType } from '@fastgpt/global/core/chat/type'; import type { SandboxStatusItemType } from '@fastgpt/global/core/chat/type';
...@@ -133,61 +129,47 @@ export async function createEditDebugSandbox( ...@@ -133,61 +129,47 @@ export async function createEditDebugSandbox(
'metadata.sandboxType': SandboxTypeEnum.editDebug 'metadata.sandboxType': SandboxTypeEnum.editDebug
}); });
if (existingInstance?.status === SandboxStatusEnum.running) { if (existingInstance) {
// Reuse running sandbox - return stored endpoint directly addLog.info('[Sandbox] Found existing sandbox instance, ensuring running', {
addLog.info('[Sandbox] Found running sandbox instance, reusing', {
instanceId: existingInstance._id, instanceId: existingInstance._id,
sandboxId: existingInstance.sandboxId sandboxId: existingInstance.sandboxId
}); });
const endpointInfo = existingInstance.metadata!.endpoint!; try {
onProgress?.({ sandboxId: skillId, phase: 'creatingContainer' });
await MongoSandboxInstance.updateOne(
{ _id: existingInstance._id },
{ lastActiveAt: new Date() }
);
onProgress?.({
sandboxId: skillId,
phase: 'ready',
endpoint: endpointInfo,
providerSandboxId: existingInstance.sandboxId
});
return { // getSandboxClient internally calls ensureAvailable():
sandboxId: existingInstance._id.toString(), // - updates DB status=running, lastActiveAt
providerSandboxId: existingInstance.sandboxId, // - calls provider.ensureRunning() to handle both running and stopped containers
endpoint: endpointInfo, // Pass skill-specific createConfig so the container is rebuilt with the
status: { // correct image/entrypoint/env/metadata if it was accidentally deleted.
state: existingInstance.status const client = await getSandboxClient(
// message: existingInstance.metadata!.providerStatus.message { sandboxId: existingInstance.sandboxId },
{
createConfig: {
image: sandboxImage,
entrypoint: [entrypoint ?? defaults.entrypoint],
env: buildBaseContainerEnv(existingInstance.sandboxId, defaults.workDirectory, true),
metadata: {
skillId,
teamId,
sandboxType: SandboxTypeEnum.editDebug,
sessionId: existingInstance.sandboxId
} }
};
} }
}
);
if (existingInstance?.status === SandboxStatusEnum.stopped) { const endpointInfo = await getProviderSandboxEndpoint(client.provider, defaults.targetPort);
// Resume stopped sandbox
addLog.info('[Sandbox] Found stopped sandbox instance, resuming', {
instanceId: existingInstance._id,
sandboxId: existingInstance.sandboxId
});
let resumeSandboxAdapter: ISandbox | null = null;
try {
const newAdapter = await connectToProviderSandbox(providerConfig, existingInstance.sandboxId);
resumeSandboxAdapter = newAdapter;
onProgress?.({ sandboxId: skillId, phase: 'creatingContainer' });
await newAdapter.start();
await newAdapter.waitUntilReady(60000);
const endpointInfo = await getProviderSandboxEndpoint(newAdapter, defaults.targetPort); // Wait for the HTTP service inside the container to bind its port before
// sending the ready SSE event — prevents ECONNREFUSED in the client iframe.
await waitForEndpointReady(endpointInfo);
// Update endpoint and sandbox metadata in DB
await MongoSandboxInstance.updateOne( await MongoSandboxInstance.updateOne(
{ _id: existingInstance._id }, { _id: existingInstance._id },
{ {
status: SandboxStatusEnum.running,
lastActiveAt: new Date(),
'metadata.endpoint': endpointInfo, 'metadata.endpoint': endpointInfo,
'metadata.providerStatus': { state: 'Running' } 'metadata.providerStatus': { state: 'Running' }
} }
...@@ -207,12 +189,8 @@ export async function createEditDebugSandbox( ...@@ -207,12 +189,8 @@ export async function createEditDebugSandbox(
status: { state: 'Running' } status: { state: 'Running' }
}; };
} catch (error) { } catch (error) {
addLog.error('[Sandbox] Failed to resume stopped sandbox', { error }); addLog.error('[Sandbox] Failed to ensure sandbox running', { error });
throw error; throw error;
} finally {
if (resumeSandboxAdapter) {
await disconnectFromProviderSandbox(resumeSandboxAdapter);
}
} }
} }
...@@ -248,6 +226,7 @@ export async function createEditDebugSandbox( ...@@ -248,6 +226,7 @@ export async function createEditDebugSandbox(
// === Phase 3: Sandbox operations === // === Phase 3: Sandbox operations ===
let sandbox: ISandbox | null = null; let sandbox: ISandbox | null = null;
let sandboxClient: SandboxClient | null = null;
try { try {
addLog.info('[Sandbox] Creating sandbox instance', { addLog.info('[Sandbox] Creating sandbox instance', {
...@@ -257,24 +236,16 @@ export async function createEditDebugSandbox( ...@@ -257,24 +236,16 @@ export async function createEditDebugSandbox(
onProgress?.({ sandboxId: skillId, phase: 'creatingContainer' }); onProgress?.({ sandboxId: skillId, phase: 'creatingContainer' });
const sessionId = new mongoose.Types.ObjectId().toHexString(); const sessionId = new mongoose.Types.ObjectId().toHexString();
const createEntrypoint = defaults.entrypoint; // getSandboxClient handles volumes internally (via getVolumeManagerConfig) and calls
// provider.ensureRunning() which creates the container when it doesn't exist
let volumes: OpenSandboxVolume[] | undefined; const client = await getSandboxClient(
if (providerConfig.provider === 'opensandbox' && env.AGENT_SANDBOX_ENABLE_VOLUME) { { sandboxId: sessionId },
const vmConfig = getVolumeManagerConfig(); {
const claimName = await ensureSessionVolume(sessionId, vmConfig);
volumes = [
buildVolumeConfig(providerConfig.runtime, sessionId, claimName, vmConfig.mountPath)
];
}
const newSandbox = buildSandboxAdapter(providerConfig, {
providerSandboxId: sessionId,
createConfig: { createConfig: {
image: sandboxImage, image: sandboxImage,
entrypoint: [entrypoint ?? createEntrypoint], entrypoint: [entrypoint ?? defaults.entrypoint],
env: buildBaseContainerEnv(sessionId, defaults.workDirectory, true), env: buildBaseContainerEnv(sessionId, defaults.workDirectory, true),
volumes, // volumes: handled internally by getSandboxClient via getVolumeManagerConfig
metadata: { metadata: {
skillId, skillId,
teamId, teamId,
...@@ -282,14 +253,12 @@ export async function createEditDebugSandbox( ...@@ -282,14 +253,12 @@ export async function createEditDebugSandbox(
sessionId sessionId
} }
} }
}); }
sandbox = newSandbox; // keep outer ref for finally cleanup );
sandboxClient = client;
await newSandbox.create(); sandbox = client.provider;
addLog.info('[Sandbox] Waiting for sandbox to be ready'); const sandboxInfo = await client.provider.getInfo();
await newSandbox.waitUntilReady(60000);
const sandboxInfo = await newSandbox.getInfo();
if (!sandboxInfo) throw new Error('Failed to get sandbox info after creation'); if (!sandboxInfo) throw new Error('Failed to get sandbox info after creation');
// Upload package to sandbox and extract // Upload package to sandbox and extract
...@@ -298,7 +267,7 @@ export async function createEditDebugSandbox( ...@@ -298,7 +267,7 @@ export async function createEditDebugSandbox(
addLog.info('[Sandbox] Uploading package to sandbox', { path: zipPath }); addLog.info('[Sandbox] Uploading package to sandbox', { path: zipPath });
onProgress?.({ sandboxId: skillId, phase: 'uploadingPackage' }); onProgress?.({ sandboxId: skillId, phase: 'uploadingPackage' });
await newSandbox.writeFiles([ await client.provider.writeFiles([
{ {
path: zipPath, path: zipPath,
data: standardizedBuffer data: standardizedBuffer
...@@ -307,7 +276,7 @@ export async function createEditDebugSandbox( ...@@ -307,7 +276,7 @@ export async function createEditDebugSandbox(
addLog.info('[Sandbox] Extracting package'); addLog.info('[Sandbox] Extracting package');
onProgress?.({ sandboxId: skillId, phase: 'extractingPackage' }); onProgress?.({ sandboxId: skillId, phase: 'extractingPackage' });
const extractResult = await newSandbox.execute( const extractResult = await client.provider.execute(
`mkdir -p ${defaults.workDirectory} && cd ${defaults.workDirectory} && unzip -o package.zip && rm package.zip` `mkdir -p ${defaults.workDirectory} && cd ${defaults.workDirectory} && unzip -o package.zip && rm package.zip`
); );
...@@ -319,23 +288,24 @@ export async function createEditDebugSandbox( ...@@ -319,23 +288,24 @@ export async function createEditDebugSandbox(
// Get endpoint // Get endpoint
addLog.info('[Sandbox] Getting endpoint', { port: defaults.targetPort }); addLog.info('[Sandbox] Getting endpoint', { port: defaults.targetPort });
const endpointInfo = await getProviderSandboxEndpoint(newSandbox, defaults.targetPort); const endpointInfo = await getProviderSandboxEndpoint(client.provider, defaults.targetPort);
// Wait for the HTTP service to accept connections before persisting and emitting ready.
// The container may still be initializing even after package extraction completes.
await waitForEndpointReady(endpointInfo);
addLog.info('[Sandbox] Endpoint obtained', endpointInfo); addLog.info('[Sandbox] Endpoint obtained', endpointInfo);
// Persist to MongoDB // Enrich the DB record created by getSandboxClient.ensureAvailable() with full skill metadata.
const newSandboxDoc = await mongoSessionRun(async (session) => { // Use sessionId (the client-side key) because ensureAvailable() stores the record with
const doc = await MongoSandboxInstance.create( // sandboxId=sessionId, not with the provider-assigned sandboxInfo.id.
[ const newSandboxDoc = await MongoSandboxInstance.findOneAndUpdate(
{ sandboxId: sessionId },
{ {
provider: providerConfig.provider, $set: {
sandboxId: sandboxInfo.id,
appId: skillId, appId: skillId,
userId: tmbId, userId: tmbId,
chatId: EDIT_DEBUG_CHAT_ID, chatId: EDIT_DEBUG_CHAT_ID,
status: SandboxStatusEnum.running,
lastActiveAt: new Date(),
createdAt: new Date(),
metadata: { metadata: {
sandboxType: SandboxTypeEnum.editDebug, sandboxType: SandboxTypeEnum.editDebug,
teamId, teamId,
...@@ -362,12 +332,11 @@ export async function createEditDebugSandbox( ...@@ -362,12 +332,11 @@ export async function createEditDebugSandbox(
]) ])
} }
} }
], },
{ session } { new: true }
); );
return doc[0]; if (!newSandboxDoc) throw new Error('Failed to find sandbox document after creation');
});
addLog.info('[Sandbox] Sandbox info saved to database', { addLog.info('[Sandbox] Sandbox info saved to database', {
sandboxId: newSandboxDoc._id sandboxId: newSandboxDoc._id
...@@ -377,7 +346,7 @@ export async function createEditDebugSandbox( ...@@ -377,7 +346,7 @@ export async function createEditDebugSandbox(
sandboxId: skillId, sandboxId: skillId,
phase: 'ready', phase: 'ready',
endpoint: endpointInfo, endpoint: endpointInfo,
providerSandboxId: sandboxInfo.id providerSandboxId: sessionId
}); });
return { return {
...@@ -395,10 +364,12 @@ export async function createEditDebugSandbox( ...@@ -395,10 +364,12 @@ export async function createEditDebugSandbox(
rawBody: (error as any)?.cause?.rawBody ?? (error as any)?.rawBody rawBody: (error as any)?.cause?.rawBody ?? (error as any)?.rawBody
}); });
// Cleanup provider sandbox if it was created // sandboxClient.delete() cleans up: provider container + session volume + DB record
if (sandbox) { if (sandboxClient) {
try { try {
await sandbox.delete(); await sandboxClient.delete();
// Prevent finally from trying to disconnect an already-deleted sandbox
sandbox = null;
} catch (cleanupError) { } catch (cleanupError) {
addLog.error('[Sandbox] Failed to cleanup sandbox after error', { cleanupError }); addLog.error('[Sandbox] Failed to cleanup sandbox after error', { cleanupError });
} }
......
...@@ -45,9 +45,10 @@ export const buildOpenSandboxCreateConfig = ( ...@@ -45,9 +45,10 @@ export const buildOpenSandboxCreateConfig = (
opts: { opts: {
volumes?: OpenSandboxConfigType['volumes']; volumes?: OpenSandboxConfigType['volumes'];
resourceLimits?: OpenSandboxConfigType['resourceLimits']; resourceLimits?: OpenSandboxConfigType['resourceLimits'];
createConfig?: OpenSandboxConfigType;
} = {} } = {}
): OpenSandboxConfigType => { ): OpenSandboxConfigType => {
if (!env.AGENT_SANDBOX_OPENSANDBOX_IMAGE_REPO) { if (!env.AGENT_SANDBOX_OPENSANDBOX_IMAGE_REPO && !opts.createConfig?.image) {
throw new Error('AGENT_SANDBOX_OPENSANDBOX_IMAGE_REPO is required for opensandbox provider'); throw new Error('AGENT_SANDBOX_OPENSANDBOX_IMAGE_REPO is required for opensandbox provider');
} }
return { return {
...@@ -56,6 +57,7 @@ export const buildOpenSandboxCreateConfig = ( ...@@ -56,6 +57,7 @@ export const buildOpenSandboxCreateConfig = (
tag: env.AGENT_SANDBOX_OPENSANDBOX_IMAGE_TAG tag: env.AGENT_SANDBOX_OPENSANDBOX_IMAGE_TAG
}, },
...(opts.resourceLimits ? { resourceLimits: opts.resourceLimits } : {}), ...(opts.resourceLimits ? { resourceLimits: opts.resourceLimits } : {}),
...opts.createConfig,
...(opts.volumes ? { volumes: opts.volumes } : {}) ...(opts.volumes ? { volumes: opts.volumes } : {})
}; };
}; };
......
...@@ -9,7 +9,8 @@ import { ...@@ -9,7 +9,8 @@ import {
createSandbox, createSandbox,
type ExecuteResult, type ExecuteResult,
type ISandbox, type ISandbox,
type ResourceLimits type ResourceLimits,
type OpenSandboxConfigType
} from '@fastgpt-sdk/sandbox-adapter'; } from '@fastgpt-sdk/sandbox-adapter';
import { import {
getOpenSandboxConnectionConfig, getOpenSandboxConnectionConfig,
...@@ -49,6 +50,7 @@ export class SandboxClient { ...@@ -49,6 +50,7 @@ export class SandboxClient {
private readonly opts: { private readonly opts: {
resourceLimits?: ResourceLimits; resourceLimits?: ResourceLimits;
vmConfig?: VolumeManagerResult | undefined; vmConfig?: VolumeManagerResult | undefined;
createConfig?: OpenSandboxConfigType;
} }
) { ) {
this.sandboxId = props.sandboxId; this.sandboxId = props.sandboxId;
...@@ -62,13 +64,15 @@ export class SandboxClient { ...@@ -62,13 +64,15 @@ export class SandboxClient {
const config = getSealosConnectionConfig(this.sandboxId); const config = getSealosConnectionConfig(this.sandboxId);
this.provider = createSandbox('sealosdevbox', config, undefined); this.provider = createSandbox('sealosdevbox', config, undefined);
} else if (providerName === 'opensandbox') { } else if (providerName === 'opensandbox') {
// volumes 在 ensureAvailable 中异步获取后重建 provider,此处用基础 createConfig // volumes always come from vmConfig (ensures PVC binding is correct);
// custom createConfig takes priority for image/entrypoint/env/metadata
this.provider = createSandbox( this.provider = createSandbox(
'opensandbox', 'opensandbox',
getOpenSandboxConnectionConfig({ sessionId: this.sandboxId }), getOpenSandboxConnectionConfig({ sessionId: this.sandboxId }),
buildOpenSandboxCreateConfig({ buildOpenSandboxCreateConfig({
resourceLimits: opts?.resourceLimits, resourceLimits: opts?.resourceLimits,
volumes: opts?.vmConfig?.volumes volumes: opts?.vmConfig?.volumes,
createConfig: opts?.createConfig
}) })
); );
} else if (providerName === 'e2b') { } else if (providerName === 'e2b') {
...@@ -170,6 +174,7 @@ export const getSandboxClient = async ( ...@@ -170,6 +174,7 @@ export const getSandboxClient = async (
| UnionIdType, | UnionIdType,
opts: { opts: {
resourceLimits?: ResourceLimits; resourceLimits?: ResourceLimits;
createConfig?: OpenSandboxConfigType;
} = {} } = {}
) => { ) => {
const sandboxId = (() => { const sandboxId = (() => {
......
...@@ -8,7 +8,7 @@ ...@@ -8,7 +8,7 @@
* - releaseAgentSandbox:断开 SDK 连接,不销毁容器 * - releaseAgentSandbox:断开 SDK 连接,不销毁容器
*/ */
import type { ISandbox, OpenSandboxVolume } from '@fastgpt-sdk/sandbox-adapter'; import type { ISandbox } from '@fastgpt-sdk/sandbox-adapter';
import type { HydratedDocument } from 'mongoose'; import type { HydratedDocument } from 'mongoose';
import { MongoAgentSkills } from '../../../../../../agentSkills/schema'; import { MongoAgentSkills } from '../../../../../../agentSkills/schema';
import { MongoSandboxInstance } from '../../../../../../ai/sandbox/schema'; import { MongoSandboxInstance } from '../../../../../../ai/sandbox/schema';
...@@ -19,16 +19,12 @@ import { ...@@ -19,16 +19,12 @@ import {
getSandboxProviderConfig, getSandboxProviderConfig,
getSandboxDefaults, getSandboxDefaults,
validateSandboxConfig, validateSandboxConfig,
buildSandboxAdapter,
connectToProviderSandbox,
disconnectFromProviderSandbox, disconnectFromProviderSandbox,
getVolumeManagerConfig,
ensureSessionVolume,
buildVolumeConfig,
buildBaseContainerEnv buildBaseContainerEnv
} from '../../../../../../agentSkills/sandboxConfig'; } from '../../../../../../agentSkills/sandboxConfig';
import { SandboxTypeEnum } from '@fastgpt/global/core/agentSkills/constants'; import { SandboxTypeEnum } from '@fastgpt/global/core/agentSkills/constants';
import { SandboxStatusEnum } from '@fastgpt/global/core/ai/sandbox/constants'; import { SandboxStatusEnum } from '@fastgpt/global/core/ai/sandbox/constants';
import { getSandboxClient, type SandboxClient } from '../../../../../../ai/sandbox/controller';
import { env } from '../../../../../../../env'; import { env } from '../../../../../../../env';
import type { import type {
AgentSkillSchemaType, AgentSkillSchemaType,
...@@ -202,21 +198,11 @@ export async function createAgentSandbox( ...@@ -202,21 +198,11 @@ export async function createAgentSandbox(
}); });
onProgress?.({ sandboxId: sessionId, phase: 'connecting', isWarmStart: true }); onProgress?.({ sandboxId: sessionId, phase: 'connecting', isWarmStart: true });
const sandbox = await connectToProviderSandbox(providerConfig, existingInstance.sandboxId);
if (existingInstance.status === SandboxStatusEnum.stopped) { // getSandboxClient internally calls ensureAvailable():
logger.info('[Agent Sandbox] Resuming stopped sandbox', { // - updates DB status=running, lastActiveAt
sessionId, // - calls provider.ensureRunning() to ensure container is running (handles stopped→running)
providerSandboxId: existingInstance.sandboxId const client = await getSandboxClient({ sandboxId: existingInstance.sandboxId });
});
await sandbox.start();
await sandbox.waitUntilReady(60000);
}
await MongoSandboxInstance.updateOne(
{ _id: existingInstance._id },
{ lastActiveAt: new Date() }
);
const reusedSkillIds = existingInstance.metadata?.skillIds const reusedSkillIds = existingInstance.metadata?.skillIds
? existingInstance.metadata.skillIds.map(String) ? existingInstance.metadata.skillIds.map(String)
...@@ -227,10 +213,9 @@ export async function createAgentSandbox( ...@@ -227,10 +213,9 @@ export async function createAgentSandbox(
const mergedSkills = skills.map((skill) => const mergedSkills = skills.map((skill) =>
mergeSkillWithVersion(skill.toJSON(), versionMap.get(String(skill._id))) mergeSkillWithVersion(skill.toJSON(), versionMap.get(String(skill._id)))
); );
// Dynamically discover deployed skills instead of reconstructing from DB name assumptions const deployedSkills = await discoverSkillsInSandbox(client.provider, defaults.workDirectory);
const deployedSkills = await discoverSkillsInSandbox(sandbox, defaults.workDirectory);
return { return {
sandbox, sandbox: client.provider,
providerSandboxId: existingInstance.sandboxId, providerSandboxId: existingInstance.sandboxId,
sessionId, sessionId,
skills: mergedSkills, skills: mergedSkills,
...@@ -284,26 +269,22 @@ export async function createAgentSandbox( ...@@ -284,26 +269,22 @@ export async function createAgentSandbox(
} }
} }
// Step 3: Create sandbox container, inject SESSION_ID // Step 3: Create sandbox container via getSandboxClient (handles volumes internally)
let sandbox: ISandbox | null = null; let sandboxClient: SandboxClient | null = null;
try { try {
const createEntrypoint = defaults.entrypoint; onProgress?.({ sandboxId: sessionId, phase: 'creatingContainer', isWarmStart: false });
let volumes: OpenSandboxVolume[] | undefined;
if (providerConfig.provider === 'opensandbox' && env.AGENT_SANDBOX_ENABLE_VOLUME) {
const vmConfig = getVolumeManagerConfig();
const claimName = await ensureSessionVolume(sessionId, vmConfig);
volumes = [
buildVolumeConfig(providerConfig.runtime, sessionId, claimName, vmConfig.mountPath)
];
}
const createConfig = { // getSandboxClient handles volumes internally (via getVolumeManagerConfig) and calls
// provider.ensureRunning() which creates the container when it doesn't exist
const client = await getSandboxClient(
{ sandboxId: sessionId },
{
createConfig: {
image: image ?? defaults.defaultImage, image: image ?? defaults.defaultImage,
entrypoint: [entrypoint ?? createEntrypoint], entrypoint: [entrypoint ?? defaults.entrypoint],
env: buildBaseContainerEnv(sessionId, defaults.workDirectory, false), env: buildBaseContainerEnv(sessionId, defaults.workDirectory, false),
volumes, // volumes: handled internally by getSandboxClient via getVolumeManagerConfig
metadata: { metadata: {
teamId, teamId,
tmbId, tmbId,
...@@ -311,19 +292,12 @@ export async function createAgentSandbox( ...@@ -311,19 +292,12 @@ export async function createAgentSandbox(
skillIds: skillIds.join('-'), skillIds: skillIds.join('-'),
sessionId sessionId
} }
}; }
}
sandbox = buildSandboxAdapter(providerConfig, { );
providerSandboxId: sessionId, sandboxClient = client;
createConfig
});
onProgress?.({ sandboxId: sessionId, phase: 'creatingContainer', isWarmStart: false });
await sandbox.create();
await sandbox.waitUntilReady(60000);
const sandboxInfo = await sandbox.getInfo(); const sandboxInfo = await client.provider.getInfo();
if (!sandboxInfo) throw new Error('Failed to get sandbox info after creation'); if (!sandboxInfo) throw new Error('Failed to get sandbox info after creation');
logger.info('[Agent Sandbox] Sandbox created', { logger.info('[Agent Sandbox] Sandbox created', {
...@@ -334,7 +308,7 @@ export async function createAgentSandbox( ...@@ -334,7 +308,7 @@ export async function createAgentSandbox(
// Step 4: Deploy skill packages (only when skills are configured) // Step 4: Deploy skill packages (only when skills are configured)
if (hasSkills) { if (hasSkills) {
await deploySkillsToSandbox( await deploySkillsToSandbox(
sandbox, client.provider,
deployableSkills, deployableSkills,
versionMap, versionMap,
defaults.workDirectory, defaults.workDirectory,
...@@ -343,19 +317,19 @@ export async function createAgentSandbox( ...@@ -343,19 +317,19 @@ export async function createAgentSandbox(
); );
} }
const deployedSkills = hasSkills const deployedSkills = hasSkills
? await discoverSkillsInSandbox(sandbox, defaults.workDirectory) ? await discoverSkillsInSandbox(client.provider, defaults.workDirectory)
: []; : [];
// Step 5: Persist to MongoDB // Step 5: Enrich the DB record created by getSandboxClient.ensureAvailable() with full metadata.
await MongoSandboxInstance.create({ // Use sessionId (the client-side key) because ensureAvailable() stores the record with
provider: providerConfig.provider, // sandboxId=sessionId, not with the provider-assigned sandboxInfo.id.
sandboxId: sandboxInfo.id, await MongoSandboxInstance.findOneAndUpdate(
{ sandboxId: sessionId },
{
$set: {
appId: teamId, // session-runtime uses teamId as appId appId: teamId, // session-runtime uses teamId as appId
userId: tmbId, userId: tmbId,
chatId: sessionId, chatId: sessionId,
status: SandboxStatusEnum.running,
lastActiveAt: new Date(),
createdAt: new Date(),
metadata: { metadata: {
sandboxType: SandboxTypeEnum.sessionRuntime, sandboxType: SandboxTypeEnum.sessionRuntime,
teamId, teamId,
...@@ -371,13 +345,15 @@ export async function createAgentSandbox( ...@@ -371,13 +345,15 @@ export async function createAgentSandbox(
}, },
providerCreatedAt: sandboxInfo.createdAt providerCreatedAt: sandboxInfo.createdAt
} }
}); }
}
);
logger.info('[Agent Sandbox] Sandbox info saved to MongoDB', { sessionId }); logger.info('[Agent Sandbox] Sandbox info saved to MongoDB', { sessionId });
onProgress?.({ sandboxId: sessionId, phase: 'ready', isWarmStart: false }); onProgress?.({ sandboxId: sessionId, phase: 'ready', isWarmStart: false });
return { return {
sandbox, sandbox: client.provider,
providerSandboxId: sandboxInfo.id, providerSandboxId: sandboxInfo.id,
sessionId, sessionId,
skills: hasSkills skills: hasSkills
...@@ -392,13 +368,14 @@ export async function createAgentSandbox( ...@@ -392,13 +368,14 @@ export async function createAgentSandbox(
} catch (error) { } catch (error) {
logger.error('[Agent Sandbox] Failed to create sandbox', { error }); logger.error('[Agent Sandbox] Failed to create sandbox', { error });
if (sandbox) { if (sandboxClient) {
try { try {
await sandbox.delete(); // sandboxClient.delete() cleans up: provider container + session volume + DB record
await sandboxClient.delete();
} catch (cleanupError) { } catch (cleanupError) {
logger.error('[Agent Sandbox] Cleanup failed after creation error', { cleanupError }); logger.error('[Agent Sandbox] Cleanup failed after creation error', { cleanupError });
} }
await disconnectFromProviderSandbox(sandbox); await disconnectFromProviderSandbox(sandboxClient.provider);
} }
throw error; throw error;
...@@ -436,9 +413,7 @@ export async function connectEditDebugSandbox( ...@@ -436,9 +413,7 @@ export async function connectEditDebugSandbox(
params: ConnectEditDebugSandboxParams params: ConnectEditDebugSandboxParams
): Promise<AgentSandboxContext> { ): Promise<AgentSandboxContext> {
const { skillId, teamId } = params; const { skillId, teamId } = params;
const providerConfig = getSandboxProviderConfig();
const defaults = getSandboxDefaults(); const defaults = getSandboxDefaults();
validateSandboxConfig(providerConfig);
const instanceDoc = await MongoSandboxInstance.findOne({ const instanceDoc = await MongoSandboxInstance.findOne({
appId: skillId, appId: skillId,
...@@ -458,9 +433,10 @@ export async function connectEditDebugSandbox( ...@@ -458,9 +433,10 @@ export async function connectEditDebugSandbox(
throw new Error('Skill not found'); throw new Error('Skill not found');
} }
const sandbox = await connectToProviderSandbox(providerConfig, instanceDoc.sandboxId); // getSandboxClient internally calls ensureAvailable():
// - updates DB status=running, lastActiveAt
await MongoSandboxInstance.updateOne({ _id: instanceDoc._id }, { lastActiveAt: new Date() }); // - calls provider.ensureRunning() to ensure container is running (handles stopped→running)
const client = await getSandboxClient({ sandboxId: instanceDoc.sandboxId });
logger.info('[Agent Sandbox] Connected to edit-debug sandbox', { logger.info('[Agent Sandbox] Connected to edit-debug sandbox', {
skillId, skillId,
...@@ -468,10 +444,10 @@ export async function connectEditDebugSandbox( ...@@ -468,10 +444,10 @@ export async function connectEditDebugSandbox(
}); });
// Dynamically discover deployed skills instead of reading from persisted metadata // Dynamically discover deployed skills instead of reading from persisted metadata
const deployedSkills = await discoverSkillsInSandbox(sandbox, defaults.workDirectory); const deployedSkills = await discoverSkillsInSandbox(client.provider, defaults.workDirectory);
return { return {
sandbox, sandbox: client.provider,
providerSandboxId: instanceDoc.sandboxId, providerSandboxId: instanceDoc.sandboxId,
sessionId: String(instanceDoc._id), // editDebug sandbox uses its own _id as sessionId sessionId: String(instanceDoc._id), // editDebug sandbox uses its own _id as sessionId
skills: [skill.toJSON()], skills: [skill.toJSON()],
......
...@@ -8,7 +8,7 @@ ...@@ -8,7 +8,7 @@
}, },
"dependencies": { "dependencies": {
"@apidevtools/json-schema-ref-parser": "^11.7.2", "@apidevtools/json-schema-ref-parser": "^11.7.2",
"@fastgpt-sdk/sandbox-adapter": "^0.0.34", "@fastgpt-sdk/sandbox-adapter": "^0.0.35",
"@fastgpt-sdk/otel": "catalog:", "@fastgpt-sdk/otel": "catalog:",
"@fastgpt-sdk/storage": "catalog:", "@fastgpt-sdk/storage": "catalog:",
"@fastgpt/global": "workspace:*", "@fastgpt/global": "workspace:*",
......
...@@ -250,8 +250,8 @@ importers: ...@@ -250,8 +250,8 @@ importers:
specifier: 'catalog:' specifier: 'catalog:'
version: 0.1.2 version: 0.1.2
'@fastgpt-sdk/sandbox-adapter': '@fastgpt-sdk/sandbox-adapter':
specifier: ^0.0.34 specifier: ^0.0.35
version: 0.0.34 version: 0.0.35
'@fastgpt-sdk/storage': '@fastgpt-sdk/storage':
specifier: 'catalog:' specifier: 'catalog:'
version: 0.6.15(@opentelemetry/api@1.9.0)(@types/node@24.0.13)(jiti@2.6.0)(lightningcss@1.30.1)(proxy-agent@6.5.0)(sass@1.85.1)(terser@5.39.0)(tsx@4.20.6)(yaml@2.8.1) version: 0.6.15(@opentelemetry/api@1.9.0)(@types/node@24.0.13)(jiti@2.6.0)(lightningcss@1.30.1)(proxy-agent@6.5.0)(sass@1.85.1)(terser@5.39.0)(tsx@4.20.6)(yaml@2.8.1)
...@@ -1213,6 +1213,10 @@ importers: ...@@ -1213,6 +1213,10 @@ importers:
packages: packages:
'@alibaba-group/opensandbox@0.1.6':
resolution: {integrity: sha512-mZ2Q2qXNC0dgctoPIlcotnlPSJ1ODMG4DKQ3AA2lTO4ZoC/vWU3CzSL5pNEU7hakfMOotQiZVxunNItVGY4W8w==}
engines: {node: '>=20'}
'@alloc/quick-lru@5.2.0': '@alloc/quick-lru@5.2.0':
resolution: {integrity: sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==} resolution: {integrity: sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==}
engines: {node: '>=10'} engines: {node: '>=10'}
...@@ -2747,8 +2751,8 @@ packages: ...@@ -2747,8 +2751,8 @@ packages:
'@fastgpt-sdk/plugin@0.3.8': '@fastgpt-sdk/plugin@0.3.8':
resolution: {integrity: sha512-GjKrXMHxeF5UMkYGXawrUpzZjVRw3DICNYODeYwsUVOy+/ltu5zuwsqLkuuGQ7Arp/SBCmYRjG/MHmeNp4xxfw==} resolution: {integrity: sha512-GjKrXMHxeF5UMkYGXawrUpzZjVRw3DICNYODeYwsUVOy+/ltu5zuwsqLkuuGQ7Arp/SBCmYRjG/MHmeNp4xxfw==}
'@fastgpt-sdk/sandbox-adapter@0.0.34': '@fastgpt-sdk/sandbox-adapter@0.0.35':
resolution: {integrity: sha512-YXCwycqs2yByOPUMMjm2tf0BYUJfLR9D4bvHDv6xIbfKT5btT+hR1pujW5nVawXJDefYNsDfLy8dQ+IkMd21xQ==} resolution: {integrity: sha512-pgK4qRqt24xhs4tz5oZfYK7GYloYbJrFsONWZMiFvuRvPVF7hn0BRN0er3akXhX10gUj3ASFwLnWxycEsggapQ==}
engines: {node: '>=18'} engines: {node: '>=18'}
'@fastgpt-sdk/storage@0.6.15': '@fastgpt-sdk/storage@0.6.15':
...@@ -11772,6 +11776,11 @@ packages: ...@@ -11772,6 +11776,11 @@ packages:
snapshots: snapshots:
'@alibaba-group/opensandbox@0.1.6':
dependencies:
openapi-fetch: 0.14.1
undici: 7.18.2
'@alloc/quick-lru@5.2.0': {} '@alloc/quick-lru@5.2.0': {}
'@ampproject/remapping@2.3.0': '@ampproject/remapping@2.3.0':
...@@ -13767,8 +13776,9 @@ snapshots: ...@@ -13767,8 +13776,9 @@ snapshots:
'@fortaine/fetch-event-source': 3.0.6 '@fortaine/fetch-event-source': 3.0.6
zod: 4.1.12 zod: 4.1.12
'@fastgpt-sdk/sandbox-adapter@0.0.34': '@fastgpt-sdk/sandbox-adapter@0.0.35':
dependencies: dependencies:
'@alibaba-group/opensandbox': 0.1.6
'@e2b/code-interpreter': 2.4.0 '@e2b/code-interpreter': 2.4.0
'@fastgpt-sdk/storage@0.6.15(@opentelemetry/api@1.9.0)(@types/node@20.17.24)(jiti@2.6.0)(lightningcss@1.30.1)(sass@1.85.1)(terser@5.39.0)(tsx@4.20.6)(yaml@2.8.1)': '@fastgpt-sdk/storage@0.6.15(@opentelemetry/api@1.9.0)(@types/node@20.17.24)(jiti@2.6.0)(lightningcss@1.30.1)(sass@1.85.1)(terser@5.39.0)(tsx@4.20.6)(yaml@2.8.1)':
......
...@@ -50,6 +50,7 @@ AGENT_SANDBOX_OPENSANDBOX_IMAGE_TAG=v0.1 ...@@ -50,6 +50,7 @@ AGENT_SANDBOX_OPENSANDBOX_IMAGE_TAG=v0.1
AGENT_SANDBOX_ENABLE_VOLUME=true AGENT_SANDBOX_ENABLE_VOLUME=true
AGENT_SANDBOX_VOLUME_MANAGER_URL=http://localhost:3005 AGENT_SANDBOX_VOLUME_MANAGER_URL=http://localhost:3005
AGENT_SANDBOX_VOLUME_MANAGER_TOKEN=vmtoken AGENT_SANDBOX_VOLUME_MANAGER_TOKEN=vmtoken
# Recommended to set mount path to /home/sandbox when sandbox provider is opensandbox
AGENT_SANDBOX_VOLUME_MANAGER_MOUNT_PATH=/workspace AGENT_SANDBOX_VOLUME_MANAGER_MOUNT_PATH=/workspace
# E2B 配置(PROVIDER=e2b 时生效) # E2B 配置(PROVIDER=e2b 时生效)
AGENT_SANDBOX_E2B_API_KEY= AGENT_SANDBOX_E2B_API_KEY=
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment