feat(sandbox): enhance assistant directory handling and update RunPrepareSteps signature

- Updated the sandbox initialization to resolve both the assistant directory and skills subdirectory, improving directory management.
- Modified the RunPrepareSteps function signature to include an assistantDir parameter, allowing for better handling of file copy operations.
- Adjusted all relevant calls to RunPrepareSteps across the codebase to accommodate the new parameter, ensuring consistency in execution.
- Enhanced test cases to reflect the updated function signature, improving coverage and reliability of the sandbox preparation process.
This commit is contained in:
Max 2026-03-18 22:58:38 +08:00
parent 42b45f9357
commit 020927e9af
6 changed files with 68 additions and 55 deletions

View file

@ -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)

View file

@ -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)
}
}

View file

@ -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:///<assistantDir>/<src>"
// - 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")

View file

@ -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)
}

View file

@ -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.

View file

@ -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
}