diff --git a/agent/assistant/sandbox_v2.go b/agent/assistant/sandbox_v2.go index 027e07b4..1cab3a79 100644 --- a/agent/assistant/sandbox_v2.go +++ b/agent/assistant/sandbox_v2.go @@ -94,10 +94,12 @@ func (ast *Assistant) initSandboxV2(ctx *context.Context, opts *context.Options) return nil, nil, nil, "", fmt.Errorf("get runner %q: %w", cfg.Runner.Name, err) } - // 5. Resolve skills directory. + // 5. Resolve assistant directory and skills subdirectory. + assistantDir := "" skillsDir := "" if ast.Path != "" { - dir := filepath.Join(config.Conf.AppSource, ast.Path, "skills") + assistantDir = filepath.Join(config.Conf.AppSource, ast.Path) + dir := filepath.Join(assistantDir, "skills") if info, e := os.Stat(dir); e == nil && info.IsDir() { skillsDir = dir } @@ -117,13 +119,14 @@ func (ast *Assistant) initSandboxV2(ctx *context.Context, opts *context.Options) // 7. Runner.Prepare (standard context). err = runner.Prepare(stdCtx, &sandboxTypes.PrepareRequest{ - Computer: computer, - Config: cfg, - Connector: conn, - SkillsDir: skillsDir, - MCPServers: mcpServers, - ConfigHash: ast.ConfigHash, - RunSteps: sandboxv2.RunPrepareSteps, + Computer: computer, + Config: cfg, + Connector: conn, + SkillsDir: skillsDir, + AssistantDir: assistantDir, + MCPServers: mcpServers, + ConfigHash: ast.ConfigHash, + RunSteps: sandboxv2.RunPrepareSteps, }) if err != nil { runner.Cleanup(stdCtx, computer) diff --git a/agent/sandbox/v2/claude/runner.go b/agent/sandbox/v2/claude/runner.go index 8dd00949..4c50f524 100644 --- a/agent/sandbox/v2/claude/runner.go +++ b/agent/sandbox/v2/claude/runner.go @@ -69,7 +69,7 @@ func (r *ClaudeRunner) Prepare(ctx context.Context, req *types.PrepareRequest) e } if req.RunSteps != nil && len(steps) > 0 { - if err := req.RunSteps(ctx, steps, req.Computer, req.Config.ID, req.ConfigHash); err != nil { + if err := req.RunSteps(ctx, steps, req.Computer, req.Config.ID, req.ConfigHash, req.AssistantDir); err != nil { return fmt.Errorf("claude prepare steps: %w", err) } } diff --git a/agent/sandbox/v2/prepare.go b/agent/sandbox/v2/prepare.go index ae8951b1..57006c79 100644 --- a/agent/sandbox/v2/prepare.go +++ b/agent/sandbox/v2/prepare.go @@ -5,6 +5,7 @@ import ( "fmt" "log" "path" + pathpkg "path/filepath" "strings" "github.com/yaoapp/yao/agent/sandbox/v2/types" @@ -17,7 +18,9 @@ const onceMarkerDir = ".yao/prepare" // RunPrepareSteps executes a list of PrepareStep actions on the given Computer. // file/copy/marker operations use computer.Workplace() (gRPC volume, cross-platform). // exec operations use shell via Computer.Exec. -func RunPrepareSteps(ctx context.Context, steps []types.PrepareStep, computer infra.Computer, assistantID, configHash string) error { +// assistantDir is the absolute host path to the assistant source directory; +// copy steps with a relative src resolve against it (host → workspace push). +func RunPrepareSteps(ctx context.Context, steps []types.PrepareStep, computer infra.Computer, assistantID, configHash, assistantDir string) error { if len(steps) == 0 { return nil } @@ -52,7 +55,7 @@ func RunPrepareSteps(ctx context.Context, steps []types.PrepareStep, computer in case "file": err = runFileStep(ws, step) case "copy": - err = runCopyStep(ws, step) + err = runCopyStep(ws, step, assistantDir) case "exec": err = runExecStep(ctx, computer, step) case "process": @@ -103,7 +106,14 @@ func runFileStep(ws workspace.FS, step types.PrepareStep) error { return nil } -func runCopyStep(ws workspace.FS, step types.PrepareStep) error { +// runCopyStep copies files into the workspace using ws.Copy which supports +// the "local:///" URI scheme for host-to-workspace transfers. +// +// src resolution: +// - Already a host URI ("local:///..." or "tmp:///...") → used as-is +// - Relative path + assistantDir provided → resolved to "local:////" +// - Relative path without assistantDir → treated as workspace-internal path +func runCopyStep(ws workspace.FS, step types.PrepareStep, assistantDir string) error { if step.Src == "" || step.Dst == "" { return fmt.Errorf("copy step requires src and dst") } @@ -111,24 +121,21 @@ func runCopyStep(ws workspace.FS, step types.PrepareStep) error { return fmt.Errorf("copy step requires workspace") } - data, err := ws.ReadFile(step.Src) - if err != nil { - return fmt.Errorf("read src %s: %w", step.Src, err) + src := step.Src + if !isHostURI(src) && assistantDir != "" { + src = "local:///" + pathpkg.Join(assistantDir, src) } - dir := path.Dir(step.Dst) - if dir != "." && dir != "/" { - if err := ws.MkdirAll(dir, 0755); err != nil { - return fmt.Errorf("mkdir %s: %w", dir, err) - } - } - - if err := ws.WriteFile(step.Dst, data, 0644); err != nil { - return fmt.Errorf("write dst %s: %w", step.Dst, err) + if _, err := ws.Copy(src, step.Dst); err != nil { + return fmt.Errorf("copy %s -> %s: %w", src, step.Dst, err) } return nil } +func isHostURI(s string) bool { + return strings.HasPrefix(s, "local:///") || strings.HasPrefix(s, "tmp:///") +} + func runExecStep(ctx context.Context, computer infra.Computer, step types.PrepareStep) error { if step.Cmd == "" { return fmt.Errorf("exec step requires cmd") diff --git a/agent/sandbox/v2/prepare_test.go b/agent/sandbox/v2/prepare_test.go index b607a83e..980bb1d6 100644 --- a/agent/sandbox/v2/prepare_test.go +++ b/agent/sandbox/v2/prepare_test.go @@ -32,7 +32,7 @@ func TestRunPrepareSteps_Exec(t *testing.T) { {Action: "exec", Cmd: "echo world >> /tmp/prep-test"}, } - err := sandboxv2.RunPrepareSteps(ctx, steps, box, "test-assistant", "") + err := sandboxv2.RunPrepareSteps(ctx, steps, box, "test-assistant", "", "") if err != nil { t.Fatalf("RunPrepareSteps: %v", err) } @@ -67,7 +67,7 @@ func TestRunPrepareSteps_File(t *testing.T) { {Action: "file", Path: "config/test.txt", Content: []byte("file-content-v2")}, } - err := sandboxv2.RunPrepareSteps(ctx, steps, box, "test-assistant", "") + err := sandboxv2.RunPrepareSteps(ctx, steps, box, "test-assistant", "", "") if err != nil { t.Fatalf("RunPrepareSteps: %v", err) } @@ -105,7 +105,7 @@ func TestRunPrepareSteps_Copy(t *testing.T) { {Action: "copy", Src: "src.txt", Dst: "dst.txt"}, } - err := sandboxv2.RunPrepareSteps(ctx, steps, box, "test-assistant", "") + err := sandboxv2.RunPrepareSteps(ctx, steps, box, "test-assistant", "", "") if err != nil { t.Fatalf("RunPrepareSteps: %v", err) } @@ -143,7 +143,7 @@ func TestRunPrepareSteps_OnceMarker(t *testing.T) { hash := "abc123" assistantID := "test-once" - if err := sandboxv2.RunPrepareSteps(ctx, steps, box, assistantID, hash); err != nil { + if err := sandboxv2.RunPrepareSteps(ctx, steps, box, assistantID, hash, ""); err != nil { t.Fatalf("first run: %v", err) } r1, _ := box.Exec(ctx, []string{"cat", counter}) @@ -151,7 +151,7 @@ func TestRunPrepareSteps_OnceMarker(t *testing.T) { t.Fatalf("first run: got %q, want %q", r1.Stdout, "x") } - if err := sandboxv2.RunPrepareSteps(ctx, steps, box, assistantID, hash); err != nil { + if err := sandboxv2.RunPrepareSteps(ctx, steps, box, assistantID, hash, ""); err != nil { t.Fatalf("second run: %v", err) } r2, _ := box.Exec(ctx, []string{"cat", counter}) @@ -159,7 +159,7 @@ func TestRunPrepareSteps_OnceMarker(t *testing.T) { t.Errorf("second run: got %q, want %q (once step should be skipped)", r2.Stdout, "x") } - if err := sandboxv2.RunPrepareSteps(ctx, steps, box, assistantID, "new-hash"); err != nil { + if err := sandboxv2.RunPrepareSteps(ctx, steps, box, assistantID, "new-hash", ""); err != nil { t.Fatalf("third run: %v", err) } r3, _ := box.Exec(ctx, []string{"cat", counter}) @@ -193,10 +193,10 @@ func TestRunPrepareSteps_OnceIsolation(t *testing.T) { hash := "same-hash" - if err := sandboxv2.RunPrepareSteps(ctx, stepsA, box, "assistant-a", hash); err != nil { + if err := sandboxv2.RunPrepareSteps(ctx, stepsA, box, "assistant-a", hash, ""); err != nil { t.Fatalf("assistant-a: %v", err) } - if err := sandboxv2.RunPrepareSteps(ctx, stepsB, box, "assistant-b", hash); err != nil { + if err := sandboxv2.RunPrepareSteps(ctx, stepsB, box, "assistant-b", hash, ""); err != nil { t.Fatalf("assistant-b: %v", err) } @@ -209,10 +209,10 @@ func TestRunPrepareSteps_OnceIsolation(t *testing.T) { t.Errorf("assistant-b: got %q, want %q", rB.Stdout, "B") } - if err := sandboxv2.RunPrepareSteps(ctx, stepsA, box, "assistant-a", hash); err != nil { + if err := sandboxv2.RunPrepareSteps(ctx, stepsA, box, "assistant-a", hash, ""); err != nil { t.Fatalf("assistant-a re-run: %v", err) } - if err := sandboxv2.RunPrepareSteps(ctx, stepsB, box, "assistant-b", hash); err != nil { + if err := sandboxv2.RunPrepareSteps(ctx, stepsB, box, "assistant-b", hash, ""); err != nil { t.Fatalf("assistant-b re-run: %v", err) } rA2, _ := box.Exec(ctx, []string{"cat", "/tmp/iso-a"}) @@ -244,7 +244,7 @@ func TestRunPrepareSteps_IgnoreError(t *testing.T) { {Action: "exec", Cmd: "echo survived > /tmp/survived"}, } - err := sandboxv2.RunPrepareSteps(ctx, steps, box, "test-assistant", "") + err := sandboxv2.RunPrepareSteps(ctx, steps, box, "test-assistant", "", "") if err != nil { t.Fatalf("RunPrepareSteps: %v (ignore_error should have prevented failure)", err) } @@ -274,7 +274,7 @@ func TestRunPrepareSteps_FailOnError(t *testing.T) { {Action: "exec", Cmd: "echo should-not-reach > /tmp/unreachable"}, } - err := sandboxv2.RunPrepareSteps(ctx, steps, box, "test-assistant", "") + err := sandboxv2.RunPrepareSteps(ctx, steps, box, "test-assistant", "", "") if err == nil { t.Fatal("expected error from failing step without ignore_error") } @@ -303,7 +303,7 @@ func TestRunPrepareSteps_UnknownAction(t *testing.T) { {Action: "unknown_action"}, } - err := sandboxv2.RunPrepareSteps(ctx, steps, nil, "test-assistant", "") + err := sandboxv2.RunPrepareSteps(ctx, steps, nil, "test-assistant", "", "") if err == nil { t.Fatal("expected error for unknown action") } @@ -315,7 +315,7 @@ func TestRunPrepareSteps_UnknownAction(t *testing.T) { } func TestRunPrepareSteps_EmptySteps(t *testing.T) { - err := sandboxv2.RunPrepareSteps(context.Background(), nil, nil, "test-assistant", "hash") + err := sandboxv2.RunPrepareSteps(context.Background(), nil, nil, "test-assistant", "hash", "") if err != nil { t.Fatalf("empty steps should succeed: %v", err) } @@ -338,7 +338,7 @@ func TestRunPrepareSteps_Background(t *testing.T) { {Action: "exec", Cmd: "echo after-bg > /tmp/after-bg"}, } - err := sandboxv2.RunPrepareSteps(ctx, steps, box, "test-assistant", "") + err := sandboxv2.RunPrepareSteps(ctx, steps, box, "test-assistant", "", "") if err != nil { t.Fatalf("RunPrepareSteps: %v", err) } @@ -371,7 +371,7 @@ func TestRunPrepareSteps_MixedActions(t *testing.T) { {Action: "copy", Src: "mixed.conf", Dst: "mixed-copy.conf"}, } - err := sandboxv2.RunPrepareSteps(ctx, steps, box, "test-assistant", "") + err := sandboxv2.RunPrepareSteps(ctx, steps, box, "test-assistant", "", "") if err != nil { t.Fatalf("RunPrepareSteps: %v", err) } @@ -425,7 +425,7 @@ func TestRunPrepareSteps_HostExec(t *testing.T) { {Action: "exec", Cmd: cmd}, } - err := sandboxv2.RunPrepareSteps(ctx, steps, host, "test-host", "") + err := sandboxv2.RunPrepareSteps(ctx, steps, host, "test-host", "", "") if err != nil { t.Fatalf("RunPrepareSteps on host: %v", err) } @@ -451,7 +451,7 @@ func TestRunPrepareSteps_HostExecFile(t *testing.T) { {Action: "file", Path: "host-test.txt", Content: []byte("host-file-data")}, } - err := sandboxv2.RunPrepareSteps(ctx, steps, host, "test-host", "") + err := sandboxv2.RunPrepareSteps(ctx, steps, host, "test-host", "", "") if err != nil { t.Fatalf("RunPrepareSteps file: %v", err) } @@ -489,7 +489,7 @@ func TestRunPrepareSteps_HostExecCopy(t *testing.T) { {Action: "copy", Src: "copy-src.txt", Dst: "copy-dst.txt"}, } - err := sandboxv2.RunPrepareSteps(ctx, steps, host, "test-host", "") + err := sandboxv2.RunPrepareSteps(ctx, steps, host, "test-host", "", "") if err != nil { t.Fatalf("RunPrepareSteps copy: %v", err) } @@ -532,7 +532,7 @@ func TestRunPrepareSteps_HostExecOnce(t *testing.T) { hash := "host-once-hash" aid := "host-once-aid" - if err := sandboxv2.RunPrepareSteps(ctx, steps, host, aid, hash); err != nil { + if err := sandboxv2.RunPrepareSteps(ctx, steps, host, aid, hash, ""); err != nil { t.Fatalf("first run: %v", err) } diff --git a/agent/sandbox/v2/types/runner.go b/agent/sandbox/v2/types/runner.go index 8674e2e2..6268efbb 100644 --- a/agent/sandbox/v2/types/runner.go +++ b/agent/sandbox/v2/types/runner.go @@ -28,18 +28,21 @@ type MCPServer struct { } // RunStepsFunc is the signature of RunPrepareSteps. Workspace is obtained -// internally via computer.Workplace(). -type RunStepsFunc func(ctx context.Context, steps []PrepareStep, computer infra.Computer, assistantID, configHash string) error +// internally via computer.Workplace(). assistantDir is the absolute path to +// the assistant source directory on the host; copy steps resolve relative src +// paths against it. +type RunStepsFunc func(ctx context.Context, steps []PrepareStep, computer infra.Computer, assistantID, configHash, assistantDir string) error // PrepareRequest carries everything needed by Runner.Prepare. type PrepareRequest struct { - Computer infra.Computer - Config *SandboxConfig - Connector connector.Connector - SkillsDir string - MCPServers []MCPServer - ConfigHash string - RunSteps RunStepsFunc + Computer infra.Computer + Config *SandboxConfig + Connector connector.Connector + SkillsDir string + AssistantDir string // absolute host path to the assistant source directory + MCPServers []MCPServer + ConfigHash string + RunSteps RunStepsFunc } // StreamRequest carries everything needed by Runner.Stream. diff --git a/agent/sandbox/v2/yao/runner.go b/agent/sandbox/v2/yao/runner.go index 7bbd7109..7f1012e8 100644 --- a/agent/sandbox/v2/yao/runner.go +++ b/agent/sandbox/v2/yao/runner.go @@ -21,7 +21,7 @@ func (r *YaoRunner) Name() string { return "yao" } // no runner-specific steps. Connector is not required. func (r *YaoRunner) Prepare(ctx context.Context, req *types.PrepareRequest) error { if req.RunSteps != nil && len(req.Config.Prepare) > 0 { - return req.RunSteps(ctx, req.Config.Prepare, req.Computer, req.Config.ID, req.ConfigHash) + return req.RunSteps(ctx, req.Config.Prepare, req.Computer, req.Config.ID, req.ConfigHash, req.AssistantDir) } return nil }