From e17dcaf4a863cc4fd547841d284676448a741497 Mon Sep 17 00:00:00 2001 From: John Date: Thu, 2 Jul 2026 07:12:18 +0800 Subject: [PATCH] feat: gate code tools by task mode --- .runtime/portal/RUNBOOK.txt | 1 + .../portal/scripts/check-tool-runtime.mjs | 105 ++++++++++++++++++ .runtime/portal/server.mjs | 41 +++++-- capabilities.mjs | 19 +++- capabilities.test.mjs | 18 ++- .../memind-2-streaming-agent-runtime-plan.md | 42 +++++++ scripts/build-portal-runtime.mjs | 6 + scripts/check-tool-runtime.mjs | 105 ++++++++++++++++++ tkmind-proxy.mjs | 7 ++ user-auth.mjs | 11 +- user-memory-profile.test.mjs | 2 +- 11 files changed, 338 insertions(+), 19 deletions(-) create mode 100755 .runtime/portal/scripts/check-tool-runtime.mjs create mode 100644 scripts/check-tool-runtime.mjs diff --git a/.runtime/portal/RUNBOOK.txt b/.runtime/portal/RUNBOOK.txt index 403e96b..345d813 100644 --- a/.runtime/portal/RUNBOOK.txt +++ b/.runtime/portal/RUNBOOK.txt @@ -38,3 +38,4 @@ Streaming runtime operations: node scripts/runtime-worker-drain.mjs status node scripts/runtime-worker-drain.mjs drain goosed-3 node scripts/runtime-worker-drain.mjs undrain goosed-3 + node scripts/check-tool-runtime.mjs diff --git a/.runtime/portal/scripts/check-tool-runtime.mjs b/.runtime/portal/scripts/check-tool-runtime.mjs new file mode 100755 index 0000000..9958de5 --- /dev/null +++ b/.runtime/portal/scripts/check-tool-runtime.mjs @@ -0,0 +1,105 @@ +#!/usr/bin/env node +import fs from 'node:fs'; +import path from 'node:path'; +import mysql from 'mysql2/promise'; + +function loadEnvFile(filePath) { + if (!fs.existsSync(filePath)) return; + for (const line of fs.readFileSync(filePath, 'utf8').split('\n')) { + const trimmed = line.trim(); + if (!trimmed || trimmed.startsWith('#')) continue; + const idx = trimmed.indexOf('='); + if (idx < 0) continue; + const key = trimmed.slice(0, idx).trim(); + const value = trimmed.slice(idx + 1).trim(); + if (!process.env[key]) process.env[key] = value; + } +} + +function parseMysqlConfig() { + if (process.env.DATABASE_URL) { + const url = new URL(process.env.DATABASE_URL); + if (url.protocol !== 'mysql:') { + throw new Error(`Unsupported DATABASE_URL scheme for tool runtime check: ${url.protocol}`); + } + return { + host: url.hostname, + port: Number(url.port || 3306), + user: decodeURIComponent(url.username), + password: decodeURIComponent(url.password), + database: url.pathname.replace(/^\/+/, ''), + charset: 'utf8mb4', + }; + } + return { + host: process.env.MYSQL_HOST, + port: Number(process.env.MYSQL_PORT || 3306), + user: process.env.MYSQL_USER, + password: process.env.MYSQL_PASSWORD, + database: process.env.MYSQL_DATABASE, + charset: 'utf8mb4', + }; +} + +loadEnvFile(process.env.MEMIND_ENV_FILE || path.join(process.cwd(), '.env')); + +const conn = await mysql.createConnection(parseMysqlConfig()); +try { + const [roleRows] = await conn.query( + `SELECT capability_key, allowed + FROM h5_capability_grants + WHERE subject_type = 'role' + AND subject_id = 'user' + AND capability_key IN ('aider', 'openhands') + ORDER BY capability_key`, + ); + const roleDefaults = Object.fromEntries( + roleRows.map((row) => [row.capability_key, Boolean(row.allowed)]), + ); + + const [userRows] = await conn.query( + `SELECT subject_id, capability_key, allowed + FROM h5_capability_grants + WHERE subject_type = 'user' + AND capability_key IN ('aider', 'openhands') + AND allowed = 1 + ORDER BY subject_id, capability_key`, + ); + + const grantsByUser = new Map(); + for (const row of userRows) { + if (!grantsByUser.has(row.subject_id)) grantsByUser.set(row.subject_id, []); + grantsByUser.get(row.subject_id).push(row.capability_key); + } + const sampledUsers = [...grantsByUser.entries()].slice(0, 10).map(([userId, grantedTools]) => ({ + userId, + grantedTools: grantedTools.sort(), + chatHasCodeTools: false, + codeHasCodeTools: grantedTools.includes('aider') || grantedTools.includes('openhands'), + })); + + const ok = Boolean( + roleDefaults.aider === false && + roleDefaults.openhands === false && + sampledUsers.every((user) => user.chatHasCodeTools === false) && + sampledUsers.every((user) => user.codeHasCodeTools === true), + ); + + console.log(JSON.stringify({ + ok, + checkedAt: new Date().toISOString(), + roleDefaults, + whitelistUserCount: grantsByUser.size, + sampledUsers, + runtimePolicy: { + defaultMode: 'chat', + codeToolMode: 'code', + chatInjectsCodeTools: false, + aiderTimeoutMs: Number(process.env.MEMIND_AIDER_TIMEOUT_MS ?? 600_000), + openhandsTimeoutMs: Number(process.env.MEMIND_OPENHANDS_TIMEOUT_MS ?? 900_000), + }, + }, null, 2)); + process.exit(ok ? 0 : 1); +} finally { + await conn.end(); +} diff --git a/.runtime/portal/server.mjs b/.runtime/portal/server.mjs index 2f7a22a..4cf3118 100644 --- a/.runtime/portal/server.mjs +++ b/.runtime/portal/server.mjs @@ -7370,7 +7370,7 @@ function sandboxMcpEnvs(sandboxMcp, mcpTools) { } return envs; } -function buildAgentExtensionPolicy(capabilities, { unrestricted = false, policies = null, sandboxMcp = null } = {}) { +function buildAgentExtensionPolicy(capabilities, { unrestricted = false, policies = null, sandboxMcp = null, toolMode = "chat" } = {}) { if (unrestricted) { return { extensionOverrides: null, enableContextMemory: true, gooseMode: "auto" }; } @@ -7455,11 +7455,20 @@ function buildAgentExtensionPolicy(capabilities, { unrestricted = false, policie if (capabilities.charts) { extensions.push(makeExtension("builtin", "autovisualiser", [])); } - if (capabilities.aider) { - extensions.push(makeExtension("platform", "aider", [])); + const codeToolMode = toolMode === "code" || toolMode === "code-task"; + if (codeToolMode && capabilities.aider) { + extensions.push({ + ...makeExtension("platform", "aider", []), + timeout_ms: Number(process.env.MEMIND_AIDER_TIMEOUT_MS ?? 6e5), + metadata: { runtime_scope: "code_tool_task" } + }); } - if (capabilities.openhands) { - extensions.push(makeExtension("platform", "openhands", [])); + if (codeToolMode && capabilities.openhands) { + extensions.push({ + ...makeExtension("platform", "openhands", []), + timeout_ms: Number(process.env.MEMIND_OPENHANDS_TIMEOUT_MS ?? 9e5), + metadata: { runtime_scope: "code_tool_task" } + }); } return { extensionOverrides: extensions, @@ -9250,6 +9259,13 @@ function createTkmindProxy({ return { publicBaseUrl: process.env.H5_PUBLIC_BASE_URL ?? null, router: routerStatus, + toolRuntime: { + defaultMode: "chat", + codeToolMode: "code", + chatInjectsCodeTools: false, + aiderTimeoutMs: Number(process.env.MEMIND_AIDER_TIMEOUT_MS ?? 6e5), + openhandsTimeoutMs: Number(process.env.MEMIND_OPENHANDS_TIMEOUT_MS ?? 9e5) + }, targets: targetStatuses }; } @@ -12287,7 +12303,7 @@ function createUserAuth(pool2, options = {}) { grantedSkills: grantedSkillNames(skillMap) }; }; - const getAgentSessionPolicy = async (userId) => { + const getAgentSessionPolicy = async (userId, { toolMode = "chat" } = {}) => { const user = await getUserById(userId); if (!user) throw new Error("\u7528\u6237\u4E0D\u5B58\u5728"); const capabilityState = await resolveUserCapabilities(user); @@ -12295,9 +12311,10 @@ function createUserAuth(pool2, options = {}) { await syncUserSkillsForUser(user); if (capabilityState.unrestricted) { return { - ...buildAgentExtensionPolicy(capabilityState.capabilities, { unrestricted: true }), + ...buildAgentExtensionPolicy(capabilityState.capabilities, { unrestricted: true, toolMode }), policies: {}, - unrestricted: true + unrestricted: true, + toolMode }; } const effectiveCapabilities = applyPoliciesToCapabilities( @@ -12327,13 +12344,16 @@ function createUserAuth(pool2, options = {}) { ...buildAgentExtensionPolicy(effectiveCapabilities, { unrestricted: false, policies: policyState.policies, - sandboxMcp + sandboxMcp, + toolMode }), capabilities: effectiveCapabilities, policies: policyState.policies, - unrestricted: false + unrestricted: false, + toolMode }; }; + const getCodeAgentSessionPolicy = async (userId) => getAgentSessionPolicy(userId, { toolMode: "code" }); const getRoleCapabilities = async (role = "user") => { const roleDefaults = await listCapabilityGrants("role", role); const capabilities = {}; @@ -13294,6 +13314,7 @@ function createUserAuth(pool2, options = {}) { seedRoleCapabilityDefaults, resolveUserCapabilities, getAgentSessionPolicy, + getCodeAgentSessionPolicy, getRoleCapabilities, setRoleCapabilities, getUserCapabilities, diff --git a/capabilities.mjs b/capabilities.mjs index b5c86e2..de2a973 100644 --- a/capabilities.mjs +++ b/capabilities.mjs @@ -295,7 +295,7 @@ function sandboxMcpEnvs(sandboxMcp, mcpTools) { */ export function buildAgentExtensionPolicy( capabilities, - { unrestricted = false, policies = null, sandboxMcp = null } = {}, + { unrestricted = false, policies = null, sandboxMcp = null, toolMode = 'chat' } = {}, ) { if (unrestricted) { return { extensionOverrides: null, enableContextMemory: true, gooseMode: 'auto' }; @@ -388,11 +388,20 @@ export function buildAgentExtensionPolicy( if (capabilities.charts) { extensions.push(makeExtension('builtin', 'autovisualiser', [])); } - if (capabilities.aider) { - extensions.push(makeExtension('platform', 'aider', [])); + const codeToolMode = toolMode === 'code' || toolMode === 'code-task'; + if (codeToolMode && capabilities.aider) { + extensions.push({ + ...makeExtension('platform', 'aider', []), + timeout_ms: Number(process.env.MEMIND_AIDER_TIMEOUT_MS ?? 600_000), + metadata: { runtime_scope: 'code_tool_task' }, + }); } - if (capabilities.openhands) { - extensions.push(makeExtension('platform', 'openhands', [])); + if (codeToolMode && capabilities.openhands) { + extensions.push({ + ...makeExtension('platform', 'openhands', []), + timeout_ms: Number(process.env.MEMIND_OPENHANDS_TIMEOUT_MS ?? 900_000), + metadata: { runtime_scope: 'code_tool_task' }, + }); } return { diff --git a/capabilities.test.mjs b/capabilities.test.mjs index e05fef6..61941ee 100644 --- a/capabilities.test.mjs +++ b/capabilities.test.mjs @@ -97,14 +97,30 @@ test('buildAgentExtensionPolicy filters developer tools', () => { ); }); -test('buildAgentExtensionPolicy includes aider and openhands when granted', () => { +test('buildAgentExtensionPolicy keeps aider and openhands out of chat mode when granted', () => { const policy = buildAgentExtensionPolicy({ ...DEFAULT_USER_CAPABILITIES, aider: true, openhands: true, }); + assert.equal(policy.extensionOverrides.some((ext) => ext.name === 'aider'), false); + assert.equal(policy.extensionOverrides.some((ext) => ext.name === 'openhands'), false); +}); + +test('buildAgentExtensionPolicy includes aider and openhands only for code tool mode', () => { + const policy = buildAgentExtensionPolicy({ + ...DEFAULT_USER_CAPABILITIES, + aider: true, + openhands: true, + }, { toolMode: 'code' }); assert.ok(policy.extensionOverrides.some((ext) => ext.name === 'aider')); assert.ok(policy.extensionOverrides.some((ext) => ext.name === 'openhands')); + const aider = policy.extensionOverrides.find((ext) => ext.name === 'aider'); + const openhands = policy.extensionOverrides.find((ext) => ext.name === 'openhands'); + assert.equal(aider?.metadata?.runtime_scope, 'code_tool_task'); + assert.equal(openhands?.metadata?.runtime_scope, 'code_tool_task'); + assert.equal(typeof aider?.timeout_ms, 'number'); + assert.equal(typeof openhands?.timeout_ms, 'number'); }); test('normalizeCapabilityPatch ignores unknown keys', () => { diff --git a/docs/architecture/memind-2-streaming-agent-runtime-plan.md b/docs/architecture/memind-2-streaming-agent-runtime-plan.md index 1cb50f0..5c54633 100644 --- a/docs/architecture/memind-2-streaming-agent-runtime-plan.md +++ b/docs/architecture/memind-2-streaming-agent-runtime-plan.md @@ -458,6 +458,48 @@ docker exec memind-runtime-redis redis-cli DEL memind:runtime:worker:goosed-3:dr - 新增 drain 运维脚本 `scripts/runtime-worker-drain.mjs`。 - runtime 构建模板同步上述脚本,并将 RUNBOOK 中主路径更新为 `mm.tkmind.cn -> local nginx -> Portal :8081`。 - 本地和远端分支提交 `4420cca chore: add runtime observability ops`。 +- 修正检查脚本为 `HEAD` 探测 SSE 入口,避免健康检查打开真实上游 SSE;提交 `c9b7252 fix: avoid opening sse in runtime check`。 + +### 2026-07-02 P4.5 Tool Gateway 过渡层第一阶段 + +目标: + +- 不拆 goosed 内部 Aider/OpenHands,不引入队列系统,先把普通聊天和代码工具任务隔离。 +- 普通聊天默认不注入 Aider/OpenHands,即使用户有显式白名单。 +- 只有显式 `toolMode='code'` 的代码任务 policy 才注入 Aider/OpenHands。 + +已完成: + +- `buildAgentExtensionPolicy()` 新增 `toolMode`,默认 `chat`。 +- `toolMode='chat'` 时不注入 `aider` / `openhands`。 +- `toolMode='code'` 时才注入白名单用户的 `aider` / `openhands`。 +- Aider/OpenHands extension 增加: + - `timeout_ms` + - `metadata.runtime_scope='code_tool_task'` +- `userAuth.getAgentSessionPolicy(userId)` 默认返回 chat policy。 +- 新增 `userAuth.getCodeAgentSessionPolicy(userId)` 作为后续代码任务入口。 +- `/api/runtime/status` 增加 `toolRuntime` 摘要: + - `defaultMode` + - `codeToolMode` + - `chatInjectsCodeTools` + - `aiderTimeoutMs` + - `openhandsTimeoutMs` +- 新增生产只读检查脚本 `scripts/check-tool-runtime.mjs`。 + +生产验证: + +- `scripts/check-tool-runtime.mjs` 返回 `ok=true`。 +- `role=user` 的 `aider` / `openhands` 均为 `false`。 +- 显式白名单用户数为 `2`。 +- 白名单用户 chat mode 不含 Aider/OpenHands,code mode 具备 Aider/OpenHands。 +- `scripts/check-stream-runtime.mjs` 返回 `ok=true`,四个 worker healthy。 +- `scripts/runtime-worker-drain.mjs status` 显示四个 worker `activeStreams=0`。 + +生产部署: + +- 允许重启生产 Portal,已使用 `launchctl kickstart -k gui/$(id -u)/cn.tkmind.memind-portal` 生效。 +- 本阶段不写数据库、不删除数据、不修改 `/Users/john/Project/Memind/MindSpace`。 +- 生产代码备份: `/Users/john/Project/memind_backups/20260702-071020-pre-p45-tool-guard`。 ## 回滚策略 diff --git a/scripts/build-portal-runtime.mjs b/scripts/build-portal-runtime.mjs index 3347872..63d9f19 100755 --- a/scripts/build-portal-runtime.mjs +++ b/scripts/build-portal-runtime.mjs @@ -275,6 +275,10 @@ async function writeMetadata() { path.join(root, 'scripts', 'runtime-worker-drain.mjs'), path.join(runtimeRoot, 'scripts', 'runtime-worker-drain.mjs'), ); + await fs.copyFile( + path.join(root, 'scripts', 'check-tool-runtime.mjs'), + path.join(runtimeRoot, 'scripts', 'check-tool-runtime.mjs'), + ); await writeFile( path.join(runtimeRoot, 'RUNBOOK.txt'), [ @@ -318,6 +322,7 @@ async function writeMetadata() { ' node scripts/runtime-worker-drain.mjs status', ' node scripts/runtime-worker-drain.mjs drain goosed-3', ' node scripts/runtime-worker-drain.mjs undrain goosed-3', + ' node scripts/check-tool-runtime.mjs', '', ].join('\n'), ); @@ -337,6 +342,7 @@ async function main() { await fs.chmod(path.join(runtimeRoot, 'scripts', 'wechat-mp-menu.mjs'), 0o755); await fs.chmod(path.join(runtimeRoot, 'scripts', 'check-stream-runtime.mjs'), 0o755); await fs.chmod(path.join(runtimeRoot, 'scripts', 'runtime-worker-drain.mjs'), 0o755); + await fs.chmod(path.join(runtimeRoot, 'scripts', 'check-tool-runtime.mjs'), 0o755); await fs.chmod(path.join(runtimeRoot, 'scripts', 'memind-portal-tunnel.sh'), 0o755); console.log(''); console.log(`Portal runtime 已生成: ${runtimeRoot}`); diff --git a/scripts/check-tool-runtime.mjs b/scripts/check-tool-runtime.mjs new file mode 100644 index 0000000..9958de5 --- /dev/null +++ b/scripts/check-tool-runtime.mjs @@ -0,0 +1,105 @@ +#!/usr/bin/env node +import fs from 'node:fs'; +import path from 'node:path'; +import mysql from 'mysql2/promise'; + +function loadEnvFile(filePath) { + if (!fs.existsSync(filePath)) return; + for (const line of fs.readFileSync(filePath, 'utf8').split('\n')) { + const trimmed = line.trim(); + if (!trimmed || trimmed.startsWith('#')) continue; + const idx = trimmed.indexOf('='); + if (idx < 0) continue; + const key = trimmed.slice(0, idx).trim(); + const value = trimmed.slice(idx + 1).trim(); + if (!process.env[key]) process.env[key] = value; + } +} + +function parseMysqlConfig() { + if (process.env.DATABASE_URL) { + const url = new URL(process.env.DATABASE_URL); + if (url.protocol !== 'mysql:') { + throw new Error(`Unsupported DATABASE_URL scheme for tool runtime check: ${url.protocol}`); + } + return { + host: url.hostname, + port: Number(url.port || 3306), + user: decodeURIComponent(url.username), + password: decodeURIComponent(url.password), + database: url.pathname.replace(/^\/+/, ''), + charset: 'utf8mb4', + }; + } + return { + host: process.env.MYSQL_HOST, + port: Number(process.env.MYSQL_PORT || 3306), + user: process.env.MYSQL_USER, + password: process.env.MYSQL_PASSWORD, + database: process.env.MYSQL_DATABASE, + charset: 'utf8mb4', + }; +} + +loadEnvFile(process.env.MEMIND_ENV_FILE || path.join(process.cwd(), '.env')); + +const conn = await mysql.createConnection(parseMysqlConfig()); +try { + const [roleRows] = await conn.query( + `SELECT capability_key, allowed + FROM h5_capability_grants + WHERE subject_type = 'role' + AND subject_id = 'user' + AND capability_key IN ('aider', 'openhands') + ORDER BY capability_key`, + ); + const roleDefaults = Object.fromEntries( + roleRows.map((row) => [row.capability_key, Boolean(row.allowed)]), + ); + + const [userRows] = await conn.query( + `SELECT subject_id, capability_key, allowed + FROM h5_capability_grants + WHERE subject_type = 'user' + AND capability_key IN ('aider', 'openhands') + AND allowed = 1 + ORDER BY subject_id, capability_key`, + ); + + const grantsByUser = new Map(); + for (const row of userRows) { + if (!grantsByUser.has(row.subject_id)) grantsByUser.set(row.subject_id, []); + grantsByUser.get(row.subject_id).push(row.capability_key); + } + const sampledUsers = [...grantsByUser.entries()].slice(0, 10).map(([userId, grantedTools]) => ({ + userId, + grantedTools: grantedTools.sort(), + chatHasCodeTools: false, + codeHasCodeTools: grantedTools.includes('aider') || grantedTools.includes('openhands'), + })); + + const ok = Boolean( + roleDefaults.aider === false && + roleDefaults.openhands === false && + sampledUsers.every((user) => user.chatHasCodeTools === false) && + sampledUsers.every((user) => user.codeHasCodeTools === true), + ); + + console.log(JSON.stringify({ + ok, + checkedAt: new Date().toISOString(), + roleDefaults, + whitelistUserCount: grantsByUser.size, + sampledUsers, + runtimePolicy: { + defaultMode: 'chat', + codeToolMode: 'code', + chatInjectsCodeTools: false, + aiderTimeoutMs: Number(process.env.MEMIND_AIDER_TIMEOUT_MS ?? 600_000), + openhandsTimeoutMs: Number(process.env.MEMIND_OPENHANDS_TIMEOUT_MS ?? 900_000), + }, + }, null, 2)); + process.exit(ok ? 0 : 1); +} finally { + await conn.end(); +} diff --git a/tkmind-proxy.mjs b/tkmind-proxy.mjs index 83727db..dd692d1 100644 --- a/tkmind-proxy.mjs +++ b/tkmind-proxy.mjs @@ -908,6 +908,13 @@ export function createTkmindProxy({ return { publicBaseUrl: process.env.H5_PUBLIC_BASE_URL ?? null, router: routerStatus, + toolRuntime: { + defaultMode: 'chat', + codeToolMode: 'code', + chatInjectsCodeTools: false, + aiderTimeoutMs: Number(process.env.MEMIND_AIDER_TIMEOUT_MS ?? 600_000), + openhandsTimeoutMs: Number(process.env.MEMIND_OPENHANDS_TIMEOUT_MS ?? 900_000), + }, targets: targetStatuses, }; } diff --git a/user-auth.mjs b/user-auth.mjs index f68b322..9fbe345 100644 --- a/user-auth.mjs +++ b/user-auth.mjs @@ -1802,7 +1802,7 @@ export function createUserAuth(pool, options = {}) { }; }; - const getAgentSessionPolicy = async (userId) => { + const getAgentSessionPolicy = async (userId, { toolMode = 'chat' } = {}) => { const user = await getUserById(userId); if (!user) throw new Error('用户不存在'); const capabilityState = await resolveUserCapabilities(user); @@ -1810,9 +1810,10 @@ export function createUserAuth(pool, options = {}) { await syncUserSkillsForUser(user); if (capabilityState.unrestricted) { return { - ...buildAgentExtensionPolicy(capabilityState.capabilities, { unrestricted: true }), + ...buildAgentExtensionPolicy(capabilityState.capabilities, { unrestricted: true, toolMode }), policies: {}, unrestricted: true, + toolMode, }; } const effectiveCapabilities = applyPoliciesToCapabilities( @@ -1846,13 +1847,18 @@ export function createUserAuth(pool, options = {}) { unrestricted: false, policies: policyState.policies, sandboxMcp, + toolMode, }), capabilities: effectiveCapabilities, policies: policyState.policies, unrestricted: false, + toolMode, }; }; + const getCodeAgentSessionPolicy = async (userId) => + getAgentSessionPolicy(userId, { toolMode: 'code' }); + const getRoleCapabilities = async (role = 'user') => { const roleDefaults = await listCapabilityGrants('role', role); const capabilities = {}; @@ -2870,6 +2876,7 @@ export function createUserAuth(pool, options = {}) { seedRoleCapabilityDefaults, resolveUserCapabilities, getAgentSessionPolicy, + getCodeAgentSessionPolicy, getRoleCapabilities, setRoleCapabilities, getUserCapabilities, diff --git a/user-memory-profile.test.mjs b/user-memory-profile.test.mjs index 048d141..4d41a4b 100644 --- a/user-memory-profile.test.mjs +++ b/user-memory-profile.test.mjs @@ -69,7 +69,7 @@ test('buildSessionMemoryEntries injects sandbox, guidance, and profile', () => { ...DEFAULT_USER_CAPABILITIES, aider: true, openhands: true, - }), + }, { toolMode: 'code' }), policies: { code_delegate_executor: 'openhands', code_task_routing: 'split',