#!/usr/bin/env node /** * Milestone 2 smoke: load sandbox-fs stdio MCP via extension_overrides on local Goose v1.49. */ import fs from 'node:fs'; import os from 'node:os'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; import { Agent, fetch } from 'undici'; const memindRoot = path.join(path.dirname(fileURLToPath(import.meta.url)), '..'); const port = process.env.GOOSE_V149_PORT || '18049'; const host = process.env.GOOSE_V149_HOST || '127.0.0.1'; const secret = process.env.GOOSE_SERVER__SECRET_KEY || 'local-v149-dev-secret'; const workingDir = process.env.GOOSE_V149_WORKING_DIR || memindRoot; const nodeExec = process.env.GOOSED_MCP_NODE_PATH || process.execPath; const serverPath = process.env.GOOSED_MCP_SERVER_PATH || path.join(memindRoot, 'mindspace-sandbox-mcp.mjs'); const pollMs = Number(process.env.GOOSE_V149_SANDBOX_POLL_MS || 500); const timeoutMs = Number(process.env.GOOSE_V149_SANDBOX_TIMEOUT_MS || 30_000); const blockedHosts = ['58.38.22.103', '120.26.184.105']; if (blockedHosts.includes(host)) { console.error(`GOOSE_V149_SANDBOX_FS_FAIL: refusing production host ${host}`); process.exit(1); } if (!fs.existsSync(serverPath)) { console.error(`GOOSE_V149_SANDBOX_FS_FAIL: MCP entry missing: ${serverPath}`); process.exit(1); } const sandboxRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'goose-v149-sandbox-')); fs.mkdirSync(path.join(sandboxRoot, 'public'), { recursive: true }); const base = `https://${host}:${port}`; const dispatcher = new Agent({ connect: { rejectUnauthorized: false } }); async function apiFetch(pathname, init = {}) { const headers = { ...(init.headers ?? {}), 'X-Secret-Key': secret, }; if (init.body && !headers['Content-Type']) { headers['Content-Type'] = 'application/json'; } return fetch(`${base}${pathname}`, { ...init, headers, dispatcher, }); } async function apiJson(pathname, body) { const response = await apiFetch(pathname, { method: 'POST', body: JSON.stringify(body), }); const text = await response.text(); if (!response.ok) { throw new Error(`${pathname} ${response.status}: ${text.slice(0, 800)}`); } return text.trim() ? JSON.parse(text) : {}; } async function waitForSandboxTools(sessionId) { const deadline = Date.now() + timeoutMs; while (Date.now() < deadline) { const response = await apiFetch( `/agent/tools?session_id=${encodeURIComponent(sessionId)}&extension_name=sandbox-fs`, ); const text = await response.text(); if (!response.ok) { throw new Error(`/agent/tools ${response.status}: ${text.slice(0, 400)}`); } const tools = JSON.parse(text); const names = tools.map((tool) => tool.name); const writeTool = names.find((name) => name === 'sandbox-fs__write_file'); if (writeTool) { return names; } await new Promise((resolve) => setTimeout(resolve, pollMs)); } throw new Error(`sandbox-fs tools not ready within ${timeoutMs}ms`); } try { const session = await apiJson('/agent/start', { working_dir: workingDir, extension_overrides: [ { type: 'stdio', name: 'sandbox-fs', description: 'MindSpace sandbox filesystem MCP (local v1.49 smoke)', cmd: nodeExec, args: [serverPath, sandboxRoot], envs: { ALLOWED_TOOLS: 'write_file,read_file,edit_file,create_dir,list_dir', SANDBOX_ROOT: sandboxRoot, }, available_tools: ['write_file', 'read_file', 'edit_file', 'create_dir', 'list_dir'], }, { type: 'platform', name: 'skills', description: 'skills', available_tools: [], }, ], }); if (!session?.id) { throw new Error('missing session id from /agent/start'); } const extResponse = await apiFetch(`/sessions/${session.id}/extensions`); const extBody = await extResponse.json(); const configured = (extBody.extensions ?? []).some((ext) => ext.name === 'sandbox-fs'); if (!configured) { throw new Error('sandbox-fs not present in session extension_data'); } const toolNames = await waitForSandboxTools(session.id); console.log( `GOOSE_V149_SANDBOX_FS_OK: session=${session.id} sandbox=${sandboxRoot} tools=${toolNames.length}`, ); } catch (error) { console.error(`GOOSE_V149_SANDBOX_FS_FAIL: ${error.message}`); process.exit(1); }