Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# Bash rejects carriage returns, so shell scripts stay LF on Windows checkouts.
*.sh text eol=lf
2 changes: 2 additions & 0 deletions .github/workflows/cache-warm.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
5 changes: 2 additions & 3 deletions .github/workflows/native.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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: >-
Expand All @@ -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
Expand Down
6 changes: 1 addition & 5 deletions apps/daemon/internal/agent/mcode/discovery_workspace.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
10 changes: 5 additions & 5 deletions apps/daemon/internal/agent/mcode/executor.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -69,16 +69,16 @@ 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
}
return resource, err
}
}

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
Expand Down
2 changes: 1 addition & 1 deletion apps/daemon/internal/agent/mcode/installation.go
Original file line number Diff line number Diff line change
Expand Up @@ -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}
},
Expand Down
10 changes: 10 additions & 0 deletions apps/daemon/internal/agent/mcode/link_other.go
Original file line number Diff line number Diff line change
@@ -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)
}
70 changes: 70 additions & 0 deletions apps/daemon/internal/agent/mcode/link_windows.go
Original file line number Diff line number Diff line change
@@ -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 \??\<target> and the print name <target>, 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
}
60 changes: 60 additions & 0 deletions apps/daemon/internal/agent/mcode/native_command_test.go
Original file line number Diff line number Diff line change
@@ -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)
}
}
4 changes: 2 additions & 2 deletions apps/daemon/internal/agent/mcode/session.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down
2 changes: 1 addition & 1 deletion apps/daemon/internal/agent/mcode/subagents.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
33 changes: 32 additions & 1 deletion apps/daemon/internal/agent/mcode/version.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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)
}
Expand Down
8 changes: 2 additions & 6 deletions apps/daemon/internal/agent/mcode/workspace.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import (
"maps"
"os"
"path/filepath"
"runtime"
"slices"

"github.com/MiniMax-AI/OpenAgentCore/apps/daemon/internal/localworkspace"
Expand All @@ -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")
}
}
Expand Down Expand Up @@ -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
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
11 changes: 7 additions & 4 deletions apps/daemon/internal/agent/versionprobe/versionprobe.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 `<binary> --version` and returns its trimmed first output line.
// Check runs `<binary> [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
Expand All @@ -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 {
Expand Down
5 changes: 0 additions & 5 deletions apps/daemon/internal/cli/native_install_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
2 changes: 1 addition & 1 deletion apps/web/src/i18n/locales/en/sessions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
Loading