diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 000000000..0ee3aecde --- /dev/null +++ b/.gitattributes @@ -0,0 +1,2 @@ +# Bash rejects carriage returns, so shell scripts stay LF on Windows checkouts. +*.sh text eol=lf diff --git a/.github/workflows/cache-warm.yml b/.github/workflows/cache-warm.yml index 1471f5d52..4f33cbb36 100644 --- a/.github/workflows/cache-warm.yml +++ b/.github/workflows/cache-warm.yml @@ -24,6 +24,8 @@ jobs: github-runner: ubuntu-22.04 - runner: macos-15 github-runner: macos-15 + - runner: blacksmith-2vcpu-windows-2025 + github-runner: windows-2025 runs-on: ${{ vars.OAC_USE_GITHUB_RUNNERS == 'true' && matrix.github-runner || matrix.runner }} timeout-minutes: 15 steps: diff --git a/.github/workflows/native.yml b/.github/workflows/native.yml index e5394a926..03814d5f0 100644 --- a/.github/workflows/native.yml +++ b/.github/workflows/native.yml @@ -97,7 +97,7 @@ jobs: ./apps/daemon/internal/agent/codex ./apps/daemon/internal/agent/claudesdk ./apps/daemon/internal/agent/mcode - -run 'TestJSONRPCClientOwnsToolDescendants|TestSelfHostedToolEnvironment' + -run 'TestJSONRPCClientOwnsToolDescendants|TestSelfHostedToolEnvironment|TestNativeCommand|TestSelfHostedSkillDirectoryLink' - name: Native capability snapshots, files and installation id: files run: >- @@ -124,8 +124,7 @@ jobs: rm -rf "$RUNNER_TEMP/claude-runtime/node_modules" pnpm --dir "$RUNNER_TEMP/claude-runtime" install --prod --frozen-lockfile --config.node-linker=hoisted --ignore-scripts npm install --prefix "$RUNNER_TEMP/native-tools" --no-save @openai/codex@0.153.4 - - name: Restore or build the pinned MiniMax companion on Unix - if: runner.os != 'Windows' + - name: Restore or build the pinned MiniMax companion uses: ./.github/actions/mcode-companion - name: Package and exercise CLI-only installation, additions and reuse id: package diff --git a/apps/daemon/internal/agent/mcode/discovery_workspace.go b/apps/daemon/internal/agent/mcode/discovery_workspace.go index d8f60f021..77170013b 100644 --- a/apps/daemon/internal/agent/mcode/discovery_workspace.go +++ b/apps/daemon/internal/agent/mcode/discovery_workspace.go @@ -27,11 +27,7 @@ func discoverWorkspace(parent context.Context, options agent.DiscoveryOptions, r narrowWorkspace(&runtime.Info.Capabilities, false) return nil } - node := os.Getenv("OAC_RUNTIME_MCODE_NODE") - if node == "" { - node = "node" - } - node, err = exec.LookPath(node) + node, err := exec.LookPath(defaultNode()) if err != nil { fail(err) return nil diff --git a/apps/daemon/internal/agent/mcode/executor.go b/apps/daemon/internal/agent/mcode/executor.go index 9853f9d5a..dbb0ee104 100644 --- a/apps/daemon/internal/agent/mcode/executor.go +++ b/apps/daemon/internal/agent/mcode/executor.go @@ -49,11 +49,11 @@ func NewExecutorFactory(runtimeRoot string, config *WorkspaceConfig) agent.Execu return nil, err } var opts launchOptions - binary := defaultBinary() + command := nativeCommand(defaultBinary(), defaultNode()) if frozen == nil { opts, err = prepareOptions(runtimeRoot, req, nil) } else { - binary = frozen.Binary + command = nativeCommand(frozen.Binary, frozen.Node) opts, err = prepareWorkspaceOptions(runtimeRoot, *frozen, req) } if err != nil { @@ -69,7 +69,7 @@ func NewExecutorFactory(runtimeRoot string, config *WorkspaceConfig) agent.Execu if opts.Functions != nil { opts.MCP = append(opts.MCP, opts.Functions.mcpServer()) } - resource, err := newExecutor(ctx, req, opts, binary) + resource, err := newExecutor(ctx, req, opts, command) if resource == nil { return nil, err } @@ -77,8 +77,8 @@ func NewExecutorFactory(runtimeRoot string, config *WorkspaceConfig) agent.Execu } } -func newExecutor(ctx context.Context, req proto.PromptRequestPayload, opts launchOptions, binary string) (*executor, error) { - bootstrap, err := launch(ctx, req, opts, binary) +func newExecutor(ctx context.Context, req proto.PromptRequestPayload, opts launchOptions, command []string) (*executor, error) { + bootstrap, err := launch(ctx, req, opts, command) if err != nil { opts.Functions.close() return nil, err diff --git a/apps/daemon/internal/agent/mcode/installation.go b/apps/daemon/internal/agent/mcode/installation.go index ce35f0f42..f94e55c6e 100644 --- a/apps/daemon/internal/agent/mcode/installation.go +++ b/apps/daemon/internal/agent/mcode/installation.go @@ -10,7 +10,7 @@ import ( ) func Installation() agent.Installation { - return agent.Installation{AgentKind: "mcode", Version: SupportedVersion, Supported: func() bool { return runtime.GOOS == "linux" || runtime.GOOS == "darwin" }, + return agent.Installation{AgentKind: "mcode", Version: SupportedVersion, Supported: func() bool { return runtime.GOOS == "linux" || runtime.GOOS == "darwin" || runtime.GOOS == "windows" }, Environment: func(dir, node string) map[string]string { return map[string]string{"OAC_RUNTIME_MCODE_BIN": filepath.Join(dir, "native", "cli.js"), "OAC_RUNTIME_MCODE_NODE": node, "OAC_RUNTIME_MCODE_COMPANION": dir} }, diff --git a/apps/daemon/internal/agent/mcode/link_other.go b/apps/daemon/internal/agent/mcode/link_other.go new file mode 100644 index 000000000..e955fa7b9 --- /dev/null +++ b/apps/daemon/internal/agent/mcode/link_other.go @@ -0,0 +1,10 @@ +//go:build !windows + +package mcode + +import "os" + +// linkDirectory makes link resolve to the existing directory target. +func linkDirectory(target, link string) error { + return os.Symlink(target, link) +} diff --git a/apps/daemon/internal/agent/mcode/link_windows.go b/apps/daemon/internal/agent/mcode/link_windows.go new file mode 100644 index 000000000..bce93f879 --- /dev/null +++ b/apps/daemon/internal/agent/mcode/link_windows.go @@ -0,0 +1,70 @@ +//go:build windows + +package mcode + +import ( + "encoding/binary" + "fmt" + "os" + "path/filepath" + "unicode/utf16" + + "golang.org/x/sys/windows" +) + +// linkDirectory makes link resolve to the existing directory target. +// +// It creates a directory junction. A directory symbolic link needs Developer +// Mode or elevation on Windows; a junction needs neither, and os.Readlink +// returns its target, so the native Skill root is verified as on Unix. +func linkDirectory(target, link string) error { + if !filepath.IsAbs(target) { + return fmt.Errorf("mcode: junction target must be absolute") + } + if err := os.Mkdir(link, 0700); err != nil { + return err + } + if err := setJunction(link, target); err != nil { + _ = os.Remove(link) + return err + } + return nil +} + +// setJunction writes a mount point reparse point to the empty directory link. +func setJunction(link, target string) error { + path, err := windows.UTF16PtrFromString(link) + if err != nil { + return err + } + handle, err := windows.CreateFile(path, windows.GENERIC_WRITE, 0, nil, windows.OPEN_EXISTING, + windows.FILE_FLAG_OPEN_REPARSE_POINT|windows.FILE_FLAG_BACKUP_SEMANTICS, 0) + if err != nil { + return err + } + defer windows.CloseHandle(handle) + data := junctionReparseData(target) + var returned uint32 + return windows.DeviceIoControl(handle, windows.FSCTL_SET_REPARSE_POINT, &data[0], uint32(len(data)), nil, 0, &returned, nil) +} + +// junctionReparseData encodes REPARSE_DATA_BUFFER for IO_REPARSE_TAG_MOUNT_POINT: +// the NT substitute name \??\ and the print name , each +// followed by a UTF-16 NUL that the lengths exclude. +func junctionReparseData(target string) []byte { + substitute := utf16.Encode([]rune(`\??\` + target)) + printName := utf16.Encode([]rune(target)) + names := make([]uint16, 0, len(substitute)+len(printName)+2) + names = append(append(append(append(names, substitute...), 0), printName...), 0) + data := make([]byte, 16+2*len(names)) + binary.LittleEndian.PutUint32(data[0:], windows.IO_REPARSE_TAG_MOUNT_POINT) + binary.LittleEndian.PutUint16(data[4:], uint16(8+2*len(names))) + binary.LittleEndian.PutUint16(data[8:], 0) + binary.LittleEndian.PutUint16(data[10:], uint16(2*len(substitute))) + binary.LittleEndian.PutUint16(data[12:], uint16(2*len(substitute)+2)) + binary.LittleEndian.PutUint16(data[14:], uint16(2*len(printName))) + for i, unit := range names { + binary.LittleEndian.PutUint16(data[16+2*i:], unit) + } + return data +} diff --git a/apps/daemon/internal/agent/mcode/native_command_test.go b/apps/daemon/internal/agent/mcode/native_command_test.go new file mode 100644 index 000000000..407d1ce99 --- /dev/null +++ b/apps/daemon/internal/agent/mcode/native_command_test.go @@ -0,0 +1,60 @@ +package mcode + +import ( + "os" + "path/filepath" + "slices" + "testing" +) + +func TestNativeCommandRunsJavaScriptEntrypointWithNode(t *testing.T) { + for _, test := range []struct { + binary string + want []string + }{ + {filepath.Join("opt", "native", "cli.js"), []string{"node22", filepath.Join("opt", "native", "cli.js")}}, + {filepath.Join("opt", "native", "CLI.JS"), []string{"node22", filepath.Join("opt", "native", "CLI.JS")}}, + {"mcode", []string{"mcode"}}, + {filepath.Join("bin", "mcode.exe"), []string{filepath.Join("bin", "mcode.exe")}}, + } { + if got := nativeCommand(test.binary, "node22"); !slices.Equal(got, test.want) { + t.Fatalf("nativeCommand(%q) = %q, want %q", test.binary, got, test.want) + } + } +} + +// The Skill root links each installed Skill directory; on Windows this must +// work without Developer Mode or elevation. +func TestSelfHostedSkillDirectoryLink(t *testing.T) { + root := t.TempDir() + target := filepath.Join(root, "installed", "skill") + if err := os.MkdirAll(target, 0700); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(filepath.Join(target, "SKILL.md"), []byte("skill"), 0600); err != nil { + t.Fatal(err) + } + link := filepath.Join(root, "skills", "skill") + if err := os.MkdirAll(filepath.Dir(link), 0700); err != nil { + t.Fatal(err) + } + if err := linkDirectory(target, link); err != nil { + t.Fatal(err) + } + if actual, err := os.Readlink(link); err != nil || actual != target { + t.Fatalf("Readlink = %q, %v; want %q", actual, err, target) + } + if raw, err := os.ReadFile(filepath.Join(link, "SKILL.md")); err != nil || string(raw) != "skill" { + t.Fatalf("read through link = %q, %v", raw, err) + } + if err := linkDirectory(target, link); err == nil { + t.Fatal("an existing link was replaced") + } + // Removing the Session's Skill root must leave the installed Skill intact. + if err := os.RemoveAll(filepath.Dir(link)); err != nil { + t.Fatal(err) + } + if _, err := os.Stat(filepath.Join(target, "SKILL.md")); err != nil { + t.Fatalf("removing the link removed the target: %v", err) + } +} diff --git a/apps/daemon/internal/agent/mcode/session.go b/apps/daemon/internal/agent/mcode/session.go index 826214f7d..5e4be70cc 100644 --- a/apps/daemon/internal/agent/mcode/session.go +++ b/apps/daemon/internal/agent/mcode/session.go @@ -53,8 +53,8 @@ type Session struct { var _ agent.Session = (*Session)(nil) -func launch(ctx context.Context, req proto.PromptRequestPayload, opts launchOptions, binary string) (*Session, error) { - process, err := clirunner.Start(clirunner.StartOptions{Parent: ctx, Binary: binary, Args: []string{"acp"}, Dir: opts.Dir, Env: opts.Env, NeedStdin: true}) +func launch(ctx context.Context, req proto.PromptRequestPayload, opts launchOptions, command []string) (*Session, error) { + process, err := clirunner.Start(clirunner.StartOptions{Parent: ctx, Binary: command[0], Args: append(slices.Clone(command[1:]), "acp"), Dir: opts.Dir, Env: opts.Env, NeedStdin: true}) if err != nil { return nil, err } diff --git a/apps/daemon/internal/agent/mcode/subagents.go b/apps/daemon/internal/agent/mcode/subagents.go index 56ea16303..19a0501e1 100644 --- a/apps/daemon/internal/agent/mcode/subagents.go +++ b/apps/daemon/internal/agent/mcode/subagents.go @@ -53,7 +53,7 @@ type nativeSubagentTask struct { } func subagentReader() (string, string, error) { - node := os.Getenv("OAC_RUNTIME_MCODE_NODE") + node := defaultNode() reader := filepath.Join(os.Getenv("OAC_RUNTIME_MCODE_COMPANION"), "subagent-snapshot.mjs") for _, path := range []string{node, reader} { resolved, err := filepath.EvalSymlinks(path) diff --git a/apps/daemon/internal/agent/mcode/version.go b/apps/daemon/internal/agent/mcode/version.go index 95c476007..ecdba95e1 100644 --- a/apps/daemon/internal/agent/mcode/version.go +++ b/apps/daemon/internal/agent/mcode/version.go @@ -4,6 +4,9 @@ import ( "context" "errors" "fmt" + "os" + "path/filepath" + "strings" "github.com/MiniMax-AI/OpenAgentCore/apps/daemon/internal/agent/binpath" "github.com/MiniMax-AI/OpenAgentCore/apps/daemon/internal/agent/versionprobe" @@ -15,8 +18,36 @@ const SupportedVersion = "0.4.12" func defaultBinary() string { return binpath.MCode() } +// defaultNode is the Node.js that runs a JavaScript CLI entrypoint. +func defaultNode() string { + if node := strings.TrimSpace(os.Getenv("OAC_RUNTIME_MCODE_NODE")); node != "" { + return node + } + return "node" +} + +// nativeCommand returns the program and leading arguments that start the CLI. +// A JavaScript entrypoint, such as the packaged native/cli.js, runs under the +// given Node.js: Windows cannot execute it directly, and on Unix this selects +// the same pinned Node.js that its shebang would find first on PATH. +func nativeCommand(binary, node string) []string { + if strings.EqualFold(filepath.Ext(binary), ".js") { + return []string{node, binary} + } + return []string{binary} +} + func CheckCLIAvailable(ctx context.Context, binary string) (string, error) { - version, err := versionprobe.Check(ctx, binary, versionprobe.Config{Name: "mcode", DefaultBinary: defaultBinary(), MissingError: ErrCLINotFound, TrimBinary: true}) + if binary = strings.TrimSpace(binary); binary == "" { + binary = strings.TrimSpace(defaultBinary()) + } + command := nativeCommand(binary, defaultNode()) + if len(command) > 1 { + if info, err := os.Stat(binary); err != nil || !info.Mode().IsRegular() { + return "", fmt.Errorf("%w: %s", ErrCLINotFound, binary) + } + } + version, err := versionprobe.Check(ctx, command[0], versionprobe.Config{Name: "mcode", Args: command[1:], DefaultBinary: command[0], MissingError: ErrCLINotFound, TrimBinary: true}) if err == nil && version != SupportedVersion { err = fmt.Errorf("mcode: unsupported version %s; install %s", version, SupportedVersion) } diff --git a/apps/daemon/internal/agent/mcode/workspace.go b/apps/daemon/internal/agent/mcode/workspace.go index 334d6c782..0e4d3c319 100644 --- a/apps/daemon/internal/agent/mcode/workspace.go +++ b/apps/daemon/internal/agent/mcode/workspace.go @@ -5,7 +5,6 @@ import ( "maps" "os" "path/filepath" - "runtime" "slices" "github.com/MiniMax-AI/OpenAgentCore/apps/daemon/internal/localworkspace" @@ -19,11 +18,8 @@ type WorkspaceConfig struct { func ConfigureLocal(binary, node, companion, workspace string) (WorkspaceConfig, error) { c := WorkspaceConfig{Binary: binary, Node: node, Companion: companion, Directory: workspace} - if runtime.GOOS == "windows" { - return c, fmt.Errorf("mcode: native execution requires Linux or macOS") - } for _, path := range []string{binary, node, companion, workspace} { - if !filepath.IsAbs(path) || filepath.Clean(path) != path || path == "/" { + if !filepath.IsAbs(path) || filepath.Clean(path) != path || filepath.Dir(path) == path { return c, fmt.Errorf("mcode: canonical absolute deployment paths are required") } } @@ -77,7 +73,7 @@ func prepareWorkspaceOptions(runtimeRoot string, c WorkspaceConfig, req proto.Pr } } else if !os.IsNotExist(err) { return opts, err - } else if err := os.Symlink(target, link); err != nil { + } else if err := linkDirectory(target, link); err != nil { return opts, err } } diff --git a/apps/daemon/internal/agent/mcode/workspace_readiness_test.go b/apps/daemon/internal/agent/mcode/workspace_readiness_test.go index 910a4d78d..b1a01ab3a 100644 --- a/apps/daemon/internal/agent/mcode/workspace_readiness_test.go +++ b/apps/daemon/internal/agent/mcode/workspace_readiness_test.go @@ -12,7 +12,7 @@ import ( func TestWorkspaceReadinessRejectsOldCleanupContract(t *testing.T) { if runtime.GOOS == "windows" { - t.Skip("MiniMax adapter supports Linux and macOS") + t.Skip("POSIX shell fixture") } for _, protocol := range []int{2, 3} { t.Run(fmt.Sprint(protocol), func(t *testing.T) { diff --git a/apps/daemon/internal/agent/versionprobe/versionprobe.go b/apps/daemon/internal/agent/versionprobe/versionprobe.go index 92effd9a5..e630858d9 100644 --- a/apps/daemon/internal/agent/versionprobe/versionprobe.go +++ b/apps/daemon/internal/agent/versionprobe/versionprobe.go @@ -5,19 +5,22 @@ import ( "context" "fmt" "os/exec" + "slices" "strings" ) // Config describes an adapter's CLI version command and error contract. type Config struct { - Name string - DefaultBinary string + Name string + DefaultBinary string + // Args precede --version, for a CLI started through an interpreter. + Args []string MissingError error TrimBinary bool StderrFallback bool } -// Check runs ` --version` and returns its trimmed first output line. +// Check runs ` [args...] --version` and returns its trimmed first output line. func Check(ctx context.Context, binary string, config Config) (string, error) { if (config.TrimBinary && strings.TrimSpace(binary) == "") || (!config.TrimBinary && binary == "") { binary = config.DefaultBinary @@ -27,7 +30,7 @@ func Check(ctx context.Context, binary string, config Config) (string, error) { } var stdout, stderr bytes.Buffer - command := exec.CommandContext(ctx, binary, "--version") + command := exec.CommandContext(ctx, binary, append(slices.Clone(config.Args), "--version")...) command.Stdout = &stdout command.Stderr = &stderr if err := command.Run(); err != nil { diff --git a/apps/daemon/internal/cli/native_install_test.go b/apps/daemon/internal/cli/native_install_test.go index bae3f95b7..7f490177b 100644 --- a/apps/daemon/internal/cli/native_install_test.go +++ b/apps/daemon/internal/cli/native_install_test.go @@ -276,11 +276,6 @@ func TestNativeInstallationPlatformMismatchAndUnsupportedSelection(t *testing.T) if _, err := selectedNativeHarnesses("anything"); err == nil { t.Fatal("unknown Harness accepted") } - if runtime.GOOS == "windows" { - if _, err := selectedNativeHarnesses("minimax"); err == nil { - t.Fatal("unsupported Windows MiniMax accepted") - } - } } func TestNativeInstallationDoesNotRepairMissingDaemon(t *testing.T) { diff --git a/apps/web/src/i18n/locales/en/sessions.ts b/apps/web/src/i18n/locales/en/sessions.ts index 3384645fe..59c429669 100644 --- a/apps/web/src/i18n/locales/en/sessions.ts +++ b/apps/web/src/i18n/locales/en/sessions.ts @@ -184,7 +184,7 @@ export const sessions = { hostDone: "Host connected", hostPending: "Awaiting connection", title: "Connect a host", - lifecycle: "Install and run the daemon with your current account. Choose one or more Harnesses; the Session Harness is required. Windows does not support MiniMax. The workspace remains the one selected for this Session.", + lifecycle: "Install and run the daemon with your current account. Choose one or more Harnesses; the Session Harness is required. The workspace remains the one selected for this Session.", steps: "Copy this command and run it on your machine. It downloads the matching installer, installs your Harnesses, starts the daemon and checks its connection.", archived: "This project is archived. New installation authorizations are unavailable.", guide: "Installation guide", diff --git a/apps/web/src/i18n/locales/zh-CN/sessions.ts b/apps/web/src/i18n/locales/zh-CN/sessions.ts index cdec25270..5ece21436 100644 --- a/apps/web/src/i18n/locales/zh-CN/sessions.ts +++ b/apps/web/src/i18n/locales/zh-CN/sessions.ts @@ -186,7 +186,7 @@ export const sessions = { hostDone: "主机已连接", hostPending: "等待连接", title: "连接主机", - lifecycle: "使用当前账户安装和运行 daemon。可选择多个 Harness,其中必须包含此 Session 使用的 Harness;Windows 不支持 MiniMax。工作目录与此 Session 的配置保持一致。", + lifecycle: "使用当前账户安装和运行 daemon。可选择多个 Harness,其中必须包含此 Session 使用的 Harness。工作目录与此 Session 的配置保持一致。", steps: "复制命令并在自己的机器上运行,即可下载匹配的安装包、安装 Harness、启动 daemon 并验证连接。", archived: "此项目已归档,无法获取新的安装授权。", guide: "安装指南", diff --git a/docs/getting-started/self-hosted.md b/docs/getting-started/self-hosted.md index 9462b1c8d..ccafda6be 100644 --- a/docs/getting-started/self-hosted.md +++ b/docs/getting-started/self-hosted.md @@ -17,7 +17,7 @@ The Session brings its own model provider; the installation default never applie | --- | --- | --- | --- | | Linux amd64 | Supported | Supported | Supported | | macOS arm64 | Supported | Supported | Supported | -| Windows amd64 | Supported | Supported | Not supported | +| Windows amd64 | Supported | Supported | Supported | The installer brings its own pinned Node.js and Harness versions (listed in [`scripts/build-native-installer.mjs`](https://github.com/MiniMax-AI/OpenAgentCore/blob/main/scripts/build-native-installer.mjs)) and leaves other installations of those tools untouched. On a platform without a matching installer, the command fails. @@ -33,7 +33,7 @@ Components install once per machine and version, in `components/` under The machine needs: - HTTP or HTTPS access to Core, and to the release download host unless Core carries an offline copy of this platform's installer (an offline installation carries only Linux amd64); -- Bash for environment setup and MiniMax Code tools; on Windows, Git Bash, which Claude Code also requires; +- Bash for environment setup and, on Unix, MiniMax Code tools; on Windows, Git Bash, which Claude Code also requires. MiniMax Code on Windows runs its `bash` tool in PowerShell, as a local MiniMax Code run does; - Python and pip when the Session's packages need them; - any system packages your setup needs. The daemon never runs apt, sudo or another elevation command, so install them through the host's normal administration. diff --git a/docs/maintainers.md b/docs/maintainers.md index 74a1a22ef..c32bed82e 100644 --- a/docs/maintainers.md +++ b/docs/maintainers.md @@ -54,7 +54,7 @@ node scripts/build-native-catalog.mjs INPUT_DIR OUTPUT_DIR export OAC_NATIVE_INSTALLER_BUILD_DIR=OUTPUT_DIR ``` -Each platform has a small base archive `-.tar.gz` (`oac-daemon` and `bundle.json`, which lists every component with the SHA-256 of each file) and one archive per component, `--node` and `--` (MiniMax Code is not built for Windows). `build-native-installer.mjs` writes them. Core serves these artifacts by name and knows nothing of their content; `oac-daemon install` downloads the components it needs. +Each platform has a small base archive `-.tar.gz` (`oac-daemon` and `bundle.json`, which lists every component with the SHA-256 of each file) and one archive per component, `--node` and `--`. `build-native-installer.mjs` writes them. Core serves these artifacts by name and knows nothing of their content; `oac-daemon install` downloads the components it needs. The catalog records the commit, the Runtime protocol version, each artifact's SHA-256 and, with a release base, its versioned URL. The control archive and Core image carry only `native-installers/catalog.json`; the archives become separate `oac-native--.tar.gz` release assets, and the offline archive holds a copy of every Linux amd64 archive outside the Core image, which its `install.py` copies into the installation for Core to serve. Core redirects the other platforms to their release assets; a catalog without a release base has no such URL, so its offline archive holds every archive. Without a catalog, Sessions report the install command as unavailable, and the release workflow refuses to publish. @@ -84,7 +84,7 @@ docker build --platform linux/amd64 -t oac-runtime:claude "${OAC_DEV_HOME:-$HOME The first step exports the adapter with the pinned Claude Agent SDK (`packages/claude-sdk-adapter/package.json`) as a checksummed archive for the host platform; the second verifies it and adds the daemon. The image step needs the `linux-x64-glibc` archive, so build both on Linux x86_64 with glibc. Keep the exported archive unchanged. -**MiniMax Code Runtime image.** Build the companion from a checkout of the revision pinned in `packages/mcode-harness/source.json`, with the `@minimax-ai/code` npm package of the same version for native dependencies. The companion build runs on Linux x86_64 or macOS arm64 into a new directory; for the Linux Runtime image, build it on Linux x86_64 (macOS arm64 serves only the native installer): +**MiniMax Code Runtime image.** Build the companion from a checkout of the revision pinned in `packages/mcode-harness/source.json`, with the `@minimax-ai/code` npm package of the same version for native dependencies. The companion build runs on Linux x86_64, macOS arm64 or Windows x86_64 (in Git Bash) into a new directory; for the Linux Runtime image, build it on Linux x86_64 (macOS and Windows builds serve only the native installer): ```sh MCODE_NATIVE_SOURCE=/absolute/minimax-code \ diff --git a/docs/zh/getting-started/self-hosted.md b/docs/zh/getting-started/self-hosted.md index 58c4faa25..a1da20c3a 100644 --- a/docs/zh/getting-started/self-hosted.md +++ b/docs/zh/getting-started/self-hosted.md @@ -1,7 +1,7 @@ --- title: "运行自托管 Session" source: docs/getting-started/self-hosted.md -source_hash: de2bf583004e0aabb735d434133780e26f96e9912dd7e11100327b2f83f403c4 +source_hash: 396b1580ad57ab4c9d7beebd6b0cb37b254e2c48f6cb70b3a6b6f5996e98c07a --- `self_hosted` Session 在应用拥有的机器上运行,例如工作站、虚拟机或容器,而不是 Core 管理的沙箱。本指南面向用 `oac-daemon` 连接此类机器并保持其运行的应用开发者。 @@ -19,7 +19,7 @@ Session 自带模型提供商;安装默认模型不适用([原因](../../../ | --- | --- | --- | --- | | Linux amd64 | 支持 | 支持 | 支持 | | macOS arm64 | 支持 | 支持 | 支持 | -| Windows amd64 | 支持 | 支持 | 不支持 | +| Windows amd64 | 支持 | 支持 | 支持 | 安装程序自带固定版本的 Node.js 和 Harness(列于 [`scripts/build-native-installer.mjs`](https://github.com/MiniMax-AI/OpenAgentCore/blob/main/scripts/build-native-installer.mjs)),不修改这些工具的其他安装。在没有匹配安装程序的平台上,命令会失败。 @@ -35,7 +35,7 @@ Session 自带模型提供商;安装默认模型不适用([原因](../../../ 机器需要: - 通过 HTTP 或 HTTPS 访问 Core,以及访问发布下载主机;如果 Core 已有本平台安装程序的离线副本,则无需后者(离线安装只带有 Linux amd64 的安装程序); -- 用于环境设置和 MiniMax Code 工具的 Bash;Windows 上需要 Git Bash,Claude Code 也要求它; +- 用于环境设置的 Bash,Unix 上的 MiniMax Code 工具也使用它;Windows 上需要 Git Bash,Claude Code 也要求它。Windows 上的 MiniMax Code 与本地运行一样,在 PowerShell 中执行其 `bash` 工具; - Session 的软件包需要时,安装 Python 和 pip; - 环境设置所需的系统软件包。守护进程不运行 apt、sudo 或其他提权命令,请通过主机的常规管理方式安装。 diff --git a/docs/zh/maintainers.md b/docs/zh/maintainers.md index 013a1cc94..9bb9604ce 100644 --- a/docs/zh/maintainers.md +++ b/docs/zh/maintainers.md @@ -1,7 +1,7 @@ --- title: "构建并发布 OpenAgentCore" source: docs/maintainers.md -source_hash: f66f608093cc824add3215a9df520082b1679af1852ee213b7a6adfca40221ed +source_hash: 049fe3c4d666a7b2f075597a6ee238f6be6e7e1e9f4cc259d48c08e12637b7d0 --- 本指南面向负责构建和发布 OpenAgentCore 的维护者。要安装 Core 和 Web,请按 [安装 Core 和 Web](getting-started/install.md) 操作。安装程序代码遵循的规则见 [部署](https://github.com/MiniMax-AI/OpenAgentCore/blob/main/deploy/README.md) 和 [节点安装程序](https://github.com/MiniMax-AI/OpenAgentCore/blob/main/deploy/node/README.md);必需检查见 [CONTRIBUTING](https://github.com/MiniMax-AI/OpenAgentCore/blob/main/CONTRIBUTING.md#required-checks)。 @@ -56,7 +56,7 @@ node scripts/build-native-catalog.mjs INPUT_DIR OUTPUT_DIR export OAC_NATIVE_INSTALLER_BUILD_DIR=OUTPUT_DIR ``` -每个平台有一个小型基础归档 `-.tar.gz`(`oac-daemon` 和 `bundle.json`,后者列出每个组件及其每个文件的 SHA-256),以及每个组件一个归档,即 `--node` 和 `--`(MiniMax Code 不为 Windows 构建)。`build-native-installer.mjs` 生成它们。Core 按名称提供这些制品,不了解其内容;`oac-daemon install` 下载所需的组件。 +每个平台有一个小型基础归档 `-.tar.gz`(`oac-daemon` 和 `bundle.json`,后者列出每个组件及其每个文件的 SHA-256),以及每个组件一个归档,即 `--node` 和 `--`。`build-native-installer.mjs` 生成它们。Core 按名称提供这些制品,不了解其内容;`oac-daemon install` 下载所需的组件。 该目录会记录提交、Runtime 协议版本、每个制品的 SHA-256,并在提供发布基址时记录其带版本号的 URL。控制归档和 Core 镜像仅携带 `native-installers/catalog.json`;这些归档会成为独立的 `oac-native--.tar.gz` 发布资源,离线归档则会在 Core 镜像之外保留所有 Linux amd64 归档的一份副本,由其 `install.py` 复制到安装目录中供 Core 提供下载。其他平台由 Core 重定向到对应的发布资源;没有发布基址的目录没有这类 URL,因此其离线归档会保留所有归档。如果没有目录,Sessions 会将安装命令报告为不可用,并且发布工作流会拒绝发布。 @@ -86,7 +86,7 @@ docker build --platform linux/amd64 -t oac-runtime:claude "${OAC_DEV_HOME:-$HOME 第一步会将适配器和固定版本的 Claude Agent SDK(`packages/claude-sdk-adapter/package.json`)导出为适用于主机平台且带校验和的归档;第二步会验证该归档并添加守护进程。镜像步骤需要 `linux-x64-glibc` 归档,因此必须在带 glibc 的 Linux x86_64 上构建两者。请保持导出的归档不变。 -**MiniMax Code Runtime 镜像。** 使用 `packages/mcode-harness/source.json` 中固定修订版本的检出,并使用相同版本的 `@minimax-ai/code` npm 包提供原生依赖,以构建配套程序。配套程序构建可在 Linux x86_64 或 macOS arm64 上运行,并输出到一个新目录;对于 Linux Runtime 镜像,请在 Linux x86_64 上构建它(macOS arm64 仅用于原生安装程序): +**MiniMax Code Runtime 镜像。** 使用 `packages/mcode-harness/source.json` 中固定修订版本的检出,并使用相同版本的 `@minimax-ai/code` npm 包提供原生依赖,以构建配套程序。配套程序构建可在 Linux x86_64、macOS arm64 或 Windows x86_64(Git Bash 中)上运行,并输出到一个新目录;对于 Linux Runtime 镜像,请在 Linux x86_64 上构建它(macOS 和 Windows 上的构建仅用于原生安装程序): ```sh MCODE_NATIVE_SOURCE=/absolute/minimax-code \ diff --git a/packages/mcode-harness/README.md b/packages/mcode-harness/README.md index 8a66d12fd..fd68eafb7 100644 --- a/packages/mcode-harness/README.md +++ b/packages/mcode-harness/README.md @@ -14,7 +14,7 @@ No second model or scheduling loop is introduced. Hosted public execution is not ## Build -`source.json` pins the CLI source. The pinned npm package supplies native runtime dependencies; the CLI is built from the patched source with its upstream build script and lockfile. On Linux x86_64 or macOS arm64: +`source.json` pins the CLI source. The pinned npm package supplies native runtime dependencies; the CLI is built from the patched source with its upstream build script and lockfile. On Linux x86_64, macOS arm64 or Windows x86_64 (in Git Bash): ```sh MCODE_NATIVE_SOURCE=/absolute/upstream/checkout MCODE_CLI_DIR=/absolute/pinned/package bash scripts/build-mcode-harness.sh diff --git a/packages/mcode-harness/check.mjs b/packages/mcode-harness/check.mjs index 1794221c0..eade1579a 100644 --- a/packages/mcode-harness/check.mjs +++ b/packages/mcode-harness/check.mjs @@ -1,11 +1,13 @@ -import { accessSync, readFileSync } from 'node:fs'; +import { accessSync, constants, readFileSync } from 'node:fs'; import { fileURLToPath } from 'node:url'; -import { execFileSync } from 'node:child_process'; import { createRequire } from 'node:module'; +import { delimiter, join } from 'node:path'; -if (!['linux', 'darwin'].includes(process.platform)) throw new Error('MiniMax native execution requires Linux or macOS'); +if (!['linux', 'darwin', 'win32'].includes(process.platform)) throw new Error('MiniMax native execution requires Linux, macOS or Windows'); for (const file of ['subagent-snapshot.mjs', 'native/cli.js', 'native-patch.json']) accessSync(new URL(file, import.meta.url)); -for (const command of ['bash', 'rg']) execFileSync('which', [command], { stdio: 'ignore' }); +// Native file search runs rg. Native shell commands run Bash on Unix; on Windows +// MiniMax Code selects PowerShell itself, as in a local run. +for (const command of process.platform === 'win32' ? ['rg'] : ['bash', 'rg']) requireCommand(command); const pin = JSON.parse(readFileSync(new URL('source.json', import.meta.url))); const patch = JSON.parse(readFileSync(new URL('native-patch.json', import.meta.url))); if (patch.revision !== pin.revision || !/^[a-f0-9]{64}$/.test(patch.files?.['patch-native.mjs'] ?? '') || @@ -18,3 +20,14 @@ try { database.close(); } catch { throw new Error('Native database dependency check failed'); } process.stdout.write(JSON.stringify({ protocol: 3, native: pin.version, source: pin.revision }) + '\n'); + +// requireCommand finds an executable on PATH as the native process will. +function requireCommand(name) { + const names = process.platform === 'win32' ? (process.env.PATHEXT ?? '.EXE').split(';').filter(Boolean).map(ext => name + ext) : [name]; + for (const directory of (process.env.PATH ?? '').split(delimiter).filter(Boolean)) { + for (const candidate of names) { + try { accessSync(join(directory, candidate), process.platform === 'win32' ? constants.F_OK : constants.X_OK); return; } catch { /* Try the next PATH entry. */ } + } + } + throw new Error(`Required command ${name} is missing`); +} diff --git a/scripts/build-mcode-harness.sh b/scripts/build-mcode-harness.sh index 1df969b58..bb8542494 100644 --- a/scripts/build-mcode-harness.sh +++ b/scripts/build-mcode-harness.sh @@ -1,24 +1,27 @@ #!/usr/bin/env bash set -euo pipefail -repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" -package="$repo_root/packages/mcode-harness" -source="${MCODE_NATIVE_SOURCE:?Set MCODE_NATIVE_SOURCE to the pinned upstream checkout}" -native="${MCODE_CLI_DIR:?Set MCODE_CLI_DIR to the pinned CLI package supplying native dependencies}" case "$(uname -s):$(uname -m)" in - Linux:x86_64|Darwin:arm64) ;; - *) printf 'Build the MiniMax Code Runtime artifact on Linux x86_64 or macOS arm64\n' >&2; exit 1 ;; + Linux:x86_64|Darwin:arm64) native_path() { printf '%s\n' "$1"; }; absolute='/*' ;; + # Git Bash converts POSIX paths only in arguments, not in environment + # variables, so Node and Git receive drive-letter paths that MSYS tools also accept. + MINGW64_NT-*:x86_64) native_path() { cygpath -m "$1"; }; absolute='[A-Za-z]:/*' ;; + *) printf 'Build the MiniMax Code Runtime artifact on Linux x86_64, macOS arm64 or Windows x86_64\n' >&2; exit 1 ;; esac -root="${OAC_DEV_HOME:-$HOME/.oac}/build" -destination="${MCODE_HARNESS_BUILD_DIR:-$root/mcode-harness-$(date +%Y%m%d%H%M%S)}" +repo_root="$(native_path "$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)")" +package="$repo_root/packages/mcode-harness" +source="$(native_path "${MCODE_NATIVE_SOURCE:?Set MCODE_NATIVE_SOURCE to the pinned upstream checkout}")" +native="$(native_path "${MCODE_CLI_DIR:?Set MCODE_CLI_DIR to the pinned CLI package supplying native dependencies}")" +root="$(native_path "${OAC_DEV_HOME:-$HOME/.oac}/build")" +destination="$(native_path "${MCODE_HARNESS_BUILD_DIR:-$root/mcode-harness-$(date +%Y%m%d%H%M%S)}")" for directory in "$root" "$destination"; do - [[ "$directory" == /* ]] || { printf 'Absolute build directories are required\n' >&2; exit 1; } + [[ "$directory" == $absolute ]] || { printf 'Absolute build directories are required\n' >&2; exit 1; } done [[ ! -e "$destination" ]] || { printf 'MiniMax artifact destination already exists\n' >&2; exit 1; } mkdir -p "$root" "$(dirname "$destination")" context="$(mktemp -d "$root/mcode-build.XXXXXX")" trap 'rm -rf "$context"' EXIT -revision="$(python3 -c 'import json,sys; print(json.load(open(sys.argv[1]))["revision"])' "$package/source.json")" +revision="$(node -p 'require(process.argv[1]).revision' "$package/source.json")" if [[ "$(git -C "$source" rev-parse HEAD)" != "$revision" ]]; then printf 'MiniMax Code source revision does not match source.json\n' >&2 exit 1 @@ -48,13 +51,20 @@ cp -R "$native/node_modules" "$artifact/native/" cp "$context/upstream/.oac-native-patch.json" "$artifact/native-patch.json" cp "$context/upstream/LICENSE" "$artifact/UPSTREAM_LICENSE" cp "$context/upstream/third_party/pi-mono/LICENSE" "$artifact/PI_LICENSE" -python3 - "$artifact" "$package" <<'PY' -import hashlib,json,pathlib,sys -artifact,package=map(pathlib.Path,sys.argv[1:]) -pin=json.loads((package/'source.json').read_text()) -pin['files']={str(p.relative_to(artifact)):hashlib.sha256(p.read_bytes()).hexdigest() - for p in sorted(artifact.rglob('*')) if p.is_file()} -(artifact/'provenance.json').write_text(json.dumps(pin,indent=2)+'\n') -PY +node - "$artifact" "$package" <<'JS' +const { createHash } = require('node:crypto'); +const { readdirSync, readFileSync, statSync, writeFileSync } = require('node:fs'); +const { join } = require('node:path'); +const [artifact, pkg] = process.argv.slice(2); +const pin = JSON.parse(readFileSync(join(pkg, 'source.json'), 'utf8')); +// Hash every regular file, including file symlinks, without descending into directory symlinks. +const isFile = path => { try { return statSync(path).isFile(); } catch { return false; } }; +const walk = relative => readdirSync(join(artifact, relative), { withFileTypes: true }).flatMap(entry => { + const path = relative ? `${relative}/${entry.name}` : entry.name; + return entry.isDirectory() ? walk(path) : isFile(join(artifact, path)) ? [path] : []; +}); +pin.files = Object.fromEntries(walk('').sort().map(path => [path, createHash('sha256').update(readFileSync(join(artifact, path))).digest('hex')])); +writeFileSync(join(artifact, 'provenance.json'), JSON.stringify(pin, null, 2) + '\n'); +JS mv "$artifact" "$destination" printf 'MiniMax Code Runtime artifact: %s\n' "$destination" diff --git a/scripts/build-native-installer-ci.mjs b/scripts/build-native-installer-ci.mjs index 4daeff856..c73a7300e 100644 --- a/scripts/build-native-installer-ci.mjs +++ b/scripts/build-native-installer-ci.mjs @@ -30,7 +30,7 @@ async function findCodex(directory, depth = 0) { const candidates = await findCodex(join(root, 'native-tools', 'node_modules', '@openai')); assert.equal(candidates.length, 1, 'Expected exactly one native Codex artifact'); const bundle = join(root, 'native-installer'); -const receipt = await buildBundle({ daemon, node: await realpath(node), codex: candidates[0], claude: join(root, 'claude-runtime'), ...(!windows ? { minimax: join(root, 'minimax-runtime') } : {}), output: bundle }); +const receipt = await buildBundle({ daemon, node: await realpath(node), codex: candidates[0], claude: join(root, 'claude-runtime'), minimax: join(root, 'minimax-runtime'), output: bundle }); await archiveBundle(bundle, join(root, 'native-archives')); console.log(JSON.stringify({ stage: 'bundle', os: receipt.os, arch: receipt.arch, components: Object.keys(receipt.components), model_requests: 0 })); const workspace = join(root, 'native-install-workspace'); await mkdir(workspace); @@ -51,10 +51,9 @@ function install(extra, expected) { assert.ok(!`${result.stdout}${result.stderr}`.includes('fixture-only-not-a-real-credential'), 'Installer echoed a credential'); } install(['--harness', 'codex'], true); -const allHarnesses = windows ? 'codex,claude' : 'codex,claude,minimax'; +const allHarnesses = 'codex,claude,minimax'; install(['--harness', allHarnesses], true); install(['--harness', allHarnesses], true); -if (windows) install(['--harness', 'minimax'], false); // Components are installed once per machine and version, read-only, and the installation only references them. const store = join(env.OAC_RUNTIME_HOME, 'components', receipt.daemon_version); for (const name of ['node', ...allHarnesses.split(',')]) { diff --git a/scripts/build-native-installer.mjs b/scripts/build-native-installer.mjs index 55c7f4e04..95e1a82b8 100644 --- a/scripts/build-native-installer.mjs +++ b/scripts/build-native-installer.mjs @@ -127,7 +127,6 @@ export async function validateComponents(root, names, daemonVersion) { const info = JSON.parse(probe(node, [join(component, 'dist/runtime_check.js')], component, env)); if (info.type !== 'runtime_ready' || info.protocol !== 3 || info.sdk !== pins.claude || info.native !== '2.1.269 (Claude Code)' || !info.features?.includes('local_runtime_v2')) throw new Error('Claude runtime compatibility failed'); } else if (name === 'minimax') { - if (windows) throw new Error('MiniMax is not supported on Windows'); const upstream = await json(join(component, 'source.json')); const expected = await json(new URL('../packages/mcode-harness/source.json', import.meta.url)); if (upstream.version !== pins.minimax || upstream.revision !== expected.revision) throw new Error('MiniMax source pin mismatch'); @@ -145,7 +144,6 @@ export async function buildBundle(options) { for (const path of Object.values(options)) if (!isAbsolute(path)) throw new Error('Bundle paths must be absolute'); const names = ['codex', 'claude', 'minimax'].filter(name => options[name]); if (!names.length) throw new Error('At least one Harness source is required'); - if (process.platform === 'win32' && options.minimax) throw new Error('MiniMax is not supported on Windows'); try { await lstat(options.output); throw new Error('Bundle output already exists'); } catch (error) { if (error.code !== 'ENOENT') throw error; } const output = await outputRealPath(options.output); // Refuse staging inside a source: recursive copying must not ingest its own output. @@ -167,9 +165,10 @@ export async function buildBundle(options) { const rg = await realpath(nativeRequire('@vscode/ripgrep').rgPath); if (!inside(component, rg)) throw new Error('MiniMax ripgrep escaped the component'); await requireFile(rg); + const rgName = process.platform === 'win32' ? 'rg.exe' : 'rg'; await mkdir(join(component, 'bin'), { recursive: true }); - await copyFile(rg, join(component, 'bin', 'rg')); await chmod(join(component, 'bin', 'rg'), 0o755); - components.minimax.files['bin/rg'] = { sha256: createHash('sha256').update(await readFile(rg)).digest('hex'), executable: true }; + await copyFile(rg, join(component, 'bin', rgName)); await chmod(join(component, 'bin', rgName), 0o755); + components.minimax.files[`bin/${rgName}`] = { sha256: createHash('sha256').update(await readFile(rg)).digest('hex'), executable: true }; } await validateComponents(staging, names, daemonVersion); const manifest = { schema: 1, daemon_version: daemonVersion, os: platforms[process.platform], arch: architectures[process.arch], components }; diff --git a/scripts/native-onboarding-smoke.mjs b/scripts/native-onboarding-smoke.mjs index 05bf83c73..1d3efa99a 100644 --- a/scripts/native-onboarding-smoke.mjs +++ b/scripts/native-onboarding-smoke.mjs @@ -99,7 +99,7 @@ try { const script = resolve(`services/core/internal/nativeinstaller/assets/bootstrap.${windows ? 'ps1' : 'sh'}`); const bootstrapArgs = windows ? ['-NoProfile', '-ExecutionPolicy', 'Bypass', '-File', script, '-Base', base] : [script, base]; // Select every Harness of this platform so each Harness archive passes the installer's extraction and readiness checks. - const harnesses = windows ? ['codex', 'claude'] : ['codex', 'claude', 'minimax']; + const harnesses = ['codex', 'claude', 'minimax']; const result = await run(windows ? 'powershell.exe' : 'bash', [...bootstrapArgs, '--install-dir', installation], harnesses.join(',') + '\n\n'); assert.equal(result.code, 0, `Interactive bootstrap failed: ${result.output}`); assert.match(result.output, /Daemon connection: connected/); diff --git a/services/core/deploy/mcode/README.md b/services/core/deploy/mcode/README.md index c8c7f2cba..099c3d61d 100644 --- a/services/core/deploy/mcode/README.md +++ b/services/core/deploy/mcode/README.md @@ -6,7 +6,7 @@ Native tools run with the daemon user's permissions; the outer sandbox provides ## Native pin and readiness -The adapter accepts only `@minimax-ai/code` `0.4.12` ([`version.go`](../../../../apps/daemon/internal/agent/mcode/version.go)), built from the upstream source revision in [`source.json`](../../../../packages/mcode-harness/source.json) and run with Node.js 22. MiniMax Code runs on Linux and macOS. +The adapter accepts only `@minimax-ai/code` `0.4.12` ([`version.go`](../../../../apps/daemon/internal/agent/mcode/version.go)), built from the upstream source revision in [`source.json`](../../../../packages/mcode-harness/source.json) and run with Node.js 22. MiniMax Code runs on Linux, macOS and Windows. Workspace execution also requires the companion's readiness report: private protocol 3, the pinned native version and the pinned source revision ([`workspace_readiness.go`](../../../../apps/daemon/internal/agent/mcode/workspace_readiness.go)). An older companion is rejected even when the upstream version matches. @@ -23,9 +23,9 @@ Each Session has its own native data directory, `$OAC_RUNTIME_HOME/runtime/mcode | Text (`environment: none`) | A private working directory inside the Session's data directory | No native file, process or fetch tools, web search, `mcode-tools`, thread goals or user questions. The native task controls (`task_query`, `task_output`, `task_stop`) are always present; with Subagents enabled, the default agent also gets delegation. Permission mode `bypassPermissions`, so MCP tools run without approval prompts | | Workspace | The Environment's workspace directory, for the native process, the ACP Session and the native tools | The native `read`, `write`, `edit`, `bash`, `grep`, `glob`, `todowrite` and `web_fetch` tools, permission mode `bypassPermissions`, the selected workspace Skills and the frozen tool environment in the native process environment | -The text profile rejects workspace tools; it accepts service-origin HTTP MCP servers, which the adapter passes in the ACP `mcpServers` of the native Session. In both profiles public functions run as native MCP tools: the daemon serves them from a loopback HTTP MCP server named `functions` that exists for the Session, and passes it in `mcpServers` with a per-Session bearer token. Native fails a call after 120 s ([difference](../../../../contracts/agents-api/index.md#differences-from-openai)). Functions and `multi_agent` cannot be combined. Workspace execution requires Linux or macOS ([`workspace.go`](../../../../apps/daemon/internal/agent/mcode/workspace.go)). +The text profile rejects workspace tools; it accepts service-origin HTTP MCP servers, which the adapter passes in the ACP `mcpServers` of the native Session. In both profiles public functions run as native MCP tools: the daemon serves them from a loopback HTTP MCP server named `functions` that exists for the Session, and passes it in `mcpServers` with a per-Session bearer token. Native fails a call after 120 s ([difference](../../../../contracts/agents-api/index.md#differences-from-openai)). Functions and `multi_agent` cannot be combined. -In the workspace profile, the frozen installation's Skills are linked into the Session's `skills` directory under their exact names and loaded through the native `skill` tool. Native discovery also reads the workspace's `.minimax`, `.claude` and `.agents` Skills, as a local run does. Public MCP servers use the Environment origin on a workspace and the service origin on the text profile, as the [MCP origin contract](../../../../contracts/agents-api/environments.md#public-mcp-connection-origin) describes. With Subagents enabled, child agents use the same native tools. In both profiles the adapter turns off what needs a MiniMax account or outlives a Turn or asks the user: `askUser`, `beta.mcodeTools`, `beta.threadGoal`, `features.mavis` (its tool carries cron, session send, agent create and MCP create) and `features.webSearch`, and it leaves `website_deploy` out of `tools`. `web_search: disabled` also removes `web_fetch`. Background Bash is off: the patched CLI gates it on `OAC_RUNTIME_MCODE_BACKGROUND_BASH=off`, and the adapter rejects a CLI that does not report it off. +In the workspace profile, the frozen installation's Skills are linked into the Session's `skills` directory under their exact names (directory junctions on Windows) and loaded through the native `skill` tool. Native discovery also reads the workspace's `.minimax`, `.claude` and `.agents` Skills, as a local run does. Public MCP servers use the Environment origin on a workspace and the service origin on the text profile, as the [MCP origin contract](../../../../contracts/agents-api/environments.md#public-mcp-connection-origin) describes. With Subagents enabled, child agents use the same native tools. In both profiles the adapter turns off what needs a MiniMax account or outlives a Turn or asks the user: `askUser`, `beta.mcodeTools`, `beta.threadGoal`, `features.mavis` (its tool carries cron, session send, agent create and MCP create) and `features.webSearch`, and it leaves `website_deploy` out of `tools`. `web_search: disabled` also removes `web_fetch`. Background Bash is off: the patched CLI gates it on `OAC_RUNTIME_MCODE_BACKGROUND_BASH=off`, and the adapter rejects a CLI that does not report it off. ## Turns, cancellation and continuation