/** @typedef {'auto' | 'approve' | 'smart_approve' | 'chat'} GooseMode */ /** @typedef {'readwrite' | 'readonly'} WorkspaceAccess */ /** @typedef {'allow' | 'deny'} NetworkEgress */ export const POLICY_CATALOG = [ { key: 'goose_mode', label: 'TKMind 模式', description: 'H5 终端用户不在聊天里点「允许/拒绝」;可用工具由「能力权限」决定。chat 不调用工具,auto 在授权范围内自动执行。', type: 'select', options: [ { value: 'chat', label: '仅聊天 (chat)' }, { value: 'auto', label: '自动执行 (auto)' }, ], defaultValue: 'chat', category: 'goose', risk: 'high', }, { key: 'workspace_access', label: '工作区访问', description: 'readonly 时禁止 shell 与文件写入,仅允许浏览与分析', type: 'select', options: [ { value: 'readwrite', label: '读写' }, { value: 'readonly', label: '只读' }, ], defaultValue: 'readwrite', category: 'workspace', risk: 'medium', }, { key: 'network_egress', label: '网络出站', description: 'deny 时禁用 shell、电脑控制、沙箱脚本等可能访问网络的扩展', type: 'select', options: [ { value: 'deny', label: '禁止(推荐)' }, { value: 'allow', label: '允许(需同时开启对应能力)' }, ], defaultValue: 'deny', category: 'network', risk: 'high', }, { key: 'api_lockdown', label: 'API 代理锁定', description: '开启后仅允许聊天相关 API,拦截改模式、加扩展、写配置等危险请求', type: 'boolean', defaultValue: true, category: 'proxy', risk: 'medium', }, { key: 'code_delegate_executor', label: '代码委托执行器', description: '控制 Goose 在多文件编码、修复与重构任务中优先委托给谁。auto 由 Goose 结合当前可用扩展自行判断;aider / openhands 则优先使用指定执行器。', type: 'select', options: [ { value: 'auto', label: '自动选择 (auto)' }, { value: 'aider', label: '优先 Aider' }, { value: 'openhands', label: '优先 OpenHands' }, ], defaultValue: 'auto', category: 'routing', risk: 'medium', }, { key: 'code_task_routing', label: '代码任务路由策略', description: '定义代码任务如何在 Aider 与 OpenHands 之间分流。balanced 由 Goose 按任务复杂度判断;split 建议小改动走 Aider、复杂任务走 OpenHands;force_* 则强制优先单一路径。', type: 'select', options: [ { value: 'balanced', label: '平衡路由 (balanced)' }, { value: 'split', label: '小改动 Aider,复杂任务 OpenHands' }, { value: 'force_aider', label: '尽量统一走 Aider' }, { value: 'force_openhands', label: '尽量统一走 OpenHands' }, ], defaultValue: 'balanced', category: 'routing', risk: 'medium', }, ]; export const DEFAULT_USER_POLICIES = Object.fromEntries( POLICY_CATALOG.map((item) => [item.key, item.defaultValue]), ); const POLICY_KEYS = new Set(POLICY_CATALOG.map((item) => item.key)); const GOOSE_MODES = new Set(['auto', 'approve', 'smart_approve', 'chat']); const WORKSPACE_ACCESS = new Set(['readwrite', 'readonly']); const NETWORK_EGRESS = new Set(['allow', 'deny']); const CODE_DELEGATE_EXECUTORS = new Set(['auto', 'aider', 'openhands']); const CODE_TASK_ROUTINGS = new Set(['balanced', 'split', 'force_aider', 'force_openhands']); export function policyKeys() { return [...POLICY_KEYS]; } export function isValidPolicyKey(key) { return POLICY_KEYS.has(key); } export function normalizePolicyPatch(patch) { const normalized = {}; for (const [key, raw] of Object.entries(patch ?? {})) { if (!isValidPolicyKey(key)) continue; const def = POLICY_CATALOG.find((item) => item.key === key); if (!def) continue; if (def.type === 'boolean') { normalized[key] = raw === true || raw === 'true' || raw === 1 || raw === '1'; continue; } const value = String(raw ?? '').trim(); if (def.key === 'goose_mode' && GOOSE_MODES.has(value)) normalized[key] = value; if (def.key === 'workspace_access' && WORKSPACE_ACCESS.has(value)) normalized[key] = value; if (def.key === 'network_egress' && NETWORK_EGRESS.has(value)) normalized[key] = value; if (def.key === 'code_delegate_executor' && CODE_DELEGATE_EXECUTORS.has(value)) { normalized[key] = value; } if (def.key === 'code_task_routing' && CODE_TASK_ROUTINGS.has(value)) { normalized[key] = value; } } return normalized; } export function resolvePolicies(rolePolicies, userOverrides) { const resolved = { ...DEFAULT_USER_POLICIES }; for (const [key, value] of Object.entries(rolePolicies ?? {})) { if (isValidPolicyKey(key)) resolved[key] = value; } for (const [key, value] of Object.entries(userOverrides ?? {})) { if (isValidPolicyKey(key)) resolved[key] = value; } if (resolved.goose_mode === 'approve' || resolved.goose_mode === 'smart_approve') { resolved.goose_mode = 'auto'; } return resolved; } function hasExecutableTools(capabilities) { return Boolean( capabilities?.static_publish || capabilities?.shell || capabilities?.filesystem || capabilities?.code_browse || capabilities?.subagent || capabilities?.code_sandbox || capabilities?.computer || capabilities?.charts || capabilities?.aider || capabilities?.openhands || capabilities?.apps || capabilities?.todo || capabilities?.skills || capabilities?.chat_recall, ); } /** * Goose mode for H5 agent sessions. Tool allow/deny is enforced via capability grants, * not end-user confirmation dialogs. */ export function resolveAgentGooseMode(capabilities, policies) { const requested = policies?.goose_mode ?? DEFAULT_USER_POLICIES.goose_mode; if (!hasExecutableTools(capabilities)) { return 'chat'; } if (requested === 'chat' || requested === 'approve' || requested === 'smart_approve') { return 'auto'; } return 'auto'; } /** * Apply security policies on top of capability flags before building extension overrides. */ export function applyPoliciesToCapabilities(capabilities, policies) { const effective = { ...capabilities }; if (policies.workspace_access === 'readonly') { effective.shell = false; effective.filesystem = false; effective.static_publish = false; effective.aider = false; effective.openhands = false; effective.code_sandbox = false; effective.image_read = false; } if (policies.network_egress === 'deny') { // static_publish 用户的 shell 仅能在本工作区内执行本地命令,不依赖外网出站 if (!effective.static_publish) { effective.shell = false; } effective.computer = false; effective.web = false; effective.code_sandbox = false; } return effective; } const USER_API_ALLOWLIST = [ { method: 'GET', pattern: /^\/status$/ }, { method: 'POST', pattern: /^\/agent\/start$/ }, { method: 'POST', pattern: /^\/agent\/resume$/ }, { method: 'POST', pattern: /^\/agent\/runs$/ }, { method: 'GET', pattern: /^\/agent\/runs\/[^/]+$/ }, { method: 'GET', pattern: /^\/agent\/runs\/[^/]+\/events$/ }, { method: 'GET', pattern: /^\/sessions$/ }, { method: 'GET', pattern: /^\/sessions\/[^/]+$/ }, { method: 'DELETE', pattern: /^\/sessions\/[^/]+$/ }, { method: 'GET', pattern: /^\/sessions\/[^/]+\/events$/ }, { method: 'POST', pattern: /^\/sessions\/[^/]+\/reply$/ }, { method: 'POST', pattern: /^\/sessions\/[^/]+\/cancel$/ }, { method: 'POST', pattern: /^\/action-required\/tool-confirmation$/ }, { method: 'POST', pattern: /^\/agent\/update_provider$/ }, { method: 'POST', pattern: /^\/config\/read$/ }, { method: 'POST', pattern: /^\/agent\/harness_bootstrap$/ }, { method: 'POST', pattern: /^\/agent\/harness_remember$/ }, ]; export function isNativeH5ApiPath(pathname) { const path = String(pathname ?? ''); return path.startsWith('/mindspace/') || path.startsWith('/plaza/'); } /** * @returns {{ allowed: boolean, reason?: string }} */ export function evaluateProxyRequest(method, pathname, policies, { unrestricted = false } = {}) { if (unrestricted) return { allowed: true }; if (!policies.api_lockdown) return { allowed: true }; if (isNativeH5ApiPath(pathname)) { return { allowed: false, reason: `H5 本地接口不应走 Agent 代理:${method.toUpperCase()} ${pathname}`, }; } const upper = method.toUpperCase(); const allowed = USER_API_ALLOWLIST.some( (rule) => rule.method === upper && rule.pattern.test(pathname), ); if (allowed) return { allowed: true }; return { allowed: false, reason: `策略已锁定 API:${upper} ${pathname}`, }; }