From 7f3485b8b80a81e7f00eee08bf7309009eebc1a5 Mon Sep 17 00:00:00 2001 From: SaladDay <1203511142@qq.com> Date: Fri, 9 Oct 2026 15:48:54 +0000 Subject: [PATCH] Prepare required Mcode workspace tools --- apps/daemon/internal/agent/mcode/session.go | 9 ++-- .../internal/agent/mcode/session_test.go | 8 ++- apps/daemon/internal/agent/mcode/view_test.go | 16 ++++++ contracts/agents-api/index.md | 1 + contracts/agents-api/zh/index.md | 3 +- packages/mcode-harness/README.md | 6 +-- .../native-mcp-lifecycle.test.mjs | 51 +++++++++++++++++-- packages/mcode-harness/patch-native.mjs | 23 ++++++++- scripts/build-mcode-harness.sh | 13 ++++- 9 files changed, 114 insertions(+), 16 deletions(-) diff --git a/apps/daemon/internal/agent/mcode/session.go b/apps/daemon/internal/agent/mcode/session.go index 1e51d1a8c..6d87ddb21 100644 --- a/apps/daemon/internal/agent/mcode/session.go +++ b/apps/daemon/internal/agent/mcode/session.go @@ -90,10 +90,11 @@ func (s *Session) prepareNative() error { if initialized.ProtocolVersion != 1 { return fmt.Errorf("mcode: unsupported ACP protocol version %d", initialized.ProtocolVersion) } - for _, binding := range s.opts.bindings { - if binding.Transport == "stdio" && initialized.Meta.MCPLifecycle.Version != 2 { - return fmt.Errorf("mcode: native MCP lifecycle is unavailable") - } + requiresMCP := !s.req.DisableExecutionEnvironment || slices.ContainsFunc(s.opts.bindings, func(binding agent.MCPBinding) bool { + return binding.Transport == "stdio" + }) + if requiresMCP && initialized.Meta.MCPLifecycle.Version != 3 { + return fmt.Errorf("mcode: native MCP lifecycle is unavailable") } if !s.req.DisableSubagents && (initialized.Meta.Subagents.Version != 1 || initialized.Meta.Subagents.WorkspaceTools != "protected-mcp-v1" || s.req.MaxConcurrentSubagents == nil || initialized.Meta.Subagents.MaxConcurrent != *s.req.MaxConcurrentSubagents) { return fmt.Errorf("mcode: native Subagent admission is unavailable") diff --git a/apps/daemon/internal/agent/mcode/session_test.go b/apps/daemon/internal/agent/mcode/session_test.go index 44a153114..a8f02d2e5 100644 --- a/apps/daemon/internal/agent/mcode/session_test.go +++ b/apps/daemon/internal/agent/mcode/session_test.go @@ -300,14 +300,18 @@ func TestMCodeProcess(t *testing.T) { if scenario == "unattended" && strings.Contains(string(frame.Params), "elicitation") { os.Exit(7) } - result = map[string]any{"protocolVersion": 1, "_meta": map[string]any{"oac/mcp-lifecycle": map[string]int{"version": 2}}} + result = map[string]any{"protocolVersion": 1, "_meta": map[string]any{"oac/mcp-lifecycle": map[string]int{"version": 3}}} if scenario == "unpatched-mcp" { result = map[string]any{"protocolVersion": 1} } if scenario == "old-mcp-lifecycle" { - result = map[string]any{"protocolVersion": 1, "_meta": map[string]any{"oac/mcp-lifecycle": map[string]int{"version": 1}}} + result = map[string]any{"protocolVersion": 1, "_meta": map[string]any{"oac/mcp-lifecycle": map[string]int{"version": 2}}} } case "session/new", "session/load": + if scenario == "workspace-not-ready" { + send(rpcFrame{JSONRPC: "2.0", ID: frame.ID, Error: &rpcError{Code: -32603, Message: "Required workspace MCP tools are unavailable."}}) + continue + } if strings.HasPrefix(scenario, "prepared-mcp-") { if writeMCPRegistry(os.Getenv("MINIMAX_DATA_DIR"), mcpRegistryEntry("proof.server", "proof_server", "read.status", "read_status"), mcpRegistryEntry("late.server", "late_server", "read.status", "read_status"), mcpRegistryEntry("remote", "remote", "read.status", "read_status")) != nil { os.Exit(12) diff --git a/apps/daemon/internal/agent/mcode/view_test.go b/apps/daemon/internal/agent/mcode/view_test.go index cd75e9ad6..ce0469f56 100644 --- a/apps/daemon/internal/agent/mcode/view_test.go +++ b/apps/daemon/internal/agent/mcode/view_test.go @@ -123,6 +123,22 @@ func TestViewLaunchesNodeWithGatewayOnly(t *testing.T) { } } +func TestWorkspacePreparationRequiresNativeReadiness(t *testing.T) { + for _, scenario := range []string{"old-mcp-lifecycle", "workspace-not-ready"} { + t.Run(scenario, func(t *testing.T) { + record := filepath.Join(t.TempDir(), "calls") + e, err := hostExecutor(t, t.Context(), helperInstall(t, scenario, record), workspaceRequest(t), hostSession(t)) + if err == nil || e != nil { + t.Fatal("workspace preparation accepted an unready native owner", e, err) + } + raw, err := os.ReadFile(record) + if err != nil || strings.Contains(string(raw), "session/prompt") || strings.Contains(string(raw), "session/set_config_option") { + t.Fatal("failed readiness proceeded to model selection or prompt", string(raw), err) + } + }) + } +} + // With environment none the CLI runs in the work directory without the // workspace tools. In the workspace it runs each stdio binding's alias // without arguments and loads each installed Skill from its sandbox path. diff --git a/contracts/agents-api/index.md b/contracts/agents-api/index.md index cbaf3d513..2eb0109ca 100644 --- a/contracts/agents-api/index.md +++ b/contracts/agents-api/index.md @@ -122,6 +122,7 @@ Each item is Core's deliberate or native behavior where the official service beh - Native Item variants beyond those listed under [Turns and Items](./sessions-events.md#turns-and-items) are not projected, and Items cannot be modified. - A function result that cancellation prevents from being applied never appears as an Item. - Claude Code's native MCP error frames do not distinguish a local timeout or transport failure from a server's `isError` reply. Failed stdio calls remain eligible for cancellation within their current Turn, including calls for which the server may already have returned an error; successful native results remove their calls from that set. Cancelling such a service also ends its earlier background work under the [cancellation and settlement contract](./harness-onboarding.md#executor-and-turn-lifetimes). The next Turn does not inherit this set. +- MiniMax Code requires its workspace bridge to complete native MCP tool discovery before preparation succeeds. An unavailable or incomplete workspace tool inventory fails preparation; optional MCP retains native lazy discovery. - MiniMax Code makes declared MCP available to its root ACP Session. Its native task children receive only `oac_workspace` from the generated shared MCP configuration; they do not inherit the root Session's declared MCP bindings. - Codex cancellation is unqualified for MCP calls in an already active child Turn that a different root Turn steers, when the observer first sees that child Turn after it has settled. Native attribution remains with the original root Turn, and the steering receipt does not identify the child Turn; its locally failed calls cannot be reliably assigned to the cancelling root. - Pinned Codex can lose command output emitted before its stream subscription. diff --git a/contracts/agents-api/zh/index.md b/contracts/agents-api/zh/index.md index abfb5727b..6f2f461f7 100644 --- a/contracts/agents-api/zh/index.md +++ b/contracts/agents-api/zh/index.md @@ -1,7 +1,7 @@ --- title: "Agents API 覆盖台账" source: contracts/agents-api/index.md -source_hash: a99cbf206e87abf84c43ed803ac1a4b054dc26f4ba09bc9ed849af91af066a6f +source_hash: 53d85b1cabf4b8389dd288f86594d9e9605e928412beeba8d240936f5acad06c --- Core 旨在以下方固定版本为准支持完整的 OpenAI Agents API([public API rule](https://github.com/MiniMax-AI/OpenAgentCore/blob/main/AGENTS.md#public-api))。本台账记录 Core 对各项资源实现了哪些内容、哪些契约保存其详细信息,并列出相对于 OpenAI 服务的所有已知差异和所有未解决缺口。[API namespaces and credentials](../../../docs/zh/api/index.md) 说明谁调用哪些 API;[Agents API guide](../../../docs/zh/api/public-agent-api.md) 介绍使用方法。 @@ -124,6 +124,7 @@ Core 自身字段位于 `x_agents_core` 中([Core extensions](../../../docs/zh - 除 [Turns and Items](sessions-events.md#turns-and-items) 中列出的变体外,其他原生 Item 变体不会被投影,而且 Items 无法修改。 - 如果取消导致函数结果无法应用,该结果将永远不会作为 Item 出现。 - Claude Code 的原生 MCP 错误帧无法区分本地超时或传输失败与服务端的 `isError` 回复。失败的 stdio 调用会在其当前 Turn 内保留为取消目标,包括服务端可能已经返回错误的调用;成功的原生结果会将其调用从该集合移除。按照[取消与结算契约](./harness-onboarding.md#executor-and-turn-lifetimes),取消此类服务也会终止该服务先前的后台工作。后继 Turn 不继承这个集合。 +- MiniMax Code 要求工作区桥在准备成功前完成原生 MCP 工具发现。工作区工具清单不可用或不完整时,准备会失败;可选 MCP 保持原生惰性发现。 - MiniMax Code 仅向 root ACP Session 提供已声明的 MCP。其原生 task 子代理只从生成的共享 MCP 配置中获得 `oac_workspace`,不继承 root Session 已声明的 MCP 绑定。 - 当另一个 root Turn 向已活跃的子 Turn 追加引导,而观察器首次看到该子 Turn 时它已经结算,Codex 对其中 MCP 调用的取消尚未通过资格验证。原生归属仍指向最初的 root Turn,且引导回执不标识子 Turn;因此无法可靠地将其中本地失败的调用归属于正在取消的 root Turn。 - 固定版本的 Codex 可能会丢失在订阅其流之前发出的命令输出。 diff --git a/packages/mcode-harness/README.md b/packages/mcode-harness/README.md index cc14f1dba..2a1a6f419 100644 --- a/packages/mcode-harness/README.md +++ b/packages/mcode-harness/README.md @@ -6,7 +6,7 @@ One patch script (`patch-native.mjs`) patches the pinned native CLI source. The ## Workspace tools -The native process, its ACP Session and the workspace tools share the Session's workspace as their working directory; native configuration, Skills and history stay in the private Session data directory. Builtin file tools are disabled, so project files are read and written through the bridge's tools. Only the adapter registers this bridge; callers cannot supply its command, profile, working directory or environment. Native diff/undo capture is not provided by this path. Common Files and Artifacts use the same bound workspace. +The native process, its ACP Session and the workspace tools share the Session's workspace as their working directory; native configuration, Skills and history stay in the private Session data directory. Builtin file tools are disabled, so project files are read and written through the bridge's tools. Native Session creation and loading connect the required workspace bridge and validate its tool inventory before preparation succeeds, using the existing 15-second stdio discovery limit and the same connection pool as Turn execution. An unavailable or incomplete bridge fails preparation; optional MCP servers retain native lazy discovery. The expected inventory is generated from the worker's `--describe` output. Only the adapter registers this bridge; callers cannot supply its command, profile, working directory or environment. Native diff/undo capture is not provided by this path. Common Files and Artifacts use the same bound workspace. For each tool call, the bridge starts `launch.mjs` with the Session's private profile (`workspace-profile.json`, written by the daemon). The launcher checks that the profile's `workspace` is a canonical absolute path, creates the `scratch` directory, and runs the worker in the workspace with the bridge's environment. An invalid profile rejects the call. @@ -26,7 +26,7 @@ This standalone companion uses its own npm lock and is excluded from the root pn The bridge owns each launcher until exit. MCP cancellation and transport shutdown stop all owned workers before releasing the bridge. The outer Runtime owns the native process group. Both boundaries require real Docker cancellation tests. -For declared stdio MCP calls, the adapter captures the affected server identities before sending cancellation and retains identities from callbacks received while cancellation drains. A local failure remains unconfirmed within its Turn even if native reports the tool as finished. The native tool wrapper sets the private `details.oac_response_received` field only for a result returned by the MCP SDK, including a server's `isError` reply; the adapter validates the result identity before releasing that call's owner. It waits for the Runtime to close every selected server's process scope, including background descendants, then calls the private `oac/session/mcp/disconnect` ACP extension with the Session ID and those server names. The native owner disconnects only the selected Session connections and waits for their old transports' actual close events. Configurations remain installed for lazy reconnection on a later call; other MCP servers and the workspace bridge keep their connections. HTTP servers, unknown names and `oac_workspace` are rejected by this control operation. ACP initialization advertises `oac/mcp-lifecycle` version 2, which the adapter requires for declared stdio MCP. The request uses the existing native Session, MCP service and connection pool; it adds no model or tool execution loop. +For declared stdio MCP calls, the adapter captures the affected server identities before sending cancellation and retains identities from callbacks received while cancellation drains. A local failure remains unconfirmed within its Turn even if native reports the tool as finished. The native tool wrapper sets the private `details.oac_response_received` field only for a result returned by the MCP SDK, including a server's `isError` reply; the adapter validates the result identity before releasing that call's owner. It waits for the Runtime to close every selected server's process scope, including background descendants, then calls the private `oac/session/mcp/disconnect` ACP extension with the Session ID and those server names. The native owner disconnects only the selected Session connections and waits for their old transports' actual close events. Configurations remain installed for lazy reconnection on a later call; other MCP servers and the workspace bridge keep their connections. HTTP servers, unknown names and `oac_workspace` are rejected by this control operation. ACP initialization advertises `oac/mcp-lifecycle` version 3, which the adapter requires for a workspace or declared stdio MCP. The request uses the existing native Session, MCP service and connection pool; it adds no model or tool execution loop. The daemon sets the protected `protected-mcp-v1` tool policy independently of the concurrency limit. The native catalog applies it to root and child profiles, withholding direct native filesystem and process tools. The Session-private `oac_workspace` MCP server supplies the authorized workspace tools to workers. Other MCP servers keep their native selection rules. The same authored policy filters native prompt capabilities, so the ACP root and child templates name builtin workspace tools only when they are available; MCP tool names and schemas come from their declarations. Native Explore and Verifier profiles keep their stricter native capability ceiling. ACP initialization reports the applied policy and admission limit; enabled Subagents reject an unpatched CLI before accepting model input. @@ -36,7 +36,7 @@ Native tool schemas are retained. Text and image results use standard MCP conten `make check-mcode-harness` runs the package's Node tests and syntax checks. The native prompt regression requires `MCODE_SOURCE` pointing to the pinned, patched native source with upstream dependencies installed; the companion build always runs it. It checks ordinary native guidance, disabled local tools and protected root and child capabilities without a model. Qualify changes with the [Harness acceptance checklist](../../contracts/agents-api/harness-onboarding.md#qualify-the-adapter); synthetic probes and native model runs do not complete public Files/Artifacts or independent Core acceptance. -With the same `MCODE_SOURCE`, `native-mcp-lifecycle.test.mjs` exercises the patched native Session configurations, connection pool and MCP SDK against controlled transports. It checks exact Session/server selection, delayed close events, connection attempts interrupted during initialization, and later lazy reconnection without closing unaffected services. +With the same `MCODE_SOURCE`, `native-mcp-lifecycle.test.mjs` exercises the patched native Session configurations, connection pool and MCP SDK against controlled transports. It checks exact Session/server selection, delayed close events, connection attempts interrupted during initialization, and later lazy reconnection without closing unaffected services. It also checks workspace readiness before preparation completes, rejection of incomplete or failed discovery, and reuse of the prepared connection. For the packaged Linux regression, provide an operator-owned private profile and artifact directory, then run `native.test.mjs` inside the qualified Docker Runtime: diff --git a/packages/mcode-harness/native-mcp-lifecycle.test.mjs b/packages/mcode-harness/native-mcp-lifecycle.test.mjs index 1cb644e82..e99e6fa60 100644 --- a/packages/mcode-harness/native-mcp-lifecycle.test.mjs +++ b/packages/mcode-harness/native-mcp-lifecycle.test.mjs @@ -15,6 +15,8 @@ test('native MCP shutdown fences the old scoped transport and reconnects lazily' // the transport is controlled, so close() can return before its close event. const fixture = ` export const transports = []; + let workspaceListed; + export const workspaceDiscovery = new Promise(resolve => { workspaceListed = resolve; }); export function createTransport(config) { const previous = transports.filter(t => t.name === config.command).length; const transport = { @@ -26,10 +28,24 @@ test('native MCP shutdown fences the old scoped transport and reconnects lazily' if (!('id' in message)) return; if (message.method === 'tools/call' && message.params.name === 'hold') return; if (message.method === 'initialize' && this.name === 'connecting' && previous === 0) return; + if (message.method === 'initialize' && this.name === 'workspace-timeout') return; + if (message.method === 'tools/list' && this.name === 'workspace-held') { + await new Promise(resolve => workspaceListed(resolve)); + } + const names = this.name.startsWith('workspace-') + ? ['read', 'write', 'edit', 'bash', 'grep', 'glob'].map(name => 'workspace_' + name) + : ['read', 'hold', 'server_error']; + if (this.name === 'workspace-incomplete') names.pop(); + if (this.name === 'workspace-empty') names.length = 0; + if (this.name === 'workspace-failed' && message.method === 'initialize') { + queueMicrotask(() => this.onmessage?.({ jsonrpc: '2.0', id: message.id, + error: { code: -32603, message: 'fixture initialization failed' } })); + return; + } const result = message.method === 'initialize' ? { protocolVersion: '2025-11-25', capabilities: { tools: {} }, serverInfo: { name: this.name, version: '1' } } : message.method === 'tools/list' - ? { tools: ['read', 'hold', 'server_error'].map(name => ({ name, inputSchema: { type: 'object' } })) } + ? { tools: names.map(name => ({ name, inputSchema: { type: 'object' } })) } : { content: [{ type: 'text', text: this.name }], ...(message.params.name === 'server_error' ? { isError: true } : {}), _meta: { oac_response_received: false }, details: { oac_response_received: false } }; @@ -44,7 +60,7 @@ test('native MCP shutdown fences the old scoped transport and reconnects lazily' stdin: { contents: `export { McpConnectionPool } from '@mavis/mcp/runtime/connection-pool'; export { SessionMcpServers } from ${JSON.stringify(join(root, 'packages/local-runtime-v2/src/service/mcp/runtime/session-servers.ts'))}; export { LocalMcpService } from ${JSON.stringify(join(root, 'packages/local-runtime-v2/src/service/mcp/runtime/local-mcp.service.ts'))}; - export { transports } from 'lifecycle-transport';`, resolveDir: root }, + export { transports, workspaceDiscovery } from 'lifecycle-transport';`, resolveDir: root }, bundle: true, write: false, platform: 'node', format: 'esm', target: 'node22', tsconfig: join(root, 'tsconfig.standalone.json'), nodePaths: [join(root, 'node_modules')], banner: { js: `import { createRequire as lifecycleCreateRequire } from 'node:module'; const require = lifecycleCreateRequire(${JSON.stringify(join(root, 'package.json'))});` }, @@ -61,7 +77,7 @@ test('native MCP shutdown fences the old scoped transport and reconnects lazily' t.after(() => rmSync(directory, { recursive: true, force: true })); const bundle = join(directory, 'lifecycle.mjs'); writeFileSync(bundle, result.outputFiles[0].text); - const { McpConnectionPool, SessionMcpServers, LocalMcpService, transports } = await import(pathToFileURL(bundle).href); + const { McpConnectionPool, SessionMcpServers, LocalMcpService, transports, workspaceDiscovery } = await import(pathToFileURL(bundle).href); const pool = new McpConnectionPool({ getResolvedServer() { throw new Error('Session overrides were lost'); } }); const sessions = new SessionMcpServers(pool); const service = new LocalMcpService(() => directory, { connectionPool: pool }); @@ -133,6 +149,35 @@ test('native MCP shutdown fences the old scoped transport and reconnects lazily' else assert.match(result.details.mcp.content[0].text, /timed out/i); } + + const previousPolicy = process.env.OAC_RUNTIME_MCODE_TOOL_POLICY; + process.env.OAC_RUNTIME_MCODE_TOOL_POLICY = 'protected-mcp-v1'; + try { + const workspace = command => ({ name: 'oac_workspace', config: { type: 'stdio', command, args: [], + ...(command === 'workspace-timeout' ? { timeout: 20 } : {}) } }); + const before = transports.length; + await service.configureSessionServers('optional', [stdio('optional-unused')]); + assert.equal(transports.length, before, 'optional MCP remains lazy'); + let prepared = false; + const preparation = service.configureSessionServers('workspace', [workspace('workspace-held')]) + .then(() => { prepared = true; }); + const releaseList = await workspaceDiscovery; + assert.equal(prepared, false, 'preparation must wait for the actual tool inventory'); + releaseList(); + await preparation; + const connected = transports.length; + const inventory = await service.listNativeToolsForTurn({ sessionId: 'workspace' }); + assert.deepEqual(inventory.map(tool => tool.toolName).sort(), + ['bash', 'edit', 'glob', 'grep', 'read', 'write'].map(name => 'workspace_' + name)); + assert.equal(transports.length, connected, 'Turn discovery must reuse the prepared connection'); + for (const command of ['workspace-empty', 'workspace-incomplete', 'workspace-failed', 'workspace-timeout']) { + await assert.rejects(service.configureSessionServers(command, [workspace(command)]), /Required workspace MCP tools/); + assert.deepEqual(await service.listNativeToolsForTurn({ sessionId: command }), [], 'failed preparation must remove its configuration'); + } + } finally { + if (previousPolicy === undefined) delete process.env.OAC_RUNTIME_MCODE_TOOL_POLICY; + else process.env.OAC_RUNTIME_MCODE_TOOL_POLICY = previousPolicy; + } } finally { for (const transport of transports) transport.finishClose(); await service.close(); diff --git a/packages/mcode-harness/patch-native.mjs b/packages/mcode-harness/patch-native.mjs index 9442cfac0..6db7ca1ea 100644 --- a/packages/mcode-harness/patch-native.mjs +++ b/packages/mcode-harness/patch-native.mjs @@ -144,6 +144,27 @@ replaceNative('packages/local-runtime-v2/src/service/mcp/runtime/local-mcp.servi async clearSessionServers(sessionId: string): Promise {`); const mcpServiceFile = 'packages/local-runtime-v2/src/service/mcp/runtime/local-mcp.service.ts'; +// The workspace bridge is required, unlike optional user-configured MCP. Use +// the same scoped native connection that Turn assembly will subsequently read. +replaceNative(mcpServiceFile, "import { dirname, join } from 'node:path';", + "import { dirname, join } from 'node:path';\nimport { requiredWorkspaceTools } from './oac-workspace-tools.js';"); +replaceNative(mcpServiceFile, + ' return this.enqueueMutation(() => this.sessionServers.configure(sessionId, servers));', + ` return this.enqueueMutation(async () => { + await this.sessionServers.configure(sessionId, servers); + const workspace = this.sessionServers.get(sessionId)?.oac_workspace; + if (process.env.OAC_RUNTIME_MCODE_TOOL_POLICY !== 'protected-mcp-v1' || !workspace) return; + try { + const tools = await this.listToolsForTurn('oac_workspace', workspace, { sessionId }); + const names = new Set(tools.map((tool) => tool.name)); + if (names.size !== requiredWorkspaceTools.length || + !requiredWorkspaceTools.every((name) => names.has(name))) + throw new Error('Required workspace MCP tools are unavailable.'); + } catch (error) { + await this.sessionServers.remove(sessionId); + throw error; + } + });`); // callLive preserves local failures as MCP-shaped error results. Remember only // SDK replies without changing those results or exposing a server-spoofable bit. replaceNative(mcpServiceFile, ' private closed = false;', @@ -184,7 +205,7 @@ for (const [file, owner] of [ clearSessionMcpServers(sessionId: string): Promise {`); } -replaceNative('packages/tui/src/acp/agent.ts', " 'oac/subagents': {", " 'oac/mcp-lifecycle': { version: 2 },\n 'oac/subagents': {"); +replaceNative('packages/tui/src/acp/agent.ts', " 'oac/subagents': {", " 'oac/mcp-lifecycle': { version: 3 },\n 'oac/subagents': {"); replaceNative('packages/tui/src/acp/agent.ts', ' app.onNotification(acp.methods.agent.session.cancel, async ({ params }) => {', ` app.onRequest('oac/session/mcp/disconnect', (value: unknown) => { diff --git a/scripts/build-mcode-harness.sh b/scripts/build-mcode-harness.sh index de1ca5cd4..59cb88f19 100644 --- a/scripts/build-mcode-harness.sh +++ b/scripts/build-mcode-harness.sh @@ -47,16 +47,25 @@ test "$(node "$native/cli.js" --version)" = "$version" MCODE_SOURCE="$context/upstream" node patch-native.mjs cd upstream corepack pnpm install --frozen-lockfile - MCODE_SOURCE="$context/upstream" node --test "$context/native-prompt.test.mjs" "$context/native-mcp-lifecycle.test.mjs" - node scripts/build.mjs ) ( cd "$context" npm ci --no-audit --no-fund MCODE_SOURCE="$context/upstream" node build.mjs node dist/worker.mjs /workspace --describe > dist/tools.json + node --input-type=module -e ' + import { readFileSync, writeFileSync } from "node:fs"; + const tools = JSON.parse(readFileSync("dist/tools.json", "utf8")); + writeFileSync("upstream/packages/local-runtime-v2/src/service/mcp/runtime/oac-workspace-tools.ts", + "export const requiredWorkspaceTools = " + JSON.stringify(tools.map(tool => "workspace_" + tool.name)) + ";\n"); + ' npm prune --omit=dev --no-audit --no-fund ) +( + cd "$context/upstream" + MCODE_SOURCE="$context/upstream" node --test "$context/native-prompt.test.mjs" "$context/native-mcp-lifecycle.test.mjs" + node scripts/build.mjs +) artifact="$context/artifact" mkdir "$artifact" cp -R "$context/dist" "$context/node_modules" "$artifact/"