From e989526886b2e459dcb6795bc3a61f0792748e7a Mon Sep 17 00:00:00 2001 From: John Date: Thu, 2 Jul 2026 07:25:16 +0800 Subject: [PATCH] feat: gate code agent runs server side --- .env.example | 2 ++ .../scripts/check-agent-code-run-entry.mjs | 3 ++ .runtime/portal/server.mjs | 16 ++++++++- agent-run-routes.mjs | 14 +++++++- agent-run-routes.test.mjs | 34 +++++++++++++++++++ scripts/check-agent-code-run-entry.mjs | 3 ++ tkmind-proxy.mjs | 3 ++ 7 files changed, 73 insertions(+), 2 deletions(-) diff --git a/.env.example b/.env.example index ed6d838..5b57002 100644 --- a/.env.example +++ b/.env.example @@ -20,7 +20,9 @@ H5_PUBLIC_BASE_URL=http://127.0.0.1:5173 # MEMIND_RUNTIME_REDIS_NAMESPACE=memind:runtime # Code-tool agent run 灰度开关(默认关闭)。 +# 后端必须先开启 MEMIND_AGENT_CODE_RUNS_ENABLED,H5 构建开关才会生效。 # 开启后,页面编辑子聊天会传 tool_mode=code;普通聊天仍需同时开启 AUTODETECT 才会按文本自动识别代码任务。 +# MEMIND_AGENT_CODE_RUNS_ENABLED=0 # VITE_AGENT_CODE_RUNS_ENABLED=0 # VITE_AGENT_CODE_RUNS_AUTODETECT=0 diff --git a/.runtime/portal/scripts/check-agent-code-run-entry.mjs b/.runtime/portal/scripts/check-agent-code-run-entry.mjs index 348b6ca..a4ec79c 100644 --- a/.runtime/portal/scripts/check-agent-code-run-entry.mjs +++ b/.runtime/portal/scripts/check-agent-code-run-entry.mjs @@ -140,6 +140,8 @@ const toolGrants = await queryToolGrants().catch((err) => ({ })); const buildFlags = { + serverCodeRunsEnabledEnv: process.env.MEMIND_AGENT_CODE_RUNS_ENABLED ?? null, + serverCodeRunsEnabledTruthy: truthy(process.env.MEMIND_AGENT_CODE_RUNS_ENABLED), enabledEnv: process.env.VITE_AGENT_CODE_RUNS_ENABLED ?? null, autodetectEnv: process.env.VITE_AGENT_CODE_RUNS_AUTODETECT ?? null, enabledTruthy: truthy(process.env.VITE_AGENT_CODE_RUNS_ENABLED), @@ -157,6 +159,7 @@ const ok = Boolean( runtimePolicy.defaultMode === 'chat' && runtimePolicy.codeToolMode === 'code' && runtimePolicy.chatInjectsCodeTools === false && + runtimePolicy.codeRunsEnabled === buildFlags.serverCodeRunsEnabledTruthy && roleDefaults.aider === false && roleDefaults.openhands === false && (toolGrants.sampledUsers ?? []).every((user) => user.chatHasCodeTools === false) && diff --git a/.runtime/portal/server.mjs b/.runtime/portal/server.mjs index 847ecd9..703bf64 100644 --- a/.runtime/portal/server.mjs +++ b/.runtime/portal/server.mjs @@ -6744,7 +6744,14 @@ function createAgentRunGateway({ } // agent-run-routes.mjs -function createPostAgentRunsHandler({ userAuth: userAuth2, agentRunGateway: agentRunGateway2 }) { +function envFlag(value) { + return ["1", "true", "yes", "on"].includes(String(value ?? "").trim().toLowerCase()); +} +function createPostAgentRunsHandler({ + userAuth: userAuth2, + agentRunGateway: agentRunGateway2, + codeRunsEnabled = envFlag(process.env.MEMIND_AGENT_CODE_RUNS_ENABLED) +}) { return async function postAgentRuns(request, response) { try { const sessionId = String(request.body?.session_id ?? "").trim() || null; @@ -6769,6 +6776,10 @@ function createPostAgentRunsHandler({ userAuth: userAuth2, agentRunGateway: agen }); return; } + if (toolMode === "code" && !codeRunsEnabled) { + response.status(403).json({ message: "\u4EE3\u7801\u4EFB\u52A1\u7070\u5EA6\u672A\u5F00\u542F" }); + return; + } if (sessionId) { const owns = await userAuth2.ownsSession(request.currentUser.id, sessionId); if (!owns) { @@ -9338,6 +9349,9 @@ function createTkmindProxy({ defaultMode: "chat", codeToolMode: "code", chatInjectsCodeTools: false, + codeRunsEnabled: ["1", "true", "yes", "on"].includes( + String(process.env.MEMIND_AGENT_CODE_RUNS_ENABLED ?? "").trim().toLowerCase() + ), aiderTimeoutMs: Number(process.env.MEMIND_AIDER_TIMEOUT_MS ?? 6e5), openhandsTimeoutMs: Number(process.env.MEMIND_OPENHANDS_TIMEOUT_MS ?? 9e5) }, diff --git a/agent-run-routes.mjs b/agent-run-routes.mjs index 9c2b258..5649a46 100644 --- a/agent-run-routes.mjs +++ b/agent-run-routes.mjs @@ -1,6 +1,14 @@ import { normalizeAgentRunToolMode } from './agent-run-gateway.mjs'; -export function createPostAgentRunsHandler({ userAuth, agentRunGateway }) { +function envFlag(value) { + return ['1', 'true', 'yes', 'on'].includes(String(value ?? '').trim().toLowerCase()); +} + +export function createPostAgentRunsHandler({ + userAuth, + agentRunGateway, + codeRunsEnabled = envFlag(process.env.MEMIND_AGENT_CODE_RUNS_ENABLED), +}) { return async function postAgentRuns(request, response) { try { const sessionId = String(request.body?.session_id ?? '').trim() || null; @@ -25,6 +33,10 @@ export function createPostAgentRunsHandler({ userAuth, agentRunGateway }) { }); return; } + if (toolMode === 'code' && !codeRunsEnabled) { + response.status(403).json({ message: '代码任务灰度未开启' }); + return; + } if (sessionId) { const owns = await userAuth.ownsSession(request.currentUser.id, sessionId); if (!owns) { diff --git a/agent-run-routes.test.mjs b/agent-run-routes.test.mjs index 4af1bb8..d8c40b2 100644 --- a/agent-run-routes.test.mjs +++ b/agent-run-routes.test.mjs @@ -114,6 +114,7 @@ test('POST /agent/runs accepts explicit code tool mode and task type', async () return { id: 'run-code', status: 'queued' }; }, }, + codeRunsEnabled: true, }); const res = createResponseRecorder(); @@ -141,6 +142,39 @@ test('POST /agent/runs accepts explicit code tool mode and task type', async () }); }); +test('POST /agent/runs rejects code tool mode when server gate is disabled', async () => { + const handler = createPostAgentRunsHandler({ + userAuth: { + async ownsSession() { + throw new Error('should not check ownership after disabled code mode'); + }, + }, + agentRunGateway: { + async createRun() { + throw new Error('should not be called'); + }, + }, + codeRunsEnabled: false, + }); + const res = createResponseRecorder(); + + await handler( + { + currentUser: { id: 'user-1' }, + body: { + session_id: 'session-1', + request_id: 'req-code', + user_message: { role: 'user', content: [] }, + tool_mode: 'code', + }, + }, + res, + ); + + assert.equal(res.statusCode, 403); + assert.deepEqual(res.body, { message: '代码任务灰度未开启' }); +}); + test('POST /agent/runs rejects unsupported tool mode', async () => { const handler = createPostAgentRunsHandler({ userAuth: { diff --git a/scripts/check-agent-code-run-entry.mjs b/scripts/check-agent-code-run-entry.mjs index 348b6ca..a4ec79c 100644 --- a/scripts/check-agent-code-run-entry.mjs +++ b/scripts/check-agent-code-run-entry.mjs @@ -140,6 +140,8 @@ const toolGrants = await queryToolGrants().catch((err) => ({ })); const buildFlags = { + serverCodeRunsEnabledEnv: process.env.MEMIND_AGENT_CODE_RUNS_ENABLED ?? null, + serverCodeRunsEnabledTruthy: truthy(process.env.MEMIND_AGENT_CODE_RUNS_ENABLED), enabledEnv: process.env.VITE_AGENT_CODE_RUNS_ENABLED ?? null, autodetectEnv: process.env.VITE_AGENT_CODE_RUNS_AUTODETECT ?? null, enabledTruthy: truthy(process.env.VITE_AGENT_CODE_RUNS_ENABLED), @@ -157,6 +159,7 @@ const ok = Boolean( runtimePolicy.defaultMode === 'chat' && runtimePolicy.codeToolMode === 'code' && runtimePolicy.chatInjectsCodeTools === false && + runtimePolicy.codeRunsEnabled === buildFlags.serverCodeRunsEnabledTruthy && roleDefaults.aider === false && roleDefaults.openhands === false && (toolGrants.sampledUsers ?? []).every((user) => user.chatHasCodeTools === false) && diff --git a/tkmind-proxy.mjs b/tkmind-proxy.mjs index fe628fb..25a0b15 100644 --- a/tkmind-proxy.mjs +++ b/tkmind-proxy.mjs @@ -912,6 +912,9 @@ export function createTkmindProxy({ defaultMode: 'chat', codeToolMode: 'code', chatInjectsCodeTools: false, + codeRunsEnabled: ['1', 'true', 'yes', 'on'].includes( + String(process.env.MEMIND_AGENT_CODE_RUNS_ENABLED ?? '').trim().toLowerCase(), + ), aiderTimeoutMs: Number(process.env.MEMIND_AIDER_TIMEOUT_MS ?? 600_000), openhandsTimeoutMs: Number(process.env.MEMIND_OPENHANDS_TIMEOUT_MS ?? 900_000), },