Commit 50d29b0c by DigHuang Committed by GitHub

feat(sandbox): require AGENT_SANDBOX_SEALOS_IMAGE for sealosdevbox (#7214)

parent a48fd5ba
......@@ -450,4 +450,4 @@
"content/self-host/upgrading/upgrade-intruction.mdx": "2026-04-26T21:08:47+08:00",
"content/toc.en.mdx": "2026-06-29T23:38:28+08:00",
"content/toc.mdx": "2026-06-29T23:38:28+08:00"
}
\ No newline at end of file
}
......@@ -7,6 +7,14 @@ import {
SANDBOX_SHELL_TOOL_NAME,
SANDBOX_WRITE_FILE_TOOL_NAME
} from './tools';
import type { SandboxProviderType } from '@fastgpt-sdk/sandbox-adapter';
// ---- 沙盒提供方 ----
export const agentSandboxProviderList = [
'sealosdevbox',
'opensandbox',
'e2b'
] as const satisfies readonly SandboxProviderType[];
// ---- 沙盒状态 ----
export const SandboxStatusEnum = {
......
import { agentSandboxProviderList } from './constants';
import type { SandboxProviderType } from '@fastgpt-sdk/sandbox-adapter';
export const agentSandboxProviderRequiredEnvKeys = {
sealosdevbox: [
'AGENT_SANDBOX_SEALOS_BASEURL',
'AGENT_SANDBOX_SEALOS_TOKEN',
'AGENT_SANDBOX_SEALOS_IMAGE'
],
opensandbox: ['AGENT_SANDBOX_OPENSANDBOX_BASEURL', 'AGENT_SANDBOX_OPENSANDBOX_API_KEY'],
e2b: ['AGENT_SANDBOX_E2B_API_KEY']
} satisfies Record<SandboxProviderType, readonly string[]>;
export type AgentSandboxEnvSource = Record<string, string | undefined>;
const isAgentSandboxProvider = (provider: string | undefined): provider is SandboxProviderType =>
agentSandboxProviderList.includes(provider as SandboxProviderType);
/**
* 判断系统是否显式配置了 Agent 虚拟机能力。
* 必须基于原始 env 判断,避免被服务端 env schema 的默认 provider 误判为已启用。
*/
export const hasAgentSandboxConfig = (env: AgentSandboxEnvSource): boolean => {
const provider = env.AGENT_SANDBOX_PROVIDER;
if (provider === 'sealosdevbox') {
return !!(env.AGENT_SANDBOX_SEALOS_BASEURL && env.AGENT_SANDBOX_SEALOS_TOKEN);
}
if (provider === 'opensandbox') {
return !!(env.AGENT_SANDBOX_OPENSANDBOX_BASEURL && env.AGENT_SANDBOX_OPENSANDBOX_API_KEY);
}
if (provider === 'e2b') {
return !!env.AGENT_SANDBOX_E2B_API_KEY;
if (!isAgentSandboxProvider(provider)) {
return false;
}
return false;
return agentSandboxProviderRequiredEnvKeys[provider].every((key) => !!env[key]);
};
......@@ -14,6 +14,7 @@
"@apidevtools/swagger-parser": "^10.1.0",
"@bany/curl-to-json": "^1.2.8",
"@fastgpt-plugin/sdk-client": "0.0.1-alpha.11",
"@fastgpt-sdk/sandbox-adapter": "workspace:*",
"axios": "catalog:",
"ipaddr.js": "catalog:",
"cron-parser": "^4.9.0",
......
import { describe, expect, it } from 'vitest';
import { agentSandboxProviderList } from '@fastgpt/global/core/ai/sandbox/constants';
import {
agentSandboxProviderRequiredEnvKeys,
hasAgentSandboxConfig
} from '@fastgpt/global/core/ai/sandbox/env';
describe('agent sandbox env config', () => {
it('keeps provider list aligned with required env keys', () => {
expect(Object.keys(agentSandboxProviderRequiredEnvKeys).sort()).toEqual(
[...agentSandboxProviderList].sort()
);
});
it('requires all provider env keys before enabling agent sandbox', () => {
expect(
hasAgentSandboxConfig({
AGENT_SANDBOX_PROVIDER: 'sealosdevbox',
AGENT_SANDBOX_SEALOS_BASEURL: 'https://devbox.example.com',
AGENT_SANDBOX_SEALOS_TOKEN: 'token'
})
).toBe(false);
expect(
hasAgentSandboxConfig({
AGENT_SANDBOX_PROVIDER: 'sealosdevbox',
AGENT_SANDBOX_SEALOS_BASEURL: 'https://devbox.example.com',
AGENT_SANDBOX_SEALOS_TOKEN: 'token',
AGENT_SANDBOX_SEALOS_IMAGE: 'runtime/fastgpt:stable'
})
).toBe(true);
});
it('ignores missing or unsupported providers', () => {
expect(hasAgentSandboxConfig({})).toBe(false);
expect(hasAgentSandboxConfig({ AGENT_SANDBOX_PROVIDER: 'unknown' })).toBe(false);
});
});
......@@ -7,10 +7,10 @@ import {
createSandbox,
type ISandbox,
type ResourceLimits,
type SandboxCreateSpec
type SandboxCreateSpec,
type SandboxProviderType
} from '@fastgpt-sdk/sandbox-adapter';
import { getSandboxAdapterConfig, type SandboxProviderConfig } from './config';
import type { SandboxProviderType } from '../../type';
import type { VolumeManagerResult } from '../volume/service';
function assertNever(value: never): never {
......
......@@ -27,6 +27,10 @@ export function buildSealosRuntimeProfile(): SandboxRuntimeProfile {
buildConfig(input = {}) {
const createConfig = input.createConfig ?? {};
const image = input.image ?? createConfig.image ?? defaultImage;
if (!image?.repository) {
throw new Error('AGENT_SANDBOX_SEALOS_IMAGE is required for sealosdevbox provider');
}
const env = mergeStringRecord(createConfig.env, input.env);
const metadata = mergeUnknownRecord(createConfig.metadata, input.metadata);
// Sealos adapter 会把 workingDir 写入 CODEX_GATEWAY_CWD,让 exec/code-server 落在同一工作区。
......@@ -36,7 +40,7 @@ export function buildSealosRuntimeProfile(): SandboxRuntimeProfile {
return {
...createConfig,
...(image ? { image } : {}),
image,
...(env ? { env } : {}),
...(metadata ? { metadata } : {}),
...(workingDir ? { workingDir } : {}),
......
......@@ -4,11 +4,15 @@
* 只定义 sandbox 实例、provider、archive 和 metadata schema,不访问服务端资源。
*/
import z from 'zod';
import { SandboxStatusEnum, SandboxTypeEnum } from '@fastgpt/global/core/ai/sandbox/constants';
import {
agentSandboxProviderList,
SandboxStatusEnum,
SandboxTypeEnum
} from '@fastgpt/global/core/ai/sandbox/constants';
import { ChatSourceTypeEnum } from '@fastgpt/global/core/chat/constants';
// ---- 沙盒实例 DB 类型 ----
export const SandboxProviderSchema = z.enum(['sealosdevbox', 'opensandbox', 'e2b']);
export const SandboxProviderSchema = z.enum(agentSandboxProviderList);
export type SandboxProviderType = z.infer<typeof SandboxProviderSchema>;
export const SharedSandboxStatusSchema = z.enum(SandboxStatusEnum);
export type SharedSandboxStatusType = z.infer<typeof SharedSandboxStatusSchema>;
......
......@@ -3,6 +3,7 @@ import z from 'zod';
import { isPhaseProductionBuild } from '@fastgpt/global/common/system/constants';
import { DEFAULT_MAX_FOLDER_DEPTH } from '@fastgpt/global/common/parentFolder/depth';
import { BoolSchema, IntSchema, NumSchema, UrlSchema } from '@fastgpt/global/common/zod';
import { agentSandboxProviderList } from '@fastgpt/global/core/ai/sandbox/constants';
import { hasAgentSandboxConfig as hasAgentSandboxConfigFromEnv } from '@fastgpt/global/core/ai/sandbox/env';
const defaultableIntSchema = (defaultValue: number) =>
......@@ -79,7 +80,7 @@ export const serviceEnv = createEnv({
.optional(),
AGENT_SANDBOX_PROXY_URL: AgentSandboxProxyUrlSchema.optional(),
// Agent sandbox
AGENT_SANDBOX_PROVIDER: z.enum(['sealosdevbox', 'opensandbox', 'e2b']).optional(),
AGENT_SANDBOX_PROVIDER: z.enum(agentSandboxProviderList).optional(),
IDE_AGENT_BIND_ADDR: z.string().default('0.0.0.0:1318'),
// E2B配置
AGENT_SANDBOX_E2B_API_KEY: z.string().optional(),
......
......@@ -19,6 +19,7 @@ import {
} from '@fastgpt/service/core/ai/sandbox/infrastructure/instance/repository';
import { connectionMongo } from '@fastgpt/service/common/mongo';
import { SandboxStatusEnum, SandboxTypeEnum } from '@fastgpt/global/core/ai/sandbox/constants';
import { hasAgentSandboxConfig } from '@fastgpt/global/core/ai/sandbox/env';
import { ChatSourceTypeEnum } from '@fastgpt/global/core/chat/constants';
import { delay } from '@fastgpt/global/common/system/utils';
import { getRunningSandboxId } from '@fastgpt/service/core/ai/sandbox/utils/id';
......@@ -26,13 +27,7 @@ import { getRunningSandboxId } from '@fastgpt/service/core/ai/sandbox/utils/id';
const { Types } = connectionMongo;
const hasSandboxEnv =
process.env.SANDBOX_INTEGRATION === 'true' &&
!!process.env.AGENT_SANDBOX_PROVIDER &&
(process.env.AGENT_SANDBOX_PROVIDER === 'e2b'
? !!process.env.AGENT_SANDBOX_E2B_API_KEY
: process.env.AGENT_SANDBOX_PROVIDER === 'sealosdevbox'
? !!process.env.AGENT_SANDBOX_SEALOS_BASEURL
: !!process.env.AGENT_SANDBOX_OPENSANDBOX_BASEURL);
process.env.SANDBOX_INTEGRATION === 'true' && hasAgentSandboxConfig(process.env);
const runFullIntegration = process.env.SANDBOX_INTEGRATION_FULL === 'true';
vi.mock('@fastgpt/service/env', () => ({
......@@ -46,6 +41,7 @@ vi.mock('@fastgpt/service/env', () => ({
AGENT_SANDBOX_PROVIDER: process.env.AGENT_SANDBOX_PROVIDER,
AGENT_SANDBOX_SEALOS_BASEURL: process.env.AGENT_SANDBOX_SEALOS_BASEURL,
AGENT_SANDBOX_SEALOS_TOKEN: process.env.AGENT_SANDBOX_SEALOS_TOKEN,
AGENT_SANDBOX_SEALOS_IMAGE: process.env.AGENT_SANDBOX_SEALOS_IMAGE,
AGENT_SANDBOX_OPENSANDBOX_BASEURL: process.env.AGENT_SANDBOX_OPENSANDBOX_BASEURL,
AGENT_SANDBOX_OPENSANDBOX_API_KEY: process.env.AGENT_SANDBOX_OPENSANDBOX_API_KEY,
......
......@@ -14,7 +14,7 @@ const originalEnv = {
AGENT_SANDBOX_OPENSANDBOX_IMAGE_TAG: process.env.AGENT_SANDBOX_OPENSANDBOX_IMAGE_TAG,
AGENT_SANDBOX_DISK_MB: process.env.AGENT_SANDBOX_DISK_MB,
AGENT_SANDBOX_PROXY_SECRET: process.env.AGENT_SANDBOX_PROXY_SECRET,
AGENT_SANDBOX_PROXY_URL: process.env.AGENT_SANDBOX_PROXY_URL
AGENT_SANDBOX_PROXY_URL: process.env.AGENT_SANDBOX_PROXY_URL,
AGENT_SANDBOX_WS_MAX_MESSAGE_BYTES: process.env.AGENT_SANDBOX_WS_MAX_MESSAGE_BYTES,
AGENT_SANDBOX_WS_MAX_FRAME_BYTES: process.env.AGENT_SANDBOX_WS_MAX_FRAME_BYTES,
};
......@@ -81,6 +81,7 @@ describe('sandbox provider config', () => {
vi.stubEnv('AGENT_SANDBOX_PROVIDER', 'sealosdevbox');
vi.stubEnv('AGENT_SANDBOX_SEALOS_BASEURL', 'https://devbox.example.com');
vi.stubEnv('AGENT_SANDBOX_SEALOS_TOKEN', 'sealos-token');
vi.stubEnv('AGENT_SANDBOX_SEALOS_IMAGE', 'default-sealos-image:latest');
const { getSandboxProviderConfig } = await loadSandboxConfigModule();
......@@ -145,12 +146,12 @@ describe('sandbox provider config', () => {
vi.stubEnv('AGENT_SANDBOX_SEALOS_BASEURL', 'https://devbox.example.com');
vi.stubEnv('AGENT_SANDBOX_SEALOS_TOKEN', 'sealos-token');
vi.stubEnv('AGENT_SANDBOX_SEALOS_WORK_DIRECTORY', '/home/devbox/workspace');
vi.stubEnv('AGENT_SANDBOX_SEALOS_IMAGE', 'default-sealos-image:latest');
vi.stubEnv('AGENT_SANDBOX_WS_MAX_MESSAGE_BYTES', '67108864');
vi.stubEnv('AGENT_SANDBOX_WS_MAX_FRAME_BYTES', '16777216');
const { getSandboxAdapterConfig } = await loadSandboxConfigModule();
// 1. 无环境变量 Image 时,传空 repository 让 Sealos 走默认 agent 镜像
const result = getSandboxAdapterConfig({
provider: 'sealosdevbox',
runtime: true,
......@@ -165,7 +166,8 @@ describe('sandbox provider config', () => {
expect(result.createConfig).toEqual({
image: {
repository: ''
repository: 'default-sealos-image',
tag: 'latest'
},
workingDir: '/home/devbox/workspace',
upstreamID: 'session-1',
......@@ -180,23 +182,7 @@ describe('sandbox provider config', () => {
}
});
// 2. 有环境变量 Image 时,携带 image 字段
vi.stubEnv('AGENT_SANDBOX_SEALOS_IMAGE', 'default-sealos-image:latest');
vi.resetModules();
const { getSandboxAdapterConfig: getSandboxAdapterConfigWithImage } =
await loadSandboxConfigModule();
const resultWithEnvImage = getSandboxAdapterConfigWithImage({
provider: 'sealosdevbox',
runtime: true,
sessionId: 'session-1'
});
expect(resultWithEnvImage.createConfig?.image).toEqual({
repository: 'default-sealos-image',
tag: 'latest'
});
// 3. 显式传入镜像时,覆盖环境变量中的默认镜像
const resultWithExplicitImage = getSandboxAdapterConfigWithImage({
const resultWithExplicitImage = getSandboxAdapterConfig({
provider: 'sealosdevbox',
runtime: true,
sessionId: 'session-1',
......@@ -210,6 +196,22 @@ describe('sandbox provider config', () => {
});
});
it('requires sealos runtime image when runtime adapter config is requested', async () => {
vi.stubEnv('AGENT_SANDBOX_SEALOS_BASEURL', 'https://devbox.example.com');
vi.stubEnv('AGENT_SANDBOX_SEALOS_TOKEN', 'sealos-token');
vi.stubEnv('AGENT_SANDBOX_SEALOS_IMAGE', undefined);
const { getSandboxAdapterConfig } = await loadSandboxConfigModule();
expect(() =>
getSandboxAdapterConfig({
provider: 'sealosdevbox',
runtime: true,
sessionId: 'session-1'
})
).toThrow('AGENT_SANDBOX_SEALOS_IMAGE is required for sealosdevbox provider');
});
it('normalizes missing provider env values before validation', async () => {
vi.resetModules();
vi.doMock('@fastgpt/service/env', () => ({
......
......@@ -4,7 +4,8 @@ const originalEnv = {
AGENT_SANDBOX_PROVIDER: process.env.AGENT_SANDBOX_PROVIDER,
AGENT_SANDBOX_OPENSANDBOX_IMAGE_REPO: process.env.AGENT_SANDBOX_OPENSANDBOX_IMAGE_REPO,
AGENT_SANDBOX_OPENSANDBOX_IMAGE_TAG: process.env.AGENT_SANDBOX_OPENSANDBOX_IMAGE_TAG,
AGENT_SANDBOX_SEALOS_WORK_DIRECTORY: process.env.AGENT_SANDBOX_SEALOS_WORK_DIRECTORY
AGENT_SANDBOX_SEALOS_WORK_DIRECTORY: process.env.AGENT_SANDBOX_SEALOS_WORK_DIRECTORY,
AGENT_SANDBOX_SEALOS_IMAGE: process.env.AGENT_SANDBOX_SEALOS_IMAGE
};
const loadSandboxRuntimeProfileModule = async () => {
......@@ -27,6 +28,7 @@ describe('sandbox runtime profile', () => {
'AGENT_SANDBOX_SEALOS_WORK_DIRECTORY',
originalEnv.AGENT_SANDBOX_SEALOS_WORK_DIRECTORY
);
vi.stubEnv('AGENT_SANDBOX_SEALOS_IMAGE', originalEnv.AGENT_SANDBOX_SEALOS_IMAGE);
});
it('uses fixed /workspace as opensandbox work directory', async () => {
......@@ -51,12 +53,17 @@ describe('sandbox runtime profile', () => {
it('uses devbox defaults for sealosdevbox provider', async () => {
vi.stubEnv('AGENT_SANDBOX_PROVIDER', 'sealosdevbox');
vi.stubEnv('AGENT_SANDBOX_SEALOS_IMAGE', 'runtime/fastgpt:stable');
const { getSandboxRuntimeProfile } = await loadSandboxRuntimeProfileModule();
const runtimeProfile = getSandboxRuntimeProfile();
expect(runtimeProfile).toMatchObject({
provider: 'sealosdevbox',
defaultImage: {
repository: 'runtime/fastgpt',
tag: 'stable'
},
workDirectory: '/home/devbox/workspace',
entrypoint: ''
});
......@@ -66,6 +73,7 @@ describe('sandbox runtime profile', () => {
it('uses sealos work directory from env', async () => {
vi.stubEnv('AGENT_SANDBOX_PROVIDER', 'sealosdevbox');
vi.stubEnv('AGENT_SANDBOX_SEALOS_WORK_DIRECTORY', '/custom/devbox/workspace');
vi.stubEnv('AGENT_SANDBOX_SEALOS_IMAGE', 'runtime/fastgpt:stable');
const { getSandboxRuntimeProfile } = await loadSandboxRuntimeProfileModule();
......@@ -76,8 +84,21 @@ describe('sandbox runtime profile', () => {
});
});
it('requires sealos runtime image when building create config', async () => {
vi.stubEnv('AGENT_SANDBOX_PROVIDER', 'sealosdevbox');
vi.stubEnv('AGENT_SANDBOX_SEALOS_IMAGE', undefined);
const { getSandboxRuntimeProfile } = await loadSandboxRuntimeProfileModule();
const runtimeProfile = getSandboxRuntimeProfile();
expect(() => runtimeProfile.buildConfig()).toThrow(
'AGENT_SANDBOX_SEALOS_IMAGE is required for sealosdevbox provider'
);
});
it('builds provider-specific create config through runtime profile', async () => {
vi.stubEnv('AGENT_SANDBOX_SEALOS_WORK_DIRECTORY', '/custom/devbox/workspace');
vi.stubEnv('AGENT_SANDBOX_SEALOS_IMAGE', 'runtime/fastgpt:stable');
const { buildBaseSandboxRuntimeEnv, getSandboxRuntimeProfile } =
await loadSandboxRuntimeProfileModule();
......@@ -100,6 +121,10 @@ describe('sandbox runtime profile', () => {
metadata: { teamId: 'team-1' }
})
).toMatchObject({
image: {
repository: 'runtime/fastgpt',
tag: 'stable'
},
env: {
FASTGPT_SESSION_ID: 'session-1',
FASTGPT_WORKDIR: '/custom/devbox/workspace',
......
......@@ -408,6 +408,9 @@ importers:
'@fastgpt-plugin/sdk-client':
specifier: 0.0.1-alpha.11
version: 0.0.1-alpha.11(zod@4.1.12)
'@fastgpt-sdk/sandbox-adapter':
specifier: workspace:*
version: link:../../sdk/sandbox-adapter
axios:
specifier: 'catalog:'
version: 1.16.0
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