import test from 'node:test'; import assert from 'node:assert/strict'; import fs from 'node:fs'; import { buildAgentExtensionPolicy, buildPageEditAgentPolicy, CAPABILITY_CATALOG, clampUserCapabilities, DEFAULT_USER_CAPABILITIES, normalizeCapabilityPatch, resolveSandboxMcpNodeExecPath, resolveSandboxMcpServerPath, resolveSandboxMcpUserDataPgUrl, sandboxDeveloperTools, sandboxMcpTools, } from './capabilities.mjs'; import { applyPoliciesToCapabilities } from './policies.mjs'; test('resolveSandboxMcpServerPath resolves to an existing MCP entry file', () => { const serverPath = resolveSandboxMcpServerPath(); assert.match(serverPath, /mindspace-sandbox-mcp\.mjs$/); assert.ok(fs.existsSync(serverPath)); }); test('resolveSandboxMcpServerPath honors container-path override without host fs checks', () => { assert.equal( resolveSandboxMcpServerPath('/opt/portal/mindspace-sandbox-mcp.mjs'), '/opt/portal/mindspace-sandbox-mcp.mjs', ); }); test('resolveSandboxMcpUserDataPgUrl rewrites a portal loopback URL for container MCP access', () => { const resolved = resolveSandboxMcpUserDataPgUrl({ portalUrl: 'postgresql://mindspace:secret@127.0.0.1:5433/mindspace_userdata_prod', containerized: true, }); const parsed = new URL(resolved); assert.equal(parsed.hostname, 'host.docker.internal'); assert.equal(parsed.port, '5433'); assert.equal(parsed.pathname, '/mindspace_userdata_prod'); }); test('resolveSandboxMcpUserDataPgUrl preserves native URLs and honors an explicit MCP URL', () => { const portalUrl = 'postgresql://mindspace:secret@127.0.0.1:5433/mindspace_userdata_prod'; assert.equal(resolveSandboxMcpUserDataPgUrl({ portalUrl }), portalUrl); assert.equal( resolveSandboxMcpUserDataPgUrl({ portalUrl, mcpUrl: 'postgresql://mindspace:secret@pg-proxy.internal:6432/mindspace_userdata_prod', containerized: true, }), 'postgresql://mindspace:secret@pg-proxy.internal:6432/mindspace_userdata_prod', ); }); test('resolveSandboxMcpNodeExecPath honors container-path override without host fs checks', () => { assert.equal(resolveSandboxMcpNodeExecPath('/usr/local/bin/node'), '/usr/local/bin/node'); assert.equal(resolveSandboxMcpNodeExecPath(''), process.execPath); }); test('web capability mounts platform web extension with search tools', () => { const caps = applyPoliciesToCapabilities( { ...DEFAULT_USER_CAPABILITIES, web: true }, { network_egress: 'allow' }, ); const policy = buildAgentExtensionPolicy(caps, { policies: { network_egress: 'allow', goose_mode: 'auto' }, }); const web = policy.extensionOverrides.find((ext) => ext.name === 'web'); assert.ok(web); assert.equal(web.type, 'platform'); assert.deepEqual(web.available_tools, ['web_search', 'fetch_url']); }); test('web capability is stripped when network egress is denied', () => { const caps = applyPoliciesToCapabilities( { ...DEFAULT_USER_CAPABILITIES, web: true }, { network_egress: 'deny' }, ); assert.equal(caps.web, false); const policy = buildAgentExtensionPolicy(caps, { policies: { network_egress: 'deny', goose_mode: 'auto' }, }); assert.equal(policy.extensionOverrides.some((ext) => ext.name === 'web'), false); }); test('default user policy blocks dangerous capabilities', () => { assert.equal(DEFAULT_USER_CAPABILITIES.shell, false); assert.equal(DEFAULT_USER_CAPABILITIES.filesystem, false); assert.equal(DEFAULT_USER_CAPABILITIES.extension_admin, false); assert.equal(DEFAULT_USER_CAPABILITIES.static_publish, false); assert.equal(DEFAULT_USER_CAPABILITIES.private_data_space, true); assert.equal(DEFAULT_USER_CAPABILITIES.code_browse, false); assert.equal(DEFAULT_USER_CAPABILITIES.memory_store, true); assert.equal(DEFAULT_USER_CAPABILITIES.skills, true); assert.equal(DEFAULT_USER_CAPABILITIES.chat_recall, true); assert.equal(DEFAULT_USER_CAPABILITIES.web, true); assert.equal(DEFAULT_USER_CAPABILITIES.aider, false); assert.equal(DEFAULT_USER_CAPABILITIES.openhands, false); }); test('buildAgentExtensionPolicy returns null overrides and auto mode for unrestricted users', () => { const policy = buildAgentExtensionPolicy({}, { unrestricted: true }); assert.equal(policy.extensionOverrides, null); assert.equal(policy.gooseMode, 'auto'); }); test('static_publish enables sandbox developer tools without shell by default', () => { const caps = { ...DEFAULT_USER_CAPABILITIES, static_publish: true }; assert.deepEqual(sandboxDeveloperTools(caps), ['write', 'edit', 'read_image']); const policy = buildAgentExtensionPolicy(caps); const developer = policy.extensionOverrides.find((ext) => ext.name === 'developer'); assert.deepEqual(developer?.available_tools, ['write', 'edit', 'read_image']); assert.equal(developer?.available_tools.includes('shell'), false); assert.equal(developer?.available_tools.includes('tree'), false); const summon = policy.extensionOverrides.find((ext) => ext.name === 'summon'); assert.deepEqual(summon?.available_tools, ['load_skill']); assert.ok(policy.extensionOverrides.some((ext) => ext.name === 'projectmemory')); assert.equal(policy.enableContextMemory, true); }); test('static_publish with shell survives network deny and includes tree', () => { const caps = applyPoliciesToCapabilities( { ...DEFAULT_USER_CAPABILITIES, static_publish: true, shell: true }, { network_egress: 'deny' }, ); assert.equal(caps.shell, true); const policy = buildAgentExtensionPolicy(caps, { policies: { network_egress: 'deny', goose_mode: 'chat' }, }); const developer = policy.extensionOverrides.find((ext) => ext.name === 'developer'); assert.deepEqual(developer?.available_tools, ['write', 'edit', 'shell', 'tree', 'read_image']); }); test('buildAgentExtensionPolicy filters developer tools', () => { const policy = buildAgentExtensionPolicy({ ...DEFAULT_USER_CAPABILITIES, shell: true, filesystem: false, code_browse: true, }); const developer = policy.extensionOverrides.find((ext) => ext.name === 'developer'); assert.ok(developer); assert.deepEqual(developer.available_tools, ['shell', 'tree', 'read_image']); assert.equal( policy.extensionOverrides.some((ext) => ext.name === 'extensionmanager'), false, ); }); 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', () => { assert.deepEqual(normalizeCapabilityPatch({ shell: true, unknown: true }), { shell: true }); }); test('clampUserCapabilities blocks extension_admin even when stored as true', () => { const clamped = clampUserCapabilities({ ...DEFAULT_USER_CAPABILITIES, extension_admin: true, shell: true, }); assert.equal(clamped.extension_admin, false); assert.equal(clamped.shell, true); }); test('clampUserCapabilities blocks Apps for regular H5 users even when granted by role', () => { const clamped = clampUserCapabilities({ ...DEFAULT_USER_CAPABILITIES, apps: true, }); assert.equal(clamped.apps, false); const policy = buildAgentExtensionPolicy(clamped); assert.equal(policy.extensionOverrides.some((ext) => ext.name === 'apps'), false); }); test('catalog keys are unique', () => { const keys = CAPABILITY_CATALOG.map((item) => item.key); assert.equal(keys.length, new Set(keys).size); }); test('buildPageEditAgentPolicy narrows tools to shell when available', () => { const base = buildAgentExtensionPolicy({ ...DEFAULT_USER_CAPABILITIES, shell: true, filesystem: true, skills: true, }); const narrowed = buildPageEditAgentPolicy(base); assert.equal(narrowed.enableContextMemory, false); assert.equal(narrowed.gooseMode, 'auto'); assert.deepEqual(narrowed.extensionOverrides, [ { type: 'platform', name: 'developer', description: '', display_name: 'developer', bundled: true, available_tools: ['shell'], }, ]); }); test('buildPageEditAgentPolicy without shell keeps empty developer tools', () => { const base = buildAgentExtensionPolicy(DEFAULT_USER_CAPABILITIES); const narrowed = buildPageEditAgentPolicy(base); assert.deepEqual(narrowed.extensionOverrides, []); }); test('buildPageEditAgentPolicy grants shell for static_publish sandbox users with shell capability', () => { const base = { ...buildAgentExtensionPolicy( { ...DEFAULT_USER_CAPABILITIES, static_publish: true, shell: true }, { sandboxMcp: { serverPath: '/tmp/mindspace-sandbox-mcp.mjs', sandboxRoot: '/tmp/sandbox', }, }, ), capabilities: { ...DEFAULT_USER_CAPABILITIES, static_publish: true, shell: true }, }; const narrowed = buildPageEditAgentPolicy(base); assert.deepEqual(narrowed.extensionOverrides?.[0]?.available_tools, ['shell']); }); test('sandboxMcpTools returns correct tool list based on capabilities', () => { const base = { ...DEFAULT_USER_CAPABILITIES, static_publish: true }; assert.deepEqual(sandboxMcpTools(base), [ 'read_file', 'write_file', 'edit_file', 'create_dir', 'generate_image', 'generate_docx', 'generate_long_image', 'private_data_info', 'private_data_schema', 'private_data_query', 'private_data_execute', 'private_data_register_dataset', 'private_data_set_page_policy', 'private_data_close_page_dataset', 'private_data_bind_workspace_page', 'schedule_create_item', 'schedule_create_reminder', 'schedule_list_items', ]); const withBrowse = { ...base, code_browse: true }; assert.ok(sandboxMcpTools(withBrowse).includes('list_dir')); const withShell = { ...base, shell: true }; assert.ok(sandboxMcpTools(withShell).includes('list_dir')); }); test('private_data_space alone exposes private data tools through sandbox MCP', () => { const caps = { ...DEFAULT_USER_CAPABILITIES, static_publish: false, private_data_space: true }; assert.deepEqual(sandboxMcpTools(caps), [ 'private_data_info', 'private_data_schema', 'private_data_query', 'private_data_execute', 'private_data_register_dataset', 'private_data_set_page_policy', 'private_data_close_page_dataset', 'private_data_bind_workspace_page', 'schedule_create_item', 'schedule_create_reminder', 'schedule_list_items', ]); const policy = buildAgentExtensionPolicy(caps, { sandboxMcp: { serverPath: '/opt/h5/mindspace-sandbox-mcp.mjs', sandboxRoot: '/opt/h5/MindSpace/user-1', userId: 'user-1', containerized: true, userDataBackend: 'postgres', userDataPgUrl: 'postgresql://mindspace:secret@127.0.0.1:5433/mindspace_userdata_prod', userDataAutoProvision: '1', }, }); const sandboxExt = policy.extensionOverrides.find((ext) => ext.name === 'sandbox-fs'); assert.ok(sandboxExt); assert.equal(sandboxExt.envs.PRIVATE_DATA_USER_ID, 'user-1'); assert.equal(new URL(sandboxExt.envs.MINDSPACE_USERDATA_PG_URL).hostname, 'host.docker.internal'); assert.equal(sandboxExt.envs.MINDSPACE_USERDATA_BACKEND, 'postgres'); assert.equal(sandboxExt.envs.MINDSPACE_USERDATA_AUTO_PROVISION, '1'); assert.deepEqual(sandboxExt.available_tools, [ 'private_data_info', 'private_data_schema', 'private_data_query', 'private_data_execute', 'private_data_register_dataset', 'private_data_set_page_policy', 'private_data_close_page_dataset', 'private_data_bind_workspace_page', 'schedule_create_item', 'schedule_create_reminder', 'schedule_list_items', ]); }); test('static_publish with sandboxMcp uses stdio sandbox-fs extension instead of developer', () => { const caps = { ...DEFAULT_USER_CAPABILITIES, static_publish: true }; const sandboxMcp = { serverPath: '/opt/h5/mindspace-sandbox-mcp.mjs', sandboxRoot: '/opt/h5/MindSpace/abc123', agentApiBaseUrl: 'http://host.docker.internal:8081/api', internalAgentSecret: 'internal-secret', }; const policy = buildAgentExtensionPolicy(caps, { sandboxMcp }); const sandboxExt = policy.extensionOverrides.find((ext) => ext.name === 'sandbox-fs'); assert.ok(sandboxExt, 'sandbox-fs extension should be present'); assert.equal(sandboxExt.type, 'stdio'); assert.equal(sandboxExt.cmd, process.execPath); assert.equal(sandboxExt.envs.SANDBOX_ROOT, '/opt/h5/MindSpace/abc123'); assert.equal(sandboxExt.args[1], '/opt/h5/MindSpace/abc123'); // also passed as argv[2] assert.equal(sandboxExt.envs.MINDSPACE_AGENT_API_BASE_URL, 'http://host.docker.internal:8081/api'); assert.equal(sandboxExt.envs.MINDSPACE_INTERNAL_AGENT_SECRET, 'internal-secret'); assert.ok(sandboxExt.available_tools.includes('write_file')); assert.ok(sandboxExt.available_tools.includes('read_file')); assert.ok(sandboxExt.available_tools.includes('generate_docx')); assert.ok(sandboxExt.available_tools.includes('generate_long_image')); assert.ok(sandboxExt.available_tools.includes('generate_image')); // built-in developer extension should only remain for read_image (image_read: true by default) const developer = policy.extensionOverrides.find((ext) => ext.name === 'developer'); assert.ok(developer, 'developer should remain for read_image'); assert.deepEqual(developer.available_tools, ['read_image']); // no full developer write/edit/shell in extensions const allTools = policy.extensionOverrides.flatMap((e) => e.available_tools ?? []); assert.ok(!allTools.includes('write'), 'built-in write should not be exposed'); assert.ok(!allTools.includes('shell'), 'shell should not be exposed'); }); test('sandboxMcp honors container node executable override', () => { const caps = { ...DEFAULT_USER_CAPABILITIES, static_publish: true }; const policy = buildAgentExtensionPolicy(caps, { sandboxMcp: { serverPath: '/opt/h5/mindspace-sandbox-mcp.mjs', sandboxRoot: '/opt/h5/MindSpace/abc123', nodeExecPath: '/usr/local/bin/node', }, }); const sandboxExt = policy.extensionOverrides.find((ext) => ext.name === 'sandbox-fs'); assert.equal(sandboxExt?.cmd, '/usr/local/bin/node'); }); test('sandboxMcp can use workspaceRoot as the local runtime root compatibility field', () => { const caps = { ...DEFAULT_USER_CAPABILITIES, static_publish: true }; const policy = buildAgentExtensionPolicy(caps, { sandboxMcp: { serverPath: '/opt/h5/mindspace-sandbox-mcp.mjs', workspaceRoot: '/opt/h5/MindSpace/abc123', workspaceRef: 'mindspace://users/abc123/workspace', }, }); const sandboxExt = policy.extensionOverrides.find((ext) => ext.name === 'sandbox-fs'); assert.ok(sandboxExt); assert.equal(sandboxExt.args[1], '/opt/h5/MindSpace/abc123'); assert.equal(sandboxExt.envs.SANDBOX_ROOT, '/opt/h5/MindSpace/abc123'); assert.equal(sandboxExt.envs.MINDSPACE_WORKSPACE_ROOT, '/opt/h5/MindSpace/abc123'); assert.equal(sandboxExt.envs.MINDSPACE_WORKSPACE_REF, 'mindspace://users/abc123/workspace'); });