Compare commits

...

1 Commits

Author SHA1 Message Date
Eve
f33a1c78b8 fix(daemon): copy CLI into agent workdir
Co-authored-by: multica-agent <github@multica.ai>
2026-05-15 20:04:02 +08:00
3 changed files with 175 additions and 5 deletions

View File

@@ -0,0 +1,109 @@
package daemon
import (
"fmt"
"io"
"log/slog"
"os"
"path/filepath"
"runtime"
)
func agentCLIBinaryName() string {
if runtime.GOOS == "windows" {
return "multica.exe"
}
return "multica"
}
func prependPathValue(dir, current string) string {
if current == "" {
return dir
}
return dir + string(os.PathListSeparator) + current
}
func prependAgentPath(env map[string]string, dir string) {
if env == nil || dir == "" {
return
}
env["PATH"] = prependPathValue(dir, env["PATH"])
}
func copyFileExecutable(src, dst string) error {
in, err := os.Open(src)
if err != nil {
return err
}
defer in.Close()
info, err := in.Stat()
if err != nil {
return err
}
if !info.Mode().IsRegular() {
return fmt.Errorf("%s is not a regular file", src)
}
if err := os.MkdirAll(filepath.Dir(dst), 0o755); err != nil {
return err
}
tmp, err := os.CreateTemp(filepath.Dir(dst), "."+filepath.Base(dst)+".tmp-*")
if err != nil {
return err
}
tmpPath := tmp.Name()
cleanup := true
defer func() {
if cleanup {
_ = os.Remove(tmpPath)
}
}()
if _, err := io.Copy(tmp, in); err != nil {
_ = tmp.Close()
return err
}
if err := tmp.Close(); err != nil {
return err
}
mode := info.Mode().Perm() | 0o755
if err := os.Chmod(tmpPath, mode); err != nil {
return err
}
// Windows cannot rename over an existing executable. The per-task copy is
// only used after the previous agent process exits, so replacing it here is
// safe and keeps reused workdirs aligned with a freshly updated daemon.
_ = os.Remove(dst)
if err := os.Rename(tmpPath, dst); err != nil {
return err
}
cleanup = false
return nil
}
func prepareAgentCLIPath(workDir string, logger *slog.Logger) string {
selfBin, err := os.Executable()
if err != nil {
if logger != nil {
logger.Warn("agent env: resolve multica executable failed", "error", err)
}
return ""
}
if workDir == "" {
return filepath.Dir(selfBin)
}
destDir := filepath.Join(workDir, ".multica", "bin")
dest := filepath.Join(destDir, agentCLIBinaryName())
if err := copyFileExecutable(selfBin, dest); err == nil {
return destDir
} else if logger != nil {
logger.Warn("agent env: copy multica CLI into task workdir failed", "source", selfBin, "dest", dest, "error", err)
}
return filepath.Dir(selfBin)
}

View File

@@ -0,0 +1,60 @@
package daemon
import (
"os"
"path/filepath"
"testing"
)
func TestPrepareAgentCLIPathCopiesExecutableIntoWorkdir(t *testing.T) {
t.Parallel()
workDir := t.TempDir()
binDir := prepareAgentCLIPath(workDir, nil)
if binDir == "" {
t.Fatal("prepareAgentCLIPath returned empty bin dir")
}
wantDir := filepath.Join(workDir, ".multica", "bin")
if binDir != wantDir {
t.Fatalf("bin dir = %q, want %q", binDir, wantDir)
}
dest := filepath.Join(binDir, agentCLIBinaryName())
destInfo, err := os.Stat(dest)
if err != nil {
t.Fatalf("expected copied CLI at %s: %v", dest, err)
}
if !destInfo.Mode().IsRegular() {
t.Fatalf("copied CLI is not a regular file: %s", dest)
}
self, err := os.Executable()
if err != nil {
t.Fatalf("os.Executable: %v", err)
}
selfInfo, err := os.Stat(self)
if err != nil {
t.Fatalf("stat self executable: %v", err)
}
if destInfo.Size() != selfInfo.Size() {
t.Fatalf("copied CLI size = %d, want %d", destInfo.Size(), selfInfo.Size())
}
}
func TestPrependAgentPath(t *testing.T) {
t.Parallel()
sep := string(os.PathListSeparator)
env := map[string]string{"PATH": "base"}
prependAgentPath(env, "task-bin")
if got, want := env["PATH"], "task-bin"+sep+"base"; got != want {
t.Fatalf("PATH = %q, want %q", got, want)
}
env = map[string]string{}
prependAgentPath(env, "task-bin")
if got := env["PATH"]; got != "task-bin" {
t.Fatalf("PATH = %q, want task-bin", got)
}
}

View File

@@ -2265,11 +2265,12 @@ func (d *Daemon) runTask(ctx context.Context, task Task, provider string, slot i
}
// Ensure the multica CLI is on PATH inside the agent's environment.
// Some runtimes (e.g. Codex) run in an isolated sandbox that may not
// inherit the daemon's PATH. Prepend the directory of the running
// multica binary so that `multica` commands in the agent always resolve.
if selfBin, err := os.Executable(); err == nil {
binDir := filepath.Dir(selfBin)
agentEnv["PATH"] = binDir + string(os.PathListSeparator) + os.Getenv("PATH")
// inherit the daemon's PATH or may be unable to access Desktop's unpacked
// app resources directory on Windows. Copy the running CLI into the task
// workdir first so `multica` resolves to a path the agent can execute.
agentEnv["PATH"] = os.Getenv("PATH")
if binDir := prepareAgentCLIPath(env.WorkDir, d.logger); binDir != "" {
prependAgentPath(agentEnv, binDir)
}
// Point Codex to the per-task CODEX_HOME so it discovers skills natively
// without polluting the system ~/.codex/skills/.