From fdf833adc1ae901af87fbdee130e04b5c72fc53d Mon Sep 17 00:00:00 2001 From: Chunhao Zhang <1226577187zch@gmail.com> Date: Thu, 22 Oct 2026 18:56:17 +0800 Subject: [PATCH] Run MiniMax Code on Windows self-hosted machines Start the native CLI's JavaScript entrypoint with the pinned Node.js, link Skill directories as junctions on Windows, admit the Windows workspace profile, package ripgrep as rg.exe, and build the companion in Git Bash so native-check installs all three Harnesses on Windows. --- .gitattributes | 2 + .github/workflows/cache-warm.yml | 2 + .github/workflows/native.yml | 5 +- .../agent/mcode/discovery_workspace.go | 6 +- apps/daemon/internal/agent/mcode/executor.go | 10 +-- .../internal/agent/mcode/installation.go | 2 +- .../daemon/internal/agent/mcode/link_other.go | 10 +++ .../internal/agent/mcode/link_windows.go | 70 +++++++++++++++++++ .../agent/mcode/native_command_test.go | 60 ++++++++++++++++ apps/daemon/internal/agent/mcode/session.go | 4 +- apps/daemon/internal/agent/mcode/subagents.go | 2 +- apps/daemon/internal/agent/mcode/version.go | 33 ++++++++- apps/daemon/internal/agent/mcode/workspace.go | 8 +-- .../agent/mcode/workspace_readiness_test.go | 2 +- .../agent/versionprobe/versionprobe.go | 11 +-- .../internal/cli/native_install_test.go | 5 -- apps/web/src/i18n/locales/en/sessions.ts | 2 +- apps/web/src/i18n/locales/zh-CN/sessions.ts | 2 +- docs/getting-started/self-hosted.md | 4 +- docs/maintainers.md | 4 +- docs/zh/getting-started/self-hosted.md | 6 +- docs/zh/maintainers.md | 6 +- packages/mcode-harness/README.md | 2 +- packages/mcode-harness/check.mjs | 21 ++++-- scripts/build-mcode-harness.sh | 46 +++++++----- scripts/build-native-installer-ci.mjs | 5 +- scripts/build-native-installer.mjs | 7 +- scripts/native-onboarding-smoke.mjs | 2 +- services/core/deploy/mcode/README.md | 6 +- 29 files changed, 265 insertions(+), 80 deletions(-) create mode 100644 .gitattributes create mode 100644 apps/daemon/internal/agent/mcode/link_other.go create mode 100644 apps/daemon/internal/agent/mcode/link_windows.go create mode 100644 apps/daemon/internal/agent/mcode/native_command_test.go 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